stranka configuration
Some checks failed
test / smoke-test (push) Has been cancelled
deploy / deploy (push) Has been cancelled

This commit is contained in:
Dusan Vojacek
2026-04-12 16:56:44 +02:00
parent b50041cfc7
commit 4e81a36371
6 changed files with 796 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ from app.routers.energy_flows import router as energy_flows_router
from app.routers.ev import router as ev_router
from app.routers.full_status import router as full_status_router
from app.routers.plan import router as plan_router
from app.routers.site_configuration import router as site_configuration_router
from app.ws_log_handler import WSLogHandler
from app.ws_manager import manager
from fastapi import (
@@ -526,6 +527,7 @@ app = FastAPI(title="EMS Platform", lifespan=lifespan)
app.include_router(plan_router, prefix="/api/v1")
app.include_router(ev_router, prefix="/api/v1")
app.include_router(full_status_router, prefix="/api/v1")
app.include_router(site_configuration_router, prefix="/api/v1")
app.include_router(economics_router, prefix="/api/v1")
app.include_router(energy_flows_router, prefix="/api/v1")

View File

@@ -0,0 +1,248 @@
"""GET /sites/{site_id}/configuration read-only souhrn konfigurace lokality."""
from __future__ import annotations
from datetime import datetime, timezone
from typing import Annotated, Any
import asyncpg
from fastapi import APIRouter, Depends, HTTPException
from app.db_json import record_to_dict
from app.deps import get_pg_pool
router = APIRouter(prefix="/sites/{site_id}", tags=["sites"])
_DEYE_KEYS = frozenset(
{
"deye_last_system_time_sync_at",
"deye_last_system_time_sync_minute",
"deye_last_tou_inactive_write_prague_date",
"deye_tou_inactive_signature",
}
)
def _mask_secret_reference(raw: str | None) -> str | None:
if raw is None:
return None
s = str(raw).strip()
if not s:
return None
if len(s) <= 4:
return "nastaveno"
return f"{s[-2:]}"
def _iso_utc(dt: datetime | None) -> str | None:
if dt is None:
return None
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc).isoformat()
@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:
site_row = await conn.fetchrow(
"""
SELECT id, code, name, timezone, latitude, longitude, active, notes, created_at
FROM ems.site
WHERE id = $1
""",
site_id,
)
if site_row is None:
raise HTTPException(status_code=404, detail="Site not found")
grid_row = await conn.fetchrow(
"SELECT * FROM ems.site_grid_connection WHERE site_id = $1",
site_id,
)
market_row = await conn.fetchrow(
"""
SELECT *
FROM ems.site_market_config
WHERE site_id = $1
AND valid_from <= now()
AND (valid_to IS NULL OR valid_to > now())
ORDER BY valid_from DESC
LIMIT 1
""",
site_id,
)
endpoint_rows = await conn.fetch(
"""
SELECT id, site_id, endpoint_type, host, port, protocol, unit_id,
auth_reference, enabled, notes
FROM ems.site_endpoint
WHERE site_id = $1
ORDER BY id
""",
site_id,
)
endpoints: list[dict[str, Any]] = []
for er in endpoint_rows:
d = record_to_dict(er)
d["auth_reference"] = _mask_secret_reference(er["auth_reference"])
endpoints.append(d)
inv_rows = await conn.fetch(
"""
SELECT ai.*,
(SELECT ep.host || CASE
WHEN ep.port IS NOT NULL THEN ':' || ep.port::text
ELSE ''
END
FROM ems.site_endpoint ep
WHERE ep.id = ai.endpoint_id) AS endpoint_connection
FROM ems.asset_inverter ai
WHERE ai.site_id = $1
ORDER BY ai.id
""",
site_id,
)
inverters: list[dict[str, Any]] = []
for ir in inv_rows:
full = record_to_dict(ir)
ep_label = full.pop("endpoint_connection", None)
core = {k: v for k, v in full.items() if k not in _DEYE_KEYS}
deye_meta = {k: full[k] for k in _DEYE_KEYS if full.get(k) is not None}
core["endpoint_connection"] = ep_label
core["deye_meta"] = deye_meta if deye_meta else None
inverters.append(core)
bat_rows = await conn.fetch(
"SELECT * FROM ems.asset_battery WHERE site_id = $1 ORDER BY id",
site_id,
)
pv_rows = await conn.fetch(
"SELECT * FROM ems.asset_pv_array WHERE site_id = $1 ORDER BY id",
site_id,
)
ev_rows = await conn.fetch(
"""
SELECT ec.*,
se.host || CASE
WHEN se.port IS NOT NULL THEN ':' || se.port::text
ELSE ''
END AS endpoint_connection
FROM ems.asset_ev_charger ec
LEFT JOIN ems.site_endpoint se ON se.id = ec.endpoint_id
WHERE ec.site_id = $1
ORDER BY ec.id
""",
site_id,
)
ev_chargers = [record_to_dict(r) for r in ev_rows]
veh_rows = await conn.fetch(
"""
SELECT id, site_id, code, name, make, model, battery_capacity_kwh,
max_charge_power_w, default_charger_id, api_type, api_reference,
default_target_soc_pct, default_deadline_hour, active
FROM ems.asset_vehicle
WHERE site_id = $1
ORDER BY code
""",
site_id,
)
vehicles: list[dict[str, Any]] = []
for vr in veh_rows:
d = record_to_dict(vr)
d["api_reference"] = _mask_secret_reference(vr["api_reference"])
vehicles.append(d)
hp_rows = await conn.fetch(
"""
SELECT hp.*,
se.host || CASE
WHEN se.port IS NOT NULL THEN ':' || se.port::text
ELSE ''
END AS endpoint_connection
FROM ems.asset_heat_pump hp
LEFT JOIN ems.site_endpoint se ON se.id = hp.endpoint_id
WHERE hp.site_id = $1
ORDER BY hp.id
""",
site_id,
)
heat_pumps = [record_to_dict(r) for r in hp_rows]
mode_row = await conn.fetchrow(
"""
SELECT m.mode_code, m.activated_at, m.activated_by, m.valid_until,
m.previous_mode, m.notes,
d.name AS mode_name, d.description AS mode_description,
d.loxone_mode_value, d.ev_enabled, d.heat_pump_enabled,
d.battery_mode, d.grid_mode, d.is_autonomous
FROM ems.site_operating_mode m
JOIN ems.operating_mode_def d ON d.code = m.mode_code
WHERE m.site_id = $1
""",
site_id,
)
override_rows = await conn.fetch(
"""
SELECT id, override_type, value_json, valid_from, valid_to, reason, created_by, created_at
FROM ems.site_override
WHERE site_id = $1
AND valid_from <= now()
AND (valid_to IS NULL OR valid_to > now())
ORDER BY valid_from DESC
LIMIT 50
""",
site_id,
)
hb_row = await conn.fetchrow(
"SELECT last_seen, status FROM ems.site_heartbeat WHERE site_id = $1",
site_id,
)
run_row = await conn.fetchrow(
"""
SELECT id, created_at
FROM ems.planning_run
WHERE site_id = $1 AND status = 'active'
ORDER BY created_at DESC
LIMIT 1
""",
site_id,
)
site = record_to_dict(site_row)
lat = site_row["latitude"]
lon = site_row["longitude"]
site["latitude"] = float(lat) if lat is not None else None
site["longitude"] = float(lon) if lon is not None else None
operating_mode = record_to_dict(mode_row) if mode_row else None
return {
"site": site,
"grid_connection": record_to_dict(grid_row) if grid_row else None,
"market_config": record_to_dict(market_row) if market_row else None,
"market_config_note": (
"Zelený bonus za výrobu je u FVE polí (asset_pv_array), ne v obchodní konfiguraci."
),
"endpoints": endpoints,
"inverters": inverters,
"batteries": [record_to_dict(r) for r in bat_rows],
"pv_arrays": [record_to_dict(r) for r in pv_rows],
"ev_chargers": ev_chargers,
"vehicles": vehicles,
"heat_pumps": heat_pumps,
"operating_mode": operating_mode,
"active_overrides": [record_to_dict(r) for r in override_rows],
"operational": {
"heartbeat_last_seen": _iso_utc(hb_row["last_seen"]) if hb_row else None,
"heartbeat_status": hb_row["status"] if hb_row else None,
"has_active_plan": run_row is not None,
"active_plan_created_at": _iso_utc(run_row["created_at"]) if run_row else None,
},
}