|
@@ -17,6 +17,7 @@ Valikuline kaitse: WEB_ACCESS_TOKEN keskkonnamuutuja (tühi = kaitseta).
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
+import asyncio
|
|
|
import hmac
|
|
import hmac
|
|
|
import html
|
|
import html
|
|
|
import secrets
|
|
import secrets
|
|
@@ -30,6 +31,7 @@ from datetime import datetime, timedelta
|
|
|
from pathlib import Path
|
|
from pathlib import Path
|
|
|
from zoneinfo import ZoneInfo
|
|
from zoneinfo import ZoneInfo
|
|
|
|
|
|
|
|
|
|
+import paho.mqtt.client as mqtt
|
|
|
import requests
|
|
import requests
|
|
|
from fastapi import Body, Depends, FastAPI, Form, HTTPException, Query, Request
|
|
from fastapi import Body, Depends, FastAPI, Form, HTTPException, Query, Request
|
|
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
|
|
@@ -46,6 +48,16 @@ CH_USER = os.environ.get("CH_USER", "zigbee")
|
|
|
CH_PASS = os.environ.get("CH_PASS", "")
|
|
CH_PASS = os.environ.get("CH_PASS", "")
|
|
|
CH_DB = os.environ.get("CH_DB", "uus67")
|
|
CH_DB = os.environ.get("CH_DB", "uus67")
|
|
|
|
|
|
|
|
|
|
+# Värvisensori juhtimine (MQTT, edge broker). OFF peatab ESP32 publitseerimise
|
|
|
|
|
+# -> ClickHouse'i ei kirjutata.
|
|
|
|
|
+VARV_MQTT_HOST = os.environ.get("VARV_MQTT_HOST", "")
|
|
|
|
|
+VARV_MQTT_PORT = int(os.environ.get("VARV_MQTT_PORT", "8883"))
|
|
|
|
|
+VARV_MQTT_TLS = os.environ.get("VARV_MQTT_TLS", "true").strip().lower() in ("1", "true", "yes", "on")
|
|
|
|
|
+VARV_MQTT_USER = os.environ.get("VARV_MQTT_USER", "")
|
|
|
|
|
+VARV_MQTT_PASS = os.environ.get("VARV_MQTT_PASS", "")
|
|
|
|
|
+VARV_CMD_TOPIC = os.environ.get("VARV_CMD_TOPIC", "varvus/switch/sensor/command")
|
|
|
|
|
+VARV_STATE_TOPIC = os.environ.get("VARV_STATE_TOPIC", "varvus/switch/sensor/state")
|
|
|
|
|
+
|
|
|
WEB_ACCESS_TOKEN = os.environ.get("WEB_ACCESS_TOKEN", "")
|
|
WEB_ACCESS_TOKEN = os.environ.get("WEB_ACCESS_TOKEN", "")
|
|
|
WEB_PASSWORD = os.environ.get("WEB_PASSWORD", "")
|
|
WEB_PASSWORD = os.environ.get("WEB_PASSWORD", "")
|
|
|
WEB_SECRET_KEY = os.environ.get("WEB_SECRET_KEY", "") or secrets.token_hex(32)
|
|
WEB_SECRET_KEY = os.environ.get("WEB_SECRET_KEY", "") or secrets.token_hex(32)
|
|
@@ -594,6 +606,90 @@ async def varv_history(hours: int = Query(24, ge=1, le=168)) -> dict:
|
|
|
return {"hours": hours, "rows": out}
|
|
return {"hours": hours, "rows": out}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+# --- Värvisensori mõõtmise sisse/välja lüliti (MQTT, ainult sisselogitult) ---
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _varv_mqtt_client() -> mqtt.Client:
|
|
|
|
|
+ client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
|
|
|
|
|
+ if VARV_MQTT_TLS:
|
|
|
|
|
+ client.tls_set()
|
|
|
|
|
+ if VARV_MQTT_USER:
|
|
|
|
|
+ client.username_pw_set(VARV_MQTT_USER, VARV_MQTT_PASS)
|
|
|
|
|
+ return client
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def varv_read_state(timeout: float = 4.0) -> str | None:
|
|
|
|
|
+ """Loe lüliti retained olek edge-brokerist (lühiajaline ühendus)."""
|
|
|
|
|
+ if not VARV_MQTT_HOST:
|
|
|
|
|
+ return None
|
|
|
|
|
+ box: dict[str, str] = {}
|
|
|
|
|
+
|
|
|
|
|
+ def on_connect(c, userdata, flags, rc, props=None):
|
|
|
|
|
+ if rc == 0:
|
|
|
|
|
+ c.subscribe(VARV_STATE_TOPIC)
|
|
|
|
|
+
|
|
|
|
|
+ def on_message(c, userdata, msg):
|
|
|
|
|
+ box["state"] = msg.payload.decode().strip()
|
|
|
|
|
+
|
|
|
|
|
+ client = _varv_mqtt_client()
|
|
|
|
|
+ client.on_connect = on_connect
|
|
|
|
|
+ client.on_message = on_message
|
|
|
|
|
+ try:
|
|
|
|
|
+ client.connect_async(VARV_MQTT_HOST, VARV_MQTT_PORT, 10)
|
|
|
|
|
+ client.loop_start()
|
|
|
|
|
+ deadline = time.monotonic() + timeout
|
|
|
|
|
+ while time.monotonic() < deadline and "state" not in box:
|
|
|
|
|
+ time.sleep(0.05)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ client.loop_stop()
|
|
|
|
|
+ try:
|
|
|
|
|
+ client.disconnect()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+ state = box.get("state")
|
|
|
|
|
+ return state if state in ("ON", "OFF") else None
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def varv_set_power(on: bool) -> None:
|
|
|
|
|
+ if not VARV_MQTT_HOST:
|
|
|
|
|
+ raise HTTPException(status_code=503, detail="Värvisensori MQTT pole seadistatud")
|
|
|
|
|
+ client = _varv_mqtt_client()
|
|
|
|
|
+ try:
|
|
|
|
|
+ client.connect(VARV_MQTT_HOST, VARV_MQTT_PORT, 10)
|
|
|
|
|
+ client.loop_start()
|
|
|
|
|
+ info = client.publish(VARV_CMD_TOPIC, "ON" if on else "OFF", qos=1, retain=True)
|
|
|
|
|
+ info.wait_for_publish(timeout=5)
|
|
|
|
|
+ finally:
|
|
|
|
|
+ client.loop_stop()
|
|
|
|
|
+ try:
|
|
|
|
|
+ client.disconnect()
|
|
|
|
|
+ except Exception:
|
|
|
|
|
+ pass
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.get("/api/varv/power", dependencies=[Depends(require_auth)])
|
|
|
|
|
+def varv_power_get() -> dict:
|
|
|
|
|
+ """Värvisensori mõõtmise olek (ainult sisselogitult)."""
|
|
|
|
|
+ state = varv_read_state()
|
|
|
|
|
+ return {
|
|
|
|
|
+ "available": bool(VARV_MQTT_HOST),
|
|
|
|
|
+ "state": state,
|
|
|
|
|
+ "on": None if state is None else (state == "ON"),
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+@app.post("/api/varv/power", dependencies=[Depends(require_auth)])
|
|
|
|
|
+async def varv_power_set(payload: dict = Body(...)) -> dict:
|
|
|
|
|
+ """Lülita värvisensori mõõtmine sisse/välja (ainult sisselogitult).
|
|
|
|
|
+
|
|
|
|
|
+ OFF peatab ESP32 publitseerimise -> ClickHouse'i ei kirjutata.
|
|
|
|
|
+ """
|
|
|
|
|
+ on = bool(payload.get("on"))
|
|
|
|
|
+ await asyncio.to_thread(varv_set_power, on)
|
|
|
|
|
+ state = await asyncio.to_thread(varv_read_state, 3.0)
|
|
|
|
|
+ return {"on": on, "state": state}
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
@app.get("/api/latest")
|
|
@app.get("/api/latest")
|
|
|
async def latest(request: Request) -> dict:
|
|
async def latest(request: Request) -> dict:
|
|
|
readings = ch_query(
|
|
readings = ch_query(
|