Skip to content

Commit f525520

Browse files
authored
Add support for WLED presets (#322)
1 parent c8c937a commit f525520

4 files changed

Lines changed: 125 additions & 7 deletions

File tree

examples/control.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import asyncio
55

6-
from wled import WLED
6+
from wled import WLED, Preset
77

88

99
async def main():
@@ -12,6 +12,9 @@ async def main():
1212
device = await led.update()
1313
print(device.info.version)
1414

15+
if isinstance(device.state.preset, Preset):
16+
print(f"Preset active! Name: {device.state.preset.name}")
17+
1518
# Turn strip on, full brightness
1619
await led.master(on=True, brightness=255)
1720

src/wled/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
Live,
99
Nightlight,
1010
Palette,
11+
Preset,
1112
Segment,
1213
State,
1314
Sync,
@@ -28,6 +29,7 @@
2829
"Live",
2930
"Nightlight",
3031
"Palette",
32+
"Preset",
3133
"Segment",
3234
"State",
3335
"Sync",

src/wled/models.py

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ class State:
326326
nightlight: Nightlight
327327
on: bool
328328
playlist: int
329-
preset: int
329+
preset: Preset | int | None
330330
segments: list[Segment]
331331
sync: Sync
332332
transition: int
@@ -352,14 +352,18 @@ def preset_active(self) -> bool:
352352

353353
@staticmethod
354354
def from_dict(
355-
data: dict[str, Any], effects: list[Effect], palettes: list[Palette]
355+
data: dict[str, Any],
356+
effects: list[Effect],
357+
palettes: list[Palette],
358+
presets: list[Preset],
356359
) -> State:
357360
"""Return State object from WLED API response.
358361
359362
Args:
360363
data: The state response received from the WLED device API.
361364
effects: A list of effect objects.
362-
palettes: A list of palette object.
365+
palettes: A list of palette objects.
366+
presets: A list of presets objects.
363367
364368
Returns:
365369
A State object.
@@ -380,25 +384,92 @@ def from_dict(
380384
for segment_id, segment in enumerate(data.get("seg", []))
381385
]
382386

387+
preset = data.get("ps", -1)
388+
if presets:
389+
preset = next(
390+
(item for item in presets if item.preset_id == data.get("ps")),
391+
None,
392+
)
393+
383394
return State(
384395
brightness=brightness,
385396
nightlight=Nightlight.from_dict(data),
386397
on=on,
387398
playlist=data.get("pl", -1),
388-
preset=data.get("ps", -1),
399+
preset=preset,
389400
segments=segments,
390401
sync=Sync.from_dict(data),
391402
transition=data.get("transition", 0),
392403
lor=Live(lor),
393404
)
394405

395406

407+
@dataclass
408+
class Preset:
409+
"""Object representing a WLED preset."""
410+
411+
preset_id: int
412+
name: str
413+
quick_label: str | None
414+
415+
on: bool
416+
transition: int
417+
main_segment: Segment | None
418+
segments: list[Segment]
419+
420+
@staticmethod
421+
def from_dict(
422+
preset_id: int,
423+
data: dict[str, Any],
424+
effects: list[Effect],
425+
palettes: list[Palette],
426+
) -> Preset:
427+
"""Return Preset object from WLED API response.
428+
429+
Args:
430+
preset_id: The ID of the preset.
431+
data: The data from the WLED device API.
432+
effects: A list of effect objects.
433+
palettes: A list of palette object.
434+
435+
Returns:
436+
A Preset object.
437+
"""
438+
segments = [
439+
Segment.from_dict(
440+
segment_id=segment_id,
441+
data=segment,
442+
effects=effects,
443+
palettes=palettes,
444+
state_on=False,
445+
state_brightness=0,
446+
)
447+
for segment_id, segment in enumerate(data.get("seg", []))
448+
]
449+
450+
main_segment = next(
451+
(item for item in segments if item.segment_id == data.get("mainseg", 0)),
452+
None,
453+
)
454+
455+
return Preset(
456+
main_segment=main_segment,
457+
name=data["n"],
458+
on=data.get("on", False),
459+
preset_id=preset_id,
460+
quick_label=data.get("ql"),
461+
segments=segments,
462+
transition=data.get("transition", 0),
463+
)
464+
465+
396466
class Device:
397467
"""Object holding all information of WLED."""
398468

399469
effects: list[Effect] = []
400470
info: Info
401471
palettes: list[Palette] = []
472+
presets: list[Preset] = []
402473
state: State
403474

404475
def __init__(self, data: dict) -> None:
@@ -445,11 +516,22 @@ def update_from_dict(self, data: dict) -> Device:
445516
palettes.sort(key=lambda x: x.name)
446517
self.palettes = palettes
447518

519+
if _presets := data.get("presets"):
520+
_presets.pop("0")
521+
presets = [
522+
Preset.from_dict(int(preset_id), preset, self.effects, self.palettes)
523+
for preset_id, preset in _presets.items()
524+
]
525+
presets.sort(key=lambda x: x.name)
526+
self.presets = presets
527+
448528
if _info := data.get("info"):
449529
self.info = Info.from_dict(_info)
450530

451531
if _state := data.get("state"):
452-
self.state = State.from_dict(_state, self.effects, self.palettes)
532+
self.state = State.from_dict(
533+
_state, self.effects, self.palettes, self.presets
534+
)
453535

454536
return self
455537

src/wled/wled.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ class WLED:
3535
_close_session: bool = False
3636
_device: Device | None = None
3737
_supports_si_request: bool | None = None
38+
_supports_presets: bool | None = None
3839

3940
@property
4041
def connected(self) -> bool:
@@ -230,6 +231,15 @@ async def update(self, full_update: bool = False) -> Device:
230231
f"WLED device at {self.host} returned an empty API"
231232
" response on full update"
232233
)
234+
235+
# Try to get presets, introduced in WLED 0.11
236+
try:
237+
presets = await self.request("/presets.json")
238+
data["presets"] = presets
239+
self._supports_presets = True
240+
except WLEDError:
241+
self._supports_presets = False
242+
233243
self._device = Device(data)
234244

235245
# Try to figure out if this version supports
@@ -249,6 +259,15 @@ async def update(self, full_update: bool = False) -> Device:
249259

250260
return self._device
251261

262+
if self._supports_presets:
263+
presets = await self.request("/presets.json")
264+
if not presets:
265+
raise WLEDEmptyResponseError(
266+
f"WLED device at {self.host} returned an empty API"
267+
" response on presets update"
268+
)
269+
self._device.update_from_dict({"presets": presets})
270+
252271
# Handle legacy state and update in separate requests
253272
if not self._supports_si_request:
254273
info = await self.request("/json/info")
@@ -274,6 +293,7 @@ async def update(self, full_update: bool = False) -> Device:
274293
" response on state & info update"
275294
)
276295
self._device.update_from_dict(state_info)
296+
277297
return self._device
278298

279299
async def master(
@@ -454,12 +474,23 @@ async def transition(self, transition: int) -> None:
454474
"/json/state", method="POST", data={"transition": transition}
455475
)
456476

457-
async def preset(self, preset: int) -> None:
477+
async def preset(self, preset: int | str) -> None:
458478
"""Set a preset on a WLED device.
459479
460480
Args:
461481
preset: The preset number to activate on this WLED device.
462482
"""
483+
# Find preset if it was based on a name
484+
if self._device and self._device.presets and isinstance(preset, str):
485+
preset = next(
486+
(
487+
item.preset_id
488+
for item in self._device.presets
489+
if item.name.lower() == preset.lower()
490+
),
491+
preset,
492+
)
493+
463494
await self.request("/json/state", method="POST", data={"ps": preset})
464495

465496
async def live(self, live: Live) -> None:

0 commit comments

Comments
 (0)