diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index 376132c18a7..a05a0ddaf08 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -1,12 +1,13 @@ """Support for WLED switches.""" from __future__ import annotations +from functools import partial from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ENTITY_CATEGORY_CONFIG -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( @@ -31,12 +32,22 @@ async def async_setup_entry( """Set up WLED switch based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - switches = [ - WLEDNightlightSwitch(coordinator), - WLEDSyncSendSwitch(coordinator), - WLEDSyncReceiveSwitch(coordinator), - ] - async_add_entities(switches) + async_add_entities( + [ + WLEDNightlightSwitch(coordinator), + WLEDSyncSendSwitch(coordinator), + WLEDSyncReceiveSwitch(coordinator), + ] + ) + + update_segments = partial( + async_update_segments, + coordinator, + set(), + async_add_entities, + ) + coordinator.async_add_listener(update_segments) + update_segments() class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): @@ -140,3 +151,69 @@ class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the WLED sync receive switch.""" await self.coordinator.wled.sync(receive=True) + + +class WLEDReverseSwitch(WLEDEntity, SwitchEntity): + """Defines a WLED reverse effect switch.""" + + _attr_icon = "mdi:swap-horizontal-bold" + _attr_entity_category = ENTITY_CATEGORY_CONFIG + _segment: int + + def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: + """Initialize WLED reverse effect switch.""" + super().__init__(coordinator=coordinator) + + # Segment 0 uses a simpler name, which is more natural for when using + # a single segment / using WLED with one big LED strip. + self._attr_name = f"{coordinator.data.info.name} Segment {segment} Reverse" + if segment == 0: + self._attr_name = f"{coordinator.data.info.name} Reverse" + + self._attr_unique_id = f"{coordinator.data.info.mac_address}_reverse_{segment}" + self._segment = segment + + @property + def available(self) -> bool: + """Return True if entity is available.""" + try: + self.coordinator.data.state.segments[self._segment] + except IndexError: + return False + + return super().available + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return self.coordinator.data.state.segments[self._segment].reverse + + @wled_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the WLED reverse effect switch.""" + await self.coordinator.wled.segment(segment_id=self._segment, reverse=False) + + @wled_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the WLED reverse effect switch.""" + await self.coordinator.wled.segment(segment_id=self._segment, reverse=True) + + +@callback +def async_update_segments( + coordinator: WLEDDataUpdateCoordinator, + current_ids: set[int], + async_add_entities, +) -> None: + """Update segments.""" + segment_ids = {segment.segment_id for segment in coordinator.data.state.segments} + + new_entities = [] + + # Process new segments, add them to Home Assistant + for segment_id in segment_ids - current_ids: + current_ids.add(segment_id) + new_entities.append(WLEDReverseSwitch(coordinator, segment_id)) + + if new_entities: + async_add_entities(new_entities) diff --git a/tests/components/wled/fixtures/rgb.json b/tests/components/wled/fixtures/rgb.json index 20647c0f946..2f0d4d8fd12 100644 --- a/tests/components/wled/fixtures/rgb.json +++ b/tests/components/wled/fixtures/rgb.json @@ -41,7 +41,7 @@ "ix": 64, "pal": 1, "sel": true, - "rev": false, + "rev": true, "cln": -1 } ] diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 2d71126e0be..7826fe5521b 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -76,7 +76,7 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_INTENSITY) == 64 assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" assert state.attributes.get(ATTR_PRESET) is None - assert state.attributes.get(ATTR_REVERSE) is False + assert state.attributes.get(ATTR_REVERSE) is True assert state.attributes.get(ATTR_SPEED) == 16 assert state.state == STATE_ON diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 7ba86960d2b..c47d7012f6e 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,8 +1,9 @@ """Tests for the WLED switch platform.""" +import json from unittest.mock import MagicMock import pytest -from wled import WLEDConnectionError, WLEDError +from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -10,6 +11,7 @@ from homeassistant.components.wled.const import ( ATTR_FADE, ATTR_TARGET_BRIGHTNESS, ATTR_UDP_PORT, + SCAN_INTERVAL, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -23,8 +25,9 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +import homeassistant.util.dt as dt_util -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture async def test_switch_state( @@ -68,6 +71,16 @@ async def test_switch_state( assert entry.unique_id == "aabbccddeeff_sync_receive" assert entry.entity_category == ENTITY_CATEGORY_CONFIG + state = hass.states.get("switch.wled_rgb_light_reverse") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:swap-horizontal-bold" + assert state.state == STATE_OFF + + entry = entity_registry.async_get("switch.wled_rgb_light_reverse") + assert entry + assert entry.unique_id == "aabbccddeeff_reverse_0" + assert entry.entity_category == ENTITY_CATEGORY_CONFIG + async def test_switch_change_state( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock @@ -137,6 +150,26 @@ async def test_switch_change_state( assert mock_wled.sync.call_count == 4 mock_wled.sync.assert_called_with(receive=True) + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_reverse"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 1 + mock_wled.segment.assert_called_with(segment_id=0, reverse=True) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_reverse"}, + blocking=True, + ) + await hass.async_block_till_done() + assert mock_wled.segment.call_count == 2 + mock_wled.segment.assert_called_with(segment_id=0, reverse=False) + async def test_switch_error( hass: HomeAssistant, @@ -182,3 +215,45 @@ async def test_switch_connection_error( assert state assert state.state == STATE_UNAVAILABLE assert "Error communicating with API" in caplog.text + + +@pytest.mark.parametrize("mock_wled", ["wled/rgb_single_segment.json"], indirect=True) +async def test_switch_dynamically_handle_segments( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_wled: MagicMock, +) -> None: + """Test if a new/deleted segment is dynamically added/removed.""" + segment0 = hass.states.get("switch.wled_rgb_light_reverse") + segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse") + assert segment0 + assert segment0.state == STATE_OFF + assert not segment1 + + # Test adding a segment dynamically... + return_value = mock_wled.update.return_value + mock_wled.update.return_value = WLEDDevice( + json.loads(load_fixture("wled/rgb.json")) + ) + + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("switch.wled_rgb_light_reverse") + segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse") + assert segment0 + assert segment0.state == STATE_OFF + assert segment1 + assert segment1.state == STATE_ON + + # Test remove segment again... + mock_wled.update.return_value = return_value + async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) + await hass.async_block_till_done() + + segment0 = hass.states.get("switch.wled_rgb_light_reverse") + segment1 = hass.states.get("switch.wled_rgb_light_segment_1_reverse") + assert segment0 + assert segment0.state == STATE_OFF + assert segment1 + assert segment1.state == STATE_UNAVAILABLE