"""Test cases that are in common among wemo platform modules. This is not a test module. These test methods are used by the platform test modules. """ import asyncio import threading from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN from homeassistant.components.wemo import wemo_device from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component def _perform_registry_callback(coordinator): """Return a callable method to trigger a state callback from the device.""" async def async_callback(): await coordinator.hass.async_add_executor_job( coordinator.subscription_callback, coordinator.wemo, "", "" ) return async_callback def _perform_async_update(coordinator): """Return a callable method to cause hass to update the state of the entity.""" async def async_callback(): await coordinator._async_update_data() return async_callback async def _async_multiple_call_helper(hass, pywemo_device, call1, call2): """Create two calls (call1 & call2) in parallel; verify only one polls the device. There should only be one poll on the device at a time. Any parallel updates # that happen at the same time should be ignored. This is verified by blocking in the get_state method. The polling method should only be called once as a result of calling call1 & call2 simultaneously. """ event = threading.Event() waiting = asyncio.Event() call_count = 0 def get_state(force_update=None): if force_update is None: return nonlocal call_count call_count += 1 hass.loop.call_soon_threadsafe(waiting.set) event.wait() # Danger! Do not use a Mock side_effect here. The test will deadlock. When # called though hass.async_add_executor_job, Mock objects !surprisingly! # run in the same thread as the asyncio event loop. # https://github.com/home-assistant/core/blob/1ba5c1c9fb1e380549cb655986b5f4d3873d7352/tests/common.py#L179 pywemo_device.get_state = get_state # One of these two calls will block on `event`. The other will return right # away because the `_update_lock` is held. done, pending = await asyncio.wait( [asyncio.create_task(call1()), asyncio.create_task(call2())], return_when=asyncio.FIRST_COMPLETED, ) _ = [d.result() for d in done] # Allow any exceptions to be raised. # Allow the blocked call to return. await waiting.wait() event.set() if pending: done, _ = await asyncio.wait(pending) _ = [d.result() for d in done] # Allow any exceptions to be raised. # Make sure the state update only happened once. assert call_count == 1 async def test_async_update_locked_callback_and_update( hass: HomeAssistant, pywemo_device, wemo_entity ) -> None: """Test that a callback and a state update request can't both happen at the same time. When a state update is received via a callback from the device at the same time as hass is calling `async_update`, verify that only one of the updates proceeds. """ coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id) await async_setup_component(hass, HA_DOMAIN, {}) callback = _perform_registry_callback(coordinator) update = _perform_async_update(coordinator) await _async_multiple_call_helper(hass, pywemo_device, callback, update) async def test_async_update_locked_multiple_updates( hass: HomeAssistant, pywemo_device, wemo_entity ) -> None: """Test that two hass async_update state updates do not proceed at the same time.""" coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id) await async_setup_component(hass, HA_DOMAIN, {}) update = _perform_async_update(coordinator) await _async_multiple_call_helper(hass, pywemo_device, update, update) async def test_async_update_locked_multiple_callbacks( hass: HomeAssistant, pywemo_device, wemo_entity ) -> None: """Test that two device callback state updates do not proceed at the same time.""" coordinator = wemo_device.async_get_coordinator(hass, wemo_entity.device_id) await async_setup_component(hass, HA_DOMAIN, {}) callback = _perform_registry_callback(coordinator) await _async_multiple_call_helper(hass, pywemo_device, callback, callback) async def test_avaliable_after_update( hass: HomeAssistant, pywemo_registry, pywemo_device, wemo_entity, domain ) -> None: """Test the avaliability when an On call fails and after an update. This test expects that the pywemo_device Mock has been setup to raise an ActionException when the SERVICE_TURN_ON method is called and that the state will be On after the update. """ await hass.services.async_call( domain, SERVICE_TURN_ON, {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, blocking=True, ) assert hass.states.get(wemo_entity.entity_id).state == STATE_UNAVAILABLE pywemo_registry.callbacks[pywemo_device.name](pywemo_device, "", "") await hass.async_block_till_done() assert hass.states.get(wemo_entity.entity_id).state == STATE_ON async def test_turn_off_state(hass: HomeAssistant, wemo_entity, domain) -> None: """Test that the device state is updated after turning off.""" await hass.services.async_call( domain, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: [wemo_entity.entity_id]}, blocking=True, ) assert hass.states.get(wemo_entity.entity_id).state == STATE_OFF class EntityTestHelpers: """Common state update helpers.""" async def test_async_update_locked_multiple_updates( self, hass, pywemo_device, wemo_entity ): """Test that two hass async_update state updates do not proceed at the same time.""" await test_async_update_locked_multiple_updates( hass, pywemo_device, wemo_entity ) async def test_async_update_locked_multiple_callbacks( self, hass, pywemo_device, wemo_entity ): """Test that two device callback state updates do not proceed at the same time.""" await test_async_update_locked_multiple_callbacks( hass, pywemo_device, wemo_entity ) async def test_async_update_locked_callback_and_update( self, hass, pywemo_device, wemo_entity ): """Test that a callback and a state update request can't both happen at the same time. When a state update is received via a callback from the device at the same time as hass is calling `async_update`, verify that only one of the updates proceeds. """ await test_async_update_locked_callback_and_update( hass, pywemo_device, wemo_entity )