From 629c68221e1756635e202242fffe51d77dd2c61a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 28 Jun 2022 19:54:27 -0500 Subject: [PATCH] Avoid retriggering HomeKit doorbells on forced updates (#74141) --- .../components/homekit/type_cameras.py | 14 +++-- homeassistant/components/homekit/util.py | 10 +++- tests/components/homekit/test_type_cameras.py | 55 ++++++++++++++++++- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index 3a09e3a48ff..e612c8248be 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -14,7 +14,7 @@ from pyhap.const import CATEGORY_CAMERA from homeassistant.components import camera from homeassistant.components.ffmpeg import get_ffmpeg_manager from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.core import Event, callback from homeassistant.helpers.event import ( async_track_state_change_event, async_track_time_interval, @@ -56,7 +56,7 @@ from .const import ( SERV_SPEAKER, SERV_STATELESS_PROGRAMMABLE_SWITCH, ) -from .util import pid_is_alive +from .util import pid_is_alive, state_changed_event_is_same_state _LOGGER = logging.getLogger(__name__) @@ -265,9 +265,10 @@ class Camera(HomeAccessory, PyhapCamera): await super().run() @callback - def _async_update_motion_state_event(self, event): + def _async_update_motion_state_event(self, event: Event) -> None: """Handle state change event listener callback.""" - self._async_update_motion_state(event.data.get("new_state")) + if not state_changed_event_is_same_state(event): + self._async_update_motion_state(event.data.get("new_state")) @callback def _async_update_motion_state(self, new_state): @@ -288,9 +289,10 @@ class Camera(HomeAccessory, PyhapCamera): ) @callback - def _async_update_doorbell_state_event(self, event): + def _async_update_doorbell_state_event(self, event: Event) -> None: """Handle state change event listener callback.""" - self._async_update_doorbell_state(event.data.get("new_state")) + if not state_changed_event_is_same_state(event): + self._async_update_doorbell_state(event.data.get("new_state")) @callback def _async_update_doorbell_state(self, new_state): diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8b010f85fb6..34df1008e76 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -37,7 +37,7 @@ from homeassistant.const import ( CONF_TYPE, TEMP_CELSIUS, ) -from homeassistant.core import HomeAssistant, State, callback, split_entity_id +from homeassistant.core import Event, HomeAssistant, State, callback, split_entity_id import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR import homeassistant.util.temperature as temp_util @@ -572,3 +572,11 @@ def state_needs_accessory_mode(state: State) -> bool: and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & RemoteEntityFeature.ACTIVITY ) + + +def state_changed_event_is_same_state(event: Event) -> bool: + """Check if a state changed event is the same state.""" + event_data = event.data + old_state: State | None = event_data.get("old_state") + new_state: State | None = event_data.get("new_state") + return bool(new_state and old_state and new_state.state == old_state.state) diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 9a8b284b97e..83afcedd839 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -642,11 +642,15 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): assert char assert char.value is True + broker = MagicMock() + char.broker = broker hass.states.async_set( motion_entity_id, STATE_OFF, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() + assert len(broker.mock_calls) == 2 + broker.reset_mock() assert char.value is False char.set_value(True) @@ -654,8 +658,28 @@ async def test_camera_with_linked_motion_sensor(hass, run_driver, events): motion_entity_id, STATE_ON, {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION} ) await hass.async_block_till_done() + assert len(broker.mock_calls) == 2 + broker.reset_mock() assert char.value is True + hass.states.async_set( + motion_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION}, + force_update=True, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + motion_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.MOTION, "other": "attr"}, + ) + await hass.async_block_till_done() + assert len(broker.mock_calls) == 0 + broker.reset_mock() # Ensure we do not throw when the linked # motion sensor is removed hass.states.async_remove(motion_entity_id) @@ -747,7 +771,8 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): assert service2 char2 = service.get_characteristic(CHAR_PROGRAMMABLE_SWITCH_EVENT) assert char2 - + broker = MagicMock() + char2.broker = broker assert char2.value is None hass.states.async_set( @@ -758,9 +783,12 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await hass.async_block_till_done() assert char.value is None assert char2.value is None + assert len(broker.mock_calls) == 0 char.set_value(True) char2.set_value(True) + broker.reset_mock() + hass.states.async_set( doorbell_entity_id, STATE_ON, @@ -769,6 +797,31 @@ async def test_camera_with_linked_doorbell_sensor(hass, run_driver, events): await hass.async_block_till_done() assert char.value is None assert char2.value is None + assert len(broker.mock_calls) == 2 + broker.reset_mock() + + hass.states.async_set( + doorbell_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY}, + force_update=True, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + broker.reset_mock() + + hass.states.async_set( + doorbell_entity_id, + STATE_ON, + {ATTR_DEVICE_CLASS: BinarySensorDeviceClass.OCCUPANCY, "other": "attr"}, + ) + await hass.async_block_till_done() + assert char.value is None + assert char2.value is None + assert len(broker.mock_calls) == 0 + broker.reset_mock() # Ensure we do not throw when the linked # doorbell sensor is removed