fix telemtrie na dahsbaordu (15min misto 1h)
This commit is contained in:
@@ -129,7 +129,7 @@ Multi-site Energy Management System: optimalizuje FVE, baterii a flexibilní zá
|
|||||||
| `modbus_command` | Journal Modbus zápisů (pending → written → verified / mismatch / failed); retry a vazba na `planning_run`; u Deye exportu `deye_physical_mode` (PASSIVE/SELL/CHARGE). |
|
| `modbus_command` | Journal Modbus zápisů (pending → written → verified / mismatch / failed); retry a vazba na `planning_run`; u Deye exportu `deye_physical_mode` (PASSIVE/SELL/CHARGE). |
|
||||||
| `cutoff_switch_log` | Log přepnutí cut-off přepínačů (mikroinvertory); edge trigger, důvod a cena. |
|
| `cutoff_switch_log` | Log přepnutí cut-off přepínačů (mikroinvertory); edge trigger, důvod a cena. |
|
||||||
|
|
||||||
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_latest_telemetry`, `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`.
|
**View / funkce (nejsou tabulky):** `vw_site_effective_price`, `vw_latest_telemetry`, `vw_telemetry_hourly_7d`, `vw_telemetry_15m_7d` (15min agregát pro dashboard sloty; repeatable `R__vw_telemetry_15m_7d.sql`), `vw_audit_summary`, `vw_operating_mode`, `vw_forecast_accuracy_by_lead_time`, `vw_forecast_accuracy_daily`; `fn_effective_price`, `fn_green_bonus_revenue`, `fn_cop_estimate`, `fn_fill_audit_interval`, `fn_fill_forecast_accuracy`, `fn_set_mode`, `fn_expire_modes` (vrací řádky přepnutí pro Discord), `fn_restore_previous_mode`, `fn_update_ev_arrival_stats`, `fn_ev_expected_arrival`, `fn_update_baseline_stats`, `fn_get_baseline_forecast`, `fn_update_market_price_stats`, `fn_update_tuv_usage_stats`, `fn_get_predicted_price`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -166,6 +166,7 @@ Specifikace z `docs/02-architecture.md`, modulových docs a komentářů v `plan
|
|||||||
| Bazální spotřeba | `docs/04-modules/consumption.md` |
|
| Bazální spotřeba | `docs/04-modules/consumption.md` |
|
||||||
| TČ, COP, TUV | `docs/04-modules/heat-pump.md`, `db/routines/R__fn_cop_estimate.sql` |
|
| TČ, COP, TUV | `docs/04-modules/heat-pump.md`, `db/routines/R__fn_cop_estimate.sql` |
|
||||||
| Modbus, telemetrie, agregace | `docs/04-modules/telemetry.md` |
|
| Modbus, telemetrie, agregace | `docs/04-modules/telemetry.md` |
|
||||||
|
| Dashboard přehled – 15min grafy slotů, SoC vs. živá telemetrie | `docs/04-modules/telemetry.md` (CA `telemetry_inverter_15m`, view `vw_telemetry_15m_7d`), `frontend/src/hooks/useDashboardData.ts`, `frontend/src/components/charts/SocTuvChart.tsx` |
|
||||||
| Modbus journal, verifikace, Discord | `docs/04-modules/modbus-command-journal.md` |
|
| Modbus journal, verifikace, Discord | `docs/04-modules/modbus-command-journal.md` |
|
||||||
| Deye registry (FC 0x10, 108/109/141/142/178/143) | `docs/04-modules/modbus-registers.md` |
|
| Deye registry (FC 0x10, 108/109/141/142/178/143) | `docs/04-modules/modbus-registers.md` |
|
||||||
| Export setpointů, Loxone HTTP | `docs/04-modules/control.md`, `docs/loxone-integration.md` |
|
| Export setpointů, Loxone HTTP | `docs/04-modules/control.md`, `docs/loxone-integration.md` |
|
||||||
|
|||||||
33
db/migration/V039__telemetry_inverter_15m_aggregate.sql
Normal file
33
db/migration/V039__telemetry_inverter_15m_aggregate.sql
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
-- ============================================================
|
||||||
|
-- 15min continuous aggregate telemetrie střídače (dashboard sloty)
|
||||||
|
-- ============================================================
|
||||||
|
-- Zarovnáno s 15min sloty UI (UTC time_bucket = floorSlotUtcMs v frontendu).
|
||||||
|
-- Hodinový CA telemetry_inverter_hourly zůstává pro dlouhé grafy / legacy.
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS ems.telemetry_inverter_15m
|
||||||
|
WITH (timescaledb.continuous) AS
|
||||||
|
SELECT
|
||||||
|
time_bucket('15 minutes', measured_at) AS slot_start,
|
||||||
|
site_id,
|
||||||
|
AVG(pv_power_w)::INT AS avg_pv_w,
|
||||||
|
AVG(battery_power_w)::INT AS avg_battery_w,
|
||||||
|
AVG(grid_power_w)::INT AS avg_grid_w,
|
||||||
|
AVG(load_power_w)::INT AS avg_load_w,
|
||||||
|
LAST(battery_soc_percent, measured_at) AS last_soc_pct,
|
||||||
|
COUNT(*) AS sample_count
|
||||||
|
FROM ems.telemetry_inverter
|
||||||
|
GROUP BY slot_start, site_id
|
||||||
|
WITH NO DATA;
|
||||||
|
|
||||||
|
-- Refresh: ≥2× time_bucket (15 min) → start_offset > 30 min
|
||||||
|
SELECT add_continuous_aggregate_policy(
|
||||||
|
'ems.telemetry_inverter_15m',
|
||||||
|
start_offset => INTERVAL '45 minutes',
|
||||||
|
end_offset => INTERVAL '1 minute',
|
||||||
|
schedule_interval => INTERVAL '15 minutes'
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON MATERIALIZED VIEW ems.telemetry_inverter_15m IS
|
||||||
|
'Čtvrthodinové agregáty telemetrie střídače. TimescaleDB continuous aggregate.
|
||||||
|
Refresh každých 15 minut. Dashboard přehled (sloty 15 min).
|
||||||
|
View vw_telemetry_15m_7d je v repeatable R__vw_telemetry_15m_7d.sql.';
|
||||||
19
db/views/R__vw_telemetry_15m_7d.sql
Normal file
19
db/views/R__vw_telemetry_15m_7d.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
-- =============================================================
|
||||||
|
-- R__vw_telemetry_15m_7d.sql
|
||||||
|
-- EMS Platform – telemetrie střídače po 15 min (dashboard sloty)
|
||||||
|
-- Repeatable migration – jedna aktuální definice view
|
||||||
|
-- =============================================================
|
||||||
|
-- Zdroj: continuous aggregate ems.telemetry_inverter_15m (V039).
|
||||||
|
-- security_invoker=false: PostgREST ems_anon čte bez GRANT na podkladový CA.
|
||||||
|
|
||||||
|
CREATE OR REPLACE VIEW ems.vw_telemetry_15m_7d
|
||||||
|
WITH (security_invoker = false)
|
||||||
|
AS
|
||||||
|
SELECT *
|
||||||
|
FROM ems.telemetry_inverter_15m
|
||||||
|
WHERE slot_start >= now() - INTERVAL '7 days'
|
||||||
|
ORDER BY slot_start DESC;
|
||||||
|
|
||||||
|
COMMENT ON VIEW ems.vw_telemetry_15m_7d IS
|
||||||
|
'Telemetrie střídače po 15 min za 7 dní (zdroj: telemetry_inverter_15m).
|
||||||
|
security_invoker=false: čtení přes PostgREST role ems_anon bez GRANT na podkladový CA.';
|
||||||
@@ -26,6 +26,7 @@ GRANT SELECT ON ems.vw_audit_weekly TO ems_anon;
|
|||||||
GRANT SELECT ON ems.vw_mode_log_recent TO ems_anon;
|
GRANT SELECT ON ems.vw_mode_log_recent TO ems_anon;
|
||||||
GRANT SELECT ON ems.vw_operating_mode TO ems_anon;
|
GRANT SELECT ON ems.vw_operating_mode TO ems_anon;
|
||||||
GRANT SELECT ON ems.vw_telemetry_hourly_7d TO ems_anon;
|
GRANT SELECT ON ems.vw_telemetry_hourly_7d TO ems_anon;
|
||||||
|
GRANT SELECT ON ems.vw_telemetry_15m_7d TO ems_anon;
|
||||||
GRANT SELECT ON ems.forecast_accuracy TO ems_anon;
|
GRANT SELECT ON ems.forecast_accuracy TO ems_anon;
|
||||||
GRANT SELECT ON ems.vw_forecast_accuracy_by_lead_time TO ems_anon;
|
GRANT SELECT ON ems.vw_forecast_accuracy_by_lead_time TO ems_anon;
|
||||||
GRANT SELECT ON ems.vw_forecast_accuracy_daily TO ems_anon;
|
GRANT SELECT ON ems.vw_forecast_accuracy_daily TO ems_anon;
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ ems-platform/
|
|||||||
views/
|
views/
|
||||||
R__vw_site_effective_price.sql
|
R__vw_site_effective_price.sql
|
||||||
R__vw_latest_telemetry.sql
|
R__vw_latest_telemetry.sql
|
||||||
|
R__vw_telemetry_15m_7d.sql
|
||||||
R__vw_actual_baseline.sql
|
R__vw_actual_baseline.sql
|
||||||
R__vw_audit_summary.sql
|
R__vw_audit_summary.sql
|
||||||
R__vw_heat_pump_cop_history.sql
|
R__vw_heat_pump_cop_history.sql
|
||||||
|
|||||||
@@ -266,6 +266,15 @@ CREATE TABLE telemetry_inverter (
|
|||||||
-- SELECT create_hypertable('telemetry_inverter', 'measured_at');
|
-- SELECT create_hypertable('telemetry_inverter', 'measured_at');
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Continuous aggregates a views (výkon střídače pro UI)
|
||||||
|
|
||||||
|
Z `telemetry_inverter` počítá TimescaleDB materializované agregáty (viz `db/migration/V011__indexes_and_aggregates.sql`, `V039__telemetry_inverter_15m_aggregate.sql`):
|
||||||
|
|
||||||
|
- **`telemetry_inverter_hourly`** – hodinové průměry + `LAST(battery_soc_percent, measured_at)`; čtení přes view **`vw_telemetry_hourly_7d`**.
|
||||||
|
- **`telemetry_inverter_15m`** – čtvrthodinové bucket odpovídající 15min slotům EMS; čtení přes **`vw_telemetry_15m_7d`** (definice v **`db/views/R__vw_telemetry_15m_7d.sql`**, repeatable).
|
||||||
|
|
||||||
|
PostgREST role `ems_anon` má `SELECT` na tyto views (ne na samotné CA); u view nad CA je `security_invoker = false`, stejně jako u `vw_telemetry_hourly_7d` (viz `db/views/R__z_postgrest_ems_anon_grants.sql`).
|
||||||
|
|
||||||
### `telemetry_ev_charger`
|
### `telemetry_ev_charger`
|
||||||
Stav EV nabíječek.
|
Stav EV nabíječek.
|
||||||
|
|
||||||
|
|||||||
@@ -142,6 +142,21 @@ GROUP BY site_id, time_bucket('15 minutes', measured_at);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Timescale continuous aggregates (střídač → dashboard)
|
||||||
|
|
||||||
|
Nad `ems.telemetry_inverter` běží dva **continuous aggregate** (TimescaleDB); oba se periodicky obnovují (řádově každých 15 minut). Definice CA je ve **verzovaných** migracích (`V011`, `V039`); **view** nad CA držíme v **repeatable** souborech (`db/views/R__*.sql`), aby šla měnit jedna aktuální definice bez nové V migrace.
|
||||||
|
|
||||||
|
| Objekt | Bucket | View pro PostgREST / UI | Poznámka |
|
||||||
|
|--------|--------|-------------------------|----------|
|
||||||
|
| `ems.telemetry_inverter_hourly` | 1 hodina | `ems.vw_telemetry_hourly_7d` | CA a view v **V011**; `security_invoker` v **V031**. Hodinové trendy. |
|
||||||
|
| `ems.telemetry_inverter_15m` | 15 minut | `ems.vw_telemetry_15m_7d` | **`db/views/R__vw_telemetry_15m_7d.sql`** – posledních 7 dní, zarovnání s 15min sloty přehledu. |
|
||||||
|
|
||||||
|
**Frontend přehled** (`frontend/src/hooks/useDashboardData.ts`): skutečné výkony a SoC po slotech bere z **`/vw_telemetry_15m_7d`** (klíč slotu = začátek 15min intervalu v UTC, stejně jako `floorSlotUtcMs` v grafu). Horní karty a **aktuální SoC** v grafu jsou dál z **`vw_site_status`** (poslední 1min vzorek) a z WebSocketu `/ws/telemetry`, aby „teď“ odpovídalo boxu i po refreshi agregátu.
|
||||||
|
|
||||||
|
**Plánovač** počáteční SoC nečte z těchto view – bere poslední řádek z `ems.telemetry_inverter` (`planning_engine._load_site_context`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Konfigurace (env proměnné)
|
## Konfigurace (env proměnné)
|
||||||
|
|
||||||
```env
|
```env
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import {
|
|||||||
type Props = {
|
type Props = {
|
||||||
slots: SlotData[]
|
slots: SlotData[]
|
||||||
nowIndex: number
|
nowIndex: number
|
||||||
|
/** Stejný zdroj jako horní karta SOC (živá telemetrie); bod v aktuálním slotu. */
|
||||||
|
liveBatSoc?: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SocTuvChart({ slots, nowIndex }: Props) {
|
export function SocTuvChart({ slots, nowIndex, liveBatSoc = null }: Props) {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
const chartRef = useRef<Chart | null>(null)
|
const chartRef = useRef<Chart | null>(null)
|
||||||
|
|
||||||
@@ -50,12 +52,22 @@ export function SocTuvChart({ slots, nowIndex }: Props) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const series = useMemo(() => {
|
const series = useMemo(() => {
|
||||||
const socReal = slots.map((s, i) => (i <= nowIndex ? s.soc_actual_pct : null))
|
const socReal = slots.map((s, i) => {
|
||||||
|
if (i > nowIndex) return null
|
||||||
|
if (
|
||||||
|
i === nowIndex &&
|
||||||
|
liveBatSoc != null &&
|
||||||
|
Number.isFinite(liveBatSoc)
|
||||||
|
) {
|
||||||
|
return liveBatSoc
|
||||||
|
}
|
||||||
|
return s.soc_actual_pct
|
||||||
|
})
|
||||||
const socPlan = slots.map((s) => s.soc_plan_pct)
|
const socPlan = slots.map((s) => s.soc_plan_pct)
|
||||||
const tuvReal = slots.map((s, i) => (i <= nowIndex ? s.tuv_actual_c : null))
|
const tuvReal = slots.map((s, i) => (i <= nowIndex ? s.tuv_actual_c : null))
|
||||||
const tuvPlan = slots.map((s) => s.tuv_plan_c)
|
const tuvPlan = slots.map((s) => s.tuv_plan_c)
|
||||||
return { socReal, socPlan, tuvReal, tuvPlan }
|
return { socReal, socPlan, tuvReal, tuvPlan }
|
||||||
}, [slots, nowIndex])
|
}, [slots, nowIndex, liveBatSoc])
|
||||||
|
|
||||||
const bgPlugin = useMemo(
|
const bgPlugin = useMemo(
|
||||||
() => createSlotBackgroundPluginRefs(slotsRef, negRangesRef),
|
() => createSlotBackgroundPluginRefs(slotsRef, negRangesRef),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import type {
|
|||||||
HeatPumpLatestRow,
|
HeatPumpLatestRow,
|
||||||
ModeLogRecentRow,
|
ModeLogRecentRow,
|
||||||
SiteStatusRow,
|
SiteStatusRow,
|
||||||
TelemetryHourly7dRow,
|
Telemetry15m7dRow,
|
||||||
} from '../types/ems'
|
} from '../types/ems'
|
||||||
import type { PlanningIntervalDto } from '../types/plan'
|
import type { PlanningIntervalDto } from '../types/plan'
|
||||||
|
|
||||||
@@ -56,13 +56,6 @@ function buildLiveMetrics(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hourFloorUtcMs(ms: number): number {
|
|
||||||
const d = new Date(ms)
|
|
||||||
d.setUTCMinutes(0, 0, 0)
|
|
||||||
d.setUTCSeconds(0, 0)
|
|
||||||
return d.getTime()
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Klíč hodiny v Europe/Prague (pro shodu s vw_audit_today_hourly.hour_local). */
|
/** Klíč hodiny v Europe/Prague (pro shodu s vw_audit_today_hourly.hour_local). */
|
||||||
function pragueHourKey(ms: number): string {
|
function pragueHourKey(ms: number): string {
|
||||||
return new Intl.DateTimeFormat('sv-SE', {
|
return new Intl.DateTimeFormat('sv-SE', {
|
||||||
@@ -198,7 +191,7 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
const [
|
const [
|
||||||
planMaybe,
|
planMaybe,
|
||||||
statusArr,
|
statusArr,
|
||||||
hourly7d,
|
telemetry15m7d,
|
||||||
auditHourly,
|
auditHourly,
|
||||||
modeLog,
|
modeLog,
|
||||||
hpArr,
|
hpArr,
|
||||||
@@ -211,10 +204,10 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
throw e
|
throw e
|
||||||
}),
|
}),
|
||||||
getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
|
getJson<SiteStatusRow[]>('/vw_site_status', { site_id: `eq.${siteId}` }),
|
||||||
getJson<TelemetryHourly7dRow[]>('/vw_telemetry_hourly_7d', {
|
getJson<Telemetry15m7dRow[]>('/vw_telemetry_15m_7d', {
|
||||||
site_id: `eq.${siteId}`,
|
site_id: `eq.${siteId}`,
|
||||||
order: 'hour.asc',
|
order: 'slot_start.asc',
|
||||||
limit: '500',
|
limit: '1000',
|
||||||
}),
|
}),
|
||||||
getJson<AuditTodayHourlyRow[]>('/vw_audit_today_hourly', {
|
getJson<AuditTodayHourlyRow[]>('/vw_audit_today_hourly', {
|
||||||
site_id: `eq.${siteId}`,
|
site_id: `eq.${siteId}`,
|
||||||
@@ -297,10 +290,10 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
}
|
}
|
||||||
setForecastWeek(forecastDays)
|
setForecastWeek(forecastDays)
|
||||||
|
|
||||||
const hourlyMap = new Map<number, TelemetryHourly7dRow>()
|
const telemetryBySlot = new Map<string, Telemetry15m7dRow>()
|
||||||
if (Array.isArray(hourly7d)) {
|
if (Array.isArray(telemetry15m7d)) {
|
||||||
for (const r of hourly7d) {
|
for (const r of telemetry15m7d) {
|
||||||
hourlyMap.set(new Date(r.hour).getTime(), r)
|
telemetryBySlot.set(slotTimeKey(new Date(r.slot_start).getTime()), r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,7 +314,7 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
const base = emptySlot(iso)
|
const base = emptySlot(iso)
|
||||||
const k = slotTimeKey(startMs)
|
const k = slotTimeKey(startMs)
|
||||||
|
|
||||||
const tel = hourlyMap.get(hourFloorUtcMs(startMs))
|
const tel = telemetryBySlot.get(k)
|
||||||
if (tel) {
|
if (tel) {
|
||||||
base.pv_power_w = tel.avg_pv_w ?? base.pv_power_w
|
base.pv_power_w = tel.avg_pv_w ?? base.pv_power_w
|
||||||
base.battery_power_w = tel.avg_battery_w ?? base.battery_power_w
|
base.battery_power_w = tel.avg_battery_w ?? base.battery_power_w
|
||||||
@@ -373,6 +366,12 @@ export function useDashboardData(siteId: number | null) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const liveSoc = parseNum(status?.battery_soc_percent)
|
||||||
|
if (liveSoc != null && nIdx >= 0 && nIdx < built.length) {
|
||||||
|
const cur = built[nIdx]!
|
||||||
|
built[nIdx] = { ...cur, soc_actual_pct: liveSoc }
|
||||||
|
}
|
||||||
|
|
||||||
const neg: NegPriceItem[] = []
|
const neg: NegPriceItem[] = []
|
||||||
const nowMs = Date.now()
|
const nowMs = Date.now()
|
||||||
for (const r of flatPrices) {
|
for (const r of flatPrices) {
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ const MemoRegimeBar = memo(RegimeBar, (prev, next) =>
|
|||||||
)
|
)
|
||||||
|
|
||||||
const MemoSocTuvChart = memo(SocTuvChart, (prev, next) =>
|
const MemoSocTuvChart = memo(SocTuvChart, (prev, next) =>
|
||||||
prev.slots === next.slots && prev.nowIndex === next.nowIndex,
|
prev.slots === next.slots &&
|
||||||
|
prev.nowIndex === next.nowIndex &&
|
||||||
|
prev.liveBatSoc === next.liveBatSoc,
|
||||||
)
|
)
|
||||||
|
|
||||||
function fmtKw2(w: number | null | undefined): string {
|
function fmtKw2(w: number | null | undefined): string {
|
||||||
@@ -324,7 +326,11 @@ export function Dashboard() {
|
|||||||
chartArea={chartArea}
|
chartArea={chartArea}
|
||||||
/>
|
/>
|
||||||
<div className="border-t border-slate-800 px-2 py-2">
|
<div className="border-t border-slate-800 px-2 py-2">
|
||||||
<MemoSocTuvChart slots={data.slots} nowIndex={data.nowIndex} />
|
<MemoSocTuvChart
|
||||||
|
slots={data.slots}
|
||||||
|
nowIndex={data.nowIndex}
|
||||||
|
liveBatSoc={data.liveMetrics?.bat_soc ?? null}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -48,6 +48,18 @@ export type TelemetryHourly7dRow = {
|
|||||||
sample_count: number | null
|
sample_count: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** ems.vw_telemetry_15m_7d (řádky z telemetry_inverter_15m) */
|
||||||
|
export type Telemetry15m7dRow = {
|
||||||
|
slot_start: string
|
||||||
|
site_id: number
|
||||||
|
avg_pv_w: number | null
|
||||||
|
avg_battery_w: number | null
|
||||||
|
avg_grid_w: number | null
|
||||||
|
avg_load_w: number | null
|
||||||
|
last_soc_pct: string | number | null
|
||||||
|
sample_count: number | null
|
||||||
|
}
|
||||||
|
|
||||||
/** ems.vw_latest_heat_pump */
|
/** ems.vw_latest_heat_pump */
|
||||||
export type HeatPumpLatestRow = {
|
export type HeatPumpLatestRow = {
|
||||||
site_id: number
|
site_id: number
|
||||||
|
|||||||
Reference in New Issue
Block a user