Skip to content

Commit ee4222c

Browse files
authored
Merge pull request #475 from JnyJny/features/refactor-effects-taskmixin
Refactor effects system with TaskableMixin integration
2 parents 0e59cab + e759dfd commit ee4222c

File tree

15 files changed

+304
-192
lines changed

15 files changed

+304
-192
lines changed

src/busylight/__main__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from . import __version__
99
from .busyserve import busyserve_cli
1010
from .callbacks import string_to_scaled_color
11-
from .effects import Effects
1211
from .global_options import GlobalOptions
1312
from .manager import LightManager
1413
from .speed import Speed

src/busylight/effects/blink.py

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,43 @@
1-
"""change a light between two colors with a short interval."""
1+
"""Change a light between two colors with a short interval."""
22

3-
from typing import List, Optional, Tuple
3+
from typing import TYPE_CHECKING
4+
5+
from busylight_core.mixins.taskable import TaskPriority
46

57
from .effect import BaseEffect
68

9+
if TYPE_CHECKING:
10+
from busylight_core import Light
11+
712

813
class Blink(BaseEffect):
914
def __init__(
1015
self,
11-
on_color: Tuple[int, int, int],
12-
duty_cycle: float,
13-
off_color: Optional[Tuple[int, int, int]] = None,
16+
on_color: tuple[int, int, int],
17+
off_color: tuple[int, int, int] | None = None,
1418
count: int = 0,
1519
) -> None:
16-
"""This effect turns a light on and off with the specified color(s),
17-
pausing for `duty_cycle` seconds in between each operation. If count is
18-
given and greater than zero, the light will blink count times.
19-
20-
:param on_color: Tuple[int,int,int]
21-
:param duty_cycle: float
22-
:param off_color: Tuple[int,int,int] defaults to black.
23-
:param count: int defaults to 0, indicating no limit.
20+
"""This effect alternates between on_color and off_color.
21+
22+
If count is given and greater than zero, the light will blink
23+
count times.
24+
25+
:param on_color: RGB tuple for the "on" state
26+
:param off_color: RGB tuple for the "off" state, defaults to black
27+
:param count: Number of blink cycles, 0 means infinite
2428
"""
2529
self.on_color = on_color
2630
self.off_color = off_color or (0, 0, 0)
27-
self.duty_cycle = duty_cycle
2831
self.count = count
32+
self.priority = TaskPriority.NORMAL
2933

3034
def __repr__(self) -> str:
31-
return f"{self.name}(on_color={self.on_color!r}, duty_cycle={self.duty_cycle!r}, off_color={self.off_color!r})"
35+
return f"{self.name}(on_color={self.on_color!r}, off_color={self.off_color!r})"
36+
37+
@property
38+
def colors(self) -> list[tuple[int, int, int]]:
39+
return [self.on_color, self.off_color]
3240

3341
@property
34-
def colors(self) -> List[Tuple[int, int, int]]:
35-
try:
36-
return self._colors
37-
except AttributeError:
38-
pass
39-
self._colors: List[Tuple[int, int, int]] = [self.on_color, self.off_color]
40-
return self._colors
42+
def default_interval(self) -> float:
43+
return 0.5

src/busylight/effects/effect.py

Lines changed: 53 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
1-
""" """
1+
"""Base class for all effects."""
22

33
import abc
44
import asyncio
5-
from functools import lru_cache
5+
from functools import cache
66
from itertools import cycle, islice
7-
from typing import Dict, List, Tuple
7+
from typing import TYPE_CHECKING
88

9-
from busylight_core import Light
9+
from busylight_core.mixins.taskable import TaskPriority
1010
from loguru import logger
1111

12+
if TYPE_CHECKING:
13+
from busylight_core import Light
14+
1215

1316
class BaseEffect(abc.ABC):
1417
@classmethod
15-
@lru_cache
16-
def subclasses(cls) -> Dict[str, "BaseEffect"]:
18+
@cache
19+
def subclasses(cls) -> dict[str, "BaseEffect"]:
1720
"""Returns a dictionary of Effect subclasses, keyed by name."""
1821
subclasses = {}
1922
if cls is BaseEffect:
@@ -33,7 +36,7 @@ def subclasses(cls) -> Dict[str, "BaseEffect"]:
3336

3437
@classmethod
3538
def for_name(cls, name: str) -> "BaseEffect":
36-
"""Finds an effect subclass with the given name.
39+
"""Return an effect subclass with the given name.
3740
3841
:param name: str
3942
:return: BaseEffect or subclass
@@ -47,26 +50,22 @@ def for_name(cls, name: str) -> "BaseEffect":
4750
except KeyError:
4851
raise ValueError(f"Unknown effect {name}") from None
4952

53+
@classmethod
54+
def effects(cls) -> list[str]:
55+
"""Return a list of effect names."""
56+
return list(cls.subclasses().keys())
57+
5058
def __repr__(self) -> str:
5159
return f"{self.name}(...)"
5260

5361
def __str__(self) -> str:
54-
return f"{self.name} duty_cycle={self.duty_cycle}"
62+
return f"{self.name} count={self.count}"
5563

5664
@property
5765
def name(self) -> str:
5866
"""The name of this effect."""
5967
return self.__class__.__name__
6068

61-
@property
62-
def duty_cycle(self) -> float:
63-
"""Interval in seconds for current frame of the effect to be displayed."""
64-
return getattr(self, "_duty_cycle", 0)
65-
66-
@duty_cycle.setter
67-
def duty_cycle(self, seconds: float) -> None:
68-
self._duty_cycle = seconds
69-
7069
@property
7170
def count(self) -> int:
7271
"""Number of cycles to run the effect.
@@ -79,23 +78,50 @@ def count(self) -> int:
7978
def count(self, count: int) -> None:
8079
self._count = int(count)
8180

