Dateien nach "app/animations" hochladen

This commit is contained in:
Till-Immer committed 2026-10-08 14:45:21 +00:00
1 parent 8f8d206166
commit 1c5b8c5685
5 files changed
+256

No files matched your search

+20
View File
@@ -0,0 +1,20 @@
"""Animation registry – add new animations here."""
from .chill import ChillAnimation
from .gaming import GamingAnimation
from .party import PartyAnimation
# Map of mode-name (lowercase) → Animation class
# The MQTT payload / HTTP body should match these keys.
REGISTRY = {
"chill": ChillAnimation,
"chillmode": ChillAnimation,
"party": PartyAnimation,
"partymode": PartyAnimation,
"gaming": GamingAnimation,
"gamingmode": GamingAnimation,
}
def get_animation_class(name: str):
return REGISTRY.get(name.strip().lower())
+76
View File
@@ -0,0 +1,76 @@
"""Base class for all animations."""
from __future__ import annotations
import abc
import logging
import threading
import time
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from shelly import ShellyRGBW2
log = logging.getLogger("animation")
class Animation(abc.ABC):
"""Subclass this and implement `tick()` to create a new animation.
`tick()` is called roughly every `interval` seconds while the animation
is active. Keep it fast and non-blocking.
"""
name: str = "base"
interval: float = 0.12 # seconds between ticks
def __init__(self, shelly: "ShellyRGBW2"):
self.shelly = shelly
self._stop = threading.Event()
self._thread: threading.Thread | None = None
def start(self) -> None:
if self._thread and self._thread.is_alive():
return
self._stop.clear()
self.on_start()
self._thread = threading.Thread(
target=self._run, name=f"anim-{self.name}", daemon=True
)
self._thread.start()
log.info("Animation '%s' started", self.name)
def stop(self) -> None:
self._stop.set()
if self._thread:
self._thread.join(timeout=2.0)
self._thread = None
self.on_stop()
log.info("Animation '%s' stopped", self.name)
@property
def running(self) -> bool:
return self._thread is not None and self._thread.is_alive()
def _run(self) -> None:
while not self._stop.is_set():
try:
self.tick()
except Exception:
log.exception("Error in animation '%s'", self.name)
self._stop.wait(self.interval)
# ---- hooks for subclasses ----
def on_start(self) -> None:
"""Called once when the animation starts (before first tick)."""
self.shelly.turn_on()
def on_stop(self) -> None:
"""Called once when the animation stops."""
pass
@abc.abstractmethod
def tick(self) -> None:
"""Called every `interval` seconds. Update the light here."""
...
+48
View File
@@ -0,0 +1,48 @@
"""Warm evening / chill breathing animation."""
from __future__ import annotations
import colorsys
import math
from .base import Animation
class ChillAnimation(Animation):
name = "chill"
interval = 0.12
# Warm hues (0–1 range for colorsys)
COLORS = [
(15 / 360, 1.0), # warm red-orange
(25 / 360, 0.95), # orange
(35 / 360, 0.90), # amber
(45 / 360, 0.85), # golden
(30 / 360, 1.0), # deep orange
(10 / 360, 1.0), # deep red
]
def on_start(self) -> None:
super().on_start()
self._phase = 0.0
self._color_idx = 0
def tick(self) -> None:
self._phase += 0.08
brightness = 40 + math.sin(self._phase) * 18 # ~22–58
brightness = max(20, min(70, brightness))
if self._phase >= math.pi * 2:
self._phase = 0.0
self._color_idx = (self._color_idx + 1) % len(self.COLORS)
h, s = self.COLORS[self._color_idx]
r, g, b = colorsys.hsv_to_rgb(h, s, 1.0)
gain = int(brightness)
self.shelly.set_color(
int(r * 255),
int(g * 255),
int(b * 255),
gain=gain,
)
+46
View File
@@ -0,0 +1,46 @@
"""Gaming animation – strong pulse through gaming colors."""
from __future__ import annotations
import colorsys
from .base import Animation
class GamingAnimation(Animation):
name = "gaming"
interval = 0.09
# HSB hues
COLORS = [
220 / 360, # blue
190 / 360, # cyan
275 / 360, # violet
300 / 360, # magenta
]
def on_start(self) -> None:
super().on_start()
self._color_idx = 0
self._brightness = 40.0
self._direction = 1.0
def tick(self) -> None:
self._brightness += self._direction * 4
if self._brightness >= 95:
self._brightness = 95
self._direction = -1
if self._brightness <= 25:
self._brightness = 25
self._direction = 1
self._color_idx = (self._color_idx + 1) % len(self.COLORS)
h = self.COLORS[self._color_idx]
r, g, b = colorsys.hsv_to_rgb(h, 1.0, 1.0)
self.shelly.set_color(
int(r * 255),
int(g * 255),
int(b * 255),
gain=int(self._brightness),
)
+66
View File
@@ -0,0 +1,66 @@
"""Party animation – slow color drift + irregular beat pulses."""
from __future__ import annotations
import colorsys
import random
import time
from .base import Animation
class PartyAnimation(Animation):
name = "party"
interval = 0.10
def on_start(self) -> None:
super().on_start()
self._hue = 210.0 # start blue-ish
self._brightness = 35.0
self._direction = 1.0
self._pulse = 0.0
self._next_pulse = time.monotonic() + 1.2
self._next_color_jump = time.monotonic() + 2.5
def tick(self) -> None:
now = time.monotonic()
# slow hue drift (blue → cyan → violet → magenta → blue)
self._hue += 1.2
if self._hue >= 330:
self._hue = 210.0
# gentle base breathing
self._brightness += self._direction * 0.7
if self._brightness >= 55:
self._brightness = 55
self._direction = -1
if self._brightness <= 28:
self._brightness = 28
self._direction = 1
# irregular beat pulses
if now >= self._next_pulse:
self._pulse = 20
self._next_pulse = now + 0.6 + random.random() * 1.2
if self._pulse > 0:
self._brightness += self._pulse
self._pulse *= 0.65
brightness = max(20, min(80, self._brightness))
# occasional stronger color jump
if now >= self._next_color_jump:
self._hue += 25 + random.random() * 60
if self._hue >= 330:
self._hue = 210.0
self._next_color_jump = now + 1.8 + random.random() * 3.0
r, g, b = colorsys.hsv_to_rgb(self._hue / 360.0, 1.0, 1.0)
self.shelly.set_color(
int(r * 255),
int(g * 255),
int(b * 255),
gain=int(brightness),
)