Dateien nach "app/animations" hochladen
This commit is contained in:
1 parent
8f8d206166
commit
1c5b8c5685
5 files changed
+256
No files matched your search
@@ -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())
|
||||||
@@ -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."""
|
||||||
|
...
|
||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
@@ -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),
|
||||||
|
)
|
||||||
Reference in new issue
Block a user