This commit is contained in:
rxf
2023-03-08 16:17:02 +01:00
commit 0b6959d4f6
35 changed files with 5804 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+3
View File
@@ -0,0 +1,3 @@
node_modules/
.idea
.env
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
# Editor-based HTTP Client requests
/httpRequests/
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/../SensorApi" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/untitled1.iml" filepath="$PROJECT_DIR$/.idea/untitled1.iml" />
</modules>
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="bin/www" type="NodeJSConfigurationType" path-to-js-file="bin/www.js" working-dir="$PROJECT_DIR$">
<envs>
<env name="DEBUG" value="sensorapi:*" />
</envs>
<EXTENSION ID="com.jetbrains.nodejs.run.NodeJSStartBrowserRunConfigurationExtension">
<browser url="http://localhost:3000/" />
</EXTENSION>
<method v="2" />
</configuration>
</component>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+23
View File
@@ -0,0 +1,23 @@
FROM node:alpine
ADD package.json /tmp/package.json
RUN cd /tmp && npm install
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /opt/app/
WORKDIR /opt/app
ADD . /opt/app
RUN apk add busybox-initscripts
RUN apk add --no-cache tzdata
ENV TZ Europe/Berlin
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN deluser --remove-home node
RUN touch cmds.sh \
&& echo 'npm start' >>cmds.sh
CMD sh ./cmds.sh
+138
View File
@@ -0,0 +1,138 @@
// Fetch the actual (= newest) data out of the dbase to show it on the map
import {DateTime} from "luxon"
import {logit} from "../utilities/logit.js"
import * as mongo from "../databases/mongo.js"
import {reportError, returnOnError} from "../utilities/reporterror.js"
import * as ERR from "../utilities/errortexts.js"
// Default distance for center search ( in km)
const DEFAULT_DISTANCE = 10
// Relations between types and value type
const vtype2measurement = {
P1: 'pm', P2: 'pm', P0: 'pm',
temperature: 'thp', humidity: 'thp', pressure: 'thp',
noise_LAeq: 'noise',
counts_per_minute: 'radioactivity'
};
// find first value type from measurement
const getfieldfromtype = (typ) => {
for (const [key, value] of Object.entries(vtype2measurement)) {
if (value === typ) {
return key
}
}
return ' '
}
export const getData4map = async (params) => {
let start = DateTime.now()
let retur = { err: null, data: { params: params} }
// check parameters
if ((params.type === undefined) || (params.type === '')) {
return returnOnError(retur, ERR.NOTYP, getData4map.name)
}
const typ = params.type
let poly = []
let south = null, north = null, east = null, west = null, center = null
let distance = DEFAULT_DISTANCE
if (!((params.box === undefined) || (params.box == ""))) {
const box = params.box
if (!((box === undefined) || (box === ' ') || (box === 'null'))) {
south = parseFloat(box.south)
north = parseFloat(box.north)
east = parseFloat(box.east)
west = parseFloat(box.west)
logit(`getData4map: S=${south} N=${north} E=${east} W=${west}`)
}
}
if (!((params.poly === undefined) || (params.poly === ' '))){
poly = JSON.parse(params.poly)
}
if (params.center !== undefined) {
center = params.center
if ((params.distance !== undefined) &&
(params.distance >= 1) && (params.distance <= 1000)) {
distance = params.distance
}
}
const aktData = []
let lastDate = 0
let query = {type: typ}
// if polyline or box were given, set query
if (poly.length !== 0) { // polyline given
query.location = {
$geoWithin: {
$geometry: {
type: "Polygon",
coordinates: [poly],
}
}
}
} else if (south !== null) { // box given
query["location.loc"] = {
$geoWithin: {
$box: [
[west, south],
[east, north]
]
}
}
} else if (center !== null) { // center point given
query["location.loc"] = {
$nearSphere: {
$geometry: {
type: "Point",
coordinates: center
},
$maxDistance: distance * 1000
}
}
}
// depending of the type find the measurement field to fetch
const vtype = getfieldfromtype(typ)
// fetch mapdata from mongodb
try {
let { values, err } = await mongo.readMapdata(query, 0)
if(err) {
return returnOnError(retur, ERR.NOSENSFOUND, getData4map.name)
}
for (let item of values) {
const oneAktData = {
location: item.location.loc.coordinates,
id: item._id,
name: item.name,
indoor: item.location.indoor,
value: Math.round(item.value[vtype] * 100) / 100
}
let now = new Date().getTime()
let diff = now - item.timestamp
if (diff >= 7 * 24 * 3600 * 1000) {
oneAktData.value = -2
} else if (diff >= 2 * 3600 * 1000) {
oneAktData.value = -1
}
if (item.timestamp > lastDate) {
lastDate = item.timestamp
}
aktData.push(oneAktData)
}
logit(`Query duration: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
retur.data.lastDate = lastDate
retur.data.valuetype = vtype
retur.data.count = aktData.length
retur.data.values = aktData
return retur
}
catch(e) {
return returnOnError(retur, `catch\n${e}`, getData4map.name)
}
}
+62
View File
@@ -0,0 +1,62 @@
// get data for one sensor
import {DateTime} from "luxon"
import { logit, logerror } from '../utilities/logit.js'
import * as mongo from "../databases/mongo.js"
import {reportError, returnOnError} from "../utilities/reporterror.js"
import {csv2Json} from "../utilities/csv2json.js"
// Fetch all akw data out of the dbase
// router.get('/getakwdata/', async function (req, res) {
export const getakwdata = async (options) => {
let data = {err: null, ...options, count: 0, values: []}
let erg = []
try {
let rawAKWs = await mongo.readAKWs(options)
if (rawAKWs.err) {
return returnOnError(date, rawAKWs.err, getakwdata.name)
}
for (let item of rawAKWs.values.akws) {
var oneAktData = {};
oneAktData['location'] = {
type: 'Point',
coordinates: [item.lon, item.lat]
};
oneAktData['name'] = item.Name;
oneAktData['active'] = item.Status == 'aktiv';
oneAktData['start'] = item.Baujahr;
oneAktData['end'] = item.Stillgeleg;
oneAktData['type'] = item.Status === 'aktiv' ? 'akw_a' : 'akw_s';
oneAktData['link'] = item.Wiki_Link;
erg.push(oneAktData); // dies ganzen Werte nun in das Array
}
for (let item of rawAKWs.values.th1_akws) {
let oneAktData = {};
let loc = item.geo.substr(6).split(' ');
let lon = parseFloat(loc[0]);
let lat = parseFloat(loc[1]);
oneAktData['location'] = {
type: 'Point',
coordinates: [lon, lat]
};
oneAktData['name'] = item.name;
oneAktData['typeText'] = item.types;
oneAktData['type'] = item.types == "Nuclear power plant" ? 'akw_a' : 'other';
oneAktData['link'] = item.item;
if (item.itemServiceretirement != undefined) {
oneAktData['ende'] = item.itemServiceretirement.substr(0, 4);
}
if (item.itemServiceentry != undefined) {
oneAktData['begin'] = item.itemServiceentry.substr(0, 4);
}
erg.push(oneAktData);
}
data.values = erg
data.count = erg.length
} catch (e) {
return returnOnError(data, e, getakwdata.name)
}
return data
}
+53
View File
@@ -0,0 +1,53 @@
import {returnOnError} from "../utilities/reporterror.js"
import axios from 'axios'
import {logit} from "../utilities/logit.js"
import {getOneProperty} from "./getproperties.js"
const NOMINATIM_URL = `https://nominatim.openstreetmap.org/reverse?lat=${'xx'}&lon=${'yy'}&format=json`
export const getAddress = async (params) => {
let ret = {address: "", err: null}
let {props, err} = await getOneProperty(params)
if (err) {
return returnOnError(ret, err, getAddress.name)
}
let coord = props.location[0].loc.coordinates
let url = NOMINATIM_URL.replace('xx', coord[1]).replace('yy', coord[0])
try {
const response = await axios(encodeURI(url));
if (response.status !== 200) {
return returnOnError(ret, ERR.RESPSTATUS.replace('ss', response.status, getAddress.name))
}
let akt = response.data.address
logit(JSON.stringify(akt))
const CITY = ['city', 'town', 'village', 'suburb', 'county']
let city = "unknown"
for (let c of CITY) {
if(akt[c] !== undefined) {
city = akt[c]
break
}
}
ret.address = `${(akt.road ? akt.road : "")} ${(akt.postcode ? akt.postcode : "")} ${city}`;
} catch (e) {
return returnOnError(ret, e, getAddress.name)
}
return ret
}
/*
let addr = "Addr";
try {
let ret = await $.get("api/getprops?sensorid=" + marker.options.name);
if(ret.values[0].address.city == null) {
addr += " unbekannt !"
} else {
let akt = ret.values[0].address;
addr = (akt.street ? akt.street : "") + "&nbsp;&nbsp;" + (akt.plz ? akt.plz : "") + " " + akt.city;
}
} catch (e) {
console.log("onMarker - getpops", e)
}
console.log("addr:", addr);
return addr;
*/
+55
View File
@@ -0,0 +1,55 @@
// Fetch the properties for the given sensor
import * as mongo from "../databases/mongo.js"
import * as mock from "../mocks/mongo_mock.js"
import {returnOnError} from "../utilities/reporterror.js"
import * as ERR from "../utilities/errortexts.js"
import checkParams from "../utilities/checkparams.js"
const mockdb = false
let readProperties
if (mockdb) {
readProperties = mock.readProperties
} else {
readProperties = mongo.readProperties
}
export const getOneProperty = async (params) => {
let properties = {props: {}, err: null}
let {opts, err} = checkParams(params, {mandatory:[{name:'sensorid', type: 'int'}], optional:[]})
if (err) {
return returnOnError(properties, err, getOneProperty.name)
}
let sensorEntries = [];
try {
let pp = await readProperties({sid: opts.sensorid});
if ((pp.values == null) || (pp.error)) {
return returnOnError(properties, ERR.NOPROPSREAD.replace('xx', opts.sensorid), getOneProperty.name)
}
// now find sensors with same location
let query
if (pp.values.location_id !== undefined) {
query = {location_id: pp.values.location_id}
} else {
query = {id: pp.values.id}
}
let others = await readProperties(query)
if (others.error) {
return returnOnError(properties, ERR.NOPROPSREAD.replace('xx',others.errortext), getOneProperty.name)
}
if (others.values.length > 0) {
for (const x of others.values) {
if(x.name[0].name === undefined) {
sensorEntries.push({name: x.name, sid: x._id})
} else {
sensorEntries.push({name: x.name[0].name, sid: x._id})
}
}
}
properties.props = pp.values
properties.props.othersensors = sensorEntries;
} catch (e) {
return returnOnError(properties, e, getOneProperty.name)
}
return properties
}
+247
View File
@@ -0,0 +1,247 @@
// get data for one sensor
import {DateTime} from "luxon"
import * as influx from "../databases/influx.js"
import {returnOnError} from "../utilities/reporterror.js"
import {csv2Json} from "../utilities/csv2json.js"
import checkParams from "../utilities/checkparams.js";
import * as ERR from "../utilities/errortexts.js"
import {getOneProperty} from "./getproperties.js";
import {getNoiseData} from "../sensorspecials/noise.js";
import {getRadioData} from "../sensorspecials/radioact.js";
// Possible params for the different sensor types
const noiseParams = [
{name:'data', type: 'string', default: 'live'},
{name: 'span', type: 'int', default: ''},
{name: 'daystart', type: 'bool', default: null},
{name: 'peak', type: 'int', default: 70},
{name: 'since', type: 'date', default: '1900-01-01T00:00:00Z'},
{name: 'box', type: 'array', default: null},
{name: 'out', type: 'string', default: ''},
{name: 'csv', type: 'bool', default: false},
{name: 'long', type: 'bool', default: false},
{name: 'sort', type: 'int', default: 1},
{name: 'last_seen', type: 'date', default: '1900-01-01T00:00:00Z'},
{name: 'datetime', type: 'date', default: null}
]
const thpParams = []
const pmParams = []
const radioParams = []
// >>>>>>>>>>>>>>>>>>>>> DUMMIES
async function getPmData(opts) {
}
async function getThpData(opts) {
}
// <<<<<<<<<<<<<<<<<<<<< DUMMIES
// Table for the different sensor types
const sensorTypeTable = [
{ typ: 'thp', possibleParams: thpParams, func: getThpData},
{ typ: 'noise', possibleParams: noiseParams, func: getNoiseData},
{ typ: 'pm', possibleParams: pmParams, func: getPmData},
{ typ: 'radioactivity', possiblePramas: radioParams, func: getRadioData}
]
/* Units:
* span -> days
* avg -> minutes
*/
// *********************************************
// calcRange
//
// Calculate the date/time range ro read the data from
//
// If 'datetime' is not given, use 'now()' as end and 'now()' - span as start
// If 'datetime' is given, use it as start and 'datetime'+span as end
// If 'daystart'==true, change start and end so, that they begin at 00:00:00h
// if 'avg' has a value, move start backward ba 'avg' minutes
//
// params:
// opts: Object with different options, specially
// datetime, avg, daystart, span
//
// return:
// Object with start and stop in ISO-Format
// *********************************************
export const calcRange = (opts) => {
let start, end
let ret = { start: start, stop: end, err: null}
if((opts.datetime === null) || (opts.datetime === undefined) || (opts.datetime === '')) {
start = end = DateTime.local().toUTC()
start = start.minus({days: opts.span})
} else {
start = end = DateTime.fromISO(opts.datetime, {zone: 'utc'})
end = end.plus({days: opts.span})
}
if(opts.daystart) {
start = start.startOf("day")
end = end.startOf("day")
}
if(opts.avg !== undefined) {
start = start.minus({minutes: opts.avg})
}
ret.start = `start: ${start.toISO()}`
ret.stop = `stop: ${end.toISO()}`
return ret
}
// *********************************************
// getSensorData
//
// Depending on the parameter 'sensorid', ditribute to the speciell routines for
// the sensors
//
// params:
// params: all parameters from the url
//
// return:
// Retirns from the special routines
// *********************************************
export const getSensorData = async (params) => {
let ret = {err: null}
let {opts, err} = checkParams(params, { // check for sensorid
mandatory: [{name: 'sensorid', type: 'int'}],
optional: []
})
if (err) {
return returnOnError(ret, err, getSensordata.name)
}
// with the sensorid get the type of this sensor
let {props, err1} = await getOneProperty({sensorid: opts.sensorid})
if (err1) {
return returnOnError(ret, err1, getSensorData.name)
}
// distribute to the right routine
for(let item of sensorTypeTable) {
if(item.typ === props.type) {
ret = item.func(params, item.possibleParams, props) // get the values from database
return ret
}
}
return returnOnError(ret, ERR.CMNDUNKOWN, getActData.name)
}
export const getActData = async (opts) => {
let ret = {data: {count: 0, values: []}, err: null}
let sorting = ''
if(opts.sort) {
if (opts.sort === 1) {
sorting = '|> sort(columns: ["_time"], desc: false)'
} else if (opts.sort === -1) {
sorting = '|> sort(columns: ["_time"], desc: true)'
}
}
// build the flux query
let query = `
from(bucket: "sensor_data")
|> range(${opts.start}, ${opts.stop})
|> filter(fn: (r) => r.sid == "${opts.sensorid}")
${sorting}
|> keep(columns: ["_time","_field","_value"])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
`
return await fetchFromInflux(ret, query)
}
// ..../api/getavgdata?sensorid=123&span=2&avg=10&moving=true&datetime=2022-04-12T13:14:15Z
export const getAvgData = async (params) => {
let ret = {data: {count: 0, values: []}, err: null}
let {opts, err} = checkParams(params,
{
mandatory:
[
{name: 'sensorid', type: 'int'},
],
optional:
[
{name: 'span', type: 'int', default: 1},
{name: 'datetime', type: 'date', default: null},
{name: 'avg', type: 'int', default: 10},
{name: 'moving', type: 'bool', default: true},
]
}
)
if (err) {
return returnOnError(ret, err, getActdata.name)
}
let {start, stop} = calcRange(opts)
if (stop === '') {
ret.data.start = DateTime.now().toUTC().minus({days: `${opts.span}`}).toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'")
} else {
ret.data.start = DateTime.fromISO(opts.datetime, {zone: 'utc'}).toISO()
}
ret.data.span = opts.span
ret.data.moving = opts.moving
ret.data.avg = opts.avg
let every = opts.moving ? '150s' : `${opts.avg}m`
let period = `${opts.avg}m`
let query = `
from(bucket: "sensor_data")
|> range(${start}, ${stop})
|> filter(fn: (r) => r.sid == "${opts.sensorid}")
|> timedMovingAverage(every: ${every}, period: ${period})
|> keep(columns: ["_time","_field","_value"])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
`
return await fetchFromInflux(ret, query)
}
// ..../api/getlongavg?sensorid=123&span=2
export const getLongAvg = async (params) => {
let ret = {data: {count: 0, values: []}, err: null}
let {opts, err} = checkParams(params,
{
mandatory:
[
{name: 'sensorid', type: 'int'},
],
optional:
[
{name: 'span', type: 'int', default: 2},
]
}
)
if (err) {
return returnOnError(ret, err, getActdata.name)
}
ret.data.span = opts.span
let query = `
from(bucket: "sensor_data")
|> range(start: -${opts.span}d)
|> filter(fn: (r) => r.sid == "${opts.sensorid}")
|> mean()
|> drop(columns: ["_start","_measurement", "sid"])
|> pivot(rowKey:["_stop"], columnKey: ["_field"], valueColumn: "_value")
`
return fetchFromInflux(ret, query)
}
export const fetchFromInflux = async (data, query) => {
let ret = {err: null}
let { values, err} = await influx.influxRead(query)
if(err) {
if(err.toString().includes('400')) {
return returnOnError(ret, ERR.SYNTAXURL, fetchFromInflux.name)
} else {
return returnOnError(ret, err, fetchFromInflux.name)
}
}
if (values.length === 0) {
return returnOnError(data, ERR.NODATA, fetchFromInflux.name)
}
ret.values = csv2Json(values)
return ret
}
+38
View File
@@ -0,0 +1,38 @@
import createError from 'http-errors'
import logger from 'morgan'
import express from 'express'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import indexRouter from './routes/index.js'
import apiRouter from './routes/index.js'
const app = express()
app.use(cors())
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
app.use('/api', apiRouter)
app.use('/', indexRouter)
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404))
})
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message
res.locals.error = req.app.get('env') === 'development' ? err : {}
// render the error page
res.status(err.status || 500)
res.send(`ERROR: ${err.status}, ${err.stack}`)
})
export default app
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from '../app.js'
import debug from 'debug'
import http from 'http'
// const debug = 'untitled1:server'
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
const port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# Build Docker-Container
#
# Call: buildit.sh name [target]
#
# The Dockerfile must be named like Dockerfile_name
#
# 2018-09-20 rxf
# - before sending docker image to remote, tag actual remote image
#
# 2018-09-14 rxf
# - first Version
#
set -x
port=""
orgName=sensorapi
name=sensorapi
usage()
{
echo "Usage build_and_copy.sh [-p port] [-n name] target"
echo " Build docker container $name and copy to target"
echo "Params:"
echo " target: Where to copy the container to "
echo " -p port: ssh port (default 22)"
echo " -n name: new name for container (default: $orgName)"
}
while getopts n:p:h? o
do
case "$o" in
n) name="$OPTARG";;
p) port="-p $OPTARG";;
h) usage; exit 0;;
*) usage; exit 1;;
esac
done
shift $((OPTIND-1))
while [ $# -gt 0 ]; do
if [[ -z "$target" ]]; then
target=$1
shift
else
echo "bad option $1"
# exit 1
shift
fi
done
docker build -f Dockerfile_$orgName -t $name .
dat=`date +%Y%m%d%H%M`
if [ "$target" == "localhost" ]
then
docker tag $name $name:V_$dat
exit
fi
ssh $port $target "docker tag $name $name:V_$dat"
docker save $name | bzip2 | pv | ssh $port $target 'bunzip2 | docker load'
+71
View File
@@ -0,0 +1,71 @@
// Access to influxDB vie HTTP
import axios from 'axios'
import { DateTime } from 'luxon'
// import csvParse from 'csv-parser'
import { logit, logerror } from '../utilities/logit.js'
import {returnOnError} from "../utilities/reporterror.js";
let INFLUXHOST = process.env.INFLUXHOST || "localhost"
let INFLUXPORT = process.env.INFLUXPORT || 8086
let INFLUXTOKEN = process.env.INFLUXTOKEN ||
//"rklEClT22KfdXZhA47eyJhbqcvekb8bcKCqlUG7n72uDSmR2xGvif0CmGJe0WQtXB96y29mmt-9BdsgWA5npfg=="
"BNR6cGdb006O1T6hQkGcfB8tgH-UPO6QkOPToeAvrP7LATJbCuWi1wYf3HBpVdZQEBxHxNSrNenZsOSMogX-lg=="
let INFLUXDATABUCKET = process.env.INFLUXDATABUCKET || "sensor_data"
let INFLUXORG = process.env.INFLUXORG || "citysensor"
const INFLUXURL_READ = `http://${INFLUXHOST}:${INFLUXPORT}/api/v2/query?org=${INFLUXORG}`
const INFLUXURL_WRITE = `http://${INFLUXHOST}:${INFLUXPORT}/api/v2/write?org=${INFLUXORG}&bucket=${INFLUXDATABUCKET}`
export const influxRead = async (query) => {
let start = DateTime.now()
let erg = { values: [], err: null}
try {
let ret = await axios({
method: 'post',
url: INFLUXURL_READ,
data: query,
headers: {
Authorization: `Token ${INFLUXTOKEN}`,
Accept: 'application/csv',
'Content-type': 'application/vnd.flux'
},
timeout: 10000,
})
if (ret.status !== 200) {
let error = ERR.RESPSTATUS.replace('xx',ret.status)
return returnOnError(erg, error, influxRead.name)
}
erg.values = ret.data
} catch (e) {
return returnOnError(erg, e, influxRead.name)
}
logit(`Influx read time: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
return erg
}
export const influxWrite = async (data) => {
let start = DateTime.now()
let ret
try {
ret = await axios({
method: 'post',
url: INFLUXURL_WRITE,
data: data,
headers: {
Authorization: `Token ${INFLUXTOKEN}`,
Accept: 'application/json',
'Content-Type': 'text/plain; charset=utf-8'
},
timeout: 10000,
})
if (ret.status !== 204) {
logerror(`doWrite2API Status: ${ret.status}`)
}
} catch (e) {
logerror(`doWrite2API ${e}`)
}
logit(`Influx-Write-Time: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
return ret
}
+252
View File
@@ -0,0 +1,252 @@
/* Interface for MongoDB
*/
import { MongoClient } from 'mongodb'
import { logit, logerror } from '../utilities/logit.js'
import { DateTime } from 'luxon'
import {returnOnError} from "../utilities/reporterror.js";
// const nodemailer = require('nodemailer');
let MONGOHOST = process.env.MONGOHOST;
let MONGOPORT = process.env.MONGOPORT;
let MONGOAUTH = process.env.MONGOAUTH;
let MONGOUSRP = process.env.MONGOUSRP;
let MONGOBASE = process.env.MONGOBASE;
if (MONGOHOST === undefined) { MONGOHOST = 'localhost';}
if (MONGOPORT === undefined) { MONGOPORT = 27097; }
if (MONGOAUTH === undefined) { MONGOAUTH = 'false'; }
if (MONGOBASE === undefined) { MONGOBASE = 'allsensors'; }
let MONGO_URL = 'mongodb://'+MONGOHOST+':'+MONGOPORT; // URL to mongo database
if (MONGOAUTH === 'true') {
MONGO_URL = 'mongodb://'+MONGOUSRP+'@' + MONGOHOST + ':' + MONGOPORT + '/?authSource=' + MONGOBASE; // URL to mongo database
}
let statistics = {}
let client
export const connectMongo = async () => {
try {
logit(`Try to connect to ${MONGO_URL}`)
// let client = await MongoClient.connect(MONGO_URL, { useNewUrlParser: true , useUnifiedTopology: true })
let client = await MongoClient.connect(MONGO_URL)
logit(`Mongodbase connected to ${MONGO_URL}`)
return client
}
catch(error){
throw(error)
}
}
export const closeMongo = async (client) => {
client.close()
}
const listDatabases = async (client) => {
let databasesList = await client.db().admin().listDatabases();
console.log("Databases:");
databasesList.databases.forEach(db => console.log(` - ${db.name}`));
}
/* ***************************************************
// READ routines
******************************************************/
// Read properties from the database
export const readProperties = async (query, limit = 0) => {
let ret = {err: null, values: null}
client = await connectMongo()
try {
if ("sid" in query) { // if sid is given, read property for sid
ret.values = await client.db(MONGOBASE).collection("properties").findOne({_id: query.sid})
} else { // otherwise read props corresponding to query
ret.values = await client.db(MONGOBASE).collection("properties").find(query).limit(limit).toArray()
}
} catch (e) {
ret.err = e
}
finally {
await closeMongo(client)
client = null
}
return ret
}
// read mapdata from database
export const readMapdata = async (query, limit) => {
let ret = {err: null, values: []}
client = await connectMongo()
try {
ret.values = await client.db(MONGOBASE).collection("mapdata").find(query).limit(limit).toArray()
} catch (e) {
ret.err = e
}
finally {
await closeMongo(client)
client = null
}
return ret
}
export const getallProperties = async (client) => {
return await client.db(MONGOBASE).collection("properties")
.find().toArray()
}
export const getOneproperty = async (sid) => {
let ret = {error: false}
client = await connectMongo()
try {
ret.properties = await client.db(MONGOBASE).collection("properties")
.findOne({_id: sid})
} catch (e) {
ret = {error: true, errortext: e}
}
finally {
await closeMongo(client)
client = null
}
return ret
}
export const doAggregate = async (coll, query, conn, clos) => {
let ret = {error: false}
if (conn) {
client = await connectMongo()
}
try {
ret.values = await client.db(MONGOBASE).collection(coll).aggregate(query).toArray()
} catch (e) {
ret = {error: true, errortext: e}
}
finally {
if (clos) {
await closeMongo(client)
client = null
}
}
return ret
}
export const writeOneproperty = async (client, prop) => {
try {
let result = await client.db(MONGOBASE).collection("properties")
.insertOne(prop)
} catch(e) {
if (e.code === 11000) {
return false
} else {
throw(e)
}
}
return true
}
export const writeProperties = async (client, props) => {
let start = DateTime.now();
if (props.new.length !== 0) {
try {
let result = await client.db(MONGOBASE).collection("properties")
.insertMany(props.new)
} catch (e) {
console.error(e)
}
}
if (props.loc.length !== 0) {
try {
for (let item of props.loc) {
let result = await client.db(MONGOBASE).collection("properties")
.updateOne({_id: item._id}, {
$set: {location_id: item.location_id},
$push: {location: { $each: [item.location[0]], $position: 0}}
})
}
} catch (e) {
console.error(e)
}
}
if (props.sname.lenth !== 0) {
try {
for (let item of props.sname) {
let result = await client.db(MONGOBASE).collection("properties")
.updateOne({_id: item._id}, {
$set: {name: item.name, since: item.since}
})
}
} catch (e) {
console.error(e)
}
}
statistics.writePropsTime = DateTime.now().diff(start, ['seconds']).toObject().seconds
logit(`Write properties to mongoDB: ${statistics.writePropsTime} sec.`)
}
export const writeDataArray = async (data, collection, conn, clos) => {
let start = DateTime.now();
let result
if (conn) {
client = await connectMongo()
}
try {
result = await client.db(MONGOBASE).collection(collection)
.insertMany(data)
} catch (e) {
console.error(e)
} finally {
if(clos) {
await closeMongo(client)
client = null
}
}
statistics.writeDataTime = DateTime.now().diff(start, ['seconds']).toObject().seconds
logit(`Write Data to mongoDB: ${statistics.writeDataTime} sec.`)
return result
}
export const writeStatistic = async (client, stat) => {
let start = DateTime.now();
let entry = { timestamp: new Date(), ...stat }
try {
let result = await client.db(MONGOBASE).collection("statistics")
.insertOne(entry)
} catch (e) {
console.error(e)
}
statistics.writeStatisticTime = DateTime.now().diff(start, ['seconds']).toObject().seconds
logit(`Write statistics to mongoDB: ${statistics.writeStatisticTime} sec.`)
}
export const readAKWs = async (options) => {
let ret = {values: { akws: [], th1_akws: []}, err: null}
let erg = []
client = await connectMongo()
try {
let docs = await client.db(MONGOBASE).collection("akws")
.find().toArray()
if(docs == null) {
return returnOnError(ret, 'akws - docs == null', readAKWs.name)
}
logit(`getawkdata: data fetched from akws, length= ${docs.length}`);
ret.values.akws = docs
let docs1 = await client.db(MONGOBASE).collection("th1_akws")
.find().toArray()
if(docs1 == null) {
return returnOnError(ret, 'th1_akws - docs == null', readAKWs.name)
}
logit(`getawkdata: data fetched from th1_akws, length= ${docs1.length}`)
ret.values.th1_akws = docs1
} catch (e) {
return returnOnError(ret, e, readAKWs.name)
}
finally {
await closeMongo(client)
client = null
}
return ret
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

+23
View File
@@ -0,0 +1,23 @@
##Planung, Ideen und Entscheidungen
###Websocket
Da Websockets nur dann Vorteile bringen, wenn der Server von sich aus was 'zu sagen' hat, wird in diesem Projekt auf
Websockets verzichtet und der Verkher zwischen der API und dem GUI über normale HTTP-Calls ausgeführt.
###Planung
![Planung](planung.jpg "Text to show on mouseover").
####Ergänzungen zur Grafik (2022-05-26)
* Für die 3 GUIs je ein extra Container, so wie jetzt auch (Feinstaub, Geiger, Laerm)
* ab sofort (d.h. möglichst bald) die **influx-** und die **mongo-**Databases parallel laufen lassen
* nach ca. 1 Monat **nur noch mongo** verwenden
*
####Versions-History
|Version | Datum | Author
|--------|------|-------
|1.0 | 2022-05-27 | rxf
+18
View File
@@ -0,0 +1,18 @@
version: '3.9'
services:
node:
image: sensorapi
environment:
DEVELOP: "true"
MONGOHOST: ${MONGOHOST}
MONGOPORT: ${MONGOPORT}
INFLUXHOST: ${INFLUXHOST}
INFLUXTOKEN: ${INFLUXTOKEN}
LIVE: "true"
ports:
- "3031:3000"
volumes:
- /var/log/sensorapi:/var/log
container_name: node_sensorapi
restart: unless-stopped
+74
View File
@@ -0,0 +1,74 @@
// Gibt für eine Property-Anfrage immer den 140er Sensor zurück
// rxf 2022-05-25
export const readProperties = async (query, limit = 0) => {
let ret = {error: false}
if ("sid" in query) { // if sid is given, read property for sid
ret.values = {
"_id" : 140,
"location_id" : 65,
"name" : "SDS011",
"since" : "2021-04-21T09:44:12.888Z",
"location" : [
{
"loc" :
{
"type" : "Point",
"coordinates" : [ 9.16, 48.778 ]
},
"id" : 65,
"altitude" : 282,
"since" : "2021-04-21T09:44:12.888Z",
"address" : { },
"exact_loc" : 0,
"indoor" : 0
}
]
}
} else { // otherwise read props corresponding to query
ret.values = [
{
"_id" : 140,
"location_id" : 65,
"name" : "SDS011",
"since" : "2021-04-21T09:44:12.888Z",
"location" : [
{
"loc" :
{
"type" : "Point",
"coordinates" : [ 9.16, 48.778 ]
},
"id" : 65,
"altitude" : 282,
"since" : "2021-04-21T09:44:12.888Z",
"address" : { },
"exact_loc" : 0,
"indoor" : 0
}
]
},{
"_id" : 141,
"location_id" : 65,
"name" : "BME280",
"since" : "2021-04-21T09:44:12.888Z",
"location" : [
{
"loc" :
{
"type" : "Point",
"coordinates" : [ 9.16, 48.778 ]
},
"id" : 65,
"altitude" : 282,
"since" : "2021-04-21T09:44:12.888Z",
"address" : { },
"exact_loc" : 0,
"indoor" : 0
}
]
}
]
}
return ret
}
+3698
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "sensorapi",
"version": "1.0.3",
"date": "0222-06-06",
"private": true,
"scripts": {
"start": "node ./bin/www.js",
"test": "mocha ./test/test.js"
},
"type": "module",
"bin": {
"www": "./bin/www.js"
},
"dependencies": {
"@influxdata/influxdb-client": "^1.24.0",
"@influxdata/influxdb-client-apis": "^1.24.0",
"axios": "^0.26.1",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"luxon": "^2.3.1",
"mongodb": "^4.4.1",
"morgan": "~1.9.1",
"pug": "^3.0.2",
"ws": "^8.5.0"
},
"devDependencies": {
"mocha": "^9.2.2"
}
}
+43
View File
@@ -0,0 +1,43 @@
import express from 'express'
import {getData4map} from "../actions/data4map.js"
import cmdTable from "../utilities/commands.js"
import * as ERR from "../utilities/errortexts.js"
const router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.status(200).json({message: ERR.NOTHIMG})
});
// normal routes called from javascript client
router.get('/:cmd', async (req, res) => {
const cmd = req.params.cmd
for (let c of cmdTable) {
if (c.cmd === cmd) {
let erg = await c.func(req.query)
if (typeof erg === 'string') {
res.type('text')
res.send(erg)
} else {
res.json(erg)
}
return
}
}
res.json({err: ERR.CMNDUNKOWN})
})
router.post('/:cmd', async (req, res) => {
const cmd = req.params.cmd
if (cmd === 'getdata4maps') {
const erg = await getData4map(req.body)
res.json(erg)
} else {
res.json({err: ERR.CMNDUNKOWN})
}
})
export default router
+519
View File
@@ -0,0 +1,519 @@
// Data preparation for fetching noise data
// rxf 2023-03-05
import {returnOnError} from "../utilities/reporterror.js";
import { getActData, getAvgData, getLongAvg, fetchFromInflux, calcRange } from "../actions/getsensorData.js"
import checkParams from "../utilities/checkparams.js";
import {getOneProperty} from "../actions/getproperties.js";
import * as ERR from "../utilities/errortexts.js"
import {DateTime} from 'luxon'
const setoptionfromtable = (opt,tabval) => {
let ret = opt
if ((opt === null) || (opt === '')) {
ret = tabval
}
return ret
}
export const getNoiseData = async (params, possibles, props) => {
let ret = {err: null}
let {opts, err} = checkParams(params, {
mandatory:[
{name:'sensorid', type: 'int'},
],
optional: possibles
})
// To be compatible with old API:
if (opts.out === 'csv') {
opts.csv = true
}
if (err) {
return returnOnError(ret, err, getNoiseData.name)
}
// execute function depending on given 'data'
for(let x of whatTable) {
if (x.what === opts.data) {
opts.span = setoptionfromtable(opts.span, x.span)
opts.daystart = setoptionfromtable(opts.daystart, x.daystart)
let {start, stop} = calcRange(opts) // calc time range
opts.start = start
opts.stop = stop
let erg = await x.func(opts) // get the data
ret.sid = opts.sensorid
ret.start = opts.start.slice(7)
ret.span = opts.span
if ((x.what === 'havg') || (x.what === 'davg')) {
ret.average = x.avg
ret.peak = opts.peak
}
ret.count = erg.length
ret.values = erg
return ret
}
}
return returnOnError(ret, ERR.CMNDUNKOWN, getNoiseData.name)
}
// *********************************************
// getLiveData
//
// Get all actual data from database. Values are stored every 2.5min
//
// params:
// db: Database
// opt: different options (see further down)
//
// return:
// JSON:
// { sid: 29212, span: 1, start: "2019-10-23T00:00", count: 381, values: [
// { datetime: "2019-10-22T22:05:34.000Z", noise_LAeq: 42.22, noise_LA_min: 39.91, noise_LA_max: 45.18, E10tel_eq: 16672.47212551061 },
// { datetime: "2019-10-22T22:07:59.000Z", noise_LAeq: 53.72, noise_LA_min: 39.97, noise_LA_max: 63.54, E10tel_eq: 235504.9283896009 },
// .........
// ]}
// CSV
// datetime,LAeq,LAmax,LAmin,"10^(LAeq/10)"
// 2019-10-22T22:05:34.000Z,42.22,45.18,39.91,16672.47212551061
// 2019-10-22T22:07:59.000Z,53.72,63.54,39.97,235504.9283896009
// 2019-10-22T22:15:16.000Z,44.02,48.99,42.14,25234.807724805756
// ....
//
// *********************************************
const getLiveData = async (opt) => {
let retur = {sid: opt.sensorid, span: opt.span, start: opt.datetime, count: 0, values: [], err: null};
return await getActData(opt)
}
// *********************************************
// gethavgData
//
// Get average per hour, default: 5 days
//
// params:
// db: Database
// opt: different options (see further down)
//
// return:
// JSON:
// { sid: 29212, span: 5, start: "2019-11-01T23:00:00Z", average: 'hour', peak: 70, count: 120, values: [
// { datetime: "2019-10-22T23:00:00.000Z", n_AVG: 58.27, peakcount: 3 },
// { datetime: "2019-10-23T00:00:00.000Z", n_AVG: 45.77, peakcount: 4 },
// { datetime: "2019-10-23T01:00:00.000Z", n_AVG: 62.34, peakcount: 6 },
// .........
// ]}
// CSV:
// datetime,n_AVG,peakcount
// 2019-10-22T23:00:00.000Z,58.27,3
// 2019-10-23T00:00:00.000Z,45.77,4
// 2019-10-23T01:00:00.000Z,62.34,6
// ....
//
// *********************************************
const gethavgData = async (opts) => {
let e = await getNoiseAVGData(opts)
if (opts.csv) {
let csvStr = "datetime,n_AVG,peakcount\n";
for (let item of e) {
if (item.n_AVG != -1) {
csvStr += item.datetime + ',' + item.n_AVG + ',' + item.peakcount + '\n'
}
}
return csvStr;
} else {
return e
}
}
// *********************************************
// getdavgData
//
// Get average per day , default: 30 days
//
// params:
// db: Database
// opt: different options (see further down)
//
// return:
// JSON:
// { sid: 29212, span: 30, start: "2019-10-23T00:00", average: 'day', peak: 70, count: 30, values: [
// { datetime: "2019-10-22T23:00:00.000Z", n_AVG: 58.27, peakcount: 300 },
// { datetime: "2019-10-23T23:00:00.000Z", n_AVG: 62.34, peakcount: 245 },
// .........
// ]}
//
// CSV:
// datetime,n_AVG,peakcount
// 2019-10-22T23:00:00.000Z,58.27,300
// 2019-10-23T23:00:00.000Z,62.34,245
// ....
//
// *********************************************
async function getdavgData(opts) {
opts.long = true;
let erg = await getNoiseAVGData(opts);
let val = [];
let csvStr = 'datetime,n_AVG,peakcount\n';
for (let i = 0; i < erg.length; i+=24) {
let sum = 0;
let count = 0;
let pk = 0;
let werte = {};
for(let k=0; k<24; k++) {
if (( erg[i+k] != null) && (erg[i+k].n_sum != -1)) {
sum += erg[i + k].n_sum;
count += erg[i + k].count;
pk += erg[i + k].peakcount;
if (werte.datetime === undefined) {
let dt = DateTime.fromISO(erg[i + k].datetime, {zone: 'utc'})
werte.datetime = dt.startOf('day').toISO()
}
}
}
werte.n_AVG = 10 * Math.log10( sum/count);
werte.peakcount = pk;
if (opts.csv) {
csvStr += werte.datetime + ',' + werte.n_AVG + ',' + werte.peakcount + '\n'
} else {
val.push(werte);
}
}
if (opts.csv) {
return csvStr;
} else {
return val
// {
// sid: opts.sensorid,
// span: opts.span,
// start: opts.start.slice(7),
// 'average': 'day',
// peak: opts.peak,
// count: val.length,
// values: val
// };
}
}
// *********************************************
// getdaynightData
//
// Get average for day (6h00 - 22h00) and night (22h00 - 6h00) separated
// Use the hour average calculation, which brings the sum and the count for every hour
// then add these values up for the desired time range and calculate the average.
//
// The night-value of the last day is always 0, because the night is not complete (day is
// over at 24:00 and the night lasts til 6:00)
//
// params:
// db: Database
// opt: different options (see further down)
//
// return
// JSON
// { sid: 29212, span: 30, start: "2019-09-29", count: 30, values: [
// { date: "2019-09-29", n_dayAVG: 49.45592437272605, n_nightAVG: 53.744277577490614 },
// { date: "2019-09-30", n_dayAVG: 51.658169450663465, n_nightAVG: 47.82407695888631 },
// .........
// ]}
// CSV
// datetime,n_dayAVG,n_nightAVG
// 2019-09-29,49.45592437272605,53.744277577490614
// 2019-09-30,51.658169450663465,47.82407695888631
// ....
//
// *********************************************
async function getdaynightData(opts) {
opts.long = true;
let erg = await getNoiseAVGData(opts);
let val = [];
let csvStr = 'datetime,n_dayAVG,n_nightAVG\n';
let done = false;
let dt;
// The received houerly data array always (!!) starts at 0h00 local (!) time.
// So to calculate day values, we skip the first 6 hour and start from there
// now we add 16 hour for day and following 8 hour for night
for (let i = 6; i < erg.length;) {
let dsum = 0, dcnt = 0;
let nsum = 0, ncnt = 0;
let werte = {};
for (let k = 0; k < 16; k++, i++) {
if(erg[i].n_sum != -1) {
if (werte.datetime === undefined) {
let dt = DateTime.fromISO(erg[i].datetime, {zone: 'utc'})
werte.datetime = dt.startOf('day').toISO()
}
dsum += erg[i].n_sum;
dcnt += erg[i].count;
}
}
if (i < (erg.length - 8)) {
for (let k = 0; k < 8; k++, i++)
{
if(erg[i].n_sum != -1) {
if (werte.datetime === undefined) {
let dt = DateTime.fromISO(erg[i].datetime, {zone: 'utc'})
werte.datetime = dt.startOf('day').toISO()
}
nsum += erg[i].n_sum;
ncnt += erg[i].count;
}
}
} else {
done = true;
}
if (dcnt != 0) {
werte.n_dayAVG = 10 * Math.log10(dsum / dcnt);
} else {
werte.n_dayAVG = 0;
}
if (ncnt != 0) {
werte.n_nightAVG = 10 * Math.log10(nsum / ncnt);
} else {
werte.n_nightAVG = 0;
}
if (opts.csv) {
csvStr += werte.datetime + ',' + werte.n_dayAVG + ',' + werte.n_nightAVG + '\n'
} else {
val.push(werte);
}
if (done) {
break;
}
}
if (opts.csv) {
return csvStr;
} else {
return val
// {
// sid: opts.sensorid,
// span: opts.span,
// start: opts.start.slice(7),
// count: val.length,
// values: val
// };
} }
// *********************************************
// getLdenData
//
// Use hour averages to calculate the LDEN.
// Formula:
// LDEN = 10 * log10 ( 1/24 ( (12 * 10^(Lday/10)) + (4*10^((Levn+5)/10) + (8*10^((Lnight+10)/10)) )
//
// params:
// db: Database
// sid: sensor number
// opt: different options (see further down)
//
// return:
// JSON:
// { sid: 29212, span: 30, start: "2019-09-29", count: 30, values: [
// { lden: 59.53553743437777, date: "2019-09-29" },
// { lden: 55.264733497513554, date: "2019-09-30" },
// .........
// ]}
// CSV
// datetime,lden
// 2019-09-29,59.53553743437777
// 2019-09-30,55.264733497513554
// ....
//
// *********************************************
async function getLdenData(opts) {
opts.long = true;
let erg = await getNoiseAVGData(opts);
let val = [];
let csvStr = 'datetime,lden\n';
let done = false;
const calcAVG = (sum,cnt) => {
if (cnt != 0) {
return (10 * Math.log10(sum / cnt));
} else {
return 0;
}
}
// The received hourly data array always (!!) starts at 0h00 local (!) time.
// So to calculate day values, we skip the first 6 hour and start from there
// now we add 12 hour for day and following 4 hour for evening and
// additional 8 hours for night
for (let i = 6; i < erg.length;) {
let dsum = 0, dcnt = 0;
let nsum = 0, ncnt = 0;
let esum = 0, ecnt = 0;
let werte = {};
let dayAVG = 0, evnAVG = 0, nightAVG = 0;
for (let k = 0; k < 12; k++, i++) {
if (erg[i].n_sum != -1)
{
if (werte.datetime == undefined) {
werte.datetime = erg[i].datetime;
}
dsum += erg[i].n_sum;
dcnt += erg[i].count;
}
}
for (let k = 0; k < 4; k++, i++) {
if (erg[i].n_sum != -1)
{
if (werte.datetime == undefined) {
werte.datetime = erg[i].datetime;
}
esum += erg[i].n_sum;
ecnt += erg[i].count;
}
}
if (i < (erg.length - 8)) {
for (let k = 0; k < 8; k++, i++)
{
if (erg[i].n_sum != -1)
{
if (werte.datetime == undefined) {
werte.datetime = erg[i].datetime;
}
nsum += erg[i].n_sum;
ncnt += erg[i].count;
}
}
} else {
done = true;
}
dayAVG = calcAVG(dsum, dcnt);
evnAVG = calcAVG(esum, ecnt);
nightAVG = calcAVG(nsum, ncnt);
// Calculate LDEN:
let day = 12 * Math.pow(10,dayAVG/10); // ... and calculate the LDEN values following ...
let evn = 4 * Math.pow(10, (evnAVG+5)/10); // ... the LDEN formaula (see function description)
let night = 8 * Math.pow(10,(nightAVG+10)/10);
werte.lden = 10 * Math.log10((day+evn+night)/24);
if (opts.csv) {
csvStr += werte.datetime + ',' + werte.lden + '\n'
} else {
val.push(werte);
}
if (done) {
break;
}
}
if (opts.csv) {
return csvStr;
} else {
return val
// {
// sid: opts.sensorid,
// span: opts.span,
// start: opts.start.slice(7),
// count: val.length,
// values: val
// };
}
}
const getAPIprops = (opt) => {
}
const getMAPaktData = (opt) => {
}
const whatTable = [
{'what':'live', 'span': 1, 'daystart': false, avg: null, 'func': getLiveData},
{'what':'havg', 'span': 30, 'daystart': true, avg: 'hour', 'func': gethavgData},
{'what':'davg', 'span': 30, 'daystart': true, avg: 'day', 'func': getdavgData},
{'what':'daynight', 'span': 30, 'daystart': true, avg: null, 'func': getdaynightData},
{'what':'lden', 'span': 30, 'daystart': true, avg: null, 'func': getLdenData},
{'what':'props', 'span': 0, 'daystart': true, avg: null, 'func': getAPIprops},
{'what':'mapdata', 'span': 0, 'daystart': false, avg: null, 'func': getMAPaktData},
{'what':'', 'span': 0, 'daystart': true, avg: null, 'func': null},
];
const getNoiseAVGData = async (opts) => {
let ret = {data: {count: 0, values: []}, err: null}
let emptyValues = {n_AVG:-1};
let small = '|> keep(columns: ["_time", "peakcount", "n_AVG"])'
if (opts.long) {
small = ''
emptyValues = {n_sum: -1, n_AVG:-1}
}
let queryAVG = `
import "math"
threshold = ${opts.peak}
data = from(bucket: "sensor_data")
|> range(${opts.start}, ${opts.stop})
|> filter(fn: (r) => r["sid"] == "${opts.sensorid}")
e10 = data
|> filter(fn: (r) => r._field == "E10tel_eq")
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|> map(fn: (r) => ({r with _value: (10.0 * math.log10(x: r._value))}))
|> keep(columns: ["_time","_field","_value"])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> rename(columns: {"E10tel_eq" : "n_AVG"})
ecnt = data
|> filter(fn: (r) => r._field == "E10tel_eq")
|> aggregateWindow(every: 1h, fn: count, createEmpty: false)
|> keep(columns: ["_time","_field","_value"])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> rename(columns: {"E10tel_eq" : "count"})
esum = data
|> filter(fn: (r) => r._field == "E10tel_eq")
|> aggregateWindow(every: 1h, fn: sum, createEmpty: false)
|> keep(columns: ["_time","_field","_value"])
|> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
|> rename(columns: {"E10tel_eq" : "n_sum"})
peak = data
|> filter(fn: (r) => r._field == "noise_LAeq")
|> aggregateWindow(
every: 1h,
fn: (column, tables=<-) => tables
|> reduce(
identity: {peakcount: 0.0},
fn: (r, accumulator) => ({
peakcount: if r._value >= threshold then
accumulator.peakcount + 1.0
else
accumulator.peakcount + 0.0,
}),
),
)
|> keep(columns: ["_time","peakcount"])
part1 = join( tables: {e10: e10, ecnt: ecnt}, on: ["_time"])
part2 = join( tables: {esum: esum, peak: peak}, on: ["_time"])
join( tables: {P1: part1, P2: part2}, on: ["_time"])
${small}
`
let erg = await fetchFromInflux(ret, queryAVG)
if(erg.err) {
return returnOnError(ret, err, getNoiseAVGData.name)
}
// The times are always the END of the period (so: period from 00:00h to 01:00h -> time is 01:00)
// To easily extract the values, we copy the data from docs into a new array, so that the
// hour in an element in docs becomes the index into the new array (for every new day this
// index will be incremented by 24). Missing values are marked by: {n_sum=-1, n_AVG=-1}.
let hoursArr = new Array(opts.span * 24); // generate new array
hoursArr.fill(emptyValues); // fill array with 'empty' values
let startDay = DateTime.fromISO(erg.values[0].datetime, {zone: 'utc'}).get('day'); // calc first day
let k = 0;
for (let d of erg.values) { // loop through docs
let stunde = DateTime.fromISO(d.datetime, {zone: 'utc'}).get('hour') // get current hour
let day = DateTime.fromISO(d.datetime, {zone: 'utc'}).get('day') // get current day
if (day != startDay) { // if date has changed
k += 24; // increment index by 24
startDay = day;
}
hoursArr[k+stunde] = d; // copy date into hourArray
}
return hoursArr;
}
+69
View File
@@ -0,0 +1,69 @@
// Data preparation for fetching radioactivity data
// rxf 2022-06-24
import {returnOnError} from "../utilities/reporterror.js";
import { getActData, getAvgData, getLongAvg } from "../actions/getsensorData.js"
const radioactFilter = (data, opts, actual) => {
let erg = {}
erg.sid = opts.sensorid
erg.sname = opts.sname
erg.values = []
for (let x of data.values) {
let entry = {}
entry.datetime = x.datetime
if(actual) {
entry.cpm = x.counts_per_minute,
entry.uSvh = x.counts_per_minute / 60 * opts.factor
} else {
entry.cpmAvg = x.counts_per_minute,
entry.uSvhAvg = x.counts_per_minute / 60 * opts.factor
}
erg.values.push(entry)
}
return erg
}
export const getRadioData = async (opts) => {
let erg = { err: null, data: {}}
let params = {
sensorid: opts.sensorid,
avg: opts.avg,
datetime: opts.start
}
if (opts.what === 'oneday') {
params.span = 1
} else if (opts.what === 'oneweek') {
params.span = 7
} else {
params.span = 31
params.moving = false
params.avg = 1440
}
let { data, err } = await getAvgData(params)
if (err != null) {
return returnOnError(erg, err, getRadioData.name)
}
erg.data.radiomovavg = radioactFilter(data, opts, false)
if (opts.what === 'oneday') {
const { data, err} = await getActData(params)
if (err != null) {
return returnOnError(erg, err, getRadioData.name)
}
erg.data.radioactual = radioactFilter(data, opts, true)
}
if(opts.climatesid && ((opts.what === 'oneday') || (opts.what === 'oneweek'))) {
params.sensorid = opts.climatesid
params.avg = 10
const { data, err} = await getAvgData(params)
if (err != null) {
return returnOnError(erg, err, getRadioData.name)
}
data.sid = opts.climatesid
data.sname = opts.climatesname
erg.data.climate = data
}
return erg
}
+29
View File
@@ -0,0 +1,29 @@
import {getData4map} from '../actions/data4map.js'
import assert from 'assert/strict'
describe("get data for map - test", function() {
const box = {
"east": 9.322391662597672,
"north": 48.86726810417461,
"south": 48.69057640500091,
"west": 8.99760833740236
}
it("radioactivity should return at least 4 elements", async function() {
const erg = await getData4map(
{
"type": "radioactivity",
"box": box,
test: true
})
assert.ok(erg.count >= 4)
})
it("pm should return at least 270 elements", async function() {
const erg = await getData4map(
{
"type": "pm",
"box": box
})
assert.ok(erg.count >= 270)
})
})
+55
View File
@@ -0,0 +1,55 @@
// parse the params from http call
import * as ERR from '../utilities/errortexts.js'
import {returnOnError} from "./reporterror.js"
const checkParams = (params, mo) => {
let o = {opts: {}, err: null}
if ((mo.mandatory.length !== 0) && (params === undefined)) {
return returnOnError(o, ERR.NOPARAMETER, checkParams.name )
}
for (let p of mo.mandatory) {
if (!(p.name in params)) {
return returnOnError(o, ERR.NOMANDPARAM.replace('xx', p.name), checkParams.name )
}
if (p.type === 'int') {
let x = parseInt(params[p.name])
if (isNaN(x)) {
return returnOnError(o, ERR.PARAMNONUM, checkParams.name )
} else {
o.opts[p.name] = x
continue
}
} else if (p.type === 'float') {
let x = parseFloat(params[p.name])
if (isNaN(x)) {
return returnOnError(o, ERR.PARAMNONUM, checkParams.name )
} else {
o.opts[p.name] = x
continue
}
}
o.opts[p.name] = params[p.name]
}
for(let p of mo.optional) {
if (!(p.name in params)) {
o.opts[p.name] = p.default
} else {
if (p.type === 'int') {
let x = parseInt(params[p.name])
if (isNaN(x)) {
o.opts[p.name] = p.default
} else {
o.opts[p.name] = x
}
} else if (p.type === 'bool') {
o.opts[p.name] = params[p.name] === 'true'
} else {
o.opts[p.name] = params[p.name]
}
}
}
return o
}
export default checkParams
+22
View File
@@ -0,0 +1,22 @@
// Command table for http calls
import * as getData from "../actions/getsensorData.js"
import * as getProps from "../actions/getproperties.js"
import * as getAKWs from "../actions/getAKWData.js"
import * as holAddr from "../actions/getaddress.js"
import * as radioact from "../sensorspecials/radioact.js"
import * as noise from "../sensorspecials/noise.js"
const cmdTable = [
{cmd: 'getactdata', func: getData.getActData},
{cmd: 'getlongavg', func: getData.getLongAvg},
{cmd: 'getavgdata', func: getData.getAvgData},
{cmd: 'getoneproperty', func: getProps.getOneProperty},
{cmd: 'getakwdata', func: getAKWs.getakwdata},
{cmd: 'getaddress', func: holAddr.getAddress},
{cmd: 'getradiodata', func: radioact.getRadioData},
{cmd: 'getnoisedata', func: noise.getNoiseData},
{cmd: 'getsensordata', func: getData.getSensorData}
]
export default cmdTable
+35
View File
@@ -0,0 +1,35 @@
// convert influx csv output to JSON
export function csv2Json(str, delimiter = ",") {
// slice from start of text to the first \n index
// use split to create an array from string by delimiter
const headers = str.slice(0, str.indexOf("\r\n")).split(delimiter).slice(3);
let x = headers.findIndex((x) => (x === '_time') || (x === '_stop'))
if (x != -1) {
headers[x] = 'datetime'
}
// slice from \n index + 1 to the end of the text
// use split to create an array of each csv value row
const rows = str.slice(str.indexOf("\r\n") + 2).split("\r\n").slice(0,-2)
// Map the rows
// split values from each row into an array
// use headers.reduce to create an object
// object properties derived from headers:values
// the object passed as an element of the array
const arr = rows.map(function (row) {
const values = row.split(delimiter).slice(3);
const el = headers.reduce(function (object, header, index) {
if(header !== 'datetime') {
object[header] = parseFloat(values[index]);
} else {
object[header] = values[index];
}
return object;
}, {});
return el;
});
// return the array
return arr;
}
+14
View File
@@ -0,0 +1,14 @@
// Errortexts
export const CMNDUNKOWN = 'Command not known'
export const NOTHIMG = 'Nothing to show'
export const PARAMNONUM = `Parameter ${'xx'} is not a number`
export const NOTYP = 'No type given'
export const NOSENSFOUND = 'No suitable sensors found in properties'
export const NOMANDPARAM = `Mandatory parameter '${'xx'}' not given`
export const NOPROPSREAD = `No properties read for sensor ${'xx'}`
export const NOPARAMETER = 'No parameter given'
export const RESPSTATUS = `Returned status = ${'xx'}`
export const NODATA = 'No data found'
export const SYNTAXURL = 'Syntax error in calling url'
export const WRONGTYPE = `Sensor ${'xx'} is not of type ${'yy'}`
+16
View File
@@ -0,0 +1,16 @@
import { DateTime} from 'luxon'
const MOCHA_TEST = process.env.MOCHA_TEST || false
export function logit(str) {
if(MOCHA_TEST) return
let s = `${DateTime.now().toISO()} => ${str}`;
console.log(s);
}
export function logerror(str) {
if(MOCHA_TEST) return
let s = `${DateTime.utc().toISO()} => *** ERROR *** ${str}`;
console.log(s);
}
+14
View File
@@ -0,0 +1,14 @@
import {logit} from "./logit.js";
export const reportError = (message, errortext) => {
message.error = true
message.errortext = errortext
return message
}
export const returnOnError = (pr, error, name) => {
pr.err = error
logit(`${name}: ${error}`)
return pr
}