"""kitchen-display-agent entrypoint.""" from __future__ import annotations import logging import os import signal import socket import sys import threading import paho.mqtt.client as mqtt from .mqtt_discovery import Discovery from .session import launch_pantry_kiosk CONFIG_PATH = os.environ.get("KITCHEN_DISPLAY_AGENT_CONFIG", "/etc/kitchen-display-agent/config.env") CONFIG_KEYS = ( "MQTT_BROKER_HOST", "MQTT_BROKER_PORT", "MQTT_USERNAME", "MQTT_PASSWORD", "PANTRY_WEB_URL", "PANTRY_VISION_URL", "PANTRY_VISION_TOKEN", "KIOSK_USERNAME", "KITCHEN_DISPLAY_NAME", ) log = logging.getLogger("kitchen-display-agent") def load_config(path: str = CONFIG_PATH) -> 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 as exc: log.warning("could not read %s (%s); falling back to the environment", path, exc) for key in CONFIG_KEYS: if key in os.environ: values[key] = os.environ[key] return values def make_client(client_id: str) -> mqtt.Client: callback_api = getattr(mqtt, "CallbackAPIVersion", None) if callback_api is not None: return mqtt.Client(callback_api.VERSION1, client_id=client_id) return mqtt.Client(client_id=client_id) def main() -> int: logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", stream=sys.stdout, ) config = load_config() hostname = socket.gethostname() node_id = "".join(c if c.isalnum() else "_" for c in hostname).strip("_") or "kitchendisplay" friendly_name = config.get("KITCHEN_DISPLAY_NAME") or f"Kitchen display ({hostname})" broker_host = config.get("MQTT_BROKER_HOST", "") broker_port = int(config.get("MQTT_BROKER_PORT") or 1883) client = make_client(f"kitchen-display-agent-{node_id}") if config.get("MQTT_USERNAME"): client.username_pw_set(config["MQTT_USERNAME"], config.get("MQTT_PASSWORD") or None) discovery = Discovery(client, node_id, friendly_name) def on_show(fragment: str) -> None: launch_pantry_kiosk(fragment) def on_connect(_client, _userdata, _flags, rc): if rc != 0: log.error("MQTT connection refused (rc=%s)", rc) return log.info("connected to MQTT broker %s:%s", broker_host, broker_port) discovery.register_screens(on_show) discovery.subscribe_all() discovery.publish_available(True) def on_disconnect(_client, _userdata, rc): log.warning("disconnected from MQTT broker (rc=%s); paho will retry", rc) def on_message(_client, _userdata, message): discovery.dispatch(message.topic, message.payload.decode("utf-8", "replace")) client.on_connect = on_connect client.on_disconnect = on_disconnect client.on_message = on_message client.will_set(discovery.availability_topic, "offline", qos=1, retain=True) stop_event = threading.Event() def handle_signal(_signum, _frame): stop_event.set() signal.signal(signal.SIGTERM, handle_signal) signal.signal(signal.SIGINT, handle_signal) if not broker_host: log.error("MQTT_BROKER_HOST is not set in %s — running without HA control", CONFIG_PATH) else: # connect_async + loop_start, never a blocking connect(): same "reactive path # never depends on a remote service" rule as every other host's agent — the # kitchen display's local scan/inventory/recipes flow must work with the # container host powered off, only the HA "Show X" buttons need it. client.connect_async(broker_host, broker_port, keepalive=60) client.loop_start() log.info("kitchen-display-agent %s started (node_id=%s)", node_id, node_id) try: stop_event.wait() finally: log.info("shutting down") if broker_host: discovery.publish_available(False) client.loop_stop() client.disconnect() return 0 if __name__ == "__main__": raise SystemExit(main())