Files
ems/backend/services/control/orchestrator.py
Dusan Vojacek c6074e9c74
Some checks failed
CI and deploy / migration-check (push) Failing after 13s
CI and deploy / deploy (push) Has been skipped
nastavitelny max sollar dle stridace (ulozeno v DB)
2026-05-25 11:25:29 +02:00

160 lines
5.3 KiB
Python

"""Top-level control export orchestration."""
from __future__ import annotations
import logging
import asyncpg
from services.control.inverter import write_inverter_setpoints
from services.control.models import ControlSetpoints
from services.control.outputs import (
send_loxone_setpoints,
write_ev_setpoints,
write_heat_pump_setpoint,
)
from services.control.repository import (
_fetch_max_charge_power_w,
_fetch_operating_mode,
_fetch_plan_row_for_slot_offset,
_load_inverter_config,
)
from services.control.setpoints import _apply_price_failsafe_guard, _build_setpoints
from services.signal_service import enqueue_site_signals
logger = logging.getLogger(__name__)
async def export_setpoints(site_id: int, db: asyncpg.Connection) -> None:
mode = await _fetch_operating_mode(site_id, db)
if mode is None:
logger.warning("control export site=%s: no operating mode row", site_id)
return
if mode.mode_code == "MANUAL":
logger.info("control export site=%s: MANUAL, skip writes", site_id)
return
try:
inv_for_pv = await _load_inverter_config(site_id, db)
cap_pv = int(inv_for_pv.pv_a_cap_w) if inv_for_pv is not None else 0
min_pv = int(inv_for_pv.pv_a_reg340_min_w) if inv_for_pv is not None else 0
reg340_en = (
bool(inv_for_pv.deye_reg340_pv_a_control_enabled)
if inv_for_pv is not None
else False
)
pi_now = await _fetch_plan_row_for_slot_offset(site_id, db, 0)
pi_next = await _fetch_plan_row_for_slot_offset(site_id, db, 1)
sp_now = _build_setpoints(
mode,
pi_now,
pv_a_cap_w=cap_pv,
pv_a_reg340_min_w=min_pv,
reg340_pv_a_control_enabled=reg340_en,
)
sp_next = _build_setpoints(
mode,
pi_next,
pv_a_cap_w=cap_pv,
pv_a_reg340_min_w=min_pv,
reg340_pv_a_control_enabled=reg340_en,
)
if mode.mode_code == "AUTO" and sp_now is None:
if pi_now is None:
logger.warning(
"control export site=%s: AUTO but no planning_interval for current slot, skip",
site_id,
)
return
if sp_now is None:
logger.warning(
"control export site=%s: no setpoints for mode %s, skip",
site_id,
mode.mode_code,
)
return
if mode.mode_code == "CHARGE_CHEAP":
max_ch = await _fetch_max_charge_power_w(site_id, db)
pw = max(1, int(max_ch))
sp_now = ControlSetpoints(
battery_w=pw,
grid_export_limit=0,
ev1_current_a=0,
ev2_current_a=0,
heat_pump_enable=False,
grid_setpoint_w=pw,
ev1_power_w=0,
ev2_power_w=0,
target_soc_pct=None,
effective_sell_price_czk_kwh=None,
)
sp_next = sp_now
else:
sp_now = _apply_price_failsafe_guard(site_id, mode, pi_now, sp_now)
if sp_next is not None:
sp_next = _apply_price_failsafe_guard(site_id, mode, pi_next, sp_next)
planning_run_id = await db.fetchval(
"""
SELECT id FROM ems.planning_run
WHERE site_id = $1 AND status = 'active'
ORDER BY created_at DESC
LIMIT 1
""",
site_id,
)
if planning_run_id is not None:
planning_run_id = int(planning_run_id)
try:
inv_res = await write_inverter_setpoints(
site_id, sp_now, sp_next, db, planning_run_id=planning_run_id
)
except Exception as e:
logger.error("inverter write failed: %s", e)
inv_res = f"FAIL inverter: {e}"
try:
ev_res = await write_ev_setpoints(site_id, sp_now, db)
except Exception as e:
logger.error("ev write failed: %s", e)
ev_res = f"FAIL ev: {e}"
try:
hp_res = await write_heat_pump_setpoint(site_id, sp_now, db)
except Exception as e:
logger.error("hp write failed: %s", e)
hp_res = f"FAIL heat pump: {e}"
try:
lox_res = await send_loxone_setpoints(site_id, sp_now, mode, db)
except Exception as e:
logger.error("loxone write failed: %s", e)
lox_res = f"FAIL Loxone: {e}"
results = list(
zip(
("inverter", "ev", "heat_pump", "loxone"),
(inv_res, ev_res, hp_res, lox_res),
)
)
for name, res in results:
if isinstance(res, Exception):
logger.error("control export site=%s %s: FAIL %s", site_id, name, res)
elif isinstance(res, str) and res.startswith("FAIL"):
logger.error("control export site=%s %s: %s", site_id, name, res)
else:
logger.info("control export site=%s %s: %s", site_id, name, res)
finally:
try:
await enqueue_site_signals(site_id, db)
except Exception as e:
logger.warning(
"control export site=%s: signal enqueue failed: %s", site_id, e
)