diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 0cb45caab87..65866935562 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 Preset from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -218,18 +217,11 @@ class WLEDSegmentLight(WLEDEntity, LightEntity): if playlist == -1: playlist = None - preset: int | None = None - if isinstance(self.coordinator.data.state.preset, Preset): - preset = self.coordinator.data.state.preset.preset_id - elif self.coordinator.data.state.preset != -1: - preset = self.coordinator.data.state.preset - segment = self.coordinator.data.state.segments[self._segment] return { ATTR_INTENSITY: segment.intensity, ATTR_PALETTE: segment.palette.name, ATTR_PLAYLIST: playlist, - ATTR_PRESET: preset, ATTR_REVERSE: segment.reverse, ATTR_SPEED: segment.speed, } diff --git a/homeassistant/components/wled/select.py b/homeassistant/components/wled/select.py index 6628334266b..845ff38b5e6 100644 --- a/homeassistant/components/wled/select.py +++ b/homeassistant/components/wled/select.py @@ -3,6 +3,8 @@ from __future__ import annotations from functools import partial +from wled import Preset + from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback @@ -23,6 +25,9 @@ async def async_setup_entry( ) -> None: """Set up WLED select based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + async_add_entities([WLEDPresetSelect(coordinator)]) + update_segments = partial( async_update_segments, coordinator, @@ -33,6 +38,37 @@ async def async_setup_entry( update_segments() +class WLEDPresetSelect(WLEDEntity, SelectEntity): + """Defined a WLED Preset select.""" + + _attr_icon = "mdi:playlist-play" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED .""" + super().__init__(coordinator=coordinator) + + self._attr_name = f"{coordinator.data.info.name} Preset" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_preset" + self._attr_options = [preset.name for preset in self.coordinator.data.presets] + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return len(self.coordinator.data.presets) > 0 and super().available + + @property + def current_option(self) -> str | None: + """Return the current selected preset.""" + if not isinstance(self.coordinator.data.state.preset, Preset): + return None + return self.coordinator.data.state.preset.name + + @wled_exception_handler + async def async_select_option(self, option: str) -> None: + """Set WLED segment to the selected preset.""" + await self.coordinator.wled.preset(preset=option) + + class WLEDPaletteSelect(WLEDEntity, SelectEntity): """Defines a WLED Palette select.""" diff --git a/tests/components/wled/test_select.py b/tests/components/wled/test_select.py index 2a0817b7c12..abdef0c2ff5 100644 --- a/tests/components/wled/test_select.py +++ b/tests/components/wled/test_select.py @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_ICON, SERVICE_SELECT_OPTION, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -44,7 +45,7 @@ async def enable_all(hass: HomeAssistant) -> None: ) -async def test_select_state( +async def test_color_palette_state( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry ) -> None: """Test the creation and values of the WLED selects.""" @@ -113,7 +114,7 @@ async def test_select_state( assert entry.unique_id == "aabbccddeeff_palette_1" -async def test_segment_change_state( +async def test_color_palette_segment_change_state( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -138,7 +139,7 @@ async def test_segment_change_state( @pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) -async def test_dynamically_handle_segments( +async def test_color_palette_dynamically_handle_segments( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -179,7 +180,7 @@ async def test_dynamically_handle_segments( assert segment1.state == STATE_UNAVAILABLE -async def test_select_error( +async def test_color_palette_select_error( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -208,7 +209,7 @@ async def test_select_error( mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") -async def test_select_connection_error( +async def test_color_palette_select_connection_error( hass: HomeAssistant, enable_all: None, init_integration: MockConfigEntry, @@ -237,6 +238,126 @@ async def test_select_connection_error( mock_wled.segment.assert_called_with(segment_id=1, palette="Whatever") +async def test_preset_unavailable_without_presets( + hass: HomeAssistant, + init_integration: MockConfigEntry, +) -> None: + """Test WLED preset entity is unavailable when presets are not available.""" + state = hass.states.get("select.wled_rgb_light_preset") + assert state + assert state.state == STATE_UNAVAILABLE + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_preset_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_preset") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:playlist-play" + assert state.attributes.get(ATTR_OPTIONS) == ["Preset 1", "Preset 2"] + assert state.state == "Preset 1" + + entry = entity_registry.async_get("select.wled_rgbw_light_preset") + assert entry + assert entry.unique_id == "aabbccddee11_preset" + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset="Preset 2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_old_style_preset_active( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test unknown preset returned (when old style/unknown) preset is active.""" + # Set device preset state to a random number + mock_wled.update.return_value.state.preset = 99 + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.state == STATE_UNKNOWN + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_preset_select_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.preset.side_effect = WLEDError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.state == "Preset 1" + assert "Invalid response from API" in caplog.text + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset="Preset 2") + + +@pytest.mark.parametrize("mock_wled", ["wled/rgbw.json"], indirect=True) +async def test_preset_select_connection_error( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling of the WLED selects.""" + mock_wled.preset.side_effect = WLEDConnectionError + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.wled_rgbw_light_preset", + ATTR_OPTION: "Preset 2", + }, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("select.wled_rgbw_light_preset") + assert state + assert state.state == STATE_UNAVAILABLE + assert "Error communicating with API" in caplog.text + assert mock_wled.preset.call_count == 1 + mock_wled.preset.assert_called_with(preset="Preset 2") + + @pytest.mark.parametrize( "entity_id", ( diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json index ce7033c5888..d5ba9e8d00c 100644 --- a/tests/fixtures/wled/rgbw.json +++ b/tests/fixtures/wled/rgbw.json @@ -3,7 +3,7 @@ "on": true, "bri": 140, "transition": 7, - "ps": -1, + "ps": 1, "pl": -1, "nl": { "on": false, @@ -200,5 +200,158 @@ "Orangery", "C9", "Sakura" - ] + ], + "presets": { + "0": {}, + "1": { + "on": false, + "bri": 255, + "transition": 7, + "mainseg": 0, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "grp": 1, + "spc": 0, + "on": true, + "bri": 255, + "col": [ + [ + 97, + 144, + 255 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ] + ], + "fx": 9, + "sx": 183, + "ix": 255, + "pal": 1, + "sel": true, + "rev": false, + "mi": false + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + } + ], + "n": "Preset 1" + }, + "2": { + "on": false, + "bri": 255, + "transition": 7, + "mainseg": 0, + "seg": [ + { + "id": 0, + "start": 0, + "stop": 13, + "grp": 1, + "spc": 0, + "on": true, + "bri": 255, + "col": [ + [ + 97, + 144, + 255 + ], + [ + 0, + 0, + 0 + ], + [ + 0, + 0, + 0 + ] + ], + "fx": 9, + "sx": 183, + "ix": 255, + "pal": 1, + "sel": true, + "rev": false, + "mi": false + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + }, + { + "stop": 0 + } + ], + "n": "Preset 2" + } + } }