"""Export plánovaných setpointů na Modbus (Deye, EV, TČ) a HTTP do Loxone.""" from __future__ import annotations import logging from typing import Any import asyncpg from services.control.deye_helpers import ( BATT_VOLTAGE_V, DEYE_CLOCK_DRIFT_OK_SEC, DEYE_CLOCK_REGS, DEYE_CLOCK_RESYNC_INTERVAL_HOURS, DEYE_CLOCK_VERIFY_MAX_DELTA_SEC, # noqa: F401 - re-export for compatibility DEYE_CRITICAL_REGS_SELF_SUSTAIN, # noqa: F401 - re-export for compatibility DEYE_REGISTER_NAMES, # noqa: F401 - re-export for compatibility DEYE_TOU_INACTIVE_HHMM, DEYE_TOU_POWER_REGS, PRAGUE_TZ, REG143_SELL_CAP_MIN_W, REG178_MI_EXPORT_DISABLE, REG178_MI_EXPORT_ENABLE, REG178_MI_EXPORT_MASK, REG178_PASSIVE, REG178_SELL, REG178_VERIFY_MASK, REG178_VERIFY_MASK_COMBINED, _DEYE_INACTIVE_TOU_REGISTERS, _deye_clock_registers_verify_match, _deye_reg178_verify_match, _deye_reg178_verify_with_double_read, _deye_registers_to_prague_datetime, # noqa: F401 - re-export for compatibility _deye_should_skip_time_sync_after_read, _deye_tou_power_verify_match, _prague_minute_start_utc, battery_watts_to_amps, compute_pv_a_reg340_max_solar_w, current_slot_hhmm, deye_reg_triggers_self_sustain_after_verify_exhaust, # noqa: F401 - re-export next_slot_hhmm, watts_to_amps, ) from services.control.inverter import read_deye_registers_live, write_inverter_setpoints from services.control.models import ControlSetpoints, InverterConfig, OperatingModeInfo from services.control.modbus_journal import ( _drop_registers_matching_last_verified, _fetch_last_verified_inverter_registers, create_modbus_commands, execute_modbus_commands, ) from services.control.outputs import ( _current_limit_for_charger, 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, _get_current_soc, _load_inverter_config, ) from services.control.setpoints import ( _DictRecord, _apply_price_failsafe_guard, _build_setpoints, _clamp_deye_tou_soc_pct, _deye_passive_tou_battery_soc_pct, _deye_reg143_export_w, _deye_system_time_register_rows, _deye_time_point_rows, _deye_tou_min_soc_pct, _deye_tou_params, _deye_tou_reserve_soc_pct, _deye_zero_export_amps_for_passive, get_deye_mode, ) from services.control.verify import ( _deye_expected_clock_triplet_for_verify, _modbus_cmd_register, _switch_to_self_sustain, _verify_deye_clock_written_bundle, 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 )