81+
@property
82+
def priority(self) -> TaskPriority:
83+
"""Task priority for this effect."""
84+
return getattr(self, "_priority", TaskPriority.NORMAL)
85+
86+
@priority.setter
87+
def priority(self, priority: TaskPriority) -> None:
88+
self._priority = priority
89+
8290
@property
8391
@abc.abstractmethod
84-
def colors(self) -> List[Tuple[int, int, int]]:
92+
def colors(self) -> list[tuple[int, int, int]]:
8593
"""A list of color tuples."""
8694

87-
async def __call__(self, light: Light) -> None:
88-
"""Apply this effect to the given light.
95+
@property
96+
@abc.abstractmethod
97+
def default_interval(self) -> float:
98+
"""Default interval between color changes in seconds."""
99+
100+
async def execute(self, light: "Light", interval: float | None = None) -> None:
101+
"""Execute this effect on the given light.
89102
90-
:param light: Light
103+
This method runs the full effect cycle similar to the original
104+
generator-based approach but as a single long-running async function.
105+
106+
:param light: Light instance with TaskableMixin capabilities
107+
:param interval: Override default interval between color changes
91108
"""
109+
sleep_interval = interval if interval is not None else self.default_interval
110+
92111
if self.count > 0:
93112
cycle_count = self.count * len(self.colors)
113+
color_iterator = islice(cycle(self.colors), cycle_count)
94114
else:
95-
cycle_count = None
96-
97-
for color in islice(cycle(self.colors), cycle_count):
98-
light.on(color)
99-
await asyncio.sleep(self.duty_cycle)
115+
color_iterator = cycle(self.colors)
100116

101-
light.off()
117+
try:
118+
for color in color_iterator:
119+
light.on(color)
120+
await asyncio.sleep(sleep_interval)
121+
finally:
122+
light.off()
123+
124+
def reset(self) -> None:
125+
"""Reset the effect's internal state."""
126+
if hasattr(self, "_color_cycle"):
127+
delattr(self, "_color_cycle")

src/busylight/effects/gradient.py

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,61 @@
1-
"""a smooth color gradient for a given color."""
1+
"""A smooth color gradient for a given color."""
22

3-
from typing import List, Tuple
3+
from typing import TYPE_CHECKING
4+
5+
from busylight_core.mixins.taskable import TaskPriority
46

57
from .effect import BaseEffect
68

9+
if TYPE_CHECKING:
10+
from busylight_core import Light
11+
712

813
class Gradient(BaseEffect):
9-
"""This effect will produce a color range from black to the given
10-
color and then back to black again with the given number of steps
11-
between off and on. If count is given and is greater than zero the
12-
light will cycle thru the sequence count times.
14+
"""This effect produces a smooth color gradient from black to the given
15+
color and then back to black again with the given number of steps.
16+
If count is given and is greater than zero the light will cycle through
17+
the sequence count times.
1318
"""
1419

1520
def __init__(
1621
self,
17-
color: Tuple[int, int, int],
18-
duty_cycle: float,
22+
color: tuple[int, int, int],
1923
step: int = 1,
24+
step_max: int = 255,
2025
count: int = 0,
2126
) -> None:
22-
""":param color: Tuple[int,int,int]
23-
:param duty_cycle: float
24-
:param step: int defaults to 1.
25-
:param count: int defaults to 0, indicating no limit.
27+
"""Initialize gradient effect.
28+
29+
:param color: Target RGB color for the gradient
30+
:param step: Step size for gradient calculation
31+
:param step_max: Maximum step value, determines gradient smoothness
32+
:param count: Number of gradient cycles, 0 means infinite
2633
"""
2734
self.color = color
28-
self.duty_cycle = duty_cycle
29-
# XXX need to choose steps that make sense for scaled colors
30-
# where the max(color) << 255
31-
self.step = max(0, min(step, 255))
35+
self.step = max(1, min(step, step_max))
36+
self.step_max = step_max
3237
self.count = count
38+
self.priority = TaskPriority.LOW
3339

3440
@property
35-
def colors(self) -> List[Tuple[int, int, int]]:
36-
try:
41+
def colors(self) -> list[tuple[int, int, int]]:
42+
if hasattr(self, "_colors"):
3743
return self._colors
38-
except AttributeError:
39-
pass
4044

4145
red, green, blue = self.color
42-
4346
colors = []
44-
for i in range(1, 256, self.step):
45-
scale = i / 255
47+
48+
for i in range(self.step, self.step_max + 1, self.step):
49+
scale = i / self.step_max
4650
r = round(scale * red)
4751
g = round(scale * green)
4852
b = round(scale * blue)
4953
colors.append((r, g, b))
5054

51-
self._colors: List[Tuple[int, int, int]] = colors + list(reversed(colors[:-1]))
52-
55+
ramp_down = list(reversed(colors[:-1]))
56+
self._colors: list[tuple[int, int, int]] = colors + ramp_down
5357
return self._colors
58+
59+
@property
60+
def default_interval(self) -> float:
61+
return 0.1

0 commit comments

Comments
 (0)