Dateien nach "app" hochladen
This commit is contained in:
1 parent
c049570ab5
commit
8f8d206166
2 files changed
+248
No files matched your search
+161
@@ -0,0 +1,161 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user