Add google_assistant alarm_control_panel (#26249)
* add alarm_control_panel to google_assistant * add cancel arming option * raise error if requested state is same as current * rework executing command logic * Add tests * fixed tests * fixed level synonyms
This commit is contained in:
parent
4582b6e668
commit
d1adb28c6b
5 changed files with 478 additions and 3 deletions
|
@ -15,6 +15,7 @@ from homeassistant.components import (
|
||||||
sensor,
|
sensor,
|
||||||
switch,
|
switch,
|
||||||
vacuum,
|
vacuum,
|
||||||
|
alarm_control_panel,
|
||||||
)
|
)
|
||||||
|
|
||||||
DOMAIN = "google_assistant"
|
DOMAIN = "google_assistant"
|
||||||
|
@ -48,6 +49,7 @@ DEFAULT_EXPOSED_DOMAINS = [
|
||||||
"lock",
|
"lock",
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
"sensor",
|
"sensor",
|
||||||
|
"alarm_control_panel",
|
||||||
]
|
]
|
||||||
|
|
||||||
PREFIX_TYPES = "action.devices.types."
|
PREFIX_TYPES = "action.devices.types."
|
||||||
|
@ -66,6 +68,7 @@ TYPE_SENSOR = PREFIX_TYPES + "SENSOR"
|
||||||
TYPE_DOOR = PREFIX_TYPES + "DOOR"
|
TYPE_DOOR = PREFIX_TYPES + "DOOR"
|
||||||
TYPE_TV = PREFIX_TYPES + "TV"
|
TYPE_TV = PREFIX_TYPES + "TV"
|
||||||
TYPE_SPEAKER = PREFIX_TYPES + "SPEAKER"
|
TYPE_SPEAKER = PREFIX_TYPES + "SPEAKER"
|
||||||
|
TYPE_ALARM = PREFIX_TYPES + "SECURITYSYSTEM"
|
||||||
|
|
||||||
SERVICE_REQUEST_SYNC = "request_sync"
|
SERVICE_REQUEST_SYNC = "request_sync"
|
||||||
HOMEGRAPH_URL = "https://homegraph.googleapis.com/"
|
HOMEGRAPH_URL = "https://homegraph.googleapis.com/"
|
||||||
|
@ -81,6 +84,9 @@ ERR_PROTOCOL_ERROR = "protocolError"
|
||||||
ERR_UNKNOWN_ERROR = "unknownError"
|
ERR_UNKNOWN_ERROR = "unknownError"
|
||||||
ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported"
|
ERR_FUNCTION_NOT_SUPPORTED = "functionNotSupported"
|
||||||
|
|
||||||
|
ERR_ALREADY_DISARMED = "alreadyDisarmed"
|
||||||
|
ERR_ALREADY_ARMED = "alreadyArmed"
|
||||||
|
|
||||||
ERR_CHALLENGE_NEEDED = "challengeNeeded"
|
ERR_CHALLENGE_NEEDED = "challengeNeeded"
|
||||||
ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup"
|
ERR_CHALLENGE_NOT_SETUP = "challengeFailedNotSetup"
|
||||||
ERR_TOO_MANY_FAILED_ATTEMPTS = "tooManyFailedAttempts"
|
ERR_TOO_MANY_FAILED_ATTEMPTS = "tooManyFailedAttempts"
|
||||||
|
@ -106,6 +112,7 @@ DOMAIN_TO_GOOGLE_TYPES = {
|
||||||
script.DOMAIN: TYPE_SCENE,
|
script.DOMAIN: TYPE_SCENE,
|
||||||
switch.DOMAIN: TYPE_SWITCH,
|
switch.DOMAIN: TYPE_SWITCH,
|
||||||
vacuum.DOMAIN: TYPE_VACUUM,
|
vacuum.DOMAIN: TYPE_VACUUM,
|
||||||
|
alarm_control_panel.DOMAIN: TYPE_ALARM,
|
||||||
}
|
}
|
||||||
|
|
||||||
DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
||||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.components import (
|
||||||
sensor,
|
sensor,
|
||||||
switch,
|
switch,
|
||||||
vacuum,
|
vacuum,
|
||||||
|
alarm_control_panel,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
@ -31,6 +32,20 @@ from homeassistant.const import (
|
||||||
ATTR_SUPPORTED_FEATURES,
|
ATTR_SUPPORTED_FEATURES,
|
||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
ATTR_ASSUMED_STATE,
|
ATTR_ASSUMED_STATE,
|
||||||
|
SERVICE_ALARM_DISARM,
|
||||||
|
SERVICE_ALARM_ARM_HOME,
|
||||||
|
SERVICE_ALARM_ARM_AWAY,
|
||||||
|
SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
|
SERVICE_ALARM_TRIGGER,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
ATTR_CODE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import DOMAIN as HA_DOMAIN
|
from homeassistant.core import DOMAIN as HA_DOMAIN
|
||||||
|
@ -43,6 +58,8 @@ from .const import (
|
||||||
CHALLENGE_ACK_NEEDED,
|
CHALLENGE_ACK_NEEDED,
|
||||||
CHALLENGE_PIN_NEEDED,
|
CHALLENGE_PIN_NEEDED,
|
||||||
CHALLENGE_FAILED_PIN_NEEDED,
|
CHALLENGE_FAILED_PIN_NEEDED,
|
||||||
|
ERR_ALREADY_DISARMED,
|
||||||
|
ERR_ALREADY_ARMED,
|
||||||
)
|
)
|
||||||
from .error import SmartHomeError, ChallengeNeeded
|
from .error import SmartHomeError, ChallengeNeeded
|
||||||
|
|
||||||
|
@ -62,6 +79,7 @@ TRAIT_FANSPEED = PREFIX_TRAITS + "FanSpeed"
|
||||||
TRAIT_MODES = PREFIX_TRAITS + "Modes"
|
TRAIT_MODES = PREFIX_TRAITS + "Modes"
|
||||||
TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose"
|
TRAIT_OPENCLOSE = PREFIX_TRAITS + "OpenClose"
|
||||||
TRAIT_VOLUME = PREFIX_TRAITS + "Volume"
|
TRAIT_VOLUME = PREFIX_TRAITS + "Volume"
|
||||||
|
TRAIT_ARMDISARM = PREFIX_TRAITS + "ArmDisarm"
|
||||||
|
|
||||||
PREFIX_COMMANDS = "action.devices.commands."
|
PREFIX_COMMANDS = "action.devices.commands."
|
||||||
COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff"
|
COMMAND_ONOFF = PREFIX_COMMANDS + "OnOff"
|
||||||
|
@ -85,6 +103,7 @@ COMMAND_MODES = PREFIX_COMMANDS + "SetModes"
|
||||||
COMMAND_OPENCLOSE = PREFIX_COMMANDS + "OpenClose"
|
COMMAND_OPENCLOSE = PREFIX_COMMANDS + "OpenClose"
|
||||||
COMMAND_SET_VOLUME = PREFIX_COMMANDS + "setVolume"
|
COMMAND_SET_VOLUME = PREFIX_COMMANDS + "setVolume"
|
||||||
COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + "volumeRelative"
|
COMMAND_VOLUME_RELATIVE = PREFIX_COMMANDS + "volumeRelative"
|
||||||
|
COMMAND_ARMDISARM = PREFIX_COMMANDS + "ArmDisarm"
|
||||||
|
|
||||||
TRAITS = []
|
TRAITS = []
|
||||||
|
|
||||||
|
@ -873,6 +892,98 @@ class LockUnlockTrait(_Trait):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register_trait
|
||||||
|
class ArmDisArmTrait(_Trait):
|
||||||
|
"""Trait to Arm or Disarm a Security System.
|
||||||
|
|
||||||
|
https://developers.google.com/actions/smarthome/traits/armdisarm
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = TRAIT_ARMDISARM
|
||||||
|
commands = [COMMAND_ARMDISARM]
|
||||||
|
|
||||||
|
state_to_service = {
|
||||||
|
STATE_ALARM_ARMED_HOME: SERVICE_ALARM_ARM_HOME,
|
||||||
|
STATE_ALARM_ARMED_AWAY: SERVICE_ALARM_ARM_AWAY,
|
||||||
|
STATE_ALARM_ARMED_NIGHT: SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS: SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
|
STATE_ALARM_TRIGGERED: SERVICE_ALARM_TRIGGER,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def supported(domain, features, device_class):
|
||||||
|
"""Test if state is supported."""
|
||||||
|
return domain == alarm_control_panel.DOMAIN
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def might_2fa(domain, features, device_class):
|
||||||
|
"""Return if the trait might ask for 2FA."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def sync_attributes(self):
|
||||||
|
"""Return ArmDisarm attributes for a sync request."""
|
||||||
|
response = {}
|
||||||
|
levels = []
|
||||||
|
for state in self.state_to_service:
|
||||||
|
# level synonyms are generated from state names
|
||||||
|
# 'armed_away' becomes 'armed away' or 'away'
|
||||||
|
level_synonym = [state.replace("_", " ")]
|
||||||
|
if state != STATE_ALARM_TRIGGERED:
|
||||||
|
level_synonym.append(state.split("_")[1])
|
||||||
|
|
||||||
|
level = {
|
||||||
|
"level_name": state,
|
||||||
|
"level_values": [{"level_synonym": level_synonym, "lang": "en"}],
|
||||||
|
}
|
||||||
|
levels.append(level)
|
||||||
|
response["availableArmLevels"] = {"levels": levels, "ordered": False}
|
||||||
|
return response
|
||||||
|
|
||||||
|
def query_attributes(self):
|
||||||
|
"""Return ArmDisarm query attributes."""
|
||||||
|
if "post_pending_state" in self.state.attributes:
|
||||||
|
armed_state = self.state.attributes["post_pending_state"]
|
||||||
|
else:
|
||||||
|
armed_state = self.state.state
|
||||||
|
response = {"isArmed": armed_state in self.state_to_service}
|
||||||
|
if response["isArmed"]:
|
||||||
|
response.update({"currentArmLevel": armed_state})
|
||||||
|
return response
|
||||||
|
|
||||||
|
async def execute(self, command, data, params, challenge):
|
||||||
|
"""Execute an ArmDisarm command."""
|
||||||
|
if params["arm"] and not params.get("cancel"):
|
||||||
|
if self.state.state == params["armLevel"]:
|
||||||
|
raise SmartHomeError(ERR_ALREADY_ARMED, "System is already armed")
|
||||||
|
if self.state.attributes["code_arm_required"]:
|
||||||
|
_verify_pin_challenge(data, self.state, challenge)
|
||||||
|
service = self.state_to_service[params["armLevel"]]
|
||||||
|
# disarm the system without asking for code when
|
||||||
|
# 'cancel' arming action is received while current status is pending
|
||||||
|
elif (
|
||||||
|
params["arm"]
|
||||||
|
and params.get("cancel")
|
||||||
|
and self.state.state == STATE_ALARM_PENDING
|
||||||
|
):
|
||||||
|
service = SERVICE_ALARM_DISARM
|
||||||
|
else:
|
||||||
|
if self.state.state == STATE_ALARM_DISARMED:
|
||||||
|
raise SmartHomeError(ERR_ALREADY_DISARMED, "System is already disarmed")
|
||||||
|
_verify_pin_challenge(data, self.state, challenge)
|
||||||
|
service = SERVICE_ALARM_DISARM
|
||||||
|
|
||||||
|
await self.hass.services.async_call(
|
||||||
|
alarm_control_panel.DOMAIN,
|
||||||
|
service,
|
||||||
|
{
|
||||||
|
ATTR_ENTITY_ID: self.state.entity_id,
|
||||||
|
ATTR_CODE: data.config.secure_devices_pin,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
context=data.context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_trait
|
@register_trait
|
||||||
class FanSpeedTrait(_Trait):
|
class FanSpeedTrait(_Trait):
|
||||||
"""Trait to control speed of Fan.
|
"""Trait to control speed of Fan.
|
||||||
|
@ -1343,7 +1454,6 @@ def _verify_pin_challenge(data, state, challenge):
|
||||||
"""Verify a pin challenge."""
|
"""Verify a pin challenge."""
|
||||||
if not data.config.should_2fa(state):
|
if not data.config.should_2fa(state):
|
||||||
return
|
return
|
||||||
|
|
||||||
if not data.config.secure_devices_pin:
|
if not data.config.secure_devices_pin:
|
||||||
raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP, "Challenge is not set up")
|
raise SmartHomeError(ERR_CHALLENGE_NOT_SETUP, "Challenge is not set up")
|
||||||
|
|
||||||
|
|
|
@ -230,4 +230,11 @@ DEMO_DEVICES = [
|
||||||
"type": "action.devices.types.LOCK",
|
"type": "action.devices.types.LOCK",
|
||||||
"willReportState": False,
|
"willReportState": False,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "alarm_control_panel.alarm",
|
||||||
|
"name": {"name": "Alarm"},
|
||||||
|
"traits": ["action.devices.traits.ArmDisarm"],
|
||||||
|
"type": "action.devices.types.SECURITYSYSTEM",
|
||||||
|
"willReportState": False,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,7 +7,15 @@ from aiohttp.hdrs import CONTENT_TYPE, AUTHORIZATION
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant import core, const, setup
|
from homeassistant import core, const, setup
|
||||||
from homeassistant.components import fan, cover, light, switch, lock, media_player
|
from homeassistant.components import (
|
||||||
|
fan,
|
||||||
|
cover,
|
||||||
|
light,
|
||||||
|
switch,
|
||||||
|
lock,
|
||||||
|
media_player,
|
||||||
|
alarm_control_panel,
|
||||||
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.components import google_assistant as ga
|
from homeassistant.components import google_assistant as ga
|
||||||
|
@ -98,6 +106,14 @@ def hass_fixture(loop, hass):
|
||||||
setup.async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]})
|
setup.async_setup_component(hass, lock.DOMAIN, {"lock": [{"platform": "demo"}]})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
loop.run_until_complete(
|
||||||
|
setup.async_setup_component(
|
||||||
|
hass,
|
||||||
|
alarm_control_panel.DOMAIN,
|
||||||
|
{"alarm_control_panel": [{"platform": "demo"}]},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return hass
|
return hass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"""Tests for the Google Assistant traits."""
|
"""Tests for the Google Assistant traits."""
|
||||||
from unittest.mock import patch, Mock
|
from unittest.mock import patch, Mock
|
||||||
|
import logging
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components import (
|
from homeassistant.components import (
|
||||||
|
@ -18,12 +18,16 @@ from homeassistant.components import (
|
||||||
switch,
|
switch,
|
||||||
vacuum,
|
vacuum,
|
||||||
group,
|
group,
|
||||||
|
alarm_control_panel,
|
||||||
)
|
)
|
||||||
from homeassistant.components.climate import const as climate
|
from homeassistant.components.climate import const as climate
|
||||||
from homeassistant.components.google_assistant import trait, helpers, const, error
|
from homeassistant.components.google_assistant import trait, helpers, const, error
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
STATE_OFF,
|
STATE_OFF,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_TURN_ON,
|
SERVICE_TURN_ON,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
|
@ -40,6 +44,7 @@ from homeassistant.util import color
|
||||||
from tests.common import async_mock_service, mock_coro
|
from tests.common import async_mock_service, mock_coro
|
||||||
from . import BASIC_CONFIG, MockConfig
|
from . import BASIC_CONFIG, MockConfig
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
|
REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf"
|
||||||
|
|
||||||
|
@ -816,6 +821,336 @@ async def test_lock_unlock_unlock(hass):
|
||||||
assert len(calls) == 2
|
assert len(calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_arm_disarm_arm_away(hass):
|
||||||
|
"""Test ArmDisarm trait Arming support for alarm_control_panel domain."""
|
||||||
|
assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None
|
||||||
|
assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None)
|
||||||
|
assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None)
|
||||||
|
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
assert trt.sync_attributes() == {
|
||||||
|
"availableArmLevels": {
|
||||||
|
"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_values": [
|
||||||
|
{
|
||||||
|
"level_synonym": ["armed custom bypass", "custom"],
|
||||||
|
"lang": "en",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level_name": "triggered",
|
||||||
|
"level_values": [{"level_synonym": ["triggered"], "lang": "en"}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"ordered": False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert trt.query_attributes() == {
|
||||||
|
"isArmed": True,
|
||||||
|
"currentArmLevel": STATE_ALARM_ARMED_AWAY,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert trt.can_execute(
|
||||||
|
trait.COMMAND_ARMDISARM, {"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY}
|
||||||
|
)
|
||||||
|
|
||||||
|
calls = async_mock_service(
|
||||||
|
hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_ARM_AWAY
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with no secure_pin configured
|
||||||
|
|
||||||
|
with pytest.raises(error.SmartHomeError) as err:
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
BASIC_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM,
|
||||||
|
BASIC_DATA,
|
||||||
|
{"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
|
||||||
|
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
# No challenge data
|
||||||
|
with pytest.raises(error.ChallengeNeeded) as err:
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM,
|
||||||
|
PIN_DATA,
|
||||||
|
{"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
||||||
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
||||||
|
|
||||||
|
# invalid pin
|
||||||
|
with pytest.raises(error.ChallengeNeeded) as err:
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM,
|
||||||
|
PIN_DATA,
|
||||||
|
{"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
|
||||||
|
{"pin": 9999},
|
||||||
|
)
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
||||||
|
assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
||||||
|
|
||||||
|
# correct pin
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM,
|
||||||
|
PIN_DATA,
|
||||||
|
{"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
|
||||||
|
{"pin": "1234"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
# Test already armed
|
||||||
|
with pytest.raises(error.SmartHomeError) as err:
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM,
|
||||||
|
PIN_DATA,
|
||||||
|
{"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert err.value.code == const.ERR_ALREADY_ARMED
|
||||||
|
|
||||||
|
# Test with code_arm_required False
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM,
|
||||||
|
PIN_DATA,
|
||||||
|
{"arm": True, "armLevel": STATE_ALARM_ARMED_AWAY},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
assert len(calls) == 2
|
||||||
|
|
||||||
|
|
||||||
|
async def test_arm_disarm_disarm(hass):
|
||||||
|
"""Test ArmDisarm trait Disarming support for alarm_control_panel domain."""
|
||||||
|
assert helpers.get_google_type(alarm_control_panel.DOMAIN, None) is not None
|
||||||
|
assert trait.ArmDisArmTrait.supported(alarm_control_panel.DOMAIN, 0, None)
|
||||||
|
assert trait.ArmDisArmTrait.might_2fa(alarm_control_panel.DOMAIN, 0, None)
|
||||||
|
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
assert trt.sync_attributes() == {
|
||||||
|
"availableArmLevels": {
|
||||||
|
"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_values": [
|
||||||
|
{
|
||||||
|
"level_synonym": ["armed custom bypass", "custom"],
|
||||||
|
"lang": "en",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level_name": "triggered",
|
||||||
|
"level_values": [{"level_synonym": ["triggered"], "lang": "en"}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"ordered": False,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert trt.query_attributes() == {"isArmed": False}
|
||||||
|
|
||||||
|
assert trt.can_execute(trait.COMMAND_ARMDISARM, {"arm": False})
|
||||||
|
|
||||||
|
calls = async_mock_service(
|
||||||
|
hass, alarm_control_panel.DOMAIN, alarm_control_panel.SERVICE_ALARM_DISARM
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test without secure_pin configured
|
||||||
|
with pytest.raises(error.SmartHomeError) as err:
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
BASIC_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(trait.COMMAND_ARMDISARM, BASIC_DATA, {"arm": False}, {})
|
||||||
|
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NOT_SETUP
|
||||||
|
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
|
||||||
|
# No challenge data
|
||||||
|
with pytest.raises(error.ChallengeNeeded) as err:
|
||||||
|
await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {})
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
||||||
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
||||||
|
|
||||||
|
# invalid pin
|
||||||
|
with pytest.raises(error.ChallengeNeeded) as err:
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": 9999}
|
||||||
|
)
|
||||||
|
assert len(calls) == 0
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
||||||
|
assert err.value.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
||||||
|
|
||||||
|
# correct pin
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {"pin": "1234"}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
# Test already disarmed
|
||||||
|
with pytest.raises(error.SmartHomeError) as err:
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: True},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": False}, {})
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert err.value.code == const.ERR_ALREADY_DISARMED
|
||||||
|
|
||||||
|
# Cancel arming after already armed will require pin
|
||||||
|
with pytest.raises(error.SmartHomeError) as err:
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {}
|
||||||
|
)
|
||||||
|
assert len(calls) == 1
|
||||||
|
assert err.value.code == const.ERR_CHALLENGE_NEEDED
|
||||||
|
assert err.value.challenge_type == const.CHALLENGE_PIN_NEEDED
|
||||||
|
|
||||||
|
# Cancel arming while pending to arm doesn't require pin
|
||||||
|
trt = trait.ArmDisArmTrait(
|
||||||
|
hass,
|
||||||
|
State(
|
||||||
|
"alarm_control_panel.alarm",
|
||||||
|
STATE_ALARM_PENDING,
|
||||||
|
{alarm_control_panel.ATTR_CODE_ARM_REQUIRED: False},
|
||||||
|
),
|
||||||
|
PIN_CONFIG,
|
||||||
|
)
|
||||||
|
await trt.execute(
|
||||||
|
trait.COMMAND_ARMDISARM, PIN_DATA, {"arm": True, "cancel": True}, {}
|
||||||
|
)
|
||||||
|
assert len(calls) == 2
|
||||||
|
|
||||||
|
|
||||||
async def test_fan_speed(hass):
|
async def test_fan_speed(hass):
|
||||||
"""Test FanSpeed trait speed control support for fan domain."""
|
"""Test FanSpeed trait speed control support for fan domain."""
|
||||||
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
assert helpers.get_google_type(fan.DOMAIN, None) is not None
|
||||||
|
|
Loading…
Add table
Reference in a new issue