import type { QueryApi } from "@influxdata/influxdb-client"; import type { Env } from "../env.js"; import type { PowerSample } from "../types.js"; interface SeriesConfig { bucket: string; measurement: string; field: string; fieldUnit: "W" | "kW"; domain?: string; entityId?: string; } /** * Fetches raw samples for one series between `sinceMs` and now, converting * the configured field to watts based on `fieldUnit`. */ async function fetchSeriesSince( queryApi: QueryApi, config: SeriesConfig, sinceMs: number, ): Promise { const sinceIso = new Date(sinceMs).toISOString(); const domainFilter = config.domain ? `|> filter(fn: (r) => r["domain"] == "${config.domain}")\n ` : ""; const entityFilter = config.entityId ? `|> filter(fn: (r) => r["entity_id"] == "${config.entityId}")\n ` : ""; const flux = ` from(bucket: "${config.bucket}") |> range(start: ${sinceIso}) |> filter(fn: (r) => r["_measurement"] == "${config.fieldUnit}") |> filter(fn: (r) => r["_field"] == "${config.field}") ${domainFilter}${entityFilter}|> sort(columns: ["_time"]) `; const unitMultiplier = config.fieldUnit === "kW" ? 1000 : 1; const samples: PowerSample[] = []; for await (const { values, tableMeta } of queryApi.iterateRows(flux)) { const row = tableMeta.toObject(values) as { _time: string; _value: number }; samples.push({ timestampMs: new Date(row._time).getTime(), watts: row._value * unitMultiplier, }); } return samples; } function housePowerConfig(env: Env): SeriesConfig { return { bucket: env.INFLUX_BUCKET, measurement: env.INFLUX_MEASUREMENT, field: env.INFLUX_FIELD, fieldUnit: env.INFLUX_FIELD_UNIT, domain: env.INFLUX_DOMAIN, entityId: env.INFLUX_ENTITY_ID, }; } /** * Fetches raw power samples between `sinceMs` and now. * Converts the configured field to watts based on INFLUX_FIELD_UNIT. */ export async function fetchPowerSamplesSince( queryApi: QueryApi, env: Env, sinceMs: number, ): Promise { return fetchSeriesSince(queryApi, housePowerConfig(env), sinceMs); } /** Fetches the single most recent power sample, if any exists within the lookback window. */ export async function fetchLatestPowerSample( queryApi: QueryApi, env: Env, lookbackMs = 60_000, ): Promise { const samples = await fetchPowerSamplesSince( queryApi, env, Date.now() - lookbackMs, ); return samples.length > 0 ? samples[samples.length - 1] : null; } /** * Fetches the most recent solar production sample from InfluxDB, if any * exists within the lookback window. Returns null when no solar production * series is configured (SOLAR_INFLUX_ENTITY_ID unset). */ export async function fetchLatestSolarProductionSample( queryApi: QueryApi, env: Env, lookbackMs = 60_000, ): Promise { if (!env.SOLAR_INFLUX_ENTITY_ID) return null; const config: SeriesConfig = { bucket: env.INFLUX_BUCKET, measurement: env.SOLAR_INFLUX_MEASUREMENT, field: env.SOLAR_INFLUX_FIELD, fieldUnit: env.SOLAR_INFLUX_FIELD_UNIT, domain: env.SOLAR_INFLUX_DOMAIN, entityId: env.SOLAR_INFLUX_ENTITY_ID, }; const samples = await fetchSeriesSince(queryApi, config, Date.now() - lookbackMs); return samples.length > 0 ? samples[samples.length - 1] : null; }