refactor control export orchestrator
This commit is contained in:
@@ -2,11 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
import asyncpg
|
|
||||||
|
|
||||||
from services.control.deye_helpers import (
|
from services.control.deye_helpers import (
|
||||||
BATT_VOLTAGE_V,
|
BATT_VOLTAGE_V,
|
||||||
DEYE_CLOCK_DRIFT_OK_SEC,
|
DEYE_CLOCK_DRIFT_OK_SEC,
|
||||||
@@ -55,6 +50,7 @@ from services.control.outputs import (
|
|||||||
write_ev_setpoints,
|
write_ev_setpoints,
|
||||||
write_heat_pump_setpoint,
|
write_heat_pump_setpoint,
|
||||||
)
|
)
|
||||||
|
from services.control.orchestrator import export_setpoints
|
||||||
from services.control.repository import (
|
from services.control.repository import (
|
||||||
_fetch_max_charge_power_w,
|
_fetch_max_charge_power_w,
|
||||||
_fetch_operating_mode,
|
_fetch_operating_mode,
|
||||||
@@ -84,138 +80,3 @@ from services.control.verify import (
|
|||||||
_verify_deye_clock_written_bundle,
|
_verify_deye_clock_written_bundle,
|
||||||
verify_modbus_commands,
|
verify_modbus_commands,
|
||||||
)
|
)
|
||||||
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
|
|
||||||
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,
|
|
||||||
reg340_pv_a_control_enabled=reg340_en,
|
|
||||||
)
|
|
||||||
sp_next = _build_setpoints(
|
|
||||||
mode,
|
|
||||||
pi_next,
|
|
||||||
pv_a_cap_w=cap_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)
|
|
||||||
# Oba setpointy kladné → get_deye_mode CHARGE; min. 1 W, aby režim nebyl PASSIVE při nulové 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
|
|
||||||
)
|
|
||||||
|
|||||||
156
backend/services/control/orchestrator.py
Normal file
156
backend/services/control/orchestrator.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
"""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
|
||||||
|
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,
|
||||||
|
reg340_pv_a_control_enabled=reg340_en,
|
||||||
|
)
|
||||||
|
sp_next = _build_setpoints(
|
||||||
|
mode,
|
||||||
|
pi_next,
|
||||||
|
pv_a_cap_w=cap_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
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user