"""Coordinator behaviour that is not specific to one platform.""" from __future__ import annotations from copy import deepcopy from datetime import timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError import pytest from pytest_homeassistant_custom_component.common import async_fire_time_changed from custom_components.ninlight.api import ( NinlightBusyError, NinlightConnectionError, NinlightRequestError, ) from tests.helpers import entity_id_for from tests_unit.samples import STATE_RGB @pytest.mark.parametrize( ("error", "key", "placeholders"), [ ( NinlightRequestError(400, "invalid brightness"), "request_failed", {"message": "invalid brightness"}, ), (NinlightBusyError(409, "an update is already running"), "update_busy", None), (NinlightConnectionError("reset"), "cannot_connect", None), ], ) async def test_write_errors_are_translated( hass: HomeAssistant, init_integration, mock_client: MagicMock, error, key, placeholders ) -> None: mock_client.set_state.side_effect = error with pytest.raises(HomeAssistantError) as raised: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id_for(hass, "light", "light")}, blocking=True, ) assert raised.value.translation_key == key assert raised.value.translation_placeholders == placeholders async def test_first_and_second_consecutive_poll_failures_keep_lamp_available( 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") reads = mock_client.get_state.await_count freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() assert mock_client.get_state.await_count == reads + 1 assert hass.states.get(entity_id).state == STATE_ON freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() assert mock_client.get_state.await_count == reads + 2 assert hass.states.get(entity_id).state == STATE_ON async def test_third_consecutive_poll_failure_makes_lamp_unavailable( 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 async def test_successful_poll_resets_failure_counter( 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") # Two failures tolerated. for _ in range(2): 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 # A successful poll resets the counter. 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 # Two more failures in a row must still be tolerated. mock_client.get_state.side_effect = NinlightConnectionError("down") for _ in range(2): 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 async def test_turn_on_reaches_lamp_during_a_tolerated_poll_failure( 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") 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 await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True, ) mock_client.set_state.assert_awaited_with(on=True) async def test_successful_write_makes_unavailable_lamp_available_again( 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 await init_integration.runtime_data.async_set_state(on=True) await hass.async_block_till_done() assert hass.states.get(entity_id).state != STATE_UNAVAILABLE # The counter was reset by the write: one more failure must still be tolerated. 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 async def test_polling_pauses_while_an_update_runs( hass: HomeAssistant, init_integration, mock_client: MagicMock, freezer: FrozenDateTimeFactory ) -> None: init_integration.runtime_data.ota_in_progress = True mock_client.get_state.side_effect = NinlightConnectionError("rebooting") reads = mock_client.get_state.await_count freezer.tick(timedelta(seconds=31)) async_fire_time_changed(hass) await hass.async_block_till_done() assert mock_client.get_state.await_count == reads assert hass.states.get(entity_id_for(hass, "light", "light")).state == STATE_ON