Implement telemetry enhancements: add reading of Deye registers 145 and 179 in telemetry collector to derive is_export_limited and pv_derating_flags. Update fn_telemetry_inverter_sample to store these flags, and adjust related documentation and API endpoints accordingly.
This commit is contained in:
@@ -98,7 +98,7 @@ Projekt je **SQL-first**: doménová logika, agregace, joiny mezi tabulkami a st
|
||||
|
||||
13. **Endpoint `GET …/forecast/pv`** vrací `DISTINCT ON (interval_start, pv_array_id)` seřazené podle nejnovějšího `forecast_pv_run.created_at`, aby UI nemělo duplikáty slotů; plná historie běhů zůstává v tabulkách.
|
||||
|
||||
13a. **PV delta kalibrace:** `GET …/forecast/pv-delta-profile` vrací JSON z `fn_pv_forecast_delta_profile`; `GET …/configuration` obsahuje `pv_forecast_calibration` z `ems.site_pv_forecast_calibration`; `PATCH …/configuration/pv-forecast-calibration` mění cutoff / policy / přepsání parametrů delty. Telemetrie `telemetry_inverter.is_export_limited` / `pv_derating_flags` (V058) řídí vyloučení slotu z učení v `fn_fill_forecast_accuracy` (`telemetry_derating`).
|
||||
13a. **PV delta kalibrace:** `GET …/forecast/pv-delta-profile` vrací JSON z `fn_pv_forecast_delta_profile`; `GET …/configuration` obsahuje `pv_forecast_calibration` z `ems.site_pv_forecast_calibration`; `PATCH …/configuration/pv-forecast-calibration` mění cutoff / policy / přepsání parametrů delty. Telemetrie `telemetry_inverter.is_export_limited` / `pv_derating_flags` (V058) řídí vyloučení slotu z učení v `fn_fill_forecast_accuracy` (`telemetry_derating`); `telemetry_collector` je plní čtením Deye reg **145** a **179** při poll střídače.
|
||||
|
||||
14. **Příchod a odjezd EV** detekuje `telemetry_collector` z telemetrie nabíječky: přechod `available` → `preparing` / `charging` (resp. jakýkoli stav ≠ `available`) znamená příjezd; přechod na `available` uzavře `ev_session`. Tabulka `ev_arrival_stats` se při příjezdu doplňuje přes `fn_update_ev_arrival_stats` a **nemá se mazat** (dlouhodobá historická statistika).
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ DEYE_REG_GRID_EXPORT_TOTAL_LO = 524
|
||||
DEYE_REG_GRID_EXPORT_TOTAL_HI = 525
|
||||
DEYE_REG_PV1_POWER = 672
|
||||
DEYE_REG_PV2_POWER = 673
|
||||
# Solar sell (0 = přebytek řiditelné FVE nesmí do sítě) a GEN/MI cut-off (bits0–1 == 2 → cut-off ON); viz modbus-registers.md
|
||||
DEYE_REG_SOLAR_SELL = 145
|
||||
DEYE_REG_CONTROL_BOARD_SPECIAL1 = 179
|
||||
|
||||
|
||||
def aggregate_pv_production_w(pv1_w: int, pv2_w: int, gen_port_w: int) -> int:
|
||||
@@ -38,6 +41,18 @@ def aggregate_pv_production_w(pv1_w: int, pv2_w: int, gen_port_w: int) -> int:
|
||||
return max(0, int(pv1_w)) + max(0, int(pv2_w)) + max(0, int(gen_port_w))
|
||||
|
||||
|
||||
def _export_limit_flags_from_deye_regs(reg145: int | None, reg179: int | None) -> tuple[bool | None, int | None]:
|
||||
"""Odvoď is_export_limited / pv_derating_flags z přečtených holding registrů (NULL = neznámé)."""
|
||||
if reg145 is None and reg179 is None:
|
||||
return None, None
|
||||
flags = 0
|
||||
if reg145 is not None and int(reg145) == 0:
|
||||
flags |= 1
|
||||
if reg179 is not None and (int(reg179) & 3) == 2:
|
||||
flags |= 2
|
||||
return (flags != 0), flags
|
||||
|
||||
|
||||
async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None:
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
@@ -70,14 +85,17 @@ async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None:
|
||||
grid_energy_regs = await mb.read_holding_registers(
|
||||
DEYE_REG_GRID_IMPORT_TOTAL_LO, 4
|
||||
)
|
||||
reg145 = await mb.read_register(DEYE_REG_SOLAR_SELL)
|
||||
reg179 = await mb.read_register(DEYE_REG_CONTROL_BOARD_SPECIAL1)
|
||||
pv_power_w = aggregate_pv_production_w(pv1_power, pv2_power, gen_port_power)
|
||||
grid_import_total_wh = (grid_energy_regs[1] << 16 | grid_energy_regs[0]) * 100
|
||||
grid_export_total_wh = (grid_energy_regs[3] << 16 | grid_energy_regs[2]) * 100
|
||||
is_export_limited, pv_derating_flags = _export_limit_flags_from_deye_regs(reg145, reg179)
|
||||
|
||||
logger.debug("inverter:%s Deye run_state raw=%s", code, run_state)
|
||||
|
||||
await db.execute(
|
||||
"select ems.fn_telemetry_inverter_sample($1::int, $2::int, $3::timestamptz, $4::int, $5::int, $6::int, $7::int, $8::float8, $9::int, $10::int, $11::int, $12::int, $13::int, $14::bigint, $15::bigint, $16::int)",
|
||||
"select ems.fn_telemetry_inverter_sample($1::int, $2::int, $3::timestamptz, $4::int, $5::int, $6::int, $7::int, $8::float8, $9::int, $10::int, $11::int, $12::int, $13::int, $14::bigint, $15::bigint, $16::int, $17::boolean, $18::int)",
|
||||
site_id,
|
||||
inv_id,
|
||||
measured_at,
|
||||
@@ -94,6 +112,8 @@ async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None:
|
||||
grid_import_total_wh,
|
||||
grid_export_total_wh,
|
||||
run_state,
|
||||
is_export_limited,
|
||||
pv_derating_flags,
|
||||
)
|
||||
inv_temp: float | None = None
|
||||
await manager.broadcast_telemetry(
|
||||
@@ -108,6 +128,8 @@ async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None:
|
||||
"load_power_w": load_power,
|
||||
"gen_port_power_w": gen_port_power,
|
||||
"inverter_temp_c": inv_temp,
|
||||
"is_export_limited": is_export_limited,
|
||||
"pv_derating_flags": pv_derating_flags,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
|
||||
28
backend/tests/test_telemetry_export_limit_flags.py
Normal file
28
backend/tests/test_telemetry_export_limit_flags.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Logika is_export_limited / pv_derating_flags z Deye reg 145 a 179."""
|
||||
|
||||
from services.telemetry_collector import _export_limit_flags_from_deye_regs
|
||||
|
||||
|
||||
def test_both_none_unknown() -> None:
|
||||
lim, flags = _export_limit_flags_from_deye_regs(None, None)
|
||||
assert lim is None and flags is None
|
||||
|
||||
|
||||
def test_solar_sell_disabled() -> None:
|
||||
lim, flags = _export_limit_flags_from_deye_regs(0, None)
|
||||
assert lim is True and flags == 1
|
||||
|
||||
|
||||
def test_solar_sell_enabled_only() -> None:
|
||||
lim, flags = _export_limit_flags_from_deye_regs(1, None)
|
||||
assert lim is False and flags == 0
|
||||
|
||||
|
||||
def test_gen_mi_cutoff_bits() -> None:
|
||||
lim, flags = _export_limit_flags_from_deye_regs(None, 2)
|
||||
assert lim is True and flags == 2
|
||||
|
||||
|
||||
def test_combined_flags() -> None:
|
||||
lim, flags = _export_limit_flags_from_deye_regs(0, 2)
|
||||
assert lim is True and flags == 3
|
||||
@@ -1,4 +1,6 @@
|
||||
create or replace function ems.fn_telemetry_inverter_sample(
|
||||
DROP FUNCTION IF EXISTS ems.fn_telemetry_inverter_sample;
|
||||
|
||||
CREATE OR REPLACE FUNCTION ems.fn_telemetry_inverter_sample(
|
||||
p_site_id int,
|
||||
p_inverter_id int,
|
||||
p_measured_at timestamptz,
|
||||
@@ -14,12 +16,14 @@ create or replace function ems.fn_telemetry_inverter_sample(
|
||||
p_load_power_w int,
|
||||
p_grid_import_total_wh bigint,
|
||||
p_grid_export_total_wh bigint,
|
||||
p_run_state int
|
||||
p_run_state int,
|
||||
p_is_export_limited boolean DEFAULT NULL,
|
||||
p_pv_derating_flags int DEFAULT NULL
|
||||
)
|
||||
returns void
|
||||
language sql
|
||||
as $fn$
|
||||
insert into ems.telemetry_inverter (
|
||||
RETURNS void
|
||||
LANGUAGE sql
|
||||
AS $fn$
|
||||
INSERT INTO ems.telemetry_inverter (
|
||||
site_id,
|
||||
inverter_id,
|
||||
measured_at,
|
||||
@@ -35,9 +39,11 @@ as $fn$
|
||||
load_power_w,
|
||||
grid_import_total_wh,
|
||||
grid_export_total_wh,
|
||||
run_state
|
||||
run_state,
|
||||
is_export_limited,
|
||||
pv_derating_flags
|
||||
)
|
||||
values (
|
||||
VALUES (
|
||||
p_site_id,
|
||||
p_inverter_id,
|
||||
p_measured_at,
|
||||
@@ -53,10 +59,12 @@ as $fn$
|
||||
p_load_power_w,
|
||||
p_grid_import_total_wh,
|
||||
p_grid_export_total_wh,
|
||||
p_run_state
|
||||
p_run_state,
|
||||
p_is_export_limited,
|
||||
p_pv_derating_flags
|
||||
)
|
||||
on conflict (inverter_id, measured_at) do nothing;
|
||||
ON CONFLICT (inverter_id, measured_at) DO NOTHING;
|
||||
$fn$;
|
||||
|
||||
comment on function ems.fn_telemetry_inverter_sample is
|
||||
'Insert jednoho vzorku telemetrie střídače (telemetry_collector).';
|
||||
COMMENT ON FUNCTION ems.fn_telemetry_inverter_sample IS
|
||||
'Insert jednoho vzorku telemetrie střídače (telemetry_collector). Volitelně is_export_limited / pv_derating_flags (Deye reg 145/179) pro vyloučení slotů z učení PV delty.';
|
||||
|
||||
@@ -100,6 +100,7 @@ def calculate_pv_power(
|
||||
- Endpoint `GET /api/v1/sites/{site_id}/forecast/pv?date=YYYY-MM-DD` vrací vždy poslední `ok` run per `(interval_start, pv_array_id)` (`DISTINCT ON`), takže UI nevidí duplikáty z historických běhů.
|
||||
- **Kalibrace delty:** `GET /api/v1/sites/{site_id}/forecast/pv-delta-profile?from=…&to=…` vrací JSON z `ems.fn_pv_forecast_delta_profile` (`deltas`, `deltas_by_array`, `delta_learn_min_ts` z `ems.site_pv_forecast_calibration`). Volitelné query parametry: `half_life_days`, `threshold_w`, `top_n_days`, `non_top_day_factor`, `day_weight_gamma` (NULL u numerických přepsání = hodnota z kalibrační tabulky / default funkce).
|
||||
- **Úprava kalibrace z API:** `PATCH /api/v1/sites/{site_id}/configuration/pv-forecast-calibration` s JSON tělem (částečný update); odpověď je aktuální řádek kalibrace. Souhrn konfigurace v `GET …/configuration` obsahuje klíč `pv_forecast_calibration`.
|
||||
- **Telemetrie pro učení delty:** `telemetry_collector` při Modbus poll přidá čtení reg. **145** a **179**; `fn_telemetry_inverter_sample` ukládá `is_export_limited` / `pv_derating_flags` (bity 1 = solar sell off, 2 = GEN/MI cut-off aktivní dle masky `(reg179 & 3) == 2`). `fn_fill_forecast_accuracy` sloty s těmito signály označí `telemetry_derating`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ Shrnutí otevřených bodů z `docs/06-open-questions.md`, checklistů v modulec
|
||||
|
||||
| Popis | Kde | Kdo |
|
||||
|-------|-----|-----|
|
||||
| Sloupce `telemetry_inverter.is_export_limited` / `pv_derating_flags` jsou v DB (V058) a `fn_fill_forecast_accuracy` je respektuje; **doplnit zápis z collectoru** (režim / reg 145/179). Volitelné rozšíření API o korigovaný výkon per `pv_array_id` v grafu. | `telemetry_collector`, `db/routines/R__022_fn_fill_forecast_accuracy.sql` | programátor |
|
||||
| **Telemetry collector** čte Deye reg **145** (solar sell) a **179** (bits0–1 MI cut-off) do `is_export_limited` / `pv_derating_flags`. Volitelné rozšíření API o korigovaný výkon per `pv_array_id` v grafu. | `backend/services/telemetry_collector.py` | programátor |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user