Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ask users for a pin when interacting with locks/garage doors #23223

Merged
merged 2 commits into from Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/cloud/client.py
Expand Up @@ -102,7 +102,7 @@ def should_expose(entity):

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),
)

Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/cloud/const.py
Expand Up @@ -5,7 +5,7 @@
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'

Expand Down
21 changes: 8 additions & 13 deletions homeassistant/components/cloud/http_api.py
Expand Up @@ -19,7 +19,7 @@

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__)

Expand All @@ -30,15 +30,6 @@
})


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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
12 changes: 6 additions & 6 deletions homeassistant/components/cloud/prefs.py
Expand Up @@ -3,7 +3,7 @@

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
Expand All @@ -29,7 +29,7 @@ async def async_initialize(self):
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,
}
Expand All @@ -38,14 +38,14 @@ async def async_initialize(self):

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),
):
Expand Down Expand Up @@ -85,9 +85,9 @@ def google_enabled(self):
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):
Expand Down
27 changes: 15 additions & 12 deletions homeassistant/components/google_assistant/__init__.py
Expand Up @@ -20,7 +20,7 @@
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
Expand All @@ -35,17 +35,20 @@
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
Expand Down
15 changes: 12 additions & 3 deletions homeassistant/components/google_assistant/const.py
Expand Up @@ -28,13 +28,13 @@
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'
Expand All @@ -55,7 +55,7 @@
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"
Expand All @@ -64,6 +64,12 @@
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'
Expand Down Expand Up @@ -95,5 +101,8 @@
(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'
29 changes: 29 additions & 0 deletions homeassistant/components/google_assistant/error.py
@@ -1,4 +1,5 @@
"""Errors for Google Assistant."""
from .const import ERR_CHALLENGE_NEEDED


class SmartHomeError(Exception):
Expand All @@ -11,3 +12,31 @@ def __init__(self, code, msg):
"""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
}
}
13 changes: 8 additions & 5 deletions homeassistant/components/google_assistant/helpers.py
Expand Up @@ -19,12 +19,12 @@
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:
Expand Down Expand Up @@ -168,15 +168,18 @@ def query_serialize(self):

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

Expand Down
21 changes: 12 additions & 9 deletions homeassistant/components/google_assistant/http.py
Expand Up @@ -10,12 +10,12 @@

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

Expand All @@ -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."""
Expand All @@ -53,8 +53,13 @@ def is_exposed(entity) -> bool:

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):
Expand All @@ -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."""
Expand Down
6 changes: 2 additions & 4 deletions homeassistant/components/google_assistant/smart_home.py
Expand Up @@ -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())
Expand Down