21050f51c9
Ruft dieselben Parameter (sensorid/data/span/datetime/peak) gegen zwei "getsensordata"-Endpunkte ab und diffed die Werte zeitstempelweise, generisch über alle Felder (funktioniert für live genauso wie für havg/davg/daynight/lden). Diente als Verifikation der VictoriaMetrics-Anbindung: live-Werte stimmen exakt, havg-Werte liegen weit innerhalb der Toleranz gegenüber der Influx-Produktion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
250 lines
8.9 KiB
JavaScript
250 lines
8.9 KiB
JavaScript
// compare.js - vergleicht die Laermwerte zweier "noise"-API-Instanzen
|
|
// (z.B. lokaler VictoriaMetrics-Test-Stack vs. Produktion) fuer denselben
|
|
// Sensor/Zeitraum/Auswertungstyp. Siehe Laerm_API.md fuer die API-Parameter.
|
|
//
|
|
// Aufruf: node compare.js [-s sensorid] [-d live|havg|davg|daynight|lden] ...
|
|
// node compare.js -h fuer alle Optionen
|
|
|
|
import axios from 'axios'
|
|
import { DateTime } from 'luxon'
|
|
import mod_getopt from 'posix-getopt'
|
|
|
|
const DEFAULT_URL_A = 'http://localhost:3003/api/getsensordata'
|
|
const DEFAULT_URL_B = 'https://noise.citysensor.de/api/getsensordata'
|
|
const DEFAULT_SENSORID = '37833'
|
|
const DEFAULT_DATA = 'live'
|
|
const DEFAULT_SPAN = '1'
|
|
const DEFAULT_PEAK = '70'
|
|
const DEFAULT_TOLERANCE = 0.05
|
|
const MAX_EXAMPLES = 20
|
|
|
|
function parseArgs(argv) {
|
|
let opts = {
|
|
urlA: DEFAULT_URL_A,
|
|
urlB: DEFAULT_URL_B,
|
|
sensorid: DEFAULT_SENSORID,
|
|
data: DEFAULT_DATA,
|
|
span: DEFAULT_SPAN,
|
|
datetime: null,
|
|
peak: DEFAULT_PEAK,
|
|
tolerance: DEFAULT_TOLERANCE,
|
|
}
|
|
let parser = new mod_getopt.BasicParser(
|
|
'a:(urlA)b:(urlB)s:(sensorid)d:(data)p:(span)t:(datetime)k:(peak)e:(tolerance)h(help)v(version)',
|
|
argv
|
|
)
|
|
let option
|
|
while ((option = parser.getopt()) !== undefined) {
|
|
switch (option.option) {
|
|
case 'a': opts.urlA = option.optarg; break
|
|
case 'b': opts.urlB = option.optarg; break
|
|
case 's': opts.sensorid = option.optarg; break
|
|
case 'd': opts.data = option.optarg; break
|
|
case 'p': opts.span = option.optarg; break
|
|
case 't': opts.datetime = option.optarg; break
|
|
case 'k': opts.peak = option.optarg; break
|
|
case 'e': opts.tolerance = parseFloat(option.optarg); break
|
|
case 'v':
|
|
console.log('compare-laermwerte 1.0.0')
|
|
process.exit()
|
|
break
|
|
case 'h':
|
|
console.log('Usage: node compare.js [options]')
|
|
console.log('Options:')
|
|
console.log(` -a urlA Basis-URL Implementierung A (default: ${DEFAULT_URL_A})`)
|
|
console.log(` -b urlB Basis-URL Implementierung B (default: ${DEFAULT_URL_B})`)
|
|
console.log(` -s sensorid Sensor-ID (default: ${DEFAULT_SENSORID})`)
|
|
console.log(` -d data live|havg|davg|daynight|lden (default: ${DEFAULT_DATA})`)
|
|
console.log(` -p span Zeitspanne in Tagen (default: ${DEFAULT_SPAN})`)
|
|
console.log(' -t datetime Start-Zeitpunkt ISO8601 (default: unbelegt -> jetzt - span)')
|
|
console.log(` -k peak dB-Schwelle fuer peakcount, nur havg/davg (default: ${DEFAULT_PEAK})`)
|
|
console.log(` -e tolerance Toleranz fuer Zahlenvergleich, absolut (default: ${DEFAULT_TOLERANCE})`)
|
|
console.log(' -v Version anzeigen')
|
|
console.log(' -h diese Hilfe')
|
|
process.exit()
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return opts
|
|
}
|
|
|
|
function buildURL(base, opts) {
|
|
let params = {
|
|
sensorid: opts.sensorid,
|
|
data: opts.data,
|
|
span: opts.span,
|
|
peak: opts.peak,
|
|
}
|
|
if (opts.datetime) {
|
|
params.datetime = opts.datetime
|
|
}
|
|
let qs = new URLSearchParams(params).toString()
|
|
return `${base}?${qs}`
|
|
}
|
|
|
|
async function fetchSide(label, url) {
|
|
try {
|
|
let ret = await axios.get(url, { timeout: 15000 })
|
|
if (ret.data && ret.data.err) {
|
|
return { ok: false, err: `${label}: API-Fehler: ${ret.data.err}` }
|
|
}
|
|
return { ok: true, values: ret.data.values || [], options: ret.data.options }
|
|
} catch (e) {
|
|
let msg = e.response ? `HTTP ${e.response.status}` : e.message
|
|
return { ok: false, err: `${label}: Request fehlgeschlagen: ${msg}` }
|
|
}
|
|
}
|
|
|
|
function toMap(values) {
|
|
let map = new Map()
|
|
for (let row of values) {
|
|
let ms = DateTime.fromISO(row.datetime, { zone: 'utc' }).toMillis()
|
|
if (Number.isNaN(ms)) {
|
|
continue
|
|
}
|
|
map.set(ms, row)
|
|
}
|
|
return map
|
|
}
|
|
|
|
function diffRows(mapA, mapB, tolerance) {
|
|
let onlyA = []
|
|
let onlyB = []
|
|
let commonCount = 0
|
|
let fieldStats = new Map() // field -> {count, sumDiff, maxDiff, mismatchCount}
|
|
let examples = []
|
|
|
|
for (let ts of mapA.keys()) {
|
|
if (!mapB.has(ts)) {
|
|
onlyA.push(ts)
|
|
}
|
|
}
|
|
for (let ts of mapB.keys()) {
|
|
if (!mapA.has(ts)) {
|
|
onlyB.push(ts)
|
|
}
|
|
}
|
|
|
|
for (let [ts, rowA] of mapA) {
|
|
let rowB = mapB.get(ts)
|
|
if (!rowB) {
|
|
continue
|
|
}
|
|
commonCount++
|
|
let fields = new Set([...Object.keys(rowA), ...Object.keys(rowB)])
|
|
fields.delete('datetime')
|
|
for (let field of fields) {
|
|
let a = (field in rowA) ? rowA[field] : null
|
|
let b = (field in rowB) ? rowB[field] : null
|
|
if (a === null && b === null) {
|
|
continue
|
|
}
|
|
let stat = fieldStats.get(field)
|
|
if (!stat) {
|
|
stat = { count: 0, sumDiff: 0, maxDiff: 0, mismatchCount: 0 }
|
|
fieldStats.set(field, stat)
|
|
}
|
|
stat.count++
|
|
if (typeof a !== 'number' || typeof b !== 'number') {
|
|
stat.mismatchCount++
|
|
if (examples.length < MAX_EXAMPLES) {
|
|
examples.push({ ts, field, a, b, reason: 'null-mismatch' })
|
|
}
|
|
continue
|
|
}
|
|
let diff = Math.abs(a - b)
|
|
stat.sumDiff += diff
|
|
if (diff > stat.maxDiff) {
|
|
stat.maxDiff = diff
|
|
}
|
|
if (diff > tolerance) {
|
|
stat.mismatchCount++
|
|
if (examples.length < MAX_EXAMPLES) {
|
|
examples.push({ ts, field, a, b, diff })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { onlyA, onlyB, commonCount, fieldStats, examples }
|
|
}
|
|
|
|
function fmtTs(ms) {
|
|
return DateTime.fromMillis(ms, { zone: 'utc' }).toISO()
|
|
}
|
|
|
|
function printSummary(opts, sideA, sideB, diff) {
|
|
console.log('=== Vergleich Laermwerte ===')
|
|
console.log(`Sensor: ${opts.sensorid} data: ${opts.data} span: ${opts.span}${opts.datetime ? ` datetime: ${opts.datetime}` : ''}`)
|
|
console.log(`A: ${sideA.count} Werte`)
|
|
console.log(`B: ${sideB.count} Werte`)
|
|
console.log(`Gemeinsame Zeitstempel: ${diff.commonCount}`)
|
|
console.log(`Nur in A: ${diff.onlyA.length}${diff.onlyA.length ? ` (z.B. ${diff.onlyA.slice(0, 3).map(fmtTs).join(', ')})` : ''}`)
|
|
console.log(`Nur in B: ${diff.onlyB.length}${diff.onlyB.length ? ` (z.B. ${diff.onlyB.slice(0, 3).map(fmtTs).join(', ')})` : ''}`)
|
|
console.log('')
|
|
console.log('Felder (bei gemeinsamen Zeitstempeln):')
|
|
let totalMismatches = 0
|
|
for (let [field, stat] of diff.fieldStats) {
|
|
let avg = stat.count ? stat.sumDiff / stat.count : 0
|
|
totalMismatches += stat.mismatchCount
|
|
console.log(` ${field.padEnd(14)} verglichen: ${String(stat.count).padStart(5)} max diff: ${stat.maxDiff.toFixed(4).padStart(10)} avg diff: ${avg.toFixed(4).padStart(10)} mismatches (>${opts.tolerance}): ${stat.mismatchCount}`)
|
|
}
|
|
if (diff.examples.length > 0) {
|
|
console.log('')
|
|
console.log(`Beispiel-Mismatches (max ${MAX_EXAMPLES}):`)
|
|
for (let ex of diff.examples) {
|
|
if (ex.reason === 'null-mismatch') {
|
|
console.log(` ${fmtTs(ex.ts)} ${ex.field}: A=${ex.a} B=${ex.b}`)
|
|
} else {
|
|
console.log(` ${fmtTs(ex.ts)} ${ex.field}: A=${ex.a} B=${ex.b} diff=${ex.diff.toFixed(4)}`)
|
|
}
|
|
}
|
|
}
|
|
console.log('')
|
|
if (totalMismatches > 0) {
|
|
console.log(`FEHLGESCHLAGEN: ${totalMismatches} Feld-Mismatches ausserhalb der Toleranz gefunden.`)
|
|
} else {
|
|
console.log('OK: Keine Mismatches ausserhalb der Toleranz bei gemeinsamen Zeitstempeln.')
|
|
}
|
|
return totalMismatches
|
|
}
|
|
|
|
async function main() {
|
|
let opts = parseArgs(process.argv)
|
|
let urlA = buildURL(opts.urlA, opts)
|
|
let urlB = buildURL(opts.urlB, opts)
|
|
console.log(`A: ${urlA}`)
|
|
console.log(`B: ${urlB}`)
|
|
console.log('')
|
|
|
|
let [sideA, sideB] = await Promise.all([
|
|
fetchSide('A', urlA),
|
|
fetchSide('B', urlB),
|
|
])
|
|
|
|
if (!sideA.ok || !sideB.ok) {
|
|
if (!sideA.ok) console.error(sideA.err)
|
|
if (!sideB.ok) console.error(sideB.err)
|
|
process.exitCode = 2
|
|
return
|
|
}
|
|
|
|
let mapA = toMap(sideA.values)
|
|
let mapB = toMap(sideB.values)
|
|
let diff = diffRows(mapA, mapB, opts.tolerance)
|
|
let mismatches = printSummary(
|
|
opts,
|
|
{ count: sideA.values.length },
|
|
{ count: sideB.values.length },
|
|
diff
|
|
)
|
|
process.exitCode = mismatches > 0 ? 1 : 0
|
|
}
|
|
|
|
main().catch((e) => {
|
|
console.error(e)
|
|
process.exitCode = 2
|
|
})
|