From 0f7200809039a2a5b73ad384085da9712139f85a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jun 2020 11:25:26 -0500 Subject: [PATCH] Ensure homekit state changed listeners are unsubscribed on reload (#37200) * Ensure homekit state changed listeners are unsubscribed on reload * fix mocking --- homeassistant/components/homekit/__init__.py | 2 ++ .../components/homekit/accessories.py | 33 +++++++++++++------ tests/components/homekit/test_accessories.py | 17 ++++++++++ tests/components/homekit/test_homekit.py | 2 ++ 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index bdab5a8ad07..de774fbf500 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -576,6 +576,8 @@ class HomeKit: self.status = STATUS_STOPPED _LOGGER.debug("Driver stop for %s", self._name) self.hass.add_job(self.driver.stop) + for acc in self.bridge.accessories.values(): + acc.async_stop() @callback def _async_configure_linked_sensors(self, ent_reg_ent, device_lookup, state): diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index cb1c76656bb..0077f0bb018 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -270,6 +270,7 @@ class HomeAccessory(Accessory): self.entity_id = entity_id self.hass = hass self.debounce = {} + self._subscriptions = [] self._char_battery = None self._char_charging = None self._char_low_battery = None @@ -343,8 +344,10 @@ class HomeAccessory(Accessory): """ state = self.hass.states.get(self.entity_id) self.async_update_state_callback(None, None, state) - async_track_state_change( - self.hass, self.entity_id, self.async_update_state_callback + self._subscriptions.append( + async_track_state_change( + self.hass, self.entity_id, self.async_update_state_callback + ) ) battery_charging_state = None @@ -357,10 +360,12 @@ class HomeAccessory(Accessory): battery_charging_state = linked_battery_sensor_state.attributes.get( ATTR_BATTERY_CHARGING ) - async_track_state_change( - self.hass, - self.linked_battery_sensor, - self.async_update_linked_battery_callback, + self._subscriptions.append( + async_track_state_change( + self.hass, + self.linked_battery_sensor, + self.async_update_linked_battery_callback, + ) ) else: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) @@ -369,10 +374,12 @@ class HomeAccessory(Accessory): self.hass.states.get(self.linked_battery_charging_sensor).state == STATE_ON ) - async_track_state_change( - self.hass, - self.linked_battery_charging_sensor, - self.async_update_linked_battery_charging_callback, + self._subscriptions.append( + async_track_state_change( + self.hass, + self.linked_battery_charging_sensor, + self.async_update_linked_battery_charging_callback, + ) ) elif battery_charging_state is None: battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING) @@ -481,6 +488,12 @@ class HomeAccessory(Accessory): self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) await self.hass.services.async_call(domain, service, service_data) + @ha_callback + def async_stop(self): + """Cancel any subscriptions when the bridge is stopped.""" + while self._subscriptions: + self._subscriptions.pop(0)() + class HomeBridge(Bridge): """Adapter class for Bridge.""" diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index abc6d9b5528..fd99230f206 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -45,6 +45,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, __version__, ) +from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS import homeassistant.util.dt as dt_util from tests.async_mock import Mock, patch @@ -83,6 +84,22 @@ async def test_debounce(hass): assert counter == 2 +async def test_accessory_cancels_track_state_change_on_stop(hass, hk_driver): + """Ensure homekit state changed listeners are unsubscribed on reload.""" + entity_id = "sensor.accessory" + hass.states.async_set(entity_id, None) + acc = HomeAccessory( + hass, hk_driver, "Home Accessory", entity_id, 2, {"platform": "isy994"} + ) + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + await acc.run_handler() + assert len(hass.data[TRACK_STATE_CHANGE_CALLBACKS][entity_id]) == 1 + acc.async_stop() + assert entity_id not in hass.data[TRACK_STATE_CHANGE_CALLBACKS] + + async def test_home_accessory(hass, hk_driver): """Test HomeAccessory class.""" entity_id = "sensor.accessory" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 1903e3eca8f..954758fe3f5 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -605,6 +605,8 @@ async def test_homekit_stop(hass): entry_id=entry.entry_id, ) homekit.driver = Mock() + homekit.bridge = Mock() + homekit.bridge.accessories = {} await async_init_integration(hass)