Reduce context switching in homekit state updates (#35147)

pull/35592/head
J. Nick Koston 2020-05-11 00:10:08 -05:00 committed by GitHub
parent 2e018ad841
commit 742e36ba26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 283 additions and 164 deletions

View File

@ -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.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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))

View File

@ -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:

View File

@ -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)

View File

@ -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,48 +130,78 @@ 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
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
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
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
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)
with patch(
"homeassistant.components.homekit.accessories.HomeAccessory.async_update_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"
) 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
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}
)
@ -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
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,11 +246,17 @@ 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
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
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
@ -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
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
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
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
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
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
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,13 +444,20 @@ 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
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
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

View File

@ -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 "