diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 6d31862509b9..aa4110ea6869 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -2,16 +2,17 @@ import asyncio import json import logging -from typing import Dict, Optional +from typing import Optional import aiohttp import async_timeout from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON from homeassistant.core import State +from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util -from .const import API_CHANGE, Cause +from .const import API_CHANGE, DOMAIN, Cause from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .messages import AlexaResponse @@ -27,7 +28,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): # Validate we can get access token. await smart_home_config.async_get_access_token() - progress: Dict[str, AlexaEntity] = {} + checker = await create_checker(hass, DOMAIN) async def async_entity_state_listener( changed_entity: str, @@ -51,12 +52,6 @@ async def async_enable_proactive_mode(hass, smart_home_config): hass, smart_home_config, new_state ) - # Queue up entity to be sent later. - # If two states come in while we are reporting the state, only the last one will be reported. - if changed_entity in progress: - progress[changed_entity] = alexa_changed_entity - return - # Determine how entity should be reported on should_report = False should_doorbell = False @@ -75,51 +70,21 @@ async def async_enable_proactive_mode(hass, smart_home_config): if not should_report and not should_doorbell: return + if not checker.async_is_significant_change(new_state): + return + if should_doorbell: should_report = False - # Store current state change information - last_state: Optional[AlexaEntity] = None - if old_state: - last_state = ENTITY_ADAPTERS[old_state.domain]( - hass, smart_home_config, old_state + if should_report: + await async_send_changereport_message( + hass, smart_home_config, alexa_changed_entity ) - progress[changed_entity] = alexa_changed_entity - # Start reporting on entity. Keep reporting as long as new states come in - # while we were reporting a state. - while last_state != progress[changed_entity]: - to_report = progress[changed_entity] - alexa_properties = None - - if should_report: - # this sends all the properties of the Alexa Entity, whether they have - # changed or not. this should be improved, and properties that have not - # changed should be moved to the 'context' object - alexa_properties = list(alexa_changed_entity.serialize_properties()) - - if last_state and last_state.entity.state == to_report.entity.state: - old_alexa_properties = list(last_state.serialize_properties()) - if old_alexa_properties == alexa_properties: - return - - try: - if should_report: - await async_send_changereport_message( - hass, smart_home_config, alexa_changed_entity, alexa_properties - ) - - elif should_doorbell: - await async_send_doorbell_event_message( - hass, smart_home_config, alexa_changed_entity - ) - except Exception: - progress.pop(changed_entity) - raise - - last_state = to_report - - progress.pop(changed_entity) + elif should_doorbell: + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -127,7 +92,7 @@ async def async_enable_proactive_mode(hass, smart_home_config): async def async_send_changereport_message( - hass, config, alexa_entity, properties, *, invalidate_access_token=True + hass, config, alexa_entity, *, invalidate_access_token=True ): """Send a ChangeReport message for an Alexa entity. @@ -140,7 +105,10 @@ async def async_send_changereport_message( endpoint = alexa_entity.alexa_id() payload = { - API_CHANGE: {"cause": {"type": Cause.APP_INTERACTION}, "properties": properties} + API_CHANGE: { + "cause": {"type": Cause.APP_INTERACTION}, + "properties": list(alexa_entity.serialize_properties()), + } } message = AlexaResponse(name="ChangeReport", namespace="Alexa", payload=payload) @@ -178,7 +146,7 @@ async def async_send_changereport_message( ): config.async_invalidate_access_token() return await async_send_changereport_message( - hass, config, alexa_entity, properties, invalidate_access_token=False + hass, config, alexa_entity, invalidate_access_token=False ) _LOGGER.error( diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index afa024ce89b1..809bca5638b2 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -1,6 +1,5 @@ """Test report state.""" -import asyncio -from unittest.mock import Mock, patch +from unittest.mock import patch from homeassistant import core from homeassistant.components.alexa import state_report @@ -179,23 +178,24 @@ async def test_doorbell_event(hass, aioclient_mock): async def test_proactive_mode_filter_states(hass, aioclient_mock): """Test all the cases that filter states.""" - hass.states.async_set( - "binary_sensor.test_contact", - "on", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) - await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) - # Force update should not report + # First state should report + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + assert len(aioclient_mock.mock_calls) == 0 + + aioclient_mock.clear_requests() + + # Second one shouldn't hass.states.async_set( "binary_sensor.test_contact", "on", {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - force_update=True, ) - await hass.async_block_till_done() - await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 # hass not running should not report @@ -238,78 +238,3 @@ async def test_proactive_mode_filter_states(hass, aioclient_mock): await hass.async_block_till_done() await hass.async_block_till_done() assert len(aioclient_mock.mock_calls) == 0 - - -async def test_proactive_mode_filter_in_progress(hass, aioclient_mock): - """When in progress, queue up state.""" - hass.states.async_set( - "binary_sensor.test_contact", - "off", - {"friendly_name": "Test Contact Sensor", "device_class": "door"}, - ) - - await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) - - # Progress should filter out the 2nd event. - long_sendchange = asyncio.Event() - - with patch( - "homeassistant.components.alexa.state_report.async_send_changereport_message", - Mock(side_effect=lambda *args: long_sendchange.wait()), - ) as mock_report: - hass.states.async_set( - "binary_sensor.test_contact", - "on", - { - "friendly_name": "Test Contact Sensor", - "device_class": "door", - "update": 1, - }, - ) - - await asyncio.sleep(0) - await asyncio.sleep(0) - await asyncio.sleep(0) - - assert len(mock_report.mock_calls) == 1 - - with patch( - "homeassistant.components.alexa.state_report.async_send_changereport_message", - ) as mock_report_2: - hass.states.async_set( - "binary_sensor.test_contact", - "off", - { - "friendly_name": "Test Contact Sensor", - "device_class": "door", - "update": 2, - }, - ) - hass.states.async_set( - "binary_sensor.test_contact", - "on", - { - "friendly_name": "Test Contact Sensor", - "device_class": "door", - "update": 3, - }, - ) - hass.states.async_set( - "binary_sensor.test_contact", - "off", - { - "friendly_name": "Test Contact Sensor", - "device_class": "door", - "update": 4, - }, - ) - - await asyncio.sleep(0) - await asyncio.sleep(0) - await asyncio.sleep(0) - long_sendchange.set() - await hass.async_block_till_done() - - # Should be 1 because the 4rd state change - assert len(mock_report_2.mock_calls) == 1 - mock_report_2.mock_calls[0][1][2].entity.attributes["update"] == 4