diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 765de468350..180ef89c1b7 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -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" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 403aa5e7368..2081208e398 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -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, } diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 373565b7ef7..f8473a8f26d 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -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.""" diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 14166e1956e..2d71126e0be 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -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 diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index abdef0c2ff5..dbc1bf7c970 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -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", ( diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json index d5ba9e8d00c..824612613b1 100644 --- a/tests/fixtures/wled/rgbw.json +++ b/tests/fixtures/wled/rgbw.json @@ -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" } } }