#!/usr/bin/env python3 """ Shelly RGBW2 Animator Service ----------------------------- Listens on MQTT for animation commands and drives the Shelly directly via its HTTP API. Designed to run as a lightweight Docker container or systemd service on a Raspberry Pi. MQTT topics (configurable): /set – write: animation name or "stop" / "off" /state – read: current animation name or "off" Example: mosquitto_pub -t shelly-animator/set -m "party" mosquitto_pub -t shelly-animator/set -m "stop" """ from __future__ import annotations import logging import os import signal import sys import threading import time import paho.mqtt.client as mqtt from animations import REGISTRY, get_animation_class from shelly import ShellyRGBW2 # --------------------------------------------------------------------------- # Config from environment # --------------------------------------------------------------------------- SHELLY_HOST = os.getenv("SHELLY_HOST", "192.168.178.151") SHELLY_USER = os.getenv("SHELLY_USER", "shelly@till.immers.ch") SHELLY_PASS = os.getenv("SHELLY_PASS", "sh3llyTi33*") MQTT_HOST = os.getenv("MQTT_HOST", "localhost") MQTT_PORT = int(os.getenv("MQTT_PORT", "1884")) MQTT_USER = os.getenv("MQTT_USER", "") MQTT_PASS = os.getenv("MQTT_PASS", "") MQTT_PREFIX = os.getenv("MQTT_PREFIX", "shelly-animator") LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper() logging.basicConfig( level=getattr(logging, LOG_LEVEL, logging.INFO), format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", datefmt="%H:%M:%S", ) log = logging.getLogger("main") # --------------------------------------------------------------------------- # Runtime state # --------------------------------------------------------------------------- shelly = ShellyRGBW2(SHELLY_HOST, SHELLY_USER, SHELLY_PASS) current_anim = None # type: ignore lock = threading.Lock() def stop_current() -> None: global current_anim with lock: if current_anim and current_anim.running: current_anim.stop() current_anim = None def start_animation(name: str) -> bool: global current_anim cls = get_animation_class(name) if cls is None: log.warning("Unknown animation: %s (available: %s)", name, list(REGISTRY)) return False with lock: if current_anim and current_anim.running: if current_anim.name == cls.name: log.info("Animation '%s' already running", cls.name) return True current_anim.stop() anim = cls(shelly) anim.start() current_anim = anim return True def publish_state(client: mqtt.Client) -> None: state = "off" with lock: if current_anim and current_anim.running: state = current_anim.name client.publish(f"{MQTT_PREFIX}/state", state, retain=True) # --------------------------------------------------------------------------- # MQTT callbacks # --------------------------------------------------------------------------- def on_connect(client: mqtt.Client, userdata, flags, reason_code, properties=None): log.info("MQTT connected (rc=%s)", reason_code) client.subscribe(f"{MQTT_PREFIX}/set") publish_state(client) def on_message(client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): payload = msg.payload.decode("utf-8", errors="ignore").strip() log.info("MQTT command: %s", payload) if payload.lower() in ("stop", "off", "none", ""): stop_current() publish_state(client) return if start_animation(payload): publish_state(client) else: log.warning("Ignored unknown command: %s", payload) # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main() -> None: log.info("Shelly Animator starting") log.info(" Shelly : %s", SHELLY_HOST) log.info(" MQTT : %s:%s prefix=%s", MQTT_HOST, MQTT_PORT, MQTT_PREFIX) log.info(" Anims : %s", ", ".join(sorted(set(c.name for c in REGISTRY.values())))) client = mqtt.Client( mqtt.CallbackAPIVersion.VERSION2, client_id="shelly-animator", ) if MQTT_USER: client.username_pw_set(MQTT_USER, MQTT_PASS) client.on_connect = on_connect client.on_message = on_message # Graceful shutdown def shutdown(signum, frame): log.info("Shutting down...") stop_current() client.disconnect() sys.exit(0) signal.signal(signal.SIGTERM, shutdown) signal.signal(signal.SIGINT, shutdown) while True: try: client.connect(MQTT_HOST, MQTT_PORT, keepalive=60) client.loop_forever() except Exception as exc: log.error("MQTT connection error: %s – retry in 5s", exc) time.sleep(5) if __name__ == "__main__": main()