@@ -63,7 +63,7 @@ class Light(abc.ABC, TaskableMixin):
63
63
and managing USB connected lights without having to know aprori
64
64
details of the hardware present.
65
65
66
- - Light.available_lights () lists devices discovered
66
+ - Light.available_hardware () lists devices discovered
67
67
- Light.all_lights() returns all discovered lights ready for use
68
68
- Light.first_light() returns the first available light
69
69
@@ -84,7 +84,14 @@ class Light(abc.ABC, TaskableMixin):
84
84
@classmethod
85
85
@lru_cache (maxsize = 1 )
86
86
def vendor (cls ) -> str :
87
- """Return the vendor name in title case."""
87
+ """Get the vendor name for this device class.
88
+
89
+ Returns a human-readable vendor name derived from the module structure.
90
+ Device-specific subclasses should override this method to provide
91
+ accurate vendor branding that matches the physical device labeling.
92
+
93
+ :return: Title-cased vendor name for display in user interfaces
94
+ """
88
95
# EJO this is a low-effort way to get the vendor
89
96
# name from the module name. Subclasses can
90
97
# and should override this method to provide
@@ -94,18 +101,41 @@ def vendor(cls) -> str:
94
101
@classmethod
95
102
@lru_cache (maxsize = 1 )
96
103
def unique_device_names (cls ) -> list [str ]:
97
- """Return a list of unique device names."""
104
+ """Get all unique marketing names for devices supported by this class.
105
+
106
+ Returns the human-readable product names from the supported_device_ids
107
+ mapping, with duplicates removed. Use this to display available device
108
+ types to users or for device capability documentation.
109
+
110
+ :return: Sorted list of unique device marketing names
111
+ """
98
112
return sorted (set (cls .supported_device_ids .values ()))
99
113
100
114
@classmethod
101
115
@lru_cache (maxsize = 1 )
102
116
def unique_device_ids (cls ) -> list [tuple [int , int ]]:
103
- """Return a list of unique device IDs."""
117
+ """Get all unique vendor/product ID pairs supported by this class.
118
+
119
+ Returns the USB vendor and product ID combinations that this class
120
+ can control. Use this for hardware enumeration, udev rule generation,
121
+ or debugging device detection issues.
122
+
123
+ :return: Sorted list of (vendor_id, product_id) tuples
124
+ """
104
125
return sorted (set (cls .supported_device_ids .keys ()))
105
126
106
127
@classmethod
107
128
def claims (cls , hardware : Hardware ) -> bool :
108
- """Return True if the hardware is claimed by this class."""
129
+ """Check if this class can control the given hardware device.
130
+
131
+ Determines whether this Light subclass supports the specific hardware
132
+ by checking if the device's vendor/product ID pair matches any entry
133
+ in the supported_device_ids mapping. Use this during device discovery
134
+ to find the appropriate Light subclass for each detected device.
135
+
136
+ :param hardware: Hardware instance to test for compatibility
137
+ :return: True if this class can control the hardware device
138
+ """
109
139
return hardware .device_id in cls .supported_device_ids
110
140
111
141
@classmethod
@@ -138,16 +168,19 @@ def supported_lights(cls) -> dict[str, list[str]]:
138
168
return supported_lights
139
169
140
170
@classmethod
141
- def available_lights (cls ) -> dict [type [Light ], list [Hardware ]]:
142
- """Return a dictionary of available hardware by type.
171
+ def available_hardware (cls ) -> dict [type [Light ], list [Hardware ]]:
172
+ """Discover all compatible hardware devices available for control.
173
+
174
+ Scans the system for USB devices that match known vendor/product ID
175
+ combinations and groups them by the Light subclass that can control
176
+ them. Use this for device discovery, inventory management, or when
177
+ you need to present users with available device options.
143
178
144
- Keys are Light subclasses, values are a list of Hardware instances.
179
+ The returned Hardware instances represent devices that were found
180
+ and claimed by Light subclasses, but may still be in use by other
181
+ processes. Actual device acquisition occurs during Light initialization.
145
182
146
- The Hardware instances are a record of light devices that were
147
- discovered during the enumeration process and were claimed by
148
- a Light subclass. The hardware device may be in use by another
149
- process, which will be reported when attempting to acquire the
150
- device during Light subclass initialization.
183
+ :return: Mapping from Light subclass to list of compatible Hardware instances
151
184
"""
152
185
available_lights : dict [type [Light ], list [Hardware ]] = {}
153
186
@@ -168,22 +201,22 @@ def available_lights(cls) -> dict[type[Light], list[Hardware]]:
168
201
169
202
@classmethod
170
203
def all_lights (cls , * , reset : bool = True , exclusive : bool = True ) -> list [Light ]:
171
- """Return a list of all lights ready for use .
204
+ """Create initialized Light instances for all available compatible devices .
172
205
173
- All the lights in the list have been initialized with the
174
- given reset and exclusive parameters. Lights acquired with
175
- exclusive=True can only be used by the current process and
176
- will block other processes from using the same hardware until
177
- the light is released.
206
+ Discovers all compatible hardware and returns Light instances ready for
207
+ immediate use. Each light is initialized with the specified configuration
208
+ and can be used to control its device without further setup.
178
209
179
- If no lights are found, an empty list is returned.
210
+ Use this when you want to control all connected lights simultaneously,
211
+ such as for synchronized effects or system-wide status indication.
180
212
181
- :param: reset - bool - reset the hardware to a known state
182
- :param: exclusive - bool - acquire exclusive access to the hardware
213
+ :param reset: Reset devices to known state during initialization
214
+ :param exclusive: Acquire exclusive access to prevent interference
215
+ :return: List of initialized Light instances, empty if none found
183
216
"""
184
217
lights : list [Light ] = []
185
218
186
- for subclass , devices in cls .available_lights ().items ():
219
+ for subclass , devices in cls .available_hardware ().items ():
187
220
for device in devices :
188
221
try :
189
222
lights .append (subclass (device , reset = reset , exclusive = exclusive ))
@@ -194,18 +227,19 @@ def all_lights(cls, *, reset: bool = True, exclusive: bool = True) -> list[Light
194
227
195
228
@classmethod
196
229
def first_light (cls , * , reset : bool = True , exclusive : bool = True ) -> Light :
197
- """Return the first unused light ready for use.
198
-
199
- The light returned has been initialized with the given reset
200
- and exclusive parameters. If exclusive=True, the light can
201
- only be used by the current process and will block other
202
- processes from using the light until it is released.
230
+ """Create the first available Light instance ready for immediate use.
203
231
204
- Raises:
205
- - NoLightsFoundError
232
+ Discovers compatible devices and returns the first successfully
233
+ initialized Light instance. Use this when you need a single light
234
+ for simple status indication and don't care about the specific
235
+ device type or vendor.
206
236
237
+ :param reset: Reset device to known state during initialization
238
+ :param exclusive: Acquire exclusive access to prevent interference
239
+ :return: Initialized Light instance ready for control
240
+ :raises NoLightsFoundError: If no compatible devices found or init fails
207
241
"""
208
- for subclass , devices in cls .available_lights ().items ():
242
+ for subclass , devices in cls .available_hardware ().items ():
209
243
for device in devices :
210
244
try :
211
245
return subclass (device , reset = reset , exclusive = exclusive )
@@ -255,29 +289,19 @@ def __init__(
255
289
reset : bool = False ,
256
290
exclusive : bool = True ,
257
291
) -> None :
258
- """Initialize a Light with the given hardware information.
259
-
260
- The hardware argument is an instance of the Hardware class
261
- usually obtained from the Hardware.enumerate method. Due to
262
- vagaries in vendor USB implementations, the supplied hardware
263
- can contain incomplete information compared to other vendors
264
- and it's up to the Light subclass to fill in some of the
265
- blanks.
266
-
267
- The reset keyword-only parameter controls whether the hardware
268
- is reset to a known state using the Light.reset method.
292
+ """Initialize a Light instance with the specified hardware device.
269
293
270
- The exclusive keyword-only parameter controls whether the
271
- process creating this light has exclusive access to the
272
- hardware .
294
+ Creates a Light instance bound to the given hardware device and
295
+ configures it for use. The hardware should be obtained from
296
+ Hardware.enumerate() and verified with the class's claims() method .
273
297
274
- :param: hardware - Hardware
275
- :param: reset - bool
276
- :param: exclusive - bool
277
-
278
- Raises:
279
- - HardwareUnsupportedError
298
+ Use this constructor when you have specific hardware and want to
299
+ create a Light instance for direct device control.
280
300
301
+ :param hardware: Hardware instance representing the device to control
302
+ :param reset: Reset the device to a known state during initialization
303
+ :param exclusive: Acquire exclusive access to prevent interference
304
+ :raises HardwareUnsupportedError: If Light class cannot control hardware
281
305
"""
282
306
if not self .__class__ .claims (hardware ):
283
307
raise HardwareUnsupportedError (hardware , self .__class__ )
@@ -400,11 +424,17 @@ def exclusive_access(self) -> Generator[None, None, None]:
400
424
self .hardware .release ()
401
425
402
426
def update (self ) -> None :
403
- """Obtain the current state of the light and write it to the device.
427
+ """Send the current light state to the physical device.
428
+
429
+ Serializes the light's current state and transmits it to the hardware
430
+ device using the appropriate platform-specific protocol. Call this
431
+ method after making changes to light properties to apply them to
432
+ the physical device.
404
433
405
- Raises:
406
- - LightUnavailableError
434
+ The method handles platform-specific protocol differences automatically,
435
+ such as adding leading zero bytes on Windows 10.
407
436
437
+ :raises LightUnavailableError: If device communication fails
408
438
"""
409
439
state = bytes (self )
410
440
@@ -426,12 +456,15 @@ def update(self) -> None:
426
456
427
457
@contextlib .contextmanager
428
458
def batch_update (self ) -> Generator [None , None , None ]:
429
- """Update the software state of the light on exit.
459
+ """Defer device updates until multiple properties are changed.
460
+
461
+ Context manager that accumulates multiple property changes and sends
462
+ them to the device in a single update operation when exiting the
463
+ context. Use this when changing multiple light properties (color,
464
+ brightness, effects) to reduce USB communication overhead and improve
465
+ performance.
430
466
431
- This context manager is convenience for updating multiple
432
- light attribute values at once and write the new state to the
433
- hardware on exit. This approach reduces the number of writes
434
- to the hardware, which is beneficial for performance.
467
+ :return: Context manager for batching multiple property updates
435
468
"""
436
469
yield
437
470
self .update ()
@@ -442,30 +475,32 @@ def on(
442
475
color : tuple [int , int , int ],
443
476
led : int = 0 ,
444
477
) -> None :
445
- """Activate the light with the given red, green, blue color tuple .
478
+ """Activate the light with the specified RGB color.
446
479
447
- If a subclass supports multiple LEDs, the `led` parameter
448
- specifies which LED to activate. If `led` is 0, all LEDs
449
- are activated with the specified color. If a subclasss
450
- does not support multiple LEDs, the `led` parameter
451
- is ignored and defaults to 0.
480
+ Sets the light to display the given color immediately. This is the
481
+ primary method for controlling light appearance and should be
482
+ implemented by all device-specific subclasses.
452
483
453
- Color tuple values should be in the range of 0-255.
484
+ For devices with multiple LEDs, use led parameter to target specific
485
+ LEDs or set to 0 to affect all LEDs simultaneously. Single-LED devices
486
+ should ignore the led parameter.
454
487
455
- :param: color: tuple[int, int, int] - RGB color tuple
456
- :param: led: int - LED index, 0 for all LEDs
488
+ :param color: RGB intensity values from 0-255 for each color component
489
+ :param led: Target LED index, 0 affects all LEDs on the device
457
490
"""
458
491
raise NotImplementedError
459
492
460
493
def off (self , led : int = 0 ) -> None :
461
- """Deactivate the light.
494
+ """Turn off the light by setting it to black .
462
495
463
- If a subclass supports multiple LEDs, the `led` parameter
464
- specifies which LED to deactivate. If `led` is 0, all LEDs
465
- are deactivated. If a subclass does not support multiple LEDs,
466
- the `led` parameter is ignored and defaults to 0.
496
+ Deactivates the specified LED(s) by setting their color to black (0, 0, 0).
497
+ Use this to turn off status indication while keeping the device available
498
+ for future color changes.
467
499
468
- :param: led: int - LED index, 0 for all LEDs
500
+ For multi-LED devices, specify the LED index or use 0 to turn off all LEDs.
501
+ Single-LED devices ignore the led parameter.
502
+
503
+ :param led: Target LED index, 0 affects all LEDs on the device
469
504
"""
470
505
self .on ((0 , 0 , 0 ), led )
471
506
@@ -490,7 +525,11 @@ def color(self) -> tuple[int, int, int]:
490
525
def color (self , value : tuple [int , int , int ]) -> None :
491
526
"""Set the RGB color of the light.
492
527
493
- :param: value: tuple[int, int, int] - RGB color tuple
528
+ Updates the light's color state to the specified RGB values.
529
+ Device-specific implementations should store this value and
530
+ apply it during the next update() call.
531
+
532
+ :param value: RGB intensity values from 0-255 for each color component
494
533
"""
495
534
raise NotImplementedError
496
535
0 commit comments