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