1
- """ """
1
+ """Base class for all effects. """
2
2
3
3
import abc
4
4
import asyncio
5
- from functools import lru_cache
5
+ from functools import cache
6
6
from itertools import cycle , islice
7
- from typing import Dict , List , Tuple
7
+ from typing import TYPE_CHECKING
8
8
9
- from busylight_core import Light
9
+ from busylight_core . mixins . taskable import TaskPriority
10
10
from loguru import logger
11
11
12
+ if TYPE_CHECKING :
13
+ from busylight_core import Light
14
+
12
15
13
16
class BaseEffect (abc .ABC ):
14
17
@classmethod
15
- @lru_cache
16
- def subclasses (cls ) -> Dict [str , "BaseEffect" ]:
18
+ @cache
19
+ def subclasses (cls ) -> dict [str , "BaseEffect" ]:
17
20
"""Returns a dictionary of Effect subclasses, keyed by name."""
18
21
subclasses = {}
19
22
if cls is BaseEffect :
@@ -33,7 +36,7 @@ def subclasses(cls) -> Dict[str, "BaseEffect"]:
33
36
34
37
@classmethod
35
38
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.
37
40
38
41
:param name: str
39
42
:return: BaseEffect or subclass
@@ -47,26 +50,22 @@ def for_name(cls, name: str) -> "BaseEffect":
47
50
except KeyError :
48
51
raise ValueError (f"Unknown effect { name } " ) from None
49
52
53
+ @classmethod
54
+ def effects (cls ) -> list [str ]:
55
+ """Return a list of effect names."""
56
+ return list (cls .subclasses ().keys ())
57
+
50
58
def __repr__ (self ) -> str :
51
59
return f"{ self .name } (...)"
52
60
53
61
def __str__ (self ) -> str :
54
- return f"{ self .name } duty_cycle ={ self .duty_cycle } "
62
+ return f"{ self .name } count ={ self .count } "
55
63
56
64
@property
57
65
def name (self ) -> str :
58
66
"""The name of this effect."""
59
67
return self .__class__ .__name__
60
68
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
-
70
69
@property
71
70
def count (self ) -> int :
72
71
"""Number of cycles to run the effect.
@@ -79,23 +78,50 @@ def count(self) -> int:
79
78
def count (self , count : int ) -> None :
80
79
self ._count = int (count )
81
80
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
+
82
90
@property
83
91
@abc .abstractmethod
84
- def colors (self ) -> List [ Tuple [int , int , int ]]:
92
+ def colors (self ) -> list [ tuple [int , int , int ]]:
85
93
"""A list of color tuples."""
86
94
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.
89
102
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
91
108
"""
109
+ sleep_interval = interval if interval is not None else self .default_interval
110
+
92
111
if self .count > 0 :
93
112
cycle_count = self .count * len (self .colors )
113
+ color_iterator = islice (cycle (self .colors ), cycle_count )
94
114
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 )
100
116
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" )
0 commit comments