Add WLED playlist support (#53381)
Co-authored-by: Anders Melchiorsen <amelchio@nogoto.net>
This commit is contained in:
parent
3a5347f69e
commit
01c8114e93
6 changed files with 196 additions and 16 deletions
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
(
|
||||
|
|
42
tests/fixtures/wled/rgbw.json
vendored
42
tests/fixtures/wled/rgbw.json
vendored
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue