mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +00:00
Ask users for a pin when interacting with locks/garage doors (#23223)
* Ask users for a pin when interacting with locks/garage doors * Deprecate allow_unlock option
This commit is contained in:
parent
416af5cf57
commit
0533f56fe3
@ -102,7 +102,7 @@ class CloudClient(Interface):
|
||||
|
||||
self._google_config = ga_h.Config(
|
||||
should_expose=should_expose,
|
||||
allow_unlock=self._prefs.google_allow_unlock,
|
||||
secure_devices_pin=self._prefs.google_secure_devices_pin,
|
||||
entity_config=google_conf.get(CONF_ENTITY_CONFIG),
|
||||
)
|
||||
|
||||
|
@ -5,7 +5,7 @@ REQUEST_TIMEOUT = 10
|
||||
PREF_ENABLE_ALEXA = 'alexa_enabled'
|
||||
PREF_ENABLE_GOOGLE = 'google_enabled'
|
||||
PREF_ENABLE_REMOTE = 'remote_enabled'
|
||||
PREF_GOOGLE_ALLOW_UNLOCK = 'google_allow_unlock'
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN = 'google_secure_devices_pin'
|
||||
PREF_CLOUDHOOKS = 'cloudhooks'
|
||||
PREF_CLOUD_USER = 'cloud_user'
|
||||
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.components.google_assistant import (
|
||||
|
||||
from .const import (
|
||||
DOMAIN, REQUEST_TIMEOUT, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK, InvalidTrustedNetworks)
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN, InvalidTrustedNetworks)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -30,15 +30,6 @@ SCHEMA_WS_STATUS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
})
|
||||
|
||||
|
||||
WS_TYPE_UPDATE_PREFS = 'cloud/update_prefs'
|
||||
SCHEMA_WS_UPDATE_PREFS = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_UPDATE_PREFS,
|
||||
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
||||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||
vol.Optional(PREF_GOOGLE_ALLOW_UNLOCK): bool,
|
||||
})
|
||||
|
||||
|
||||
WS_TYPE_SUBSCRIPTION = 'cloud/subscription'
|
||||
SCHEMA_WS_SUBSCRIPTION = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
|
||||
vol.Required('type'): WS_TYPE_SUBSCRIPTION,
|
||||
@ -77,9 +68,7 @@ async def async_setup(hass):
|
||||
SCHEMA_WS_SUBSCRIPTION
|
||||
)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_UPDATE_PREFS, websocket_update_prefs,
|
||||
SCHEMA_WS_UPDATE_PREFS
|
||||
)
|
||||
websocket_update_prefs)
|
||||
hass.components.websocket_api.async_register_command(
|
||||
WS_TYPE_HOOK_CREATE, websocket_hook_create,
|
||||
SCHEMA_WS_HOOK_CREATE
|
||||
@ -358,6 +347,12 @@ async def websocket_subscription(hass, connection, msg):
|
||||
|
||||
@_require_cloud_login
|
||||
@websocket_api.async_response
|
||||
@websocket_api.websocket_command({
|
||||
vol.Required('type'): 'cloud/update_prefs',
|
||||
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
||||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||
})
|
||||
async def websocket_update_prefs(hass, connection, msg):
|
||||
"""Handle request for account info."""
|
||||
cloud = hass.data[DOMAIN]
|
||||
|
@ -3,7 +3,7 @@ from ipaddress import ip_address
|
||||
|
||||
from .const import (
|
||||
DOMAIN, PREF_ENABLE_ALEXA, PREF_ENABLE_GOOGLE, PREF_ENABLE_REMOTE,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_CLOUDHOOKS, PREF_CLOUD_USER,
|
||||
InvalidTrustedNetworks)
|
||||
|
||||
STORAGE_KEY = DOMAIN
|
||||
@ -29,7 +29,7 @@ class CloudPreferences:
|
||||
PREF_ENABLE_ALEXA: True,
|
||||
PREF_ENABLE_GOOGLE: True,
|
||||
PREF_ENABLE_REMOTE: False,
|
||||
PREF_GOOGLE_ALLOW_UNLOCK: False,
|
||||
PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||
PREF_CLOUDHOOKS: {},
|
||||
PREF_CLOUD_USER: None,
|
||||
}
|
||||
@ -38,14 +38,14 @@ class CloudPreferences:
|
||||
|
||||
async def async_update(self, *, google_enabled=_UNDEF,
|
||||
alexa_enabled=_UNDEF, remote_enabled=_UNDEF,
|
||||
google_allow_unlock=_UNDEF, cloudhooks=_UNDEF,
|
||||
google_secure_devices_pin=_UNDEF, cloudhooks=_UNDEF,
|
||||
cloud_user=_UNDEF):
|
||||
"""Update user preferences."""
|
||||
for key, value in (
|
||||
(PREF_ENABLE_GOOGLE, google_enabled),
|
||||
(PREF_ENABLE_ALEXA, alexa_enabled),
|
||||
(PREF_ENABLE_REMOTE, remote_enabled),
|
||||
(PREF_GOOGLE_ALLOW_UNLOCK, google_allow_unlock),
|
||||
(PREF_GOOGLE_SECURE_DEVICES_PIN, google_secure_devices_pin),
|
||||
(PREF_CLOUDHOOKS, cloudhooks),
|
||||
(PREF_CLOUD_USER, cloud_user),
|
||||
):
|
||||
@ -85,9 +85,9 @@ class CloudPreferences:
|
||||
return self._prefs[PREF_ENABLE_GOOGLE]
|
||||
|
||||
@property
|
||||
def google_allow_unlock(self):
|
||||
def google_secure_devices_pin(self):
|
||||
"""Return if Google is allowed to unlock locks."""
|
||||
return self._prefs.get(PREF_GOOGLE_ALLOW_UNLOCK, False)
|
||||
return self._prefs.get(PREF_GOOGLE_SECURE_DEVICES_PIN)
|
||||
|
||||
@property
|
||||
def cloudhooks(self):
|
||||
|
@ -20,7 +20,7 @@ from .const import (
|
||||
CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS, CONF_API_KEY,
|
||||
SERVICE_REQUEST_SYNC, REQUEST_SYNC_BASE_URL, CONF_ENTITY_CONFIG,
|
||||
CONF_EXPOSE, CONF_ALIASES, CONF_ROOM_HINT, CONF_ALLOW_UNLOCK,
|
||||
DEFAULT_ALLOW_UNLOCK
|
||||
CONF_SECURE_DEVICES_PIN
|
||||
)
|
||||
from .const import EVENT_COMMAND_RECEIVED, EVENT_SYNC_RECEIVED # noqa: F401
|
||||
from .const import EVENT_QUERY_RECEIVED # noqa: F401
|
||||
@ -35,17 +35,20 @@ ENTITY_SCHEMA = vol.Schema({
|
||||
vol.Optional(CONF_ROOM_HINT): cv.string,
|
||||
})
|
||||
|
||||
GOOGLE_ASSISTANT_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_PROJECT_ID): cv.string,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT,
|
||||
default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS,
|
||||
default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA},
|
||||
vol.Optional(CONF_ALLOW_UNLOCK,
|
||||
default=DEFAULT_ALLOW_UNLOCK): cv.boolean,
|
||||
}, extra=vol.PREVENT_EXTRA)
|
||||
GOOGLE_ASSISTANT_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_ALLOW_UNLOCK, invalidation_version='0.95'),
|
||||
vol.Schema({
|
||||
vol.Required(CONF_PROJECT_ID): cv.string,
|
||||
vol.Optional(CONF_EXPOSE_BY_DEFAULT,
|
||||
default=DEFAULT_EXPOSE_BY_DEFAULT): cv.boolean,
|
||||
vol.Optional(CONF_EXPOSED_DOMAINS,
|
||||
default=DEFAULT_EXPOSED_DOMAINS): cv.ensure_list,
|
||||
vol.Optional(CONF_API_KEY): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CONFIG): {cv.entity_id: ENTITY_SCHEMA},
|
||||
vol.Optional(CONF_ALLOW_UNLOCK): cv.boolean,
|
||||
# str on purpose, makes sure it is configured correctly.
|
||||
vol.Optional(CONF_SECURE_DEVICES_PIN): str,
|
||||
}, extra=vol.PREVENT_EXTRA))
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: GOOGLE_ASSISTANT_SCHEMA
|
||||
|
@ -28,13 +28,13 @@ CONF_ALIASES = 'aliases'
|
||||
CONF_API_KEY = 'api_key'
|
||||
CONF_ROOM_HINT = 'room'
|
||||
CONF_ALLOW_UNLOCK = 'allow_unlock'
|
||||
CONF_SECURE_DEVICES_PIN = 'secure_devices_pin'
|
||||
|
||||
DEFAULT_EXPOSE_BY_DEFAULT = True
|
||||
DEFAULT_EXPOSED_DOMAINS = [
|
||||
'climate', 'cover', 'fan', 'group', 'input_boolean', 'light',
|
||||
'media_player', 'scene', 'script', 'switch', 'vacuum', 'lock',
|
||||
]
|
||||
DEFAULT_ALLOW_UNLOCK = False
|
||||
|
||||
PREFIX_TYPES = 'action.devices.types.'
|
||||
TYPE_CAMERA = PREFIX_TYPES + 'CAMERA'
|
||||
@ -55,7 +55,7 @@ HOMEGRAPH_URL = 'https://homegraph.googleapis.com/'
|
||||
REQUEST_SYNC_BASE_URL = HOMEGRAPH_URL + 'v1/devices:requestSync'
|
||||
|
||||
# Error codes used for SmartHomeError class
|
||||
# https://developers.google.com/actions/smarthome/create-app#error_responses
|
||||
# https://developers.google.com/actions/reference/smarthome/errors-exceptions
|
||||
ERR_DEVICE_OFFLINE = "deviceOffline"
|
||||
ERR_DEVICE_NOT_FOUND = "deviceNotFound"
|
||||
ERR_VALUE_OUT_OF_RANGE = "valueOutOfRange"
|
||||
@ -64,6 +64,12 @@ ERR_PROTOCOL_ERROR = 'protocolError'
|
||||
ERR_UNKNOWN_ERROR = 'unknownError'
|
||||
ERR_FUNCTION_NOT_SUPPORTED = 'functionNotSupported'
|
||||
|
||||
ERR_CHALLENGE_NEEDED = 'challengeNeeded'
|
||||
ERR_CHALLENGE_NOT_SETUP = 'challengeFailedNotSetup'
|
||||
ERR_TOO_MANY_FAILED_ATTEMPTS = 'tooManyFailedAttempts'
|
||||
ERR_PIN_INCORRECT = 'pinIncorrect'
|
||||
ERR_USER_CANCELLED = 'userCancelled'
|
||||
|
||||
# Event types
|
||||
EVENT_COMMAND_RECEIVED = 'google_assistant_command'
|
||||
EVENT_QUERY_RECEIVED = 'google_assistant_query'
|
||||
@ -95,5 +101,8 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
|
||||
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_LOCK): TYPE_SENSOR,
|
||||
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_OPENING): TYPE_SENSOR,
|
||||
(binary_sensor.DOMAIN, binary_sensor.DEVICE_CLASS_WINDOW): TYPE_SENSOR,
|
||||
|
||||
}
|
||||
|
||||
CHALLENGE_ACK_NEEDED = 'ackNeeded'
|
||||
CHALLENGE_PIN_NEEDED = 'pinNeeded'
|
||||
CHALLENGE_FAILED_PIN_NEEDED = 'challengeFailedPinNeeded'
|
||||
|
@ -1,4 +1,5 @@
|
||||
"""Errors for Google Assistant."""
|
||||
from .const import ERR_CHALLENGE_NEEDED
|
||||
|
||||
|
||||
class SmartHomeError(Exception):
|
||||
@ -11,3 +12,31 @@ class SmartHomeError(Exception):
|
||||
"""Log error code."""
|
||||
super().__init__(msg)
|
||||
self.code = code
|
||||
|
||||
def to_response(self):
|
||||
"""Convert to a response format."""
|
||||
return {
|
||||
'errorCode': self.code
|
||||
}
|
||||
|
||||
|
||||
class ChallengeNeeded(SmartHomeError):
|
||||
"""Google Assistant Smart Home errors.
|
||||
|
||||
https://developers.google.com/actions/smarthome/create-app#error_responses
|
||||
"""
|
||||
|
||||
def __init__(self, challenge_type):
|
||||
"""Initialize challenge needed error."""
|
||||
super().__init__(ERR_CHALLENGE_NEEDED,
|
||||
'Challenge needed: {}'.format(challenge_type))
|
||||
self.challenge_type = challenge_type
|
||||
|
||||
def to_response(self):
|
||||
"""Convert to a response format."""
|
||||
return {
|
||||
'errorCode': self.code,
|
||||
'challengeNeeded': {
|
||||
'type': self.challenge_type
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ from .error import SmartHomeError
|
||||
class Config:
|
||||
"""Hold the configuration for Google Assistant."""
|
||||
|
||||
def __init__(self, should_expose, allow_unlock,
|
||||
entity_config=None):
|
||||
def __init__(self, should_expose,
|
||||
entity_config=None, secure_devices_pin=None):
|
||||
"""Initialize the configuration."""
|
||||
self.should_expose = should_expose
|
||||
self.entity_config = entity_config or {}
|
||||
self.allow_unlock = allow_unlock
|
||||
self.secure_devices_pin = secure_devices_pin
|
||||
|
||||
|
||||
class RequestData:
|
||||
@ -168,15 +168,18 @@ class GoogleEntity:
|
||||
|
||||
return attrs
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, data, command_payload):
|
||||
"""Execute a command.
|
||||
|
||||
https://developers.google.com/actions/smarthome/create-app#actiondevicesexecute
|
||||
"""
|
||||
command = command_payload['command']
|
||||
params = command_payload.get('params', {})
|
||||
challenge = command_payload.get('challenge', {})
|
||||
executed = False
|
||||
for trt in self.traits():
|
||||
if trt.can_execute(command, params):
|
||||
await trt.execute(command, data, params)
|
||||
await trt.execute(command, data, params, challenge)
|
||||
executed = True
|
||||
break
|
||||
|
||||
|
@ -10,12 +10,12 @@ from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
|
||||
from .const import (
|
||||
GOOGLE_ASSISTANT_API_ENDPOINT,
|
||||
CONF_ALLOW_UNLOCK,
|
||||
CONF_EXPOSE_BY_DEFAULT,
|
||||
CONF_EXPOSED_DOMAINS,
|
||||
CONF_ENTITY_CONFIG,
|
||||
CONF_EXPOSE,
|
||||
)
|
||||
CONF_SECURE_DEVICES_PIN,
|
||||
)
|
||||
from .smart_home import async_handle_message
|
||||
from .helpers import Config
|
||||
|
||||
@ -28,7 +28,7 @@ def async_register_http(hass, cfg):
|
||||
expose_by_default = cfg.get(CONF_EXPOSE_BY_DEFAULT)
|
||||
exposed_domains = cfg.get(CONF_EXPOSED_DOMAINS)
|
||||
entity_config = cfg.get(CONF_ENTITY_CONFIG) or {}
|
||||
allow_unlock = cfg.get(CONF_ALLOW_UNLOCK, False)
|
||||
secure_devices_pin = cfg.get(CONF_SECURE_DEVICES_PIN)
|
||||
|
||||
def is_exposed(entity) -> bool:
|
||||
"""Determine if an entity should be exposed to Google Assistant."""
|
||||
@ -53,8 +53,13 @@ def async_register_http(hass, cfg):
|
||||
|
||||
return is_default_exposed or explicit_expose
|
||||
|
||||
hass.http.register_view(
|
||||
GoogleAssistantView(is_exposed, entity_config, allow_unlock))
|
||||
config = Config(
|
||||
should_expose=is_exposed,
|
||||
entity_config=entity_config,
|
||||
secure_devices_pin=secure_devices_pin
|
||||
)
|
||||
|
||||
hass.http.register_view(GoogleAssistantView(config))
|
||||
|
||||
|
||||
class GoogleAssistantView(HomeAssistantView):
|
||||
@ -64,11 +69,9 @@ class GoogleAssistantView(HomeAssistantView):
|
||||
name = 'api:google_assistant'
|
||||
requires_auth = True
|
||||
|
||||
def __init__(self, is_exposed, entity_config, allow_unlock):
|
||||
def __init__(self, config):
|
||||
"""Initialize the Google Assistant request handler."""
|
||||
self.config = Config(is_exposed,
|
||||
allow_unlock,
|
||||
entity_config)
|
||||
self.config = config
|
||||
|
||||
async def post(self, request: Request) -> Response:
|
||||
"""Handle Google Assistant requests."""
|
||||
|
@ -177,14 +177,12 @@ async def handle_devices_execute(hass, data, payload):
|
||||
entities[entity_id] = GoogleEntity(hass, data.config, state)
|
||||
|
||||
try:
|
||||
await entities[entity_id].execute(execution['command'],
|
||||
data,
|
||||
execution.get('params', {}))
|
||||
await entities[entity_id].execute(data, execution)
|
||||
except SmartHomeError as err:
|
||||
results[entity_id] = {
|
||||
'ids': [entity_id],
|
||||
'status': 'ERROR',
|
||||
'errorCode': err.code
|
||||
**err.to_response()
|
||||
}
|
||||
|
||||
final_results = list(results.values())
|
||||
|
@ -19,6 +19,7 @@ from homeassistant.components import (
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_DEVICE_CLASS,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_LOCKED,
|
||||
@ -37,8 +38,12 @@ from .const import (
|
||||
ERR_VALUE_OUT_OF_RANGE,
|
||||
ERR_NOT_SUPPORTED,
|
||||
ERR_FUNCTION_NOT_SUPPORTED,
|
||||
ERR_CHALLENGE_NOT_SETUP,
|
||||
CHALLENGE_ACK_NEEDED,
|
||||
CHALLENGE_PIN_NEEDED,
|
||||
CHALLENGE_FAILED_PIN_NEEDED,
|
||||
)
|
||||
from .error import SmartHomeError
|
||||
from .error import SmartHomeError, ChallengeNeeded
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -114,7 +119,7 @@ class _Trait:
|
||||
"""Test if command can be executed."""
|
||||
return command in self.commands
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a trait command."""
|
||||
raise NotImplementedError
|
||||
|
||||
@ -164,7 +169,7 @@ class BrightnessTrait(_Trait):
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a brightness command."""
|
||||
domain = self.state.domain
|
||||
|
||||
@ -219,7 +224,7 @@ class CameraStreamTrait(_Trait):
|
||||
"""Return camera stream attributes."""
|
||||
return self.stream_info or {}
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a get camera stream command."""
|
||||
url = await self.hass.components.camera.async_request_stream(
|
||||
self.state.entity_id, 'hls')
|
||||
@ -260,7 +265,7 @@ class OnOffTrait(_Trait):
|
||||
"""Return OnOff query attributes."""
|
||||
return {'on': self.state.state != STATE_OFF}
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an OnOff command."""
|
||||
domain = self.state.domain
|
||||
|
||||
@ -353,7 +358,7 @@ class ColorSettingTrait(_Trait):
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a color temperature command."""
|
||||
if 'temperature' in params['color']:
|
||||
temp = color_util.color_temperature_kelvin_to_mired(
|
||||
@ -424,7 +429,7 @@ class SceneTrait(_Trait):
|
||||
"""Return scene query attributes."""
|
||||
return {}
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a scene command."""
|
||||
# Don't block for scripts as they can be slow.
|
||||
await self.hass.services.async_call(
|
||||
@ -459,7 +464,7 @@ class DockTrait(_Trait):
|
||||
"""Return dock query attributes."""
|
||||
return {'isDocked': self.state.state == vacuum.STATE_DOCKED}
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a dock command."""
|
||||
await self.hass.services.async_call(
|
||||
self.state.domain, vacuum.SERVICE_RETURN_TO_BASE, {
|
||||
@ -498,7 +503,7 @@ class StartStopTrait(_Trait):
|
||||
'isPaused': self.state.state == vacuum.STATE_PAUSED,
|
||||
}
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a StartStop command."""
|
||||
if command == COMMAND_STARTSTOP:
|
||||
if params['start']:
|
||||
@ -634,7 +639,7 @@ class TemperatureSettingTrait(_Trait):
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute a temperature point or mode command."""
|
||||
# All sent in temperatures are always in Celsius
|
||||
unit = self.hass.config.units.temperature_unit
|
||||
@ -748,13 +753,10 @@ class LockUnlockTrait(_Trait):
|
||||
"""Return LockUnlock query attributes."""
|
||||
return {'isLocked': self.state.state == STATE_LOCKED}
|
||||
|
||||
def can_execute(self, command, params):
|
||||
"""Test if command can be executed."""
|
||||
allowed_unlock = not params['lock'] and self.config.allow_unlock
|
||||
return params['lock'] or allowed_unlock
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an LockUnlock command."""
|
||||
_verify_pin_challenge(data, challenge)
|
||||
|
||||
if params['lock']:
|
||||
service = lock.SERVICE_LOCK
|
||||
else:
|
||||
@ -832,7 +834,7 @@ class FanSpeedTrait(_Trait):
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an SetFanSpeed command."""
|
||||
await self.hass.services.async_call(
|
||||
fan.DOMAIN, fan.SERVICE_SET_SPEED, {
|
||||
@ -1006,7 +1008,7 @@ class ModesTrait(_Trait):
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an SetModes command."""
|
||||
settings = params.get('updateModeSettings')
|
||||
requested_source = settings.get(
|
||||
@ -1097,11 +1099,16 @@ class OpenCloseTrait(_Trait):
|
||||
|
||||
return response
|
||||
|
||||
async def execute(self, command, data, params):
|
||||
async def execute(self, command, data, params, challenge):
|
||||
"""Execute an Open, close, Set position command."""
|
||||
domain = self.state.domain
|
||||
|
||||
if domain == cover.DOMAIN:
|
||||
if self.state.attributes.get(ATTR_DEVICE_CLASS) in (
|
||||
cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE
|
||||
):
|
||||
_verify_pin_challenge(data, challenge)
|
||||
|
||||
position = self.state.attributes.get(cover.ATTR_CURRENT_POSITION)
|
||||
if params['openPercent'] == 0:
|
||||
await self.hass.services.async_call(
|
||||
@ -1123,3 +1130,24 @@ class OpenCloseTrait(_Trait):
|
||||
raise SmartHomeError(
|
||||
ERR_FUNCTION_NOT_SUPPORTED,
|
||||
'Setting a position is not supported')
|
||||
|
||||
|
||||
def _verify_pin_challenge(data, challenge):
|
||||
"""Verify a pin challenge."""
|
||||
if not data.config.secure_devices_pin:
|
||||
raise SmartHomeError(
|
||||
ERR_CHALLENGE_NOT_SETUP, 'Challenge is not set up')
|
||||
|
||||
if not challenge:
|
||||
raise ChallengeNeeded(CHALLENGE_PIN_NEEDED)
|
||||
|
||||
pin = challenge.get('pin')
|
||||
|
||||
if pin != data.config.secure_devices_pin:
|
||||
raise ChallengeNeeded(CHALLENGE_FAILED_PIN_NEEDED)
|
||||
|
||||
|
||||
def _verify_ack_challenge(data, challenge):
|
||||
"""Verify a pin challenge."""
|
||||
if not challenge or not challenge.get('ack'):
|
||||
raise ChallengeNeeded(CHALLENGE_ACK_NEEDED)
|
||||
|
@ -26,7 +26,7 @@ def mock_cloud_prefs(hass, prefs={}):
|
||||
prefs_to_set = {
|
||||
const.PREF_ENABLE_ALEXA: True,
|
||||
const.PREF_ENABLE_GOOGLE: True,
|
||||
const.PREF_GOOGLE_ALLOW_UNLOCK: True,
|
||||
const.PREF_GOOGLE_SECURE_DEVICES_PIN: None,
|
||||
}
|
||||
prefs_to_set.update(prefs)
|
||||
hass.data[cloud.DOMAIN].client._prefs._prefs = prefs_to_set
|
||||
|
@ -9,7 +9,8 @@ from hass_nabucasa.const import STATE_CONNECTED
|
||||
|
||||
from homeassistant.auth.providers import trusted_networks as tn_auth
|
||||
from homeassistant.components.cloud.const import (
|
||||
PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_ALLOW_UNLOCK, DOMAIN)
|
||||
PREF_ENABLE_GOOGLE, PREF_ENABLE_ALEXA, PREF_GOOGLE_SECURE_DEVICES_PIN,
|
||||
DOMAIN)
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
@ -493,21 +494,21 @@ async def test_websocket_update_preferences(hass, hass_ws_client,
|
||||
"""Test updating preference."""
|
||||
assert setup_api[PREF_ENABLE_GOOGLE]
|
||||
assert setup_api[PREF_ENABLE_ALEXA]
|
||||
assert setup_api[PREF_GOOGLE_ALLOW_UNLOCK]
|
||||
assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] is None
|
||||
client = await hass_ws_client(hass)
|
||||
await client.send_json({
|
||||
'id': 5,
|
||||
'type': 'cloud/update_prefs',
|
||||
'alexa_enabled': False,
|
||||
'google_enabled': False,
|
||||
'google_allow_unlock': False,
|
||||
'google_secure_devices_pin': '1234',
|
||||
})
|
||||
response = await client.receive_json()
|
||||
|
||||
assert response['success']
|
||||
assert not setup_api[PREF_ENABLE_GOOGLE]
|
||||
assert not setup_api[PREF_ENABLE_ALEXA]
|
||||
assert not setup_api[PREF_GOOGLE_ALLOW_UNLOCK]
|
||||
assert setup_api[PREF_GOOGLE_SECURE_DEVICES_PIN] == '1234'
|
||||
|
||||
|
||||
async def test_enabling_webhook(hass, hass_ws_client, setup_api,
|
||||
|
@ -22,7 +22,6 @@ from tests.common import (mock_device_registry, mock_registry,
|
||||
|
||||
BASIC_CONFIG = helpers.Config(
|
||||
should_expose=lambda state: True,
|
||||
allow_unlock=False
|
||||
)
|
||||
REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf'
|
||||
|
||||
@ -57,7 +56,6 @@ async def test_sync_message(hass):
|
||||
|
||||
config = helpers.Config(
|
||||
should_expose=lambda state: state.entity_id != 'light.not_expose',
|
||||
allow_unlock=False,
|
||||
entity_config={
|
||||
'light.demo_light': {
|
||||
const.CONF_ROOM_HINT: 'Living Room',
|
||||
@ -146,7 +144,6 @@ async def test_sync_in_area(hass, registries):
|
||||
|
||||
config = helpers.Config(
|
||||
should_expose=lambda _: True,
|
||||
allow_unlock=False,
|
||||
entity_config={}
|
||||
)
|
||||
|
||||
|
@ -19,7 +19,8 @@ from homeassistant.components import (
|
||||
group,
|
||||
)
|
||||
from homeassistant.components.climate import const as climate
|
||||
from homeassistant.components.google_assistant import trait, helpers, const
|
||||
from homeassistant.components.google_assistant import (
|
||||
trait, helpers, const, error)
|
||||
from homeassistant.const import (
|
||||
STATE_ON, STATE_OFF, ATTR_ENTITY_ID, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
TEMP_CELSIUS, TEMP_FAHRENHEIT, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE,
|
||||
@ -30,7 +31,6 @@ from tests.common import async_mock_service, mock_coro
|
||||
|
||||
BASIC_CONFIG = helpers.Config(
|
||||
should_expose=lambda state: True,
|
||||
allow_unlock=False
|
||||
)
|
||||
|
||||
REQ_ID = 'ff36a3cc-ec34-11e6-b1a0-64510650abcf'
|
||||
@ -41,9 +41,15 @@ BASIC_DATA = helpers.RequestData(
|
||||
REQ_ID,
|
||||
)
|
||||
|
||||
UNSAFE_CONFIG = helpers.Config(
|
||||
PIN_CONFIG = helpers.Config(
|
||||
should_expose=lambda state: True,
|
||||
allow_unlock=True,
|
||||
secure_devices_pin='1234'
|
||||
)
|
||||
|
||||
PIN_DATA = helpers.RequestData(
|
||||
PIN_CONFIG,
|
||||
'test-agent',
|
||||
REQ_ID,
|
||||
)
|
||||
|
||||
|
||||
@ -69,7 +75,7 @@ async def test_brightness_light(hass):
|
||||
calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON)
|
||||
await trt.execute(
|
||||
trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA,
|
||||
{'brightness': 50})
|
||||
{'brightness': 50}, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
@ -108,7 +114,7 @@ async def test_brightness_media_player(hass):
|
||||
hass, media_player.DOMAIN, media_player.SERVICE_VOLUME_SET)
|
||||
await trt.execute(
|
||||
trait.COMMAND_BRIGHTNESS_ABSOLUTE, BASIC_DATA,
|
||||
{'brightness': 60})
|
||||
{'brightness': 60}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
@ -139,7 +145,7 @@ async def test_camera_stream(hass):
|
||||
|
||||
with patch('homeassistant.components.camera.async_request_stream',
|
||||
return_value=mock_coro('/api/streams/bla')):
|
||||
await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {})
|
||||
await trt.execute(trait.COMMAND_GET_CAMERA_STREAM, BASIC_DATA, {}, {})
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'cameraStreamAccessUrl': 'http://1.1.1.1:8123/api/streams/bla'
|
||||
@ -169,7 +175,7 @@ async def test_onoff_group(hass):
|
||||
on_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_ON)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': True})
|
||||
{'on': True}, {})
|
||||
assert len(on_calls) == 1
|
||||
assert on_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'group.bla',
|
||||
@ -178,7 +184,7 @@ async def test_onoff_group(hass):
|
||||
off_calls = async_mock_service(hass, HA_DOMAIN, SERVICE_TURN_OFF)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': False})
|
||||
{'on': False}, {})
|
||||
assert len(off_calls) == 1
|
||||
assert off_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'group.bla',
|
||||
@ -209,7 +215,7 @@ async def test_onoff_input_boolean(hass):
|
||||
on_calls = async_mock_service(hass, input_boolean.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': True})
|
||||
{'on': True}, {})
|
||||
assert len(on_calls) == 1
|
||||
assert on_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'input_boolean.bla',
|
||||
@ -219,7 +225,7 @@ async def test_onoff_input_boolean(hass):
|
||||
SERVICE_TURN_OFF)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': False})
|
||||
{'on': False}, {})
|
||||
assert len(off_calls) == 1
|
||||
assert off_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'input_boolean.bla',
|
||||
@ -250,7 +256,7 @@ async def test_onoff_switch(hass):
|
||||
on_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': True})
|
||||
{'on': True}, {})
|
||||
assert len(on_calls) == 1
|
||||
assert on_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'switch.bla',
|
||||
@ -259,7 +265,7 @@ async def test_onoff_switch(hass):
|
||||
off_calls = async_mock_service(hass, switch.DOMAIN, SERVICE_TURN_OFF)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': False})
|
||||
{'on': False}, {})
|
||||
assert len(off_calls) == 1
|
||||
assert off_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'switch.bla',
|
||||
@ -287,7 +293,7 @@ async def test_onoff_fan(hass):
|
||||
on_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': True})
|
||||
{'on': True}, {})
|
||||
assert len(on_calls) == 1
|
||||
assert on_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'fan.bla',
|
||||
@ -296,7 +302,7 @@ async def test_onoff_fan(hass):
|
||||
off_calls = async_mock_service(hass, fan.DOMAIN, SERVICE_TURN_OFF)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': False})
|
||||
{'on': False}, {})
|
||||
assert len(off_calls) == 1
|
||||
assert off_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'fan.bla',
|
||||
@ -326,7 +332,7 @@ async def test_onoff_light(hass):
|
||||
on_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': True})
|
||||
{'on': True}, {})
|
||||
assert len(on_calls) == 1
|
||||
assert on_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'light.bla',
|
||||
@ -335,7 +341,7 @@ async def test_onoff_light(hass):
|
||||
off_calls = async_mock_service(hass, light.DOMAIN, SERVICE_TURN_OFF)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': False})
|
||||
{'on': False}, {})
|
||||
assert len(off_calls) == 1
|
||||
assert off_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'light.bla',
|
||||
@ -366,7 +372,7 @@ async def test_onoff_media_player(hass):
|
||||
on_calls = async_mock_service(hass, media_player.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': True})
|
||||
{'on': True}, {})
|
||||
assert len(on_calls) == 1
|
||||
assert on_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
@ -377,7 +383,7 @@ async def test_onoff_media_player(hass):
|
||||
|
||||
await trt_on.execute(
|
||||
trait.COMMAND_ONOFF, BASIC_DATA,
|
||||
{'on': False})
|
||||
{'on': False}, {})
|
||||
assert len(off_calls) == 1
|
||||
assert off_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'media_player.bla',
|
||||
@ -408,7 +414,7 @@ async def test_dock_vacuum(hass):
|
||||
calls = async_mock_service(hass, vacuum.DOMAIN,
|
||||
vacuum.SERVICE_RETURN_TO_BASE)
|
||||
await trt.execute(
|
||||
trait.COMMAND_DOCK, BASIC_DATA, {})
|
||||
trait.COMMAND_DOCK, BASIC_DATA, {}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'vacuum.bla',
|
||||
@ -433,7 +439,7 @@ async def test_startstop_vacuum(hass):
|
||||
|
||||
start_calls = async_mock_service(hass, vacuum.DOMAIN,
|
||||
vacuum.SERVICE_START)
|
||||
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': True})
|
||||
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': True}, {})
|
||||
assert len(start_calls) == 1
|
||||
assert start_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'vacuum.bla',
|
||||
@ -441,7 +447,8 @@ async def test_startstop_vacuum(hass):
|
||||
|
||||
stop_calls = async_mock_service(hass, vacuum.DOMAIN,
|
||||
vacuum.SERVICE_STOP)
|
||||
await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': False})
|
||||
await trt.execute(
|
||||
trait.COMMAND_STARTSTOP, BASIC_DATA, {'start': False}, {})
|
||||
assert len(stop_calls) == 1
|
||||
assert stop_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'vacuum.bla',
|
||||
@ -449,7 +456,8 @@ async def test_startstop_vacuum(hass):
|
||||
|
||||
pause_calls = async_mock_service(hass, vacuum.DOMAIN,
|
||||
vacuum.SERVICE_PAUSE)
|
||||
await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': True})
|
||||
await trt.execute(
|
||||
trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': True}, {})
|
||||
assert len(pause_calls) == 1
|
||||
assert pause_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'vacuum.bla',
|
||||
@ -457,7 +465,8 @@ async def test_startstop_vacuum(hass):
|
||||
|
||||
unpause_calls = async_mock_service(hass, vacuum.DOMAIN,
|
||||
vacuum.SERVICE_START)
|
||||
await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': False})
|
||||
await trt.execute(
|
||||
trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {'pause': False}, {})
|
||||
assert len(unpause_calls) == 1
|
||||
assert unpause_calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'vacuum.bla',
|
||||
@ -502,7 +511,7 @@ async def test_color_setting_color_light(hass):
|
||||
'color': {
|
||||
'spectrumRGB': 1052927
|
||||
}
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'light.bla',
|
||||
@ -517,7 +526,7 @@ async def test_color_setting_color_light(hass):
|
||||
'value': .20,
|
||||
}
|
||||
}
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 2
|
||||
assert calls[1].data == {
|
||||
ATTR_ENTITY_ID: 'light.bla',
|
||||
@ -565,14 +574,14 @@ async def test_color_setting_temperature_light(hass):
|
||||
'color': {
|
||||
'temperature': 5555
|
||||
}
|
||||
})
|
||||
}, {})
|
||||
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
|
||||
|
||||
await trt.execute(trait.COMMAND_COLOR_ABSOLUTE, BASIC_DATA, {
|
||||
'color': {
|
||||
'temperature': 2857
|
||||
}
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'light.bla',
|
||||
@ -608,7 +617,7 @@ async def test_scene_scene(hass):
|
||||
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
||||
|
||||
calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {})
|
||||
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'scene.bla',
|
||||
@ -626,7 +635,7 @@ async def test_scene_script(hass):
|
||||
assert trt.can_execute(trait.COMMAND_ACTIVATE_SCENE, {})
|
||||
|
||||
calls = async_mock_service(hass, script.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {})
|
||||
await trt.execute(trait.COMMAND_ACTIVATE_SCENE, BASIC_DATA, {}, {})
|
||||
|
||||
# We don't wait till script execution is done.
|
||||
await hass.async_block_till_done()
|
||||
@ -671,14 +680,14 @@ async def test_temperature_setting_climate_onoff(hass):
|
||||
hass, climate.DOMAIN, SERVICE_TURN_ON)
|
||||
await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {
|
||||
'thermostatMode': 'on',
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 1
|
||||
|
||||
calls = async_mock_service(
|
||||
hass, climate.DOMAIN, SERVICE_TURN_OFF)
|
||||
await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {
|
||||
'thermostatMode': 'off',
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 1
|
||||
|
||||
|
||||
@ -731,7 +740,7 @@ async def test_temperature_setting_climate_range(hass):
|
||||
trait.COMMAND_THERMOSTAT_TEMPERATURE_SET_RANGE, BASIC_DATA, {
|
||||
'thermostatTemperatureSetpointHigh': 25,
|
||||
'thermostatTemperatureSetpointLow': 20,
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'climate.bla',
|
||||
@ -743,7 +752,7 @@ async def test_temperature_setting_climate_range(hass):
|
||||
hass, climate.DOMAIN, climate.SERVICE_SET_OPERATION_MODE)
|
||||
await trt.execute(trait.COMMAND_THERMOSTAT_SET_MODE, BASIC_DATA, {
|
||||
'thermostatMode': 'heatcool',
|
||||
})
|
||||
}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'climate.bla',
|
||||
@ -753,7 +762,7 @@ async def test_temperature_setting_climate_range(hass):
|
||||
with pytest.raises(helpers.SmartHomeError) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
|
||||
{'thermostatTemperatureSetpoint': -100})
|
||||
{'thermostatTemperatureSetpoint': -100}, {})
|
||||
assert err.value.code == const.ERR_VALUE_OUT_OF_RANGE
|
||||
hass.config.units.temperature_unit = TEMP_CELSIUS
|
||||
|
||||
@ -799,11 +808,11 @@ async def test_temperature_setting_climate_setpoint(hass):
|
||||
with pytest.raises(helpers.SmartHomeError):
|
||||
await trt.execute(
|
||||
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
|
||||
{'thermostatTemperatureSetpoint': -100})
|
||||
{'thermostatTemperatureSetpoint': -100}, {})
|
||||
|
||||
await trt.execute(
|
||||
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
|
||||
{'thermostatTemperatureSetpoint': 19})
|
||||
{'thermostatTemperatureSetpoint': 19}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'climate.bla',
|
||||
@ -851,7 +860,7 @@ async def test_temperature_setting_climate_setpoint_auto(hass):
|
||||
|
||||
await trt.execute(
|
||||
trait.COMMAND_THERMOSTAT_TEMPERATURE_SETPOINT, BASIC_DATA,
|
||||
{'thermostatTemperatureSetpoint': 19})
|
||||
{'thermostatTemperatureSetpoint': 19}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'climate.bla',
|
||||
@ -867,7 +876,7 @@ async def test_lock_unlock_lock(hass):
|
||||
|
||||
trt = trait.LockUnlockTrait(hass,
|
||||
State('lock.front_door', lock.STATE_UNLOCKED),
|
||||
BASIC_CONFIG)
|
||||
PIN_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
@ -878,7 +887,26 @@ async def test_lock_unlock_lock(hass):
|
||||
assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': True})
|
||||
|
||||
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_LOCK)
|
||||
await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': True})
|
||||
|
||||
# No challenge data
|
||||
with pytest.raises(error.ChallengeNeeded) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': True}, {})
|
||||
assert len(calls) == 0
|
||||
assert err.code == const.ERR_CHALLENGE_NEEDED
|
||||
assert err.challenge_type == const.CHALLENGE_PIN_NEEDED
|
||||
|
||||
# invalid pin
|
||||
with pytest.raises(error.ChallengeNeeded) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': True},
|
||||
{'pin': 9999})
|
||||
assert len(calls) == 0
|
||||
assert err.code == const.ERR_CHALLENGE_NEEDED
|
||||
assert err.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
||||
|
||||
await trt.execute(trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': True},
|
||||
{'pin': '1234'})
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
@ -894,19 +922,7 @@ async def test_lock_unlock_unlock(hass):
|
||||
|
||||
trt = trait.LockUnlockTrait(hass,
|
||||
State('lock.front_door', lock.STATE_LOCKED),
|
||||
BASIC_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
assert trt.query_attributes() == {
|
||||
'isLocked': True
|
||||
}
|
||||
|
||||
assert not trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False})
|
||||
|
||||
trt = trait.LockUnlockTrait(hass,
|
||||
State('lock.front_door', lock.STATE_LOCKED),
|
||||
UNSAFE_CONFIG)
|
||||
PIN_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
|
||||
@ -917,7 +933,26 @@ async def test_lock_unlock_unlock(hass):
|
||||
assert trt.can_execute(trait.COMMAND_LOCKUNLOCK, {'lock': False})
|
||||
|
||||
calls = async_mock_service(hass, lock.DOMAIN, lock.SERVICE_UNLOCK)
|
||||
await trt.execute(trait.COMMAND_LOCKUNLOCK, BASIC_DATA, {'lock': False})
|
||||
|
||||
# No challenge data
|
||||
with pytest.raises(error.ChallengeNeeded) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': False}, {})
|
||||
assert len(calls) == 0
|
||||
assert err.code == const.ERR_CHALLENGE_NEEDED
|
||||
assert err.challenge_type == const.CHALLENGE_PIN_NEEDED
|
||||
|
||||
# invalid pin
|
||||
with pytest.raises(error.ChallengeNeeded) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': False},
|
||||
{'pin': 9999})
|
||||
assert len(calls) == 0
|
||||
assert err.code == const.ERR_CHALLENGE_NEEDED
|
||||
assert err.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
||||
|
||||
await trt.execute(
|
||||
trait.COMMAND_LOCKUNLOCK, PIN_DATA, {'lock': False}, {'pin': '1234'})
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
@ -1000,7 +1035,7 @@ async def test_fan_speed(hass):
|
||||
|
||||
calls = async_mock_service(hass, fan.DOMAIN, fan.SERVICE_SET_SPEED)
|
||||
await trt.execute(
|
||||
trait.COMMAND_FANSPEED, BASIC_DATA, {'fanSpeed': 'medium'})
|
||||
trait.COMMAND_FANSPEED, BASIC_DATA, {'fanSpeed': 'medium'}, {})
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
@ -1089,7 +1124,7 @@ async def test_modes(hass):
|
||||
trait.COMMAND_MODES, BASIC_DATA, {
|
||||
'updateModeSettings': {
|
||||
trt.HA_TO_GOOGLE.get(media_player.ATTR_INPUT_SOURCE): 'media'
|
||||
}})
|
||||
}}, {})
|
||||
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
@ -1145,7 +1180,58 @@ async def test_openclose_cover(hass):
|
||||
hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
|
||||
await trt.execute(
|
||||
trait.COMMAND_OPENCLOSE, BASIC_DATA,
|
||||
{'openPercent': 50})
|
||||
{'openPercent': 50}, {})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'cover.bla',
|
||||
cover.ATTR_POSITION: 50
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('device_class', (
|
||||
cover.DEVICE_CLASS_DOOR,
|
||||
cover.DEVICE_CLASS_GARAGE,
|
||||
))
|
||||
async def test_openclose_cover_secure(hass, device_class):
|
||||
"""Test OpenClose trait support for cover domain."""
|
||||
assert helpers.get_google_type(cover.DOMAIN, device_class) is not None
|
||||
assert trait.OpenCloseTrait.supported(
|
||||
cover.DOMAIN, cover.SUPPORT_SET_POSITION, device_class)
|
||||
|
||||
trt = trait.OpenCloseTrait(hass, State('cover.bla', cover.STATE_OPEN, {
|
||||
ATTR_DEVICE_CLASS: device_class,
|
||||
cover.ATTR_CURRENT_POSITION: 75
|
||||
}), PIN_CONFIG)
|
||||
|
||||
assert trt.sync_attributes() == {}
|
||||
assert trt.query_attributes() == {
|
||||
'openPercent': 75
|
||||
}
|
||||
|
||||
calls = async_mock_service(
|
||||
hass, cover.DOMAIN, cover.SERVICE_SET_COVER_POSITION)
|
||||
|
||||
# No challenge data
|
||||
with pytest.raises(error.ChallengeNeeded) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_OPENCLOSE, PIN_DATA,
|
||||
{'openPercent': 50}, {})
|
||||
assert len(calls) == 0
|
||||
assert err.code == const.ERR_CHALLENGE_NEEDED
|
||||
assert err.challenge_type == const.CHALLENGE_PIN_NEEDED
|
||||
|
||||
# invalid pin
|
||||
with pytest.raises(error.ChallengeNeeded) as err:
|
||||
await trt.execute(
|
||||
trait.COMMAND_OPENCLOSE, PIN_DATA,
|
||||
{'openPercent': 50}, {'pin': '9999'})
|
||||
assert len(calls) == 0
|
||||
assert err.code == const.ERR_CHALLENGE_NEEDED
|
||||
assert err.challenge_type == const.CHALLENGE_FAILED_PIN_NEEDED
|
||||
|
||||
await trt.execute(
|
||||
trait.COMMAND_OPENCLOSE, PIN_DATA,
|
||||
{'openPercent': 50}, {'pin': '1234'})
|
||||
assert len(calls) == 1
|
||||
assert calls[0].data == {
|
||||
ATTR_ENTITY_ID: 'cover.bla',
|
||||
|
Loading…
x
Reference in New Issue
Block a user