Skip to content

Commit d6ab0f6

Browse files
authored
Add playlist support (#359)
1 parent 4d7ef1c commit d6ab0f6

5 files changed

Lines changed: 137 additions & 18 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, Preset
6+
from wled import WLED, Playlist, Preset
77

88

99
async def main():
@@ -15,6 +15,9 @@ async def main():
1515
if isinstance(device.state.preset, Preset):
1616
print(f"Preset active! Name: {device.state.preset.name}")
1717

18+
if isinstance(device.state.playlist, Playlist):
19+
print(f"Playlist active! Name: {device.state.playlist.name}")
20+
1821
# Turn strip on, full brightness
1922
await led.master(on=True, brightness=255)
2023

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ max-line-length=88
129129
max-attributes=20
130130

131131
[tool.pytest.ini_options]
132-
addopts = "--cov"
132+
addopts = " --cov"
133133

134134
[tool.vulture]
135135
min_confidence = 80

src/wled/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
Live,
99
Nightlight,
1010
Palette,
11+
Playlist,
12+
PlaylistEntry,
1113
Preset,
1214
Segment,
1315
State,
@@ -29,6 +31,8 @@
2931
"Live",
3032
"Nightlight",
3133
"Palette",
34+
"Playlist",
35+
"PlaylistEntry",
3236
"Preset",
3337
"Segment",
3438
"State",

src/wled/models.py

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ class State:
325325
brightness: int
326326
nightlight: Nightlight
327327
on: bool
328-
playlist: int
328+
playlist: Playlist | int | None
329329
preset: Preset | int | None
330330
segments: list[Segment]
331331
sync: Sync
@@ -356,14 +356,16 @@ def from_dict(
356356
effects: list[Effect],
357357
palettes: list[Palette],
358358
presets: list[Preset],
359+
playlists: list[Playlist],
359360
) -> State:
360361
"""Return State object from WLED API response.
361362
362363
Args:
363364
data: The state response received from the WLED device API.
364365
effects: A list of effect objects.
365366
palettes: A list of palette objects.
366-
presets: A list of presets objects.
367+
presets: A list of preset objects.
368+
playlists: A list of playlist objects.
367369
368370
Returns:
369371
A State object.
@@ -384,18 +386,24 @@ def from_dict(
384386
for segment_id, segment in enumerate(data.get("seg", []))
385387
]
386388

389+
playlist = data.get("pl", -1)
387390
preset = data.get("ps", -1)
388391
if presets:
389392
preset = next(
390393
(item for item in presets if item.preset_id == data.get("ps")),
391394
None,
392395
)
393396

397+
playlist = next(
398+
(item for item in playlists if item.playlist_id == data.get("pl")),
399+
None,
400+
)
401+
394402
return State(
395403
brightness=brightness,
396404
nightlight=Nightlight.from_dict(data),
397405
on=on,
398-
playlist=data.get("pl", -1),
406+
playlist=playlist,
399407
preset=preset,
400408
segments=segments,
401409
sync=Sync.from_dict(data),
@@ -469,12 +477,82 @@ def from_dict(
469477
)
470478

471479

480+
@dataclass
481+
class PlaylistEntry:
482+
"""Object representing a entry in a WLED playlist."""
483+
484+
duration: int
485+
entry_id: int
486+
preset: Preset | None
487+
transition: int
488+
489+
490+
@dataclass
491+
class Playlist:
492+
"""Object representing a WLED playlist."""
493+
494+
end: Preset | None
495+
entries: list[PlaylistEntry]
496+
name: str
497+
playlist_id: int
498+
repeat: int
499+
shuffle: bool
500+
501+
@staticmethod
502+
def from_dict(
503+
playlist_id: int,
504+
data: dict[str, Any],
505+
presets: list[Preset],
506+
) -> Playlist:
507+
"""Return Playlist object from WLED API response.
508+
509+
Args:
510+
playlist_id: The ID of the playlist.
511+
data: The data from the WLED device API.
512+
presets: A list of preset objects.
513+
514+
Returns:
515+
A Playlist object.
516+
"""
517+
playlist = data.get("playlist", {})
518+
entries_durations = playlist.get("dur", [])
519+
entries_presets = playlist.get("ps", [])
520+
entries_transitions = playlist.get("transition", [])
521+
522+
entries = [
523+
PlaylistEntry(
524+
entry_id=entry_id,
525+
duration=entries_durations[entry_id],
526+
transition=entries_transitions[entry_id],
527+
preset=next(
528+
(item for item in presets if item.preset_id == preset_id), None
529+
),
530+
)
531+
for entry_id, preset_id in enumerate(entries_presets)
532+
]
533+
534+
end = next(
535+
(item for item in presets if item.preset_id == playlist.get("end")),
536+
None,
537+
)
538+
539+
return Playlist(
540+
playlist_id=playlist_id,
541+
shuffle=playlist.get("r", False),
542+
name=data.get("n", str(playlist_id)),
543+
repeat=playlist.get("repeat", 0),
544+
end=end,
545+
entries=entries,
546+
)
547+
548+
472549
class Device:
473550
"""Object holding all information of WLED."""
474551

475552
effects: list[Effect] = []
476553
info: Info
477554
palettes: list[Palette] = []
555+
playlists: list[Playlist] = []
478556
presets: list[Preset] = []
479557
state: State
480558

@@ -523,20 +601,37 @@ def update_from_dict(self, data: dict) -> Device:
523601
self.palettes = palettes
524602

525603
if _presets := data.get("presets"):
604+
# The preset data contains both presets and playlists,
605+
# we split those out, so we can handle those correctly.
606+
607+
# Nobody cares about 0.
526608
_presets.pop("0")
609+
527610
presets = [
528611
Preset.from_dict(int(preset_id), preset, self.effects, self.palettes)
529612
for preset_id, preset in _presets.items()
613+
if "playlist" not in preset
614+
or not ("ps" in preset["playlist"] and preset["playlist"]["ps"])
530615
]
531616
presets.sort(key=lambda x: x.name)
532617
self.presets = presets
533618

619+
playlists = [
620+
Playlist.from_dict(int(playlist_id), playlist, self.presets)
621+
for playlist_id, playlist in _presets.items()
622+
if "playlist" in playlist
623+
and "ps" in playlist["playlist"]
624+
and playlist["playlist"]["ps"]
625+
]
626+
playlists.sort(key=lambda x: x.name)
627+
self.playlists = playlists
628+
534629
if _info := data.get("info"):
535630
self.info = Info.from_dict(_info)
536631

537632
if _state := data.get("state"):
538633
self.state = State.from_dict(
539-
_state, self.effects, self.palettes, self.presets
634+
_state, self.effects, self.palettes, self.presets, self.playlists
540635
)
541636

542637
return self

src/wled/wled.py

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
WLEDEmptyResponseError,
2121
WLEDError,
2222
)
23-
from .models import Device, Live
23+
from .models import Device, Live, Playlist, Preset
2424

2525

2626
@dataclass
@@ -474,11 +474,11 @@ async def transition(self, transition: int) -> None:
474474
"/json/state", method="POST", data={"transition": transition}
475475
)
476476

477-
async def preset(self, preset: int | str) -> None:
477+
async def preset(self, preset: int | str | Preset) -> None:
478478
"""Set a preset on a WLED device.
479479
480480
Args:
481-
preset: The preset number to activate on this WLED device.
481+
preset: The preset to activate on this WLED device.
482482
"""
483483
# Find preset if it was based on a name
484484
if self._device and self._device.presets and isinstance(preset, str):
@@ -491,24 +491,41 @@ async def preset(self, preset: int | str) -> None:
491491
preset,
492492
)
493493

494+
if isinstance(preset, Preset):
495+
preset = preset.preset_id
496+
494497
await self.request("/json/state", method="POST", data={"ps": preset})
495498

496-
async def live(self, live: Live) -> None:
497-
"""Set the live override mode on a WLED device.
499+
async def playlist(self, playlist: int | str | Playlist) -> None:
500+
"""Set a playlist on a WLED device.
498501
499502
Args:
500-
live: The live override mode to set on this WLED device.
503+
playlist: The playlist to activate on this WLED device.
501504
"""
502-
await self.request("/json/state", method="POST", data={"lor": live.value})
503505

504-
async def playlist(self, playlist: int) -> None:
505-
"""Set a running playlist on a WLED device.
506+
# Find playlist if it was based on a name
507+
if self._device and self._device.playlists and isinstance(playlist, str):
508+
playlist = next(
509+
(
510+
item.playlist_id
511+
for item in self._device.playlists
512+
if item.name.lower() == playlist.lower()
513+
),
514+
playlist,
515+
)
516+
517+
if isinstance(playlist, Playlist):
518+
playlist = playlist.playlist_id
519+
520+
await self.request("/json/state", method="POST", data={"ps": playlist})
521+
522+
async def live(self, live: Live) -> None:
523+
"""Set the live override mode on a WLED device.
506524
507525
Args:
508-
playlist: ID of playlist to run. For now, this sets the preset
509-
cycle feature, -1 is off and 0 is on.
526+
live: The live override mode to set on this WLED device.
510527
"""
511-
await self.request("/json/state", method="POST", data={"pl": playlist})
528+
await self.request("/json/state", method="POST", data={"lor": live.value})
512529

513530
async def sync(
514531
self, *, send: bool | None = None, receive: bool | None = None

0 commit comments

Comments
 (0)