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_OPENING = 2
HK_DOOR_CLOSING = 3 HK_DOOR_CLOSING = 3
HK_DOOR_STOPPED = 4 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_CLOSING,
HK_DOOR_OPEN, HK_DOOR_OPEN,
HK_DOOR_OPENING, HK_DOOR_OPENING,
HK_POSITION_GOING_TO_MAX,
HK_POSITION_GOING_TO_MIN,
HK_POSITION_STOPPED,
SERV_GARAGE_DOOR_OPENER, SERV_GARAGE_DOOR_OPENER,
SERV_WINDOW_COVERING, SERV_WINDOW_COVERING,
) )
@ -134,10 +137,9 @@ class WindowCoveringBase(HomeAccessory):
def __init__(self, *args, category): def __init__(self, *args, category):
"""Initialize a WindowCoveringBase accessory object.""" """Initialize a WindowCoveringBase accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING) 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( self.features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
ATTR_SUPPORTED_FEATURES, 0
)
self._supports_stop = self.features & SUPPORT_STOP self._supports_stop = self.features & SUPPORT_STOP
self._homekit_target_tilt = None self._homekit_target_tilt = None
self.chars = [] self.chars = []
@ -192,7 +194,8 @@ class WindowCoveringBase(HomeAccessory):
# We'll have to normalize to [0,100] # We'll have to normalize to [0,100]
current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 current_tilt = (current_tilt / 100.0 * 180.0) - 90.0
current_tilt = int(current_tilt) 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. # 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 # 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( if self._homekit_target_tilt is None or abs(
current_tilt - self._homekit_target_tilt < DEVICE_PRECISION_LEEWAY 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 self._homekit_target_tilt = None
@ -215,7 +219,7 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a WindowCovering accessory object.""" """Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING) super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
state = self.hass.states.get(self.entity_id)
self._homekit_target = None self._homekit_target = None
self.char_current_position = self.serv_cover.configure_char( 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 CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
) )
self.char_position_state = self.serv_cover.configure_char( 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 @debounce
def move_cover(self, value): def move_cover(self, value):
@ -242,7 +247,8 @@ class WindowCovering(WindowCoveringBase, HomeAccessory):
current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) current_position = new_state.attributes.get(ATTR_CURRENT_POSITION)
if isinstance(current_position, (float, int)): if isinstance(current_position, (float, int)):
current_position = int(current_position) 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. # 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 # 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) or abs(current_position - self._homekit_target)
< DEVICE_PRECISION_LEEWAY < 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 self._homekit_target = None
if new_state.state == STATE_OPENING: 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: 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: 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) super().update_state(new_state)
@ -276,7 +286,7 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a WindowCovering accessory object.""" """Initialize a WindowCovering accessory object."""
super().__init__(*args, category=CATEGORY_WINDOW_COVERING) super().__init__(*args, category=CATEGORY_WINDOW_COVERING)
state = self.hass.states.get(self.entity_id)
self.char_current_position = self.serv_cover.configure_char( self.char_current_position = self.serv_cover.configure_char(
CHAR_CURRENT_POSITION, value=0 CHAR_CURRENT_POSITION, value=0
) )
@ -284,8 +294,9 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover CHAR_TARGET_POSITION, value=0, setter_callback=self.move_cover
) )
self.char_position_state = self.serv_cover.configure_char( 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 @debounce
def move_cover(self, value): def move_cover(self, value):
@ -317,13 +328,18 @@ class WindowCoveringBasic(WindowCoveringBase, HomeAccessory):
position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0} position_mapping = {STATE_OPEN: 100, STATE_CLOSED: 0}
hk_position = position_mapping.get(new_state.state) hk_position = position_mapping.get(new_state.state)
if hk_position is not None: if hk_position is not None:
self.char_current_position.set_value(hk_position) if self.char_current_position.value != hk_position:
self.char_target_position.set_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: 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: 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: 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) super().update_state(new_state)

View File

