Set homekit alarm/sensor/switch/cover state as soon as possible (#34245)

* Set homekit alarm/sensor/switch state as soon as possible

This change is part of a multi-part effort to fix the
HomeKit event storms on startup.

Previously we would set the states after HomeKit
had started up which meant that when the controller
client connected it would request the states and get
a list of default states so all the initial states
would always be wrong. The defaults states generally went
unnoticed because we set the state of each HomeKit device
soon after which would result in an event storm in the log
that looked like the following for every client and every
device:

Sending event to client: ('192.168.x.x', 58410)
Sending event to client: ('192.168.x.x', 53399)
Sending event to client: ('192.168.x.x', 53399)

To solve this, we now set the state right away when we
create the entity in HomeKit, so it is correct on
initial sync, which avoids the event storm.  Additionally,
we now check all states values before sending an update
to HomeKit to ensure we do not send events when nothing
has changed.

* pylint

* Fix event storm in covers as well

* fix refactoring error in security system

* cover positions, now with constants
This commit is contained in:
J. Nick Koston 2020-04-15 21:38:31 -05:00 committed by GitHub
parent 188f3e35fd
commit d6a47cb3e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 146 additions and 80 deletions

View file

@ -190,3 +190,8 @@ HK_DOOR_CLOSED = 1
HK_DOOR_OPENING = 2
HK_DOOR_CLOSING = 3
HK_DOOR_STOPPED = 4
# ### Position State ####
HK_POSITION_GOING_TO_MIN = 0
HK_POSITION_GOING_TO_MAX = 1
HK_POSITION_STOPPED = 2

View file

@ -42,6 +42,9 @@ from .const import (
HK_DOOR_CLOSING,
HK_DOOR_OPEN,
HK_DOOR_OPENING,
HK_POSITION_GOING_TO_MAX,
HK_POSITION_GOING_TO_MIN,
HK_POSITION_STOPPED,
SERV_GARAGE_DOOR_OPENER,
SERV_WINDOW_COVERING,
)
@ -134,10 +137,9 @@ class WindowCoveringBase(HomeAccessory):
def __init__(self, *args, category):
"""Initialize a WindowCoveringBase accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
state = self.hass.states.get(self.entity_id)
self.features = self.hass.states.get(self.entity_id).attributes.get(
ATTR_SUPPORTED_FEATURES, 0
)
self.features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
self._supports_stop = self.features & SUPPORT_STOP
self._homekit_target_tilt = None
self.chars = []
@ -192,7 +194,8 @@ class WindowCoveringBase(HomeAccessory):
# We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt)
self.char_current_tilt.set_value(current_tilt)
if self.char_current_tilt.value != current_tilt:
self.char_current_tilt.set_value(current_tilt)
# We have to assume that the device has worse precision than HomeKit.
# If it reports back a state that is only _close_ to HK's requested
@ -201,7 +204,8 @@ class WindowCoveringBase(HomeAccessory):
if self._homekit_target_tilt is None or abs(
current_tilt - self._homekit_target_tilt < DEVICE_PRECISION_LEEWAY
):
self.char_target_tilt.set_value(current_tilt)
if self.char_target_tilt.value != current_tilt:
self.char_target_tilt.set_value(current_tilt)
self._homekit_target_tilt = None
@ -215,7 +219,7 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
state = self.hass.states.get(self.entity_id)
self._homekit_target = None
self.char_current_position = self.serv_cover.configure_char(
@ -225,8 +229,9 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
)
self.char_position_state = self.serv_cover.configure_char(
CHAR_POSITION_STATE, value=2
CHAR_POSITION_STATE, value=HK_POSITION_STOPPED
)
self.update_state(state)
@debounce
def move_cover(self, value):
@ -242,7 +247,8 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, (float, int)):
current_position = int(current_position)
self.char_current_position.set_value(current_position)
if self.char_current_position.value != current_position:
self.char_current_position.set_value(current_position)
# We have to assume that the device has worse precision than HomeKit.
# If it reports back a state that is only _close_ to HK's requested
@ -253,14 +259,18 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
or abs(current_position - self._homekit_target)
< DEVICE_PRECISION_LEEWAY
):
self.char_target_position.set_value(current_position)
if self.char_target_position.value != current_position:
self.char_target_position.set_value(current_position)
self._homekit_target = None
if new_state.state == STATE_OPENING:
self.char_position_state.set_value(1)
if self.char_position_state.value != HK_POSITION_GOING_TO_MAX:
self.char_position_state.set_value(HK_POSITION_GOING_TO_MAX)
elif new_state.state == STATE_CLOSING:
self.char_position_state.set_value(0)
if self.char_position_state.value != HK_POSITION_GOING_TO_MIN:
self.char_position_state.set_value(HK_POSITION_GOING_TO_MIN)
else:
self.char_position_state.set_value(2)
if self.char_position_state.value != HK_POSITION_STOPPED:
self.char_position_state.set_value(HK_POSITION_STOPPED)
super().update_state(new_state)
@ -276,7 +286,7 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
def __init__(self, *args):
"""Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
state = self.hass.states.get(self.entity_id)
self.char_current_position = self.serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0
)
@ -284,8 +294,9 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
)
self.char_position_state = self.serv_cover.configure_char(
CHAR_POSITION_STATE, value=2
CHAR_POSITION_STATE, value=HK_POSITION_STOPPED
)
self.update_state(state)
@debounce
def move_cover(self, value):
@ -317,13 +328,18 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
hk_position = position_mapping.get(new_state.state)
if hk_position is not None:
self.char_current_position.set_value(hk_position)
self.char_target_position.set_value(hk_position)
if self.char_current_position.value != hk_position:
self.char_current_position.set_value(hk_position)
if self.char_target_position.value != hk_position:
self.char_target_position.set_value(hk_position)
if new_state.state == STATE_OPENING:
self.char_position_state.set_value(1)
if self.char_position_state.value != HK_POSITION_GOING_TO_MAX:
self.char_position_state.set_value(HK_POSITION_GOING_TO_MAX)
elif new_state.state == STATE_CLOSING:
self.char_position_state.set_value(0)
if self.char_position_state.value != HK_POSITION_GOING_TO_MIN:
self.char_position_state.set_value(HK_POSITION_GOING_TO_MIN)
else:
self.char_position_state.set_value(2)
if self.char_position_state.value != HK_POSITION_STOPPED:
self.char_position_state.set_value(HK_POSITION_STOPPED)
super().update_state(new_state)

