From d38b6afc7c4a2c8692277c0d3a3964952db6f592 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Jul 2021 12:03:57 +0200 Subject: [PATCH 1/2] Add playlist support --- examples/control.py | 5 ++- pyproject.toml | 2 +- src/wled/__init__.py | 4 ++ src/wled/models.py | 103 +++++++++++++++++++++++++++++++++++++++++-- src/wled/wled.py | 41 ++++++++++++----- 5 files changed, 137 insertions(+), 18 deletions(-) diff --git a/examples/control.py b/examples/control.py index 6193821c..e428ae2a 100644 --- a/examples/control.py +++ b/examples/control.py @@ -3,7 +3,7 @@ import asyncio -from wled import WLED, Preset +from wled import WLED, Playlist, Preset async def main(): @@ -15,6 +15,9 @@ async def main(): if isinstance(device.state.preset, Preset): print(f"Preset active! Name: {device.state.preset.name}") + if isinstance(device.state.playlist, Playlist): + print(f"Playlist active! Name: {device.state.playlist.name}") + # Turn strip on, full brightness await led.master(on=True, brightness=255) diff --git a/pyproject.toml b/pyproject.toml index 918a9ef1..fb09831c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,7 @@ max-line-length=88 max-attributes=20 [tool.pytest.ini_options] -addopts = "--cov" +addopts = " --cov" [tool.vulture] min_confidence = 80 diff --git a/src/wled/__init__.py b/src/wled/__init__.py index 9a7d008c..d8ac952b 100644 --- a/src/wled/__init__.py +++ b/src/wled/__init__.py @@ -8,6 +8,8 @@ Live, Nightlight, Palette, + Playlist, + PlaylistEntry, Preset, Segment, State, @@ -29,6 +31,8 @@ "Live", "Nightlight", "Palette", + "Playlist", + "PlaylistEntry", "Preset", "Segment", "State", diff --git a/src/wled/models.py b/src/wled/models.py index 4e3a400a..cbef290e 100644 --- a/src/wled/models.py +++ b/src/wled/models.py @@ -325,7 +325,7 @@ class State: brightness: int nightlight: Nightlight on: bool - playlist: int + playlist: Playlist | int | None preset: Preset | int | None segments: list[Segment] sync: Sync @@ -356,6 +356,7 @@ def from_dict( effects: list[Effect], palettes: list[Palette], presets: list[Preset], + playlists: list[Playlist], ) -> State: """Return State object from WLED API response. @@ -363,7 +364,8 @@ def from_dict( data: The state response received from the WLED device API. effects: A list of effect objects. palettes: A list of palette objects. - presets: A list of presets objects. + presets: A list of preset objects. + playlists: A list of playlist objects. Returns: A State object. @@ -384,6 +386,7 @@ def from_dict( for segment_id, segment in enumerate(data.get("seg", [])) ] + playlist = data.get("pl", -1) preset = data.get("ps", -1) if presets: preset = next( @@ -391,11 +394,16 @@ def from_dict( None, ) + playlist = next( + (item for item in playlists if item.playlist_id == data.get("pl")), + None, + ) + return State( brightness=brightness, nightlight=Nightlight.from_dict(data), on=on, - playlist=data.get("pl", -1), + playlist=playlist, preset=preset, segments=segments, sync=Sync.from_dict(data), @@ -469,12 +477,82 @@ def from_dict( ) +@dataclass +class PlaylistEntry: + """Object representing a entry in a WLED playlist.""" + + duration: int + entry_id: int + preset: Preset + transition: int + + +@dataclass +class Playlist: + """Object representing a WLED playlist.""" + + end: Preset | None + entries: list[PlaylistEntry] + name: str + playlist_id: int + repeat: int + shuffle: bool + + @staticmethod + def from_dict( + playlist_id: int, + data: dict[str, Any], + presets: list[Preset], + ) -> Playlist: + """Return Playlist object from WLED API response. + + Args: + playlist_id: The ID of the playlist. + data: The data from the WLED device API. + presets: A list of preset objects. + + Returns: + A Playlist object. + """ + playlist = data.get("playlist", {}) + entries_durations = playlist.get("dur", []) + entries_presets = playlist.get("ps", []) + entries_transitions = playlist.get("transition", []) + + entries = [ + PlaylistEntry( + entry_id=entry_id, + duration=entries_durations[entry_id], + transition=entries_transitions[entry_id], + preset=next( + (item for item in presets if item.preset_id == preset_id), + ), + ) + for entry_id, preset_id in enumerate(entries_presets) + ] + + end = next( + (item for item in presets if item.preset_id == playlist.get("end")), + None, + ) + + return Playlist( + playlist_id=playlist_id, + shuffle=playlist.get("r", False), + name=data.get("n", str(playlist_id)), + repeat=playlist.get("repeat", 0), + end=end, + entries=entries, + ) + + class Device: """Object holding all information of WLED.""" effects: list[Effect] = [] info: Info palettes: list[Palette] = [] + playlists: list[Playlist] = [] presets: list[Preset] = [] state: State @@ -523,20 +601,37 @@ def update_from_dict(self, data: dict) -> Device: self.palettes = palettes if _presets := data.get("presets"): + # The preset data contains both presets and playlists, + # we split those out, so we can handle those correctly. + + # Nobody cares about 0. _presets.pop("0") + presets = [ Preset.from_dict(int(preset_id), preset, self.effects, self.palettes) for preset_id, preset in _presets.items() + if "playlist" not in preset + or not ("ps" in preset["playlist"] and preset["playlist"]["ps"]) ] presets.sort(key=lambda x: x.name) self.presets = presets + playlists = [ + Playlist.from_dict(int(playlist_id), playlist, self.presets) + for playlist_id, playlist in _presets.items() + if "playlist" in playlist + and "ps" in playlist["playlist"] + and playlist["playlist"]["ps"] + ] + playlists.sort(key=lambda x: x.name) + self.playlists = playlists + if _info := data.get("info"): self.info = Info.from_dict(_info) if _state := data.get("state"): self.state = State.from_dict( - _state, self.effects, self.palettes, self.presets + _state, self.effects, self.palettes, self.presets, self.playlists ) return self diff --git a/src/wled/wled.py b/src/wled/wled.py index 4c7ae3ca..90fdc323 100644 --- a/src/wled/wled.py +++ b/src/wled/wled.py @@ -20,7 +20,7 @@ WLEDEmptyResponseError, WLEDError, ) -from .models import Device, Live +from .models import Device, Live, Playlist, Preset @dataclass @@ -474,11 +474,11 @@ async def transition(self, transition: int) -> None: "/json/state", method="POST", data={"transition": transition} ) - async def preset(self, preset: int | str) -> None: + async def preset(self, preset: int | str | Preset) -> None: """Set a preset on a WLED device. Args: - preset: The preset number to activate on this WLED device. + preset: The preset to activate on this WLED device. """ # Find preset if it was based on a name if self._device and self._device.presets and isinstance(preset, str): @@ -491,24 +491,41 @@ async def preset(self, preset: int | str) -> None: preset, ) + if isinstance(preset, Preset): + preset = preset.preset_id + await self.request("/json/state", method="POST", data={"ps": preset}) - async def live(self, live: Live) -> None: - """Set the live override mode on a WLED device. + async def playlist(self, playlist: int | str | Playlist) -> None: + """Set a playlist on a WLED device. Args: - live: The live override mode to set on this WLED device. + playlist: The playlist to activate on this WLED device. """ - await self.request("/json/state", method="POST", data={"lor": live.value}) - async def playlist(self, playlist: int) -> None: - """Set a running playlist on a WLED device. + # Find playlist if it was based on a name + if self._device and self._device.playlists and isinstance(playlist, str): + playlist = next( + ( + item.playlist_id + for item in self._device.playlists + if item.name.lower() == playlist.lower() + ), + playlist, + ) + + if isinstance(playlist, Playlist): + playlist = playlist.playlist_id + + await self.request("/json/state", method="POST", data={"ps": playlist}) + + async def live(self, live: Live) -> None: + """Set the live override mode on a WLED device. Args: - playlist: ID of playlist to run. For now, this sets the preset - cycle feature, -1 is off and 0 is on. + live: The live override mode to set on this WLED device. """ - await self.request("/json/state", method="POST", data={"pl": playlist}) + await self.request("/json/state", method="POST", data={"lor": live.value}) async def sync( self, *, send: bool | None = None, receive: bool | None = None From d141cc4c4e86839eb9db767165b043fb66086e50 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 23 Jul 2021 12:12:37 +0200 Subject: [PATCH 2/2] Ensure next() won't throw up --- src/wled/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wled/models.py b/src/wled/models.py index cbef290e..455e2429 100644 --- a/src/wled/models.py +++ b/src/wled/models.py @@ -483,7 +483,7 @@ class PlaylistEntry: duration: int entry_id: int - preset: Preset + preset: Preset | None transition: int @@ -525,7 +525,7 @@ def from_dict( duration=entries_durations[entry_id], transition=entries_transitions[entry_id], preset=next( - (item for item in presets if item.preset_id == preset_id), + (item for item in presets if item.preset_id == preset_id), None ), ) for entry_id, preset_id in enumerate(entries_presets)