Initial monorepo: readin, sensorapi, noise (Node22, npm ci, axios1.x, i18next-fs-backend)

This commit is contained in:
rxf
2026-07-25 11:35:41 +00:00
commit 2df1f79617
97 changed files with 13547 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
node_modules/
log/
*.bak
*.bak-*
.DS_Store
*.log
+10
View File
@@ -0,0 +1,10 @@
# sensor-stack
Monorepo fuer den noisesensors-Stack (esprimo, Dockge: /opt/stacks/esprimo/noisesensors).
- **readin/** Sensordaten von sensor.community -> InfluxDB/MongoDB (Cron). Image `docker.citysensor.de/readin`
- **sensorapi/** Backend-API (liest Influx/Mongo). Image `docker.citysensor.de/sensorapi`
- **noise/** Web-Frontend (laerm_web, nutzt sensorapi). Image `docker.citysensor.de/noise`
Deploy je Service: `cd <dir> && ./deploy.sh` (nativer amd64-Build -> Registry),
dann `cd /opt/stacks/esprimo/noisesensors && docker compose up -d <service>`.
+9
View File
@@ -0,0 +1,9 @@
build_and_copy.sh
Dockerfile_noise
.vscode
node_modules
log
.env*
.gitignore
deploy.sh
Readme.*
+10
View File
@@ -0,0 +1,10 @@
# API Configuration
API_KEY=your_api_key_here
APIHOST=http://localhost:3005
# MongoDB Configuration (if needed)
MONGO_ROOT_USERNAME=admin
MONGO_ROOT_PASSWORD=your_password_here
LOCALDIR=/path/to/local/dir
REPO_USER=your_username
REPO_PASSWORD=your_password
+4
View File
@@ -0,0 +1,4 @@
node_modules
log
.env
Readme.pdf
+26
View File
@@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch Program",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/www.js",
"env": {
"PORT": "3003",
"APIHOST": "http://localhost:3005",
// "MONGOHOST": "reception",
// "MONGOUSRP": "admin:mongo4noise",
// "MONGOAUTH": "true",
// "INFLUXHOST": "reception",
// "INFLUXTOKEN": "gHGGgjaK0lmM6keMa01JeuDpqOE_vRq8UimsU4QKb2miI5BDh2PfWynEbwKizdJapXy8jVbTat5mVZLQTNmSdw==",
}
}
]
}
+19
View File
@@ -0,0 +1,19 @@
FROM node:22-alpine
ADD package.json /tmp/package.json
ADD package-lock.json /tmp/package-lock.json
RUN cd /tmp && npm ci
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /tmp/package.json /opt/app/
WORKDIR /opt/app
ADD . /opt/app/
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
EXPOSE 3000
CMD ["npm", "start"]
+28
View File
@@ -0,0 +1,28 @@
# Develop und Deploy der Noise-Anwendung
rxf 2025-08-08
## Develop local ( auf esprimo !)
* via VS die APP **sensorapi** auf dem *esprimo* starten
* als **mongo** direkt die auf *ionos* verwenden
* in VS die app **laerm_web_sensorapi** editieren und starten
* im Browser aufrufen: **esprimo:3000**
## Deploy auf *ionos*
* überprüfen, ob **portainer** richtig läuft, ggf. neu installieren
* prüfen, ob alle Container richtig laufen, vor Allem der **watchtower**, ggf. das Stack neu starten
* in VS (auf esprimo) das script **deploy.sh** aufrufen
* auf **ionos** sollte nun via **watchtower** die neue Version geladen werden (spätestens nach 1 Minute)
** nun im Browser **https://noise.fuerst-stuttgart.de** aufrufen und testen
## Deploy auf *rbsensors*
* wenn **deplay.sh** noch nicht gestartet wurde (via VS auf esprimo), diese aufrufen.
* Portainer auf rbsensors aufrufen: **192.168.51.22:9000**
* über **Images** das neueste Image (**noise:latest**) vom Repository *Reinhard* holen
* das Stack **noisesensor** aufrufen
* den Container **noise** löschen
* via *edit* das Stack neu starten
* und nun im Browser mit **laerm.rbsensors.de** testen
| Datum | Version | Bemerkung |
|-------|---------|---------|
| 2025-08-08 | 1.0.1 | Typos verbessert |
| 2024-12-20| 1.0.0 | erste Version |
+77
View File
@@ -0,0 +1,77 @@
import 'dotenv/config'
import createError from 'http-errors'
import logger from 'morgan'
import express from 'express'
import cookieParser from 'cookie-parser'
import path from 'path'
import { fileURLToPath } from 'url'
import i18next from 'i18next'
import i18nextMiddleware from 'i18next-http-middleware'
import Backend from 'i18next-fs-backend'
const app = express()
export let url2call
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import * as indexRouter from './routes/index.js'
import apiRouter from './routes/api.js'
i18next
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
backend: {
loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'de',
debug: false,
preload: ['de', 'en']
});
app.use(i18nextMiddleware.handle(i18next));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
app.get('*', (req, res, next) => {
const subdom = req.headers.host.split('.')
if (subdom[0] === 'laerm') {
subdom[0] = 'noise'
}
// app.set('category',subdom[0])
app.set('category','noise')
next()
})
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.static(path.join(__dirname,'node_modules')));
app.use('/api', apiRouter)
app.use('/srv', apiRouter)
app.use('/', indexRouter.router);
// 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.render('error');
});
export default app
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from '../app.js'
import Debug from 'debug'
import http from 'http'
const debug = Debug('laerm-web-sensorapi:server')
/* var app = require('../app');
var debug = require('debug')('laerm-web-sensorapi:server');
var http = require('http');
*/
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var 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) {
var 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;
}
var 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() {
var addr = server.address();
var 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=noise
name=noise
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 --no-cache -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'
+565
View File
@@ -0,0 +1,565 @@
// Prepare all data to plot the charts
import * as utils from '../charts/utilities.js'
import { translate as trans } from '../routes/index.js'
/**********************************************
* Live-Data
* *
* @param params
* @param values
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
*
***********************************************/
export const liveData = (params, values, req) => {
let series1 = []
let series2 = []
let series3 = []
let infoTafel
// save the current (actual) values here
let aktVal = {}
// Put values into the arrays
let cnt = 0
values.forEach((x) => {
let dat = new Date(x.datetime).getTime()
series1.push([dat, x.LAeq])
series2.push([dat, x.LA_max])
series3.push([dat, x.LA_min]) // put data and value into series array
})
if (values.length != 0) {
const lastidx = values.length - 1
// Aktuelle Werte speichern
aktVal['LAeq'] = values[lastidx].LAeq
aktVal['LAMax'] = values[lastidx].LA_max
aktVal['LAMin'] = values[lastidx].LA_min
// InfoTafel füllen
infoTafel =
'<table class="infoTafel"><tr >' +
'<th colspan="3">' + trans("ActualValues") + '</th>' +
'</tr><tr>' +
'<td>LAeq</td><td>' + (aktVal.LAeq).toFixed(1) + '</td><td>dbA</td>'
infoTafel +=
'</tr><tr>' +
'<td>LAmin</td><td>' + (aktVal.LAMin).toFixed(0) + '</td><td>dbA</td>'
infoTafel +=
'</tr><tr>' +
'<td>LAmax</td><td>' + (aktVal.LAMax).toFixed(0) + '</td><td>dbA</td>'
infoTafel +=
'</tr></table>' +
'</div>'
}
let txtMeldung = false
// Plot-Options
let options = utils.createGlobObtions('dd.LL HH:mm:ss')
let series_LAeq = {
name: 'LAeq',
data: series1,
color: utils.colors.eq,
yAxis: 0,
zIndex: 1,
marker: {
enabled: false,
symbol: 'square',
},
visible: true,
fillColor: {
linearGradient: [0, 600, 0, 0],
stops: [
[0, 'rgb(238,223,236)'],
[1, 'rgb(238,223,236)']
]
},
}
let series_LAMax = {
name: 'LAmax',
data: series2,
color: utils.colors.max,
yAxis: 0,
zIndex: 0,
marker: {
enabled: false,
symbol: 'square',
},
visible: true,
fillColor: {
linearGradient: [0, 600, 0, 0],
stops: [
[0, 'rgb(255,233,236)'],
[1, 'rgb(255,233,236)']
]
},
// fillOpacity: 0.2,
}
let series_LAMin = {
name: 'LAmin',
data: series3,
color: utils.colors.min,
yAxis: 0,
zIndex: 6,
marker: {
enabled: false,
symbol: 'square',
},
visible: true,
fillColor: {
linearGradient: [0, 600, 0, 0],
stops: [
[0, 'rgb(219,216,217)'],
[1, 'rgb(219,216,217)']
]
},
}
let yAxis_LAeq = { // 1
title: {
text: 'dbA',
style: {
color: 'black'
}
},
max: utils.noise_ymax,
// min: utils.noise_ymin,
min: 10,
// opposite: true,
tickAmount: 11,
useHTML: true,
}
options.series = []
options.yAxis = []
options.series[0] = series_LAeq
options.series[1] = series_LAMin
options.title.text = trans('NoiseLevelsLast24Hours')
options.subtitle.useHTML = true
options.subtitle.text = `${trans("25minlogAverageValues")}<br />&nbsp;`
options.series[2] = series_LAMax
// options.yAxis[2] = yAxis_LAMin
options.yAxis[0] = yAxis_LAeq
// options.yAxis[1] = yAxis_LAMax
options.chart.zoomType = 'x'
options.chart.type = 'area'
options.chart.resetZoomButton= {
position: {
align: 'left',
verticalAlign: 'bottom',
x: 20,
y: -50,
},
relativeTo: 'chart'
}
// options.tooltip.formatter = function () {
// let y = Math.round(this.y * 100) / 100
// return '<div style="border: 2px solid ' + this.point.color + '; padding: 3px;">' +
// DateTime.fromMillis(this.x, {zone: 'utc'}).toFormat("dd.LL - HH:mm:ss") + '<br />' +
// '<span style="color: ' + this.point.color + '">&#9679;&nbsp;</span>' +
// this.series.name + ':&nbsp; <b>' + y + '</b></div>'
// }
let noDataTafel = '<div class="errTafel">' +
'Für heute liegen leider keine Daten vor!<br /> Bitte den Sensor überprüfen!\' <br />' +
'</div>'
let navy = 20
let navbreit = 55
let navtxt = ['-24h', '-12h', 'live', '+12h', '+24h']
let navtime = [-24, -12, 0, 12, 24]
return { options: options, params: params, info: {class: 'infoTafel', text:infoTafel}}
}
/********************************************
* havg - Data
*
* @param params
* @param values
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params, options2: any}}
*
********************************************/
export const havgData = (params, values) => {
let series1 = []
let series2 = []
// Put values into the series
let cnt = 0
values.forEach((x) => {
if (x.n_AVG != -1) {
let dat = new Date(x.datetime).getTime() // retrieve the date
series1.push([dat, x.n_AVG])
series2.push([dat, x.peakcount])
}
})
// Plot-Options
let options = utils.createGlobObtions()
options.xAxis.tickInterval = 6 * 3600 * 1000
// options.xAxis.plotBands = utils.calcWeekends(data, true)
options.xAxis.minTickInterval = 3600 * 1000
options.yAxis = [
{
title: {
text: 'dbA',
useHTML: true,
},
min: utils.noise_ymin,
tickAmount: 10,
gridLineColor: 'lightgray'
}, {
title: {
text: 'Counts',
useHTML: true,
},
tickAmount: 10,
allowDecimals: false,
gridLineColor: 'lightgray',
opposite: true
}
]
options.series = [
{
name: `${trans('MeanValueLAeq')}`,
data: series1,
color: utils.colors.eq,
type: 'column',
zIndex: 2,
yAxis: 0
}, {
name: `${trans('Peaks')}`,
data: series2,
color: utils.colors.peaks,
type: 'column',
yAxis: 1,
zIndex: 2
}
]
// options.plotOptions.column = {
// pointWidth: 8,
// groupPadding: 0.5
// }
options.plotOptions.column = {
pointWidth: 8,
groupPadding: 0.1
}
options.title.text = `${trans('HourlyMeanValues')} / ${trans('PeaksOver')} ${params.peak} dbA`
options.subtitle.text = `${trans('HourlyMeanValuesOfLAeqForEachFullHour')} / ${trans('NumberOfPeaksPerHour')}<br />&nbsp;`
options.subtitle.useHTML = true
options.chart.zoomType = 'x'
options.chart.resetZoomButton= {
position: {
align: 'left',
x: 20,
y: 300,
},
relativeTo: 'chart'
}
return { options: options, params: params}
}
/********************************************
* davg - Data
*
* @param params
* @param values
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params, options2: any}}
*
********************************************/
export const davgData = (params, values) => {
let series1 = []
let series2 = []
// Put values into the serieses
let cnt = 0
values.forEach((x) => {
if (x.n_AVG != -1) {
let dat = new Date(x.datetime).getTime() // retrieve the date
series1.push([dat, x.n_AVG])
series2.push([dat, x.peakcount])
}
})
// Plot-Options
let options = utils.createGlobObtions()
// options.xAxis.plotBands = utils.calcWeekends(values, true)
options.xAxis.tickInterval = 24 * 3600 * 1000
options.xAxis.title.text = 'Date'
// options.xAxis.labels = {
// formatter: function () {
// return this.axis.defaultLabelFormatter.call(this);
// }
// }
options.chart.type = 'column'
options.yAxis = [
{
title: {
text: 'dbA',
useHTML: true,
},
min: utils.noise_ymin,
tickAmount: 10,
gridLineColor: 'lightgray',
}, {
title: {
text: 'Counts',
useHTML: true,
},
tickAmount: 10,
allowDecimals: false,
gridLineColor: 'lightgray',
opposite: true
}
]
options.series = [
{
name: `${trans('MeanValueLAeq')}`,
data: series1,
color: utils.colors.eq,
type: 'column',
zIndex: 2,
yAxis: 0
},{
name: `${trans('Peaks')}`,
data: series2,
color: utils.colors.peaks,
type: 'column',
yAxis: 1,
zIndex: 2,
}
]
options.plotOptions.column = {
pointWidth: 20,
groupPadding: 0.1
}
options.title.text = `${trans('DailyMeanValues')} / ${trans('PeaksOver')} ${params.peak} dbA`
options.subtitle.text = `${trans('DailyMeanValuesOfLAeqForEachFullDay')} / ${trans('NumberOfPeaksPerDay')}<br />&nbsp;`
options.subtitle.useHTML = true
options.chart.zoomType = 'x'
options.chart.resetZoomButton = {
position: {
align: 'left',
// verticalAlign: 'bottom',
x: 20,
y: 300,
},
relativeTo: 'chart'
}
return {options: options, params: params}
}
/********************************************
* dayNight - Data
*
* @param params
* @param values
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
*
********************************************/
export const dayNightData = (params, values) => {
let series1 = []
let series2 = []
// Put values into the serieses
let cnt = 0
values.forEach((x) => {
if (x.n_dayAVG > 0) {
let dat = new Date(x.datetime).getTime() // retrieve the date
series1.push([dat, x.n_dayAVG])
series2.push([dat, x.n_nightAVG])
}
})
// Plot-Options
let options = utils.createGlobObtions()
// options.xAxis.plotBands = utils.calcWeekends(data, true)
options.xAxis.tickInterval = 24 * 3600 * 1000
options.xAxis.title.text = trans('Date')
// options.xAxis.labels = {
// formatter: function () {
// return this.axis.defaultLabelFormatter.call(this);
// }
// }
options.chart.type = 'column'
options.yAxis = {
title: {
text: 'dbA',
useHTML: true,
},
min: utils.noise_ymin,
tickAmount: 10,
gridLineColor: 'lightgray',
}
options.series = [
{
name: `${trans('Day')}`,
data: series1,
color: utils.colors.eq,
type: 'column',
zIndex: 2,
},
{
name: `${trans('Night')}`,
data: series2,
color: utils.colors.max,
type: 'column',
zIndex: 2,
},
]
options.plotOptions.series = {
animation: false
}
options.plotOptions.column = {
pointWidth: 20,
groupPadding: 0.3
}
options.title.text = trans('AverageValuesDuringTheDayAndNightHours')
options.subtitle.text = `${trans('DailyMeanValuesOfLAeqFrom6h00To22h00')}<br />${trans('NightlyMeanValuesOfLAeqFrom22h00To6h00')}<br />&nbsp;`
options.subtitle.useHTML = true
options.chart.zoomType = 'x'
options.chart.resetZoomButton = {
position: {
align: 'left',
// verticalAlign: 'bottom',
x: 20,
y: 300,
},
relativeTo: 'chart'
}
return {options: options, params: params}
}
/***********************************
Lden - Data
*
* @param params
* @param values
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
**********************************/
export const ldenData = (params, values) => {
let series1 = []
// Put values into the series
let cnt = 0
values.forEach((x) => {
if (!((x.lden === -1) || (x.datetime === undefined))) {
let dat = new Date(x.datetime).getTime() // retrieve the date
series1.push([dat, x.lden])
}
})
// Plot-Options
let options = utils.createGlobObtions()
// options.xAxis.plotBands = utils.calcWeekends(data, true)
options.xAxis.tickInterval = 24 * 3600 * 1000
options.xAxis.title.text = trans('Date')
options.xAxis.labels = {
formatter: function () {
let lbl = this.axis.defaultLabelFormatter.call(this);
this.value += 86400000;
let lbl1 = this.axis.defaultLabelFormatter.call(this);
return parseInt(lbl) + '/' + lbl1;
}
}
options.chart.type = 'column'
options.yAxis = {
title: {
text: 'dbA',
useHTML: true,
},
min: utils.noise_ymin,
tickAmount: 10,
gridLineColor: 'lightgray',
}
options.series = [
{
name: 'Lden',
data: series1,
color: utils.colors.eq,
type: 'column',
zIndex: 2,
}
]
options.plotOptions.series = {
animation: false
}
options.plotOptions.column = {
pointWidth: 20,
groupPadding: 0.1
}
options.title.text = trans('LDEN-Index')
options.subtitle.text = `${trans('DailyMeanValuesOfLDEN-Index')}<br />&nbsp;`
options.subtitle.useHTML = true
options.chart.zoomType = 'x'
options.chart.resetZoomButton = {
position: {
align: 'left',
// verticalAlign: 'bottom',
x: 20,
y: 300,
},
relativeTo: 'chart'
}
/* options.tooltip.formatter = function () {
let d = DateTime.fromMillis(this.x, {zone: 'utc'})
let d1 = d.plus({days: 1})
let txt = d.toFormat('dd') + '/' + d1.toFormat('dd.LL')
return '<div style="border: 2px solid ' + this.point.color + '; padding: 3px;">' +
'&nbsp;&nbsp;' + txt + '<br />' +
'<span style="color: ' + this.point.color + '">&#9679;&nbsp;</span>' +
this.series.name + ':&nbsp; <b>' +
Highcharts.numberFormat(this.y, 1) +
'</b></div>';
}
*/
return {options: options, params: params}
}
/***********************************
Map - Data
*
* @param params
* @param values
* @returns {{options: {plotOptions: {series: {marker: {enabled: boolean}, turboThreshold: number, animation: boolean}}, xAxis: {gridLineWidth: number, type: string, title: {text: string}, labels: {formatter: function(): (string|*)}}, legend: {layout: string, borderWidth: number, align: string, enabled: boolean}, subtitle: {align: string}, tooltip: {backgroundColor: number, valueDecimals: number, borderRadius: number, useHTML: boolean, borderWidth: number}, title: {useHTML: boolean, style: {fontSize: string}, align: string}, chart: {backgroundColor: {linearGradient: number[], stops: [[number,string],[number,string]]}, spacingTop: number, borderWidth: string, spacingLeft: number, type: string, spacingRight: number, events: {selection: function(*): void}, height: number}}, params}}
**********************************/
export const mapData = (params, values) => {
let actsensors = 0
for (let x of values) {
if (x.value >= 0) {
actsensors++
}
}
return {
...params,
values: values,
actsensors: `${trans("ActiveSensors")} ${actsensors}`,
registered: `(${trans("registered")} ${params.count})`,
valfromtxt: trans("ValuesFrom"),
popuptxt: {
offline: trans('offline'),
sensor: trans('Sensor'),
lastseen: trans('LastSeen'),
showchart: trans('ShowChart')
}
}
}
+151
View File
@@ -0,0 +1,151 @@
// Utility routine for plotting the data
import { translate as trans } from '../routes/index.js'
let language = 'en'
export const colors = {'eq': '#0000FF', 'max': '#FF0000', 'min': '#008000', 'peaks': '#DAA520'};
export const noise_ymin = 20;
export const noise_ymax = 120;
export function createGlobObtions() {
// Options, die für alle Plots identisch sind
let globObject = {
chart: {
spacingRight: 20,
spacingLeft: 20,
spacingTop: 25,
backgroundColor: {
linearGradient: [0, 400, 0, 0],
stops: [
[0, '#eee'],//[0, '#ACD0AA'], //[0, '#A18D99'], // [0, '#886A8B'], // [0, '#F2D0B5'],
[1, '#fff']
]
},
type: 'line',
borderWidth: '2',
// events: {
// selection: function (event) {
// if (event.xAxis) {
// doUpdate = false;
// } else {
// doUpdate = true;
// }
// }
// }
},
title: {
align: 'left',
style: {'fontSize': '25px'},
useHTML: true,
},
subtitle: {
align: 'left',
},
tooltip: {
valueDecimals: 1,
backgroundColor: '#ffffff',
borderWidth: 0,
borderRadius: 0,
useHTML: true,
},
xAxis: {
type: 'datetime',
title: {
text: trans('datetime'),
},
gridLineWidth: 2,
labels: {
formatter: function () {
let v = this.axis.defaultLabelFormatter.call(this);
if (v.indexOf(':') == -1) {
return '<span style="font-weight:bold;color:red">' + v + '<span>';
} else {
return v;
}
}
},
},
legend: {
enabled: true,
layout: 'horizontal',
// verticalAlign: 'top',
borderWidth: 1,
align: 'center',
},
plotOptions: {
series: {
animation: false,
turboThreshold: 0,
marker: {
enabled: false,
},
},
}
};
return globObject;
}
export function calcWeekends(data, isyear) {
/* let weekend = [];
let oldDay = 8;
for (let i = 0; i < data.length; i++) {
let mom = moment(data[i].date);
if (isyear) {
mom = moment(data[i]._id)
}
let day = mom.day();
let st = mom.startOf('day');
if (day != oldDay) {
if (day == 6) {
weekend.push({
color: 'rgba(169,235,158,0.4)',
from: st.valueOf(),
to: st.add(1, 'days').valueOf(),
zIndex: 0
})
} else if (day == 0) {
weekend.push({
color: 'rgba(169,235,158,0.4)',
from: st.valueOf(),
to: st.add(1, 'days').valueOf(),
zIndex: 0
})
}
oldDay = day;
}
}
return weekend;
*/}
export function calcDays(data, isyear) {
let days = [];
if (data.length == 0) {
return days
}
let oldday = moment(data[0].date).day();
if (isyear) {
oldday = moment(data[0]._id).day();
}
for (let i = 0; i < data.length; i++) {
let m = moment(data[i].date);
if (isyear) {
m = moment(data[i]._id);
}
let tag = m.day()
if (tag != oldday) {
m.startOf('day');
days.push({color: 'lightgray', value: m.valueOf(), width: 1, zIndex: 2});
oldday = tag;
}
}
return days;
};
export const setLanguage = (lng) => {
language = lng
}
export const getLanguage = () => {
return language
}
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -e
# Angepasst aus dem readin-/sensorapi-deploy.sh (urspr. /Projekte/Logbuch/deploy.sh)
# Nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
# eigener Dockerfile-Name (Dockerfile_noise). Image-Name: noise.
REGISTRY="docker.citysensor.de"
IMAGE_NAME="noise"
TAG="${1:-latest}"
PLATFORM="linux/amd64"
DOCKERFILE="Dockerfile_noise"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
BUILD_DATE=$(date +%d.%m.%Y)
echo "=========================================="
echo "noise (laerm_web) Deploy Script (amd64)"
echo "=========================================="
echo "Registry: ${REGISTRY}"
echo "Image: ${IMAGE_NAME}"
echo "Tag: ${TAG}"
echo "Platform: ${PLATFORM}"
echo "Dockerfile: ${DOCKERFILE}"
echo "Build-Datum: ${BUILD_DATE}"
echo "=========================================="
echo ""
# Hinweis: Login wird uebersprungen, weil die Credentials fuer ${REGISTRY}
# bereits in ~/.docker/config.json liegen.
# Bei Bedarf manuell: docker login ${REGISTRY}
echo ">>> Baue Image (nur ${PLATFORM}, nativ) ..."
docker build \
--platform "${PLATFORM}" \
-f "${DOCKERFILE}" \
-t "${FULL_IMAGE}" \
.
echo ""
echo ">>> Pushe ${FULL_IMAGE} ..."
docker push "${FULL_IMAGE}"
echo ""
# Falls ein anderer Tag als 'latest' gebaut wurde, zusaetzlich latest setzen+pushen
if [ "${TAG}" != "latest" ]; then
echo ">>> Tagge zusaetzlich als :latest ..."
docker tag "${FULL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
echo ""
fi
echo "=========================================="
echo "Build + Push fertig: ${FULL_IMAGE}"
echo "=========================================="
echo ""
echo "Danach Stack aktualisieren:"
echo " cd /opt/stacks/esprimo/noisesensors && docker compose up -d noise"
+5
View File
@@ -0,0 +1,5 @@
####Neues Sensor-API
* Programm **sensor_web_api**. Läuft auf Port 3000
* Aufruf über: **laerm.test.me:3000** (oder feinstaub.test... oder multigeiger.test...)
* Zugriff auf die DB mit dem Programm **SensorApi**. Dieses reagiert auf Port 3005 und muss im Hintergrund laufen!
+5
View File
@@ -0,0 +1,5 @@
Changes ab AUG-23
- i18n in /:sid in idex.js rein
- Messung und Anzeige auf Console der Ladezeit der 5 Pages im Hintergrund
+53
View File
@@ -0,0 +1,53 @@
{
"NoiseMeasurement": "Lärm-Messung",
"Settings": "Einstellungen",
"Info": "Info",
"StartDate": "Start Datum",
"NumberOfDays": "So viele Tage anzeigen",
"Only_for_live_chart": "Nur bei 'live' Grafik",
"Count_peaks_over": "Zähle Spitzen über",
"Close": "Schließen",
"Save_changes": "Änderungen speichern",
"Language": "Sprache",
"Map": "Karte",
"Live": "Live",
"Hour_AVG": "Stundenmittel",
"Day_AVG": "Tagesmittel",
"Day_Night": "Tag/Nacht-Werte",
"ActualValues": "Aktuelle Werte",
"NoiseLevelsLast24Hours": "Schallpegel der letzten 24 Stunden",
"ValuesFrom": "Werte vom",
"ActiveSensors": "Aktive Sensoren:",
"registered": "registriert:",
"ShowChart": "Grafik anzeigen",
"Sensor": "Sensor",
"offline": "offline",
"LastSeen": "Letzter Kontakt",
"datetime": "Datum/Zeit",
"25minlogAverageValues": "2.5min log. Mittelwerte",
"HourlyMeanValues": "Stündliche Mittelwerte",
"PeaksOver": "Spitzen über",
"HourlyMeanValuesOfLAeqForEachFullHour": "Stunden-Mittelwerte von LAeq jeweils für eine volle Stunde",
"NumberOfPeaksPerHour": "Anzahl der Spitzen pro Stunde",
"DailyMeanValues": "Tages-Mittelwerte",
"DailyMeanValuesOfLAeqForEachFullDay": "Tages-Mittelwerte von LAeq jeweils für einen vollen Tag",
"NumberOfPeaksPerDay": "Anzahl der Spitzen pro Tag",
"AverageValuesDuringTheDayAndNightHours": "Mittelwerte während der Tag- und Nachtstunden",
"DailyMeanValuesOfLAeqFrom6h00To22h00": "Tages-Mittelwerte des LAeq jeweils von 6:00 bis 22:00 Uhr",
"NightlyMeanValuesOfLAeqFrom22h00To6h00": "Nacht-Mittelwerte des LAeq jeweils von 22:00 bis 6:00 Uhr",
"LDEN-Index": "L<sub>DEN</sub>-Index",
"DailyMeanValuesOfLDEN-Index": "Tages-Mittelwerte des L<sub>DEN</sub>-Index",
"Date": "Datum",
"Version": "Version",
"from": "vom",
"RemoveFromMap": "Sensoren nicht anzeigen, die nicht gesendet haben seit",
"weeks": "Wochen",
"ESYSCALL": "Fehler vom Serveraufruf: ",
"Error": "Fehler",
"MeanValueLAeq": "Mittelwert LAeq",
"Peaks": "Spitzen",
"Day": "Tag",
"Night": "Nacht",
"CenterMap": "Karte zentrieren auf",
"MyLocation": "Mein Standort"
}
+47
View File
@@ -0,0 +1,47 @@
{
"NoiseMeasurement": "Lärm-Messung",
"Settings": "Einstellungen",
"Info": "Info",
"StartDate": "Start Datum",
"NumberOfDays": "So viele Tage anzeigen",
"Only_for_live_chart": "Nur bei 'live' Grafik",
"Count_peaks_over": "Zähle Spitzen über",
"Close": "Schließen",
"Save_changes": "Änderungen speichern",
"Language": "Sprache",
"Map": "Karte",
"Live": "Live",
"Hour_AVG": "Stundenmittel",
"Day_AVG": "Tagesmittel",
"Day_Night": "Tag/Nacht-Werte",
"ActualValues": "Aktuelle Werte",
"NoiseLevelsLast24Hours": "Schallpegel der letzten 24 Stunden",
"ValuesFrom": "Werte vom",
"ActiveSensors": "Aktive Sensoren:",
"registered": "registriert:",
"ShowChart": "Grafik anzeigen",
"Sensor": "Sensor",
"offline": "offline",
"LastSeen": "Letzter Kontakt",
"datetime": "Datum/Zeit",
"25minlogAverageValues": "2.5min log. Mittelwerte",
"HourlyMeanValues": "Stündliche Mittelwerte",
"PeaksOver": "Spitzen über",
"HourlyMeanValuesOfLAeqForEachFullHour": "Stunden-Mittelwerte von LAeq jeweils für eine volle Stunde",
"NumberOfPeaksPerHour": "Anzahl der Spitzen pro Stunde",
"DailyMeanValues": "Tages-Mittelwerte",
"DailyMeanValuesOfLAeqForEachFullDay": "Tages-Mittelwerte von LAeq jeweils für einen vollen Tag",
"NumberOfPeaksPerDay": "Anzahl der Spitzen pro Tag",
"AverageValuesDuringTheDayAndNightHours": "Mittelwerte während der Tag- und Nachtstunden",
"DailyMeanValuesOfLAeqFrom6h00To22h00": "Tages-Mittelwerte des LAeq jeweils von 6:00 bis 22:00 Uhr",
"NightlyMeanValuesOfLAeqFrom22h00To6h00": "Nacht-Mittelwerte des LAeq jeweils von 22:00 bis 6:00 Uhr",
"LDEN-Index": "L<sub>DEN</sub>-Index",
"DailyMeanValuesOfLDEN-Index": "Tages-Mittelwerte des L<sub>DEN</sub>-Index",
"Date": "Datum",
"Version": "Version",
"from": "vom",
"RemoveFromMap": "Sensoren nicht anzeigen, die nicht gesendet haben seit",
"weeks": "Wochen",
"CenterMap": "Center map on"
}
+53
View File
@@ -0,0 +1,53 @@
{
"NoiseMeasurement": "Noise Measurement",
"Settings": "Settings",
"Info": "Info",
"StartDate": "Start date",
"NumberOfDays": "Show so many days",
"Only_for_live_chart": "Only for 'live' chart",
"Count_peaks_over": "Count peaks over",
"Close": "Close",
"Save_changes": "Save changes",
"Language": "Language",
"Map": "Map",
"Live": "Live",
"Hour_AVG": "Hour_AVG",
"Day_AVG": "Day_AVG",
"Day_Night": "Day_Night",
"ActualValues": "Actual Values",
"NoiseLevelsLast24Hours": "Noise levels last 24 hours",
"ValuesFrom": "Values from ",
"ActiveSensors": "Active Sensors:",
"registered": "registered:",
"ShowChart": "Show chart",
"Sensor": "Sensor",
"offline": "offline",
"LastSeen": "Last seen",
"datetime": "Date/Time",
"25minlogAverageValues": "2.5min log average values",
"HourlyMeanValues": "Hourly mean values",
"PeaksOver": "Peaks over",
"HourlyMeanValuesOfLAeqForEachFullHour": "Hourly mean values of LAeq for each full hour",
"NumberOfPeaksPerHour": "Number of peaks per hour",
"DailyMeanValues": "Daily mean values",
"DailyMeanValuesOfLAeqForEachFullDay": "Daily mean values of LAeq for each full day",
"NumberOfPeaksPerDay": "Number of peaks per day",
"AverageValuesDuringTheDayAndNightHours": "Average values during the day and night hours",
"DailyMeanValuesOfLAeqFrom6h00To22h00": "Daily mean values of LAeq from 6h00 to 22h00",
"NightlyMeanValuesOfLAeqFrom22h00To6h00": "Nightly mean values of LAeq from 22h00 to 6h00",
"LDEN-Index": "L<sub>DEN</sub>-Index",
"DailyMeanValuesOfLDEN-Index": "Daily mean values of L<sub>DEN</sub>-Index",
"Date": "Date",
"Version": "Version",
"from": "from",
"RemoveFromMap": "Don't show sensors that have not sent for",
"weeks": "weeks",
"ESYSCALL": "Error from Syscall: ",
"Error": "Error",
"MeanValueLAeq": "Mean value LAeq",
"Peaks": "Peaks",
"Day": "Day",
"Night": "Night",
"CenterMap": "Center map on",
"MyLocation": "My Location"
}
+2117
View File
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
{
"name": "laerm-web-sensorapi",
"version": "3.3.1",
"date": "2026-06-30 18:00 UTC",
"private": true,
"scripts": {
"start": "node bin/www.js >>/var/log/noise.log 2>&1"
},
"type": "module",
"bin": {
"www": "./bin/www.js"
},
"dependencies": {
"axios": "^1.3.4",
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"dotenv": "^17.2.3",
"express": "^4.18.2",
"http-errors": "~1.6.3",
"i18next": "^22.4.14",
"i18next-express-middleware": "^2.0.0",
"i18next-http-middleware": "^3.3.0",
"i18next-fs-backend": "^2.0.0",
"luxon": "^3.3.0",
"morgan": "^1.10.1",
"pug": "^3.0.2",
"sass": "^1.58.3",
"spin.js": "^4.1.2"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+276
View File
@@ -0,0 +1,276 @@
// Utility routine for plotting the data
import {DateTime} from './luxon.min.js'
export const colors = {'eq': '#0000FF', 'max': '#FF0000', 'min': '#008000', 'peaks': '#DAA520'};
// Defaults für die Mittelwertbildungen
let txtMeldung = false; // falls keine Daten da sind, Text melden
const avgTime = 30; // defaul average time für particulate matter
let doUpdate = true; // update every 5 min
let optSidsArray = []; // Arrray der letzten 5 Einträge
let nbrofdaysforavg = 8; // Stundenmittel Anzahl Tagte (default = 5)
let nbrofdaysfordaynight = 30; // use 30 days or day/night graphic
let stucols = 1; // Anzeige nur Balken (StundenMittel)
let activeTab = 'maptab'; // active TAB ID
export const noise_ymin = 30; // lowest value on y-axis for noise
export const noise_ymax = 120; // highest value on y-axis for noise
export let peaklim = 70; // threshold for peak count
export const url = '/srv'
export function createGlobObtions() {
// Options, die für alle Plots identisch sind
let globObject = {
chart: {
accessibility: {
enabled: false
},
height: 600,
// width: 1000,
spacingRight: 20,
spacingLeft: 20,
spacingTop: 25,
backgroundColor: {
linearGradient: [0, 400, 0, 0],
stops: [
[0, '#eee'],//[0, '#ACD0AA'], //[0, '#A18D99'], // [0, '#886A8B'], // [0, '#F2D0B5'],
[1, '#fff']
]
},
type: 'line',
borderWidth: '2',
useHTML: true,
events: {
selection: function (event) {
if (event.xAxis) {
doUpdate = false;
} else {
doUpdate = true;
}
}
}
},
title: {
// text: 'Feinstaub über 1 Tag',
align: 'left',
style: {'fontSize': '25px'},
useHTML: true,
},
subtitle: {
// text: 'Gemessene Werte und ' + avgTime + 'min-gleitende Mittelwerte',
align: 'left',
},
tooltip: {
valueDecimals: 1,
backgroundColor: 0,
borderWidth: 0,
borderRadius: 0,
useHTML: true,
},
labels: {
useHTML: true
},
xAxis: {
type: 'datetime',
title: {
text: 'date/time',
},
gridLineWidth: 2,
labels: {
formatter: function () {
let v = this.axis.defaultLabelFormatter.call(this);
if (v.indexOf(':') == -1) {
return '<span style="font-weight:bold;color:red">' + v + '<span>';
} else {
return v;
}
}
},
},
legend: {
enabled: true,
layout: 'horizontal',
// verticalAlign: 'top',
borderWidth: 1,
align: 'center',
},
plotOptions: {
series: {
animation: false,
turboThreshold: 0,
marker: {
enabled: false,
},
},
}
};
return globObject;
}
export function calcWeekends(data, isyear) {
let weekend = [];
let oldDay = 8;
for (let i = 0; i < data.length; i++) {
let mom = moment(data[i].date);
if (isyear) {
mom = moment(data[i]._id)
}
let day = mom.day();
let st = mom.startOf('day');
if (day != oldDay) {
if (day == 6) {
weekend.push({
color: 'rgba(169,235,158,0.4)',
from: st.valueOf(),
to: st.add(1, 'days').valueOf(),
zIndex: 0
})
} else if (day == 0) {
weekend.push({
color: 'rgba(169,235,158,0.4)',
from: st.valueOf(),
to: st.add(1, 'days').valueOf(),
zIndex: 0
})
}
oldDay = day;
}
}
return weekend;
}
export function calcDays(data, isyear) {
let days = [];
if (data.length == 0) {
return days
}
let oldday = moment(data[0].date).day();
if (isyear) {
oldday = moment(data[0]._id).day();
}
for (let i = 0; i < data.length; i++) {
let m = moment(data[i].date);
if (isyear) {
m = moment(data[i]._id);
}
let tag = m.day()
if (tag != oldday) {
m.startOf('day');
days.push({color: 'lightgray', value: m.valueOf(), width: 1, zIndex: 2});
oldday = tag;
}
}
return days;
};
export async function addSensorID2chart(chart, sensor, width) {
let indoor = sensor.indoor || false
let addr = await addAddress(sensor.sid)
chart.renderer.label(
`Sensor: &nbsp;<span class="bigger">${sensor.sid}</span> ${indoor ? ' (indoor)' : ''}<br /><br />${addr.street}<br />${addr.plz} ${addr.city}`,
width / 2 - 150, 70)
.css({
fontSize: '14pt',
'font-weight': 'bold',
})
.attr({
zIndex: 3,
}).add();
}
export const showError = (err) => {
const dialog = document.getElementById('dialogError')
const body = dialog.querySelector('.dialog-body')
body.innerHTML = err
dialog.showModal()
}
export const showReset = () => {
const dialog = document.getElementById('dialogReset')
dialog.showModal()
}
// Setup dialog close buttons
document.addEventListener('DOMContentLoaded', () => {
const errorClose = document.querySelector('#dialogError .dialog-close')
if (errorClose) {
errorClose.addEventListener('click', () => {
document.getElementById('dialogError').close()
})
}
const resetClose = document.querySelector('#dialogReset .dialog-close')
if (resetClose) {
resetClose.addEventListener('click', () => {
document.getElementById('dialogReset').close()
})
}
const resetOk = document.querySelector('#btnResetOk')
if (resetOk) {
resetOk.addEventListener('click', () => {
document.getElementById('dialogReset').close()
})
}
})
// remove the taps (if shownig the map)
export const removeTabs = () => {
document.querySelector('#navi').style.display = 'none'
}
// show thw tabs again
export const showTabs = () => {
if (activeTab !== 'maptab') {
document.querySelector('#navi').style.display = 'block'
}
}
export const setCurrentTab = (tab) => {
activeTab = tab
}
export const getCurrentTab = () => {
return activeTab
}
export const fetchfromserver = async (url) => {
const ret = await fetch(encodeURI(url))
.catch(e => {
showError(e)
});
// return await ret.json()
let x = await ret.json()
return x
}
export const addAddress = async (sid) => {
let url = `/srv/getaddress?sensorid=${sid}`
let erg = await fetchfromserver(url)
if (!erg.err) {
return erg.address
} else {
return "no address"
}
}
export const cityCoords = async (city) => {
let url = `/srv/getcitycoords?city=${city}`
let erg = await fetchfromserver(url)
if (!erg.err) {
return erg.coords
} else {
return "no coordinates"
}
}
// table to distribute the different charts
export const ttIndex = Object.freeze({
map: 0,
live: 1,
havg: 2,
davg: 3,
daynight: 4,
lden: 5
})
+15
View File
@@ -0,0 +1,15 @@
// set correct language (will be called by chglang.pug)
let currentLang
const navLang = navigator.language.slice(0,2)
const selectedLang = localStorage.getItem('curlang')
if (!selectedLang) {
currentLang = navLang
} else {
currentLang = selectedLang
}
if (sensorid !== '') {
window.location.href = `/${sensorid}?lng=${currentLang}`
} else {
window.location.href = `?lng=${currentLang}`
}
+30
View File
@@ -0,0 +1,30 @@
// all date and time functions
import { DateTime } from './luxon.min.js'
import {loadAll} from './showcharts.js'
import {getCurrentTab} from "./chart_utilities.js";
// Show date and time (every minute)
// Will be called every second
export const showDate = (sofort, params) => {
let d = DateTime.now()
if(sofort || d.second === 0) {
document.querySelector('#h1datum').innerHTML = d.toFormat('yyyy-MM-dd HH:mm')
}
if (((d.minute % params.refreshRate) == 0) && (d.second == 15)) { // alle ganzen refreshRate Minuten, 15sec danach
let start = 1
console.log(params.refreshRate, 'Minuten um, Grafik wird erneuert');
let ct = getCurrentTab()
if (ct !== 'maptab') {
loadAll(params, start)
}
}
}
export const formatJSDate = (d) => {
return DateTime.fromJSDate(d).toFormat('yyyy-LL-dd HH:mm:ss')
}
export const formatISODate = (d) => {
return DateTime.fromISO(d).toFormat('yyyy-LL-dd HH:mm:ss')
}
+303
View File
@@ -0,0 +1,303 @@
// Client side javascript
"use strict"
import * as map from './map.js'
import * as dt from './datetime.js'
import {loadAll, showChart, setNoUTC, tabtable} from './showcharts.js'
import { DateTime } from './luxon.min.js'
import {ttIndex, setCurrentTab, getCurrentTab, showError, fetchfromserver, cityCoords} from "./chart_utilities.js";
import * as spin from './spinner.js'
(async () => {
// global variables
const params = { // set defaults
center: {name: 'Stuttgart', coords: [48.778, 9.180]},
zoom: 10,
span: 1,
peak: 70,
weeks: 4,
datetime: '',
seldate: '',
refreshRate: 5,
sid: -1
}
let minDate = new Date(2023, 0, 1)
const setting = [
{
typ: 'maptab',
show: ['ccity', 'odth'],
val: ['centercity', 'olderthan'],
param: [["center","name"], 'weeks']
},
{
typ: 'livetab',
show: ['stday', 'nbday'],
val: ['startday', 'nbrofdays'],
param: ['seldate', 'span']
},{
typ: 'houravgtab',
show: ['stday','pklim'],
val: ['startday', 'peaklim'],
param: ['seldate', 'peak']
},{
typ: 'dayavgtab',
show: ['stday','pklim'],
val: ['startday', 'peaklim'],
param: ['seldate', 'peak']
},{
typ: 'daynighttab',
show: ['stday'],
val: ['startday'],
param: ['seldate']
},{
typ: 'ldentab',
show: ['stday'],
val: ['startday'],
param: ['seldate']
}
]
// END global variables
// localStorage.clear()
// Check new map center
const checkNewCenter = (oldC, newC) => {
if((oldC[0] !== newC[0]) || (oldC[1] !== newC[1])) {
map.setNewCenter(newC)
}
}
// Register events
// Button 'My Location' pressed
document.addEventListener('DOMContentLoaded', () => {
const btnMyLocation = document.querySelector('#btnMyLocation');
if (btnMyLocation) {
btnMyLocation.addEventListener('click', () => {
map.goToMyLocation();
});
}
});
// Button 'Save' pressed
document.querySelector('#btnSave').addEventListener('click', async () => {
let curtab = getCurrentTab()
for(let i = 0; i < setting.length; i++) {
if(setting[i].typ === getCurrentTab()) {
for(let j = 0; j < setting[i].show.length; j++) {
let elem = document.getElementById(setting[i].val[j])
let val = elem.value
if(elem.type === 'checkbox') {
val = elem.checked
}
let x = setting[i].param[j]
console.log('x: ', x)
if ( Array.isArray(x)) {
params[setting[i].param[j][0]][setting[i].param[j][1]] = val
} else {
params[setting[i].param[j]] = val
}
}
break
}
}
if (curtab !== 'maptab') {
let starttime
if ((params.seldate === 'today') || (params.seldate === 'heute') || (DateTime.now().toFormat('yyyy-MM-dd') === params.seldate)) {
starttime = ''
} else {
starttime = DateTime.fromFormat(params.seldate, 'yyyy-MM-dd').toUTC().toISO()
}
params.datetime = starttime
} else {
const oldCoords = params.center.coords
params.center.coords = await cityCoords(params.center.name)
checkNewCenter(oldCoords, params.center.coords)
localStorage.setItem('centercity',JSON.stringify(params.center))
}
let newlng = document.querySelector('#sellan input:checked').id
let oldlng = localStorage.getItem('curlang')
localStorage.setItem('curlang', newlng)
if(newlng !== oldlng) {
location.reload()
}
document.getElementById('dialogSettings').close()
spin.spinner.spin(spin.spindiv)
await loadAll(params,0, curtab)
spin.spinner.stop()
// ToDo:
// Load ALL incl. MAP und LIVE hier, d.h. an loadAll einen zusätzlichen Parameter übergeben
// if(params.sid !== undefined) {
// await loadAll(params)
// }
})
// Btn 'Settings' pressed
document.querySelector('#btnSet').addEventListener('click', () => {
const curtab = getCurrentTab()
const dialog = document.getElementById('dialogSettings')
// Set max and min dates for date input
const today = new Date().toISOString().split('T')[0]
const minDateStr = minDate.toISOString().split('T')[0]
const startdayInput = document.getElementById('startday')
startdayInput.max = today
startdayInput.min = minDateStr
// Convert seldate format if needed
let seldate = params.seldate
if (seldate === 'today' || seldate === 'heute') {
seldate = today
}
startdayInput.value = seldate
document.getElementById('nbrofdays').value = params.span
document.getElementById('peaklim').value = params.peak
document.getElementById('olderthan').value = params.weeks
const centercity = JSON.parse(localStorage.getItem('centercity'))
document.getElementById('centercity').value = centercity.name
// select different infos depending on the selected tab
// first clear all
document.querySelector('#ccity').style.display = 'none'
document.querySelector('#stday').style.display = 'none'
document.querySelector('#nbday').style.display = 'none'
document.querySelector('#pklim').style.display = 'none'
document.querySelector('#odth').style.display = 'none'
for(let i = 0; i < setting.length; i++) {
if(setting[i].typ === curtab) {
for(let j = 0; j < setting[i].show.length; j++) {
document.querySelector(`#${setting[i].show[j]}`).style.display = 'inline'
}
break
}
}
dialog.showModal()
})
// Close button for settings dialog
document.querySelector('#dialogSettings .dialog-close').addEventListener('click', () => {
document.getElementById('dialogSettings').close()
})
// Close button (secondary button)
document.querySelector('#btnClose').addEventListener('click', () => {
document.getElementById('dialogSettings').close()
})
const getSensorType = async (sid) => {
let url = `/srv/getoneproperty?sensorid=${sid}`
const data = await fetchfromserver(url)
return data.props
}
// main function:
// show the map if called w/o any parameter
// if called with a sensor-ID, show the live chart for this sensor
async function main() {
setNoUTC() // no UTC for HighCharts
// set correct language
let currentLang = localStorage.getItem('curlang')
if ((currentLang === null) || (currentLang === '')) {
currentLang = 'de'
localStorage.setItem('curlang', currentLang)
}
let centercity
try {
centercity = JSON.parse(localStorage.getItem('centercity'))
} catch (e) {
centercity = params.center
}
if((centercity === null) || (centercity.name === '')) {
centercity = params.center
}
localStorage.setItem('centercity', JSON.stringify(centercity))
params.center = centercity
if(currentLang === 'en') {
document.querySelector('#en').checked = true
params.seldate = 'today'
} else {
document.querySelector('#de').checked = true
params.seldate = 'heute'
}
console.log(currentLang)
if (parseInt(sysparams.csid) !== -1) {
history.pushState({}, '', `/${sysparams.csid}`)
} else {
history.pushState({}, '', '/')
}
dt.showDate(true, params)
// and show date/time every minute
setInterval(() => dt.showDate(false, params), 1000)
// initialise tabs
const triggerTabList = [].slice.call(document.querySelectorAll('.tab-button'))
triggerTabList.forEach(function (triggerEl) {
triggerEl.addEventListener('click', function (event) {
event.preventDefault()
// Remove active from all tabs
triggerTabList.forEach(tab => tab.classList.remove('active'))
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'))
// Add active to clicked tab
triggerEl.classList.add('active')
const targetId = triggerEl.getAttribute('data-target')
document.getElementById(targetId).classList.add('active')
let newtab = event.currentTarget.id
setCurrentTab(newtab)
let tab = tabtable.find(tab => tab.id === newtab)
if (document.querySelector(`#${tab.id}err`).innerHTML !== '') { // if there is an error message, clear it
showError(document.querySelector(`#${tab.id}err`).innerHTML)
}
if (newtab === 'maptab') {
map.showMap(params)
}
})
})
const csid = parseInt(sysparams.csid)
if(csid !== -1) {
spin.spinner.spin(spin.spindiv)
// check, if sid is of right type (noise)
const props = await getSensorType(csid)
if (props.type !== 'noise') {
// wrong sensor type
spin.spinner.stop()
showError('Sensor ID ' + csid + ' is not of type noise')
return
}
params.center.coords = [props.location[0].loc.coordinates[1], props.location[0].loc.coordinates[0]]
params.sid = csid
let t = triggerTabList[1]
// Switch to live tab
triggerTabList.forEach(tab => tab.classList.remove('active'))
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'))
t.classList.add('active')
document.getElementById('t_live').classList.add('active')
setCurrentTab('livetab')
let err = await showChart(params, ttIndex.live)
spin.spinner.stop()
if (err.error) {
let errtxt = document.querySelector(`#${tabtable[ttIndex.live].id}err`).innerHTML
if (errtxt !== '') {
showError(errtxt)
}
}
await loadAll(params)
} else {
// show the map
map.showMap(params)
}
}
main().catch(console.error)
})()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+12
View File
@@ -0,0 +1,12 @@
import { DateTime } from './luxon.min.js'
export function logit(str) {
let s = `${DateTime.now().toISO()} => ${str}`;
console.log(s);
}
export function logerror(str) {
let s = `${DateTime.utc().toISO()} => *** ERROR *** ${str}`;
console.log(s);
}
File diff suppressed because one or more lines are too long
+99
View File
@@ -0,0 +1,99 @@
// all function related to the map
import * as utils from "./chart_utilities.js"
import * as mapn from './map_noise.js'
import * as mapu from './map_utilities.js'
const mapparams = {
map: null,
bounds: null,
popuptext: '',
clickedSensor: 0,
APIURL: '/srv/getmapdata?',
MAXZOOM: 19
}
export async function showMap(params) {
if(sysparams.category === 'noise') {
utils.removeTabs()
if(mapparams.map && mapparams.map.remove) {
mapparams.map.off();
mapparams.map.remove();
}
}
mapparams.map = L.map('map', {scrollWheelZoom: false}).setView(params.center.coords, parseInt(params.zoom))
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: mapparams.MAXZOOM,
attribution: '&copy; <a href="http://www.js.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(mapparams.map)
mapparams.bounds = mapparams.map.getBounds();
mapparams.map.on('moveend', async function () {
mapparams.bounds = mapparams.map.getBounds()
await buildMarkers(params, mapparams)
});
if(sysparams.category === 'noise'){
mapn.buildLegend(mapparams)
}
let lastdate = await buildMarkers(params, mapparams)
mapu.showLastDate(lastdate, mapparams);
}
const buildMarkers = async (params, mapparams) => {
return mapn.buildMarkers(params, mapparams)
}
export const setNewCenter = (center) => {
mapparams.map.setView(center)
}
export const goToMyLocation = () => {
if (!navigator.geolocation) {
alert('Geolocation wird von diesem Browser nicht unterstützt');
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
const accuracy = position.coords.accuracy;
// Karte zur aktuellen Position zentrieren
mapparams.map.setView([lat, lon], 15);
// Optional: Marker an aktueller Position setzen
const myLocationMarker = L.marker([lat, lon], {
icon: L.icon({
iconUrl: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0MCIgaGVpZ2h0PSI0MCI+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iOCIgZmlsbD0iIzQyODVGNCIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIzIi8+PGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMTUiIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQyODVGNCIgc3Ryb2tlLXdpZHRoPSIyIiBvcGFjaXR5PSIwLjMiLz48L3N2Zz4=',
iconSize: [40, 40],
iconAnchor: [20, 20]
})
}).addTo(mapparams.map);
myLocationMarker.bindPopup(`📍 Ihr Standort<br/>Genauigkeit: ±${Math.round(accuracy)}m`).openPopup();
},
(error) => {
let errorMsg = 'Position konnte nicht ermittelt werden.';
switch(error.code) {
case error.PERMISSION_DENIED:
errorMsg = 'Bitte erlauben Sie den Zugriff auf Ihren Standort.';
break;
case error.POSITION_UNAVAILABLE:
errorMsg = 'Standortinformationen sind nicht verfügbar.';
break;
case error.TIMEOUT:
errorMsg = 'Die Anfrage ist abgelaufen.';
break;
}
alert(errorMsg);
},
{
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
}
);
}
+210
View File
@@ -0,0 +1,210 @@
import * as dt from './datetime.js'
import * as utils from "./chart_utilities.js"
import { showChart, loadAll} from './showcharts.js'
import * as mapu from './map_utilities.js'
import * as spin from './spinner.js'
export const colorscale = ['#d53e4f', '#fc8d59', '#fee08b', '#e6f598', '#99d594', '#3288bd', '#808080'];
const dba = [100, 80, 60, 40, 20, 0, -999];
export function getColor(d) {
let val = parseInt(d);
for (let i = 0; i < dba.length; i++) {
if (val >= dba[i]) {
return (colorscale[i]);
}
}
}
export const buildLegend = (mpp) => {
let legend = L.control({position: 'bottomright'})
legend.onAdd = function () {
let div = L.DomUtil.create('div', 'info legend')
let div_color = L.DomUtil.create('div', 'info legend inner', div)
div_color.innerHTML += 'dbA<br />';
// loop through our density intervals and generate a label with a colored square for each interval
for (let i = 0; i < dba.length - 1; i++) {
div_color.innerHTML +=
'<i style="background:' + colorscale[i] + '"></i>' +
'&nbsp;&nbsp;' + dba[i] + (i == 0 ? '+' : '') + '<br />'
}
div_color.innerHTML += '&nbsp;<i style="background:' + getColor(dba[dba.length - 1]) + '"></i> offline'
return div
};
legend.addTo(mpp.map)
}
export async function buildMarkers(params, mpp) {
const url = mpp.APIURL + `type=noise&box=${mpp.bounds.toBBoxString()}`
let sensors = await utils.fetchfromserver(url)
if (!sensors.err) {
let markers = L.markerClusterGroup({
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
zoomToBoundsOnClick: true,
// disableClusteringAtZoom: 14,
iconCreateFunction: function (cluster) {
let mymarkers = cluster.getAllChildMarkers();
let color = getMedian(mymarkers);
return new L.Icon({
iconUrl: buildIcon(color, cluster.getChildCount(), 0),
iconSize: [35, 35]
});
},
});
for (let x of sensors.values) {
if ((params.weeks > 0) && (x.weeks > params.weeks)) {
continue
}
if (x.value <= -5) {
continue
}
let marker = L.marker([x.location[1], x.location[0]], {
icon: new L.Icon({
iconUrl: buildIcon(getColor(parseInt(x.value)), 0, x.indoor),
iconSize: [35, 35],
}),
name: x.id,
value: x.value,
url: '/graph?sid=' + x.id,
lastseen: dt.formatISODate(x.lastseen),
indoor: x.indoor
})
.on('click', e => onMarkerClick(e, true, sensors.popuptxt, params, mpp)) // define click- and
markers.addLayer(marker);
}
mpp.map.addLayer(markers);
return sensors.lastdate
} else {
utils.showError(sensors.err)
}
}
function buildIcon(color, n, indoor) {
let x = 100;
if (n < 10) {
x = 200;
} else if (n < 100) {
x = 150;
}
let txtColor = "black";
if (color == colorscale[5]) {
txtColor = "white"
}
let icon = '<svg xmlns="http://www.w3.org/2000/svg" width="600" height="600">' +
'<circle cx="300" cy="300" r="250" fill="' + color + '" stroke="black" stroke-width="20" />';
if (n !== 0) {
icon +=
'<text id="marker_text" x="' + x + '" ' +
'y="400" font-size="1500%" font-family="Verdana,Lucida Sans Unicode,sans-serif" ' +
'fill="' + txtColor + '">' + n + '</text>';
}
if(indoor) {
icon += '<line x1="125" y1="475" x2="475" y2="125" stroke="black" stroke-width="20" />'
}
icon += '</svg>';
return encodeURI("data:image/svg+xml," + icon).replace(new RegExp('#', 'g'), '%23');
}
async function bauPopupText(item, txts) {
let addr = ''
let offlinetext = `
<tr><td colspan="2"><span style="color:red;">${txts.offline}</span></td></tr>
<tr><td>${txts.lastseen}:</td><td>${item.lastseen}</td></tr>`
let normaltext = `
<tr></tr><tr><td>LA_max:</td><td>${item.value}</td></tr>`
let popuptext = `
<div id="infoTitle">
<h4>${txts.sensor}: ${item.name}</h4>
<div class="addr">${addr}</div>
<div id="infoTable">
<table>
<tr><td colspan="2"><span style="color:limegreen;">${item.indoor==1 ? "indoor" : ""}</span></td></tr>
${item.value < 0 ? offlinetext : normaltext}
</table>
<div id="infoBtn">
<a href="#" class="speciallink">${txts.showchart}</a>
</div>
</div>
</div>`
return popuptext
}
async function onMarkerClick(e, click, txts, params, mpp) {
let item = e.target.options;
let popuptext = await bauPopupText(item, txts)
e.target.bindPopup(popuptext).openPopup(); // show the popup
let addr = await utils.addAddress(item.name)
document.querySelector('#infoTitle .addr').innerHTML = `${addr.street}<br />${addr.plz} ${addr.city}`
let link = document.querySelector('.speciallink')
link.addEventListener('click', async function (ev) {
console.log(`Event clicked: ${mpp.clickedSensor}`)
params.sid = item.name
history.pushState({}, '', `/${params.sid}`)
utils.setCurrentTab('livetab')
spin.spinner.spin(spin.spindiv)
let ok = await showChart(params, utils.ttIndex.live)
spin.spinner.stop()
if (ok) {
// Switch to live tab
let triggerEl = document.querySelector(`#livetab`)
document.querySelectorAll('.tab-button').forEach(tab => tab.classList.remove('active'))
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active'))
triggerEl.classList.add('active')
document.getElementById('t_live').classList.add('active')
params.center.coords = [e.latlng.lat, e.latlng.lng]
ev.preventDefault()
await loadAll(params)
}
})
}
// With all Markers in cluster (markers) calculate the median
// of the values. With this median fetch the color and return it.
// If there are 'offline' sensors (value == -1) strip then before
// calculating the median. If there are only offline sensor, return
// color of value==-1 (dark gray).
function getMedian(markers, value) {
markers.sort(function (a, b) { // first sort, the lowest first
let y1 = a.options.value;
let y2 = b.options.value;
if (y1 < y2) {
return -1;
}
if (y2 < y1) {
return 1;
}
return 0;
});
// console.log(markers);
let i = 0; // now find the 'offlines' (value == -1)
for (i = 0; i < markers.length; i++) {
if (markers[i].options.value > 0) {
break;
}
}
markers.splice(0, i); // remove these from array
let lang = markers.length;
if (lang > 1) { //
if ((lang % 2) == 1) { // uneven ->
return getColor(markers[(Math.floor(lang / 2))].options.value); // median is in the middle
} else { // evaen ->
lang = lang / 2; // median is mean of both middle values
// console.log(lang);
let wert = (markers[lang - 1].options.value +
markers[lang].options.value) / 2;
return getColor(wert);
}
} else if (lang == 1) { // only one marker -> return its color
return getColor(markers[0].options.value);
}
return getColor(-1); // only offlines
}
+17
View File
@@ -0,0 +1,17 @@
import * as utils from "./chart_utilities.js"
import * as dt from './datetime.js'
// Show the last date below tha map grafics
export async function showLastDate(ld, mpp) {
let url
if(sysparams) {
url = mpp.APIURL + 'type=noise'
} else {
url = mpp.APIURL + 'type=radiactivity'
}
let sensors = await utils.fetchfromserver(url)
document.querySelector('#mapdate').innerText = `${sensors.valfromtxt} ${dt.formatISODate(ld).slice(0,-3)}`
document.querySelector('#actsensors').innerText = `${sensors.actsensors} ${sensors.registered}`
// $('#actsensors').html(`${erg.count} aktive Sensoren (angemeldet ${allsens})`)
}
+141
View File
@@ -0,0 +1,141 @@
import * as utils from "./chart_utilities.js";
import { DateTime } from './luxon.min.js'
import * as map from "./map.js";
import {fetchfromserver, showError} from "./chart_utilities.js";
import * as spin from './spinner.js'
export const tabtable = [
{id: 'maptab', type: 'map', container: 'map', func: map.showMap, div: null, dformat: null},
{id: 'livetab', type: 'live', container: 'dlive', func: showChart, div: 2, dformat: "dd.LL - HH:mm:ss"},
{id: 'houravgtab', type: 'havg', container: 'dhour', func: showChart, div: 1, dformat: "dd.LL - HH'h'"},
{id: 'dayavgtab', type: 'davg', container: 'dday', func: showChart, div: 1, dformat: "dd.LL"},
{id: 'daynighttab', type: 'daynight', container: 'ddaynight', func: showChart, div: 1, dformat: "dd.LL"},
{id: 'ldentab', type: 'lden', container: 'dlden', func: showChart, div: 1, dformat: "dd.LL"},
]
export async function showChart(params, index) {
let container = tabtable[index].container
let typ = tabtable[index].type
let id = tabtable[index].id
function form() {
for (let item of tabtable) {
if (item.type === typ) {
const d = DateTime.fromMillis(this.x)
const d1 = d.plus({days: 1})
let fmt = d.toFormat(item.dformat)
if (((typ === 'daynight') && (this.series.name.startsWith('N'))) || (typ === 'lden')) {
fmt = `${d.toFormat('dd')}/${d1.toFormat('dd.LL')}`
}
if (this.series.name === 'Peaks') {
item.div = 0
}
return '<div style="border: 2px solid ' + this.point.color + '; padding: 3px;">' +
fmt + '<br />' +
'<span style="color: ' + this.point.color + '">&#9679;&nbsp;</span>' +
this.series.name + ':&nbsp; <b>' +
Highcharts.numberFormat(this.y, item.div) +
'</b></div>'
}
}
}
function xformat() {
if (typ === 'lden') {
let lbl = this.axis.defaultLabelFormatter.call(this);
this.value += 86400000;
let lbl1 = this.axis.defaultLabelFormatter.call(this);
return parseInt(lbl) + '/' + lbl1;
} else if ((typ === 'live') || (typ === 'havg')) {
let v = this.axis.defaultLabelFormatter.call(this);
if (v.indexOf(':') == -1) {
return '<span style="font-weight:bold;color:red">' + v + '<span>';
} else {
return v;
}
} else {
return this.axis.defaultLabelFormatter.call(this);
}
}
let url = `/srv/getsensordata?sensorid=${params.sid}&data=${typ}&datetime=${params.datetime}`
if (typ === 'live') {
url += `&span=${params.span}`
} else if ((typ === 'havg') || (typ === 'davg')) {
url += `&peak=${params.peak}`
}
console.log(`fetch ${typ} from server ${url}`)
let errdiv = document.querySelector(`#${id}err`)
errdiv.innerHTML = ''
let erg = await fetchfromserver(url)
if(sysparams.category === 'noise') {
utils.showTabs()
}
if (!erg.err) {
erg.options.xAxis.labels.formatter = xformat
erg.options.tooltip.formatter = form
let chart = Highcharts.chart(container, erg.options, function (chart) {
utils.addSensorID2chart(chart, {
sid: erg.params.sid,
indoor: erg.params.indoor
}, document.getElementById(container).offsetWidth)
if(erg.info) {
let wbreit = window.innerWidth
let cbreit = chart.chartWidth
let infoposx = wbreit - ((wbreit-cbreit)/2) -200
let infoposy = chart.plotTop - 80
chart.renderer.label(
erg.info.text,
infoposx,
infoposy,
'rect', 0, 0, true)
.css({
fontSize: '10pt',
color: 'green'
})
.attr({
zIndex: 5,
}).add();
}
})
return {error: false}
} else {
errdiv.innerHTML = erg.err
return {error: true}
}
}
export const loadAll = async (params, start = 2, curtab = '') => {
console.log('now load all in Background')
let messzeit = DateTime.now()
if (start == 0) {
map.showMap(params)
start++
}
if (params.sid !== -1) {
for (let i = start; i < tabtable.length; i++) {
let err = await showChart(params, i)
if(tabtable[i].id === curtab) {
spin.spinner.stop()
let errtxt = document.querySelector(`#${tabtable[i].id}err`).innerHTML
if (errtxt !== '') {
showError(errtxt)
}
}
}
let dauer = DateTime.now().diff(messzeit,['seconds']).toObject().seconds
console.log(`now all loaded. Zeitdauer: ${dauer}`)
}
}
export const setNoUTC = () => {
Highcharts.setOptions({
time: {
useUTC: false // Don't use UTC on the charts
},
accessibility: { // supress warning concerning accessability
enabled: false
}
});
}
+27
View File
@@ -0,0 +1,27 @@
// Aufbau eine Spinner, der während des Ladens von AJAX-Requests angezeigt wird
import { Spinner } from '/spin.js/spin.js'
const spinneropts = {
lines: 13, // The number of lines to draw
length: 38, // The length of each line
width: 17, // The line thickness
radius: 45, // The radius of the inner circle
scale: 1, // Scales overall size of the spinner
corners: 1, // Corner roundness (0..1)
speed: 1, // Rounds per second
rotate: 0, // The rotation offset
animation: 'spinner-line-fade-quick', // The CSS animation name for the lines
direction: 1, // 1: clockwise, -1: counterclockwise
color: '#404040', // CSS color or array of colors
fadeColor: 'transparent', // CSS color or array of colors
top: '50%', // Top position relative to parent
left: '50%', // Left position relative to parent
shadow: '0 0 1px transparent', // Box-shadow for the lines
zIndex: 2000000000, // The z-index (defaults to 2e9)
className: 'spinner', // The CSS class to assign to the spinner
position: 'absolute', // Element positioning
};
export const spinner = new Spinner(spinneropts)
export const spindiv = document.getElementById('spinner')
@@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}
@@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}
+487
View File
@@ -0,0 +1,487 @@
body {
font-family: Verdana, "Lucida Sans Unicode", sans-serif;
font-size: 16px;
width: 100%;
margin-left: auto;
margin-right: auto;
height: 100%;
}
html {
height: 100%;
}
#wrapper {
margin: auto;
width: 100%;
text-align: center;
margin-top: 5px;
height: 100%;
position: relative;
}
a {
color: #0000EE;
}
header {
background-color: lightgray;
}
header #hline1 {
margin: 0px 1vw 5px 1vw;
padding-top: 10px;
padding-bottom: 10px;
overflow: auto;
}
header #h1name {
font-size: 148%;
clear: both;
float: left;
text-align: left;
}
header #buttonsRight {
float: right;
margin-right: 1%;
}
header #buttonsRight #btnSet {
margin-right: 20px;
}
header #buttonsRight #btnHelp {
margin-right: 20px;
}
header #buttonsRight #h1datum {
float: right;
margin-top: 5px;
}
#fenster {
margin-left: 0.5vw;
background: white;
padding-top: 5px;
width: 99vw;
}
#fenster #navi {
float: left;
margin-left: 0.5vw;
}
#map {
margin-left: 0.5vw;
margin-top: -50px;
width: 98vw;
height: 80vh;
border-radius: 10px;
border: solid 1px seagreen;
overflow: hidden;
z-index: 1;
position: relative;
}
#map #infoTable table, #map #infoTable tr, #map #infoTable td {
margin: auto;
text-align: center;
border: none !important;
}
#map #infoBtn {
margin-top: 10px;
text-align: center;
}
#map #btnMyLocation {
position: absolute;
top: 80px;
right: 10px;
z-index: 1000;
width: 40px;
height: 40px;
padding: 0;
border: 2px solid rgba(0,0,0,0.2);
border-radius: 4px;
background-color: white;
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
cursor: pointer;
font-size: 20px;
line-height: 1;
}
#map #btnMyLocation:hover {
background-color: #f4f4f4;
}
#map #btnMyLocation:active {
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
#mapdateactsens {
width: 98vw;
font-size: 70%;
padding: 10px 0 10px 0;
overflow: auto;
}
#mapdateactsens #mapdate, #mapdateactsens #actsensors {
width: 33%;
float: left;
}
#mapdateactsens #actsensors {
text-align: left;
margin-left: 1vw;
}
footer {
background-color: lightgray;
}
footer #author {
font-size: 80%;
width: 98vw;
margin: auto;
padding-bottom: 30px;
padding-top: 10px;
height: 50px;
}
footer #author #mailadr {
float: left;
}
footer #author #versn {
float: right;
}
.info {
padding: 6px 8px;
font: 12px Arial, Helvetica, sans-serif;
background: rgba(255, 255, 255, 0.8);
box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
border-radius: 5px;
}
.info h4 {
margin: 0 0 5px;
color: #777;
}
.legend {
line-height: 18px;
color: #555;
font-size: 110%;
}
.legend i {
width: 18px;
height: 18px;
float: left;
margin-right: 0px;
opacity: 1;
}
.leglabel {
padding-top: 20px;
}
.ndmarker {
position: absolute;
font-size: 24px;
}
.tab-pane {
margin-top: 44px;
}
#dlive, #dhour, #dday, #ddaynight, #dlden {
width: 100%;
margin: auto;
height: 80vh;
}
#dialogError {
color: red;
}
.modal-content {
border: red solid 2px;
}
.highcharts-tooltip {
zIndex: 5;
}
.settings {
text-align: left;
}
.thelabels {
width: 50%;
float: left;
}
.theInputs {
float: left;
}
.rows {
width: 100%;
display: block;
}
.column {
width: 100%;
display: inline-block;
margin-bottom: 15px;
}
#startday {
width: 40%;
}
#nbrofdays, #peaklim, #olderthan {
width: 13%;
}
.addit {
width: 20%;
border: 1px solid blue;
}
.klein {
font-size: 80%;
}
.infoTafel th, td {
margin: auto;
text-align: left;
padding: 0 10px;
border: 1px solid black;
background-color: rgba(238, 238, 238, 0.6);
}
.infoTafel th {
text-align: center;
}
.infoTafel tr {
height: 20px;
}
@media only screen and (max-width: 700px) {
.infoTafel, .legend {
display: none;
}
}
#stday, #nbday, #pklim, #odth {
display: none;
}
#first {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
}
.addr {
font-weight: bold;
margin-bottom: 10px;
}
.bigger {
font-size: 120%;
font-weight: bold;
}
.highcharts-container {
margin-top: -10px;
}
.errdiv {
display: none;
}
/* Button Styles */
button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
background-color: #f8f9fa;
color: #333;
cursor: pointer;
font-size: 14px;
font-family: inherit;
}
button:hover {
background-color: #e9ecef;
}
button:active {
background-color: #dee2e6;
}
#btnSet, #btnHelp {
background-color: #2D6AF8;
color: white;
border-color: #2D6AF8;
}
#btnSet:hover, #btnHelp:hover {
background-color: #1e5ae8;
}
#btnMyLocation {
background-color: white;
}
/* Tabs */
.tabs {
display: flex;
gap: 2px;
background-color: #f1f1f1;
border-bottom: 1px solid #ccc;
padding: 0;
margin: 0 0.5vw;
}
.tab-button {
background-color: #2D6AF8;
color: white;
border: none;
border-radius: 4px 4px 0 0;
padding: 10px 20px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.tab-button:hover {
background-color: #1e5ae8;
}
.tab-button.active {
background-color: white;
color: #2D6AF8;
border-bottom: 2px solid white;
}
.tab-content {
display: block;
}
.tab-pane {
display: none;
}
.tab-pane.active {
display: block;
}
/* Dialog Styles */
dialog {
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 0;
max-width: 600px;
width: 90%;
}
dialog::backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
.dialog-content {
display: flex;
flex-direction: column;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.dialog-title {
margin: 0;
font-size: 1.25rem;
font-weight: 500;
}
.dialog-close {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
color: #000;
opacity: 0.5;
}
.dialog-close:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.dialog-body {
padding: 20px;
overflow-y: auto;
max-height: 60vh;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 16px 20px;
border-top: 1px solid #dee2e6;
background-color: #f8f9fa;
}
.dialog-button {
padding: 8px 16px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.dialog-button.primary {
background-color: #2D6AF8;
color: white;
border-color: #2D6AF8;
}
.dialog-button.primary:hover {
background-color: #1e5ae8;
}
.dialog-button.secondary {
background-color: #6c757d;
color: white;
border-color: #6c757d;
}
.dialog-button.secondary:hover {
background-color: #5a6268;
}
/* Radio buttons */
.radio-group {
display: flex;
gap: 15px;
}
.radio-label {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
}
.radio-label input[type="radio"] {
margin: 0;
cursor: pointer;
}
/*# sourceMappingURL=style.css.map */
+1
View File
@@ -0,0 +1 @@
{"version":3,"sources":["style.sass","style.css"],"names":[],"mappings":"AAYA;EACE,uDAAA;EACA,eAAA;EACA,WAAA;EACA,iBAAA;EACA,kBAAA;EACA,YAAA;ACXF;;ADcA;EACE,YAAA;ACXF;;ADaA;EACE,YAAA;EACA,WAAA;EACA,kBAAA;EAEA,eAAA;EACA,YAAA;EACA,kBAAA;ACXF;;ADaA;EACE,cAAA;ACVF;;ADYA;EACE,2BA9BoB;ACqBtB;ADWE;EACE,uBAAA;EACA,iBAAA;EACA,oBAAA;EACA,cAAA;ACTJ;ADWE;EACE,eAAA;EACA,WAAA;EACA,WAAA;EACA,gBAAA;ACTJ;ADWE;EACE,YAAA;EACA,gBAAA;ACTJ;ADUI;EACE,kBAAA;ACRN;ADSI;EACE,kBAAA;ACPN;ADQI;EACE,YAAA;EACA,eAAA;ACNN;;ADQA;EACE,kBAAA;EACA,iBAAA;EACA,gBAAA;EACA,WAAA;ACLF;ADOE;EACE,WAAA;EACA,kBAAA;ACLJ;;ADOA;EACE,kBAAA;EACA,iBAAA;EACA,WAAA;EACA,YAAA;EACA,mBA7Ec;EA8Ed,0BAAA;EACA,gBAAA;EACA,UAAA;ACJF;ADME;EACE,YAAA;EACA,kBAAA;EACA,uBAAA;ACJJ;ADKE;EACE,gBAAA;EACA,kBAAA;ACHJ;;ADMA;EACE,WAAA;EACA,cAAA;EACA,sBAAA;EACA,cAAA;ACHF;ADKE;EACE,UAAA;EACA,WAAA;ACHJ;ADIE;EACE,gBAAA;EACA,gBAAA;ACFJ;;ADIA;EACE,2BAlGoB;ACiGtB;ADGE;EACE,cAAA;EACA,WAAA;EACA,YAAA;EACA,oBAAA;EACA,iBAAA;EACA,YAAA;ACDJ;ADGI;EACE,WAAA;ACDN;ADGI;EACE,YAAA;ACDN;;ADIA;EACE,gBAAA;EACA,uCAAA;EACA,oCAAA;EACA,uCAAA;EACA,kBAAA;ACDF;;ADGA;EACE,eAAA;EACA,WAAA;ACAF;;ADEA;EACE,iBAAA;EACA,WAAA;EACA,eAAA;ACCF;;ADCA;EACE,WAAA;EACA,YAAA;EACA,WAAA;EACA,iBAAA;EACA,UAAA;ACEF;;ADAA;EACE,iBAAA;ACGF;;ADDA;EACE,kBAAA;EACA,eAAA;ACIF;;ADFA;EACE,gBAAA;ACKF;;ADHA;EACE,WAAA;EACA,YAAA;EACA,YAAA;ACMF;;ADJA;EACE,UAAA;ACOF;;ADLA;EACE,qBAAA;ACQF;;ADNA;EACE,SAAA;ACSF;;ADNA;EACE,gBAAA;ACSF;;ADPA;EACE,UAAA;EACA,WAAA;ACUF;;ADPA;EAEE,WAAA;ACSF;;ADNA;EACE,WAAA;EACA,cAAA;ACSF;;ADPA;EACE,WAAA;EACA,qBAAA;EACA,mBAAA;ACUF;;ADRA;EACE,UAAA;ACWF;;ADTA;EACE,UAAA;ACYF;;ADVA;EACE,UAAA;EACA,sBAAA;ACaF;;ADXA;EACE,cAAA;ACcF;;ADZA;EACE,YAAA;EACA,gBAAA;EACA,eAAA;EACA,uBAAA;EACA,0CAAA;ACeF;;ADbA;EACE,kBAAA;ACgBF;;ADdA;EACE,YAAA;ACiBF;;ADfA;EACE;IACE,aAAA;ECkBF;AACF;ADjBA;EACE,yBArNa;EAsNb,WAAA;ACmBF;;ADjBA;EACE,cAzNa;EA0Nb,yBAAA;ACoBF;;ADjBA;EACE,aAAA;ACoBF;;ADfA;EACE,aAAA;EACA,mBAAA;EACA,uBAAA;EACA,kBAAA;ACkBF;;ADhBA;EACE,iBAAA;EACA,mBAAA;ACmBF;;ADjBA;EACE,eAAA;EACA,iBAAA;ACoBF;;ADlBA;EACE,iBAAA;ACqBF;;ADnBA;EACE,aAAA;ACsBF","file":"style.css"}
+257
View File
@@ -0,0 +1,257 @@
$corner-radius: 10px
$border-color: seagreen
$background: rgb(146, 154, 146)
$divcolor: rgb(162, 227, 162)
$table-stripes: rgb(157, 179, 157)
$fieldset-color: rgb(208, 235, 208)
$button-color: darkslategrey
$header_footer_color: lightgray
$checkboxsize: 1.5
$inboxwidth: 70px
$navtab-color: #2D6AF8
body
font-family: Verdana, 'Lucida Sans Unicode', sans-serif
font-size: 16px
width: 100%
margin-left: auto
margin-right: auto
height: 100%
// background-color: rgba(0,0,0,0.9)
html
height: 100%
#wrapper
margin: auto
width: 100%
text-align: center
// background-color: #e9e9e9
margin-top: 5px
height: 100%
position: relative
a
color: #0000EE
header
background-color: $header_footer_color
#hline1
margin: 0px 1vw 5px 1vw
padding-top: 10px
padding-bottom: 10px
overflow: auto
#h1name
font-size: 148%
clear: both
float: left
text-align: left
#buttonsRight
float: right
margin-right: 1%
#btnSet
margin-right: 20px
#btnHelp
margin-right: 20px
#h1datum
float: right
margin-top: 5px
#fenster
margin-left: 0.5vw
background: white
padding-top: 5px
width: 99vw
#navi
float: left
margin-left: 0.5vw
#map
margin-left: 0.5vw
margin-top: -50px
width: 98vw
height: 80vh
border-radius: $corner-radius
border: solid 1px $border-color
overflow: hidden
z-index: 1
#infoTable table, #infoTable tr, #infoTable td
margin: auto
text-align: center
border: none !important
#infoBtn
margin-top: 10px
text-align: center
#mapdateactsens
width: 98vw
font-size: 70%
padding: 10px 0 10px 0
overflow: auto
#mapdate, #actsensors
width: 33%
float: left
#actsensors
text-align: left
margin-left: 1vw
footer
background-color: $header_footer_color
#author
font-size: 80%
width: 98vw
margin: auto
padding-bottom: 30px
padding-top: 10px
height: 50px
#mailadr
float: left
#versn
float: right
.info
padding: 6px 8px
font: 12px Arial, Helvetica, sans-serif
background: rgba(255,255,255,0.8)
box-shadow: 0 0 15px rgba(0,0,0,0.2)
border-radius: 5px
.info h4
margin: 0 0 5px
color: #777
.legend
line-height: 18px
color: #555
font-size: 110%
.legend i
width: 18px
height: 18px
float: left
margin-right: 0px
opacity: 1
.leglabel
padding-top: 20px
.ndmarker
position: absolute
font-size: 24px
.tab-pane
margin-top: 44px
#dlive, #dhour, #dday, #ddaynight, #dlden
width: 100%
margin: auto
height: 80vh
#dialogError
color: red
.modal-content
border: red solid 2px
.highcharts-tooltip
zIndex: 5
.settings
text-align: left
.thelabels
width: 50%
float: left
// border: 1px solid green
.theInputs
//width: 60%
float: left
// border: 1px red solid
.rows
width: 100%
display: block
.column
width: 100%
display: inline-block
margin-bottom: 15px
#startday
width: 40%
#nbrofdays, #peaklim, #olderthan
width: 13%
.addit
width: 20%
border: 1px solid blue
.klein
font-size: 80%
.infoTafel th,td
margin: auto
text-align: left
padding: 0 10px
border: 1px solid black
background-color: rgba(238,238,238,0.6)
.infoTafel th
text-align: center
.infoTafel tr
height: 20px
@media only screen and (max-width: 700px)
.infoTafel, .legend
display: none
.nav-tabs .nav-item .nav-link
background-color: $navtab-color
color: #FFF
.nav-tabs .nav-item .nav-link.active
color: $navtab-color
background-color: #FFFFFF
// for settings: set all to 'none'
#stday, #nbday, #pklim, #odth
display: none
// for first page to show 'Loading ...'
#first
display: flex
align-items: center
justify-content: center
text-align: center
.addr
font-weight: bold
margin-bottom: 10px
.bigger
font-size: 120%
font-weight: bold
.highcharts-container
margin-top: -10px
.errdiv
display: none
+72
View File
@@ -0,0 +1,72 @@
import express from 'express'
import axios from 'axios'
import * as prep from '../charts/preparecharts.js'
import { getLanguage } from '../charts/utilities.js'
import { translate as trans } from '../routes/index.js'
const router = express.Router()
const disttable = [
{type: 'live', func: prep.liveData },
{type: 'havg', func: prep.havgData },
{type: 'davg', func: prep.davgData },
{type: 'daynight', func: prep.dayNightData },
{type: 'lden', func: prep.ldenData },
{type: 'map', func: prep.mapData },
]
let APIHOST = process.env.APIHOST || 'http://localhost:3005'
const API_KEY = process.env.API_KEY
/* GET home page. */
router.get('/:cmd', async function(req, res, next) {
// req.i18n.changeLanguage(req.query.lng)
const calledAs = req.baseUrl
const cmd = req.params.cmd
const pretty = (req.query.pretty !== undefined)
const lng = getLanguage()
let url = APIHOST + '/api' + req.originalUrl.slice(4) + `&lng=${lng}`
try {
const response = await axios.get(encodeURI(url) , {
headers: {'X-API-Key': API_KEY}
});
if (response.status !== 200) {
res.json({err: `${trans('ESYSCALL')} status=${response.status}`})
}
if(response.data.err) {
res.json(response.data)
return
}
// if called as '/api' then directly return the data
if ((calledAs === '/api') || (cmd === 'getoneproperty') || (cmd === 'getaddress') || (cmd === 'getcitycoords')) {
if(pretty) {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.send(JSON.stringify(response.data,null,2))
} else {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.json(response.data)
}
return
}
const options = response.data.options
for(let x of disttable) {
if (x.type === options.data) {
let ret = x.func(options, response.data.values, req)
res.json(ret)
break;
}
}
} catch (e) {
res.json({err: `${trans('ESYSCALL')} ${e}`})
}
})
export default router
// ToDo
// Hier : getsensordata und getmapdata unterbringen und damit den externen Server aufrufen
+69
View File
@@ -0,0 +1,69 @@
import express from 'express'
import pkg from '../package.json' with { type: "json" }
import { setLanguage } from '../charts/utilities.js'
const router = express.Router()
const { name, version, date } = pkg
const renderOpts = (sid, cat) => {
return {
title: 'Noise',
version: version,
date: date.slice(0, 10),
csensor: sid,
category: cat
}
}
let i18n;
const translate = (x) => {
return i18n.t(x)
}
const getIndex = (cat) => {
return 'index_noise'
}
/* GET home page. If there is no parameter 'lng=xx', check stored language and call
te page again with correct language (chglang.js))
*/
router.get('/', function (req, res, next) {
// req.i18n.changeLanguage('de')
if (req.query.lng === undefined) {
res.render('chglang', {sensorid: ''})
} else {
i18n = req.i18n
let opts = renderOpts(-1, req.app.get('category'))
setLanguage(req.query.lng)
res.render(getIndex(opts.category), opts)
}
})
//
// router.get('/', function(req, res, next) {
// // req.i18n.changeLanguage('de')
// res.render('index', renderOpts(-1))
// })
router.get('/:sid', async function (req, res, next) {
const sid = req.params.sid
let opts
if (req.query.lng === undefined) {
res.render('chglang', { sensorid: sid })
} else {
i18n = req.i18n
if (isNaN(sid)) {
opts = renderOpts(-1, req.app.get('category'))
} else {
opts = renderOpts(sid, req.app.get('category'))
}
res.render(getIndex(opts.category), opts)
}
})
//export default router
export { router, translate }
+13
View File
@@ -0,0 +1,13 @@
doctype html
html
head
title= title
meta(name="viewport" content="width=device-width, initial-scale=1" charset="utf-8")
link(rel='stylesheet', href='/stylesheets/style.css')
script.
const sensorid = '#{sensorid}';
body
#first
h1 Loading...
script(type="module" src="/javascripts/checklang.js")
+6
View File
@@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}
+114
View File
@@ -0,0 +1,114 @@
extends layout
block content
header
#hline1
#h1name #{t("NoiseMeasurement")}
#buttonsRight
button#btnSet(value='set') #{t("Settings")}
// button#btnHelp(value='help') #{t("Info")}
#h1datum
#fenster
#spinner
#navi
.tabs#tablist
button.tab-button.active#maptab(data-target="t_map" type="button" role="tab" aria-controls="map" aria-selected="true") #{t("Map")}
button.tab-button#livetab(data-target="t_live" type="button" role="tab" aria-controls="live") #{t("Live")}
button.tab-button#houravgtab(data-target="t_houravg" type="button" role="tab" aria-controls="houravg") #{t("Hour_AVG")}
button.tab-button#dayavgtab(data-target="t_dayavg" type="button" role="tab" aria-controls="dayavg") #{t("Day_AVG")}
button.tab-button#daynighttab(data-target="t_daynight" type="button" role="tab" aria-controls="daynight") #{t("Day_Night")}
button.tab-button#ldentab(data-target="t_lden" type="button" role="tab" aria-controls="lden") L<sub>DEN</sub>-Index
.tab-content#thetabs
.tab-pane.active#t_map(role="tabpanel")
#map
// button#btnMyLocation(type="button" title=t("MyLocation"))
span 📍
#maptaberr.errdiv
#mapdateactsens
#actsensors
#mapdate
.tab-pane#t_live(role="tabpanel")
#dlive
#livetaberr.errdiv
.tab-pane#t_houravg(role="tabpanel")
#dhour
#houravgtaberr.errdiv
.tab-pane#t_dayavg(role="tabpanel")
#dday
#dayavgtaberr.errdiv
.tab-pane#t_daynight(role="tabpanel")
#ddaynight
#daynighttaberr.errdiv
.tab-pane#t_lden(role="tabpanel")
#dlden
#ldentaberr.errdiv
// Error-Dialog
dialog#dialogError
.dialog-content
.dialog-header
h3.dialog-title #{t("Error")}
button.dialog-close(type="button" aria-label="Close") &times;
.dialog-body
.dialog-footer
// Settings-Dialog
dialog#dialogSettings
.dialog-content
.dialog-header
h3.dialog-title #{t("Settings")}
button.dialog-close(type="button" aria-label="Close") &times;
.dialog-body.settings
.rows
.column#ccity
label.thelabels(for='city') #{t("CenterMap")}:
input.theInputs#centercity(name="centercity")
.column#stday
label.thelabels(for='startday') #{t("StartDate")}:
input.theInputs#startday(type='date' name='startday')
.column
.column#nbday
label.thelabels(for='nbrofdays')
| #{t("NumberOfDays")}:<br />
input.theInputs#nbrofdays(type='number' name='nbrofdays' min="1" max="10")
span.klein &nbsp;&nbsp;(max. 10)
.column#pklim
label.thelabels(for='peaklim')
| #{t("Count_peaks_over")}
input.theInputs#peaklim(type='number' name='peaklim' min="10" max="130")
| &nbsp;&nbsp;dbA
span.klein &nbsp;&nbsp;(max. 130)
.column#odth
label.thelabels(for='olderthan')
| #{t("RemoveFromMap")}
input.theInputs#olderthan(type='number' name='olderthan' min="0" max="52")
| &nbsp;&nbsp;#{t("weeks")}
span.klein &nbsp;&nbsp;(max. 52)
.column
.column#sellan
label.thelabels
| #{t("Language")}:
.radio-group
label.radio-label
input#de(type="radio" name="lanbut")
span Deutsch
label.radio-label
input#en(type="radio" name="lanbut" checked)
span English
.dialog-footer
button.dialog-button.secondary#btnClose(type="button") #{t("Close")}
button.dialog-button.primary#btnSave(type="button") #{t("Save_changes")}
// Reset-Dialog
dialog#dialogReset
.dialog-content
.dialog-header
h3.dialog-title Info
button.dialog-close(type="button" aria-label="Close") &times;
.dialog-body
.dialog-footer
button.dialog-button.primary#btnResetOk(type="button") OK
+37
View File
@@ -0,0 +1,37 @@
doctype html
html
head
title= title
meta(name="viewport" content="width=device-width, initial-scale=1" charset="utf-8")
link(rel="stylesheet" href="https://unpkg.com/leaflet@1.9.3/dist/leaflet.css"
integrity="sha256-kLaT2GOSpHechhsozzB+flnD+zUyjE2LlfWPgU04xyI="
crossorigin="")
link(rel='stylesheet', href='/stylesheets/MarkerCluster.css')
link(rel='stylesheet', href='/spin.js/spin.css')
script.
const sysparams = {
version: '#{version}',
date: '#{date}',
csid: '#{csensor}',
category: '#{category}'
};
link(rel='stylesheet', href='/stylesheets/style.css')
body
#wrapper
block content
footer
#author
#mailadr
a(href="mailto:rexfue@gmail.com") mailto:rexfue@gmail.com
#versn #{t('Version')}: #{version} #{t('from')} #{date}
script(src="https://unpkg.com/leaflet@1.9.3/dist/leaflet.js"
integrity="sha256-WBkoXOwTeyKclOHuWtc+i2uENFpDZ9YPdf5Hf+D7ewM="
crossorigin="")
script(src="/javascripts/leaflet.markercluster.js")
script(src="https://code.highcharts.com/highcharts.js")
script(type="module" src="/javascripts/global.js")
+10
View File
@@ -0,0 +1,10 @@
node-modules
.gitignore
.dockerignore
build_and_copy.sh
docker-compose.yml
Dockerfile*
INFLUXSERVER
TRANSFERE
data
+9
View File
@@ -0,0 +1,9 @@
data/
node_modules
.idea
.env
log
docs/Readme.md_x
docs/Readme_tmp.html
+28
View File
@@ -0,0 +1,28 @@
FROM node:22-alpine
ADD package.json /tmp/package.json
ADD package-lock.json /tmp/package-lock.json
RUN cd /tmp && npm ci
RUN mkdir -p /opt/app && cp -a /tmp/node_modules /tmp/package.json /opt/app/
WORKDIR /opt/app
ADD . /opt/app
ADD crontab.tmp /opt/app
RUN mkdir -p data
#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 crontab crontab.tmp
RUN deluser --remove-home node
RUN touch cmds.sh \
&& echo 'crond -f' >>cmds.sh
CMD sh ./cmds.sh
+27
View File
@@ -0,0 +1,27 @@
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
ADD crontab.tmp /opt/app
RUN mkdir -p data
#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 crontab crontab.tmp
RUN deluser --remove-home node
RUN touch cmds.sh \
&& echo 'crond -f' >>cmds.sh
CMD sh ./cmds.sh
+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=readin
name=readin
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'
+2
View File
@@ -0,0 +1,2 @@
3-59/5 * * * * cd /opt/app && npm start
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -e
# Angepasst aus /Projekte/Logbuch/deploy.sh
# Unterschiede: nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
# eigener Dockerfile-Name (Dockerfile_readin).
REGISTRY="docker.citysensor.de"
IMAGE_NAME="readin"
TAG="${1:-latest}"
PLATFORM="linux/amd64"
DOCKERFILE="Dockerfile_readin"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
BUILD_DATE=$(date +%d.%m.%Y)
echo "=========================================="
echo "readin Deploy Script (amd64)"
echo "=========================================="
echo "Registry: ${REGISTRY}"
echo "Image: ${IMAGE_NAME}"
echo "Tag: ${TAG}"
echo "Platform: ${PLATFORM}"
echo "Dockerfile: ${DOCKERFILE}"
echo "Build-Datum: ${BUILD_DATE}"
echo "=========================================="
echo ""
# Hinweis: Login wird uebersprungen, weil die Credentials fuer ${REGISTRY}
# bereits in ~/.docker/config.json liegen (Container laufen bereits aus dieser Registry).
# Bei Bedarf manuell: docker login ${REGISTRY}
echo ">>> Baue Image (nur ${PLATFORM}, nativ) ..."
docker build \
--platform "${PLATFORM}" \
-f "${DOCKERFILE}" \
-t "${FULL_IMAGE}" \
.
echo ""
echo ">>> Pushe ${FULL_IMAGE} ..."
docker push "${FULL_IMAGE}"
echo ""
# Falls ein anderer Tag als 'latest' gebaut wurde, zusaetzlich latest setzen+pushen
if [ "${TAG}" != "latest" ]; then
echo ">>> Tagge zusaetzlich als :latest ..."
docker tag "${FULL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
echo ""
fi
echo "=========================================="
echo "Build + Push fertig: ${FULL_IMAGE}"
echo "=========================================="
echo ""
echo "Danach Stack aktualisieren:"
echo " cd /opt/stacks/esprimo/noisesensors && docker compose up -d readin"
+55
View File
@@ -0,0 +1,55 @@
version: '3.9'
volumes:
mongo_vol:
influx_vol:
services:
timeseries:
image: timeseries
environment:
DEVELOP: "true"
MONGOHOST: mongodb
MONGOUSRP: "rexfue:noise4mongo"
MONGOAUTH: "true"
INFLUXHOST: influxdb
INFLUXTOKEN: "q35XUBaElzcy8dDd9HF2_mpeHvYCampZg_9mJNP5jeBQRopq3EWIzNTZ555QLSIXhZC05RXCoFgjiaT7VzyNkQ=="
TYP: "[\"noise\", \"thp\"]"
STORE: "mongo"
volumes:
- ${PWD}/log:/var/log
container_name: timeseries
restart: unless-stopped
mongodb:
image: mongo:6
volumes:
- ${PWD}/entries:/docker-entrypoint-initdb.d
- mongo_vol:/data/db
ports:
- "27017:27017"
container_name: mongodb
environment:
- MONGO_INITDB_DATABASE=sensor_data
- MONGO_INITDB_ROOT_USERNAME=${MONGO_ROOT_USERNAME}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD}
restart: unless-stopped
influxdb:
image: influxdb:2.0
ports:
- '8086:8086'
volumes:
- influx_vol:/var/lib/influxdb2
environment:
DOCKER_INFLUXDB_INIT_MODE: setup
DOCKER_INFLUXDB_INIT_USERNAME: ${DOCKER_INFLUXDB_INIT_USERNAME}
DOCKER_INFLUXDB_INIT_PASSWORD: ${DOCKER_INFLUXDB_INIT_PASSWORD}
DOCKER_INFLUXDB_INIT_ORG: citysensor
DOCKER_INFLUXDB_INIT_BUCKET: sensor_data
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: ${DOCKER_INFLUXTOKEN}
restart:
unless-stopped
container_name: influxdb
+141
View File
@@ -0,0 +1,141 @@
// Einlesen aller Sensordaten von sensor.comunity und abspeicher in einer Inlux- bzw. einer Mongo-DB
// Ausgehend von Version 1.x.x werden hier nun die Daten nach den Sensortypen getrennt gespeichert
// und zwar einstellbar in einer Mongo-DB oder in einer Influx-DB (oder auch in beide).
// Version:
//
// V 2.0.0 2023-10-13 rxf
// - Erste Version mit der Trennung nach Typen
//
// V 2.0.1 2023-10-15 rxf
// - Auswahl der Typen via Commandline (-t) oder Environment TYP
// TYP: Ist ein Array von Strings, wenn nur einzelne Typen gespeichert werden sollen, also z.B.:
// ['pm', 'noise']
const TYP = process.env.TYP || ''
const STORE = (process.env.STORE || 'both').toLowerCase() // 'mongo' | 'influx' | 'both'
import { doReadfromAPI as readin, statistics } from './readdata.js'
import { constructDBaseEntries as parse} from './parse.js'
import * as mongo from './mongo.js'
import * as influx from './influx_post.js'
import { logit, logerror } from './logit.js'
import { DateTime } from 'luxon'
import fs from 'fs'
import mod_getopt from 'posix-getopt'
import pkg from './package.json' with { type: "json" }
// import nodeSchedule from 'node-schedule'
const fetchNewData = async (args) => {
let client
let start = DateTime.now();
try {
client = await mongo.connectMongo()
} catch(e) {
logerror(`Connect to Mongo ${e}`);
logit(`Programmend - Error in connecting mongo\n`)
process.exit(-1);
}
try {
// read data from API or from disk
let dat = await readin()
if (dat.length !== 0) {
// parse the data
let [props, data, idata] = await parse(client, dat, args)
// write sensor data to mongoDB
if(args.mongo) {
for (const [k,v] of Object.entries(data)) {
if(v.length !== 0) {
await mongo.writeDataArray(client, k+'_sensors', v)
}
}
}
// write sensor data to influxDB
if(args.influx) {
await influx.influxWrite(idata)
}
// write properties to mongoDB
await mongo.bulkWrite(client, mongo.property_coll, props)
}
} catch (e) {
logerror(`Catch in main ${e}`);
} finally {
statistics.totalTime = DateTime.now().diff(start,['seconds']).toObject().seconds
await mongo.writeStatistic(client, statistics)
await mongo.closeMongo(client)
}
}
// Parse command line options
function parse_cmdline(argv) {
let parser = new mod_getopt.BasicParser('i(influx)m(mongo)t:(typ)h(help)v(version)',argv);
let option;
let ret = {influx: STORE === 'both' || STORE === 'influx', mongo: STORE === 'both' || STORE === 'mongo', typ: TYP}
while((option = parser.getopt()) !== undefined) {
switch(option.option) {
case 'i':
ret.mongo = false
break;
case 'm':
ret.influx = false
break;
case 't':
let x = option.optarg.trim()
let y = []
if(x[0] === '[') {
y = JSON.parse(x)
} else {
y.push(x)
}
ret.typ = y
break;
case 'v':
console.log(`Version: ${pkg.version} from ${pkg.date}`);
console.log();
process.exit();
break;
case 'h':
console.log("Usage: node fetchnewdata.js [-i] [-m] [-t [typ, typ, ..]] [-v] [-h]");
console.log("Params:");
console.log(" -i use only InfluxDB to store the data (default use both, InfluxDB and MongoDB))");
console.log(" -m use only MongoDB to store the data (default use both, InfluxDB and MongoDB)");
console.log(" -t [ sensorType, ..]: if given, only those sensors will be used (ex: laerm) default: all");
console.log(" MUST BE AN ARRAY!; allowed types: 'pm', 'noise', 'radiactivity', 'thp', 'gps'.")
console.log(" -v version: show version");
console.log(" -h this help text");
console.log("All parameters are optional.");
console.log();
process.exit();
break;
default:
break;
}
}
logit(JSON.stringify(ret));
return ret;
}
const main = async () => {
const json = JSON.parse(fs.readFileSync('package.json', 'utf8'))
logit(`Programmstart V ${json.version} vom ${json.date}.`);
let args = parse_cmdline(process.argv);
await fetchNewData(args)
logit(`Program end - running time: ${statistics.totalTime} sec\n\n`)
}
main().catch(console.error)
+100
View File
@@ -0,0 +1,100 @@
// Access to influxDB vie HTTP
import axios from 'axios'
import { logit, logerror } from './logit.js'
import { DateTime } from 'luxon'
import { statistics } from'./readdata.js'
let DEVELOP = process.env.DEVELOP || 'false'
let INFLUXHOST = process.env.INFLUXHOST || "localhost"
let INFLUXPORT = process.env.INFLUXPORT || 8086
let INFLUXTOKEN = process.env.INFLUXTOKEN || 'empty'
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}&precision=ms`
export const influxRead = async (query) => {
let start = DateTime.now()
let data = []
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) {
logerror(`doReadfromAPI Status: ${ret.status}`)
}
data = ret.data
} catch (e) {
logerror(`doReadfromAPI ${e}`)
}
logit(`ReadIn-Time: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
return data
}
export const influxWrite = async (data) => {
let start = DateTime.now()
let ret
logit(INFLUXURL_WRITE)
if (DEVELOP === 'true') {
logit(`Token: ${INFLUXTOKEN}`)
}
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}`)
}
let statname = `writeInfluxData[sensor_data]Time`
statistics[statname] = DateTime.now().diff(start, ['seconds']).toObject().seconds
logit(`Influx-Write-Time: ${start.diffNow('seconds').toObject().seconds * -1} sec`)
return ret
}
/*
async function main() {
let data = `
pm,sid=140 P1=12,P2=13
pm,sid=142 P1=42,P2=13
pm,sid=143 P1=43,P2=13
pm,sid=144 P1=44,P2=13
thp,sid=141 temperature=23.5,humidity=48,pressure=998
`
let ret = await influxWrite(data)
process.exit()
let query = `from(bucket:"sensor_data")
|> range(start: -1mo)
|> filter(fn: (r) => r._measurement == "pm")
|> filter(fn: (r) => r.sid == "140")
`
let erg = await influxRead(query)
console.log(erg)
}
main().catch(console.error)
*/
+12
View File
@@ -0,0 +1,12 @@
import { DateTime} from 'luxon'
export function logit(str) {
let s = `${DateTime.now().toISO()} => ${str}`;
console.log(s);
}
export function logerror(str) {
let s = `${DateTime.utc().toISO()} => *** ERROR *** ${str}`;
console.log(s);
}
+202
View File
@@ -0,0 +1,202 @@
/* Interface for MongoDB
*/
import { MongoClient } from 'mongodb'
import { logit, logerror } from './logit.js'
import { statistics } from './readdata.js'
import { DateTime } from 'luxon'
let DEVELOP = process.env.DEVELOP || 'false'
let MONGOHOST = process.env.MONGOHOST || 'localhost'
let MONGOPORT = process.env.MONGOPORT || 27017
let MONGOAUTH = process.env.MONGOAUTH || 'false'
let MONGOUSRP = process.env.MONGOUSRP || ''
let MONGOBASE = process.env.MONGOBASE || 'sensor_data'
let MONGO_URL = 'mongodb://' + MONGOHOST + ':' + MONGOPORT; // URL to mongo database
if (MONGOAUTH == 'true') {
MONGO_URL = 'mongodb://' + MONGOUSRP + '@' + MONGOHOST + ':' + MONGOPORT + '/?authSource=admin'; // URL to mongo database
}
export const property_coll = 'properties'
const addandshowstatistics = (client, text, field, start) => {
statistics[field] = DateTime.now().diff(start, ['seconds']).toObject().seconds
logit(`Write ${text} to mongoDB: Time: ${statistics[field]} sec.`)
}
export const connectMongo = async () => {
try {
if(DEVELOP === 'true') {
logit(`Try to connect to ${MONGO_URL}`)
} else {
logit(`Try to connect to ${'mongodb://' + MONGOHOST + ':' + MONGOPORT}`)
}
let client = await MongoClient.connect(MONGO_URL, { useNewUrlParser: true, useUnifiedTopology: true })
if ( DEVELOP === 'true') {
logit(`Mongodbase connected to ${MONGO_URL}`)
} else {
logit('Mongodbase connected')
}
return client
}
catch (error) {
throw (error)
}
}
export const closeMongo = async (client) => {
client.close()
}
export const getallProperties = async (client) => {
return await client.db(MONGOBASE).collection(property_coll)
.find().sort({ _id: 1 }).toArray()
}
export const checkOneproperty = async (client, sid) => {
return await client.db(MONGOBASE).collection("properties")
.findOne({ _id: sid })
}
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 result
let startAll = DateTime.now();
let start
let coll = client.db(MONGOBASE).collection("properties")
if (props.new.length !== 0) {
start = DateTime.now();
try {
result = await coll.insertMany(props.new)
} catch (e) {
logerror(`Write properties new ${e}`)
}
logit(`Write ${props.new.length} properties NEW to mongoDB: Result: ${result.acknowledged}, Time: ${start.diffNow('seconds').toObject().seconds * -1} sec.`)
}
if (props.loc.length !== 0) {
start = DateTime.now()
try {
for (let item of props.loc) {
result = await coll.updateOne({ _id: item._id }, {
$set: { location_id: item.location_id },
$push: { location: { $each: [item.location[0]], $position: 0 } }
})
}
} catch (e) {
logerror(`Write properties location ${e}`)
}
logit(`Write ${props.loc.length} properties LOC to mongoDB: Result: ${result.acknowledged}, Time: ${start.diffNow('seconds').toObject().seconds * -1} sec.`)
}
if (props.sname.length !== 0) {
start = DateTime.now()
try {
for (let item of props.sname) {
result = await coll.updateOne({ _id: item._id }, {
$push: { name: { $each: [item.name[0]], $position: 0 } }
})
}
} catch (e) {
logerror(`Write properties samename ${e}`)
}
logit(`Write ${props.sname.length} properties NAME to mongoDB: Result: ${result.acknowledged}, Time: ${start.diffNow('seconds').toObject().seconds * -1} sec.`)
}
addandshowstatistics(client, 'properties', 'writePropsTime', startAll)
}
export const writeDataArray = async (client, coll, data) => {
let result
let start = DateTime.now();
try {
result = await client.db(MONGOBASE).collection(coll)
.insertMany(data, { ordered: false })
} catch (e) {
if (e.code !== 11000) {
console.error(e)
}
}
let statname = `writeMongoData[${coll}]Time`
addandshowstatistics(client, `${data.length} entries for ${coll}`, `writeMongoData[${coll}]Time`, start)
// statistics[statname] = DateTime.now().diff(start, ['seconds']).toObject().seconds
// logit(`Write Data for ${coll} to mongoDB: Time: ${statistics[statname]} sec.`)
}
export const writeStatistic = async (client, stat) => {
let result
let start = DateTime.now();
let entry = { timestamp: new Date(), ...stat }
try {
result = await client.db(MONGOBASE).collection("statistics")
.insertOne(entry)
} catch (e) {
console.error(e)
}
addandshowstatistics(client, `statistics`, `writeStatisticTime`, start)
}
export const dropColl = async (client, coll) => {
let start = DateTime.now()
let result
try {
result = await client.db(MONGOBASE).collection(coll).drop()
} catch (e) {
console.error(e)
}
logit(`Drop collection ${coll}: Result: ${result}, Time: ${start.diffNow('second').toObject().seconds * -1} sec.`)
}
export const createIndex = async (client, coll) => {
let result
let start = DateTime.now()
try {
result = await client.db(MONGOBASE).collection(coll).createIndex({ "location.loc": "2dsphere" })
} catch (e) {
console.error(e)
}
logit(`Create-Index: Result: ${result}, Time: ${start.diffNow('second').toObject().seconds * -1} sec.`)
}
export const bulkUpdateMapdata = async (client, data) => {
let start = DateTime.now()
let result
try {
result = await client.db(MONGOBASE).collection("mapdata")
.bulkWrite(data, { ordered: false })
} catch (e) {
console.error(e)
}
logit(`Write MapData: Result: ${result}, Time: ${start.diffNow('second').toObject().seconds * -1} sec.`)
return result
}
export const bulkWrite = async (client, coll, data) => {
let start = DateTime.now()
let result
try {
result = await client.db(MONGOBASE).collection(coll)
.bulkWrite(data, { ordered: false })
} catch (e) {
console.error(e)
}
addandshowstatistics(client, `Data for ${coll}`, `writeMongoProperties[${coll}]Time`, start)
return result
}
+1020
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "timeseries_mongo",
"version": "2.0.2",
"date": "2023-12-17",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node fetchnewdata.js >>/var/log/readin.log 2>&1"
},
"type": "module",
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.12.0",
"luxon": "^2.3.1",
"mongodb": "^4.4.1",
"posix-getopt": "^1.2.1"
}
}
+243
View File
@@ -0,0 +1,243 @@
// import logit from './logit.js'
import { DateTime } from 'luxon'
import * as mongo from './mongo.js'
import { statistics } from'./readdata.js'
import { logit, logerror } from './logit.js'
let actualProps = []
let newProps = []
// Check lat/lon and convert to float
function checkLatLon(w) {
let loc = 0.0;
if (!((w === undefined) || (w == null) || (w == ''))) {
try {
loc = parseFloat(w)
} catch (e) {
logerror(`Math error with lat/lon, ${e}`)
}
}
return loc
}
// Check, if altitude is there. If so, use it, else use 0
function checkAltitude(alt) {
let altitude = 0;
if(!((alt === undefined) || (alt === ''))) {
try {
altitude = Math.floor(parseFloat(alt))
} catch (e) {
logerror(`Math error with altitude, ${e}`)
}
}
return altitude
}
// binary search fir sensor id in property array
const binarySearch = (arr, element, x) => {
let start = 0, end = arr.length - 1
// Iterate while start not meets end
while (start <= end) {
// Find the mid index
let mid = Math.floor((start + end) / 2)
// If element is present at mid, return True
if (arr[mid][element] === x) return mid
// Else look in left or right half accordingly
else if (arr[mid][element] < x)
start = mid + 1
else
end = mid - 1
}
return -1
}
const checkProperties = (item, mapvalues, typ, dt) => {
let now = DateTime.utc().toJSDate()
let entry
// read entry from actualprops
let idx = binarySearch(actualProps, '_id', item.sensor.id)
if (idx === -1) { // not in properties => new sensor
entry = buildNewEntry(item, typ, dt, now) // so build a new entry
} else {
entry = actualProps[idx] // get actual properties
// check for change of location
if (entry.location[0].id !== item.location.id) { // have got a new location
if (newProps.findIndex((obj) => {
return (obj.replaceOne.replacement.location[0].id === item.location.id)
}) === -1) {
const newloc = {
loc: {
type: "Point",
coordinates: [
checkLatLon(item.location.longitude),
checkLatLon(item.location.latitude)
]
},
id: item.location.id,
altitude: checkAltitude(item.location.altitude),
since: now,
exact_loc: item.location.exact_location,
indoor: item.location.indoor,
country: item.location.country
}
entry.location.splice(0, 0, newloc) // insert new location at pos 0 in location array
}
} else { // same location check for change in indoor or exact_location
if (entry.location[0].indoor !== item.location.indoor) {
entry.location[0].indoor = item.location.indoor // update indoor
}
if (entry.location[0].exact_loc !== item.location.exact_location) {
entry.location[0].exact_loc = item.location.exact_location // update exact_location
}
if (entry.location[0].country === '') {
entry.location[0].country = item.location.country // update country
}
}
// Check für new name
if (entry.name[0].name !== item.sensor.sensor_type.name) { // have got a new name
if (newProps.findIndex((obj) => {
return (obj.replaceOne.replacement.name[0].name === item.sensor.sensor_type.name)
}) === -1) {
let newname = {
name: item.sensor.sensor_type.name,
since: now
}
entry.name.splice(0, 0, newname)
}
}
}
// set new mapvalues
entry.values = mapvalues
delete entry.location_id
delete entry.last_seen
delete entry.since
// push this entry to the new proerties array
newProps.push({replaceOne: { filter: {_id: entry._id}, replacement: entry, upsert: true}})
}
const buildNewEntry = (item, typ, dt, now) => {
return {
_id: item.sensor.id,
type: typ,
name: [{
name: item.sensor.sensor_type.name,
since: now,
}],
location: [{
loc: {
type: "Point",
coordinates: [
checkLatLon(item.location.longitude),
checkLatLon(item.location.latitude)
]
},
id: item.location.id,
altitude: checkAltitude(item.location.altitude),
since: now,
exact_loc: item.location.exact_location,
indoor: item.location.indoor,
country: item.location.country
}]
}
}
const types = {
P1: 'pm', P2: 'pm', P0: 'pm',
temperature: 'thp', humidity: 'thp', pressure: 'thp',
noise_LAeq: 'noise', noise_LA_max: 'noise', noise_LA_min: 'noise',
counts_per_minute: 'radioactivity',
lat: 'gps'
};
function getType(typ) {
if(typ in types) {
return types[typ];
} else {
return 'unknown'
}
}
export const constructDBaseEntries = async (client, body, args) => {
logit(`Number of entries: ${body.length}`)
logit('Parsing ...')
let start = DateTime.now()
let allLines = {
pm: [], thp: [], noise: [], radioactivity: [], gps: [], unknown: []
}
let datalines = ''
actualProps = await mongo.getallProperties(client)
try {
for (let item of body) { // check all entries
const dt = item.timestamp.split(' ')
const datetime = new Date(dt[0] + 'T' + dt[1] + 'Z') // extract date of entry as utc
let values = {}
let ival = '' // fetch values
let typ = 'unknown'
let mapvalue = {}
for (let v of item.sensordatavalues) { // for all values
let vtyp = v.value_type; // extract value type
if (typ === 'unknown') { // extract measurement type
typ = getType(vtyp)
}
if(typ === 'noise') {
vtyp = vtyp.slice(6)
}
let val = v.value; // and value
let x
try {
x = parseFloat(val); // convert value to float
if (Number.isNaN(x)) {
x = -9999.9 // default if value is invalid or unknown
}
} catch (err) {
console.log('Math parse float error on value');
x = -9999.9 // default if value is invalid or unknown
}
values[vtyp] = x
ival += `${vtyp}=${x},`
// if noise sensor precalculate pow10
if (vtyp == 'LAeq') {
let e10 = Math.pow(10, x / 10)
values.E10tel_eq = e10
ival += `E10tel_eq=${e10},`
mapvalue.E10tel_eq = e10
}
mapvalue[vtyp] = x
}
let store = true
if(args.typ) {
if(!args.typ.includes(typ)) {
store = false
}
}
if (store) {
allLines[typ].push({sensorid: item.sensor.id, datetime: datetime, values: values})
datalines += `${typ},sid=${item.sensor.id} ${ival.slice(0,-1)} ${datetime.getTime()}\n`
mapvalue.timestamp = datetime
checkProperties(item, mapvalue, typ, datetime); // check if new or new location or new sensor
}
}
} catch (e) {
logerror(`constructDBaseEntries ${e}`);
}
// sort allLines on sensorID
for (const [k,v] of Object.entries(allLines)) {
allLines[k].sort((a, b) => {
if (a.sensorid < b.sensorid) {
return -1
} else if (a.sensorid > b.sensorid) {
return 1
} else {
return 0
}
})
}
statistics.parseTime = DateTime.now().diff(start, ['seconds']).toObject().seconds
logit(`Parse time: ${statistics.parseTime} sec`)
return [newProps, allLines, datalines]
}
+51
View File
@@ -0,0 +1,51 @@
import axios from 'axios'
import * as fs from 'fs'
import { logit, logerror} from'./logit.js'
import { DateTime } from 'luxon'
const API_URL = 'https://api.sensor.community/static/v1/data.json'; // URL to API on 'luftdaten.info'
const SAVE_NAME = './data/aktdata.json'; // filename for actual data
let LIVE = process.env.LIVE || 'true'
export let statistics = {};
export const doReadfromAPI = async () => {
logit(`LIVE = ${LIVE}`)
let start = DateTime.now()
let data = []
if (LIVE === 'true') {
logit(`Start Reading from API`)
let body
for(let count = 1; count <= 3; count++) {
try {
logit(`Try - ${count}`)
let ret = await axios(API_URL, {timeout: 10000})
if (ret.status != 200) {
continue
}
data = ret.data
saveDatatoFile(SAVE_NAME, JSON.stringify(data))
break
} catch (e) {
logerror(`doReadfromAPI ${e}`)
}
}
} else {
logit('Start using data on disk')
data = readDatafromFile(SAVE_NAME)
}
statistics.readInTime = start.diffNow('seconds').toObject().seconds * -1
statistics.entries = data.length
logit(`ReadIn-Time: ${statistics.readInTime} sec`)
return data
}
// die Daten in eimnr Datei zwischenspeichern
function saveDatatoFile(fn, data) {
fs.writeFileSync(fn, data)
}
// Daten wieder vom File lesen
function readDatafromFile(fn) {
return JSON.parse(fs.readFileSync(fn))
}
+9
View File
@@ -0,0 +1,9 @@
/var/log/timeseries.log {
daily
rotate 7
delaycompress
compress
notifempty
missingok
}
+9
View File
@@ -0,0 +1,9 @@
build_and_copy.sh
Dockerfile_sensorapi
docker-compose.yml
.vscode
log
mocks
test
node_modules
doc
+5
View File
@@ -0,0 +1,5 @@
node_modules/
.idea
.env*
.DS_Store
sensorapi.log
+75
View File
@@ -0,0 +1,75 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Launch reception",
"skipFiles": [
"<node_internals>/**"
],
"env": {
"DEBUG": "sensorapi:*",
"INFLUXHOST": "192.168.178.190",
"INFLUXTOKEN": "gHGGgjaK0lmM6keMa01JeuDpqOE_vRq8UimsU4QKb2miI5BDh2PfWynEbwKizdJapXy8jVbTat5mVZLQTNmSdw==",
"MONGOHOST": "192.168.178.190",
"MONGOAUTH": "true",
"MONGOUSRP": "admin:mongo4noise",
// "DBASE": "influx",
},
"program": "${workspaceFolder}/bin/www.js"
},
{
"type": "node",
"request": "launch",
"name": "Launch esprimo",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/www.js",
"env": {
"DEBUG": "sensorapi:*",
"MONGOUSRP": "admin:mongo4noise",
"MONGOPORT": "27037",
"MONGOHOST": "217.72.203.152",
//"MONGOUSRP": "rexfue:5g2h4j3XC$$C$§442dcdsvDCx",
"MONGOAUTH": "true",
"INFLUXTOKEN": "q35XUBaElzcy8dDd9HF2_mpeHvYCampZg_9mJNP5jeBQRopq3EWIzNTZ555QLSIXhZC05RXCoFgjiaT7VzyNkQ==",
"DEVELOP": "true",
}
},
{
"type": "node",
"request": "launch",
"name": "Ralf",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/www.js",
"env": {
"MONGOHOST": "192.168.51.22",
"MONGOAUTH": "true",
"MONGOUSRP": "rexfue:s25BMmW2gg",
}
},
{
"type": "node",
"request": "launch",
"name": "Launch localhost",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/bin/www.js",
"env": {
"INFLUXHOST": "localhost",
"INFLUXTOKEN": "Pt7cDkKS1pAI2a0qsAhfSY97EVsfeNwJxo-ZdiNvfwC4kBiqxmoj7WbR7XkNRr23YELydv_9HXrN2SMofq9vhQ==",
"MONGOHOST": "localhost",
"MONGOPORT": "27017",
"DEBUG": "sensorapi:*"
}
}
]
}
+24
View File
@@ -0,0 +1,24 @@
FROM node:22-alpine
ADD package.json /tmp/package.json
ADD package-lock.json /tmp/package-lock.json
RUN cd /tmp && npm ci
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
+190
View File
@@ -0,0 +1,190 @@
// Fetch the actual (= newest) data out of the dbase to show it on the map
import {DateTime} from "luxon"
import * as mongo from "../databases/mongo.js"
import { returnOnError } from "../utilities/reporterror.js"
// Default distance for center search ( in km)
const DEFAULT_DISTANCE = 10
// Value to use fpr map
const value4map = [
{ typ: 'noise', value: 'LA_max'},
{ typ: 'pm', value: 'P1'},
{ typ: 'thp', value: 'temperature'},
{ typ: 'radioactivity', value: 'counts_per_minute'},
]
// get value for map from tabel
const getValue4Map = (t) => {
for(let x of value4map) {
if(x.typ == t) {
return x.value
}
}
return ''
}
// Relations between types and value type
const vtype2measurement = {
P1: 'pm', P2: 'pm', P0: 'pm',
temperature: 'thp', humidity: 'thp', pressure: 'thp',
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 ' '
}
// read the last entries from the influx database
const readLastDates = async (typ) => {
let ret = {values: [], err: null}
let query = `
from(bucket: "sensor_data")
|> range(start: -2h)
|> filter(fn: (r) => r._measurement == "${typ}" and r._field == "${getValue4Map(typ)}")
|> last()
|> group()
|> map(fn: (r) => ({r with sid: int(v: r.sid)}))
|> sort(columns: ["sid"])
|> keep(columns: ["_time","sid","_value"])
`
return await fetchFromInflux(ret, query)
}
export var getData4map = async (params) => {
let start = DateTime.now()
let ret = {err: null}
// ***** This function will (at the moment) only be called by internal routines, so there is no need to check the parameters !
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) {
let val = params.box.split(',')
for (let i = 0; i < val.length; i++) {
val[i] = parseFloat(val[i])
}
south = parseFloat(val[1])
north = parseFloat(val[3])
east = parseFloat(val[2])
west = parseFloat(val[0])
// 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
}
}
}
try {
// fetch mapdata from mongodb
let { properties, err } = await mongo.getallProperties(mongo.properties_collection, query)
if(err) {
return returnOnError(ret, 'NOPROPSFOUND', getData4map.name)
}
let v4map = getValue4Map(typ)
for (let sensor of properties) {
let oneAktData = {}
if (sensor.values !== undefined) {
oneAktData = {
location: sensor.location[0].loc.coordinates,
id: sensor._id,
name: sensor.name[0].name,
indoor: sensor.location[0].indoor,
lastseen: sensor.values.timestamp
}
let now = new Date().getTime()
if(oneAktData.lastseen !== '') {
let diff = now - oneAktData.lastseen.getTime()
if (diff >= 365 * 24 * 3600 * 1000) {
oneAktData.value = -4
} else if (diff >= 30 * 24 * 3600 * 1000) {
oneAktData.value = -3
} else if (diff >= 7 * 24 * 3600 * 1000) {
oneAktData.value = -2
} else if (diff >= 2 * 3600 * 1000) {
oneAktData.value = -1
} else {
if (sensor.values !== undefined) {
oneAktData.value = Math.round(sensor.values[v4map] * 100) / 100
}
}
let weeks = Math.round(diff / (7 * 24 * 3600 * 1000))
oneAktData.weeks = weeks
}
if (sensor.values.timestamp > lastDate) {
lastDate = sensor.values.timestamp
}
} else {
oneAktData.value = -5
oneAktData.weeks = 0
oneAktData.lastseen = ''
}
aktData.push(oneAktData)
}
ret = {
err: null,
options: {
lastdate: lastDate,
count: aktData.length,
data: 'map'
},
values: aktData
}
return ret
}
catch(e) {
return returnOnError(ret, `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
}
+80
View File
@@ -0,0 +1,80 @@
import {returnOnError} from "../utilities/reporterror.js"
import axios from 'axios'
import {logit} from "../utilities/logit.js"
import {getOneProperty} from "./getproperties.js"
import { response } from "express"
const NOMINATIM_URL = `https://nominatim.openstreetmap.org/reverse?lat=${'xx'}&lon=${'yy'}&format=json`
const NOMINATIM_CITY_URL = `https://nominatim.openstreetmap.org/?q="${'xx'}"&format=json`
export const getCityCoords = async (params) => {
let ret = {coords: [], city: params.city, err: null}
let url = NOMINATIM_CITY_URL.replace('xx', params.city)
// let url = 'https://nominatim.openstreetmap.org/?q="K%C3%B6ln"&format=json'
try {
const response = await axios(url)
if (response.status !== 200) {
return returnOnError(ret, 'RESPSTATUS', getCityCoord.name, response.status)
}
ret.coords = [response.data[0].lat,response.data[0].lon]
logit(JSON.stringify(ret.coords))
} catch (e) {
return returnOnError(ret, e, getCityCoords.name)
}
return ret
}
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),{
headers: {
'User-Agent': 'Firefox 132.0.1'
}
});
if (response.status !== 200) {
return returnOnError(ret, 'RESPSTATUS', getAddress.name, response.status)
}
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}`;
ret.address = {
street: `${(akt.road ? akt.road : "")}`,
plz: `${(akt.postcode ? akt.postcode : "")}`,
city: `${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;
*/
+48
View File
@@ -0,0 +1,48 @@
// Fetch the properties for the given sensor
import * as mongo from "../databases/mongo.js"
import {returnOnError} from "../utilities/reporterror.js"
import checkParams from "../utilities/checkparams.js"
let readProperties = mongo.readProperties
let readChipData = mongo.readChipData
// Read properties for sensorid and properties for all other sensors on same location
export const getOneProperty = async (params) => {
let properties = {err: null, props: {}, chip: {}}
let {opts, err} = checkParams(params, {mandatory:[{name:'sensorid', type: 'int'}], optional:[]})
if (err) {
return returnOnError(properties, err, getOneProperty.name)
}
// read 'chip'-data (special for noise sensors)
const chipdata = await readChipData(opts.sensorid)
if (chipdata.err === null) {
properties.chip = chipdata
}
let sensorEntries = [];
try {
let pp = await readProperties({sid: opts.sensorid}); // read for given sensorID
if ((pp.properties == null) || (pp.error)) {
return returnOnError(properties, 'NOPROPSREAD', getOneProperty.name, opts.sensorid)
}
// now find sensors with same location
let query = {"location.0.id": pp.properties.location[0].id}
let others = await readProperties(query)
if (others.err) {
return returnOnError(properties, 'NOPROPSREAD', getOneProperty.name, others.errortext)
}
if (others.properties.length > 0) {
for (const x of others.properties) {
if(x._id === pp.properties._id) {
continue
}
sensorEntries.push({name: x.name[0].name, sid: x._id})
}
}
properties.props = pp.properties
properties.props.othersensors = sensorEntries;
} catch (e) {
return returnOnError(properties, e, getOneProperty.name)
}
return properties
}
+308
View File
@@ -0,0 +1,308 @@
// get data for one sensor
const DBASE = process.env.DBASE || 'mongo'
import {DateTime} from "luxon"
import * as influx from "../databases/influx.js"
import * as mongo from "../databases/mongo.js"
import {returnOnError} from "../utilities/reporterror.js"
import {csv2Json} from "../utilities/csv2json.js"
import checkParams from "../utilities/checkparams.js"
import {getOneProperty} from "./getproperties.js"
import {getNoiseData} from "../sensorspecials/noise.js"
import {getgeigerData} from "../sensorspecials/geigeract.js"
// Possible params for the different sensor types
const noiseParams = [
{name:'data', type: 'string', default: 'live'},
{name: 'span', type: 'int', default: 1},
{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 geigerParams = [
{name: 'what', type: 'string', default: 'day'},
{name: 'span', type: 'int', default: 1},
{name: 'daystart', type: 'bool', default: null},
{name: 'avg', type: 'int', default: 1},
{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},
{name: 'moving', type: 'bool', default: false},
]
// >>>>>>>>>>>>>>>>>>>>> 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', possibleParams: geigerParams, func: getgeigerData}
]
/* 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).toUTC()
end = end.plus({days: opts.span})
}
if(opts.daystart) {
start = start.startOf("day")
end = end.startOf("day")
}
// start = start.toUTC()
// end = end.toUTC()
// 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', distribute to the special routines for
// the sensors
//
// params:
// params: all parameters from the url
//
// return:
// Returns from the special routines
// *********************************************
// export const getSensorData = async (params) => {
export async function getSensorData(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 erg = await getOneProperty({sensorid: opts.sensorid})
if (erg.err) {
return returnOnError(ret, erg.err, getSensorData.name)
}
// distribute to the right routine
for(let item of sensorTypeTable) {
if(item.typ === erg.props.type) {
ret = await item.func(params, item.possibleParams, erg.props) // get the values from database
return ret
}
}
return returnOnError(ret, 'CMNDUNKNOWN', getActData.name)
}
// export const getActData = async (opts) => {
export async function getActData(opts) {
if (DBASE === 'mongo') {
return await mongo.fetchActData(opts)
} else if (DBASE === 'influx') {
return await influx.fetchActData(opts)
}
return {err: 'DBASEUNKNOWN', values: []}
}
// ..../api/getavgdata?
// ToDo: check UTC !!!!
export var 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 var 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 await fetchFromInflux(ret, query)
}
// *********************************************
// function getMAPaktData() {
//
// Get the actual data from the database within the bounds for the given sensor typ
//
// params:
// opt: opt.box => region for which to find sensor data
// opt.typ: type of sensor
//
// return
// JSON
// { avgs: [
// { location: [ 9.00, 48.80 ], id: 29174, lastSeen: "2019-10-23T12:03:00.000Z", max: "65" }
// { location: [ 9.10, 49.80 ], id: 28194, lastSeen: "2019-10-22T16:03:00.000Z", max: "45" }
// .........
// ], lastDate: "2019-10-29T15:05:59.000Z" }
//
// *********************************************
export const getMAPaktData = async (opt) => {
let box = opt.box
if ((box == "") || (box == undefined)) {
return {"avgs": [], "lastDate": null}
}
let south = parseFloat(box[0][1])
let north = parseFloat(box[1][1])
let east = parseFloat(box[1][0])
let west = parseFloat(box[0][0])
let aktData = []
let lastDate = 0
let loc = {
location: {
$geoWithin: {
$box: [
[west, south],
[east, north]
]
}
},
typ: opt.typ,
}
let docs = await mongo.readMapdata(loc,0)
// console.log(docs)
for (let i = 0;i < docs.length; i++) {
let item = docs[i]
let oneAktData = {}
oneAktData['location'] = item.location.coordinates
oneAktData['id'] = item._id // ID des Sensors holen
oneAktData['lastSeen'] = item.values.datetime
oneAktData['indoor'] = item.indoor
let dati = item.values.datetime
let dt = new Date(dati)
if ((now - dt) >= 31 * 24 * 3600 * 1000) { // älter als 1 Monat ->
oneAktData['noise_max'] = -2 // -2 zurückgeben
} else if ((now - dt) >= 3600 * 1000) { // älter als 1 Stunde ->
oneAktData['noise_max'] = -1 // -1 zurückgeben
} else {
oneAktData['noise_max'] = -5 // bedutet -> nicht anzeigen
if (item.values.hasOwnProperty('LA_max')) {
oneAktData['noise_max'] = item.values.LA_max.toFixed(0) // und merken
}
if (dati > lastDate) {
lastDate = dati
}
}
aktData.push(oneAktData) // dies ganzen Werte nun in das Array
}
return {"avgs": aktData, "lastDate": lastDate} // alles bearbeitet _> Array senden
}
+66
View File
@@ -0,0 +1,66 @@
import createError from 'http-errors'
import logger from 'morgan'
import express from 'express'
import cookieParser from 'cookie-parser'
import cors from 'cors'
import i18next from 'i18next'
import i18nextMiddleware from 'i18next-http-middleware'
import Backend from 'i18next-fs-backend'
import {logit} from "./utilities/logit.js"
import pkg from './package.json' with { type: "json" }
const app = express()
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import indexRouter from './routes/index.js'
import { apiRouter } from './routes/api.js'
import {fileURLToPath} from "url";
import path from "path";
i18next
.use(Backend)
.use(i18nextMiddleware.LanguageDetector)
.init({
backend: {
loadPath: __dirname + '/locales/{{lng}}/{{ns}}.json'
},
fallbackLng: 'de',
debug: false,
preload: ['de', 'en']
});
app.use(i18nextMiddleware.handle(i18next));
app.use(cors())
app.use(logger('dev'))
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use(cookieParser())
app.use('/', indexRouter)
app.use('/api', apiRouter)
// 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}`)
})
logit(`Start of Program Version: ${pkg.version} vom ${pkg.date}`)
export default app
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
import app from '../app.js'
import Debug from 'debug'
import http from 'http'
const debug = Debug('sensorapi:*')
/**
* Get port from environment and store in Express.
*/
const port = normalizePort(process.env.PORT || '3005');
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);
console.log('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'
+166
View File
@@ -0,0 +1,166 @@
// 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";
import {csv2Json} from "../utilities/csv2json.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}`
const influxRead = async (query) => {
let start = DateTime.now()
logit(`ReadInflux from ${INFLUXURL_READ}`)
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) {
return returnOnError(erg, RESPSTATUS, influxRead.name, ret.status)
}
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
}
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
}
const fetchFromInflux = async (ret, query) => {
let { values, err} = await influxRead(query)
if(err) {
if(err.toString().includes('400')) {
return returnOnError(ret, 'SYNTAXURL', fetchFromInflux.name)
} else {
return returnOnError(ret, err, fetchFromInflux.name)
}
}
if (values.length <= 2) {
return returnOnError(ret, 'NODATA', fetchFromInflux.name)
}
ret.values = csv2Json(values)
return ret
}
export const fetchActData = async (opts) => {
let ret = {err: null, values: []}
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)
}
export const fetchNoiseAVGData = async (opts) => {
let ret = {err: null, values: []}
let small = '|> keep(columns: ["_time", "peakcount", "n_AVG"])'
if (opts.long) {
small = ''
}
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 == "LA_max")
|> 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}
`
return await fetchFromInflux(ret, queryAVG)
}
+424
View File
@@ -0,0 +1,424 @@
/* 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 = 27017; }
if (MONGOAUTH === undefined) { MONGOAUTH = 'false'; }
if (MONGOBASE === undefined) { MONGOBASE = 'sensor_data'; }
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
MONGO_URL = 'mongodb://'+MONGOUSRP+'@' + MONGOHOST + ':' + MONGOPORT + '/?authSource=admin'; // URL to mongo database
}
export const properties_collection = 'properties'
export const connectMongo = async () => {
try {
// logit(`Try to connect to ${MONGO_URL}`)
let client = await MongoClient.connect(MONGO_URL)
// logit(`Mongodbase connected to ${MONGO_URL}`)
return client
}
catch(error){
throw(error)
}
}
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, properties: null}
let client = await connectMongo()
try {
if ("sid" in query) { // if sid is given, read property for sid
ret.properties = await client.db(MONGOBASE).collection(properties_collection).findOne({_id: query.sid})
} else { // otherwise read props corresponding to query
ret.properties = await client.db(MONGOBASE).collection(properties_collection).find(query).limit(limit).toArray()
}
} catch (e) {
ret.err = e
}
finally {
client.close()
}
return ret
}
export const readChipData = async (sid) => {
let ret = { err: null, chipdata: null}
let client = await connectMongo()
try {
ret.chipdata = await client.db(MONGOBASE).collection('prop_flux').findOne({_id: sid},{projection: {chip: 1, _id: 0}})
} catch (e) {
ret.err = e
}
finally {
client.close()
}
return ret
}
// read mapdata from database
export const readMapdata = async (query, limit) => {
let ret = {err: null, mapdata: []}
let client = await connectMongo()
try {
ret.mapdata = await client.db(MONGOBASE).collection("mapdata").find(query).limit(limit).toArray()
} catch (e) {
ret.err = e
}
finally {
client.close()
}
return ret
}
export const getallProperties = async (coll, query) => {
let ret = {err: null, properties: []}
let client = await connectMongo()
try {
ret.properties = await client.db(MONGOBASE).collection(coll)
.find(query).toArray()
} catch (e) {
ret.err = e
}
finally {
client.close()
}
return ret
}
export const getOneproperty = async (sid) => {
let ret = {error: false}
let client = await connectMongo()
try {
ret.properties = await client.db(MONGOBASE).collection(properties_collection)
.findOne({_id: sid})
} catch (e) {
ret = {error: true, errortext: e}
}
finally {
client.close()
}
return ret
}
export const readAKWs = async (options) => {
let ret = {values: { akws: [], th1_akws: []}, err: null}
let erg = []
let 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 {
client.close()
}
return ret
}
export const fetchActData = async (opts) => {
let ret = {err: null, values: []}
let start = opts.start.slice(7)
let end = opts.stop.slice(6)
start = DateTime.fromISO(start).toJSDate()
end = DateTime.fromISO(end).toJSDate()
let query = {sensorid: opts.sensorid, datetime: {$gte: start, $lt: end}}
let options = { projection: {_id: 0, values: 1, datetime: 1}, sort: {datetime: 1}}
let client = await connectMongo()
try {
// ret.values = await client.db(MONGOBASE).collection('noise_sensors')
// .find(query, options).toArray()
ret.values = await client.db(MONGOBASE).collection('noise_sensors').aggregate([
{$match: query},
{$sort: { datetime: 1}},
// {$replaceWith:
// {
// '$values.LA_min': '$values.noise_LA_min'
// }
// },
{$replaceWith:
{
datetime: {$dateToString: {format: '%Y-%m-%dT%H:%M:%SZ', date: '$datetime'}},
LA_min: '$values.LA_min',
LA_minx: '$values.noise_LA_min',
LA_max: '$values.LA_max',
LAeq: '$values.LAeq',
E10tel_eq: '$values.E10tel_eq' }
},
// {$project: {
// datetime: {$dateToString: {format: '%Y-%m-%dT%H:%M:%SZ', date: '$datetime'}},
// _id: 0, values:1
// }},
]).toArray()
}
catch(e) {
ret.err = e
}
finally {
client.close()
}
return ret
}
/*
Try to connect to mongodb://rexfue:s25BMmW2gg@192.168.51.22:27017
Try to connect to mongodb://rexfue:s25BMmW2gg@192.168.51.22:27017
*/
/*
let docs = await collection.find(
{ datetime:
{ $gte: start.toDate(), $lt: end.toDate() }
},
{ projection:
{_id:0, E_eq:0, E_mx:0, E_mi:0, E10tel_mx:0, E10tel_mi:0}, sort: {datetime: sort}
},
).toArray();
*/
export const fetchgeigerAVGData = async (opts) => {
let docs = []
let ret = {err: null, values: []}
let start = opts.start.slice(7)
let end = opts.stop.slice(6)
start = DateTime.fromISO(start).toJSDate()
end = DateTime.fromISO(end).toJSDate()
let datRange = {sensorid: opts.sensorid, datetime: {$gte: start, $lt: end}}
let sorting = {datetime: opts.sort};
let client = await connectMongo()
try {
if(opts.moving) {
docs = await client.db(MONGOBASE).collection('sensors').aggregate([
{
$sort: sorting
}, // sort by date
{
$match: {sensorid: opts.sensorid}
}, // select only values for given sensor
{
$match: datRange
}, // select only values in give data range
{
$setWindowFields: {
sortBy: {datetime: 1},
output: {
cpm_avg: {
$avg: "$values.counts_per_minute",
window: {
range: [-60, 0],
unit: "minute"
}
}
}
}
},
{
$project: {_id:0, cpm_avg: 1, datetime:1, uSvph_avg: { $multiply: ["$cpm_avg", opts.factor]}}
},
{
$sort: {datetime: 1}
}
]).toArray();
} else {
docs = await client.db(MONGOBASE).collection('sensors').aggregate([
{
$sort: sorting
}, // sort by date
{
$match: {sensorid: opts.sensorid}
}, // select only values for given sensor
{
$match: datRange
}, // select only values in give data range
{ $group: {
_id: {$dateTrunc: {
date: "$datetime",
unit: "minute",
binSize: 60
}},
cpm_avg: {$avg: "$values.counts_per_minute"}, // calculate the average
}
},
{ $addFields: { datetime: "$_id"}}, // change '_id' to 'datetime
{
$project: {_id:0, uSvph_avg: { $multiply: ["$cpm_avg", opts.factor]}, datetime: 1, cpm_avg: 1}
},
{
$sort: {datetime: 1}
}
]).toArray();
}
} catch(e) { // if there was an error
ret.err = e // log it to console
}
finally {
client.close()
}
ret.values = docs
return ret
}
export const fetchNoiseAVGData = async (opts) => {
let docs = []
let ret = {err: null, values: []}
let start = opts.start.slice(7)
let end = opts.stop.slice(6)
start = DateTime.fromISO(start).toJSDate()
end = DateTime.fromISO(end).toJSDate()
let peak = opts.peak; // threshold for peak count
let datRange = {sensorid: opts.sensorid, datetime: {$gte: start, $lt: end}}
let sorting = {datetime: opts.sort};
let grpId = {$dateToString: {format: '%Y-%m-%dT%H:00:00Z', date: '$datetime'}}
let client = await connectMongo()
try {
docs = await client.db(MONGOBASE).collection('noise_sensors').aggregate([
{$sort: sorting}, // sort by date
{$match: datRange}, // select only values in give data range
{
$group: {
_id: grpId,
n_average: {$avg: "$values.E10tel_eq"}, // calculate the average
n_sum: {$sum: "$values.E10tel_eq"}, // calculate the sum
peakcount: {$sum: {$cond: [{$gte: ["$values.LA_max", peak]}, 1, 0]}}, // count peaks
count: {$sum: 1}, // count entries
}
},
{$sort: {_id: 1}}, // sort by result dates
{ $addFields: { datetime: "$_id"}}, // change '_id' to 'date'
{$project: opts.long ? { _id:0, n_AVG: { $multiply: [10, {$log10: "$n_average"}]}, datetime:1, peakcount:1, count:1, n_sum:1} :
{_id:0, n_AVG: { $multiply: [10, {$log10: "$n_average"}]}, datetime:1, peakcount:1}}
]).toArray(); // return not all fields, depending on 'long'
} catch(e) { // if there was an error
ret.err = e // log it to console
}
finally {
client.close()
}
ret.values = docs
return ret
}
export const fetchAVGData = async (opts) => {
}
/*
// *********************************************
// getAverageData
//
// Calculate different values per hour
// average of E10tel_eq ( E10tel_eq => 10 ^(LAeq/10) )
// sum of E10tel_eq, to calculate day, night and eveniung averages
// count, how many values are used for average/sum
// paeakcount, how many values of LAmax are over defined peak value in every hour
//
// params:
// db: Database
// opt: different options (see further down)
//
// return
// depending an calling parameter 'what', not all values will be sent in 'values'
// JSON
// {[
// { datetime: "2019-10-23T00:00:00Z" , n_AVG: 67.22, n_sum: 32783, count: 24, peakcount: 6 }.
// { datetime: "2019-10-23T01:00:00Z" , n_AVG: 52.89, n_sum: 23561, count: 26, peakcount: 5 }.
// .........
// ]}
//
// *********************************************
async function getAverageData(db,opt) {
let start = opt.start;
let end = opt.end; // start and ent time for aggregation
let docs = []; // collect data here
const collection = db.collection('data_' + opt.sid);;
let span = opt.span // date range in days
let peak = opt.peak; // threshold for peak count
let long = opt.long; // true => give extra output
let nbrOfHours = opt.end.diff(opt.start,'hours') + 24;
let datRange = {datetime: {$gte: opt.start.toDate(), $lt: opt.end.toDate()}};
let sorting = {datetime: opt.sort};
let grpId = {$dateToString: {format: '%Y-%m-%dT%H:00:00Z', date: '$datetime'}};
try {
docs = await collection.aggregate([
{$sort: sorting}, // sort by date
{$match: datRange}, // select only values in give data range
{
$group: {
_id: grpId,
n_average: {$avg: '$E10tel_eq'}, // calculate the average
n_sum: {$sum: '$E10tel_eq'}, // calculate the sum
peakcount: {$sum: {$cond: [{$gte: ["$LA_max", peak]}, 1, 0]}}, // count peaks
count: {$sum: 1}, // count entries
}
},
{$sort: {_id: 1}}, // sort by result dates
{ $addFields: { datetime: "$_id"}}, // change '_id' to 'date'
{$project: opt.long ? { _id:0, n_AVG: { $multiply: [10, {$log10: "$n_average"}]}, datetime:1, peakcount:1, count:1, n_sum:1} :
{_id:0, n_AVG: { $multiply: [10, {$log10: "$n_average"}]}, datetime:1, peakcount:1}}
]).toArray(); // return not all fields, depending on 'long'
} catch(e) { // if there was an error
console.log(e); // log it to console
}
// 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(nbrOfHours); // generate new array
let emptyValues = opt.long ? {n_sum: -1, n_AVG:-1} : {n_AVG:-1};
hoursArr.fill(emptyValues); // fill with 'empty' value
let startDay = moment.utc(docs[0].datetime).date(); // calc first day
let k = 0;
for (let i=0; i<docs.length; i++) { // loop through docs
let stunde = moment.utc(docs[i].datetime).hours(); // extract current hour
let day = moment.utc(docs[i].datetime).date(); // and curren t day
if (day != startDay) { // if date has changed
k += 24; // increment index by 24
startDay = day;
}
hoursArr[k+stunde] = docs[i]; // copy date into hourArray
}
return hoursArr;
}
*/
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -e
# Angepasst aus dem readin-deploy.sh (bzw. urspr. /Projekte/Logbuch/deploy.sh)
# Nur linux/amd64, nativer Build auf esprimo (amd64) -> kein buildx/qemu,
# eigener Dockerfile-Name (Dockerfile_sensorapi).
REGISTRY="docker.citysensor.de"
IMAGE_NAME="sensorapi"
TAG="${1:-latest}"
PLATFORM="linux/amd64"
DOCKERFILE="Dockerfile_sensorapi"
FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${TAG}"
BUILD_DATE=$(date +%d.%m.%Y)
echo "=========================================="
echo "sensorapi Deploy Script (amd64)"
echo "=========================================="
echo "Registry: ${REGISTRY}"
echo "Image: ${IMAGE_NAME}"
echo "Tag: ${TAG}"
echo "Platform: ${PLATFORM}"
echo "Dockerfile: ${DOCKERFILE}"
echo "Build-Datum: ${BUILD_DATE}"
echo "=========================================="
echo ""
# Hinweis: Login wird uebersprungen, weil die Credentials fuer ${REGISTRY}
# bereits in ~/.docker/config.json liegen.
# Bei Bedarf manuell: docker login ${REGISTRY}
echo ">>> Baue Image (nur ${PLATFORM}, nativ) ..."
docker build \
--platform "${PLATFORM}" \
-f "${DOCKERFILE}" \
-t "${FULL_IMAGE}" \
.
echo ""
echo ">>> Pushe ${FULL_IMAGE} ..."
docker push "${FULL_IMAGE}"
echo ""
# Falls ein anderer Tag als 'latest' gebaut wurde, zusaetzlich latest setzen+pushen
if [ "${TAG}" != "latest" ]; then
echo ">>> Tagge zusaetzlich als :latest ..."
docker tag "${FULL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
echo ""
fi
echo "=========================================="
echo "Build + Push fertig: ${FULL_IMAGE}"
echo "=========================================="
echo ""
echo "Danach Stack aktualisieren:"
echo " cd /opt/stacks/esprimo/noisesensors && docker compose up -d sensorapi"
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
+6
View File
@@ -0,0 +1,6 @@
# Sensor-API
### Allgemein
* **ALLE** Zeiten werden in UTC ausgegeben.
Wird beim Aufruf die Zeit im ISO-Format **ohne** Zonenbezeichnung eingegeben, wird sie in UTC umgerechnet und auch so ausgegeben. Wenn die Eingabe schon UTC ist (Z ale letztes Zeichen) dann bleibt es so.
+19
View File
@@ -0,0 +1,19 @@
version: '3.9'
services:
node:
image: sensorapi
environment:
DEVELOP: "true"
MONGOHOST: ${MONGOHOST}
MONGOPORT: ${MONGOPORT}
INFLUXHOST: ${INFLUXHOST}
INFLUXTOKEN: ${INFLUXTOKEN}
LIVE: "true"
PORT: 3004
ports:
- "3004:3004"
volumes:
- ${PWD}/log/sensorapi:/var/log
container_name: node_sensorapi
restart: unless-stopped
+16
View File
@@ -0,0 +1,16 @@
{
"CMNDUNKNOWN": "Unbekanntes Kommando",
"NOTHING": "Nichts anzuzeigen",
"PARAMNONUM": "Parameter \"xxx\" ist keine Zahl",
"NOTYP": "KeinTyp angegeben",
"NOSENSFOUND": "Kein passender Sensor in den Properties gefunden",
"NOMANDPARAM": "Nitewendiger Parameter xxx nicht angegeben",
"NOPROPSREAD": "Keine properties für Sensor xxx eingelesen",
"NOPARAMETER": "Keine Parameter übergene",
"RESPSTATUS": "Rückgabe-Status: xxx",
"NODATA": "keine Daten gefunden",
"SYNTAXURL": "Syntax Fehler beim Aufruf der URL",
"WRONGTYPE": "Sensor xxx ist kein yyy Sensor",
"NOLASTDATES": "Probleme bein abholen der letzten Daten",
"NOPROPSFOUND": "Properties Collection nicht gefunden"
}
+16
View File
@@ -0,0 +1,16 @@
{
"CMNDUNKNOWN": "Command not known",
"NOTHING": "Nothing to show",
"PARAMNONUM": "Parameter 'xxx' is not a number",
"NOTYP": "No type given",
"NOSENSFOUND": "No suitable sensors found in properties",
"NOMANDPARAM": "Mandatory parameter xxx not given",
"NOPROPSREAD": "No properties read for sensor xxx",
"NOPARAMETER": "No parameter given",
"RESPSTATUS": "Returned status: xxxx",
"NODATA": "No data found",
"SYNTAXURL": "Syntax error in calling url!",
"WRONGTYPE": "Sensor xxx is not of type yyy",
"NOLASTDATES": "Problems fetching last dates from database",
"NOPROPSFOUND": "Properties collection not found"
}
+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
}
+2915
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -0,0 +1,36 @@
{
"name": "sensorapi",
"version": "1.4.2",
"date": "2023-11-29 16:00 UTC",
"private": true,
"scripts": {
"start": "node ./bin/www.js >>/var/log/sensorapi.log 2>&1",
"dev": "node ./bin/www.js",
"test": "mocha ./test/test.js"
},
"type": "module",
"bin": {
"www": "./bin/www.js"
},
"dependencies": {
"@influxdata/influxdb-client": "^1.35.0",
"@influxdata/influxdb-client-apis": "^1.35.0",
"axios": "^1.12.2",
"cookie-parser": "~1.4.7",
"cors": "^2.8.5",
"debug": "~4.4.3",
"express": "^5.1.0",
"http-errors": "~2.0.0",
"i18next": "^25.5.2",
"i18next-http-middleware": "^3.8.0",
"i18next-fs-backend": "^2.0.0",
"luxon": "^3.7.2",
"mongodb": "^6.19.0",
"morgan": "~1.10.1",
"pug": "^3.0.3",
"ws": "^8.18.3"
},
"devDependencies": {
"mocha": "^11.7.2"
}
}
+54
View File
@@ -0,0 +1,54 @@
import express from 'express'
import {getData4map} from "../actions/data4map.js"
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";
export const apiRouter = express.Router();
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: 'getcitycoords', func: holAddr.getCityCoords},
{cmd: 'getsensordata', func: getData.getSensorData},
{cmd: 'getmapdata', func: getData4map}
]
let i18n;
export const translate = (x) => {
return i18n.t(x)
}
export const dispatchCommand = async (cmd, table, params, res) => {
let notfound = true
for (let c of table) {
if (c.cmd === cmd) {
notfound = false
let erg = await c.func(params)
if (typeof erg === 'string') {
res.type('text')
res.send(erg)
} else {
res.json(erg)
}
}
}
if (notfound) {
res.json({err: translate('CMNDUNKNOWN')})
}
}
// normal routes called from javascript client
apiRouter.get('/:cmd', async (req, res) => {
const params = req.query
params.chart = false
i18n = req.i18n
await dispatchCommand(req.params.cmd, cmdTable, params, res)
})
+12
View File
@@ -0,0 +1,12 @@
import express from 'express'
import { translate as trans } from '../routes/api.js'
const router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.status(200).json({message: trans('NOTHING')})
});
export default router
+152
View File
@@ -0,0 +1,152 @@
// Data preparation for fetching geigeractivity data
// rxf 2022-06-24
import {returnOnError} from "../utilities/reporterror.js";
import { calcRange, getActData, getAvgData, getLongAvg } from "../actions/getsensorData.js"
import checkParams from "../utilities/checkparams.js";
import { setoptionfromtable } from "../utilities/chartoptions.js"
import {DateTime} from 'luxon'
import { fetchgeigerAVGData } from "../databases/mongo.js";
import { translate as trans } from '../routes/api.js'
const geigeractFilter = (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
}
const getgeigerDWMData = async (opts) => {
if(opts.avg === 1) {
const erg = await getActData(opts)
return erg
} else {
const erg = await fetchgeigerAVGData(opts)
return erg
}
}
const factorTable = [
{name: 'SBM-20', factor: 1 / 2.47},
{name: 'SBM-19', factor: 1 / 9.81888},
{name: 'Si22G', factor: 0.081438},
{name: 'J306', factor: 0.06536}
]
const getfactor = (name) => {
const typ = name.slice((10))
for(const x of factorTable) {
if (x.name === typ) {
return 1 / 60 * x.factor
}
}
return 1
}
export const getgeigerData = async (params, possibles, props) => {
let ret = {err: null}
let {opts, erro} = checkParams(params, {
mandatory:[
{name:'sensorid', type: 'int'},
],
optional: possibles
})
// To be compatible with old API:
if (opts.out === 'csv') {
opts.csv = true
}
if (erro) {
return returnOnError(ret, erro, getNoiseData.name)
}
opts.factor = getfactor(props.name[0].name)
opts.span = setoptionfromtable(opts.span, 1)
opts.daystart = setoptionfromtable(opts.daystart, false)
opts.avg = setoptionfromtable(opts.avg, 1)
opts.moving = setoptionfromtable(opts.moving, false)
let {start, stop} = calcRange(opts) // calc time range
opts.start = start
opts.stop = stop
let erg = await getgeigerDWMData(opts) // get the data
ret = {
err: erg.err,
options: {
sid: opts.sensorid,
indoor: props.location[0].indoor,
span: opts.span,
start: DateTime.fromISO(opts.start.slice(7)),
data: opts.data,
count: erg.values.length,
avg: opts.avg,
moving: opts.moving
},
values: erg.values,
}
if(ret.values.length === 0) {
ret.err = trans('NODATA')
}
return ret
}
/*
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, getgeigerData.name)
}
erg.data.geigermovavg = geigeractFilter(data, opts, false)
if (opts.what === 'oneday') {
const { data, err} = await getActData(params)
if (err != null) {
return returnOnError(erg, err, getgeigerData.name)
}
erg.data.geigeractual = geigeractFilter(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, getgeigerData.name)
}
data.sid = opts.climatesid
data.sname = opts.climatesname
erg.data.climate = data
}
return erg
}
*/
+499
View File
@@ -0,0 +1,499 @@
// Data preparation for fetching noise data
// rxf 2023-03-05
const DBASE = process.env.DBASE || 'mongo'
import {returnOnError} from "../utilities/reporterror.js";
import { getActData, getAvgData, getLongAvg, calcRange} from "../actions/getsensorData.js"
import checkParams from "../utilities/checkparams.js";
import {DateTime} from 'luxon'
import { translate as trans } from '../routes/api.js'
import * as influx from "../databases/influx.js"
import * as mongo from "../databases/mongo.js"
import { setoptionfromtable } from "../utilities/chartoptions.js"
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
if (opts.csv === true) {
ret = erg
} else {
ret = {
err: erg.err,
options: {
sid: opts.sensorid,
indoor: props.location[0].indoor,
span: opts.span,
start: DateTime.fromISO(opts.start.slice(7)).toUTC().toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'"),
data: opts.data,
peak: opts.peak,
count: erg.values.length,
},
values: erg.values,
}
if (!x.peak) {
delete ret.options.peak
}
if(ret.values.length === 0) {
ret.err = trans('NODATA')
}
}
return ret
}
}
return returnOnError(ret, 'CMNDUNKNOWN', 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", LAeq: 42.22, LA_min: 39.91, LA_max: 45.18, E10tel_eq: 16672.47212551061 },
// { datetime: "2019-10-22T22:07:59.000Z", LAeq: 53.72, LA_min: 39.97, 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 (opts) => {
const erg = await getActData(opts)
if (opts.csv) {
let csvStr = "datetime,LAeq,LAmax,LAmin,10^(LAeq/10)\n"
if (!erg.err) {
for (let item of erg.values) {
if (item.n_AVG != -1) {
csvStr += item.datetime + ','
+ item.LAeq + ','
+ item.LA_max + ','
+ item.LA_min+ ','
+ item.E10tel_eq + '\n'
}
}
}
return csvStr
}
return erg
}
// *********************************************
// 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, props) => {
let erg = await getNoiseAVGData(opts)
if (opts.csv) {
let csvStr = "datetime,n_AVG,peakcount\n"
if (!erg.err) {
for (let item of erg.values) {
if (item.n_AVG != -1) {
csvStr += item.datetime + ',' + item.n_AVG + ',' + item.peakcount + '\n'
}
}
}
return csvStr
}
return {err: erg.err, values: erg.values}
}
// *********************************************
// 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';
if (!erg.err) {
for (let i = 0; i < erg.values.length; i += 24) {
let sum = 0;
let count = 0;
let pk = 0;
let werte = {};
for (let k = 0; k < 24; k++) {
const item = erg.values[i + k]
if ((item != null) && (item.n_sum != -1)) {
sum += item.n_sum;
count += item.count;
pk += item.peakcount;
if (werte.datetime === undefined) {
let dt = DateTime.fromISO(item.datetime)
werte.datetime = dt.startOf('day').toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'")
}
}
}
if (count === 0) {
werte.n_AVG = -1
} else {
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;
}
return {err: erg.err, values: val}
}
// addDatetime
// add the actual date to wert, if werte is undefined
const addDatetime = (werte, item) => {
if (werte.datetime === undefined) {
let dt = DateTime.fromISO(item.datetime)
werte.datetime = dt.startOf('day').toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'")
}
}
// *********************************************
// 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';
if (!erg.err) {
let done = false;
let dt;
// 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 16 hour for day and following 8 hour for night
const length = erg.values.length
for (let i = 6; i < length;) {
let dsum = 0, dcnt = 0;
let nsum = 0, ncnt = 0;
let werte = {};
for (let k = 0; k < 16; k++, i++) {
if ( i < length) {
const item = erg.values[i]
if (item.n_sum != -1) {
addDatetime(werte, item)
dsum += item.n_sum;
dcnt += item.count;
}
}
}
if (i < (length - 8)) {
const item = erg.values[i]
for (let k = 0; k < 8; k++, i++) {
if (i < length) {
if (item.n_sum != -1) {
addDatetime(werte, item)
nsum += item.n_sum;
ncnt += item.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;
}
return {err: erg.err, 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';
if (!erg.err) {
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
const length = erg.values.length
for (let i = 6; i < 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 ( i < length) {
const item = erg.values[i]
if (item.n_sum != -1) {
addDatetime(werte, item)
dsum += item.n_sum;
dcnt += item.count;
}
}
}
for (let k = 0; k < 4; k++, i++) {
if ( i < length) {
const item = erg.values[i]
if (item.n_sum != -1) {
addDatetime(werte, item)
esum += item.n_sum;
ecnt += item.count;
}
}
}
if (i < (erg.values.length - 8)) {
for (let k = 0; k < 8; k++, i++) {
if (i < length) {
const item = erg.values[i]
if (item.n_sum != -1) {
addDatetime(werte, item)
nsum += item.n_sum;
ncnt += item.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;
}
return {err: erg.err, values: val}
}
const getAPIprops = (opt) => {
}
const getNoiseAVGData = async (opts) => {
let ret
if (DBASE === 'mongo') {
ret = await mongo.fetchNoiseAVGData(opts)
} else if (DBASE === 'influx') {
ret = await influx.fetchNoiseAVGData(opts)
// Influx stores the average from 00:00h to 01:00h as 01:00h, so we have to shift the time 1 hour back
for (let x=0; x < ret.values.length; x++) {
ret.values[x].datetime = DateTime.fromISO(ret.values[x].datetime).toUTC().minus({hours:1}).toFormat("yyyy-LL-dd'T'HH:mm:ss'Z'")
}
} else {
ret.err = 'DBASEUNKNOWN'
}
if(ret.err) {
return returnOnError(ret, ret.err, getNoiseAVGData.name)
}
if(ret.values.length === 0) {
return returnOnError(ret, 'NODATA', 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}.
// For havg add the missed hours to the arry
let emptyValues = {n_AVG: -1}
if (opts.long) {
emptyValues.n_sum = -1
}
const misshours = DateTime.fromISO(ret.values[0].datetime).toUTC().get('hour')
let hoursArr = new Array(opts.span * 24 + misshours); // generate new array
hoursArr.fill(emptyValues) // fill array with 'empty' values
let startDay = DateTime.fromISO(ret.values[0].datetime).toUTC().get('day') // calc first day
let k = 0
for (let d of ret.values) { // loop through docs
let stunde = DateTime.fromISO(d.datetime).toUTC().get('hour') // get current hour
let day = DateTime.fromISO(d.datetime).toUTC().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 { err: ret.err, values: hoursArr}
}
const whatTable = [
{'what':'live', 'span': 1, 'daystart': false, peak: false, 'func': getLiveData},
{'what':'havg', 'span': 7, 'daystart': false, peak: true, 'func': gethavgData},
{'what':'davg', 'span': 30, 'daystart': true, peak: true, 'func': getdavgData},
{'what':'daynight', 'span': 30, 'daystart': true, peak: false, 'func': getdaynightData},
{'what':'lden', 'span': 30, 'daystart': true, peak: false, 'func': getLdenData},
{'what':'props', 'span': 0, 'daystart': true, peak:false, 'func': getAPIprops},
];
+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("geigeractivity 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)
})
})
+148
View File
@@ -0,0 +1,148 @@
// Utility routine for plotting the data
export const colors = {'eq': '#0000FF', 'max': '#FF0000', 'min': '#008000', 'peaks': '#DAA520'};
export const noise_ymin = 30;
export function createGlobObtions() {
// Options, die für alle Plots identisch sind
let globObject = {
chart: {
spacingRight: 20,
spacingLeft: 20,
spacingTop: 25,
backgroundColor: {
linearGradient: [0, 400, 0, 0],
stops: [
[0, '#eee'],//[0, '#ACD0AA'], //[0, '#A18D99'], // [0, '#886A8B'], // [0, '#F2D0B5'],
[1, '#fff']
]
},
type: 'line',
borderWidth: '2',
// events: {
// selection: function (event) {
// if (event.xAxis) {
// doUpdate = false;
// } else {
// doUpdate = true;
// }
// }
// }
},
title: {
align: 'left',
style: {'fontSize': '25px'},
useHTML: true,
},
subtitle: {
align: 'left',
},
tooltip: {
valueDecimals: 1,
backgroundColor: '#ffffff',
borderWidth: 0,
borderRadius: 0,
useHTML: true,
},
xAxis: {
type: 'datetime',
title: {
text: 'date/time',
},
gridLineWidth: 2,
labels: {
formatter: function () {
let v = this.axis.defaultLabelFormatter.call(this);
if (v.indexOf(':') == -1) {
return '<span style="font-weight:bold;color:red">' + v + '<span>';
} else {
return v;
}
}
},
},
legend: {
enabled: true,
layout: 'horizontal',
// verticalAlign: 'top',
borderWidth: 1,
align: 'center',
},
plotOptions: {
series: {
animation: false,
turboThreshold: 0,
marker: {
enabled: false,
},
},
}
};
return globObject;
}
export function calcWeekends(data, isyear) {
/* let weekend = [];
let oldDay = 8;
for (let i = 0; i < data.length; i++) {
let mom = moment(data[i].date);
if (isyear) {
mom = moment(data[i]._id)
}
let day = mom.day();
let st = mom.startOf('day');
if (day != oldDay) {
if (day == 6) {
weekend.push({
color: 'rgba(169,235,158,0.4)',
from: st.valueOf(),
to: st.add(1, 'days').valueOf(),
zIndex: 0
})
} else if (day == 0) {
weekend.push({
color: 'rgba(169,235,158,0.4)',
from: st.valueOf(),
to: st.add(1, 'days').valueOf(),
zIndex: 0
})
}
oldDay = day;
}
}
return weekend;
*/}
export function calcDays(data, isyear) {
let days = [];
if (data.length == 0) {
return days
}
let oldday = moment(data[0].date).day();
if (isyear) {
oldday = moment(data[0]._id).day();
}
for (let i = 0; i < data.length; i++) {
let m = moment(data[i].date);
if (isyear) {
m = moment(data[i]._id);
}
let tag = m.day()
if (tag != oldday) {
m.startOf('day');
days.push({color: 'lightgray', value: m.valueOf(), width: 1, zIndex: 2});
oldday = tag;
}
}
return days;
};
export const setoptionfromtable = (opt,tabval) => {
let ret = opt
if ((opt === null) || (opt === '') || (opt === undefined) || (opt < tabval)){
ret = tabval
}
return ret
}
+54
View File
@@ -0,0 +1,54 @@
// parse the params from http call
import {returnOnError} from "./reporterror.js"
const checkParams = (params, mo) => {
let o = {opts: {}, err: null}
if ((mo.mandatory.length !== 0) && (params === undefined)) {
return returnOnError(o, 'NOPARAMETER', checkParams.name )
}
for (let p of mo.mandatory) {
if (!(p.name in params)) {
return returnOnError(o, 'NOMANDPARAM', checkParams.name, p.name)
}
if (p.type === 'int') {
let x = parseInt(params[p.name])
if (isNaN(x)) {
return returnOnError(o, 'PARAMNONUM', checkParams.name, p.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, 'PARAMNONUM', checkParams.name, p.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
+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;
}
+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.toISO()} => *** ERROR *** ${str}`;
console.log(s);
}
+22
View File
@@ -0,0 +1,22 @@
import {logit} from "./logit.js";
import { translate as trans } from '../routes/api.js'
export const reportError = (message, errortext) => {
message.error = true
message.errortext = errortext
return message
}
export const returnOnError = (pr, error, name, p1='', p2='') => {
error = trans(error)
if (error.indexOf('xxx') !== -1) {
error = error.replace('xxx', p1)
}
if (error.indexOf('yyy') !== -1) {
error = error.replace('yyy', p1)
}
pr.err = error
logit(`${name}: ${error}`)
return pr
}