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_MAX_POWER = "max_power"
ATTR_ON = "on" ATTR_ON = "on"
ATTR_PALETTE = "palette" ATTR_PALETTE = "palette"
ATTR_PLAYLIST = "playlist"
ATTR_PRESET = "preset" ATTR_PRESET = "preset"
ATTR_REVERSE = "reverse" ATTR_REVERSE = "reverse"
ATTR_SEGMENT_ID = "segment_id" ATTR_SEGMENT_ID = "segment_id"

View File

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

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from functools import partial from functools import partial
from wled import Preset from wled import Playlist, Preset
from homeassistant.components.select import SelectEntity from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -26,7 +26,7 @@ async def async_setup_entry(
"""Set up WLED select based on a config entry.""" """Set up WLED select based on a config entry."""
coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([WLEDPresetSelect(coordinator)]) async_add_entities([WLEDPlaylistSelect(coordinator), WLEDPresetSelect(coordinator)])
update_segments = partial( update_segments = partial(
async_update_segments, async_update_segments,
@ -69,6 +69,39 @@ class WLEDPresetSelect(WLEDEntity, SelectEntity):
await self.coordinator.wled.preset(preset=option) 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): class WLEDPaletteSelect(WLEDEntity, SelectEntity):
"""Defines a WLED Palette select.""" """Defines a WLED Palette select."""

View File

@ -17,7 +17,6 @@ from homeassistant.components.light import (
from homeassistant.components.wled.const import ( from homeassistant.components.wled.const import (
ATTR_INTENSITY, ATTR_INTENSITY,
ATTR_PALETTE, ATTR_PALETTE,
ATTR_PLAYLIST,
ATTR_PRESET, ATTR_PRESET,
ATTR_REVERSE, ATTR_REVERSE,
ATTR_SPEED, 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_ICON) == "mdi:led-strip-variant"
assert state.attributes.get(ATTR_INTENSITY) == 128 assert state.attributes.get(ATTR_INTENSITY) == 128
assert state.attributes.get(ATTR_PALETTE) == "Default" 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_PRESET) is None
assert state.attributes.get(ATTR_REVERSE) is False assert state.attributes.get(ATTR_REVERSE) is False
assert state.attributes.get(ATTR_SPEED) == 32 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_ICON) == "mdi:led-strip-variant"
assert state.attributes.get(ATTR_INTENSITY) == 64 assert state.attributes.get(ATTR_INTENSITY) == 64
assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" 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_PRESET) is None
assert state.attributes.get(ATTR_REVERSE) is False assert state.attributes.get(ATTR_REVERSE) is False
assert state.attributes.get(ATTR_SPEED) == 16 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") 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( @pytest.mark.parametrize(
"entity_id", "entity_id",
( (

View File

@ -4,7 +4,7 @@
"bri": 140, "bri": 140,
"transition": 7, "transition": 7,
"ps": 1, "ps": 1,
"pl": -1, "pl": 3,
"nl": { "nl": {
"on": false, "on": false,
"dur": 60, "dur": 60,
@ -352,6 +352,46 @@
} }
], ],
"n": "Preset 2" "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"
} }
} }
} }