diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ac6e8969d91..1cf549437b8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -313,7 +313,7 @@ class HomeAccessory(Accessory): Run inside the Home Assistant event loop. """ state = self.hass.states.get(self.entity_id) - await self.async_update_state_callback(None, None, state) + self.async_update_state_callback(None, None, state) async_track_state_change( self.hass, self.entity_id, self.async_update_state_callback ) @@ -329,7 +329,9 @@ class HomeAccessory(Accessory): ATTR_BATTERY_CHARGING ) async_track_state_change( - self.hass, self.linked_battery_sensor, self.async_update_linked_battery + self.hass, + self.linked_battery_sensor, + self.async_update_linked_battery_callback, ) else: battery_state = state.attributes.get(ATTR_BATTERY_LEVEL) @@ -341,17 +343,16 @@ class HomeAccessory(Accessory): async_track_state_change( self.hass, self.linked_battery_charging_sensor, - self.async_update_linked_battery_charging, + self.async_update_linked_battery_charging_callback, ) elif battery_charging_state is None: battery_charging_state = state.attributes.get(ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: - self.hass.async_add_executor_job( - self.update_battery, battery_state, battery_charging_state - ) + self.async_update_battery(battery_state, battery_charging_state) - async def async_update_state_callback( + @ha_callback + def async_update_state_callback( self, entity_id=None, old_state=None, new_state=None ): """Handle state change listener callback.""" @@ -371,12 +372,11 @@ class HomeAccessory(Accessory): ): battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING) if battery_state is not None or battery_charging_state is not None: - await self.hass.async_add_executor_job( - self.update_battery, battery_state, battery_charging_state - ) - await self.hass.async_add_executor_job(self.update_state, new_state) + self.async_update_battery(battery_state, battery_charging_state) + self.async_update_state(new_state) - async def async_update_linked_battery( + @ha_callback + def async_update_linked_battery_callback( self, entity_id=None, old_state=None, new_state=None ): """Handle linked battery sensor state change listener callback.""" @@ -384,19 +384,17 @@ class HomeAccessory(Accessory): battery_charging_state = None else: battery_charging_state = new_state.attributes.get(ATTR_BATTERY_CHARGING) - await self.hass.async_add_executor_job( - self.update_battery, new_state.state, battery_charging_state, - ) + self.async_update_battery(new_state.state, battery_charging_state) - async def async_update_linked_battery_charging( + @ha_callback + def async_update_linked_battery_charging_callback( self, entity_id=None, old_state=None, new_state=None ): """Handle linked battery charging sensor state change listener callback.""" - await self.hass.async_add_executor_job( - self.update_battery, None, new_state.state == STATE_ON - ) + self.async_update_battery(None, new_state.state == STATE_ON) - def update_battery(self, battery_level, battery_charging): + @ha_callback + def async_update_battery(self, battery_level, battery_charging): """Update battery service if available. Only call this function if self._support_battery_level is True. @@ -427,7 +425,8 @@ class HomeAccessory(Accessory): "%s: Updated battery charging to %d", self.entity_id, hk_charging ) - def update_state(self, new_state): + @ha_callback + def async_update_state(self, new_state): """Handle state change to update HomeKit value. Overridden by accessory types. diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py index fe181d56c8c..ae91362c5fc 100644 --- a/homeassistant/components/homekit/type_cameras.py +++ b/homeassistant/components/homekit/type_cameras.py @@ -12,6 +12,7 @@ from pyhap.const import CATEGORY_CAMERA from homeassistant.components.camera.const import DOMAIN as DOMAIN_CAMERA from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.core import callback from homeassistant.util import get_local_ip from .accessories import TYPES, HomeAccessory @@ -144,7 +145,8 @@ class Camera(HomeAccessory, PyhapCamera): options=options, ) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Handle state change to update HomeKit value.""" pass # pylint: disable=unnecessary-pass diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index 25d1782b392..1e18ad82b94 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -25,6 +25,7 @@ from homeassistant.const import ( STATE_OPEN, STATE_OPENING, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory, debounce from .const import ( @@ -92,7 +93,7 @@ class GarageDoorOpener(HomeAccessory): self.char_target_state = serv_garage_door.configure_char( CHAR_TARGET_DOOR_STATE, value=0, setter_callback=self.set_state ) - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Change garage state if call came from HomeKit.""" @@ -108,7 +109,8 @@ class GarageDoorOpener(HomeAccessory): self.char_current_state.set_value(HK_DOOR_CLOSING) self.call_service(DOMAIN, SERVICE_CLOSE_COVER, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover state after state changed.""" hass_state = new_state.state target_door_state = DOOR_TARGET_HASS_TO_HK.get(hass_state) @@ -184,7 +186,8 @@ class WindowCoveringBase(HomeAccessory): self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover position and tilt after state changed.""" # update tilt current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION) @@ -230,7 +233,7 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): self.char_position_state = self.serv_cover.configure_char( CHAR_POSITION_STATE, value=HK_POSITION_STOPPED ) - self.update_state(state) + self.async_update_state(state) @debounce def move_cover(self, value): @@ -241,7 +244,8 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_POSITION: value} self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover position and tilt after state changed.""" current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, (float, int)): @@ -271,7 +275,7 @@ class WindowCovering(WindowCoveringBase, HomeAccessory): if self.char_position_state.value != HK_POSITION_STOPPED: self.char_position_state.set_value(HK_POSITION_STOPPED) - super().update_state(new_state) + super().async_update_state(new_state) @TYPES.register("WindowCoveringBasic") @@ -295,7 +299,7 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): self.char_position_state = self.serv_cover.configure_char( CHAR_POSITION_STATE, value=HK_POSITION_STOPPED ) - self.update_state(state) + self.async_update_state(state) @debounce def move_cover(self, value): @@ -322,7 +326,8 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): self.char_current_position.set_value(position) self.char_target_position.set_value(position) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update cover position after state changed.""" position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0} hk_position = position_mapping.get(new_state.state) @@ -341,4 +346,4 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory): if self.char_position_state.value != HK_POSITION_STOPPED: self.char_position_state.set_value(HK_POSITION_STOPPED) - super().update_state(new_state) + super().async_update_state(new_state) diff --git a/homeassistant/components/homekit/type_fans.py b/homeassistant/components/homekit/type_fans.py index b80a65eede1..d6231efe7ad 100644 --- a/homeassistant/components/homekit/type_fans.py +++ b/homeassistant/components/homekit/type_fans.py @@ -26,6 +26,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -80,13 +81,13 @@ class Fan(HomeAccessory): if CHAR_ROTATION_SPEED in chars: # Initial value is set to 100 because 0 is a special value (off). 100 is - # an arbitrary non-zero value. It is updated immediately by update_state + # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_speed = serv_fan.configure_char(CHAR_ROTATION_SPEED, value=100) if CHAR_SWING_MODE in chars: self.char_swing = serv_fan.configure_char(CHAR_SWING_MODE, value=0) - self.update_state(state) + self.async_update_state(state) serv_fan.setter_callback = self._set_chars def _set_chars(self, char_values): @@ -146,7 +147,8 @@ class Fan(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_SPEED: speed} self.call_service(DOMAIN, SERVICE_SET_SPEED, params, speed) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update fan after state change.""" # Handle State state = new_state.state diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 3f4c2518202..612d8e53a02 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -23,6 +23,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -77,7 +78,7 @@ class Light(HomeAccessory): if CHAR_BRIGHTNESS in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is - # an arbitrary non-zero value. It is updated immediately by update_state + # an arbitrary non-zero value. It is updated immediately by async_update_state # to set to the correct initial value. self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) @@ -100,7 +101,7 @@ class Light(HomeAccessory): if CHAR_SATURATION in self.chars: self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) - self.update_state(state) + self.async_update_state(state) serv_light.setter_callback = self._set_chars @@ -138,7 +139,8 @@ class Light(HomeAccessory): self.call_service(DOMAIN, service, params, ", ".join(events)) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update light after state change.""" # Handle State state = new_state.state diff --git a/homeassistant/components/homekit/type_locks.py b/homeassistant/components/homekit/type_locks.py index 5697306bf32..af5b24c50e1 100644 --- a/homeassistant/components/homekit/type_locks.py +++ b/homeassistant/components/homekit/type_locks.py @@ -5,6 +5,7 @@ from pyhap.const import CATEGORY_DOOR_LOCK from homeassistant.components.lock import DOMAIN, STATE_LOCKED, STATE_UNLOCKED from homeassistant.const import ATTR_CODE, ATTR_ENTITY_ID, STATE_UNKNOWN +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import CHAR_LOCK_CURRENT_STATE, CHAR_LOCK_TARGET_STATE, SERV_LOCK @@ -45,7 +46,7 @@ class Lock(HomeAccessory): value=HASS_TO_HOMEKIT[STATE_LOCKED], setter_callback=self.set_state, ) - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Set lock state to value if call came from HomeKit.""" @@ -62,7 +63,8 @@ class Lock(HomeAccessory): params[ATTR_CODE] = self._code self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update lock after state changed.""" hass_state = new_state.state if hass_state in HASS_TO_HOMEKIT: diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 3dccca7a322..4a104972b02 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -36,6 +36,7 @@ from homeassistant.const import ( STATE_STANDBY, STATE_UNKNOWN, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -150,7 +151,7 @@ class MediaPlayer(HomeAccessory): self.chars[FEATURE_TOGGLE_MUTE] = serv_toggle_mute.configure_char( CHAR_ON, value=False, setter_callback=self.set_toggle_mute ) - self.update_state(state) + self.async_update_state(state) def generate_service_name(self, mode): """Generate name for individual service.""" @@ -189,7 +190,8 @@ class MediaPlayer(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id, ATTR_MEDIA_VOLUME_MUTED: value} self.call_service(DOMAIN, SERVICE_VOLUME_MUTE, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state @@ -321,7 +323,7 @@ class TelevisionMediaPlayer(HomeAccessory): serv_input.configure_char(CHAR_CURRENT_VISIBILITY_STATE, value=False) _LOGGER.debug("%s: Added source %s.", self.entity_id, source) - self.update_state(state) + self.async_update_state(state) def set_on_off(self, value): """Move switch state to value if call came from HomeKit.""" @@ -375,7 +377,8 @@ class TelevisionMediaPlayer(HomeAccessory): params = {ATTR_ENTITY_ID: self.entity_id} self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update Television state after state changed.""" current_state = new_state.state diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 8a2bb971cf1..7d8dcac046d 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -17,6 +17,7 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -64,7 +65,7 @@ class SecuritySystem(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def set_security_state(self, value): """Move security state to value if call came from HomeKit.""" @@ -77,7 +78,8 @@ class SecuritySystem(HomeAccessory): params[ATTR_CODE] = self._alarm_code self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update security state after state changed.""" hass_state = new_state.state if hass_state in HASS_TO_HOMEKIT: diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index c37755bc1c5..28c7ea26009 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -10,6 +10,7 @@ from homeassistant.const import ( STATE_ON, TEMP_CELSIUS, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -89,9 +90,10 @@ class TemperatureSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update temperature after state changed.""" unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT, TEMP_CELSIUS) temperature = convert_to_float(new_state.state) @@ -118,9 +120,10 @@ class HumiditySensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" humidity = convert_to_float(new_state.state) if humidity and self.char_humidity.value != humidity: @@ -145,9 +148,10 @@ class AirQualitySensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" density = convert_to_float(new_state.state) if density: @@ -181,9 +185,10 @@ class CarbonMonoxideSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: @@ -218,9 +223,10 @@ class CarbonDioxideSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" value = convert_to_float(new_state.state) if value: @@ -248,9 +254,10 @@ class LightSensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" luminance = convert_to_float(new_state.state) if luminance and self.char_light.value != luminance: @@ -281,9 +288,10 @@ class BinarySensor(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update accessory after state change.""" state = new_state.state detected = self.format(state in (STATE_ON, STATE_HOME)) diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index 072c8681a50..635b0e1d036 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -24,7 +24,7 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_ON, ) -from homeassistant.core import split_entity_id +from homeassistant.core import callback, split_entity_id from homeassistant.helpers.event import call_later from .accessories import TYPES, HomeAccessory @@ -71,7 +71,7 @@ class Outlet(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Move switch state to value if call came from HomeKit.""" @@ -80,7 +80,8 @@ class Outlet(HomeAccessory): service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state == STATE_ON if self.char_on.value is not current_state: @@ -106,7 +107,7 @@ class Switch(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def is_activate(self, state): """Check if entity is activate only.""" @@ -136,7 +137,8 @@ class Switch(HomeAccessory): if self.activate_only: call_later(self.hass, 1, self.reset_switch) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" self.activate_only = self.is_activate(new_state) if self.activate_only: @@ -162,7 +164,8 @@ class DockVacuum(Switch): service = SERVICE_START if value else SERVICE_RETURN_TO_BASE self.call_service(VACUUM_DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = new_state.state in (STATE_CLEANING, STATE_ON) if self.char_on.value is not current_state: @@ -191,7 +194,7 @@ class Valve(HomeAccessory): ) # Set the state so it is in sync on initial # GET to avoid an event storm after homekit startup - self.update_state(state) + self.async_update_state(state) def set_state(self, value): """Move value state to value if call came from HomeKit.""" @@ -201,7 +204,8 @@ class Valve(HomeAccessory): service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF self.call_service(DOMAIN, service, params) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update switch state after state changed.""" current_state = 1 if new_state.state == STATE_ON else 0 if self.char_active.value != current_state: diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 84cefced602..c202191cb7e 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -51,6 +51,7 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, UNIT_PERCENTAGE, ) +from homeassistant.core import callback from .accessories import TYPES, HomeAccessory from .const import ( @@ -220,7 +221,7 @@ class Thermostat(HomeAccessory): CHAR_CURRENT_HUMIDITY, value=50 ) - self._update_state(state) + self._async_update_state(state) serv_thermostat.setter_callback = self._set_chars @@ -391,7 +392,8 @@ class Thermostat(HomeAccessory): DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{UNIT_PERCENTAGE}" ) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update thermostat state after state changed.""" if self._state_updates < 3: # When we get the first state updates @@ -414,9 +416,10 @@ class Thermostat(HomeAccessory): ) self._state_updates += 1 - self._update_state(new_state) + self._async_update_state(new_state) - def _update_state(self, new_state): + @callback + def _async_update_state(self, new_state): """Update state without rechecking the device features.""" features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) @@ -544,7 +547,7 @@ class WaterHeater(HomeAccessory): ) state = self.hass.states.get(self.entity_id) - self.update_state(state) + self.async_update_state(state) def get_temperature_range(self): """Return min and max temperature range.""" @@ -586,7 +589,8 @@ class WaterHeater(HomeAccessory): f"{temperature}{self._unit}", ) - def update_state(self, new_state): + @callback + def async_update_state(self, new_state): """Update water_heater state after state change.""" # Update current and target temperature temperature = new_state.attributes.get(ATTR_TEMPERATURE) diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 55b7f764d76..036d30b0edd 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -100,19 +100,19 @@ async def test_home_accessory(hass, hk_driver): hass.states.async_set(entity_id, "on") await hass.async_block_till_done() with patch( - "homeassistant.components.homekit.accessories.HomeAccessory.update_state" - ) as mock_update_state: + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: await acc.run_handler() await hass.async_block_till_done() state = hass.states.get(entity_id) - mock_update_state.assert_called_with(state) + mock_async_update_state.assert_called_with(state) hass.states.async_remove(entity_id) await hass.async_block_till_done() - assert mock_update_state.call_count == 1 + assert mock_async_update_state.call_count == 1 with pytest.raises(NotImplementedError): - acc.update_state("new_state") + acc.async_update_state("new_state") # Test model name from domain entity_id = "test_model.demo" @@ -130,52 +130,82 @@ async def test_battery_service(hass, hk_driver, caplog): await hass.async_block_till_done() acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None) - acc.update_state = lambda x: None assert acc._char_battery.value == 0 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) + assert acc._char_battery.value == 50 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 - hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 15 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 2 - hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: "error"}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: "error"}) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 15 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 2 assert "ERROR" not in caplog.text # Test charging - hass.states.async_set( - entity_id, None, {ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True} - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set( + entity_id, None, {ATTR_BATTERY_LEVEL: 10, ATTR_BATTERY_CHARGING: True} + ) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) - acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None) - acc.update_state = lambda x: None - assert acc._char_battery.value == 0 - assert acc._char_low_battery.value == 0 - assert acc._char_charging.value == 2 + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + acc = HomeAccessory(hass, hk_driver, "Battery Service", entity_id, 2, None) + assert acc._char_battery.value == 0 + assert acc._char_low_battery.value == 0 + assert acc._char_charging.value == 2 - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 10 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 1 - hass.states.async_set( - entity_id, None, {ATTR_BATTERY_LEVEL: 100, ATTR_BATTERY_CHARGING: False} - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set( + entity_id, None, {ATTR_BATTERY_LEVEL: 100, ATTR_BATTERY_CHARGING: False} + ) + await hass.async_block_till_done() assert acc._char_battery.value == 100 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 0 @@ -197,11 +227,15 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_SENSOR: linked_battery}, ) - acc.update_state = lambda x: None assert acc.linked_battery_sensor == linked_battery - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 50 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 2 @@ -212,13 +246,19 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): assert acc._char_low_battery.value == 1 # Ignore battery change on entity if it has linked_battery - hass.states.async_set(entity_id, "open", {ATTR_BATTERY_LEVEL: 90}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set(entity_id, "open", {ATTR_BATTERY_LEVEL: 90}) + await hass.async_block_till_done() assert acc._char_battery.value == 10 # Test none numeric state for linked_battery - hass.states.async_set(linked_battery, "error", None) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set(linked_battery, "error", None) + await hass.async_block_till_done() assert acc._char_battery.value == 10 assert "ERROR" not in caplog.text @@ -234,15 +274,20 @@ async def test_linked_battery_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_SENSOR: linked_battery, CONF_LOW_BATTERY_THRESHOLD: 50}, ) - acc.update_state = lambda x: None - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 20 assert acc._char_low_battery.value == 1 assert acc._char_charging.value == 1 hass.states.async_set(linked_battery, 100, {ATTR_BATTERY_CHARGING: False}) await hass.async_block_till_done() + state = hass.states.get(entity_id) assert acc._char_battery.value == 100 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 0 @@ -264,23 +309,37 @@ async def test_linked_battery_charging_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_CHARGING_SENSOR: linked_battery_charging_sensor}, ) - acc.update_state = lambda x: None assert acc.linked_battery_charging_sensor == linked_battery_charging_sensor - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 100 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 1 - hass.states.async_set(linked_battery_charging_sensor, STATE_OFF, None) - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(linked_battery_charging_sensor, STATE_OFF, None) + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_charging.value == 0 - hass.states.async_set(linked_battery_charging_sensor, STATE_ON, None) - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + hass.states.async_set(linked_battery_charging_sensor, STATE_ON, None) + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_charging.value == 1 @@ -307,11 +366,15 @@ async def test_linked_battery_sensor_and_linked_battery_charging_sensor( CONF_LINKED_BATTERY_CHARGING_SENSOR: linked_battery_charging_sensor, }, ) - acc.update_state = lambda x: None assert acc.linked_battery_sensor == linked_battery - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery.value == 50 assert acc._char_low_battery.value == 0 assert acc._char_charging.value == 1 @@ -356,11 +419,15 @@ async def test_missing_linked_battery_sensor(hass, hk_driver, caplog): 2, {CONF_LINKED_BATTERY_SENSOR: linked_battery}, ) - acc.update_state = lambda x: None assert not acc.linked_battery_sensor - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert not acc.linked_battery_sensor assert acc._char_battery is None @@ -377,15 +444,22 @@ async def test_battery_appears_after_startup(hass, hk_driver, caplog): acc = HomeAccessory( hass, hk_driver, "Accessory without battery", entity_id, 2, None ) - acc.update_state = lambda x: None assert acc._char_battery is None - await acc.run_handler() - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ) as mock_async_update_state: + await acc.run_handler() + await hass.async_block_till_done() + state = hass.states.get(entity_id) + mock_async_update_state.assert_called_with(state) assert acc._char_battery is None - hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) - await hass.async_block_till_done() + with patch( + "homeassistant.components.homekit.accessories.HomeAccessory.async_update_state" + ): + hass.states.async_set(entity_id, None, {ATTR_BATTERY_LEVEL: 15}) + await hass.async_block_till_done() assert acc._char_battery is None diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 81dfb9410f0..903b1808463 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -30,6 +30,27 @@ MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDA MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6") +async def _async_start_streaming(hass, acc): + """Start streaming a camera.""" + acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) + await acc.run_handler() + await hass.async_block_till_done() + + +async def _async_setup_endpoints(hass, acc): + """Set camera endpoints.""" + acc.set_endpoints(MOCK_END_POINTS_TLV) + await acc.run_handler() + await hass.async_block_till_done() + + +async def _async_stop_stream(hass, acc): + """Stop a camera stream.""" + await acc.stop() + await acc.run_handler() + await hass.async_block_till_done() + + @pytest.fixture() def run_driver(hass): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" @@ -92,9 +113,9 @@ async def test_camera_stream_source_configured(hass, run_driver, events): assert acc.aid == 2 assert acc.category == 17 # Camera - acc.set_endpoints(MOCK_END_POINTS_TLV) - session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + await _async_setup_endpoints(hass, acc) working_ffmpeg = _get_working_mock_ffmpeg() + session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", @@ -103,8 +124,8 @@ async def test_camera_stream_source_configured(hass, run_driver, events): "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=working_ffmpeg, ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await hass.async_block_till_done() + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) expected_output = ( "-map 0:v:0 -an -c:v libx264 -profile:v high -tune zerolatency -pix_fmt " @@ -132,11 +153,10 @@ async def test_camera_stream_source_configured(hass, run_driver, events): "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=working_ffmpeg, ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.stop_stream(session_info) + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) # Calling a second time should not throw - await acc.stop_stream(session_info) - await hass.async_block_till_done() + await _async_stop_stream(hass, acc) turbo_jpeg = mock_turbo_jpeg( first_width=16, first_height=12, second_width=300, second_height=200 @@ -195,8 +215,7 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( assert acc.aid == 2 assert acc.category == 17 # Camera - acc.set_endpoints(MOCK_END_POINTS_TLV) - session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + await _async_setup_endpoints(hass, acc) with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", @@ -205,11 +224,10 @@ async def test_camera_stream_source_configured_with_failing_ffmpeg( "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=_get_failing_mock_ffmpeg(), ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.stop_stream(session_info) + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) # Calling a second time should not throw - await acc.stop_stream(session_info) - await hass.async_block_till_done() + await _async_stop_stream(hass, acc) async def test_camera_stream_source_found(hass, run_driver, events): @@ -229,8 +247,7 @@ async def test_camera_stream_source_found(hass, run_driver, events): assert acc.aid == 2 assert acc.category == 17 # Camera - acc.set_endpoints(MOCK_END_POINTS_TLV) - session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + await _async_setup_endpoints(hass, acc) with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", @@ -239,9 +256,8 @@ async def test_camera_stream_source_found(hass, run_driver, events): "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=_get_working_mock_ffmpeg(), ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.stop_stream(session_info) - await hass.async_block_till_done() + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", @@ -250,9 +266,8 @@ async def test_camera_stream_source_found(hass, run_driver, events): "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=_get_working_mock_ffmpeg(), ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.stop_stream(session_info) - await hass.async_block_till_done() + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) async def test_camera_stream_source_fails(hass, run_driver, events): @@ -272,8 +287,7 @@ async def test_camera_stream_source_fails(hass, run_driver, events): assert acc.aid == 2 assert acc.category == 17 # Camera - acc.set_endpoints(MOCK_END_POINTS_TLV) - session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] + await _async_setup_endpoints(hass, acc) with patch( "homeassistant.components.demo.camera.DemoCamera.stream_source", @@ -282,9 +296,8 @@ async def test_camera_stream_source_fails(hass, run_driver, events): "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=_get_working_mock_ffmpeg(), ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.stop_stream(session_info) - await hass.async_block_till_done() + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) async def test_camera_with_no_stream(hass, run_driver, events): @@ -302,9 +315,9 @@ async def test_camera_with_no_stream(hass, run_driver, events): assert acc.aid == 2 assert acc.category == 17 # Camera - acc.set_endpoints(MOCK_END_POINTS_TLV) - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await hass.async_block_till_done() + await _async_setup_endpoints(hass, acc) + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) with pytest.raises(HomeAssistantError): await hass.async_add_executor_job( @@ -344,7 +357,7 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, assert acc.aid == 2 assert acc.category == 17 # Camera - acc.set_endpoints(MOCK_END_POINTS_TLV) + await _async_setup_endpoints(hass, acc) session_info = acc.sessions[MOCK_START_STREAM_SESSION_UUID] working_ffmpeg = _get_working_mock_ffmpeg() @@ -356,9 +369,8 @@ async def test_camera_stream_source_configured_and_copy_codec(hass, run_driver, "homeassistant.components.homekit.type_cameras.HAFFmpeg", return_value=working_ffmpeg, ): - acc.set_selected_stream_configuration(MOCK_START_STREAM_TLV) - await acc.stop_stream(session_info) - await hass.async_block_till_done() + await _async_start_streaming(hass, acc) + await _async_stop_stream(hass, acc) expected_output = ( "-map 0:v:0 -an -c:v copy -tune zerolatency -pix_fmt yuv420p -r 30 -b:v 299k "