@ -53,8 +53,8 @@ class SecuritySystem(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a SecuritySystem accessory object.""" """Initialize a SecuritySystem accessory object."""
super().__init__(*args, category=CATEGORY_ALARM_SYSTEM) super().__init__(*args, category=CATEGORY_ALARM_SYSTEM)
state = self.hass.states.get(self.entity_id)
self._alarm_code = self.config.get(ATTR_CODE) self._alarm_code = self.config.get(ATTR_CODE)
self._flag_state = False
serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM)
self.char_current_state = serv_alarm.configure_char( self.char_current_state = serv_alarm.configure_char(
@ -63,11 +63,13 @@ class SecuritySystem(HomeAccessory):
self.char_target_state = serv_alarm.configure_char( self.char_target_state = serv_alarm.configure_char(
CHAR_TARGET_SECURITY_STATE, value=3, setter_callback=self.set_security_state 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): def set_security_state(self, value):
"""Move security state to value if call came from HomeKit.""" """Move security state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set security state to %d", self.entity_id, value) _LOGGER.debug("%s: Set security state to %d", self.entity_id, value)
self._flag_state = True
hass_value = HOMEKIT_TO_HASS[value] hass_value = HOMEKIT_TO_HASS[value]
service = STATE_TO_SERVICE[hass_value] service = STATE_TO_SERVICE[hass_value]
@ -81,15 +83,18 @@ class SecuritySystem(HomeAccessory):
hass_state = new_state.state hass_state = new_state.state
if hass_state in HASS_TO_HOMEKIT: if hass_state in HASS_TO_HOMEKIT:
current_security_state = HASS_TO_HOMEKIT[hass_state] current_security_state = HASS_TO_HOMEKIT[hass_state]
self.char_current_state.set_value(current_security_state) if self.char_current_state.value != current_security_state:
_LOGGER.debug( self.char_current_state.set_value(current_security_state)
"%s: Updated current state to %s (%d)", _LOGGER.debug(
self.entity_id, "%s: Updated current state to %s (%d)",
hass_state, self.entity_id,
current_security_state, hass_state,
) current_security_state,
)
# SecuritySystemTargetState does not support triggered # 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.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): def __init__(self, *args):
"""Initialize a TemperatureSensor accessory object.""" """Initialize a TemperatureSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR) serv_temp = self.add_preload_service(SERV_TEMPERATURE_SENSOR)
self.char_temp = serv_temp.configure_char( self.char_temp = serv_temp.configure_char(
CHAR_CURRENT_TEMPERATURE, value=0, properties=PROP_CELSIUS 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): def update_state(self, new_state):
"""Update temperature after state changed.""" """Update temperature after state changed."""
@ -94,10 +98,11 @@ class TemperatureSensor(HomeAccessory):
temperature = convert_to_float(new_state.state) temperature = convert_to_float(new_state.state)
if temperature: if temperature:
temperature = temperature_to_homekit(temperature, unit) temperature = temperature_to_homekit(temperature, unit)
self.char_temp.set_value(temperature) if self.char_temp.value != temperature:
_LOGGER.debug( self.char_temp.set_value(temperature)
"%s: Current temperature set to %.1f°C", self.entity_id, temperature _LOGGER.debug(
) "%s: Current temperature set to %.1f°C", self.entity_id, temperature
)
@TYPES.register("HumiditySensor") @TYPES.register("HumiditySensor")
@ -107,15 +112,19 @@ class HumiditySensor(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a HumiditySensor accessory object.""" """Initialize a HumiditySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR) serv_humidity = self.add_preload_service(SERV_HUMIDITY_SENSOR)
self.char_humidity = serv_humidity.configure_char( self.char_humidity = serv_humidity.configure_char(
CHAR_CURRENT_HUMIDITY, value=0 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): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
humidity = convert_to_float(new_state.state) humidity = convert_to_float(new_state.state)
if humidity: if humidity and self.char_humidity.value != humidity:
self.char_humidity.set_value(humidity) self.char_humidity.set_value(humidity)
_LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity) _LOGGER.debug("%s: Percent set to %d%%", self.entity_id, humidity)
@ -127,7 +136,7 @@ class AirQualitySensor(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a AirQualitySensor accessory object.""" """Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_air_quality = self.add_preload_service( serv_air_quality = self.add_preload_service(
SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY] SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]
) )
@ -135,14 +144,21 @@ class AirQualitySensor(HomeAccessory):
self.char_density = serv_air_quality.configure_char( self.char_density = serv_air_quality.configure_char(
CHAR_AIR_PARTICULATE_DENSITY, value=0 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): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
density = convert_to_float(new_state.state) density = convert_to_float(new_state.state)
if density: if density:
self.char_density.set_value(density) if self.char_density.value != density:
self.char_quality.set_value(density_to_air_quality(density)) self.char_density.set_value(density)
_LOGGER.debug("%s: Set to %d", self.entity_id, 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") @TYPES.register("CarbonMonoxideSensor")
@ -152,7 +168,7 @@ class CarbonMonoxideSensor(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a CarbonMonoxideSensor accessory object.""" """Initialize a CarbonMonoxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_co = self.add_preload_service( serv_co = self.add_preload_service(
SERV_CARBON_MONOXIDE_SENSOR, SERV_CARBON_MONOXIDE_SENSOR,
[CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL], [CHAR_CARBON_MONOXIDE_LEVEL, CHAR_CARBON_MONOXIDE_PEAK_LEVEL],
@ -164,16 +180,22 @@ class CarbonMonoxideSensor(HomeAccessory):
self.char_detected = serv_co.configure_char( self.char_detected = serv_co.configure_char(
CHAR_CARBON_MONOXIDE_DETECTED, value=0 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): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
value = convert_to_float(new_state.state) value = convert_to_float(new_state.state)
if value: 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: if value > self.char_peak.value:
self.char_peak.set_value(value) self.char_peak.set_value(value)
self.char_detected.set_value(value > THRESHOLD_CO) co_detected = value > THRESHOLD_CO
_LOGGER.debug("%s: Set to %d", self.entity_id, value) 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") @TYPES.register("CarbonDioxideSensor")
@ -183,7 +205,7 @@ class CarbonDioxideSensor(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a CarbonDioxideSensor accessory object.""" """Initialize a CarbonDioxideSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_co2 = self.add_preload_service( serv_co2 = self.add_preload_service(
SERV_CARBON_DIOXIDE_SENSOR, SERV_CARBON_DIOXIDE_SENSOR,
[CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL], [CHAR_CARBON_DIOXIDE_LEVEL, CHAR_CARBON_DIOXIDE_PEAK_LEVEL],
@ -195,16 +217,22 @@ class CarbonDioxideSensor(HomeAccessory):
self.char_detected = serv_co2.configure_char( self.char_detected = serv_co2.configure_char(
CHAR_CARBON_DIOXIDE_DETECTED, value=0 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): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
value = convert_to_float(new_state.state) value = convert_to_float(new_state.state)
if value: 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: if value > self.char_peak.value:
self.char_peak.set_value(value) self.char_peak.set_value(value)
self.char_detected.set_value(value > THRESHOLD_CO2) co2_detected = value > THRESHOLD_CO2
_LOGGER.debug("%s: Set to %d", self.entity_id, value) 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") @TYPES.register("LightSensor")
@ -214,16 +242,19 @@ class LightSensor(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a LightSensor accessory object.""" """Initialize a LightSensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id)
serv_light = self.add_preload_service(SERV_LIGHT_SENSOR) serv_light = self.add_preload_service(SERV_LIGHT_SENSOR)
self.char_light = serv_light.configure_char( self.char_light = serv_light.configure_char(
CHAR_CURRENT_AMBIENT_LIGHT_LEVEL, value=0 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): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
luminance = convert_to_float(new_state.state) luminance = convert_to_float(new_state.state)
if luminance: if luminance and self.char_light.value != luminance:
self.char_light.set_value(luminance) self.char_light.set_value(luminance)
_LOGGER.debug("%s: Set to %d", self.entity_id, luminance) _LOGGER.debug("%s: Set to %d", self.entity_id, luminance)
@ -235,9 +266,8 @@ class BinarySensor(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a BinarySensor accessory object.""" """Initialize a BinarySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
device_class = self.hass.states.get(self.entity_id).attributes.get( state = self.hass.states.get(self.entity_id)
ATTR_DEVICE_CLASS device_class = state.attributes.get(ATTR_DEVICE_CLASS)
)
service_char = ( service_char = (
BINARY_SENSOR_SERVICE_MAP[device_class] BINARY_SENSOR_SERVICE_MAP[device_class]
if device_class in BINARY_SENSOR_SERVICE_MAP if device_class in BINARY_SENSOR_SERVICE_MAP
@ -246,10 +276,14 @@ class BinarySensor(HomeAccessory):
service = self.add_preload_service(service_char[0]) service = self.add_preload_service(service_char[0])
self.char_detected = service.configure_char(service_char[1], value=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): def update_state(self, new_state):
"""Update accessory after state change.""" """Update accessory after state change."""
state = new_state.state state = new_state.state
detected = state in (STATE_ON, STATE_HOME) detected = state in (STATE_ON, STATE_HOME)
self.char_detected.set_value(detected) if self.char_detected.value != detected:
_LOGGER.debug("%s: Set to %d", self.entity_id, 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): def __init__(self, *args):
"""Initialize an Outlet accessory object.""" """Initialize an Outlet accessory object."""
super().__init__(*args, category=CATEGORY_OUTLET) 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) serv_outlet = self.add_preload_service(SERV_OUTLET)
self.char_on = serv_outlet.configure_char( self.char_on = serv_outlet.configure_char(
@ -64,11 +64,13 @@ class Outlet(HomeAccessory):
self.char_outlet_in_use = serv_outlet.configure_char( self.char_outlet_in_use = serv_outlet.configure_char(
CHAR_OUTLET_IN_USE, value=True 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): def set_state(self, value):
"""Move switch state to value if call came from HomeKit.""" """Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.call_service(DOMAIN, service, params) self.call_service(DOMAIN, service, params)
@ -76,10 +78,9 @@ class Outlet(HomeAccessory):
def update_state(self, new_state): def update_state(self, new_state):
"""Update switch state after state changed.""" """Update switch state after state changed."""
current_state = new_state.state == STATE_ON 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) _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
self.char_on.set_value(current_state) self.char_on.set_value(current_state)
self._flag_state = False
@TYPES.register("Switch") @TYPES.register("Switch")
@ -90,7 +91,7 @@ class Switch(HomeAccessory):
"""Initialize a Switch accessory object.""" """Initialize a Switch accessory object."""
super().__init__(*args, category=CATEGORY_SWITCH) super().__init__(*args, category=CATEGORY_SWITCH)
self._domain = split_entity_id(self.entity_id)[0] 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)) 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( self.char_on = serv_switch.configure_char(
CHAR_ON, value=False, setter_callback=self.set_state 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): def is_activate(self, state):
"""Check if entity is activate only.""" """Check if entity is activate only."""
@ -111,15 +115,15 @@ class Switch(HomeAccessory):
def reset_switch(self, *args): def reset_switch(self, *args):
"""Reset switch to emulate activate click.""" """Reset switch to emulate activate click."""
_LOGGER.debug("%s: Reset switch to off", self.entity_id) _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): def set_state(self, value):
"""Move switch state to value if call came from HomeKit.""" """Move switch state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) _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) _LOGGER.debug("%s: Ignoring turn_off call", self.entity_id)
return return
self._flag_state = True
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
self.call_service(self._domain, service, params) self.call_service(self._domain, service, params)
@ -137,10 +141,9 @@ class Switch(HomeAccessory):
return return
current_state = new_state.state == STATE_ON 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) _LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state)
self.char_on.set_value(current_state) self.char_on.set_value(current_state)
self._flag_state = False
@TYPES.register("Valve") @TYPES.register("Valve")
@ -150,7 +153,7 @@ class Valve(HomeAccessory):
def __init__(self, *args): def __init__(self, *args):
"""Initialize a Valve accessory object.""" """Initialize a Valve accessory object."""
super().__init__(*args) super().__init__(*args)
self._flag_state = False state = self.hass.states.get(self.entity_id)
valve_type = self.config[CONF_TYPE] valve_type = self.config[CONF_TYPE]
self.category = VALVE_TYPE[valve_type][0] self.category = VALVE_TYPE[valve_type][0]
@ -162,11 +165,13 @@ class Valve(HomeAccessory):
self.char_valve_type = serv_valve.configure_char( self.char_valve_type = serv_valve.configure_char(
CHAR_VALVE_TYPE, value=VALVE_TYPE[valve_type][1] 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): def set_state(self, value):
"""Move value state to value if call came from HomeKit.""" """Move value state to value if call came from HomeKit."""
_LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value)
self._flag_state = True
self.char_in_use.set_value(value) self.char_in_use.set_value(value)
params = {ATTR_ENTITY_ID: self.entity_id} params = {ATTR_ENTITY_ID: self.entity_id}
service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF service = SERVICE_TURN_ON if value else SERVICE_TURN_OFF
@ -174,9 +179,10 @@ class Valve(HomeAccessory):
def update_state(self, new_state): def update_state(self, new_state):
"""Update switch state after state changed.""" """Update switch state after state changed."""
current_state = new_state.state == STATE_ON current_state = 1 if new_state.state == STATE_ON else 0
if not self._flag_state: if self.char_active.value != current_state:
_LOGGER.debug("%s: Set current state to %s", self.entity_id, current_state) _LOGGER.debug("%s: Set active state to %s", self.entity_id, current_state)
self.char_active.set_value(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.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.aid == 2
assert acc.category == 29 # Faucet assert acc.category == 29 # Faucet
assert acc.char_active.value is False assert acc.char_active.value == 0
assert acc.char_in_use.value is False assert acc.char_in_use.value == 0
assert acc.char_valve_type.value == 0 # Generic Valve assert acc.char_valve_type.value == 0 # Generic Valve
hass.states.async_set(entity_id, STATE_ON) hass.states.async_set(entity_id, STATE_ON)
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_active.value is True assert acc.char_active.value == 1
assert acc.char_in_use.value is True assert acc.char_in_use.value == 1
hass.states.async_set(entity_id, STATE_OFF) hass.states.async_set(entity_id, STATE_OFF)
await hass.async_block_till_done() await hass.async_block_till_done()
assert acc.char_active.value is False assert acc.char_active.value == 0
assert acc.char_in_use.value is False assert acc.char_in_use.value == 0
# Set from HomeKit # Set from HomeKit
call_turn_on = async_mock_service(hass, "switch", "turn_on") call_turn_on = async_mock_service(hass, "switch", "turn_on")
call_turn_off = async_mock_service(hass, "switch", "turn_off") 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() 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
assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 1 assert len(events) == 1
assert events[-1].data[ATTR_VALUE] is None 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() 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
assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id
assert len(events) == 2 assert len(events) == 2