lifx-async 4.8.1__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lifx/api.py +34 -36
- lifx/color.py +223 -23
- lifx/devices/base.py +25 -22
- lifx/devices/hev.py +14 -18
- lifx/devices/light.py +15 -15
- lifx/devices/multizone.py +8 -12
- lifx/effects/base.py +11 -7
- lifx/effects/colorloop.py +4 -10
- lifx/effects/conductor.py +43 -46
- lifx/effects/pulse.py +4 -10
- lifx/network/connection.py +1 -1
- lifx/network/mdns/transport.py +4 -3
- lifx/network/transport.py +70 -63
- {lifx_async-4.8.1.dist-info → lifx_async-5.0.0.dist-info}/METADATA +3 -2
- {lifx_async-4.8.1.dist-info → lifx_async-5.0.0.dist-info}/RECORD +17 -17
- {lifx_async-4.8.1.dist-info → lifx_async-5.0.0.dist-info}/WHEEL +0 -0
- {lifx_async-4.8.1.dist-info → lifx_async-5.0.0.dist-info}/licenses/LICENSE +0 -0
lifx/api.py
CHANGED
|
@@ -198,9 +198,7 @@ class DeviceGroup:
|
|
|
198
198
|
await group.set_power(True, duration=1.0)
|
|
199
199
|
```
|
|
200
200
|
"""
|
|
201
|
-
|
|
202
|
-
for light in self.lights:
|
|
203
|
-
tg.create_task(light.set_power(on, duration))
|
|
201
|
+
await asyncio.gather(*(light.set_power(on, duration) for light in self.lights))
|
|
204
202
|
|
|
205
203
|
async def set_color(self, color: HSBK, duration: float = 0.0) -> None:
|
|
206
204
|
"""Set color for all Light devices in the group.
|
|
@@ -218,9 +216,9 @@ class DeviceGroup:
|
|
|
218
216
|
await group.set_color(HSBK.from_rgb(255, 0, 0), duration=2.0)
|
|
219
217
|
```
|
|
220
218
|
"""
|
|
221
|
-
|
|
222
|
-
for light in self.lights
|
|
223
|
-
|
|
219
|
+
await asyncio.gather(
|
|
220
|
+
*(light.set_color(color, duration) for light in self.lights)
|
|
221
|
+
)
|
|
224
222
|
|
|
225
223
|
async def set_brightness(self, brightness: float, duration: float = 0.0) -> None:
|
|
226
224
|
"""Set brightness for all Light devices in the group.
|
|
@@ -238,9 +236,9 @@ class DeviceGroup:
|
|
|
238
236
|
await group.set_brightness(0.5, duration=1.0)
|
|
239
237
|
```
|
|
240
238
|
"""
|
|
241
|
-
|
|
242
|
-
for light in self.lights
|
|
243
|
-
|
|
239
|
+
await asyncio.gather(
|
|
240
|
+
*(light.set_brightness(brightness, duration) for light in self.lights)
|
|
241
|
+
)
|
|
244
242
|
|
|
245
243
|
async def pulse(
|
|
246
244
|
self, color: HSBK, period: float = 1.0, cycles: float = 1.0
|
|
@@ -261,9 +259,9 @@ class DeviceGroup:
|
|
|
261
259
|
await group.pulse(Colors.RED, period=1.0, cycles=1.0)
|
|
262
260
|
```
|
|
263
261
|
"""
|
|
264
|
-
|
|
265
|
-
for light in self.lights
|
|
266
|
-
|
|
262
|
+
await asyncio.gather(
|
|
263
|
+
*(light.pulse(color, period, cycles) for light in self.lights)
|
|
264
|
+
)
|
|
267
265
|
|
|
268
266
|
# Location and Group Organization Methods
|
|
269
267
|
|
|
@@ -280,14 +278,13 @@ class DeviceGroup:
|
|
|
280
278
|
)
|
|
281
279
|
|
|
282
280
|
# Fetch all location info concurrently
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
tasks[device.serial] = tg.create_task(device.get_location())
|
|
281
|
+
location_results = await asyncio.gather(
|
|
282
|
+
*(device.get_location() for device in self._devices)
|
|
283
|
+
)
|
|
287
284
|
|
|
288
|
-
results: list[tuple[Device, CollectionInfo | None]] =
|
|
289
|
-
|
|
290
|
-
|
|
285
|
+
results: list[tuple[Device, CollectionInfo | None]] = list(
|
|
286
|
+
zip(self._devices, location_results)
|
|
287
|
+
)
|
|
291
288
|
|
|
292
289
|
# Group by location UUID
|
|
293
290
|
for device, location_info in results:
|
|
@@ -332,15 +329,14 @@ class DeviceGroup:
|
|
|
332
329
|
# Collect group info from all devices concurrently
|
|
333
330
|
group_data: dict[str, list[tuple[Device, CollectionInfo]]] = defaultdict(list)
|
|
334
331
|
|
|
335
|
-
tasks: dict[str, asyncio.Task[CollectionInfo | None]] = {}
|
|
336
|
-
async with asyncio.TaskGroup() as tg:
|
|
337
|
-
for device in self._devices:
|
|
338
|
-
tasks[device.serial] = tg.create_task(device.get_group())
|
|
339
|
-
|
|
340
332
|
# Fetch all group info concurrently
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
333
|
+
group_results = await asyncio.gather(
|
|
334
|
+
*(device.get_group() for device in self._devices)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
results: list[tuple[Device, CollectionInfo | None]] = list(
|
|
338
|
+
zip(self._devices, group_results)
|
|
339
|
+
)
|
|
344
340
|
|
|
345
341
|
# Group by group UUID
|
|
346
342
|
for device, group_info in results:
|
|
@@ -712,18 +708,20 @@ class DeviceGroup:
|
|
|
712
708
|
await group.apply_theme(evening, power_on=True, duration=1.0)
|
|
713
709
|
```
|
|
714
710
|
"""
|
|
715
|
-
|
|
711
|
+
await asyncio.gather(
|
|
716
712
|
# Apply theme to all lights
|
|
717
|
-
for light in self.lights
|
|
718
|
-
tg.create_task(light.apply_theme(theme, power_on, duration))
|
|
719
|
-
|
|
713
|
+
*(light.apply_theme(theme, power_on, duration) for light in self.lights),
|
|
720
714
|
# Apply theme to all multizone lights
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
715
|
+
*(
|
|
716
|
+
multizone.apply_theme(theme, power_on, duration)
|
|
717
|
+
for multizone in self.multizone_lights
|
|
718
|
+
),
|
|
724
719
|
# Apply theme to all matrix light devices
|
|
725
|
-
|
|
726
|
-
|
|
720
|
+
*(
|
|
721
|
+
matrix.apply_theme(theme, power_on, duration)
|
|
722
|
+
for matrix in self.matrix_lights
|
|
723
|
+
),
|
|
724
|
+
)
|
|
727
725
|
|
|
728
726
|
def invalidate_metadata_cache(self) -> None:
|
|
729
727
|
"""Clear all cached location and group metadata.
|
lifx/color.py
CHANGED
|
@@ -10,9 +10,23 @@ import colorsys
|
|
|
10
10
|
import math
|
|
11
11
|
|
|
12
12
|
from lifx.const import (
|
|
13
|
+
KELVIN_AMBER,
|
|
14
|
+
KELVIN_BLUE_DAYLIGHT,
|
|
15
|
+
KELVIN_BLUE_ICE,
|
|
16
|
+
KELVIN_BLUE_OVERCAST,
|
|
17
|
+
KELVIN_BRIGHT_DAYLIGHT,
|
|
18
|
+
KELVIN_CANDLELIGHT,
|
|
19
|
+
KELVIN_CLOUDY_DAYLIGHT,
|
|
13
20
|
KELVIN_COOL,
|
|
21
|
+
KELVIN_COOL_DAYLIGHT,
|
|
14
22
|
KELVIN_DAYLIGHT,
|
|
23
|
+
KELVIN_INCANDESCENT,
|
|
15
24
|
KELVIN_NEUTRAL,
|
|
25
|
+
KELVIN_NEUTRAL_WARM,
|
|
26
|
+
KELVIN_NOON_DAYLIGHT,
|
|
27
|
+
KELVIN_SOFT_DAYLIGHT,
|
|
28
|
+
KELVIN_SUNSET,
|
|
29
|
+
KELVIN_ULTRA_WARM,
|
|
16
30
|
KELVIN_WARM,
|
|
17
31
|
MAX_BRIGHTNESS,
|
|
18
32
|
MAX_HUE,
|
|
@@ -520,34 +534,220 @@ class HSBK:
|
|
|
520
534
|
|
|
521
535
|
# Common color presets
|
|
522
536
|
class Colors:
|
|
523
|
-
"""Common color presets for convenience.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
537
|
+
"""Common color presets for convenience.
|
|
538
|
+
|
|
539
|
+
Includes all 140 standard HTML/CSS named colors plus white temperature
|
|
540
|
+
variants and pastel variations.
|
|
541
|
+
|
|
542
|
+
HTML color reference: https://www.w3.org/TR/css-color-3/#svg-color
|
|
543
|
+
"""
|
|
544
|
+
|
|
545
|
+
# Off / Black (brightness=0 turns light off or sets zone to black)
|
|
546
|
+
OFF = HSBK(hue=0, saturation=0.0, brightness=0.0, kelvin=KELVIN_NEUTRAL)
|
|
547
|
+
|
|
548
|
+
# HTML Named Colors (alphabetical order)
|
|
549
|
+
ALICE_BLUE = HSBK.from_rgb(240, 248, 255)
|
|
550
|
+
ANTIQUE_WHITE = HSBK.from_rgb(250, 235, 215)
|
|
551
|
+
AQUA = HSBK.from_rgb(0, 255, 255)
|
|
552
|
+
AQUAMARINE = HSBK.from_rgb(127, 255, 212)
|
|
553
|
+
AZURE = HSBK.from_rgb(240, 255, 255)
|
|
554
|
+
BEIGE = HSBK.from_rgb(245, 245, 220)
|
|
555
|
+
BISQUE = HSBK.from_rgb(255, 228, 196)
|
|
556
|
+
BLACK = HSBK.from_rgb(0, 0, 0)
|
|
557
|
+
BLANCHED_ALMOND = HSBK.from_rgb(255, 235, 205)
|
|
558
|
+
BLUE = HSBK.from_rgb(0, 0, 255)
|
|
559
|
+
BLUE_VIOLET = HSBK.from_rgb(138, 43, 226)
|
|
560
|
+
BROWN = HSBK.from_rgb(165, 42, 42)
|
|
561
|
+
BURLYWOOD = HSBK.from_rgb(222, 184, 135)
|
|
562
|
+
CADET_BLUE = HSBK.from_rgb(95, 158, 160)
|
|
563
|
+
CHARTREUSE = HSBK.from_rgb(127, 255, 0)
|
|
564
|
+
CHOCOLATE = HSBK.from_rgb(210, 105, 30)
|
|
565
|
+
CORAL = HSBK.from_rgb(255, 127, 80)
|
|
566
|
+
CORNFLOWER_BLUE = HSBK.from_rgb(100, 149, 237)
|
|
567
|
+
CORNSILK = HSBK.from_rgb(255, 248, 220)
|
|
568
|
+
CRIMSON = HSBK.from_rgb(220, 20, 60)
|
|
569
|
+
CYAN = HSBK.from_rgb(0, 255, 255)
|
|
570
|
+
DARK_BLUE = HSBK.from_rgb(0, 0, 139)
|
|
571
|
+
DARK_CYAN = HSBK.from_rgb(0, 139, 139)
|
|
572
|
+
DARK_GOLDENROD = HSBK.from_rgb(184, 134, 11)
|
|
573
|
+
DARK_GRAY = HSBK.from_rgb(169, 169, 169)
|
|
574
|
+
DARK_GREEN = HSBK.from_rgb(0, 100, 0)
|
|
575
|
+
DARK_GREY = HSBK.from_rgb(169, 169, 169)
|
|
576
|
+
DARK_KHAKI = HSBK.from_rgb(189, 183, 107)
|
|
577
|
+
DARK_MAGENTA = HSBK.from_rgb(139, 0, 139)
|
|
578
|
+
DARK_OLIVE_GREEN = HSBK.from_rgb(85, 107, 47)
|
|
579
|
+
DARK_ORANGE = HSBK.from_rgb(255, 140, 0)
|
|
580
|
+
DARK_ORCHID = HSBK.from_rgb(153, 50, 204)
|
|
581
|
+
DARK_RED = HSBK.from_rgb(139, 0, 0)
|
|
582
|
+
DARK_SALMON = HSBK.from_rgb(233, 150, 122)
|
|
583
|
+
DARK_SEA_GREEN = HSBK.from_rgb(143, 188, 143)
|
|
584
|
+
DARK_SLATE_BLUE = HSBK.from_rgb(72, 61, 139)
|
|
585
|
+
DARK_SLATE_GRAY = HSBK.from_rgb(47, 79, 79)
|
|
586
|
+
DARK_SLATE_GREY = HSBK.from_rgb(47, 79, 79)
|
|
587
|
+
DARK_TURQUOISE = HSBK.from_rgb(0, 206, 209)
|
|
588
|
+
DARK_VIOLET = HSBK.from_rgb(148, 0, 211)
|
|
589
|
+
DEEP_PINK = HSBK.from_rgb(255, 20, 147)
|
|
590
|
+
DEEP_SKY_BLUE = HSBK.from_rgb(0, 191, 255)
|
|
591
|
+
DIM_GRAY = HSBK.from_rgb(105, 105, 105)
|
|
592
|
+
DIM_GREY = HSBK.from_rgb(105, 105, 105)
|
|
593
|
+
DODGER_BLUE = HSBK.from_rgb(30, 144, 255)
|
|
594
|
+
FIREBRICK = HSBK.from_rgb(178, 34, 34)
|
|
595
|
+
FLORAL_WHITE = HSBK.from_rgb(255, 250, 240)
|
|
596
|
+
FOREST_GREEN = HSBK.from_rgb(34, 139, 34)
|
|
597
|
+
FUCHSIA = HSBK.from_rgb(255, 0, 255)
|
|
598
|
+
GAINSBORO = HSBK.from_rgb(220, 220, 220)
|
|
599
|
+
GHOST_WHITE = HSBK.from_rgb(248, 248, 255)
|
|
600
|
+
GOLD = HSBK.from_rgb(255, 215, 0)
|
|
601
|
+
GOLDENROD = HSBK.from_rgb(218, 165, 32)
|
|
602
|
+
GRAY = HSBK.from_rgb(128, 128, 128)
|
|
603
|
+
GREEN = HSBK.from_rgb(0, 128, 0)
|
|
604
|
+
GREEN_YELLOW = HSBK.from_rgb(173, 255, 47)
|
|
605
|
+
GREY = HSBK.from_rgb(128, 128, 128)
|
|
606
|
+
HONEYDEW = HSBK.from_rgb(240, 255, 240)
|
|
607
|
+
HOT_PINK = HSBK.from_rgb(255, 105, 180)
|
|
608
|
+
INDIAN_RED = HSBK.from_rgb(205, 92, 92)
|
|
609
|
+
INDIGO = HSBK.from_rgb(75, 0, 130)
|
|
610
|
+
IVORY = HSBK.from_rgb(255, 255, 240)
|
|
611
|
+
KHAKI = HSBK.from_rgb(240, 230, 140)
|
|
612
|
+
LAVENDER = HSBK.from_rgb(230, 230, 250)
|
|
613
|
+
LAVENDER_BLUSH = HSBK.from_rgb(255, 240, 245)
|
|
614
|
+
LAWN_GREEN = HSBK.from_rgb(124, 252, 0)
|
|
615
|
+
LEMON_CHIFFON = HSBK.from_rgb(255, 250, 205)
|
|
616
|
+
LIGHT_BLUE = HSBK.from_rgb(173, 216, 230)
|
|
617
|
+
LIGHT_CORAL = HSBK.from_rgb(240, 128, 128)
|
|
618
|
+
LIGHT_CYAN = HSBK.from_rgb(224, 255, 255)
|
|
619
|
+
LIGHT_GOLDENROD_YELLOW = HSBK.from_rgb(250, 250, 210)
|
|
620
|
+
LIGHT_GRAY = HSBK.from_rgb(211, 211, 211)
|
|
621
|
+
LIGHT_GREEN = HSBK.from_rgb(144, 238, 144)
|
|
622
|
+
LIGHT_GREY = HSBK.from_rgb(211, 211, 211)
|
|
623
|
+
LIGHT_PINK = HSBK.from_rgb(255, 182, 193)
|
|
624
|
+
LIGHT_SALMON = HSBK.from_rgb(255, 160, 122)
|
|
625
|
+
LIGHT_SEA_GREEN = HSBK.from_rgb(32, 178, 170)
|
|
626
|
+
LIGHT_SKY_BLUE = HSBK.from_rgb(135, 206, 250)
|
|
627
|
+
LIGHT_SLATE_GRAY = HSBK.from_rgb(119, 136, 153)
|
|
628
|
+
LIGHT_SLATE_GREY = HSBK.from_rgb(119, 136, 153)
|
|
629
|
+
LIGHT_STEEL_BLUE = HSBK.from_rgb(176, 196, 222)
|
|
630
|
+
LIGHT_YELLOW = HSBK.from_rgb(255, 255, 224)
|
|
631
|
+
LIME = HSBK.from_rgb(0, 255, 0)
|
|
632
|
+
LIME_GREEN = HSBK.from_rgb(50, 205, 50)
|
|
633
|
+
LINEN = HSBK.from_rgb(250, 240, 230)
|
|
634
|
+
MAGENTA = HSBK.from_rgb(255, 0, 255)
|
|
635
|
+
MAROON = HSBK.from_rgb(128, 0, 0)
|
|
636
|
+
MEDIUM_AQUAMARINE = HSBK.from_rgb(102, 205, 170)
|
|
637
|
+
MEDIUM_BLUE = HSBK.from_rgb(0, 0, 205)
|
|
638
|
+
MEDIUM_ORCHID = HSBK.from_rgb(186, 85, 211)
|
|
639
|
+
MEDIUM_PURPLE = HSBK.from_rgb(147, 112, 219)
|
|
640
|
+
MEDIUM_SEA_GREEN = HSBK.from_rgb(60, 179, 113)
|
|
641
|
+
MEDIUM_SLATE_BLUE = HSBK.from_rgb(123, 104, 238)
|
|
642
|
+
MEDIUM_SPRING_GREEN = HSBK.from_rgb(0, 250, 154)
|
|
643
|
+
MEDIUM_TURQUOISE = HSBK.from_rgb(72, 209, 204)
|
|
644
|
+
MEDIUM_VIOLET_RED = HSBK.from_rgb(199, 21, 133)
|
|
645
|
+
MIDNIGHT_BLUE = HSBK.from_rgb(25, 25, 112)
|
|
646
|
+
MINT_CREAM = HSBK.from_rgb(245, 255, 250)
|
|
647
|
+
MISTY_ROSE = HSBK.from_rgb(255, 228, 225)
|
|
648
|
+
MOCCASIN = HSBK.from_rgb(255, 228, 181)
|
|
649
|
+
NAVAJO_WHITE = HSBK.from_rgb(255, 222, 173)
|
|
650
|
+
NAVY = HSBK.from_rgb(0, 0, 128)
|
|
651
|
+
OLD_LACE = HSBK.from_rgb(253, 245, 230)
|
|
652
|
+
OLIVE = HSBK.from_rgb(128, 128, 0)
|
|
653
|
+
OLIVE_DRAB = HSBK.from_rgb(107, 142, 35)
|
|
654
|
+
ORANGE = HSBK.from_rgb(255, 165, 0)
|
|
655
|
+
ORANGE_RED = HSBK.from_rgb(255, 69, 0)
|
|
656
|
+
ORCHID = HSBK.from_rgb(218, 112, 214)
|
|
657
|
+
PALE_GOLDENROD = HSBK.from_rgb(238, 232, 170)
|
|
658
|
+
PALE_GREEN = HSBK.from_rgb(152, 251, 152)
|
|
659
|
+
PALE_TURQUOISE = HSBK.from_rgb(175, 238, 238)
|
|
660
|
+
PALE_VIOLET_RED = HSBK.from_rgb(219, 112, 147)
|
|
661
|
+
PAPAYA_WHIP = HSBK.from_rgb(255, 239, 213)
|
|
662
|
+
PEACH_PUFF = HSBK.from_rgb(255, 218, 185)
|
|
663
|
+
PERU = HSBK.from_rgb(205, 133, 63)
|
|
664
|
+
PINK = HSBK.from_rgb(255, 192, 203)
|
|
665
|
+
PLUM = HSBK.from_rgb(221, 160, 221)
|
|
666
|
+
POWDER_BLUE = HSBK.from_rgb(176, 224, 230)
|
|
667
|
+
PURPLE = HSBK.from_rgb(128, 0, 128)
|
|
668
|
+
REBECCA_PURPLE = HSBK.from_rgb(102, 51, 153)
|
|
669
|
+
RED = HSBK.from_rgb(255, 0, 0)
|
|
670
|
+
ROSY_BROWN = HSBK.from_rgb(188, 143, 143)
|
|
671
|
+
ROYAL_BLUE = HSBK.from_rgb(65, 105, 225)
|
|
672
|
+
SADDLE_BROWN = HSBK.from_rgb(139, 69, 19)
|
|
673
|
+
SALMON = HSBK.from_rgb(250, 128, 114)
|
|
674
|
+
SANDY_BROWN = HSBK.from_rgb(244, 164, 96)
|
|
675
|
+
SEA_GREEN = HSBK.from_rgb(46, 139, 87)
|
|
676
|
+
SEASHELL = HSBK.from_rgb(255, 245, 238)
|
|
677
|
+
SIENNA = HSBK.from_rgb(160, 82, 45)
|
|
678
|
+
SILVER = HSBK.from_rgb(192, 192, 192)
|
|
679
|
+
SKY_BLUE = HSBK.from_rgb(135, 206, 235)
|
|
680
|
+
SLATE_BLUE = HSBK.from_rgb(106, 90, 205)
|
|
681
|
+
SLATE_GRAY = HSBK.from_rgb(112, 128, 144)
|
|
682
|
+
SLATE_GREY = HSBK.from_rgb(112, 128, 144)
|
|
683
|
+
SNOW = HSBK.from_rgb(255, 250, 250)
|
|
684
|
+
SPRING_GREEN = HSBK.from_rgb(0, 255, 127)
|
|
685
|
+
STEEL_BLUE = HSBK.from_rgb(70, 130, 180)
|
|
686
|
+
TAN = HSBK.from_rgb(210, 180, 140)
|
|
687
|
+
TEAL = HSBK.from_rgb(0, 128, 128)
|
|
688
|
+
THISTLE = HSBK.from_rgb(216, 191, 216)
|
|
689
|
+
TOMATO = HSBK.from_rgb(255, 99, 71)
|
|
690
|
+
TURQUOISE = HSBK.from_rgb(64, 224, 208)
|
|
691
|
+
VIOLET = HSBK.from_rgb(238, 130, 238)
|
|
692
|
+
WHEAT = HSBK.from_rgb(245, 222, 179)
|
|
693
|
+
WHITE = HSBK.from_rgb(255, 255, 255)
|
|
694
|
+
WHITE_SMOKE = HSBK.from_rgb(245, 245, 245)
|
|
695
|
+
YELLOW = HSBK.from_rgb(255, 255, 0)
|
|
696
|
+
YELLOW_GREEN = HSBK.from_rgb(154, 205, 50)
|
|
697
|
+
|
|
698
|
+
# White temperature variants (kelvin-based, warmest to coolest)
|
|
699
|
+
CANDLELIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_CANDLELIGHT)
|
|
700
|
+
SUNSET = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_SUNSET)
|
|
701
|
+
AMBER = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_AMBER)
|
|
702
|
+
ULTRA_WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_ULTRA_WARM)
|
|
703
|
+
INCANDESCENT = HSBK(
|
|
704
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_INCANDESCENT
|
|
705
|
+
)
|
|
706
|
+
WARM = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_WARM)
|
|
707
|
+
NEUTRAL_WARM = HSBK(
|
|
708
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL_WARM
|
|
709
|
+
)
|
|
710
|
+
NEUTRAL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
711
|
+
COOL = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL)
|
|
712
|
+
COOL_DAYLIGHT = HSBK(
|
|
713
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_COOL_DAYLIGHT
|
|
714
|
+
)
|
|
715
|
+
SOFT_DAYLIGHT = HSBK(
|
|
716
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_SOFT_DAYLIGHT
|
|
717
|
+
)
|
|
718
|
+
DAYLIGHT = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_DAYLIGHT)
|
|
719
|
+
NOON_DAYLIGHT = HSBK(
|
|
720
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_NOON_DAYLIGHT
|
|
721
|
+
)
|
|
722
|
+
BRIGHT_DAYLIGHT = HSBK(
|
|
723
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BRIGHT_DAYLIGHT
|
|
724
|
+
)
|
|
725
|
+
CLOUDY_DAYLIGHT = HSBK(
|
|
726
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_CLOUDY_DAYLIGHT
|
|
727
|
+
)
|
|
728
|
+
BLUE_DAYLIGHT = HSBK(
|
|
729
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_DAYLIGHT
|
|
730
|
+
)
|
|
731
|
+
BLUE_OVERCAST = HSBK(
|
|
732
|
+
hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_OVERCAST
|
|
733
|
+
)
|
|
734
|
+
BLUE_ICE = HSBK(hue=0, saturation=0.0, brightness=1.0, kelvin=KELVIN_BLUE_ICE)
|
|
735
|
+
|
|
736
|
+
# Pastel variants (reduced saturation)
|
|
543
737
|
PASTEL_RED = HSBK(hue=0, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
544
|
-
PASTEL_ORANGE = HSBK(hue=
|
|
738
|
+
PASTEL_ORANGE = HSBK(hue=39, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
545
739
|
PASTEL_YELLOW = HSBK(hue=60, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
546
740
|
PASTEL_GREEN = HSBK(hue=120, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
547
741
|
PASTEL_CYAN = HSBK(hue=180, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
548
742
|
PASTEL_BLUE = HSBK(hue=240, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
549
|
-
PASTEL_PURPLE = HSBK(hue=
|
|
743
|
+
PASTEL_PURPLE = HSBK(hue=300, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
550
744
|
PASTEL_MAGENTA = HSBK(
|
|
551
745
|
hue=300, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL
|
|
552
746
|
)
|
|
553
|
-
PASTEL_PINK = HSBK(hue=
|
|
747
|
+
PASTEL_PINK = HSBK(hue=350, saturation=0.3, brightness=1.0, kelvin=KELVIN_NEUTRAL)
|
|
748
|
+
|
|
749
|
+
# Backwards compatibility aliases
|
|
750
|
+
WHITE_WARM = WARM
|
|
751
|
+
WHITE_NEUTRAL = NEUTRAL
|
|
752
|
+
WHITE_COOL = COOL
|
|
753
|
+
WHITE_DAYLIGHT = DAYLIGHT
|
lifx/devices/base.py
CHANGED
|
@@ -9,7 +9,7 @@ import time
|
|
|
9
9
|
import uuid
|
|
10
10
|
from dataclasses import dataclass, field
|
|
11
11
|
from math import floor, log10
|
|
12
|
-
from typing import TYPE_CHECKING, Generic,
|
|
12
|
+
from typing import TYPE_CHECKING, Generic, TypeVar, cast
|
|
13
13
|
|
|
14
14
|
from lifx.const import (
|
|
15
15
|
DEFAULT_MAX_RETRIES,
|
|
@@ -26,6 +26,8 @@ from lifx.protocol import packets
|
|
|
26
26
|
from lifx.protocol.models import Serial
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
|
+
from typing_extensions import Self
|
|
30
|
+
|
|
29
31
|
from lifx.devices import (
|
|
30
32
|
CeilingLight,
|
|
31
33
|
HevLight,
|
|
@@ -681,12 +683,13 @@ class Device(Generic[StateT]):
|
|
|
681
683
|
async def _setup(self) -> None:
|
|
682
684
|
"""Populate device capabilities, state and metadata."""
|
|
683
685
|
await self._ensure_capabilities()
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
686
|
+
await asyncio.gather(
|
|
687
|
+
self.get_host_firmware(),
|
|
688
|
+
self.get_wifi_firmware(),
|
|
689
|
+
self.get_label(),
|
|
690
|
+
self.get_location(),
|
|
691
|
+
self.get_group(),
|
|
692
|
+
)
|
|
690
693
|
|
|
691
694
|
async def get_mac_address(self) -> str:
|
|
692
695
|
"""Calculate and return the MAC address for this device."""
|
|
@@ -1669,21 +1672,21 @@ class Device(Generic[StateT]):
|
|
|
1669
1672
|
|
|
1670
1673
|
# Fetch semi-static and volatile state in parallel
|
|
1671
1674
|
# get_color returns color, power, and label in one request
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1675
|
+
(
|
|
1676
|
+
label,
|
|
1677
|
+
power,
|
|
1678
|
+
host_firmware,
|
|
1679
|
+
wifi_firmware,
|
|
1680
|
+
location_info,
|
|
1681
|
+
group_info,
|
|
1682
|
+
) = await asyncio.gather(
|
|
1683
|
+
self.get_label(),
|
|
1684
|
+
self.get_power(),
|
|
1685
|
+
self.get_host_firmware(),
|
|
1686
|
+
self.get_wifi_firmware(),
|
|
1687
|
+
self.get_location(),
|
|
1688
|
+
self.get_group(),
|
|
1689
|
+
)
|
|
1687
1690
|
|
|
1688
1691
|
# Get MAC address (already calculated in get_host_firmware)
|
|
1689
1692
|
mac_address = await self.get_mac_address()
|
lifx/devices/hev.py
CHANGED
|
@@ -119,10 +119,11 @@ class HevLight(Light):
|
|
|
119
119
|
async def _setup(self) -> None:
|
|
120
120
|
"""Populate HEV light capabilities, state and metadata."""
|
|
121
121
|
await super()._setup()
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
await asyncio.gather(
|
|
123
|
+
self.get_hev_config(),
|
|
124
|
+
self.get_hev_cycle(),
|
|
125
|
+
self.get_last_hev_result(),
|
|
126
|
+
)
|
|
126
127
|
|
|
127
128
|
async def get_hev_cycle(self) -> HevCycleState:
|
|
128
129
|
"""Get current HEV cycle state.
|
|
@@ -413,12 +414,10 @@ class HevLight(Light):
|
|
|
413
414
|
await super().refresh_state()
|
|
414
415
|
|
|
415
416
|
# Fetch all HEV light state
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
hev_cycle = hev_cycle_task.result()
|
|
421
|
-
hev_result = hev_result_task.result()
|
|
417
|
+
hev_cycle, hev_result = await asyncio.gather(
|
|
418
|
+
self.get_hev_cycle(),
|
|
419
|
+
self.get_last_hev_result(),
|
|
420
|
+
)
|
|
422
421
|
|
|
423
422
|
self._state.hev_cycle = hev_cycle
|
|
424
423
|
self._state.hev_result = hev_result
|
|
@@ -440,14 +439,11 @@ class HevLight(Light):
|
|
|
440
439
|
|
|
441
440
|
# Fetch semi-static and volatile state in parallel
|
|
442
441
|
# get_color returns color, power, and label in one request
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
hev_cycle = hev_cycle_task.result()
|
|
449
|
-
hev_config = hev_config_task.result()
|
|
450
|
-
hev_result = hev_result_task.result()
|
|
442
|
+
hev_cycle, hev_config, hev_result = await asyncio.gather(
|
|
443
|
+
self.get_hev_cycle(),
|
|
444
|
+
self.get_hev_config(),
|
|
445
|
+
self.get_last_hev_result(),
|
|
446
|
+
)
|
|
451
447
|
|
|
452
448
|
# Create state instance with HEV fields
|
|
453
449
|
self._state = HevLightState.from_light_state(
|
lifx/devices/light.py
CHANGED
|
@@ -964,19 +964,19 @@ class Light(Device[LightState]):
|
|
|
964
964
|
# Fetch semi-static and volatile state in parallel
|
|
965
965
|
# get_color returns color, power, and label in one request
|
|
966
966
|
try:
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
967
|
+
(
|
|
968
|
+
(color, power, label),
|
|
969
|
+
host_firmware,
|
|
970
|
+
wifi_firmware,
|
|
971
|
+
location_info,
|
|
972
|
+
group_info,
|
|
973
|
+
) = await asyncio.gather(
|
|
974
|
+
self.get_color(),
|
|
975
|
+
self.get_host_firmware(),
|
|
976
|
+
self.get_wifi_firmware(),
|
|
977
|
+
self.get_location(),
|
|
978
|
+
self.get_group(),
|
|
979
|
+
)
|
|
980
980
|
|
|
981
981
|
# Get MAC address (already calculated in get_host_firmware)
|
|
982
982
|
mac_address = await self.get_mac_address()
|
|
@@ -1003,7 +1003,7 @@ class Light(Device[LightState]):
|
|
|
1003
1003
|
|
|
1004
1004
|
return self._state
|
|
1005
1005
|
|
|
1006
|
-
except
|
|
1006
|
+
except LifxTimeoutError:
|
|
1007
1007
|
raise LifxTimeoutError(f"Error initializing state for {self.serial}")
|
|
1008
|
-
except
|
|
1008
|
+
except LifxError:
|
|
1009
1009
|
raise LifxError(f"Error initializing state for {self.serial}")
|
lifx/devices/multizone.py
CHANGED
|
@@ -969,12 +969,10 @@ class MultiZoneLight(Light):
|
|
|
969
969
|
"""
|
|
970
970
|
await super().refresh_state()
|
|
971
971
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
zones = zones_task.result()
|
|
977
|
-
effect = effect_task.result()
|
|
972
|
+
zones, effect = await asyncio.gather(
|
|
973
|
+
self.get_all_color_zones(),
|
|
974
|
+
self.get_effect(),
|
|
975
|
+
)
|
|
978
976
|
|
|
979
977
|
self._state.zones = zones
|
|
980
978
|
self._state.effect = effect.effect_type
|
|
@@ -994,12 +992,10 @@ class MultiZoneLight(Light):
|
|
|
994
992
|
"""
|
|
995
993
|
light_state = await super()._initialize_state()
|
|
996
994
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
zones = zones_task.result()
|
|
1002
|
-
effect = effect_task.result()
|
|
995
|
+
zones, effect = await asyncio.gather(
|
|
996
|
+
self.get_all_color_zones(),
|
|
997
|
+
self.get_effect(),
|
|
998
|
+
)
|
|
1003
999
|
|
|
1004
1000
|
self._state = MultiZoneLightState.from_light_state(
|
|
1005
1001
|
light_state=light_state, zones=zones, effect=effect.effect_type
|
lifx/effects/base.py
CHANGED
|
@@ -85,22 +85,26 @@ class LIFXEffect(ABC):
|
|
|
85
85
|
if self.power_on:
|
|
86
86
|
needs_power_on = False
|
|
87
87
|
|
|
88
|
-
async def power_on_if_needed(light: Light) ->
|
|
89
|
-
"""Power on a single light if it's currently off.
|
|
90
|
-
|
|
88
|
+
async def power_on_if_needed(light: Light) -> bool:
|
|
89
|
+
"""Power on a single light if it's currently off.
|
|
90
|
+
|
|
91
|
+
Returns True if the light was powered on.
|
|
92
|
+
"""
|
|
91
93
|
is_on = await light.get_power()
|
|
92
94
|
if not is_on:
|
|
93
|
-
needs_power_on = True
|
|
94
95
|
# Get startup color for this light
|
|
95
96
|
startup_color = await self.from_poweroff_hsbk(light)
|
|
96
97
|
# Set color immediately, then power on
|
|
97
98
|
await light.set_color(startup_color, duration=0)
|
|
98
99
|
await light.set_power(True, duration=POWER_ON_TRANSITION_DURATION)
|
|
100
|
+
return True
|
|
101
|
+
return False
|
|
99
102
|
|
|
100
103
|
# Power on all lights concurrently
|
|
101
|
-
|
|
102
|
-
for light in self.participants
|
|
103
|
-
|
|
104
|
+
results = await asyncio.gather(
|
|
105
|
+
*(power_on_if_needed(light) for light in self.participants)
|
|
106
|
+
)
|
|
107
|
+
needs_power_on = any(results)
|
|
104
108
|
|
|
105
109
|
# Wait for power transition to complete if any lights were powered on
|
|
106
110
|
if needs_power_on:
|
lifx/effects/colorloop.py
CHANGED
|
@@ -353,17 +353,11 @@ class EffectColorloop(LIFXEffect):
|
|
|
353
353
|
)
|
|
354
354
|
|
|
355
355
|
# Fetch colors for all lights concurrently
|
|
356
|
-
colors
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
for idx, light in enumerate(self.participants):
|
|
360
|
-
|
|
361
|
-
async def fetch_and_store(index: int, device: Light) -> None:
|
|
362
|
-
colors[index] = await get_color_for_light(device)
|
|
363
|
-
|
|
364
|
-
tg.create_task(fetch_and_store(idx, light))
|
|
356
|
+
colors = await asyncio.gather(
|
|
357
|
+
*(get_color_for_light(light) for light in self.participants)
|
|
358
|
+
)
|
|
365
359
|
|
|
366
|
-
return colors
|
|
360
|
+
return list(colors)
|
|
367
361
|
|
|
368
362
|
async def from_poweroff_hsbk(self, _light: Light) -> HSBK:
|
|
369
363
|
"""Return startup color when light is powered off.
|
lifx/effects/conductor.py
CHANGED
|
@@ -154,36 +154,32 @@ class Conductor:
|
|
|
154
154
|
|
|
155
155
|
# Capture prestates in parallel for all lights that need it
|
|
156
156
|
if lights_needing_capture:
|
|
157
|
-
captured: list[tuple[str, PreState]] = [None] * len( # type: ignore
|
|
158
|
-
lights_needing_capture
|
|
159
|
-
)
|
|
160
157
|
|
|
161
|
-
async
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
tg.create_task(capture_and_store(capture_idx, light))
|
|
158
|
+
async def capture_and_log(device: Light) -> tuple[str, PreState]:
|
|
159
|
+
prestate = await self._state_manager.capture_state(device)
|
|
160
|
+
_LOGGER.debug(
|
|
161
|
+
{
|
|
162
|
+
"class": self.__class__.__name__,
|
|
163
|
+
"method": "start",
|
|
164
|
+
"action": "capture",
|
|
165
|
+
"values": {
|
|
166
|
+
"serial": device.serial,
|
|
167
|
+
"power": prestate.power,
|
|
168
|
+
"color": {
|
|
169
|
+
"hue": prestate.color.hue,
|
|
170
|
+
"saturation": prestate.color.saturation,
|
|
171
|
+
"brightness": prestate.color.brightness,
|
|
172
|
+
"kelvin": prestate.color.kelvin,
|
|
173
|
+
},
|
|
174
|
+
"has_zones": prestate.zone_colors is not None,
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
return (device.serial, prestate)
|
|
179
|
+
|
|
180
|
+
captured = await asyncio.gather(
|
|
181
|
+
*(capture_and_log(light) for _, light in lights_needing_capture)
|
|
182
|
+
)
|
|
187
183
|
|
|
188
184
|
# Store captured prestates
|
|
189
185
|
for serial, prestate in captured:
|
|
@@ -257,11 +253,12 @@ class Conductor:
|
|
|
257
253
|
async with self._lock:
|
|
258
254
|
# Restore all lights in parallel
|
|
259
255
|
if lights_to_restore:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
256
|
+
await asyncio.gather(
|
|
257
|
+
*(
|
|
258
|
+
self._state_manager.restore_state(light, prestate)
|
|
259
|
+
for light, prestate in lights_to_restore
|
|
260
|
+
)
|
|
261
|
+
)
|
|
265
262
|
|
|
266
263
|
# Remove from running registry after restoration
|
|
267
264
|
for light in lights:
|
|
@@ -304,11 +301,12 @@ class Conductor:
|
|
|
304
301
|
|
|
305
302
|
# Restore all lights in parallel
|
|
306
303
|
if lights_to_restore:
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
304
|
+
await asyncio.gather(
|
|
305
|
+
*(
|
|
306
|
+
self._state_manager.restore_state(light, prestate)
|
|
307
|
+
for light, prestate in lights_to_restore
|
|
308
|
+
)
|
|
309
|
+
)
|
|
312
310
|
|
|
313
311
|
# Remove from running registry after restoration
|
|
314
312
|
for light in participants:
|
|
@@ -367,10 +365,9 @@ class Conductor:
|
|
|
367
365
|
Returns:
|
|
368
366
|
List of lights compatible with the effect
|
|
369
367
|
"""
|
|
370
|
-
# Check all lights in parallel using effect's compatibility check
|
|
371
|
-
results: list[tuple[Light, bool]] = []
|
|
372
368
|
|
|
373
|
-
|
|
369
|
+
# Check all lights in parallel using effect's compatibility check
|
|
370
|
+
async def check_compatibility(light: Light) -> tuple[Light, bool]:
|
|
374
371
|
"""Check if a single light is compatible with the effect."""
|
|
375
372
|
is_compatible = await effect.is_light_compatible(light)
|
|
376
373
|
|
|
@@ -388,11 +385,11 @@ class Conductor:
|
|
|
388
385
|
}
|
|
389
386
|
)
|
|
390
387
|
|
|
391
|
-
|
|
388
|
+
return (light, is_compatible)
|
|
392
389
|
|
|
393
|
-
|
|
394
|
-
for light in participants
|
|
395
|
-
|
|
390
|
+
results = await asyncio.gather(
|
|
391
|
+
*(check_compatibility(light) for light in participants)
|
|
392
|
+
)
|
|
396
393
|
|
|
397
394
|
# Filter to only compatible lights
|
|
398
395
|
compatible = [light for light, is_compatible in results if is_compatible]
|
lifx/effects/pulse.py
CHANGED
|
@@ -184,17 +184,11 @@ class EffectPulse(LIFXEffect):
|
|
|
184
184
|
return await self.fetch_light_color(light)
|
|
185
185
|
|
|
186
186
|
# Fetch colors for all lights concurrently
|
|
187
|
-
colors
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
for idx, light in enumerate(self.participants):
|
|
191
|
-
|
|
192
|
-
async def fetch_and_store(index: int, device: Light) -> None:
|
|
193
|
-
colors[index] = await get_color_for_light(device)
|
|
194
|
-
|
|
195
|
-
tg.create_task(fetch_and_store(idx, light))
|
|
187
|
+
colors = await asyncio.gather(
|
|
188
|
+
*(get_color_for_light(light) for light in self.participants)
|
|
189
|
+
)
|
|
196
190
|
|
|
197
|
-
return colors
|
|
191
|
+
return list(colors)
|
|
198
192
|
|
|
199
193
|
async def _apply_waveform(self, light: Light, color: HSBK) -> None:
|
|
200
194
|
"""Apply waveform to a single light.
|
lifx/network/connection.py
CHANGED
lifx/network/mdns/transport.py
CHANGED
|
@@ -255,9 +255,10 @@ class MdnsTransport:
|
|
|
255
255
|
raise LifxNetworkError("Socket not open")
|
|
256
256
|
|
|
257
257
|
try:
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
258
|
+
data, addr = await asyncio.wait_for(
|
|
259
|
+
self._protocol.queue.get(), timeout=timeout
|
|
260
|
+
)
|
|
261
|
+
return data, addr
|
|
261
262
|
except TimeoutError as e:
|
|
262
263
|
raise LifxTimeoutError(f"No mDNS data received within {timeout}s") from e
|
|
263
264
|
except OSError as e:
|
lifx/network/transport.py
CHANGED
|
@@ -205,43 +205,9 @@ class UdpTransport:
|
|
|
205
205
|
raise LifxNetworkError("Socket not open")
|
|
206
206
|
|
|
207
207
|
try:
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
# Validate packet size
|
|
212
|
-
if len(data) > MAX_PACKET_SIZE:
|
|
213
|
-
from lifx.exceptions import LifxProtocolError
|
|
214
|
-
|
|
215
|
-
_LOGGER.error(
|
|
216
|
-
{
|
|
217
|
-
"class": "UdpTransport",
|
|
218
|
-
"method": "receive",
|
|
219
|
-
"action": "packet_too_large",
|
|
220
|
-
"packet_size": len(data),
|
|
221
|
-
"max_size": MAX_PACKET_SIZE,
|
|
222
|
-
}
|
|
223
|
-
)
|
|
224
|
-
raise LifxProtocolError(
|
|
225
|
-
f"Packet too big: {len(data)} bytes > {MAX_PACKET_SIZE} bytes"
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
if len(data) < MIN_PACKET_SIZE:
|
|
229
|
-
from lifx.exceptions import LifxProtocolError
|
|
230
|
-
|
|
231
|
-
_LOGGER.error(
|
|
232
|
-
{
|
|
233
|
-
"class": "UdpTransport",
|
|
234
|
-
"method": "receive",
|
|
235
|
-
"action": "packet_too_small",
|
|
236
|
-
"packet_size": len(data),
|
|
237
|
-
"min_size": MIN_PACKET_SIZE,
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
|
-
raise LifxProtocolError(
|
|
241
|
-
f"Packet too small: {len(data)} bytes < {MIN_PACKET_SIZE} bytes"
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
return data, addr
|
|
208
|
+
data, addr = await asyncio.wait_for(
|
|
209
|
+
self._protocol.queue.get(), timeout=timeout
|
|
210
|
+
)
|
|
245
211
|
except TimeoutError as e:
|
|
246
212
|
raise LifxTimeoutError(f"No data received within {timeout}s") from e
|
|
247
213
|
except OSError as e:
|
|
@@ -255,6 +221,41 @@ class UdpTransport:
|
|
|
255
221
|
)
|
|
256
222
|
raise LifxNetworkError(f"Failed to receive data: {e}") from e
|
|
257
223
|
|
|
224
|
+
# Validate packet size
|
|
225
|
+
if len(data) > MAX_PACKET_SIZE:
|
|
226
|
+
from lifx.exceptions import LifxProtocolError
|
|
227
|
+
|
|
228
|
+
_LOGGER.error(
|
|
229
|
+
{
|
|
230
|
+
"class": "UdpTransport",
|
|
231
|
+
"method": "receive",
|
|
232
|
+
"action": "packet_too_large",
|
|
233
|
+
"packet_size": len(data),
|
|
234
|
+
"max_size": MAX_PACKET_SIZE,
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
raise LifxProtocolError(
|
|
238
|
+
f"Packet too big: {len(data)} bytes > {MAX_PACKET_SIZE} bytes"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if len(data) < MIN_PACKET_SIZE:
|
|
242
|
+
from lifx.exceptions import LifxProtocolError
|
|
243
|
+
|
|
244
|
+
_LOGGER.error(
|
|
245
|
+
{
|
|
246
|
+
"class": "UdpTransport",
|
|
247
|
+
"method": "receive",
|
|
248
|
+
"action": "packet_too_small",
|
|
249
|
+
"packet_size": len(data),
|
|
250
|
+
"min_size": MIN_PACKET_SIZE,
|
|
251
|
+
}
|
|
252
|
+
)
|
|
253
|
+
raise LifxProtocolError(
|
|
254
|
+
f"Packet too small: {len(data)} bytes < {MIN_PACKET_SIZE} bytes"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
return data, addr
|
|
258
|
+
|
|
258
259
|
async def receive_many(
|
|
259
260
|
self, timeout: float = 5.0, max_packets: int | None = None
|
|
260
261
|
) -> list[tuple[bytes, tuple[str, int]]]:
|
|
@@ -273,34 +274,40 @@ class UdpTransport:
|
|
|
273
274
|
if self._protocol is None:
|
|
274
275
|
raise LifxNetworkError("Socket not open")
|
|
275
276
|
|
|
277
|
+
import time
|
|
278
|
+
|
|
276
279
|
packets: list[tuple[bytes, tuple[str, int]]] = []
|
|
280
|
+
deadline = time.monotonic() + timeout
|
|
277
281
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
282
|
+
while True:
|
|
283
|
+
if max_packets is not None and len(packets) >= max_packets:
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
remaining = deadline - time.monotonic()
|
|
287
|
+
if remaining <= 0:
|
|
288
|
+
break
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
data, addr = await asyncio.wait_for(
|
|
292
|
+
self._protocol.queue.get(), timeout=remaining
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Validate packet size
|
|
296
|
+
if len(data) > MAX_PACKET_SIZE:
|
|
297
|
+
# Drop oversized packet to prevent memory exhaustion DoS
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
if len(data) < MIN_PACKET_SIZE:
|
|
301
|
+
# Drop undersized packet (header is 36 bytes)
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
packets.append((data, addr))
|
|
305
|
+
except TimeoutError:
|
|
306
|
+
# Timeout is expected - return what we collected
|
|
307
|
+
break
|
|
308
|
+
except OSError:
|
|
309
|
+
# Ignore individual receive errors
|
|
310
|
+
break
|
|
304
311
|
|
|
305
312
|
return packets
|
|
306
313
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: lifx-async
|
|
3
|
-
Version:
|
|
3
|
+
Version: 5.0.0
|
|
4
4
|
Summary: A modern, type-safe, async Python library for controlling LIFX lights
|
|
5
5
|
Author-email: Avi Miller <me@dje.li>
|
|
6
6
|
Maintainer-email: Avi Miller <me@dje.li>
|
|
@@ -11,6 +11,7 @@ Classifier: Framework :: Pytest
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Natural Language :: English
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -18,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
18
19
|
Classifier: Topic :: Software Development :: Libraries
|
|
19
20
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
21
|
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Python: >=3.
|
|
22
|
+
Requires-Python: >=3.10
|
|
22
23
|
Description-Content-Type: text/markdown
|
|
23
24
|
|
|
24
25
|
# lifx-async
|
|
@@ -1,34 +1,34 @@
|
|
|
1
1
|
lifx/__init__.py,sha256=DKHG1vFJvPw_LpMkQgZN85gyOSD8dnceq6LnEGgR9vs,2810
|
|
2
|
-
lifx/api.py,sha256=
|
|
3
|
-
lifx/color.py,sha256=
|
|
2
|
+
lifx/api.py,sha256=tTE9H8eSVrFIr6rEGxedWoR4ChnqvTz2EUgEQk60fgU,35428
|
|
3
|
+
lifx/color.py,sha256=UfeoOiFgFih5edl2Ei-0wSzvZXRTI47yUm9GlNJZeTw,26041
|
|
4
4
|
lifx/const.py,sha256=5LEh4h0-bEJlOfpG8fgyht0LkAEV9jkkpuCiuatBhEI,3840
|
|
5
5
|
lifx/exceptions.py,sha256=pikAMppLn7gXyjiQVWM_tSvXKNh-g366nG_UWyqpHhc,815
|
|
6
6
|
lifx/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
7
|
lifx/devices/__init__.py,sha256=4b5QtO0EFWxIqN2lUYgM8uLjWyHI5hUcReiF9QCjCGw,1061
|
|
8
|
-
lifx/devices/base.py,sha256=
|
|
8
|
+
lifx/devices/base.py,sha256=mhNLX6FoLBaZtYo9InleneYdb0dk3B2Ze8Z2eqXCNHo,63180
|
|
9
9
|
lifx/devices/ceiling.py,sha256=bLAurvqTNmhKMFUUJmLqn1vDFawapYju2i4G0pHOH_4,45790
|
|
10
|
-
lifx/devices/hev.py,sha256=
|
|
10
|
+
lifx/devices/hev.py,sha256=kTRJRYnWyIY8Pkg_jOn978N-_1YXy9fRmBiGgEWscXw,15194
|
|
11
11
|
lifx/devices/infrared.py,sha256=ePk9qxX_s-hv5gQMvio1Vv8FYiCd68HF0ySbWgSrvuU,8130
|
|
12
|
-
lifx/devices/light.py,sha256=
|
|
12
|
+
lifx/devices/light.py,sha256=ZhC7zuruZ9nzmnAR_st2KMUH8UNQAcNK-eQUYnKXm-8,33833
|
|
13
13
|
lifx/devices/matrix.py,sha256=TcfVvqFCIhGXjOtaQEARQCXe9XqSFDZ4wEQdRZBiQpA,42301
|
|
14
|
-
lifx/devices/multizone.py,sha256=
|
|
14
|
+
lifx/devices/multizone.py,sha256=7Te5Z_X9hDvdypjMqPGGM2TG0P9QltzFVi7UUxRdbGI,33326
|
|
15
15
|
lifx/effects/__init__.py,sha256=4DF31yp7RJic5JoltMlz5dCtF5KQobU6NOUtLUKkVKE,1509
|
|
16
|
-
lifx/effects/base.py,sha256=
|
|
17
|
-
lifx/effects/colorloop.py,sha256=
|
|
18
|
-
lifx/effects/conductor.py,sha256=
|
|
16
|
+
lifx/effects/base.py,sha256=tKgX5PsV6hipffD2B236rOzudkMwWq59-eQGnfvNKdU,10354
|
|
17
|
+
lifx/effects/colorloop.py,sha256=Yz9XcQ_VhTPSnJn1s4WnkoXTRh2_qFJrPhQkIiTF6Tk,15574
|
|
18
|
+
lifx/effects/conductor.py,sha256=Oaq-6m1kdUF6bma_U9GcA9onZzh6YRjpExBr-OGHQJI,14552
|
|
19
19
|
lifx/effects/const.py,sha256=03LfL8v9PtoUs6-2IR3aa6nkyA4Otdno51SFJtntC-U,795
|
|
20
20
|
lifx/effects/models.py,sha256=MS5D-cxD0Ar8XhqbqKAc9q2sk38IP1vPkYwd8V7jCr8,2446
|
|
21
|
-
lifx/effects/pulse.py,sha256=
|
|
21
|
+
lifx/effects/pulse.py,sha256=k4dtBhhgVHyuwzqzx89jYVKbSRUVQdZj91cklyKarbE,8455
|
|
22
22
|
lifx/effects/state_manager.py,sha256=iDfYowiCN5IJqcR1s-pM0mQEJpe-RDsMcOOSMmtPVDE,8983
|
|
23
23
|
lifx/network/__init__.py,sha256=uSyA8r8qISG7qXUHbX8uk9A2E8rvDADgCcf94QIZ9so,499
|
|
24
|
-
lifx/network/connection.py,sha256=
|
|
24
|
+
lifx/network/connection.py,sha256=qPKEkhr9DvoL2cCJoIlDi8rowNS-gUn_F3mesnzyFjs,38412
|
|
25
25
|
lifx/network/discovery.py,sha256=syFfkDYWo0AEoBdEBjWqBm4K7UJwZW5x2K0FBMiA2I0,24186
|
|
26
26
|
lifx/network/message.py,sha256=jCLC9v0tbBi54g5CaHLFM_nP1Izu8kJmo2tt23HHBbA,2600
|
|
27
|
-
lifx/network/transport.py,sha256=
|
|
27
|
+
lifx/network/transport.py,sha256=io_3SFYQliNa_upHcKRzrLUkDVsDmNsxa2gQMlxj7Zk,10912
|
|
28
28
|
lifx/network/mdns/__init__.py,sha256=LlZgsFe6q5_SIXvXqtuZ_O9tJbcJZ-nsFkD2_wD8_TM,1412
|
|
29
29
|
lifx/network/mdns/discovery.py,sha256=EZ2zlJmy96rMDmu5J-68ystXJ2gYa18zTYP3iqmTGgU,13200
|
|
30
30
|
lifx/network/mdns/dns.py,sha256=OsvNSxLepIG3Nhw-kkQF3JrBYI-ikod5SHD2HO5_yGE,9363
|
|
31
|
-
lifx/network/mdns/transport.py,sha256=
|
|
31
|
+
lifx/network/mdns/transport.py,sha256=41E_yX9Jx42ffElTcF-73A4ma48b3xkkHnG3DTcO8u8,10250
|
|
32
32
|
lifx/network/mdns/types.py,sha256=9fhH5iuMQxLkFPhmFTf2-kOcUNoWEu7LrN15Qr9tFE0,990
|
|
33
33
|
lifx/products/__init__.py,sha256=pf2O-fzt6nOrQd-wmzhiog91tMiGa-dDbaSNtU2ZQfE,764
|
|
34
34
|
lifx/products/generator.py,sha256=DsTCJcEVPmn9sfXSbXYdFZjqMfIbodnIQL46DRASs0g,15731
|
|
@@ -47,7 +47,7 @@ lifx/theme/canvas.py,sha256=4h7lgN8iu_OdchObGDgbxTqQLCb-FRKC-M-YCWef_i4,8048
|
|
|
47
47
|
lifx/theme/generators.py,sha256=nq3Yvntq_h-eFHbmmow3LcAdA_hEbRRaP5mv9Bydrjk,6435
|
|
48
48
|
lifx/theme/library.py,sha256=tKlKZNqJp8lRGDnilWyDm_Qr1vCRGGwuvWVS82anNpQ,21326
|
|
49
49
|
lifx/theme/theme.py,sha256=qMEx_8E41C0Cc6f083XHiAXEglTv4YlXW0UFsG1rQKg,5521
|
|
50
|
-
lifx_async-
|
|
51
|
-
lifx_async-
|
|
52
|
-
lifx_async-
|
|
53
|
-
lifx_async-
|
|
50
|
+
lifx_async-5.0.0.dist-info/METADATA,sha256=Ozi93MbWMkhFydTJ_nuyQUxRLLH_iGYOxtlr0iBXe2U,2660
|
|
51
|
+
lifx_async-5.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
52
|
+
lifx_async-5.0.0.dist-info/licenses/LICENSE,sha256=eBz48GRA3gSiWn3rYZAz2Ewp35snnhV9cSqkVBq7g3k,1832
|
|
53
|
+
lifx_async-5.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|