Handle ArmDisarm execute without arm level (#36942)

This commit is contained in:
Paulus Schoutsen 2020-06-22 16:06:30 -07:00 committed by GitHub
parent 6660cf701d
commit becc011135
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 51 additions and 42 deletions

View file

@ -988,6 +988,14 @@ class ArmDisArmTrait(_Trait):
STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER, STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER,
} }
state_to_support = {
STATE_ALARM_ARMED_HOME: alarm_control_panel.const.SUPPORT_ALARM_ARM_HOME,
STATE_ALARM_ARMED_AWAY: alarm_control_panel.const.SUPPORT_ALARM_ARM_AWAY,
STATE_ALARM_ARMED_NIGHT: alarm_control_panel.const.SUPPORT_ALARM_ARM_NIGHT,
STATE_ALARM_ARMED_CUSTOM_BYPASS: alarm_control_panel.const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
STATE_ALARM_TRIGGERED: alarm_control_panel.const.SUPPORT_ALARM_TRIGGER,
}
@staticmethod @staticmethod
def supported(domain, features, device_class): def supported(domain, features, device_class):
"""Test if state is supported.""" """Test if state is supported."""
@ -998,11 +1006,20 @@ class ArmDisArmTrait(_Trait):
"""Return if the trait might ask for 2FA.""" """Return if the trait might ask for 2FA."""
return True return True
def _supported_states(self):
"""Return supported states."""
features = self.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
return [
state
for state, required_feature in self.state_to_support.items()
if features & required_feature != 0
]
def sync_attributes(self): def sync_attributes(self):
"""Return ArmDisarm attributes for a sync request.""" """Return ArmDisarm attributes for a sync request."""
response = {} response = {}
levels = [] levels = []
for state in self.state_to_service: for state in self._supported_states():
# level synonyms are generated from state names # level synonyms are generated from state names
# 'armed_away' becomes 'armed away' or 'away' # 'armed_away' becomes 'armed away' or 'away'
level_synonym = [state.replace("_", " ")] level_synonym = [state.replace("_", " ")]
@ -1014,6 +1031,7 @@ class ArmDisArmTrait(_Trait):
"level_values": [{"level_synonym": level_synonym, "lang": "en"}], "level_values": [{"level_synonym": level_synonym, "lang": "en"}],
} }
levels.append(level) levels.append(level)
response["availableArmLevels"] = {"levels": levels, "ordered": False} response["availableArmLevels"] = {"levels": levels, "ordered": False}
return response return response
@ -1031,11 +1049,26 @@ class ArmDisArmTrait(_Trait):
async def execute(self, command, data, params, challenge): async def execute(self, command, data, params, challenge):
"""Execute an ArmDisarm command.""" """Execute an ArmDisarm command."""
if params["arm"] and not params.get("cancel"): if params["arm"] and not params.get("cancel"):
if self.state.state == params["armLevel"]: arm_level = params.get("armLevel")
# If no arm level given, we can only arm it if there is
# only one supported arm type. We never default to triggered.
if not arm_level:
states = self._supported_states()
if STATE_ALARM_TRIGGERED in states:
states.remove(STATE_ALARM_TRIGGERED)
if len(states) != 1:
raise SmartHomeError(ERR_NOT_SUPPORTED, "ArmLevel missing")
arm_level = states[0]
if self.state.state == arm_level:
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed") raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed")
if self.state.attributes["code_arm_required"]: if self.state.attributes["code_arm_required"]:
_verify_pin_challenge(data, self.state, challenge) _verify_pin_challenge(data, self.state, challenge)
service = self.state_to_service[params["armLevel"]] service = self.state_to_service[arm_level]
# disarm the system without asking for code when # disarm the system without asking for code when
# 'cancel' arming action is received while current status is pending # 'cancel' arming action is received while current status is pending
elif ( elif (

View file

@ -873,7 +873,11 @@ async def test_arm_disarm_arm_away(hass):
State( State(
"alarm_control_panel.alarm", "alarm_control_panel.alarm",
STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_AWAY,
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, {
alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True,
ATTR_SUPPORTED_FEATURES: alarm_control_panel.const.SUPPORT_ALARM_ARM_HOME
| alarm_control_panel.const.SUPPORT_ALARM_ARM_AWAY,
},
), ),
PIN_CONFIG, PIN_CONFIG,
) )
@ -892,25 +896,6 @@ async def test_arm_disarm_arm_away(hass):
{"level_synonym": ["armed away", "away"], "lang": "en"} {"level_synonym": ["armed away", "away"], "lang": "en"}
], ],
}, },
{
"level_name": "armed_night",
"level_values": [
{"level_synonym": ["armed night", "night"], "lang": "en"}
],
},
{
"level_name": "armed_custom_bypass",
"level_values": [
{
"level_synonym": ["armed custom bypass", "custom"],
"lang": "en",
}
],
},
{
"level_name": "triggered",
"level_values": [{"level_synonym": ["triggered"], "lang": "en"}],
},
], ],
"ordered": False, "ordered": False,
} }
@ -1031,6 +1016,11 @@ async def test_arm_disarm_arm_away(hass):
) )
assert len(calls) == 2 assert len(calls) == 2
with pytest.raises(error.SmartHomeError) as err:
await trt.execute(
trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True}, {},
)
async def test_arm_disarm_disarm(hass): async def test_arm_disarm_disarm(hass):
"""Test ArmDisarm trait Disarming support for alarm_control_panel domain.""" """Test ArmDisarm trait Disarming support for alarm_control_panel domain."""
@ -1043,31 +1033,17 @@ async def test_arm_disarm_disarm(hass):
State( State(
"alarm_control_panel.alarm", "alarm_control_panel.alarm",
STATE_ALARM_DISARMED, STATE_ALARM_DISARMED,
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True}, {
alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True,
ATTR_SUPPORTED_FEATURES: alarm_control_panel.const.SUPPORT_ALARM_TRIGGER
| alarm_control_panel.const.SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
},
), ),
PIN_CONFIG, PIN_CONFIG,
) )
assert trt.sync_attributes() == { assert trt.sync_attributes() == {
"availableArmLevels": { "availableArmLevels": {
"levels": [ "levels": [
{
"level_name": "armed_home",
"level_values": [
{"level_synonym": ["armed home", "home"], "lang": "en"}
],
},
{
"level_name": "armed_away",
"level_values": [
{"level_synonym": ["armed away", "away"], "lang": "en"}
],
},
{
"level_name": "armed_night",
"level_values": [
{"level_synonym": ["armed night", "night"], "lang": "en"}
],
},
{ {
"level_name": "armed_custom_bypass", "level_name": "armed_custom_bypass",
"level_values": [ "level_values": [