"""Tests for the WLED light platform.""" from collections.abc import Generator from unittest.mock import MagicMock, patch from freezegun.api import FrozenDateTimeFactory import pytest from syrupy.assertion import SnapshotAssertion from wled import Device as WLEDDevice, WLEDConnectionError, WLEDError from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP_KELVIN, ATTR_EFFECT, ATTR_MAX_COLOR_TEMP_KELVIN, ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, DOMAIN as LIGHT_DOMAIN, ColorMode, ) from homeassistant.components.wled.const import ( CONF_KEEP_MAIN_LIGHT, DOMAIN, SCAN_INTERVAL, ) from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er from tests.common import ( MockConfigEntry, async_fire_time_changed, async_load_json_object_fixture, snapshot_platform, ) pytestmark = pytest.mark.usefixtures("init_integration") @pytest.fixture(autouse=True) def override_platforms() -> Generator[None]: """Override PLATFORMS.""" with patch("homeassistant.components.wled.PLATFORMS", [Platform.LIGHT]): yield @pytest.mark.parametrize( "device_fixture", ["cct", "rgb_single_segment", "rgb", "rgb_websocket", "rgbw"] ) async def test_snapshots( hass: HomeAssistant, entity_registry: er.EntityRegistry, snapshot: SnapshotAssertion, mock_config_entry: MockConfigEntry, ) -> None: """Test snapshots of the platform.""" await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) async def test_segment_change_state( hass: HomeAssistant, mock_wled: MagicMock, ) -> None: """Test the change of state of the WLED segments.""" await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, blocking=True, ) assert mock_wled.segment.call_count == 1 mock_wled.segment.assert_called_with( on=False, segment_id=0, transition=50, ) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_BRIGHTNESS: 42, ATTR_EFFECT: "Chase", ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_RGB_COLOR: [255, 0, 0], ATTR_TRANSITION: 5, }, blocking=True, ) assert mock_wled.segment.call_count == 2 mock_wled.segment.assert_called_with( brightness=42, color_primary=(255, 0, 0), effect="Chase", on=True, segment_id=0, transition=50, ) async def test_main_change_state( hass: HomeAssistant, mock_wled: MagicMock, ) -> None: """Test the change of state of the WLED main light control.""" await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.wled_rgb_light_main", ATTR_TRANSITION: 5}, blocking=True, ) assert mock_wled.master.call_count == 1 mock_wled.master.assert_called_with( on=False, transition=50, ) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_BRIGHTNESS: 42, ATTR_ENTITY_ID: "light.wled_rgb_light_main", ATTR_TRANSITION: 5, }, blocking=True, ) assert mock_wled.master.call_count == 2 mock_wled.master.assert_called_with( brightness=42, on=True, transition=50, ) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.wled_rgb_light_main", ATTR_TRANSITION: 5}, blocking=True, ) assert mock_wled.master.call_count == 3 mock_wled.master.assert_called_with( on=False, transition=50, ) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_BRIGHTNESS: 42, ATTR_ENTITY_ID: "light.wled_rgb_light_main", ATTR_TRANSITION: 5, }, blocking=True, ) assert mock_wled.master.call_count == 4 mock_wled.master.assert_called_with( brightness=42, on=True, transition=50, ) @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_dynamically_handle_segments( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test if a new/deleted segment is dynamically added/removed.""" assert (segment0 := hass.states.get("light.wled_rgb_light")) assert segment0.state == STATE_ON assert not hass.states.get("light.wled_rgb_light_main") assert not hass.states.get("light.wled_rgb_light_segment_1") return_value = mock_wled.update.return_value mock_wled.update.return_value = WLEDDevice.from_dict( await async_load_json_object_fixture(hass, "rgb.json", DOMAIN) ) freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() assert (main := hass.states.get("light.wled_rgb_light_main")) assert main.state == STATE_ON assert (segment0 := hass.states.get("light.wled_rgb_light")) assert segment0.state == STATE_ON assert (segment1 := hass.states.get("light.wled_rgb_light_segment_1")) assert segment1.state == STATE_ON # Test adding if segment shows up again, including the main entity mock_wled.update.return_value = return_value freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() assert (main := hass.states.get("light.wled_rgb_light_main")) assert main.state == STATE_UNAVAILABLE assert (segment0 := hass.states.get("light.wled_rgb_light")) assert segment0.state == STATE_ON assert (segment1 := hass.states.get("light.wled_rgb_light_segment_1")) assert segment1.state == STATE_UNAVAILABLE @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_single_segment_behavior( hass: HomeAssistant, freezer: FrozenDateTimeFactory, mock_wled: MagicMock, ) -> None: """Test the behavior of the integration with a single segment.""" device = mock_wled.update.return_value assert not hass.states.get("light.wled_rgb_light_main") assert (state := hass.states.get("light.wled_rgb_light")) assert state.state == STATE_ON # Test segment brightness takes main into account device.state.brightness = 100 device.state.segments[0].brightness = 255 freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() assert (state := hass.states.get("light.wled_rgb_light")) assert state.attributes.get(ATTR_BRIGHTNESS) == 100 # Test segment is off when main is off device.state.on = False freezer.tick(SCAN_INTERVAL) async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get("light.wled_rgb_light") assert state assert state.state == STATE_OFF # Test main is turned off when turning off a single segment await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, blocking=True, ) assert mock_wled.master.call_count == 1 mock_wled.master.assert_called_with( on=False, transition=50, ) # Test main is turned on when turning on a single segment, and segment # brightness is set to 255. await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5, ATTR_BRIGHTNESS: 42, }, blocking=True, ) assert mock_wled.segment.call_count == 1 assert mock_wled.master.call_count == 2 mock_wled.segment.assert_called_with(on=True, segment_id=0, brightness=255) mock_wled.master.assert_called_with(on=True, transition=50, brightness=42) @pytest.mark.parametrize( ("side_effect", "expected_state", "expected_translation_key"), [ (WLEDError, STATE_ON, "invalid_response_wled_error"), (WLEDConnectionError, STATE_UNAVAILABLE, "connection_error"), ], ) async def test_light_errors( hass: HomeAssistant, mock_wled: MagicMock, side_effect: Exception, expected_state: str, expected_translation_key: str, ) -> None: """Test error handling of the WLED lights.""" mock_wled.segment.side_effect = side_effect with pytest.raises(HomeAssistantError) as ex: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "light.wled_rgb_light"}, blocking=True, ) assert ex.value.translation_domain == DOMAIN assert ex.value.translation_key == expected_translation_key assert (state := hass.states.get("light.wled_rgb_light")) assert state.state == expected_state assert mock_wled.segment.call_count == 1 mock_wled.segment.assert_called_with(on=False, segment_id=0, transition=None) @pytest.mark.parametrize("device_fixture", ["rgbw"]) async def test_rgbw_light(hass: HomeAssistant, mock_wled: MagicMock) -> None: """Test RGBW support for WLED.""" assert (state := hass.states.get("light.wled_rgbw_light")) assert state.state == STATE_ON assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ColorMode.RGBW] assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.RGBW assert state.attributes.get(ATTR_RGBW_COLOR) == (255, 0, 0, 139) await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_RGBW_COLOR: (255, 255, 255, 255), }, blocking=True, ) assert mock_wled.segment.call_count == 1 mock_wled.segment.assert_called_with( color_primary=(255, 255, 255, 255), on=True, segment_id=0, ) @pytest.mark.parametrize("device_fixture", ["rgb_single_segment"]) async def test_single_segment_with_keep_main_light( hass: HomeAssistant, init_integration: MockConfigEntry, mock_wled: MagicMock, ) -> None: """Test the behavior of the integration with a single segment.""" assert not hass.states.get("light.wled_rgb_light_main") hass.config_entries.async_update_entry( init_integration, options={CONF_KEEP_MAIN_LIGHT: True} ) await hass.config_entries.async_reload(init_integration.entry_id) await hass.async_block_till_done() assert (state := hass.states.get("light.wled_rgb_light_main")) assert state.state == STATE_ON @pytest.mark.parametrize("device_fixture", ["cct"]) async def test_cct_light(hass: HomeAssistant, mock_wled: MagicMock) -> None: """Test CCT support for WLED.""" assert (state := hass.states.get("light.wled_cct_light")) assert state.state == STATE_ON assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ ColorMode.COLOR_TEMP, ColorMode.RGBW, ] assert state.attributes.get(ATTR_COLOR_MODE) == ColorMode.COLOR_TEMP assert state.attributes.get(ATTR_MIN_COLOR_TEMP_KELVIN) == 2000 assert state.attributes.get(ATTR_MAX_COLOR_TEMP_KELVIN) == 6535 assert state.attributes.get(ATTR_COLOR_TEMP_KELVIN) == 2942 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.wled_cct_light", ATTR_COLOR_TEMP_KELVIN: 4321, }, blocking=True, ) assert mock_wled.segment.call_count == 1 mock_wled.segment.assert_called_with( cct=130, on=True, segment_id=0, )