Files
ems/backend/services/control/repository.py
2026-05-02 19:42:58 +02:00

216 lines
7.8 KiB
Python

"""DB načítání pro control export."""
from __future__ import annotations
import json
from datetime import datetime, timezone
import asyncpg
from services.control.deye_helpers import DEYE_LV_BATTERY_MAX_CHARGE_DISCHARGE_A
from services.control.models import InverterConfig, OperatingModeInfo
from services.control.setpoints import _DictRecord
async def _fetch_operating_mode(
site_id: int, db: asyncpg.Connection
) -> OperatingModeInfo | None:
sql = """
SELECT som.mode_code, omd.battery_mode, omd.grid_mode,
omd.ev_enabled, omd.heat_pump_enabled, omd.loxone_mode_value,
som.valid_until
FROM ems.site_operating_mode som
JOIN ems.operating_mode_def omd ON omd.code = som.mode_code
WHERE som.site_id = $1
"""
row = await db.fetchrow(sql, site_id)
if row is None:
return None
vu = row["valid_until"]
if vu is not None:
now_utc = datetime.now(timezone.utc)
if vu.tzinfo is None:
vu = vu.replace(tzinfo=timezone.utc)
if vu <= now_utc:
exp_rows = await db.fetch("SELECT * FROM ems.fn_expire_modes()")
from services.notification_service import notify_operating_mode_changed
for er in exp_rows:
await notify_operating_mode_changed(
str(er["site_code"]),
str(er["old_mode"]),
str(er["new_mode"]),
"system:expiry",
"Automatické vypršení dočasného režimu",
)
row = await db.fetchrow(sql, site_id)
if row is None:
return None
return OperatingModeInfo(
mode_code=row["mode_code"],
battery_mode=row["battery_mode"],
grid_mode=row["grid_mode"],
ev_enabled=bool(row["ev_enabled"]),
heat_pump_enabled_def=bool(row["heat_pump_enabled"]),
loxone_mode_value=int(row["loxone_mode_value"]),
)
async def _get_current_soc(site_id: int, db: asyncpg.Connection) -> int:
soc = await db.fetchval(
"""
SELECT battery_soc_percent
FROM ems.telemetry_inverter
WHERE site_id = $1 AND battery_soc_percent IS NOT NULL
ORDER BY measured_at DESC
LIMIT 1
""",
site_id,
)
return int(soc) if soc is not None else 50
async def _load_inverter_config(
site_id: int, db: asyncpg.Connection
) -> InverterConfig | None:
row = await db.fetchrow(
"""
SELECT
ai.id, ai.code,
coalesce(ems.fn_inverter_pv_a_max_w(ai.id), 0) AS pv_a_cap_w,
se.host, se.port, se.unit_id,
sgc.max_export_power_w,
sgc.max_import_power_w,
sgc.no_export,
ai.max_battery_charge_w,
ai.max_battery_discharge_w,
ab.min_soc_percent,
ab.reserve_soc_percent,
ab.max_soc_percent,
ab.usable_capacity_wh,
ai.deye_last_system_time_sync_minute,
ai.deye_last_system_time_sync_at,
ai.deye_last_tou_inactive_write_prague_date,
ai.deye_tou_inactive_signature,
COALESCE(ai.deye_zero_export_mode, 1) AS deye_zero_export_mode,
COALESCE(ai.deye_gen_microinverter_cutoff_enabled, false) AS deye_gen_microinverter_cutoff_enabled,
coalesce(ems.fn_site_has_active_green_bonus_pv(ai.site_id), false)
AS deye_reg340_pv_a_control_enabled,
COALESCE(
ai.deye_register_max_charge_a,
FLOOR(
LEAST(
COALESCE(ab.bms_max_charge_w, ai.max_battery_charge_w),
ai.max_battery_charge_w
)::numeric / 51.2
)::int
) AS max_charge_a,
COALESCE(
ai.deye_register_max_discharge_a,
FLOOR(
LEAST(
COALESCE(ab.bms_max_discharge_w, ai.max_battery_discharge_w),
ai.max_battery_discharge_w
)::numeric / 51.2
)::int
) AS max_discharge_a
FROM ems.asset_inverter ai
JOIN ems.site_endpoint se ON se.id = ai.endpoint_id
JOIN ems.asset_battery ab ON ab.inverter_id = ai.id
LEFT JOIN ems.site_grid_connection sgc ON sgc.site_id = ai.site_id
WHERE ai.site_id = $1
AND ai.active = true
AND ai.controllable = true
AND se.enabled = true
AND se.endpoint_type = 'modbus_tcp'
ORDER BY ai.id
LIMIT 1
""",
site_id,
)
if row is None:
return None
mc = row["max_charge_a"]
md = row["max_discharge_a"]
max_charge_a = int(mc) if mc is not None else 0
max_discharge_a = int(md) if md is not None else 0
max_charge_a = min(max_charge_a, DEYE_LV_BATTERY_MAX_CHARGE_DISCHARGE_A)
max_discharge_a = min(max_discharge_a, DEYE_LV_BATTERY_MAX_CHARGE_DISCHARGE_A)
port = int(row["port"] or 502)
uid = int(row["unit_id"] if row["unit_id"] is not None else 1)
return InverterConfig(
id=int(row["id"]),
code=row["code"],
host=row["host"],
port=port,
unit_id=uid,
max_export_power_w=int(row["max_export_power_w"])
if row["max_export_power_w"] is not None
else None,
max_import_power_w=int(row["max_import_power_w"])
if row["max_import_power_w"] is not None
else None,
no_export=bool(row["no_export"] or False),
max_battery_charge_w=int(row["max_battery_charge_w"])
if row["max_battery_charge_w"] is not None
else None,
max_battery_discharge_w=int(row["max_battery_discharge_w"])
if row["max_battery_discharge_w"] is not None
else None,
min_soc_percent=int(round(float(row["min_soc_percent"])))
if row["min_soc_percent"] is not None
else None,
reserve_soc_percent=int(row["reserve_soc_percent"])
if row["reserve_soc_percent"] is not None
else None,
max_soc_percent=int(row["max_soc_percent"])
if row["max_soc_percent"] is not None
else None,
usable_capacity_wh=int(row["usable_capacity_wh"])
if row["usable_capacity_wh"] is not None
else None,
max_charge_a=max_charge_a,
max_discharge_a=max_discharge_a,
deye_last_system_time_sync_minute=row["deye_last_system_time_sync_minute"],
deye_last_system_time_sync_at=row["deye_last_system_time_sync_at"],
deye_last_tou_inactive_write_prague_date=row[
"deye_last_tou_inactive_write_prague_date"
],
deye_tou_inactive_signature=row["deye_tou_inactive_signature"],
deye_zero_export_mode=int(row["deye_zero_export_mode"]),
deye_gen_microinverter_cutoff_enabled=bool(
row["deye_gen_microinverter_cutoff_enabled"] or False
),
pv_a_cap_w=int(row["pv_a_cap_w"] or 0),
deye_reg340_pv_a_control_enabled=bool(
row["deye_reg340_pv_a_control_enabled"] or False
),
)
async def _fetch_plan_row_for_slot_offset(
site_id: int, db: asyncpg.Connection, slot_offset: int
) -> asyncpg.Record | None:
"""Řádek plánu pro slot z ems.fn_planning_interval_at_offset (jsonb -> Record-like dict)."""
raw = await db.fetchval(
"""
select ems.fn_planning_interval_at_offset($1::int, $2::int)
""",
site_id,
slot_offset,
)
if raw is None:
return None
data = raw if isinstance(raw, dict) else json.loads(raw)
if not data:
return None
return _DictRecord(data)
async def _fetch_max_charge_power_w(site_id: int, db: asyncpg.Connection) -> int:
v = await db.fetchval(
"select ems.fn_planning_max_effective_charge_w($1::int)",
site_id,
)
return int(v or 0)