Compare commits

...

3 Commits

Author SHA1 Message Date
admin 392dc3597d Version 2.1.2: noise 3.3.2, sensorapi 1.4.3
Beide Komponenten wurden fuer die Korrektur der Stadtsuche angefasst, die
Versionen im Log und in der Oberfläche sollen das zeigen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:09:30 +00:00
admin 10cd964ede Stadtsuche in den Einstellungen repariert (graue Karte)
Wer auf dem Kartenreiter die Einstellungen oeffnete und eine Stadt eingab,
bekam danach nur noch ein graues Fenster. Drei Fehler wirkten zusammen:

1. getCityCoords() rief Nominatim ohne eigenen User-Agent auf. Nominatim
   beantwortet den axios-Default mit 403, und zwar fuer jede Stadt. Die
   Schwesterfunktion getAddress() setzt seit jeher einen User-Agent -
   deshalb funktionierten die Adressen an den Sensoren, nur die
   Stadtsuche nicht. Beide nutzen jetzt dieselbe Konstante.

2. cityCoords() lieferte im Fehlerfall den String "no coordinates". Der
   landete als Kartenzentrum in L.map().setView(), Leaflet warf "Invalid
   LatLng object" - und weil showMap() die alte Karte vorher schon
   entfernt hat, blieb der Container leer. Daher grau statt Fehlermeldung.
   cityCoords() liefert jetzt {coords, err}; schlaegt die Suche fehl,
   bleibt das bisherige Zentrum stehen und der Text wird angezeigt.

3. Der Proxy in noise/routes/api.js kodierte die bereits prozentkodierte
   req.originalUrl ein zweites Mal. 'Goettingen' kam als 'G%c3%b6ttingen'
   an. Das fiel bisher nicht auf, weil schon Punkt 1 alles abfing.

Ausserdem: Tippfehler getCityCoord.name, fehlendes encodeURI bei der
Stadtsuche, Koordinaten als Zahl statt String, und eine eigene Meldung
(ENOCITY), wenn Nominatim den Ort nicht kennt.

Geprueft ueber die volle Kette noise -> sensorapi -> Nominatim:
Stuttgart, Goettingen (Umlaut), Bad Cannstatt (Leerzeichen) liefern
Koordinaten, ein erfundener Ort eine saubere Meldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 15:03:27 +00:00
admin 887399ef85 readarchive: Sensoren eines Tages parallel abholen
Die Laufzeit steckt fast vollstaendig im Warten auf die CSV-Dateien von
archive.sensor.community - je Sensor rund 0,24 s, davon nur 0,02 s der
Influx-Write und 0,01 s die Mongo-Abfrage. Die Schleife ueber die Sensoren
laeuft deshalb jetzt mit mehreren Arbeitern gleichzeitig (-p bzw. PARALLEL,
Default 4, 1 = altes Verhalten).

Gemessen fuer 2026-08-01 (280 Laermsensoren, 344660 Werte, -d influx):
1 -> 79 s, 4 -> 33 s, 8 -> 35 s, 16 -> 31 s. Ab etwa 4 gleichzeitigen
Abrufen liefert das Archiv nicht mehr schneller; derselbe Verlauf zeigt
sich mit blossem curl ohne Datenbank (71 / 32 / 28 / 29 / 29 s bei 1, 4,
8, 16, 32 Abrufen). Der Default steht deshalb auf 4 und nicht hoeher -
mehr erzeugt nur Last beim fremden Server.

