Add WLED playlist support (#53381)

Co-authored-by: Anders Melchiorsen <amelchio@nogoto.net>
This commit is contained in:
Franck Nijhof 2021-07-26 11:15:49 +02:00 committed by GitHub
parent 3a5347f69e
commit 01c8114e93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 196 additions and 16 deletions

View file

@ -21,7 +21,6 @@ ATTR_LED_COUNT = "led_count"
ATTR_MAX_POWER = "max_power"
ATTR_ON = "on"
ATTR_PALETTE = "palette"
ATTR_PLAYLIST = "playlist"
ATTR_PRESET = "preset"
ATTR_REVERSE = "reverse"
ATTR_SEGMENT_ID = "segment_id"

View file

@ -5,7 +5,6 @@ from functools import partial
from typing import Any, Tuple, cast
import voluptuous as vol
from wled import Playlist
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
@ -30,7 +29,6 @@ from .const import (
ATTR_INTENSITY,
ATTR_ON,
ATTR_PALETTE,
ATTR_PLAYLIST,
ATTR_PRESET,
ATTR_REVERSE,
ATTR_SEGMENT_ID,
@ -221,17 +219,10 @@ class WLEDSegmentLight(WLEDEntity, LightEntity):
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes of the entity."""
playlist: int | Playlist | None = self.coordinator.data.state.playlist
if isinstance(playlist, Playlist):
playlist = playlist.playlist_id
if playlist == -1:
playlist = None
segment = self.coordinator.data.state.segments[self._segment]
return {
ATTR_INTENSITY: segment.intensity,
ATTR_PALETTE: segment.palette.name,
ATTR_PLAYLIST: playlist,
ATTR_REVERSE: segment.reverse,
ATTR_SPEED: segment.speed,
}

View file

@ -3,7 +3,7 @@ from __future__ import annotations
from functools import partial
from wled import Preset
from wled import Playlist, Preset
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
@ -26,7 +26,7 @@ async def async_setup_entry(
"""Set up WLED select based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([WLEDPresetSelect(coordinator)])
async_add_entities([WLEDPlaylistSelect(coordinator), WLEDPresetSelect(coordinator)])
update_segments = partial(
async_update_segments,
@ -69,6 +69,39 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity):
await self.coordinator.wled.preset(preset=option)
class WLEDPlaylistSelect(WLEDEntity, SelectEntity):
"""Define a WLED Playlist select."""
_attr_icon = "mdi:play-speed"
def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None:
"""Initialize WLED playlist."""
super().__init__(coordinator=coordinator)
self._attr_name = f"{coordinator.data.info.name} Playlist"
self._attr_unique_id = f"{coordinator.data.info.mac_address}_playlist"
self._attr_options = [
playlist.name for playlist in self.coordinator.data.playlists
]
@property
def available(self) -> bool:
"""Return True if entity is available."""
return len(self.coordinator.data.playlists) > 0 and super().available
@property
def current_option(self) -> str | None:
"""Return the currently selected playlist."""
if not isinstance(self.coordinator.data.state.playlist, Playlist):
return None
return self.coordinator.data.state.playlist.name
@wled_exception_handler
async def async_select_option(self, option: str) -> None:
"""Set WLED segment to the selected playlist."""
await self.coordinator.wled.playlist(playlist=option)
class WLEDPaletteSelect(WLEDEntity, SelectEntity):
"""Defines a WLED Palette select."""

View file

@ -17,7 +17,6 @@ from homeassistant.components.light import (
from homeassistant.components.wled.const import (
ATTR_INTENSITY,
ATTR_PALETTE,
ATTR_PLAYLIST,
ATTR_PRESET,
ATTR_REVERSE,
ATTR_SPEED,
@ -58,7 +57,6 @@ async def test_rgb_light_state(
assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant"
assert state.attributes.get(ATTR_INTENSITY) == 128
assert state.attributes.get(ATTR_PALETTE) == "Default"
assert state.attributes.get(ATTR_PLAYLIST) is None
assert state.attributes.get(ATTR_PRESET) is None
assert state.attributes.get(ATTR_REVERSE) is False
assert state.attributes.get(ATTR_SPEED) == 32
@ -77,7 +75,6 @@ async def test_rgb_light_state(
assert state.attributes.get(ATTR_ICON) == "mdi:led-strip-variant"
assert state.attributes.get(ATTR_INTENSITY) == 64
assert state.attributes.get(ATTR_PALETTE) == "Random Cycle"
assert state.attributes.get(ATTR_PLAYLIST) is None
assert state.attributes.get(ATTR_PRESET) is None
assert state.attributes.get(ATTR_REVERSE) is False
assert state.attributes.get(ATTR_SPEED) == 16

View file

@ -358,6 +358,126 @@ async def test_preset_select_connection_error(
mock_wled.preset.assert_called_with(preset="Preset 2")
async def test_playlist_unavailable_without_playlists(
hass: HomeAssistant,
init_integration: MockConfigEntry,
) -> None:
"""Test WLED playlist entity is unavailable when playlists are not available."""
state = hass.states.get("select.wled_rgb_light_playlist")
assert state
assert state.state == STATE_UNAVAILABLE
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_playlist_state(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
) -> None:
"""Test the creation and values of the WLED selects."""
entity_registry = er.async_get(hass)
state = hass.states.get("select.wled_rgbw_light_playlist")
assert state
assert state.attributes.get(ATTR_ICON) == "mdi:play-speed"
assert state.attributes.get(ATTR_OPTIONS) == ["Playlist 1", "Playlist 2"]
assert state.state == "Playlist 1"
entry = entity_registry.async_get("select.wled_rgbw_light_playlist")
assert entry
assert entry.unique_id == "aabbccddee11_playlist"
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist",
ATTR_OPTION: "Playlist 2",
},
blocking=True,
)
await hass.async_block_till_done()
assert mock_wled.playlist.call_count == 1
mock_wled.playlist.assert_called_with(playlist="Playlist 2")
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_old_style_playlist_active(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test when old style playlist cycle is active."""
# Set device playlist to 0, which meant "cycle" previously.
mock_wled.update.return_value.state.playlist = 0
async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgbw_light_playlist")
assert state
assert state.state == STATE_UNKNOWN
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_playlist_select_error(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED selects."""
mock_wled.playlist.side_effect = WLEDError
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist",
ATTR_OPTION: "Playlist 2",
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgbw_light_playlist")
assert state
assert state.state == "Playlist 1"
assert "Invalid response from API" in caplog.text
assert mock_wled.playlist.call_count == 1
mock_wled.playlist.assert_called_with(playlist="Playlist 2")
@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True)
async def test_playlist_select_connection_error(
hass: HomeAssistant,
init_integration: MockConfigEntry,
mock_wled: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error handling of the WLED selects."""
mock_wled.playlist.side_effect = WLEDConnectionError
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
{
ATTR_ENTITY_ID: "select.wled_rgbw_light_playlist",
ATTR_OPTION: "Playlist 2",
},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("select.wled_rgbw_light_playlist")
assert state
assert state.state == STATE_UNAVAILABLE
assert "Error communicating with API" in caplog.text
assert mock_wled.playlist.call_count == 1
mock_wled.playlist.assert_called_with(playlist="Playlist 2")
@pytest.mark.parametrize(
"entity_id",
(

View file

@ -4,7 +4,7 @@
"bri": 140,
"transition": 7,
"ps": 1,
"pl": -1,
"pl": 3,
"nl": {
"on": false,
"dur": 60,
@ -352,6 +352,46 @@
}
],
"n": "Preset 2"
},
"3": {
"playlist": {
"ps": [
1,
2
],
"dur": [
30,
30
],
"transition": [
7,
7
],
"repeat": 0,
"r": false,
"end": 0
},
"n": "Playlist 1"
},
"4": {
"playlist": {
"ps": [
1,
2
],
"dur": [
30,
30
],
"transition": [
7,
7
],
"repeat": 0,
"r": false,
"end": 0
},
"n": "Playlist 2"
}
}
}