Bazén: sezóna (schedulable), filtrace dle teploty vody, Loxone čidla
- V092: ems.loxone_sensor + telemetry_loxone_sensor (hypertable) — generické čtení Loxone hodnot (poslouží i ohřevu/akumulačce); pool sloupce teplotní funkce (ref/base/per_c/min/max) + water_temp_sensor_id - R__098 fn_pool_daily_runtime_min: clamp(base+per_c×(t−ref)) z poslední teploty <24 h, fallback daily_runtime_min; JSON detail pro UI/solver - collector poll_loxone_sensors: /jdev/sps/io/<name>/state, LL.value parse, no-op bez čidel - sezóna = schedulable přepínač (dokumentováno vč. SQL); hranice filtrace × ohřev TČ (oddělené logiky, sdílí jen čidlo) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -444,6 +444,61 @@ async def poll_heat_pump(site_id: int, db: asyncpg.Connection) -> None:
|
||||
)
|
||||
|
||||
|
||||
async def poll_loxone_sensors(site_id: int, db: asyncpg.Connection) -> None:
|
||||
"""Čidla z Loxone (teplota bazénu, akumulační nádrže…): GET /jdev/sps/io/<name>/state.
|
||||
|
||||
Endpoint = site loxone_http; auth LOXONE_USER/PASSWORD (env). Hodnota
|
||||
z LL.value ("23.5°" → 23.5). Bez čidel v ems.loxone_sensor no-op.
|
||||
"""
|
||||
rows = await db.fetch(
|
||||
"""
|
||||
select ls.id, ls.loxone_name, se.host, se.port, se.protocol
|
||||
from ems.loxone_sensor ls
|
||||
join ems.site_endpoint se
|
||||
on se.site_id = ls.site_id and se.endpoint_type = 'loxone_http' and se.enabled
|
||||
where ls.site_id = $1 and ls.enabled
|
||||
""",
|
||||
site_id,
|
||||
)
|
||||
if not rows:
|
||||
return
|
||||
import os
|
||||
import re as _re
|
||||
|
||||
import httpx
|
||||
|
||||
auth = None
|
||||
user = os.getenv("LOXONE_USER") or ""
|
||||
if user:
|
||||
auth = (user, os.getenv("LOXONE_PASSWORD") or "")
|
||||
measured_at = datetime.now(timezone.utc)
|
||||
async with httpx.AsyncClient(timeout=5.0, auth=auth) as client:
|
||||
for r in rows:
|
||||
proto = (r["protocol"] or "http").lower()
|
||||
port = int(r["port"] or (443 if proto == "https" else 80))
|
||||
url = f"{proto}://{r['host']}:{port}/jdev/sps/io/{r['loxone_name']}/state"
|
||||
try:
|
||||
resp = await client.get(url)
|
||||
resp.raise_for_status()
|
||||
raw = str((resp.json().get("LL") or {}).get("value", ""))
|
||||
m = _re.search(r"-?\d+(?:[.,]\d+)?", raw)
|
||||
if m is None:
|
||||
continue
|
||||
value = float(m.group(0).replace(",", "."))
|
||||
except Exception as e:
|
||||
logger.warning("Loxone sensor %s read failed: %s", r["loxone_name"], e)
|
||||
continue
|
||||
await db.execute(
|
||||
"""
|
||||
insert into ems.telemetry_loxone_sensor (sensor_id, measured_at, value)
|
||||
values ($1, $2, $3) on conflict do nothing
|
||||
""",
|
||||
int(r["id"]),
|
||||
measured_at,
|
||||
value,
|
||||
)
|
||||
|
||||
|
||||
async def run_telemetry_loop(conn: asyncpg.Connection) -> float:
|
||||
"""Jeden průchod smyčky; vrátí uplynulý čas v sekundách (pro sleep).
|
||||
|
||||
@@ -460,6 +515,7 @@ async def run_telemetry_loop(conn: asyncpg.Connection) -> float:
|
||||
await poll_inverter(sid, conn)
|
||||
await poll_ev_chargers(sid, conn)
|
||||
await poll_heat_pump(sid, conn)
|
||||
await poll_loxone_sensors(sid, conn)
|
||||
await poll_pool_pumps(sid, conn)
|
||||
except Exception as e:
|
||||
logger.error("Telemetry loop error site %s: %s", sid, e)
|
||||
|
||||
Reference in New Issue
Block a user