SmartestHome/hosts/thin-client/configs/eww/weather-json

126 lines
4.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Feeds the eww weather-overlay widget one JSON object per change on
`smarthome/weather/current`. Installed to /usr/local/bin/weather-json, consumed
by eww's `deflisten` (see configs/eww/eww.yuck).
Session-scoped and independent of thinclient_agent — same reasoning as
now-playing-json: this dies with sway, thinclient_agent is a system service that
must survive it, and wiring the two together would mean either a second inbound
control channel into the agent (which mqtt_discovery.py's security boundary
rules out) or restarting the agent whenever this widget restarts. MQTT
credentials are just re-read from the same config.env thinclient_agent itself
reads; this script never publishes anything and never touches that file.
DATA CONTRACT: `smarthome/weather/current`, retained, a small JSON object
`{"temperature": "...", "condition": "...", "location": "..."}` — published by a
Home Assistant automation this repo does not build, see
hosts/thin-client/README.md's "Idle-gallery weather overlay" section for the
worked example. Household-wide, not per-thin-client, same reasoning as
thinclient_agent/main.py's DIGEST_VIEWED_TOPIC: weather is one household-wide
fact, not something scoped to whichever room happens to be asking.
Degrades to {"available": false} — the clock/date still show, only the weather
line disappears, see eww.yuck — whenever MQTT is unreachable or nothing has ever
been published to the topic yet. Never blocks the rest of the widget on it.
"""
from __future__ import annotations
import json
import os
import sys
import paho.mqtt.client as mqtt
CONFIG_PATH = os.environ.get("THINCLIENT_AGENT_CONFIG", "/etc/thinclient-agent/config.env")
TOPIC = "smarthome/weather/current"
def load_config(path: str) -> dict[str, str]:
values: dict[str, str] = {}
try:
with open(path, encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, value = line.partition("=")
values[key.strip()] = value.strip().strip('"').strip("'")
except OSError:
pass
return values
def emit(state: dict) -> None:
sys.stdout.write(json.dumps(state) + "\n")
sys.stdout.flush()
def unavailable() -> dict:
return {"available": False, "temperature": "", "condition": "", "location": ""}
def parse(payload: bytes) -> dict:
try:
data = json.loads(payload.decode("utf-8", "replace"))
except ValueError:
return unavailable()
if not isinstance(data, dict):
return unavailable()
return {
"available": True,
"temperature": str(data.get("temperature") or ""),
"condition": str(data.get("condition") or ""),
"location": str(data.get("location") or ""),
}
def make_client() -> mqtt.Client:
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
if callback_api is not None:
return mqtt.Client(callback_api.VERSION1, client_id="weather-json")
return mqtt.Client(client_id="weather-json")
def main() -> int:
config = load_config(CONFIG_PATH)
broker_host = config.get("MQTT_BROKER_HOST", "")
if not broker_host:
# No broker configured at all — emit once and idle rather than spin
# retrying a connection that was never going to happen.
emit(unavailable())
return 0
broker_port = int(config.get("MQTT_BROKER_PORT") or 1883)
client = make_client()
if config.get("MQTT_USERNAME"):
client.username_pw_set(config["MQTT_USERNAME"], config.get("MQTT_PASSWORD") or None)
def on_connect(_client, _userdata, _flags, rc):
if rc == 0:
client.subscribe(TOPIC, qos=1)
else:
emit(unavailable())
def on_disconnect(_client, _userdata, rc):
emit(unavailable())
def on_message(_client, _userdata, message):
emit(parse(message.payload))
client.on_connect = on_connect
client.on_disconnect = on_disconnect
client.on_message = on_message
emit(unavailable())
# connect_async, never a blocking connect(): a thin client with the container
# host powered off must still show its clock, same "never stall the session
# on a remote service" rule Phase 11.10 applies everywhere else on this image.
client.connect_async(broker_host, broker_port, keepalive=60)
client.loop_forever()
return 0
if __name__ == "__main__":
raise SystemExit(main())