Add Google Report State (#27112)
* Add Google Report State * UPDATE codeowners" * Add config option for dev mode * update library * lint * Bug fixes
This commit is contained in:
parent
3e99743244
commit
f184bf4d85
26 changed files with 510 additions and 56 deletions
|
@ -16,6 +16,7 @@ homeassistant/scripts/check_config.py @kellerza
|
|||
homeassistant/components/adguard/* @frenck
|
||||
homeassistant/components/airvisual/* @bachya
|
||||
homeassistant/components/alarm_control_panel/* @colinodell
|
||||
homeassistant/components/alexa/* @home-assistant/cloud
|
||||
homeassistant/components/alpha_vantage/* @fabaff
|
||||
homeassistant/components/amazon_polly/* @robbiet480
|
||||
homeassistant/components/ambiclimate/* @danielhiversen
|
||||
|
@ -106,6 +107,7 @@ homeassistant/components/geonetnz_quakes/* @exxamalte
|
|||
homeassistant/components/gitter/* @fabaff
|
||||
homeassistant/components/glances/* @fabaff
|
||||
homeassistant/components/gntp/* @robbiet480
|
||||
homeassistant/components/google_assistant/* @home-assistant/cloud
|
||||
homeassistant/components/google_cloud/* @lufton
|
||||
homeassistant/components/google_translate/* @awarecan
|
||||
homeassistant/components/google_travel_time/* @robbiet480
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
"name": "Alexa",
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
"http"
|
||||
],
|
||||
"codeowners": []
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/cloud"]
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ from .const import (
|
|||
CONF_REMOTE_API_URL,
|
||||
CONF_SUBSCRIPTION_INFO_URL,
|
||||
CONF_USER_POOL_ID,
|
||||
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL,
|
||||
DOMAIN,
|
||||
MODE_DEV,
|
||||
MODE_PROD,
|
||||
|
@ -96,7 +97,8 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(),
|
||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_SCHEMA,
|
||||
vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): str,
|
||||
vol.Optional(CONF_ALEXA_ACCESS_TOKEN_URL): vol.Url(),
|
||||
vol.Optional(CONF_GOOGLE_ACTIONS_REPORT_STATE_URL): vol.Url(),
|
||||
}
|
||||
)
|
||||
},
|
||||
|
|
|
@ -98,7 +98,7 @@ class CloudClient(Interface):
|
|||
if not self._google_config:
|
||||
assert self.cloud is not None
|
||||
self._google_config = google_config.CloudGoogleConfig(
|
||||
self.google_user_config, self._prefs, self.cloud
|
||||
self._hass, self.google_user_config, self._prefs, self.cloud
|
||||
)
|
||||
|
||||
return self._google_config
|
||||
|
@ -107,13 +107,17 @@ class CloudClient(Interface):
|
|||
"""Initialize the client."""
|
||||
self.cloud = cloud
|
||||
|
||||
if not self.alexa_config.should_report_state or not self.cloud.is_logged_in:
|
||||
if not self.cloud.is_logged_in:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.alexa_config.async_enable_proactive_mode()
|
||||
except alexa_errors.NoTokenAvailable:
|
||||
pass
|
||||
if self.alexa_config.should_report_state:
|
||||
try:
|
||||
await self.alexa_config.async_enable_proactive_mode()
|
||||
except alexa_errors.NoTokenAvailable:
|
||||
pass
|
||||
|
||||
if self.google_config.should_report_state:
|
||||
self.google_config.async_enable_report_state()
|
||||
|
||||
async def cleanups(self) -> None:
|
||||
"""Cleanup some stuff after logout."""
|
||||
|
|
|
@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = "google_secure_devices_pin"
|
|||
PREF_CLOUDHOOKS = "cloudhooks"
|
||||
PREF_CLOUD_USER = "cloud_user"
|
||||
PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs"
|
||||
PREF_GOOGLE_REPORT_STATE = "google_report_state"
|
||||
PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs"
|
||||
PREF_ALEXA_REPORT_STATE = "alexa_report_state"
|
||||
PREF_OVERRIDE_NAME = "override_name"
|
||||
|
@ -18,6 +19,7 @@ PREF_SHOULD_EXPOSE = "should_expose"
|
|||
DEFAULT_SHOULD_EXPOSE = True
|
||||
DEFAULT_DISABLE_2FA = False
|
||||
DEFAULT_ALEXA_REPORT_STATE = False
|
||||
DEFAULT_GOOGLE_REPORT_STATE = False
|
||||
|
||||
CONF_ALEXA = "alexa"
|
||||
CONF_ALIASES = "aliases"
|
||||
|
@ -33,6 +35,7 @@ CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url"
|
|||
CONF_REMOTE_API_URL = "remote_api_url"
|
||||
CONF_ACME_DIRECTORY_SERVER = "acme_directory_server"
|
||||
CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url"
|
||||
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url"
|
||||
|
||||
MODE_DEV = "development"
|
||||
MODE_PROD = "production"
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
"""Google config for Cloud."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import async_timeout
|
||||
from hass_nabucasa.google_report_state import ErrorResponse
|
||||
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.components.google_assistant.helpers import AbstractConfig
|
||||
from homeassistant.helpers import entity_registry
|
||||
|
||||
from .const import (
|
||||
PREF_SHOULD_EXPOSE,
|
||||
|
@ -10,15 +17,31 @@ from .const import (
|
|||
DEFAULT_DISABLE_2FA,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CloudGoogleConfig(AbstractConfig):
|
||||
"""HA Cloud Configuration for Google Assistant."""
|
||||
|
||||
def __init__(self, config, prefs, cloud):
|
||||
"""Initialize the Alexa config."""
|
||||
def __init__(self, hass, config, prefs, cloud):
|
||||
"""Initialize the Google config."""
|
||||
super().__init__(hass)
|
||||
self._config = config
|
||||
self._prefs = prefs
|
||||
self._cloud = cloud
|
||||
self._cur_entity_prefs = self._prefs.google_entity_configs
|
||||
self._sync_entities_lock = asyncio.Lock()
|
||||
|
||||
prefs.async_listen_updates(self._async_prefs_updated)
|
||||
hass.bus.async_listen(
|
||||
entity_registry.EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
self._handle_entity_registry_updated,
|
||||
)
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Return if Google is enabled."""
|
||||
return self._prefs.google_enabled
|
||||
|
||||
@property
|
||||
def agent_user_id(self):
|
||||
|
@ -35,16 +58,25 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
"""Return entity config."""
|
||||
return self._prefs.google_secure_devices_pin
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
"""Return if states should be proactively reported."""
|
||||
return self._prefs.google_report_state
|
||||
|
||||
def should_expose(self, state):
|
||||
"""If an entity should be exposed."""
|
||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
"""If a state object should be exposed."""
|
||||
return self._should_expose_entity_id(state.entity_id)
|
||||
|
||||
def _should_expose_entity_id(self, entity_id):
|
||||
"""If an entity ID should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
if not self._config["filter"].empty_filter:
|
||||
return self._config["filter"](state.entity_id)
|
||||
return self._config["filter"](entity_id)
|
||||
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(state.entity_id, {})
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
|
||||
|
||||
def should_2fa(self, state):
|
||||
|
@ -52,3 +84,72 @@ class CloudGoogleConfig(AbstractConfig):
|
|||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(state.entity_id, {})
|
||||
return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
||||
|
||||
async def async_report_state(self, message):
|
||||
"""Send a state report to Google."""
|
||||
try:
|
||||
await self._cloud.google_report_state.async_send_message(message)
|
||||
except ErrorResponse as err:
|
||||
_LOGGER.warning("Error reporting state - %s: %s", err.code, err.message)
|
||||
|
||||
async def _async_request_sync_devices(self):
|
||||
"""Trigger a sync with Google."""
|
||||
if self._sync_entities_lock.locked():
|
||||
return 200
|
||||
|
||||
websession = self.hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
async with self._sync_entities_lock:
|
||||
with async_timeout.timeout(10):
|
||||
await self._cloud.auth.async_check_token()
|
||||
|
||||
_LOGGER.debug("Requesting sync")
|
||||
|
||||
with async_timeout.timeout(30):
|
||||
req = await websession.post(
|
||||
self._cloud.google_actions_sync_url,
|
||||
headers={"authorization": self._cloud.id_token},
|
||||
)
|
||||
_LOGGER.debug("Finished requesting syncing: %s", req.status)
|
||||
return req.status
|
||||
|
||||
async def async_deactivate_report_state(self):
|
||||
"""Turn off report state and disable further state reporting.
|
||||
|
||||
Called when the user disconnects their account from Google.
|
||||
"""
|
||||
await self._prefs.async_update(google_report_state=False)
|
||||
|
||||
async def _async_prefs_updated(self, prefs):
|
||||
"""Handle updated preferences."""
|
||||
if self.should_report_state != self.is_reporting_state:
|
||||
if self.should_report_state:
|
||||
self.async_enable_report_state()
|
||||
else:
|
||||
self.async_disable_report_state()
|
||||
|
||||
# State reporting is reported as a property on entities.
|
||||
# So when we change it, we need to sync all entities.
|
||||
await self.async_sync_entities()
|
||||
return
|
||||
|
||||
# If entity prefs are the same or we have filter in config.yaml,
|
||||
# don't sync.
|
||||
if (
|
||||
self._cur_entity_prefs is prefs.google_entity_configs
|
||||
or not self._config["filter"].empty_filter
|
||||
):
|
||||
return
|
||||
|
||||
self.async_schedule_google_sync()
|
||||
|
||||
async def _handle_entity_registry_updated(self, event):
|
||||
"""Handle when entity registry updated."""
|
||||
if not self.enabled or not self._cloud.is_logged_in:
|
||||
return
|
||||
|
||||
entity_id = event.data["entity_id"]
|
||||
|
||||
# Schedule a sync if a change was made to an entity that Google knows about
|
||||
if self._should_expose_entity_id(entity_id):
|
||||
await self.async_sync_entities()
|
||||
|
|
|
@ -7,6 +7,7 @@ import attr
|
|||
import aiohttp
|
||||
import async_timeout
|
||||
import voluptuous as vol
|
||||
from hass_nabucasa import Cloud
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
|
@ -28,6 +29,7 @@ from .const import (
|
|||
InvalidTrustedNetworks,
|
||||
InvalidTrustedProxies,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
PREF_GOOGLE_REPORT_STATE,
|
||||
RequireRelink,
|
||||
)
|
||||
|
||||
|
@ -171,18 +173,9 @@ class GoogleActionsSyncView(HomeAssistantView):
|
|||
async def post(self, request):
|
||||
"""Trigger a Google Actions sync."""
|
||||
hass = request.app["hass"]
|
||||
cloud = hass.data[DOMAIN]
|
||||
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
await hass.async_add_job(cloud.auth.check_token)
|
||||
|
||||
with async_timeout.timeout(REQUEST_TIMEOUT):
|
||||
req = await websession.post(
|
||||
cloud.google_actions_sync_url, headers={"authorization": cloud.id_token}
|
||||
)
|
||||
|
||||
return self.json({}, status_code=req.status)
|
||||
cloud: Cloud = hass.data[DOMAIN]
|
||||
status = await cloud.client.google_config.async_sync_entities()
|
||||
return self.json({}, status_code=status)
|
||||
|
||||
|
||||
class CloudLoginView(HomeAssistantView):
|
||||
|
@ -366,6 +359,7 @@ async def websocket_subscription(hass, connection, msg):
|
|||
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
||||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||
vol.Optional(PREF_ALEXA_REPORT_STATE): bool,
|
||||
vol.Optional(PREF_GOOGLE_REPORT_STATE): bool,
|
||||
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "cloud",
|
||||
"name": "Cloud",
|
||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||
"requirements": ["hass-nabucasa==0.17"],
|
||||
"requirements": ["hass-nabucasa==0.22"],
|
||||
"dependencies": ["http", "webhook"],
|
||||
"codeowners": ["@home-assistant/cloud"]
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ from .const import (
|
|||
PREF_ALEXA_ENTITY_CONFIGS,
|
||||
PREF_ALEXA_REPORT_STATE,
|
||||
DEFAULT_ALEXA_REPORT_STATE,
|
||||
PREF_GOOGLE_REPORT_STATE,
|
||||
DEFAULT_GOOGLE_REPORT_STATE,
|
||||
InvalidTrustedNetworks,
|
||||
InvalidTrustedProxies,
|
||||
)
|
||||
|
@ -74,6 +76,7 @@ class CloudPreferences:
|
|||
google_entity_configs=_UNDEF,
|
||||
alexa_entity_configs=_UNDEF,
|
||||
alexa_report_state=_UNDEF,
|
||||
google_report_state=_UNDEF,
|
||||
):
|
||||
"""Update user preferences."""
|
||||
for key, value in (
|
||||
|
@ -86,6 +89,7 @@ class CloudPreferences:
|
|||
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
||||
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
|
||||
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
|
||||
(PREF_GOOGLE_REPORT_STATE, google_report_state),
|
||||
):
|
||||
if value is not _UNDEF:
|
||||
self._prefs[key] = value
|
||||
|
@ -164,6 +168,7 @@ class CloudPreferences:
|
|||
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
||||
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
|
||||
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
|
||||
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
|
||||
PREF_CLOUDHOOKS: self.cloudhooks,
|
||||
PREF_CLOUD_USER: self.cloud_user,
|
||||
}
|
||||
|
@ -196,6 +201,11 @@ class CloudPreferences:
|
|||
"""Return if Google is enabled."""
|
||||
return self._prefs[PREF_ENABLE_GOOGLE]
|
||||
|
||||
@property
|
||||
def google_report_state(self):
|
||||
"""Return if Google report state is enabled."""
|
||||
return self._prefs.get(PREF_GOOGLE_REPORT_STATE, DEFAULT_GOOGLE_REPORT_STATE)
|
||||
|
||||
@property
|
||||
def google_secure_devices_pin(self):
|
||||
"""Return if Google is allowed to unlock locks."""
|
||||
|
|
|
@ -83,6 +83,13 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
|||
try:
|
||||
with async_timeout.timeout(15):
|
||||
agent_user_id = call.data.get("agent_user_id") or call.context.user_id
|
||||
|
||||
if agent_user_id is None:
|
||||
_LOGGER.warning(
|
||||
"No agent_user_id supplied for request_sync. Call as a user or pass in user id as agent_user_id."
|
||||
)
|
||||
return
|
||||
|
||||
res = await websession.post(
|
||||
REQUEST_SYNC_BASE_URL,
|
||||
params={"key": api_key},
|
||||
|
|
|
@ -3,7 +3,8 @@ from asyncio import gather
|
|||
from collections.abc import Mapping
|
||||
from typing import List
|
||||
|
||||
from homeassistant.core import Context, callback
|
||||
from homeassistant.core import Context, callback, HomeAssistant, State
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
STATE_UNAVAILABLE,
|
||||
|
@ -22,10 +23,24 @@ from .const import (
|
|||
)
|
||||
from .error import SmartHomeError
|
||||
|
||||
SYNC_DELAY = 15
|
||||
|
||||
|
||||
class AbstractConfig:
|
||||
"""Hold the configuration for Google Assistant."""
|
||||
|
||||
_unsub_report_state = None
|
||||
|
||||
def __init__(self, hass):
|
||||
"""Initialize abstract config."""
|
||||
self.hass = hass
|
||||
self._google_sync_unsub = None
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Return if Google is enabled."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def agent_user_id(self):
|
||||
"""Return Agent User Id to use for query responses."""
|
||||
|
@ -41,6 +56,17 @@ class AbstractConfig:
|
|||
"""Return entity config."""
|
||||
return None
|
||||
|
||||
@property
|
||||
def is_reporting_state(self):
|
||||
"""Return if we're actively reporting states."""
|
||||
return self._unsub_report_state is not None
|
||||
|
||||
@property
|
||||
def should_report_state(self):
|
||||
"""Return if states should be proactively reported."""
|
||||
# pylint: disable=no-self-use
|
||||
return False
|
||||
|
||||
def should_expose(self, state) -> bool:
|
||||
"""Return if entity should be exposed."""
|
||||
raise NotImplementedError
|
||||
|
@ -50,11 +76,66 @@ class AbstractConfig:
|
|||
# pylint: disable=no-self-use
|
||||
return True
|
||||
|
||||
async def async_report_state(self, message):
|
||||
"""Send a state report to Google."""
|
||||
raise NotImplementedError
|
||||
|
||||
def async_enable_report_state(self):
|
||||
"""Enable proactive mode."""
|
||||
# Circular dep
|
||||
from .report_state import async_enable_report_state
|
||||
|
||||
if self._unsub_report_state is None:
|
||||
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
||||
|
||||
def async_disable_report_state(self):
|
||||
"""Disable report state."""
|
||||
if self._unsub_report_state is not None:
|
||||
self._unsub_report_state()
|
||||
self._unsub_report_state = None
|
||||
|
||||
async def async_sync_entities(self):
|
||||
"""Sync all entities to Google."""
|
||||
# Remove any pending sync
|
||||
if self._google_sync_unsub:
|
||||
self._google_sync_unsub()
|
||||
self._google_sync_unsub = None
|
||||
|
||||
return await self._async_request_sync_devices()
|
||||
|
||||
async def _schedule_callback(self, _now):
|
||||
"""Handle a scheduled sync callback."""
|
||||
self._google_sync_unsub = None
|
||||
await self.async_sync_entities()
|
||||
|
||||
@callback
|
||||
def async_schedule_google_sync(self):
|
||||
"""Schedule a sync."""
|
||||
if self._google_sync_unsub:
|
||||
self._google_sync_unsub()
|
||||
|
||||
self._google_sync_unsub = async_call_later(
|
||||
self.hass, SYNC_DELAY, self._schedule_callback
|
||||
)
|
||||
|
||||
async def _async_request_sync_devices(self) -> int:
|
||||
"""Trigger a sync with Google.
|
||||
|
||||
Return value is the HTTP status code of the sync request.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_deactivate_report_state(self):
|
||||
"""Turn off report state and disable further state reporting.
|
||||
|
||||
Called when the user disconnects their account from Google.
|
||||
"""
|
||||
|
||||
|
||||
class RequestData:
|
||||
"""Hold data associated with a particular request."""
|
||||
|
||||
def __init__(self, config, user_id, request_id):
|
||||
def __init__(self, config: AbstractConfig, user_id: str, request_id: str):
|
||||
"""Initialize the request data."""
|
||||
self.config = config
|
||||
self.request_id = request_id
|
||||
|
@ -71,7 +152,7 @@ def get_google_type(domain, device_class):
|
|||
class GoogleEntity:
|
||||
"""Adaptation of Entity expressed in Google's terms."""
|
||||
|
||||
def __init__(self, hass, config, state):
|
||||
def __init__(self, hass: HomeAssistant, config: AbstractConfig, state: State):
|
||||
"""Initialize a Google entity."""
|
||||
self.hass = hass
|
||||
self.config = config
|
||||
|
@ -139,7 +220,7 @@ class GoogleEntity:
|
|||
"name": {"name": name},
|
||||
"attributes": {},
|
||||
"traits": [trait.name for trait in traits],
|
||||
"willReportState": False,
|
||||
"willReportState": self.config.should_report_state,
|
||||
"type": device_type,
|
||||
}
|
||||
|
||||
|
|
|
@ -25,10 +25,16 @@ _LOGGER = logging.getLogger(__name__)
|
|||
class GoogleConfig(AbstractConfig):
|
||||
"""Config for manual setup of Google."""
|
||||
|
||||
def __init__(self, config):
|
||||
def __init__(self, hass, config):
|
||||
"""Initialize the config."""
|
||||
super().__init__(hass)
|
||||
self._config = config
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
"""Return if Google is enabled."""
|
||||
return True
|
||||
|
||||
@property
|
||||
def agent_user_id(self):
|
||||
"""Return Agent User Id to use for query responses."""
|
||||
|
@ -77,7 +83,7 @@ class GoogleConfig(AbstractConfig):
|
|||
@callback
|
||||
def async_register_http(hass, cfg):
|
||||
"""Register HTTP views for Google Assistant."""
|
||||
hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg)))
|
||||
hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg)))
|
||||
|
||||
|
||||
class GoogleAssistantView(HomeAssistantView):
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
"name": "Google assistant",
|
||||
"documentation": "https://www.home-assistant.io/integrations/google_assistant",
|
||||
"requirements": [],
|
||||
"dependencies": [
|
||||
"http"
|
||||
],
|
||||
"codeowners": []
|
||||
"dependencies": ["http"],
|
||||
"codeowners": ["@home-assistant/cloud"]
|
||||
}
|
||||
|
|
39
homeassistant/components/google_assistant/report_state.py
Normal file
39
homeassistant/components/google_assistant/report_state.py
Normal file
|
@ -0,0 +1,39 @@
|
|||
"""Google Report State implementation."""
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.const import MATCH_ALL
|
||||
|
||||
from .helpers import AbstractConfig, GoogleEntity
|
||||
|
||||
|
||||
@callback
|
||||
def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig):
|
||||
"""Enable state reporting."""
|
||||
|
||||
async def async_entity_state_listener(changed_entity, old_state, new_state):
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
if not google_config.should_expose(new_state):
|
||||
return
|
||||
|
||||
entity = GoogleEntity(hass, google_config, new_state)
|
||||
|
||||
if not entity.is_supported():
|
||||
return
|
||||
|
||||
entity_data = entity.query_serialize()
|
||||
|
||||
if old_state:
|
||||
old_entity = GoogleEntity(hass, google_config, old_state)
|
||||
|
||||
# Only report to Google if data that Google cares about has changed
|
||||
if entity_data == old_entity.query_serialize():
|
||||
return
|
||||
|
||||
await google_config.async_report_state(
|
||||
{"devices": {"states": {changed_entity: entity_data}}}
|
||||
)
|
||||
|
||||
return hass.helpers.event.async_track_state_change(
|
||||
MATCH_ALL, async_entity_state_listener
|
||||
)
|
|
@ -193,11 +193,12 @@ async def handle_devices_execute(hass, data, payload):
|
|||
|
||||
|
||||
@HANDLERS.register("action.devices.DISCONNECT")
|
||||
async def async_devices_disconnect(hass, data, payload):
|
||||
async def async_devices_disconnect(hass, data: RequestData, payload):
|
||||
"""Handle action.devices.DISCONNECT request.
|
||||
|
||||
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
|
||||
"""
|
||||
await data.config.async_deactivate_report_state()
|
||||
return None
|
||||
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ certifi>=2019.6.16
|
|||
contextvars==2.4;python_version<"3.7"
|
||||
cryptography==2.7
|
||||
distro==1.4.0
|
||||
hass-nabucasa==0.17
|
||||
hass-nabucasa==0.22
|
||||
home-assistant-frontend==20191002.0
|
||||
importlib-metadata==0.23
|
||||
jinja2>=2.10.1
|
||||
|
|
|
@ -607,7 +607,7 @@ habitipy==0.2.0
|
|||
hangups==0.4.9
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.17
|
||||
hass-nabucasa==0.22
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
hbmqtt==0.9.5
|
||||
|
|
|
@ -164,7 +164,7 @@ ha-ffmpeg==2.0
|
|||
hangups==0.4.9
|
||||
|
||||
# homeassistant.components.cloud
|
||||
hass-nabucasa==0.17
|
||||
hass-nabucasa==0.22
|
||||
|
||||
# homeassistant.components.mqtt
|
||||
hbmqtt==0.9.5
|
||||
|
|
|
@ -59,7 +59,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock):
|
|||
cloud_prefs,
|
||||
Mock(
|
||||
alexa_access_token_url="http://example/alexa_token",
|
||||
run_executor=Mock(side_effect=mock_coro),
|
||||
auth=Mock(async_check_token=Mock(side_effect=mock_coro)),
|
||||
websession=hass.helpers.aiohttp_client.async_get_clientsession(),
|
||||
),
|
||||
)
|
||||
|
@ -160,7 +160,11 @@ async def test_alexa_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
|
|||
with patch_sync_helper() as (to_update, to_remove):
|
||||
hass.bus.async_fire(
|
||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "update", "entity_id": "light.kitchen"},
|
||||
{
|
||||
"action": "update",
|
||||
"entity_id": "light.kitchen",
|
||||
"changes": ["entity_id"],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
|
121
tests/components/cloud/test_google_config.py
Normal file
121
tests/components/cloud/test_google_config.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
"""Test the Cloud Google Config."""
|
||||
from unittest.mock import patch, Mock
|
||||
|
||||
from homeassistant.components.google_assistant import helpers as ga_helpers
|
||||
from homeassistant.components.cloud import GACTIONS_SCHEMA
|
||||
from homeassistant.components.cloud.google_config import CloudGoogleConfig
|
||||
from homeassistant.util.dt import utcnow
|
||||
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
|
||||
|
||||
from tests.common import mock_coro, async_fire_time_changed
|
||||
|
||||
|
||||
async def test_google_update_report_state(hass, cloud_prefs):
|
||||
"""Test Google config responds to updating preference."""
|
||||
config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None)
|
||||
|
||||
with patch.object(
|
||||
config, "async_sync_entities", side_effect=mock_coro
|
||||
) as mock_sync, patch(
|
||||
"homeassistant.components.google_assistant.report_state.async_enable_report_state"
|
||||
) as mock_report_state:
|
||||
await cloud_prefs.async_update(google_report_state=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_sync.mock_calls) == 1
|
||||
assert len(mock_report_state.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_sync_entities(aioclient_mock, hass, cloud_prefs):
|
||||
"""Test sync devices."""
|
||||
aioclient_mock.post("http://example.com", status=404)
|
||||
config = CloudGoogleConfig(
|
||||
hass,
|
||||
GACTIONS_SCHEMA({}),
|
||||
cloud_prefs,
|
||||
Mock(
|
||||
google_actions_sync_url="http://example.com",
|
||||
auth=Mock(async_check_token=Mock(side_effect=mock_coro)),
|
||||
),
|
||||
)
|
||||
|
||||
assert await config.async_sync_entities() == 404
|
||||
|
||||
|
||||
async def test_google_update_expose_trigger_sync(hass, cloud_prefs):
|
||||
"""Test Google config responds to updating exposed entities."""
|
||||
config = CloudGoogleConfig(hass, GACTIONS_SCHEMA({}), cloud_prefs, None)
|
||||
|
||||
with patch.object(
|
||||
config, "async_sync_entities", side_effect=mock_coro
|
||||
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="light.kitchen", should_expose=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_sync.mock_calls) == 1
|
||||
|
||||
with patch.object(
|
||||
config, "async_sync_entities", side_effect=mock_coro
|
||||
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="light.kitchen", should_expose=False
|
||||
)
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="binary_sensor.door", should_expose=True
|
||||
)
|
||||
await cloud_prefs.async_update_google_entity_config(
|
||||
entity_id="sensor.temp", should_expose=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
async_fire_time_changed(hass, utcnow())
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_sync.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_google_entity_registry_sync(hass, mock_cloud_login, cloud_prefs):
|
||||
"""Test Google config responds to entity registry."""
|
||||
config = CloudGoogleConfig(
|
||||
hass, GACTIONS_SCHEMA({}), cloud_prefs, hass.data["cloud"]
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
config, "async_sync_entities", side_effect=mock_coro
|
||||
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
|
||||
hass.bus.async_fire(
|
||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "create", "entity_id": "light.kitchen"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_sync.mock_calls) == 1
|
||||
|
||||
with patch.object(
|
||||
config, "async_sync_entities", side_effect=mock_coro
|
||||
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
|
||||
hass.bus.async_fire(
|
||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{"action": "remove", "entity_id": "light.kitchen"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_sync.mock_calls) == 1
|
||||
|
||||
with patch.object(
|
||||
config, "async_sync_entities", side_effect=mock_coro
|
||||
) as mock_sync, patch.object(ga_helpers, "SYNC_DELAY", 0):
|
||||
hass.bus.async_fire(
|
||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
||||
{
|
||||
"action": "update",
|
||||
"entity_id": "light.kitchen",
|
||||
"changes": ["entity_id"],
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_sync.mock_calls) == 1
|
|
@ -33,7 +33,9 @@ SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info"
|
|||
@pytest.fixture()
|
||||
def mock_auth():
|
||||
"""Mock check token."""
|
||||
with patch("hass_nabucasa.auth.CognitoAuth.check_token"):
|
||||
with patch(
|
||||
"hass_nabucasa.auth.CognitoAuth.async_check_token", side_effect=mock_coro
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
|
@ -357,6 +359,7 @@ async def test_websocket_status(
|
|||
"google_secure_devices_pin": None,
|
||||
"alexa_entity_configs": {},
|
||||
"alexa_report_state": False,
|
||||
"google_report_state": False,
|
||||
"remote_enabled": False,
|
||||
},
|
||||
"alexa_entities": {
|
||||
|
|
|
@ -28,6 +28,13 @@ async def test_constructor_loads_info_from_config(hass):
|
|||
"user_pool_id": "test-user_pool_id",
|
||||
"region": "test-region",
|
||||
"relayer": "test-relayer",
|
||||
"google_actions_sync_url": "http://test-google_actions_sync_url",
|
||||
"subscription_info_url": "http://test-subscription-info-url",
|
||||
"cloudhook_create_url": "http://test-cloudhook_create_url",
|
||||
"remote_api_url": "http://test-remote_api_url",
|
||||
"alexa_access_token_url": "http://test-alexa-token-url",
|
||||
"acme_directory_server": "http://test-acme-directory-server",
|
||||
"google_actions_report_state_url": "http://test-google-actions-report-state-url",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
@ -39,6 +46,16 @@ async def test_constructor_loads_info_from_config(hass):
|
|||
assert cl.user_pool_id == "test-user_pool_id"
|
||||
assert cl.region == "test-region"
|
||||
assert cl.relayer == "test-relayer"
|
||||
assert cl.google_actions_sync_url == "http://test-google_actions_sync_url"
|
||||
assert cl.subscription_info_url == "http://test-subscription-info-url"
|
||||
assert cl.cloudhook_create_url == "http://test-cloudhook_create_url"
|
||||
assert cl.remote_api_url == "http://test-remote_api_url"
|
||||
assert cl.alexa_access_token_url == "http://test-alexa-token-url"
|
||||
assert cl.acme_directory_server == "http://test-acme-directory-server"
|
||||
assert (
|
||||
cl.google_actions_report_state_url
|
||||
== "http://test-google-actions-report-state-url"
|
||||
)
|
||||
|
||||
|
||||
async def test_remote_services(hass, mock_cloud_fixture, hass_read_only_user):
|
||||
|
|
|
@ -6,9 +6,15 @@ class MockConfig(helpers.AbstractConfig):
|
|||
"""Fake config that always exposes everything."""
|
||||
|
||||
def __init__(
|
||||
self, *, secure_devices_pin=None, should_expose=None, entity_config=None
|
||||
self,
|
||||
*,
|
||||
secure_devices_pin=None,
|
||||
should_expose=None,
|
||||
entity_config=None,
|
||||
hass=None,
|
||||
):
|
||||
"""Initialize config."""
|
||||
super().__init__(hass)
|
||||
self._should_expose = should_expose
|
||||
self._secure_devices_pin = secure_devices_pin
|
||||
self._entity_config = entity_config or {}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""The tests for google-assistant init."""
|
||||
import asyncio
|
||||
|
||||
from homeassistant.core import Context
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.components import google_assistant as ga
|
||||
|
||||
|
@ -20,7 +21,10 @@ def test_request_sync_service(aioclient_mock, hass):
|
|||
|
||||
assert aioclient_mock.call_count == 0
|
||||
yield from hass.services.async_call(
|
||||
ga.const.DOMAIN, ga.const.SERVICE_REQUEST_SYNC, blocking=True
|
||||
ga.const.DOMAIN,
|
||||
ga.const.SERVICE_REQUEST_SYNC,
|
||||
blocking=True,
|
||||
context=Context(user_id="123"),
|
||||
)
|
||||
|
||||
assert aioclient_mock.call_count == 1
|
||||
|
|
47
tests/components/google_assistant/test_report_state.py
Normal file
47
tests/components/google_assistant/test_report_state.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
"""Test Google report state."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.google_assistant.report_state import (
|
||||
async_enable_report_state,
|
||||
)
|
||||
from . import BASIC_CONFIG
|
||||
|
||||
from tests.common import mock_coro
|
||||
|
||||
|
||||
async def test_report_state(hass):
|
||||
"""Test report state works."""
|
||||
unsub = async_enable_report_state(hass, BASIC_CONFIG)
|
||||
|
||||
with patch.object(
|
||||
BASIC_CONFIG, "async_report_state", side_effect=mock_coro
|
||||
) as mock_report:
|
||||
hass.states.async_set("light.kitchen", "on")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_report.mock_calls) == 1
|
||||
assert mock_report.mock_calls[0][1][0] == {
|
||||
"devices": {"states": {"light.kitchen": {"on": True, "online": True}}}
|
||||
}
|
||||
|
||||
# Test that state changes that change something that Google doesn't care about
|
||||
# do not trigger a state report.
|
||||
with patch.object(
|
||||
BASIC_CONFIG, "async_report_state", side_effect=mock_coro
|
||||
) as mock_report:
|
||||
hass.states.async_set(
|
||||
"light.kitchen", "on", {"irrelevant": "should_be_ignored"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_report.mock_calls) == 0
|
||||
|
||||
unsub()
|
||||
|
||||
with patch.object(
|
||||
BASIC_CONFIG, "async_report_state", side_effect=mock_coro
|
||||
) as mock_report:
|
||||
hass.states.async_set("light.kitchen", "on")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_report.mock_calls) == 0
|
|
@ -657,14 +657,20 @@ async def test_device_media_player(hass, device_class, google_type):
|
|||
|
||||
async def test_query_disconnect(hass):
|
||||
"""Test a disconnect message."""
|
||||
result = await sh.async_handle_message(
|
||||
hass,
|
||||
BASIC_CONFIG,
|
||||
"test-agent",
|
||||
{"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID},
|
||||
)
|
||||
|
||||
config = MockConfig(hass=hass)
|
||||
config.async_enable_report_state()
|
||||
assert config._unsub_report_state is not None
|
||||
with patch.object(
|
||||
config, "async_deactivate_report_state", side_effect=mock_coro
|
||||
) as mock_deactivate:
|
||||
result = await sh.async_handle_message(
|
||||
hass,
|
||||
config,
|
||||
"test-agent",
|
||||
{"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID},
|
||||
)
|
||||
assert result is None
|
||||
assert len(mock_deactivate.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_trait_execute_adding_query_data(hass):
|
||||
|
|
Loading…
Add table
Reference in a new issue