Initial monorepo: readin, sensorapi, noise (Node22, npm ci, axios1.x, i18next-fs-backend)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
node_modules/
|
||||
log/
|
||||
*.bak
|
||||
*.bak-*
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -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>`.
|
||||
@@ -0,0 +1,9 @@
|
||||
build_and_copy.sh
|
||||
Dockerfile_noise
|
||||
.vscode
|
||||
node_modules
|
||||
log
|
||||
.env*
|
||||
.gitignore
|
||||
deploy.sh
|
||||
Readme.*
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules
|
||||
log
|
||||
.env
|
||||
Readme.pdf
|
||||
Vendored
+26
@@ -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==",
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
Executable
+97
@@ -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);
|
||||
}
|
||||
Executable
+63
@@ -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'
|
||||
@@ -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 /> `
|
||||
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 + '">● </span>' +
|
||||
// this.series.name + ': <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 /> `
|
||||
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 /> `
|
||||
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 /> `
|
||||
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 /> `
|
||||
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;">' +
|
||||
' ' + txt + '<br />' +
|
||||
'<span style="color: ' + this.point.color + '">● </span>' +
|
||||
this.series.name + ': <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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Executable
+58
@@ -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"
|
||||
@@ -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!
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
Generated
+2117
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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: <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
|
||||
})
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
@@ -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: '© <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
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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>' +
|
||||
' ' + dba[i] + (i == 0 ? '+' : '') + '<br />'
|
||||
}
|
||||
div_color.innerHTML += ' <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
|
||||
}
|
||||
|
||||
@@ -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})`)
|
||||
}
|
||||
|
||||
@@ -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 + '">● </span>' +
|
||||
this.series.name + ': <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
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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"}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
@@ -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")
|
||||
@@ -0,0 +1,6 @@
|
||||
extends layout
|
||||
|
||||
block content
|
||||
h1= message
|
||||
h2= error.status
|
||||
pre #{error.stack}
|
||||
@@ -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") ×
|
||||
.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") ×
|
||||
.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 (max. 10)
|
||||
.column#pklim
|
||||
label.thelabels(for='peaklim')
|
||||
| #{t("Count_peaks_over")}
|
||||
input.theInputs#peaklim(type='number' name='peaklim' min="10" max="130")
|
||||
| dbA
|
||||
span.klein (max. 130)
|
||||
.column#odth
|
||||
label.thelabels(for='olderthan')
|
||||
| #{t("RemoveFromMap")}
|
||||
input.theInputs#olderthan(type='number' name='olderthan' min="0" max="52")
|
||||
| #{t("weeks")}
|
||||
span.klein (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") ×
|
||||
.dialog-body
|
||||
.dialog-footer
|
||||
button.dialog-button.primary#btnResetOk(type="button") OK
|
||||
@@ -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")
|
||||
@@ -0,0 +1,10 @@
|
||||
node-modules
|
||||
.gitignore
|
||||
.dockerignore
|
||||
build_and_copy.sh
|
||||
docker-compose.yml
|
||||
Dockerfile*
|
||||
INFLUXSERVER
|
||||
TRANSFERE
|
||||
data
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
data/
|
||||
node_modules
|
||||
.idea
|
||||
.env
|
||||
log
|
||||
docs/Readme.md_x
|
||||
docs/Readme_tmp.html
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Executable
+63
@@ -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'
|
||||
@@ -0,0 +1,2 @@
|
||||
3-59/5 * * * * cd /opt/app && npm start
|
||||
|
||||
Executable
+58
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
*/
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
Generated
+1020
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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]
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/var/log/timeseries.log {
|
||||
daily
|
||||
rotate 7
|
||||
delaycompress
|
||||
compress
|
||||
notifempty
|
||||
missingok
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
build_and_copy.sh
|
||||
Dockerfile_sensorapi
|
||||
docker-compose.yml
|
||||
.vscode
|
||||
log
|
||||
mocks
|
||||
test
|
||||
node_modules
|
||||
doc
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
.idea
|
||||
.env*
|
||||
.DS_Store
|
||||
sensorapi.log
|
||||
Vendored
+75
@@ -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:*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 : "") + " " + (akt.plz ? akt.plz : "") + " " + akt.city;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("onMarker - getpops", e)
|
||||
}
|
||||
console.log("addr:", addr);
|
||||
return addr;
|
||||
|
||||
*/
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
Executable
+63
@@ -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'
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
*/
|
||||
Executable
+58
@@ -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 |
@@ -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
|
||||
.
|
||||
####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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Generated
+2915
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
*/
|
||||
@@ -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},
|
||||
];
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user