View file

@ -53,8 +53,8 @@ class SecuritySystem(HomeAccessory):
def __init__(self, *args):
"""Initialize a SecuritySystem accessory object."""
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
state = self.hass.states.get(self.entity_id)
self._alarm_code = self.config.get(ATTR_CODE)
self._flag_state = False
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm.configure_char(
@ -63,11 +63,13 @@ class SecuritySystem(HomeAccessory):
self.char_target_state = serv_alarm.configure_char(
CHAR_TARGET_SECURITY_STATE, value=3, setter_callback=self.set_security_state
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def set_security_state(self, value):
"""Move security state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set security state to %d", self.entity_id, value)
self._flag_state = True
hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value]
@ -81,15 +83,18 @@ class SecuritySystem(HomeAccessory):
hass_state = new_state.state
if hass_state in HASS_TO_HOMEKIT:
current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state)
_LOGGER.debug(
"%s: Updated current state to %s (%d)",
self.entity_id,
hass_state,
current_security_state,
)
if self.char_current_state.value != current_security_state:
self.char_current_state.set_value(current_security_state)
_LOGGER.debug(
"%s: Updated current state to %s (%d)",
self.entity_id,
hass_state,
current_security_state,
)
# SecuritySystemTargetState does not support triggered
if not self._flag_state and hass_state != STATE_ALARM_TRIGGERED:
if (
hass_state != STATE_ALARM_TRIGGERED
and self.char_target_state.value != current_security_state
):
self.char_target_state.set_value(current_security_state)
self._flag_state = False

View file

@ -83,10 +83,14 @@ class TemperatureSensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a TemperatureSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.configure_char(
CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update temperature after state changed."""
@ -94,10 +98,11 @@ class TemperatureSensor(HomeAccessory):
temperature = convert_to_float(new_state.state)
if temperature:
temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature)
_LOGGER.debug(
"%s: Current temperature set to %.1f°C", self.entity_id, temperature
)
if self.char_temp.value != temperature:
self.char_temp.set_value(temperature)
_LOGGER.debug(
"%s: Current temperature set to %.1f°C", self.entity_id, temperature
)
@TYPES.register("HumiditySensor")
@ -107,15 +112,19 @@ class HumiditySensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a HumiditySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity.configure_char(
CHAR_CURRENT_HUMIDITY, value=0
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update accessory after state change."""
humidity = convert_to_float(new_state.state)
if humidity:
if humidity and self.char_humidity.value != humidity:
self.char_humidity.set_value(humidity)
_LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity)
@ -127,7 +136,7 @@ class AirQualitySensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_air_quality = self.add_preload_service(
SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]
)
@ -135,14 +144,21 @@ class AirQualitySensor(HomeAccessory):
self.char_density = serv_air_quality.configure_char(
CHAR_AIR_PARTICULATE_DENSITY, value=0
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if density:
self.char_density.set_value(density)
self.char_quality.set_value(density_to_air_quality(density))
_LOGGER.debug("%s: Set to %d", self.entity_id, density)
if self.char_density.value != density:
self.char_density.set_value(density)
_LOGGER.debug("%s: Set density to %d", self.entity_id, density)
air_quality = density_to_air_quality(density)
if self.char_quality.value != air_quality:
self.char_quality.set_value(air_quality)
_LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality)
@TYPES.register("CarbonMonoxideSensor")
@ -152,7 +168,7 @@ class CarbonMonoxideSensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a CarbonMonoxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_co = self.add_preload_service(
SERV_CARBON_MONOXIDE_SENSOR,
[CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL],
@ -164,16 +180,22 @@ class CarbonMonoxideSensor(HomeAccessory):
self.char_detected = serv_co.configure_char(
CHAR_CARBON_MONOXIDE_DETECTED, value=0
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update accessory after state change."""
value = convert_to_float(new_state.state)
if value:
self.char_level.set_value(value)
if self.char_level.value != value:
self.char_level.set_value(value)
if value > self.char_peak.value:
self.char_peak.set_value(value)
self.char_detected.set_value(value > THRESHOLD_CO)
_LOGGER.debug("%s: Set to %d", self.entity_id, value)
co_detected = value > THRESHOLD_CO
if self.char_detected.value is not co_detected:
self.char_detected.set_value(co_detected)
_LOGGER.debug("%s: Set to %d", self.entity_id, value)
@TYPES.register("CarbonDioxideSensor")
@ -183,7 +205,7 @@ class CarbonDioxideSensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_co2 = self.add_preload_service(
SERV_CARBON_DIOXIDE_SENSOR,
[CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL],
@ -195,16 +217,22 @@ class CarbonDioxideSensor(HomeAccessory):
self.char_detected = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_DETECTED, value=0
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update accessory after state change."""
value = convert_to_float(new_state.state)
if value:
self.char_level.set_value(value)
if self.char_level.value != value:
self.char_level.set_value(value)
if value > self.char_peak.value:
self.char_peak.set_value(value)
self.char_detected.set_value(value > THRESHOLD_CO2)
_LOGGER.debug("%s: Set to %d", self.entity_id, value)
co2_detected = value > THRESHOLD_CO2
if self.char_detected.value is not co2_detected:
self.char_detected.set_value(co2_detected)
_LOGGER.debug("%s: Set to %d", self.entity_id, value)
@TYPES.register("LightSensor")
@ -214,16 +242,19 @@ class LightSensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_light = self.add_preload_service(SERV_LIGHT_SENSOR)
self.char_light = serv_light.configure_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update accessory after state change."""
luminance = convert_to_float(new_state.state)
if luminance:
if luminance and self.char_light.value != luminance:
self.char_light.set_value(luminance)
_LOGGER.debug("%s: Set to %d", self.entity_id, luminance)
@ -235,9 +266,8 @@ class BinarySensor(HomeAccessory):
def __init__(self, *args):
"""Initialize a BinarySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR)
device_class = self.hass.states.get(self.entity_id).attributes.get(
ATTR_DEVICE_CLASS
)
state = self.hass.states.get(self.entity_id)
device_class = state.attributes.get(ATTR_DEVICE_CLASS)
service_char = (
BINARY_SENSOR_SERVICE_MAP[device_class]
if device_class in BINARY_SENSOR_SERVICE_MAP
@ -246,10 +276,14 @@ class BinarySensor(HomeAccessory):
service = self.add_preload_service(service_char[0])
self.char_detected = service.configure_char(service_char[1], value=0)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def update_state(self, new_state):
"""Update accessory after state change."""
state = new_state.state
detected = state in (STATE_ON, STATE_HOME)
self.char_detected.set_value(detected)
_LOGGER.debug("%s: Set to %d", self.entity_id, detected)
if self.char_detected.value != detected:
self.char_detected.set_value(detected)
_LOGGER.debug("%s: Set to %d", self.entity_id, detected)

View file

@ -55,7 +55,7 @@ class Outlet(HomeAccessory):
def __init__(self, *args):
"""Initialize an Outlet accessory object."""
super().__init__(*args, category=CATEGORY_OUTLET)
self._flag_state = False
state = self.hass.states.get(self.entity_id)
serv_outlet = self.add_preload_service(SERV_OUTLET)
self.char_on = serv_outlet.configure_char(
@ -64,11 +64,13 @@ class Outlet(HomeAccessory):
self.char_outlet_in_use = serv_outlet.configure_char(
CHAR_OUTLET_IN_USE, value=True
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.call_service(DOMAIN, service, params)
@ -76,10 +78,9 @@ class Outlet(HomeAccessory):
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = new_state.state == STATE_ON
if not self._flag_state:
if self.char_on.value is not current_state:
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
self.char_on.set_value(current_state)
self._flag_state = False
@TYPES.register("Switch")
@ -90,7 +91,7 @@ class Switch(HomeAccessory):
"""Initialize a Switch accessory object."""
super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0]
self._flag_state = False
state = self.hass.states.get(self.entity_id)
self.activate_only = self.is_activate(self.hass.states.get(self.entity_id))
@ -98,6 +99,9 @@ class Switch(HomeAccessory):
self.char_on = serv_switch.configure_char(
CHAR_ON, value=False, setter_callback=self.set_state
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def is_activate(self, state):
"""Check if entity is activate only."""
@ -111,15 +115,15 @@ class Switch(HomeAccessory):
def reset_switch(self, *args):
"""Reset switch to emulate activate click."""
_LOGGER.debug("%s: Reset switch to off", self.entity_id)
self.char_on.set_value(0)
if self.char_on.value is not False:
self.char_on.set_value(False)
def set_state(self, value):
"""Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
if self.activate_only and value == 0:
if self.activate_only and not value:
_LOGGER.debug("%s: Ignoring turn_off call", self.entity_id)
return
self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.call_service(self._domain, service, params)
@ -137,10 +141,9 @@ class Switch(HomeAccessory):
return
current_state = new_state.state == STATE_ON
if not self._flag_state:
if self.char_on.value is not current_state:
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
self.char_on.set_value(current_state)
self._flag_state = False
@TYPES.register("Valve")
@ -150,7 +153,7 @@ class Valve(HomeAccessory):
def __init__(self, *args):
"""Initialize a Valve accessory object."""
super().__init__(*args)
self._flag_state = False
state = self.hass.states.get(self.entity_id)
valve_type = self.config[CONF_TYPE]
self.category = VALVE_TYPE[valve_type][0]
@ -162,11 +165,13 @@ class Valve(HomeAccessory):
self.char_valve_type = serv_valve.configure_char(
CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1]
)
# Set the state so it is in sync on initial
# GET to avoid an event storm after homekit startup
self.update_state(state)
def set_state(self, value):
"""Move value state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
self._flag_state = True
self.char_in_use.set_value(value)
params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
@ -174,9 +179,10 @@ class Valve(HomeAccessory):
def update_state(self, new_state):
"""Update switch state after state changed."""
current_state = new_state.state == STATE_ON
if not self._flag_state:
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
current_state = 1 if new_state.state == STATE_ON else 0
if self.char_active.value != current_state:
_LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state)
self.char_active.set_value(current_state)
if self.char_in_use.value != current_state:
_LOGGER.debug("%s: Set in_use state to %s", self.entity_id, current_state)
self.char_in_use.set_value(current_state)
self._flag_state = False

View file

@ -147,35 +147,35 @@ async def test_valve_set_state(hass, hk_driver, events):
assert acc.aid == 2
assert acc.category == 29 # Faucet
assert acc.char_active.value is False
assert acc.char_in_use.value is False
assert acc.char_active.value == 0
assert acc.char_in_use.value == 0
assert acc.char_valve_type.value == 0 # Generic Valve
hass.states.async_set(entity_id, STATE_ON)
await hass.async_block_till_done()
assert acc.char_active.value is True
assert acc.char_in_use.value is True
assert acc.char_active.value == 1
assert acc.char_in_use.value == 1
hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done()
assert acc.char_active.value is False
assert acc.char_in_use.value is False
assert acc.char_active.value == 0
assert acc.char_in_use.value == 0
# Set from HomeKit
call_turn_on = async_mock_service(hass, "switch", "turn_on")
call_turn_off = async_mock_service(hass, "switch", "turn_off")
await hass.async_add_executor_job(acc.char_active.client_update_value, True)
await hass.async_add_executor_job(acc.char_active.client_update_value, 1)
await hass.async_block_till_done()
assert acc.char_in_use.value is True
assert acc.char_in_use.value == 1
assert call_turn_on
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 1
assert events[-1].data[ATTR_VALUE] is None
await hass.async_add_executor_job(acc.char_active.client_update_value, False)
await hass.async_add_executor_job(acc.char_active.client_update_value, 0)
await hass.async_block_till_done()
assert acc.char_in_use.value is False
assert acc.char_in_use.value == 0
assert call_turn_off
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 2