"""The light entity.""" from __future__ import annotations import asyncio from copy import deepcopy from datetime import timedelta from typing import Any from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, ATTR_COLOR_TEMP_KELVIN, ATTR_MAX_COLOR_TEMP_KELVIN, ATTR_MIN_COLOR_TEMP_KELVIN, ATTR_RGB_COLOR, ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ColorMode, ) from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( ATTR_ENTITY_ID, CONF_HOST, CONF_TOKEN, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from pytest_homeassistant_custom_component.common import MockConfigEntry, async_fire_time_changed from custom_components.ninlight.api import NinlightConnectionError from custom_components.ninlight.const import DOMAIN from custom_components.ninlight.light import ( ha_to_lamp_brightness, lamp_to_ha_brightness, transition_ms, ) from tests.helpers import entity_id_for from tests_unit.samples import ( HOST_CW, HOST_RGB, INFO_CW, MAC_CW, MAC_RGB, STATE_CW, STATE_RGB, TOKEN, ) def test_brightness_never_zero_and_round_trips() -> None: for ha_value in range(1, 256): assert 1 <= ha_to_lamp_brightness(ha_value) <= 100, ha_value for lamp_value in range(1, 101): assert 1 <= lamp_to_ha_brightness(lamp_value) <= 255, lamp_value assert ha_to_lamp_brightness(lamp_to_ha_brightness(lamp_value)) == lamp_value, lamp_value def test_transition_ms_rounds_and_clamps() -> None: assert transition_ms(2.5) == 2500 assert transition_ms(0.0014) == 1 assert transition_ms(-1) == 0 assert transition_ms(90) == 60000 async def test_state_attributes_of_color_lamp(hass: HomeAssistant, init_integration) -> None: state = hass.states.get(entity_id_for(hass, "light", "light")) assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 94 # lamp 37 % assert state.attributes[ATTR_COLOR_MODE] == ColorMode.RGB assert state.attributes[ATTR_RGB_COLOR] == (12, 200, 90) assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == { ColorMode.RGB, ColorMode.COLOR_TEMP, } assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2700 assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 6500 async def test_white_lamp_offers_only_color_temp( hass: HomeAssistant, mock_client: MagicMock, lamp_state: dict[str, Any], lamp_info: dict[str, Any], ) -> None: lamp_state.clear() lamp_state.update(deepcopy(STATE_CW)) lamp_info.clear() lamp_info.update(deepcopy(INFO_CW)) entry = MockConfigEntry( domain=DOMAIN, unique_id=MAC_CW, data={CONF_HOST: HOST_CW, CONF_TOKEN: TOKEN} ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(entity_id_for(hass, "light", "light", mac=MAC_CW)) assert state.state == STATE_OFF assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.COLOR_TEMP] assert state.attributes[ATTR_MIN_COLOR_TEMP_KELVIN] == 2000 assert state.attributes[ATTR_MAX_COLOR_TEMP_KELVIN] == 5000 async def test_shield_without_ct_range_offers_only_rgb( hass: HomeAssistant, mock_client: MagicMock, lamp_state: dict[str, Any], ) -> None: """Das Montana-Black-Shield steuert analoge RGB-Streifen und hat keinen eigenen Weisskanal: sein Board meldet ct_min == ct_max (nl_board.c, "ledshield_rgb", 4000/4000) - die Konvention fuer "weiss entsteht als RGB-Mischung". Ein Bereich ohne Spielraum ist kein Farbtemperaturbereich; HA bekaeme sonst einen Regler mit min == max. """ lamp_state.clear() lamp_state.update(deepcopy(STATE_RGB)) lamp_state["capabilities"] = {"color": True, "color_temp": [4000, 4000]} entry = MockConfigEntry( domain=DOMAIN, unique_id=MAC_RGB, data={CONF_HOST: HOST_RGB, CONF_TOKEN: TOKEN} ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() state = hass.states.get(entity_id_for(hass, "light", "light")) assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ColorMode.RGB] async def test_turn_on_sends_everything_in_one_request( hass: HomeAssistant, init_integration, mock_client: MagicMock ) -> None: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: entity_id_for(hass, "light", "light"), ATTR_BRIGHTNESS: 200, ATTR_COLOR_TEMP_KELVIN: 4100, ATTR_TRANSITION: 2.5, }, blocking=True, ) mock_client.set_state.assert_awaited_once_with( on=True, brightness=78, color_temp=4100, transition=2500 ) async def test_turn_on_with_rgb_and_without_transition( hass: HomeAssistant, init_integration, mock_client: MagicMock ) -> None: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id_for(hass, "light", "light"), ATTR_RGB_COLOR: (40, 10, 220)}, blocking=True, ) mock_client.set_state.assert_awaited_once_with(on=True, color={"r": 40, "g": 10, "b": 220}) async def test_turn_off_clamps_transition( hass: HomeAssistant, init_integration, mock_client: MagicMock ) -> None: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id_for(hass, "light", "light"), ATTR_TRANSITION: 90}, blocking=True, ) mock_client.set_state.assert_awaited_once_with(on=False, transition=60000) async def test_write_adopts_lamp_answer_without_reading_again( hass: HomeAssistant, init_integration, mock_client: MagicMock ) -> None: async def lamp_clamps(**fields: Any) -> dict[str, Any]: return {**deepcopy(STATE_RGB), "mode": "white", "brightness": 55} mock_client.set_state.side_effect = lamp_clamps reads_before = mock_client.get_state.await_count entity_id = entity_id_for(hass, "light", "light") await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 200}, blocking=True, ) state = hass.states.get(entity_id) assert state.attributes[ATTR_BRIGHTNESS] == 140 # the lamp's 55 %, not the requested 78 % assert state.attributes[ATTR_COLOR_MODE] == ColorMode.COLOR_TEMP assert mock_client.get_state.await_count == reads_before async def test_requests_to_one_lamp_never_overlap( hass: HomeAssistant, init_integration, mock_client: MagicMock, lamp_state: dict[str, Any] ) -> None: active = 0 peak = 0 async def slow(**fields: Any) -> dict[str, Any]: nonlocal active, peak active += 1 peak = max(peak, active) await asyncio.sleep(0.01) active -= 1 lamp_state.update(fields) return deepcopy(lamp_state) mock_client.set_state.side_effect = slow entity_id = entity_id_for(hass, "light", "light") await asyncio.gather( *( hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: value}, blocking=True, ) for value in (60, 120, 180) ) ) assert mock_client.set_state.await_count == 3 assert peak == 1 async def test_unreachable_lamp_becomes_unavailable_and_recovers( hass: HomeAssistant, init_integration, mock_client: MagicMock, freezer: FrozenDateTimeFactory ) -> None: entity_id = entity_id_for(hass, "light", "light") mock_client.get_state.side_effect = NinlightConnectionError("down") for _ in range(3): freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE mock_client.get_state.side_effect = lambda: deepcopy(STATE_RGB) freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON