122 lines
4.2 KiB
Python
122 lines
4.2 KiB
Python
"""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"),
|
||
}
|