Compare commits
3 Commits
eeab1ddbe5
...
392dc3597d
| Author | SHA1 | Date | |
|---|---|---|---|
| 392dc3597d | |||
| 10cd964ede | |||
| 887399ef85 |
@@ -224,6 +224,7 @@ Image-Tag im Stack auf den in Schritt 1 gesicherten Stand setzen, die Volume-Zei
|
||||
2.0.0 | 2026-07-25 | Anpassung an Stack-Änderungen
|
||||
2.1.0 | 2026-08-01 | readarchive dazu, gemeinsame Module unter common/, Zeitstempel in UTC
|
||||
2.1.1 | 2026-08-02 | readarchive als eigenes Stack (readarchive-stack.yml)
|
||||
2.1.2 | 2026-08-03 | Stadtsuche in den Einstellungen repariert (graue Karte), readarchive holt die Sensoren eines Tages parallel
|
||||
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "laerm-web-sensorapi",
|
||||
"version": "3.3.1",
|
||||
"date": "2026-06-30 18:00 UTC",
|
||||
"version": "3.3.2",
|
||||
"date": "2026-08-03 12:00 UTC",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node bin/www.js >>/var/log/noise.log 2>&1"
|
||||
|
||||
@@ -255,14 +255,17 @@ export const addAddress = async (sid) => {
|
||||
}
|
||||
}
|
||||
|
||||
// liefert {coords: [lat, lon], err: null} oder {coords: null, err: <Meldung>}.
|
||||
// Kein Ersatzwert wie frueher "no coordinates": der Aufrufer muss den Fehlschlag
|
||||
// erkennen koennen, sonst landet ein ungueltiges Zentrum in der Karte und
|
||||
// Leaflet baut sie nicht mehr auf (graues Fenster).
|
||||
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"
|
||||
if (erg.err || !Array.isArray(erg.coords) || erg.coords.length !== 2) {
|
||||
return {coords: null, err: erg.err || `'${city}' ?`}
|
||||
}
|
||||
return {coords: erg.coords, err: null}
|
||||
}
|
||||
|
||||
// table to distribute the different charts
|
||||
|
||||
@@ -86,6 +86,7 @@ import * as spin from './spinner.js'
|
||||
// Button 'Save' pressed
|
||||
document.querySelector('#btnSave').addEventListener('click', async () => {
|
||||
let curtab = getCurrentTab()
|
||||
let cityErr = null // Meldung erst nach dem Schliessen des Dialogs zeigen
|
||||
for(let i = 0; i < setting.length; i++) {
|
||||
if(setting[i].typ === getCurrentTab()) {
|
||||
for(let j = 0; j < setting[i].show.length; j++) {
|
||||
@@ -115,9 +116,17 @@ import * as spin from './spinner.js'
|
||||
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))
|
||||
const erg = await cityCoords(params.center.name)
|
||||
if (erg.coords === null) {
|
||||
// Ort nicht aufloesbar: altes Zentrum behalten. Ohne das landet
|
||||
// ein ungueltiger Wert in setView() und die Karte bleibt grau.
|
||||
cityErr = erg.err
|
||||
params.center = JSON.parse(localStorage.getItem('centercity'))
|
||||
} else {
|
||||
params.center.coords = erg.coords
|
||||
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')
|
||||
@@ -129,6 +138,9 @@ import * as spin from './spinner.js'
|
||||
spin.spinner.spin(spin.spindiv)
|
||||
await loadAll(params,0, curtab)
|
||||
spin.spinner.stop()
|
||||
if (cityErr) {
|
||||
showError(cityErr)
|
||||
}
|
||||
// ToDo:
|
||||
// Load ALL incl. MAP und LIVE hier, d.h. an loadAll einen zusätzlichen Parameter übergeben
|
||||
// if(params.sid !== undefined) {
|
||||
|
||||
+4
-1
@@ -29,7 +29,10 @@ router.get('/:cmd', async function(req, res, next) {
|
||||
|
||||
let url = APIHOST + '/api' + req.originalUrl.slice(4) + `&lng=${lng}`
|
||||
try {
|
||||
const response = await axios.get(encodeURI(url) , {
|
||||
// kein encodeURI: req.originalUrl ist bereits prozentkodiert. Ein zweiter
|
||||
// Durchlauf macht aus '%C3%B6' ein '%25C3%25B6', und die Gegenseite sieht
|
||||
// dann den Text 'G%c3%b6ttingen' statt 'Göttingen'.
|
||||
const response = await axios.get(url , {
|
||||
headers: {'X-API-Key': API_KEY}
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
#
|
||||
# readFromcsv.js liest jeden Parameter auch aus einer Env-Variablen:
|
||||
# -s = START (Default: gestern), -e = END (Default: Startdatum + 1 Tag,
|
||||
# exklusiv), -t = TYP (Default: noise), -d = DBASE (Default: both).
|
||||
# exklusiv), -t = TYP (Default: noise), -d = DBASE (Default: both),
|
||||
# -p = PARALLEL (Default: 4) - so viele Sensoren werden gleichzeitig geholt.
|
||||
#
|
||||
# Voraussetzungen:
|
||||
# * Der Haupt-Stack laeuft, denn dessen Netz wird hier eingebunden. Der Name
|
||||
@@ -46,6 +47,7 @@ services:
|
||||
END: ${END:-}
|
||||
TYP: ${TYP:-noise}
|
||||
DBASE: ${DBASE:-influx}
|
||||
PARALLEL: ${PARALLEL:-4}
|
||||
volumes:
|
||||
- ${LOCALDIR}/noisesensors/log:/var/log
|
||||
- ${LOCALDIR}/noisesensors/data/readarchive:/opt/app/readarchive/data
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "sensors_readfromcsv",
|
||||
"version": "3.1.2",
|
||||
"date": "2023-12-23",
|
||||
"version": "3.2.0",
|
||||
"date": "2026-08-03",
|
||||
"description": "",
|
||||
"main": "readfromcsv.js",
|
||||
"scripts": {
|
||||
|
||||
+50
-19
@@ -6,6 +6,9 @@
|
||||
|
||||
// Version:
|
||||
//
|
||||
// V 3.2.0 2026-08-03 rxf
|
||||
// - Sensoren eines Tages werden parallel abgearbeitet (-p / PARALLEL)
|
||||
//
|
||||
// V 3.1.1 2023-11-14 rxf
|
||||
// - Enddatum eingeführt
|
||||
//
|
||||
@@ -23,7 +26,12 @@
|
||||
const TYP = process.env.TYP || 'noise'
|
||||
const START = process.env.START
|
||||
const END = process.env.END
|
||||
const DBASE = process.env.DBASE || 'both'
|
||||
const DBASE = process.env.DBASE || 'both'
|
||||
// Anzahl der Sensoren, die gleichzeitig geholt werden. Die Laufzeit steckt fast
|
||||
// vollstaendig im Warten auf archive.sensor.community (~0,15 s je CSV), nicht in
|
||||
// Mongo/Influx - deshalb bringt Parallelitaet hier den Hebel. 1 = altes Verhalten.
|
||||
// Mehr als 4 bringt nichts: ab da liefert das Archiv nicht mehr schneller.
|
||||
const PARALLEL = parseInt(process.env.PARALLEL) || 4
|
||||
|
||||
const DATABASE = ['mongo', 'influx', 'both']
|
||||
|
||||
@@ -228,29 +236,42 @@ async function readSensorsperDay(client, args) {
|
||||
return insertedCount
|
||||
}
|
||||
|
||||
// 'limit' Arbeiter teilen sich die Liste: jeder nimmt sich den naechsten freien
|
||||
// Eintrag, sobald er fertig ist. Kein Batching, damit ein langsamer Abruf die
|
||||
// anderen nicht aufhaelt. Node ist single-threaded - die gemeinsamen Zaehler und
|
||||
// Arrays brauchen deshalb keine Sperre, es laeuft immer nur ein Stueck Code
|
||||
// zwischen zwei await-Punkten.
|
||||
const runPool = async (items, limit, worker) => {
|
||||
let next = 0
|
||||
const runner = async () => {
|
||||
while (next < items.length) {
|
||||
const i = next++
|
||||
await worker(items[i])
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({length: Math.min(limit, items.length)}, runner))
|
||||
}
|
||||
|
||||
const scanOneSensor = async (client, sensorList, dt, args) => {
|
||||
let inserted = 0
|
||||
let missed = []
|
||||
let listcnt = sensorList.length
|
||||
for (let item of sensorList) { // iterate the list
|
||||
let list = sensorList
|
||||
if (args.sensornbr !== undefined) {
|
||||
list = list.filter((item) => item.sensorid === args.sensornbr)
|
||||
}
|
||||
if (args.nbrOfEntries !== undefined) {
|
||||
list = list.slice(0, args.nbrOfEntries)
|
||||
args.nbrOfEntries -= list.length
|
||||
}
|
||||
let listcnt = list.length
|
||||
await runPool(list, args.parallel, async (item) => {
|
||||
let sid = item.sensorid
|
||||
if(args.sensornbr !== undefined) {
|
||||
if(args.sensornbr !== sid) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
logit(`${item.url}`)
|
||||
let icount = await putOneSensorInDb(client, item.url, sid, dt, missed, item.indoor, item.sensorType, args) // put one sensor data into DB
|
||||
logit(`${listcnt} -- Sensor ${sid}: ${icount} Einträge`)
|
||||
inserted += icount
|
||||
listcnt--
|
||||
if (args.nbrOfEntries !== undefined) {
|
||||
if (--args.nbrOfEntries === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
return {inserted: inserted, missed: missed}
|
||||
}
|
||||
|
||||
@@ -343,7 +364,9 @@ async function readOneSensorOneDay(client, name, sid, dt, missed, indoor, args)
|
||||
dataline += `${typ},sid=${entry.sensorid} ${influxvalue} ${entry.datetime.valueOf()}\n`
|
||||
}
|
||||
}
|
||||
checkProperties(client, firstItem, indoor, sname, typ, dt, newProps)
|
||||
// await, weil newProps sonst mit dem bulkWrite am Ende von main() um die
|
||||
// Wette laeuft - bei parallelen Sensoren ist das kein Randfall mehr
|
||||
await checkProperties(client, firstItem, indoor, sname, typ, dt, newProps)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
missed.push(name)
|
||||
@@ -422,10 +445,10 @@ async function enterOneSensorinDB(client, sid, erg, dataline, day, styp, args) {
|
||||
|
||||
// Parse command line options
|
||||
function parse_cmdline(argv) {
|
||||
let parser = new mod_getopt.BasicParser('s:(start)e:(end)t:(type)h(help)v(version)a:(entries)n:(sensorid)d:(dbase)',argv);
|
||||
let parser = new mod_getopt.BasicParser('s:(start)e:(end)t:(type)h(help)v(version)a:(entries)n:(sensorid)d:(dbase)p:(parallel)',argv);
|
||||
let option;
|
||||
let std = START ? START : DateTime.now().startOf('day').minus({days: 1}).toFormat('yyyy-LL-dd'); // yesterday
|
||||
let ret = {startDate: std, endDate: '', sensorType: TYP, database: DBASE};
|
||||
let ret = {startDate: std, endDate: '', sensorType: TYP, database: DBASE, parallel: PARALLEL};
|
||||
|
||||
while((option = parser.getopt()) !== undefined) {
|
||||
switch(option.option) {
|
||||
@@ -460,6 +483,13 @@ function parse_cmdline(argv) {
|
||||
}
|
||||
break;
|
||||
|
||||
case 'p':
|
||||
let par = parseInt(option.optarg.trim())
|
||||
if (par > 0) {
|
||||
ret.parallel = par
|
||||
}
|
||||
break;
|
||||
|
||||
case 'v':
|
||||
console.log(`Version: ${pkg.version} from ${pkg.date}`);
|
||||
console.log();
|
||||
@@ -467,7 +497,7 @@ function parse_cmdline(argv) {
|
||||
break;
|
||||
|
||||
case 'h':
|
||||
console.log("Usage: node readFromcvs.js [-h] [-s startDate] [-e endDate] [-t sensorType] [-v version] [-h help]")
|
||||
console.log("Usage: node readFromcvs.js [-h] [-s startDate] [-e endDate] [-t sensorType] [-d database] [-p parallel] [-v version] [-h help]")
|
||||
if (DEVELOP) {
|
||||
console.log(" [-a entries] [-n sensorid]");
|
||||
}
|
||||
@@ -478,6 +508,7 @@ function parse_cmdline(argv) {
|
||||
console.log(" -t sensorType: if given, only those sensors will be used; default: 'noise'");
|
||||
console.log(" allowed types: 'noise', 'radiactivity', 'pm' and 'thp'")
|
||||
console.log(" -d database: 'mongo', 'influx' or 'both'; default: 'both'")
|
||||
console.log(` -p parallel: number of sensors fetched at the same time; default: ${PARALLEL}`)
|
||||
console.log(" -v version: show version");
|
||||
console.log(" -h this help text");
|
||||
if(DEVELOP) {
|
||||
|
||||
@@ -6,17 +6,27 @@ 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`
|
||||
// Nominatim verlangt laut Nutzungsbedingungen einen eigenen User-Agent und
|
||||
// beantwortet den Default von axios ('axios/1.x') mit 403.
|
||||
const NOMINATIM_UA = 'laermsensor-stack (rxf@gmx.de)'
|
||||
|
||||
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)
|
||||
const response = await axios(encodeURI(url), {
|
||||
headers: {
|
||||
'User-Agent': NOMINATIM_UA
|
||||
}
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
return returnOnError(ret, 'RESPSTATUS', getCityCoord.name, response.status)
|
||||
return returnOnError(ret, 'RESPSTATUS', getCityCoords.name, response.status)
|
||||
}
|
||||
ret.coords = [response.data[0].lat,response.data[0].lon]
|
||||
if (!response.data.length) { // Stadt nicht gefunden
|
||||
return returnOnError(ret, 'ENOCITY', getCityCoords.name, params.city)
|
||||
}
|
||||
ret.coords = [parseFloat(response.data[0].lat), parseFloat(response.data[0].lon)]
|
||||
logit(JSON.stringify(ret.coords))
|
||||
} catch (e) {
|
||||
return returnOnError(ret, e, getCityCoords.name)
|
||||
@@ -35,7 +45,7 @@ export const getAddress = async (params) => {
|
||||
try {
|
||||
const response = await axios(encodeURI(url),{
|
||||
headers: {
|
||||
'User-Agent': 'Firefox 132.0.1'
|
||||
'User-Agent': NOMINATIM_UA
|
||||
}
|
||||
});
|
||||
if (response.status !== 200) {
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"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"
|
||||
"NOPROPSFOUND": "Properties Collection nicht gefunden",
|
||||
"ENOCITY": "Ort \"xxx\" nicht gefunden"
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@
|
||||
"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"
|
||||
"NOPROPSFOUND": "Properties collection not found",
|
||||
"ENOCITY": "City 'xxx' not found"
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "sensorapi",
|
||||
"version": "1.4.2",
|
||||
"date": "2023-11-29 16:00 UTC",
|
||||
"version": "1.4.3",
|
||||
"date": "2026-08-03 12:00 UTC",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www.js >>/var/log/sensorapi.log 2>&1",
|
||||
|
||||
Reference in New Issue
Block a user