Skip to content

Commit

Permalink
Ask users for a pin when interacting with locks/garage doors (#23223)
Browse files Browse the repository at this point in the history
* Ask users for a pin when interacting with locks/garage doors

* Deprecate allow_unlock option
  • Loading branch information
balloob committed Apr 19, 2019
1 parent 416af5c commit 0533f56
Show file tree
Hide file tree
Showing 15 changed files with 289 additions and 137 deletions.
2 changes: 1 addition & 1 deletion homeassistant/components/cloud/client.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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

0 comments on commit 0533f56

Please sign in to comment.