"""GET /sites/{site_id}/configuration – read-only souhrn konfigurace lokality.""" from __future__ import annotations import json from datetime import datetime, timezone from typing import Annotated, Any import asyncpg from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, Field from app.db_json import fetch_json from app.deps import get_pg_pool router = APIRouter(prefix="/sites/{site_id}", tags=["sites"]) class InverterModbusCurrentCapsBody(BaseModel): """Tvrdý strop proudu pro zápis Deye reg 108/109 (A); NULL ve JSONu = smaž strop v DB.""" deye_register_max_charge_a: int | None = Field( default=None, ge=0, le=640, description="None při vynechání klíče = nezměnit; explicitní null = smazat strop", ) deye_register_max_discharge_a: int | None = Field( default=None, ge=0, le=640, description="Jako u nabíjení", ) def _iso_utc_from_cfg(val: Any) -> str | None: if val is None: return None if isinstance(val, str): return val if isinstance(val, datetime): dt = val if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) return dt.astimezone(timezone.utc).isoformat() return str(val) @router.get("/configuration") async def get_site_configuration( site_id: int, pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)], ) -> dict[str, Any]: async with pool.acquire() as conn: raw = await fetch_json( conn, "select ems.fn_site_configuration($1::int)", site_id, ) if raw is None: raise HTTPException(status_code=404, detail="Site not found") if not isinstance(raw, dict): raw = json.loads(raw) op = raw.get("operational") if isinstance(op, dict): op = dict(op) op["heartbeat_last_seen"] = _iso_utc_from_cfg(op.get("heartbeat_last_seen")) op["active_plan_created_at"] = _iso_utc_from_cfg(op.get("active_plan_created_at")) raw["operational"] = op lat = raw.get("site", {}).get("latitude") if isinstance(raw.get("site"), dict) else None lon = raw.get("site", {}).get("longitude") if isinstance(raw.get("site"), dict) else None if isinstance(raw.get("site"), dict): site = dict(raw["site"]) site["latitude"] = float(lat) if lat is not None else None site["longitude"] = float(lon) if lon is not None else None raw["site"] = site return raw @router.patch("/inverters/{inverter_id}/modbus-current-caps") async def patch_inverter_modbus_current_caps( site_id: int, inverter_id: int, body: InverterModbusCurrentCapsBody, pool: Annotated[asyncpg.Pool, Depends(get_pg_pool)], ) -> dict[str, Any]: """ Nastavení `deye_register_max_charge_a` / `deye_register_max_discharge_a` na `ems.asset_inverter`. """ updates = body.model_dump(exclude_unset=True) if not updates: raise HTTPException( status_code=400, detail="Send at least one of: deye_register_max_charge_a, deye_register_max_discharge_a", ) patch: dict[str, Any] = {} if "deye_register_max_charge_a" in updates: patch["deye_register_max_charge_a"] = updates["deye_register_max_charge_a"] if "deye_register_max_discharge_a" in updates: patch["deye_register_max_discharge_a"] = updates["deye_register_max_discharge_a"] async with pool.acquire() as conn: raw = await fetch_json( conn, "select ems.fn_inverter_modbus_caps_patch($1::int, $2::int, $3::jsonb)", site_id, inverter_id, json.dumps(patch), ) if not isinstance(raw, dict): raw = json.loads(raw) if not raw.get("ok"): if raw.get("error") == "not_found": raise HTTPException(status_code=404, detail="Inverter not found for this site") raise HTTPException(status_code=400, detail=raw.get("error", "patch_failed")) return { "inverter_id": int(raw["inverter_id"]), "code": raw["code"], "deye_register_max_charge_a": raw.get("deye_register_max_charge_a"), "deye_register_max_discharge_a": raw.get("deye_register_max_discharge_a"), }