Files
ShellyEffectsDriver/app/main.py
T

162 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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):
<prefix>/set – write: animation name or "stop" / "off"
<prefix>/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()