fix pv vyroby (unsinged)
All checks were successful
deploy / deploy (push) Successful in 35s
test / smoke-test (push) Successful in 7s

This commit is contained in:
Dusan Vojacek
2026-04-10 20:30:03 +02:00
parent ec55285bdd
commit 920d9ff40c
7 changed files with 75 additions and 17 deletions

View File

@@ -26,6 +26,14 @@ DEYE_REG_PV1_POWER = 672
DEYE_REG_PV2_POWER = 673 DEYE_REG_PV2_POWER = 673
def aggregate_pv_production_w(pv1_w: int, pv2_w: int, gen_port_w: int) -> int:
"""
Okamžitá „výroba FVE“ pro dashboard / audit součtu: Deye registry 672/673/667
jsou int16 W; záporné hodnoty (např. večer při exportu) nejsou DC výroba.
"""
return max(0, int(pv1_w)) + max(0, int(pv2_w)) + max(0, int(gen_port_w))
async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None: async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None:
rows = await db.fetch( rows = await db.fetch(
""" """
@@ -54,13 +62,12 @@ async def poll_inverter(site_id: int, db: asyncpg.Connection) -> None:
battery_power = await mb.read_register_signed(DEYE_REG_BATTERY_POWER_FLOW) battery_power = await mb.read_register_signed(DEYE_REG_BATTERY_POWER_FLOW)
batt_charge_today = await mb.read_register(DEYE_REG_BATT_CHARGE_TODAY) batt_charge_today = await mb.read_register(DEYE_REG_BATT_CHARGE_TODAY)
batt_discharge_today = await mb.read_register(DEYE_REG_BATT_DISCHARGE_TODAY) batt_discharge_today = await mb.read_register(DEYE_REG_BATT_DISCHARGE_TODAY)
gen_port_power = await mb.read_register(DEYE_REG_GEN_PORT_POWER)
grid_power = await mb.read_register_signed(DEYE_REG_GRID_TOTAL_POWER) grid_power = await mb.read_register_signed(DEYE_REG_GRID_TOTAL_POWER)
load_power = await mb.read_register(DEYE_REG_LOAD_TOTAL_POWER) load_power = await mb.read_register(DEYE_REG_LOAD_TOTAL_POWER)
pv1_power = await mb.read_register(DEYE_REG_PV1_POWER) pv1_power = await mb.read_register_signed(DEYE_REG_PV1_POWER)
pv2_power = await mb.read_register(DEYE_REG_PV2_POWER) pv2_power = await mb.read_register_signed(DEYE_REG_PV2_POWER)
# Celková výroba FVE na této instalaci = stringy PV + výkon přes GEN port. gen_port_power = await mb.read_register_signed(DEYE_REG_GEN_PORT_POWER)
pv_power_w = int(pv1_power) + int(pv2_power) + int(gen_port_power) pv_power_w = aggregate_pv_production_w(pv1_power, pv2_power, gen_port_power)
logger.debug("inverter:%s Deye run_state raw=%s", code, run_state) logger.debug("inverter:%s Deye run_state raw=%s", code, run_state)

View File

@@ -0,0 +1,34 @@
"""Signed PV kanály Deye → agregovaná pv_power_w (kladné příspěvky jen)."""
from __future__ import annotations
import unittest
from services.telemetry_collector import aggregate_pv_production_w
class TestAggregatePvProductionW(unittest.TestCase):
def test_daytime_typical(self) -> None:
self.assertEqual(aggregate_pv_production_w(6000, 4000, 0), 10_000)
def test_negative_gen_ignored_in_total(self) -> None:
self.assertEqual(aggregate_pv_production_w(0, 0, -61), 0)
def test_false_uint16_gen_becomes_small_negative(self) -> None:
raw = 65472
signed = raw - 65536 if raw > 32767 else raw
self.assertEqual(signed, -64)
self.assertEqual(aggregate_pv_production_w(0, 0, signed), 0)
def test_false_uint16_pv1_like_grid_export_clamped(self) -> None:
raw = 52038
signed = raw - 65536 if raw > 32767 else raw
self.assertEqual(signed, -13_498)
self.assertEqual(aggregate_pv_production_w(signed, 0, 0), 0)
def test_mixed_one_negative(self) -> None:
self.assertEqual(aggregate_pv_production_w(8000, -100, 2000), 10_000)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,12 @@
-- PV1/PV2/GEN na Deye jsou int16 W; sběr ukládá signed hodnoty do sloupců.
-- pv_power_w = součet max(0, kanál) pro metriku okamžité výroby FVE.
COMMENT ON COLUMN ems.telemetry_inverter.pv1_power_w IS
'Výkon PV1 v W (signed int16 z Modbus). Záporné = není kladná DC výroba tohoto vstupu.';
COMMENT ON COLUMN ems.telemetry_inverter.pv2_power_w IS
'Výkon PV2 v W (signed int16 z Modbus). Záporné = není kladná DC výroba tohoto vstupu.';
COMMENT ON COLUMN ems.telemetry_inverter.gen_port_power_w IS
'Výkon GEN portu v W (signed int16 z Modbus). FVE pole B (ongrid); záporné při zpětném toku / režimu bez výroby.
Nelze curtailovat. Klíčový pro audit zeleného bonusu.';
COMMENT ON COLUMN ems.telemetry_inverter.pv_power_w IS
'Okamžitá výroba FVE v W: max(0,pv1)+max(0,pv2)+max(0,gen_port) — součet kladných příspěvků kanálů po signed čtení z Deye.';

View File

@@ -128,9 +128,9 @@ Limity nabíjení/vybíjení v ampérech a export z **site_grid_connection** / *
| 590 | Battery power | 1 W S16 | + vybíjení / nabíjení | | 590 | Battery power | 1 W S16 | + vybíjení / nabíjení |
| 625 | Grid total power | 1 W S16 | + import / export | | 625 | Grid total power | 1 W S16 | + import / export |
| 653 | Load total power | 1 W S16 | | | 653 | Load total power | 1 W S16 | |
| 667 | GEN port power | 1 W | FVE pole B | | 667 | GEN port power | 1 W S16 | FVE pole B; signed — záporné při zpětném toku / bez výroby |
| 672 | PV1 power | 1 W | | | 672 | PV1 power | 1 W S16 | signed; EMS ukládá raw signed W, do `pv_power_w` jen max(0, kanál) |
| 673 | PV2 power | 1 W | | | 673 | PV2 power | 1 W S16 | jako PV1 |
## Přepočty ## Přepočty

View File

@@ -46,12 +46,17 @@ Komunikace: Modbus TCP, Unit ID dle DIP přepínače na střídači (typicky 1).
| 590 (0x024E) | int16 | Tok výkonu baterie | W | signed: **+ vybíjení, nabíjení** | | 590 (0x024E) | int16 | Tok výkonu baterie | W | signed: **+ vybíjení, nabíjení** |
| 625 (0x0271) | int16 | Výkon sítě | W | signed: **+ import, export** | | 625 (0x0271) | int16 | Výkon sítě | W | signed: **+ import, export** |
| 653 (0x028D) | uint16 | Celková spotřeba | W | `load_power_w` | | 653 (0x028D) | uint16 | Celková spotřeba | W | `load_power_w` |
| 667 (0x029B) | uint16 | Výkon GEN portu (FVE pole B) | W | `gen_port_power_w`, nelze curtailovat | | 667 (0x029B) | int16 | Výkon GEN portu (FVE pole B) | W (signed) | `gen_port_power_w`; záporné při zpětném toku / bez výroby — **číst signed** |
| 672 (0x02A0) | uint16 | Výkon PV1 | W | `pv1_power_w` | | 672 (0x02A0) | int16 | Výkon PV1 | W (signed) | `pv1_power_w`; při špatném unsigned čtení se záporné hodnoty jeví jako desítky kW |
| 673 (0x02A1) | uint16 | Výkon PV2 | W | `pv2_power_w` | | 673 (0x02A1) | int16 | Výkon PV2 | W (signed) | `pv2_power_w` |
`pv_power_w` v DB = **PV1 + PV2 + GEN port** (celková výroba na instalaci home-01). `pv1_power_w` / `pv2_power_w` / `gen_port_power_w` v DB = **signed W** z Modbus (mohou být záporné).
`gen_port_power_w` zůstává i nadále uložen samostatně pro audit a detailní diagnostiku.
`pv_power_w` = **max(0, PV1) + max(0, PV2) + max(0, GEN)** — okamžitá **kladná** výroba FVE pro dashboard a souhrny; záporné kanály do součtu nepatří (typicky večer při exportu z baterie do sítě).
`gen_port_power_w` zůstává uložen samostatně pro audit zeleného bonusu a diagnostiku.
**Ověření po změně sběru:** za denního SVitu zkontrolovat, že `pv_power_w` a jednotlivé kanály odpovídají očekávanému max. výkonu instalace (logika: `aggregate_pv_production_w` v `telemetry_collector.py`, unit testy `tests/test_telemetry_pv_signed.py`).
**Zápis setpointů (plánování → Deye):** **Zápis setpointů (plánování → Deye):**

View File

@@ -15,7 +15,7 @@ Shrnutí otevřených bodů z `docs/06-open-questions.md`, checklistů v modulec
| **Import OTE robustní provoz:** timeouty + retry/backoff v `price_importer.py`, detailní error kódy v API, fallback D+1 → dnešek, scheduler importů 13:30 / 14:00 / 00:05. | | **Import OTE robustní provoz:** timeouty + retry/backoff v `price_importer.py`, detailní error kódy v API, fallback D+1 → dnešek, scheduler importů 13:30 / 14:00 / 00:05. |
| **Fail-safe bez OTE dat:** při predikovaných cenách v kritickém okně je zákaz exportu; vybíjení baterie omezeno jen v predikovaných slotech; runtime guard v `control_exporter.py` brání SELL v nejistém stavu. | | **Fail-safe bez OTE dat:** při predikovaných cenách v kritickém okně je zákaz exportu; vybíjení baterie omezeno jen v predikovaných slotech; runtime guard v `control_exporter.py` brání SELL v nejistém stavu. |
| **Forecast provoz:** refresh každé 2 hodiny (`:05`), konfigurovatelný Open-Meteo horizont (`OPEN_METEO_FORECAST_DAYS`, default 7, clamp 2..16), endpoint pro UI vrací latest-run bez duplicit slotů. | | **Forecast provoz:** refresh každé 2 hodiny (`:05`), konfigurovatelný Open-Meteo horizont (`OPEN_METEO_FORECAST_DAYS`, default 7, clamp 2..16), endpoint pro UI vrací latest-run bez duplicit slotů. |
| **Telemetry výroba FVE:** `pv_power_w` je součet `pv1 + pv2 + gen_port`, takže dashboard reflektuje obě pole i GEN větev instalace home-01. | | **Telemetry výroba FVE:** Registry 672/673/667 jsou **signed** W; `pv_power_w` = max(0,pv1)+max(0,pv2)+max(0,gen) (dashboard); sloupce pv1/pv2/gen ukládají signed pro audit. |
| **Ekonomika baterie:** snížení `reserve_soc_percent` na 10 % a `degradation_cost_czk_kwh` na 0.1500 (migrace `V026__battery_economics_tuning.sql`), úpravy objective pro ekonomicky konzistentnější nabíjení/vybíjení. | | **Ekonomika baterie:** snížení `reserve_soc_percent` na 10 % a `degradation_cost_czk_kwh` na 0.1500 (migrace `V026__battery_economics_tuning.sql`), úpravy objective pro ekonomicky konzistentnější nabíjení/vybíjení. |
| **Planning UI operátor akce:** trvale viditelné akce import/forecast/init plan, volba data OTE (dnes/zítra), zobrazení `pv_scarcity_factor` ve stavu plánu. | | **Planning UI operátor akce:** trvale viditelné akce import/forecast/init plan, volba data OTE (dnes/zítra), zobrazení `pv_scarcity_factor` ve stavu plánu. |

View File

@@ -18,11 +18,11 @@ REGISTERS: dict[str, tuple[int, str, int, str, str]] = {
"battery_power_w": (590, "sint", 1, "W", "+ vybíjení / nabíjení"), "battery_power_w": (590, "sint", 1, "W", "+ vybíjení / nabíjení"),
"batt_charge_today_wh": (514, "uint", 1, "Wh", "dnešní nabití baterie"), "batt_charge_today_wh": (514, "uint", 1, "Wh", "dnešní nabití baterie"),
"batt_discharge_today_wh": (515, "uint", 1, "Wh", "dnešní vybití baterie"), "batt_discharge_today_wh": (515, "uint", 1, "Wh", "dnešní vybití baterie"),
"gen_port_power_w": (667, "uint", 1, "W", "GEN port FVE pole B"), "gen_port_power_w": (667, "sint", 1, "W", "GEN port FVE pole B (signed)"),
"grid_total_power_w": (625, "sint", 1, "W", "+ import ze sítě / export"), "grid_total_power_w": (625, "sint", 1, "W", "+ import ze sítě / export"),
"load_total_power_w": (653, "uint", 1, "W", "celková spotřeba"), "load_total_power_w": (653, "uint", 1, "W", "celková spotřeba"),
"pv1_power_w": (672, "uint", 1, "W", "výkon PV1"), "pv1_power_w": (672, "sint", 1, "W", "výkon PV1 (signed)"),
"pv2_power_w": (673, "uint", 1, "W", "výkon PV2"), "pv2_power_w": (673, "sint", 1, "W", "výkon PV2 (signed)"),
} }