"""The shared native-light model obeys power and report contracts without AL.""" # pylint: disable=protected-access import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP_KELVIN, ATTR_TRANSITION, ) from homeassistant.components.light.const import DATA_COMPONENT from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.core import Context from homeassistant.setup import async_setup_component from .support.native_lights import ( GROUP, LEFT, OFF_MEMBER, RIGHT, NativeGroup, NativeMember, RecordingGroup, RecordingMember, ) @pytest.fixture async def native_devices(hass): """Install real HA light services with no Adaptive Lighting profile.""" assert await async_setup_component(hass, "light", {}) commands = [] members = [ RecordingMember(entity_id, commands) for entity_id in (LEFT, RIGHT, OFF_MEMBER) ] group = RecordingGroup(members, commands) group._attr_is_on = False await hass.data[DATA_COMPONENT].async_add_entities([*members, group]) return group, members, commands @pytest.mark.parametrize("initially_lit", [False, True]) @pytest.mark.parametrize( ("params", "powers_members"), [ ({}, True), ({ATTR_TRANSITION: 2}, True), ({ATTR_BRIGHTNESS: 73}, True), ({ATTR_BRIGHTNESS: 73, ATTR_TRANSITION: 2}, True), ({ATTR_COLOR_TEMP_KELVIN: 3500}, False), ({ATTR_COLOR_TEMP_KELVIN: 3500, ATTR_TRANSITION: 2}, False), ], ) async def test_native_model_group_on_distinguishes_power_from_colour( hass, native_devices, initially_lit, params, powers_members, ): """Only power intent wakes OFF members; colour still updates optimistic caches.""" group, members, commands = native_devices group._attr_is_on = members[0]._attr_is_on = initially_lit members[0].async_write_ha_state() group.async_write_ha_state() await hass.async_block_till_done() await hass.services.async_call( "light", "turn_on", {ATTR_ENTITY_ID: GROUP, **params}, blocking=True, context=Context(), ) await hass.async_block_till_done() expected_power = [initially_lit or powers_members, powers_members, powers_members] assert group.is_on == any(expected_power) assert hass.states.get(GROUP).state == ( STATE_ON if any(expected_power) else STATE_OFF ) assert [member.is_on for member in members] == expected_power assert [hass.states.get(member.entity_id).state for member in members] == [ STATE_ON if enabled else STATE_OFF for enabled in expected_power ] assert [member.brightness for member in members] == [ params.get(ATTR_BRIGHTNESS, 110), ] * 3 assert [member.physical_kelvin for member in members] == [ params.get(ATTR_COLOR_TEMP_KELVIN, 4000) if enabled else 4000 for enabled in expected_power ] assert [member.color_temp_kelvin for member in members] == [ params.get(ATTR_COLOR_TEMP_KELVIN, 3000), ] * 3 assert commands == [{"target": GROUP, "service": "turn_on", **params}] assert all(not member.attempts for member in members) @pytest.mark.parametrize("hold_reports", [False, True]) async def test_native_model_group_off_preserves_attributes_and_independent_reports( hass, native_devices, hold_reports, ): """OFF is one broadcast, retaining member attributes even while reports lag.""" group, members, commands = native_devices group._attr_is_on = True for index, member in enumerate(members): member._attr_is_on = index < 2 member._attr_brightness = 50 + index member.physical_kelvin = 4100 + index * 100 member._attr_color_temp_kelvin = 3100 + index * 100 member.async_write_ha_state() group.async_write_ha_state() await hass.async_block_till_done() members[0].hold_reports = hold_reports await hass.services.async_call( "light", "turn_off", {ATTR_ENTITY_ID: GROUP, ATTR_TRANSITION: 2}, blocking=True, context=Context(), ) await hass.async_block_till_done() assert not group.is_on assert all(not member.is_on for member in members) assert hass.states.get(GROUP).state == STATE_OFF assert [hass.states.get(member.entity_id).state for member in members] == [ STATE_ON if hold_reports else STATE_OFF, STATE_OFF, STATE_OFF, ] assert [member.brightness for member in members] == [50, 51, 52] assert [member.physical_kelvin for member in members] == [4100, 4200, 4300] assert [member.color_temp_kelvin for member in members] == [3100, 3200, 3300] assert commands == [ {"target": GROUP, "service": "turn_off", ATTR_TRANSITION: 2}, ] assert all(not member.attempts for member in members) members[0].hold_reports = False members[0].async_write_ha_state() assert hass.states.get(LEFT).state == STATE_OFF @pytest.mark.parametrize("device_kind", ["member", "group"]) @pytest.mark.parametrize( ("params", "recorded"), [ ( {ATTR_COLOR_TEMP_KELVIN: 3500, "color_temp": 285}, {ATTR_COLOR_TEMP_KELVIN: 3500}, ), ( {ATTR_COLOR_TEMP_KELVIN: 3500, "color_temp": 286}, {ATTR_COLOR_TEMP_KELVIN: 3500, "color_temp": 286}, ), ({"color_temp": 285}, {"color_temp": 285}), ], ids=["matching-alias", "contradictory-alias", "alias-only"], ) async def test_native_model_trace_preserves_distinct_colour_parameters( native_devices, monkeypatch, device_kind, params, recorded, ): """Trace projection preserves raw device input and exposes nonredundant aliases.""" group, members, commands = native_devices group._attr_is_on = True for member in members: member._attr_is_on = True device = members[0] if device_kind == "member" else group model = NativeMember if device_kind == "member" else NativeGroup original = model.async_turn_on received = [] async def observe_device(self, **kwargs): received.append(kwargs) await original(self, **kwargs) monkeypatch.setattr(model, "async_turn_on", observe_device) # Exercise the device boundary directly: HA schemas forbid callers sending # both aliases, but older HA supplies both to devices after preprocessing. await device.async_turn_on(**params) assert received == [params] assert commands == [ {"target": device.entity_id, "service": "turn_on", **recorded}, ] assert members[0].physical_kelvin == params.get(ATTR_COLOR_TEMP_KELVIN, 4000)