Switched from influx to victoria metrics (more light weight for pi)
This commit is contained in:
62
server/package-lock.json
generated
62
server/package-lock.json
generated
@@ -8,7 +8,6 @@
|
||||
"name": "weather-station",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@influxdata/influxdb-client": "^1.33.2",
|
||||
"@scalar/express-api-reference": "^0.10.4",
|
||||
"@ztimson/utils": "^0.29.5",
|
||||
"adm-zip": "^0.5.17",
|
||||
@@ -18,20 +17,14 @@
|
||||
"yaml": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@influxdata/influxdb-client": {
|
||||
"version": "1.35.0",
|
||||
"resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.35.0.tgz",
|
||||
"integrity": "sha512-woWMi8PDpPQpvTsRaUw4Ig+nOGS/CWwAwS66Fa1Vr/EkW+NEwxI8YfPBsdBMn33jK2Y86/qMiiuX/ROHIkJLTw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scalar/client-side-rendering": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.2.4.tgz",
|
||||
"integrity": "sha512-IQC39aQQXKsZXOqQw0LNiZ/gQ1WxHpF+WLTo/hurn0S/FmNHlBGUAhFyt1BeDSUYxics7ixgzQatKJDggafBTw==",
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/client-side-rendering/-/client-side-rendering-0.2.6.tgz",
|
||||
"integrity": "sha512-wvnyBtNdYob6CdmpFqLSQoGM5q5Ra8bXyq713T/u8Nq1NXbULk1znz/LyR78u8g++0G8bkxnVzhvOf7s9FwHFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scalar/schemas": "0.5.0",
|
||||
"@scalar/types": "0.14.0",
|
||||
"@scalar/schemas": "0.7.0",
|
||||
"@scalar/types": "0.16.0",
|
||||
"@scalar/validation": "0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -39,33 +32,33 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scalar/express-api-reference": {
|
||||
"version": "0.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/express-api-reference/-/express-api-reference-0.10.4.tgz",
|
||||
"integrity": "sha512-/FkuP+B9ppWiQbf0Bhygx8CwoLXiEGFgS7OK14FM/JH0SeAaGfQiacUgPEpxJYI2qnqFHkrys8YH2jl400wDfg==",
|
||||
"version": "0.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/express-api-reference/-/express-api-reference-0.10.6.tgz",
|
||||
"integrity": "sha512-5gXV4Cv6EA8/hUo1e3ZMHKo5w5D45QTCzsA2Qvrf4OEe141iKdtSErYYxaZ/c2RIVidRUohDr3WC2HN+HRDosw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scalar/client-side-rendering": "0.2.4"
|
||||
"@scalar/client-side-rendering": "0.2.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@scalar/helpers": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.8.2.tgz",
|
||||
"integrity": "sha512-qNbqUjSB3S4Gr4A0oANcm5G1Ip+EqBxICYKhe9YzmnaBpbmW6shxqpiivApTvvuDf+uIhR3uMwWyVQbYcGLsxA==",
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/helpers/-/helpers-0.9.0.tgz",
|
||||
"integrity": "sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@scalar/schemas": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/schemas/-/schemas-0.5.0.tgz",
|
||||
"integrity": "sha512-aV0PWqFpQft9a6d4Z6HOTHKDaA+K4AquxSPfBDI1c5f6lMe5l3zViww/iBQA/Yw+V1+Y2iwSVzhuxaktC1UiMg==",
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/schemas/-/schemas-0.7.0.tgz",
|
||||
"integrity": "sha512-Roj0e7S29yTbGojwdx/b8C1H5arVN4h/VSPrtQ5vb9NT2ZAbrfPQVmpQYrF845rKOWX7kSzIvyBwrKNEMIAkxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scalar/helpers": "0.8.2",
|
||||
"@scalar/helpers": "0.9.0",
|
||||
"@scalar/validation": "0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -73,12 +66,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@scalar/types": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.14.0.tgz",
|
||||
"integrity": "sha512-aS1FvHbKhBC51mWqXmhkPe6lmhCd9+u//DQ1YTiCWOz4/fXAdWfwHvW9UZ7AzzcGwGiCo9diE81xV+UvSI1qIg==",
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@scalar/types/-/types-0.16.0.tgz",
|
||||
"integrity": "sha512-26OSrvZjKWZ7F236wWmJajBGDVFJuvXFJqKPFqbt/PxlgZKXQXfJsZorASRQmdNogT576nxYalQ7oaYWEnQwfw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@scalar/helpers": "0.8.2",
|
||||
"@scalar/helpers": "0.9.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"type-fest": "^5.3.1",
|
||||
"zod": "^4.3.5"
|
||||
@@ -838,9 +831,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz",
|
||||
"integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
|
||||
"version": "5.1.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz",
|
||||
"integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -981,12 +974,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
"start": "node src/server.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@influxdata/influxdb-client": "^1.33.2",
|
||||
"@scalar/express-api-reference": "^0.10.4",
|
||||
"@ztimson/utils": "^0.29.5",
|
||||
"adm-zip": "^0.5.17",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB |
@@ -1,152 +1,107 @@
|
||||
import {InfluxDB} from '@influxdata/influxdb-client';
|
||||
import {cfg} from './config.mjs';
|
||||
|
||||
const MEASUREMENTS = ['accumulation', 'environment', 'light', 'lightning', 'rain', 'seismic', 'wind'];
|
||||
|
||||
let client, currentUrl, currentToken;
|
||||
|
||||
function getClient() {
|
||||
const c = cfg();
|
||||
if(c.INFLUX_URL !== currentUrl || c.INFLUX_TOKEN !== currentToken) {
|
||||
client = new InfluxDB({url: c.INFLUX_URL, token: c.INFLUX_TOKEN});
|
||||
currentUrl = c.INFLUX_URL;
|
||||
currentToken = c.INFLUX_TOKEN;
|
||||
}
|
||||
return {client, c};
|
||||
async function query(c, q) {
|
||||
const url = new URL('/api/v1/query_range', c.INFLUX_URL);
|
||||
url.searchParams.set('query', q);
|
||||
url.searchParams.set('step', '60s');
|
||||
const res = await fetch(url);
|
||||
const json = await res.json();
|
||||
return json.data?.result || [];
|
||||
}
|
||||
|
||||
async function query(flux) {
|
||||
const {client, c} = getClient();
|
||||
const api = client.getQueryApi(c.INFLUX_ORG);
|
||||
const rows = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
api.queryRows(flux, {
|
||||
next(row, meta) { rows.push(meta.toObject(row)); },
|
||||
error(err) { reject(err); },
|
||||
complete() { resolve(rows); },
|
||||
});
|
||||
});
|
||||
async function queryInstant(c, q) {
|
||||
const url = new URL('/api/v1/query', c.INFLUX_URL);
|
||||
url.searchParams.set('query', q);
|
||||
const res = await fetch(url);
|
||||
const json = await res.json();
|
||||
return json.data?.result || [];
|
||||
}
|
||||
|
||||
function truncateToHour(iso) {
|
||||
const d = new Date(iso);
|
||||
d.setMinutes(0, 0, 0);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:00`;
|
||||
}
|
||||
|
||||
function truncateToDay(iso) {
|
||||
return new Date(iso).toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function extractRow(row) {
|
||||
function metricToFields(results) {
|
||||
const out = {};
|
||||
for(const [k, v] of Object.entries(row)) {
|
||||
if(!k.startsWith('_') && k !== 'result' && k !== 'table') out[k] = v;
|
||||
for (const r of results) {
|
||||
const field = r.metric.__name__ || r.metric.field;
|
||||
const val = r.value?.[1] ?? r.values?.at(-1)?.[1];
|
||||
if (val !== undefined) out[field] = parseFloat(val);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function queryCurrent() {
|
||||
const {c} = getClient();
|
||||
const flux = `
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: -1h)
|
||||
|> filter(fn: (r) => ${MEASUREMENTS.map(m => `r._measurement == "${m}"`).join(' or ')})
|
||||
|> last()
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`;
|
||||
const rows = await query(flux);
|
||||
const result = {time: new Date()};
|
||||
for(const row of rows) {
|
||||
Object.assign(result, extractRow(row));
|
||||
const c = cfg();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const out = {time: new Date()};
|
||||
|
||||
for (const m of MEASUREMENTS) {
|
||||
const results = await queryInstant(c, `{measurement="${m}"}`);
|
||||
Object.assign(out, metricToFields(results));
|
||||
}
|
||||
return result;
|
||||
return out;
|
||||
}
|
||||
|
||||
export async function queryHourly(start, end) {
|
||||
const {c} = getClient();
|
||||
const flux = `
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => ${MEASUREMENTS.map(m => `r._measurement == "${m}"`).join(' or ')})
|
||||
|> group(columns: ["_field"])
|
||||
|> aggregateWindow(every: 1h, fn: mean, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`;
|
||||
const rows = await query(flux);
|
||||
return rows.map(row => ({
|
||||
time: truncateToHour(row._time),
|
||||
...extractRow(row),
|
||||
}));
|
||||
const c = cfg();
|
||||
const s = Math.floor(start.getTime() / 1000);
|
||||
const e = Math.floor(end.getTime() / 1000);
|
||||
const byTime = {};
|
||||
|
||||
for (const m of MEASUREMENTS) {
|
||||
const results = await query(c, `avg_over_time({measurement="${m}"}[1h])`);
|
||||
results.forEach(r => {
|
||||
const field = r.metric.__name__;
|
||||
r.values.forEach(([ts, val]) => {
|
||||
const time = new Date(ts * 1000).toISOString().slice(0, 13) + ':00';
|
||||
if (!byTime[time]) byTime[time] = {time};
|
||||
byTime[time][field] = parseFloat(val);
|
||||
});
|
||||
});
|
||||
}
|
||||
return Object.values(byTime).sort((a, b) => a.time.localeCompare(b.time));
|
||||
}
|
||||
|
||||
export async function queryDaily(start, end) {
|
||||
const {c} = getClient();
|
||||
const measurements = ['accumulation', 'environment', 'light', 'lightning', 'seismic'];
|
||||
const results = {};
|
||||
const c = cfg();
|
||||
const byTime = {};
|
||||
|
||||
for(const m of measurements) {
|
||||
for (const m of ['accumulation', 'environment', 'light', 'lightning', 'seismic']) {
|
||||
const [means, mins, maxs] = await Promise.all([
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> group(columns: ["_field"])
|
||||
|> aggregateWindow(every: 1d, fn: mean, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> group(columns: ["_field"])
|
||||
|> aggregateWindow(every: 1d, fn: min, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(`
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: ${start.toISOString()}, stop: ${end.toISOString()})
|
||||
|> filter(fn: (r) => r._measurement == "${m}")
|
||||
|> group(columns: ["_field"])
|
||||
|> aggregateWindow(every: 1d, fn: max, createEmpty: false)
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`),
|
||||
query(c, `avg_over_time({measurement="${m}"}[1d])`),
|
||||
query(c, `min_over_time({measurement="${m}"}[1d])`),
|
||||
query(c, `max_over_time({measurement="${m}"}[1d])`),
|
||||
]);
|
||||
|
||||
for(const row of means) {
|
||||
const time = truncateToDay(row._time);
|
||||
if(!results[time]) results[time] = {time};
|
||||
Object.assign(results[time], extractRow(row));
|
||||
}
|
||||
means.forEach(r => {
|
||||
r.values.forEach(([ts, val]) => {
|
||||
const time = new Date(ts * 1000).toISOString().slice(0, 10);
|
||||
if (!byTime[time]) byTime[time] = {time};
|
||||
byTime[time][r.metric.__name__] = parseFloat(val);
|
||||
});
|
||||
});
|
||||
|
||||
for(const row of mins) {
|
||||
const time = truncateToDay(row._time);
|
||||
if(!results[time]) results[time] = {time};
|
||||
if(row.temperature != null) results[time].env_temp_min_c = row.temperature;
|
||||
}
|
||||
for(const row of maxs) {
|
||||
const time = truncateToDay(row._time);
|
||||
if(!results[time]) results[time] = {time};
|
||||
if(row.temperature != null) results[time].env_temp_max_c = row.temperature;
|
||||
}
|
||||
const applyMinMax = (results, suffix) => results.forEach(r => {
|
||||
if (r.metric.__name__ !== 'temperature') return;
|
||||
r.values.forEach(([ts, val]) => {
|
||||
const time = new Date(ts * 1000).toISOString().slice(0, 10);
|
||||
if (!byTime[time]) byTime[time] = {time};
|
||||
byTime[time][suffix] = parseFloat(val);
|
||||
});
|
||||
});
|
||||
|
||||
applyMinMax(mins, 'env_temp_min_c');
|
||||
applyMinMax(maxs, 'env_temp_max_c');
|
||||
}
|
||||
|
||||
return Object.values(results).sort((a, b) => a.time.localeCompare(b.time));
|
||||
return Object.values(byTime).sort((a, b) => a.time.localeCompare(b.time));
|
||||
}
|
||||
|
||||
export async function getCoords() {
|
||||
const {c} = getClient();
|
||||
const flux = `
|
||||
from(bucket: "${c.INFLUX_BUCKET}")
|
||||
|> range(start: -24h)
|
||||
|> filter(fn: (r) => r._measurement == "gps")
|
||||
|> last()
|
||||
|> pivot(rowKey: ["_time"], columnKey: ["_field"], valueColumn: "_value")
|
||||
`;
|
||||
const rows = await query(flux);
|
||||
if(rows.length && rows[0].latitude && rows[0].longitude) {
|
||||
return {latitude: rows[0].latitude, longitude: rows[0].longitude, altitude: rows[0].altitude || c.ALTITUDE};
|
||||
const c = cfg();
|
||||
const results = await queryInstant(c, `{measurement="gps"}`);
|
||||
const fields = metricToFields(results);
|
||||
|
||||
if (fields.latitude && fields.longitude) {
|
||||
return {latitude: fields.latitude, longitude: fields.longitude, altitude: fields.altitude || c.ALTITUDE};
|
||||
}
|
||||
return {latitude: c.LATITUDE, longitude: c.LONGITUDE, altitude: c.ALTITUDE};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user