From 2b7e735e3d51366bd88168fe08e5d855d9fe1be8 Mon Sep 17 00:00:00 2001 From: Niccolo Zapponi Date: Wed, 16 Sep 2020 16:00:32 +0100 Subject: [PATCH] Remove unsupported states from security systems in HomeKit (#40060) --- .../homekit/type_security_systems.py | 77 ++++++++++- .../homekit/test_type_security_systems.py | 120 ++++++++++++++++++ 2 files changed, 195 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/homekit/type_security_systems.py b/homeassistant/components/homekit/type_security_systems.py index 7d8dcac046d..a5530a45d56 100644 --- a/homeassistant/components/homekit/type_security_systems.py +++ b/homeassistant/components/homekit/type_security_systems.py @@ -2,8 +2,15 @@ import logging from pyhap.const import CATEGORY_ALARM_SYSTEM +from pyhap.loader import get_loader from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.const import ( ATTR_CODE, ATTR_ENTITY_ID, @@ -36,6 +43,13 @@ HASS_TO_HOMEKIT = { STATE_ALARM_TRIGGERED: 4, } +HASS_TO_HOMEKIT_SERVICES = { + SERVICE_ALARM_ARM_HOME: 0, + SERVICE_ALARM_ARM_AWAY: 1, + SERVICE_ALARM_ARM_NIGHT: 2, + SERVICE_ALARM_DISARM: 3, +} + HOMEKIT_TO_HASS = {c: s for s, c in HASS_TO_HOMEKIT.items()} STATE_TO_SERVICE = { @@ -56,13 +70,72 @@ class SecuritySystem(HomeAccessory): state = self.hass.states.get(self.entity_id) self._alarm_code = self.config.get(ATTR_CODE) + supported_states = state.attributes.get( + "supported_features", + ( + SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER + ), + ) + + loader = get_loader() + default_current_states = loader.get_char( + "SecuritySystemCurrentState" + ).properties.get("ValidValues") + default_target_services = loader.get_char( + "SecuritySystemTargetState" + ).properties.get("ValidValues") + + current_supported_states = [ + HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], + HASS_TO_HOMEKIT[STATE_ALARM_TRIGGERED], + ] + target_supported_services = [HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM]] + + if supported_states & SUPPORT_ALARM_ARM_HOME: + current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_HOME]) + target_supported_services.append( + HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_HOME] + ) + + if supported_states & SUPPORT_ALARM_ARM_AWAY: + current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_AWAY]) + target_supported_services.append( + HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_AWAY] + ) + + if supported_states & SUPPORT_ALARM_ARM_NIGHT: + current_supported_states.append(HASS_TO_HOMEKIT[STATE_ALARM_ARMED_NIGHT]) + target_supported_services.append( + HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_ARM_NIGHT] + ) + + new_current_states = { + key: val + for key, val in default_current_states.items() + if val in current_supported_states + } + new_target_services = { + key: val + for key, val in default_target_services.items() + if val in target_supported_services + } + serv_alarm = self.add_preload_service(SERV_SECURITY_SYSTEM) self.char_current_state = serv_alarm.configure_char( - CHAR_CURRENT_SECURITY_STATE, value=3 + CHAR_CURRENT_SECURITY_STATE, + value=HASS_TO_HOMEKIT[STATE_ALARM_DISARMED], + valid_values=new_current_states, ) 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=HASS_TO_HOMEKIT_SERVICES[SERVICE_ALARM_DISARM], + valid_values=new_target_services, + 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.async_update_state(state) diff --git a/tests/components/homekit/test_type_security_systems.py b/tests/components/homekit/test_type_security_systems.py index b139fac3657..d6bf74bb7cf 100644 --- a/tests/components/homekit/test_type_security_systems.py +++ b/tests/components/homekit/test_type_security_systems.py @@ -1,7 +1,14 @@ """Test different accessory types: Security Systems.""" +from pyhap.loader import get_loader import pytest from homeassistant.components.alarm_control_panel import DOMAIN +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, + SUPPORT_ALARM_TRIGGER, +) from homeassistant.components.homekit.const import ATTR_VALUE from homeassistant.components.homekit.type_security_systems import SecuritySystem from homeassistant.const import ( @@ -129,3 +136,116 @@ async def test_no_alarm_code(hass, hk_driver, config, events): assert acc.char_target_state.value == 0 assert len(events) == 1 assert events[-1].data[ATTR_VALUE] is None + + +async def test_supported_states(hass, hk_driver, events): + """Test different supported states.""" + code = "1234" + config = {ATTR_CODE: code} + entity_id = "alarm_control_panel.test" + + loader = get_loader() + default_current_states = loader.get_char( + "SecuritySystemCurrentState" + ).properties.get("ValidValues") + default_target_services = loader.get_char( + "SecuritySystemTargetState" + ).properties.get("ValidValues") + + # Set up a number of test configuration + test_configs = [ + { + "features": SUPPORT_ALARM_ARM_HOME, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_AWAY, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["AwayArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["AwayArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + default_current_states["AwayArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + default_target_services["AwayArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + default_current_states["AwayArm"], + default_current_states["NightArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + default_target_services["AwayArm"], + default_target_services["NightArm"], + ], + }, + { + "features": SUPPORT_ALARM_ARM_HOME + | SUPPORT_ALARM_ARM_AWAY + | SUPPORT_ALARM_ARM_NIGHT + | SUPPORT_ALARM_TRIGGER, + "current_values": [ + default_current_states["Disarmed"], + default_current_states["AlarmTriggered"], + default_current_states["StayArm"], + default_current_states["AwayArm"], + default_current_states["NightArm"], + ], + "target_values": [ + default_target_services["Disarm"], + default_target_services["StayArm"], + default_target_services["AwayArm"], + default_target_services["NightArm"], + ], + }, + ] + + for test_config in test_configs: + attrs = {"supported_features": test_config.get("features")} + + hass.states.async_set(entity_id, None, attributes=attrs) + await hass.async_block_till_done() + + acc = SecuritySystem(hass, hk_driver, "SecuritySystem", entity_id, 2, config) + await acc.run_handler() + await hass.async_block_till_done() + + valid_current_values = acc.char_current_state.properties.get("ValidValues") + valid_target_values = acc.char_target_state.properties.get("ValidValues") + + for val in valid_current_values.values(): + assert val in test_config.get("current_values") + + for val in valid_target_values.values(): + assert val in test_config.get("target_values")