checkProperties() wird jetzt awaited - der Aufruf lief bisher ohne await
gegen das bulkWrite am Ende von main(), was mit parallelen Sensoren kein
Randfall mehr waere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 10:43:33 +00:00
12 changed files with 104 additions and 40 deletions
+1
View File
@@ -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.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.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.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
View File
@@ -1,7 +1,7 @@
{ {
"name": "laerm-web-sensorapi", "name": "laerm-web-sensorapi",
"version": "3.3.1", "version": "3.3.2",
"date": "2026-06-30 18:00 UTC", "date": "2026-08-03 12:00 UTC",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node bin/www.js >>/var/log/noise.log 2>&1" "start": "node bin/www.js >>/var/log/noise.log 2>&1"
+7 -4
View File
@@ -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) => { export const cityCoords = async (city) => {
let url = `/srv/getcitycoords?city=${city}` let url = `/srv/getcitycoords?city=${city}`
let erg = await fetchfromserver(url) let erg = await fetchfromserver(url)
if (!erg.err) { if (erg.err || !Array.isArray(erg.coords) || erg.coords.length !== 2) {
return erg.coords return {coords: null, err: erg.err || `'${city}' ?`}
} else {
return "no coordinates"
} }
return {coords: erg.coords, err: null}
} }
// table to distribute the different charts // table to distribute the different charts
+15 -3
View File
@@ -86,6 +86,7 @@ import * as spin from './spinner.js'
// Button 'Save' pressed // Button 'Save' pressed
document.querySelector('#btnSave').addEventListener('click', async () => { document.querySelector('#btnSave').addEventListener('click', async () => {
let curtab = getCurrentTab() let curtab = getCurrentTab()
let cityErr = null // Meldung erst nach dem Schliessen des Dialogs zeigen
for(let i = 0; i < setting.length; i++) { for(let i = 0; i < setting.length; i++) {
if(setting[i].typ === getCurrentTab()) { if(setting[i].typ === getCurrentTab()) {
for(let j = 0; j < setting[i].show.length; j++) { for(let j = 0; j < setting[i].show.length; j++) {
@@ -115,9 +116,17 @@ import * as spin from './spinner.js'
params.datetime = starttime params.datetime = starttime
} else { } else {
const oldCoords = params.center.coords const oldCoords = params.center.coords
params.center.coords = await cityCoords(params.center.name) const erg = await cityCoords(params.center.name)
checkNewCenter(oldCoords, params.center.coords) if (erg.coords === null) {
localStorage.setItem('centercity',JSON.stringify(params.center)) // 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 newlng = document.querySelector('#sellan input:checked').id
let oldlng = localStorage.getItem('curlang') let oldlng = localStorage.getItem('curlang')
@@ -129,6 +138,9 @@ import * as spin from './spinner.js'
spin.spinner.spin(spin.spindiv) spin.spinner.spin(spin.spindiv)
await loadAll(params,0, curtab) await loadAll(params,0, curtab)
spin.spinner.stop() spin.spinner.stop()
if (cityErr) {
showError(cityErr)
}
// ToDo: // ToDo:
// Load ALL incl. MAP und LIVE hier, d.h. an loadAll einen zusätzlichen Parameter übergeben // Load ALL incl. MAP und LIVE hier, d.h. an loadAll einen zusätzlichen Parameter übergeben
// if(params.sid !== undefined) { // if(params.sid !== undefined) {
+4 -1
View File
@@ -29,7 +29,10 @@ router.get('/:cmd', async function(req, res, next) {
let url = APIHOST + '/api' + req.originalUrl.slice(4) + `&lng=${lng}` let url = APIHOST + '/api' + req.originalUrl.slice(4) + `&lng=${lng}`
try { 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} headers: {'X-API-Key': API_KEY}
}); });
if (response.status !== 200) { if (response.status !== 200) {
+3 -1
View File
@@ -16,7 +16,8 @@
# #
# readFromcsv.js liest jeden Parameter auch aus einer Env-Variablen: # readFromcsv.js liest jeden Parameter auch aus einer Env-Variablen:
# -s = START (Default: gestern), -e = END (Default: Startdatum + 1 Tag, # -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: # Voraussetzungen:
# * Der Haupt-Stack laeuft, denn dessen Netz wird hier eingebunden. Der Name # * Der Haupt-Stack laeuft, denn dessen Netz wird hier eingebunden. Der Name
@@ -46,6 +47,7 @@ services:
END: ${END:-} END: ${END:-}
TYP: ${TYP:-noise} TYP: ${TYP:-noise}
DBASE: ${DBASE:-influx} DBASE: ${DBASE:-influx}
PARALLEL: ${PARALLEL:-4}
volumes: volumes:
- ${LOCALDIR}/noisesensors/log:/var/log - ${LOCALDIR}/noisesensors/log:/var/log
- ${LOCALDIR}/noisesensors/data/readarchive:/opt/app/readarchive/data - ${LOCALDIR}/noisesensors/data/readarchive:/opt/app/readarchive/data
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "sensors_readfromcsv", "name": "sensors_readfromcsv",
"version": "3.1.2", "version": "3.2.0",
"date": "2023-12-23", "date": "2026-08-03",
"description": "", "description": "",
"main": "readfromcsv.js", "main": "readfromcsv.js",
"scripts": { "scripts": {
+50 -19
View File
@@ -6,6 +6,9 @@
// Version: // 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 // V 3.1.1 2023-11-14 rxf
// - Enddatum eingeführt // - Enddatum eingeführt
// //
@@ -23,7 +26,12 @@
const TYP = process.env.TYP || 'noise' const TYP = process.env.TYP || 'noise'
const START = process.env.START const START = process.env.START
const END = process.env.END 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'] const DATABASE = ['mongo', 'influx', 'both']
@@ -228,29 +236,42 @@ async function readSensorsperDay(client, args) {
return insertedCount 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) => { const scanOneSensor = async (client, sensorList, dt, args) => {
let inserted = 0 let inserted = 0
let missed = [] let missed = []
let listcnt = sensorList.length let list = sensorList
for (let item of sensorList) { // iterate the list 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 let sid = item.sensorid
if(args.sensornbr !== undefined) {
if(args.sensornbr !== sid) {
continue
}
}
logit(`${item.url}`) logit(`${item.url}`)
let icount = await putOneSensorInDb(client, item.url, sid, dt, missed, item.indoor, item.sensorType, args) // put one sensor data into DB 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`) logit(`${listcnt} -- Sensor ${sid}: ${icount} Einträge`)
inserted += icount inserted += icount
listcnt-- listcnt--
if (args.nbrOfEntries !== undefined) { })
if (--args.nbrOfEntries === 0) {
break
}
}
}
return {inserted: inserted, missed: missed} 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` 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) { } catch (e) {
console.log(e) console.log(e)
missed.push(name) missed.push(name)
@@ -422,10 +445,10 @@ async function enterOneSensorinDB(client, sid, erg, dataline, day, styp, args) {
// Parse command line options // Parse command line options
function parse_cmdline(argv) { 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 option;
let std = START ? START : DateTime.now().startOf('day').minus({days: 1}).toFormat('yyyy-LL-dd'); // yesterday 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) { while((option = parser.getopt()) !== undefined) {
switch(option.option) { switch(option.option) {
@@ -460,6 +483,13 @@ function parse_cmdline(argv) {
} }
break; break;
case 'p':
let par = parseInt(option.optarg.trim())
if (par > 0) {
ret.parallel = par
}
break;
case 'v': case 'v':
console.log(`Version: ${pkg.version} from ${pkg.date}`); console.log(`Version: ${pkg.version} from ${pkg.date}`);
console.log(); console.log();
@@ -467,7 +497,7 @@ function parse_cmdline(argv) {
break; break;
case 'h': 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) { if (DEVELOP) {
console.log(" [-a entries] [-n sensorid]"); 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(" -t sensorType: if given, only those sensors will be used; default: 'noise'");
console.log(" allowed types: 'noise', 'radiactivity', 'pm' and 'thp'") console.log(" allowed types: 'noise', 'radiactivity', 'pm' and 'thp'")
console.log(" -d database: 'mongo', 'influx' or 'both'; default: 'both'") 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(" -v version: show version");
console.log(" -h this help text"); console.log(" -h this help text");
if(DEVELOP) { if(DEVELOP) {
+14 -4
View File
@@ -6,17 +6,27 @@ import { response } from "express"
const NOMINATIM_URL = `https://nominatim.openstreetmap.org/reverse?lat=${'xx'}&lon=${'yy'}&format=json` 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` 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) => { export const getCityCoords = async (params) => {
let ret = {coords: [], city: params.city, err: null} let ret = {coords: [], city: params.city, err: null}
let url = NOMINATIM_CITY_URL.replace('xx', params.city) let url = NOMINATIM_CITY_URL.replace('xx', params.city)
// let url = 'https://nominatim.openstreetmap.org/?q="K%C3%B6ln"&format=json' // let url = 'https://nominatim.openstreetmap.org/?q="K%C3%B6ln"&format=json'
try { try {
const response = await axios(url) const response = await axios(encodeURI(url), {
headers: {
'User-Agent': NOMINATIM_UA
}
})
if (response.status !== 200) { 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)) logit(JSON.stringify(ret.coords))
} catch (e) { } catch (e) {
return returnOnError(ret, e, getCityCoords.name) return returnOnError(ret, e, getCityCoords.name)
@@ -35,7 +45,7 @@ export const getAddress = async (params) => {
try { try {
const response = await axios(encodeURI(url),{ const response = await axios(encodeURI(url),{
headers: { headers: {
'User-Agent': 'Firefox 132.0.1' 'User-Agent': NOMINATIM_UA
} }
}); });
if (response.status !== 200) { if (response.status !== 200) {
+2 -1
View File
@@ -12,5 +12,6 @@
"SYNTAXURL": "Syntax Fehler beim Aufruf der URL", "SYNTAXURL": "Syntax Fehler beim Aufruf der URL",
"WRONGTYPE": "Sensor xxx ist kein yyy Sensor", "WRONGTYPE": "Sensor xxx ist kein yyy Sensor",
"NOLASTDATES": "Probleme bein abholen der letzten Daten", "NOLASTDATES": "Probleme bein abholen der letzten Daten",
"NOPROPSFOUND": "Properties Collection nicht gefunden" "NOPROPSFOUND": "Properties Collection nicht gefunden",
"ENOCITY": "Ort \"xxx\" nicht gefunden"
} }
+2 -1
View File
@@ -12,5 +12,6 @@
"SYNTAXURL": "Syntax error in calling url!", "SYNTAXURL": "Syntax error in calling url!",
"WRONGTYPE": "Sensor xxx is not of type yyy", "WRONGTYPE": "Sensor xxx is not of type yyy",
"NOLASTDATES": "Problems fetching last dates from database", "NOLASTDATES": "Problems fetching last dates from database",
"NOPROPSFOUND": "Properties collection not found" "NOPROPSFOUND": "Properties collection not found",
"ENOCITY": "City 'xxx' not found"
} }
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "sensorapi", "name": "sensorapi",
"version": "1.4.2", "version": "1.4.3",
"date": "2023-11-29 16:00 UTC", "date": "2026-08-03 12:00 UTC",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "node ./bin/www.js >>/var/log/sensorapi.log 2>&1", "start": "node ./bin/www.js >>/var/log/sensorapi.log 2>&1",