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/adguard/* @frenck
|
||||||
homeassistant/components/airvisual/* @bachya
|
homeassistant/components/airvisual/* @bachya
|
||||||
homeassistant/components/alarm_control_panel/* @colinodell
|
homeassistant/components/alarm_control_panel/* @colinodell
|
||||||
|
homeassistant/components/alexa/* @home-assistant/cloud
|
||||||
homeassistant/components/alpha_vantage/* @fabaff
|
homeassistant/components/alpha_vantage/* @fabaff
|
||||||
homeassistant/components/amazon_polly/* @robbiet480
|
homeassistant/components/amazon_polly/* @robbiet480
|
||||||
homeassistant/components/ambiclimate/* @danielhiversen
|
homeassistant/components/ambiclimate/* @danielhiversen
|
||||||
|
@ -106,6 +107,7 @@ homeassistant/components/geonetnz_quakes/* @exxamalte
|
||||||
homeassistant/components/gitter/* @fabaff
|
homeassistant/components/gitter/* @fabaff
|
||||||
homeassistant/components/glances/* @fabaff
|
homeassistant/components/glances/* @fabaff
|
||||||
homeassistant/components/gntp/* @robbiet480
|
homeassistant/components/gntp/* @robbiet480
|
||||||
|
homeassistant/components/google_assistant/* @home-assistant/cloud
|
||||||
homeassistant/components/google_cloud/* @lufton
|
homeassistant/components/google_cloud/* @lufton
|
||||||
homeassistant/components/google_translate/* @awarecan
|
homeassistant/components/google_translate/* @awarecan
|
||||||
homeassistant/components/google_travel_time/* @robbiet480
|
homeassistant/components/google_travel_time/* @robbiet480
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
"name": "Alexa",
|
"name": "Alexa",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
"documentation": "https://www.home-assistant.io/integrations/alexa",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"dependencies": [
|
"dependencies": ["http"],
|
||||||
"http"
|
"codeowners": ["@home-assistant/cloud"]
|
||||||
],
|
|
||||||
"codeowners": []
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ from .const import (
|
||||||
CONF_REMOTE_API_URL,
|
CONF_REMOTE_API_URL,
|
||||||
CONF_SUBSCRIPTION_INFO_URL,
|
CONF_SUBSCRIPTION_INFO_URL,
|
||||||
CONF_USER_POOL_ID,
|
CONF_USER_POOL_ID,
|
||||||
|
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
MODE_DEV,
|
MODE_DEV,
|
||||||
MODE_PROD,
|
MODE_PROD,
|
||||||
|
@ -96,7 +97,8 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(),
|
vol.Optional(CONF_ACME_DIRECTORY_SERVER): vol.Url(),
|
||||||
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
vol.Optional(CONF_ALEXA): ALEXA_SCHEMA,
|
||||||
vol.Optional(CONF_GOOGLE_ACTIONS): GACTIONS_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:
|
if not self._google_config:
|
||||||
assert self.cloud is not None
|
assert self.cloud is not None
|
||||||
self._google_config = google_config.CloudGoogleConfig(
|
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
|
return self._google_config
|
||||||
|
@ -107,13 +107,17 @@ class CloudClient(Interface):
|
||||||
"""Initialize the client."""
|
"""Initialize the client."""
|
||||||
self.cloud = cloud
|
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
|
return
|
||||||
|
|
||||||
try:
|
if self.alexa_config.should_report_state:
|
||||||
await self.alexa_config.async_enable_proactive_mode()
|
try:
|
||||||
except alexa_errors.NoTokenAvailable:
|
await self.alexa_config.async_enable_proactive_mode()
|
||||||
pass
|
except alexa_errors.NoTokenAvailable:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if self.google_config.should_report_state:
|
||||||
|
self.google_config.async_enable_report_state()
|
||||||
|
|
||||||
async def cleanups(self) -> None:
|
async def cleanups(self) -> None:
|
||||||
"""Cleanup some stuff after logout."""
|
"""Cleanup some stuff after logout."""
|
||||||
|
|
|
@ -9,6 +9,7 @@ PREF_GOOGLE_SECURE_DEVICES_PIN = "google_secure_devices_pin"
|
||||||
PREF_CLOUDHOOKS = "cloudhooks"
|
PREF_CLOUDHOOKS = "cloudhooks"
|
||||||
PREF_CLOUD_USER = "cloud_user"
|
PREF_CLOUD_USER = "cloud_user"
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs"
|
PREF_GOOGLE_ENTITY_CONFIGS = "google_entity_configs"
|
||||||
|
PREF_GOOGLE_REPORT_STATE = "google_report_state"
|
||||||
PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs"
|
PREF_ALEXA_ENTITY_CONFIGS = "alexa_entity_configs"
|
||||||
PREF_ALEXA_REPORT_STATE = "alexa_report_state"
|
PREF_ALEXA_REPORT_STATE = "alexa_report_state"
|
||||||
PREF_OVERRIDE_NAME = "override_name"
|
PREF_OVERRIDE_NAME = "override_name"
|
||||||
|
@ -18,6 +19,7 @@ PREF_SHOULD_EXPOSE = "should_expose"
|
||||||
DEFAULT_SHOULD_EXPOSE = True
|
DEFAULT_SHOULD_EXPOSE = True
|
||||||
DEFAULT_DISABLE_2FA = False
|
DEFAULT_DISABLE_2FA = False
|
||||||
DEFAULT_ALEXA_REPORT_STATE = False
|
DEFAULT_ALEXA_REPORT_STATE = False
|
||||||
|
DEFAULT_GOOGLE_REPORT_STATE = False
|
||||||
|
|
||||||
CONF_ALEXA = "alexa"
|
CONF_ALEXA = "alexa"
|
||||||
CONF_ALIASES = "aliases"
|
CONF_ALIASES = "aliases"
|
||||||
|
@ -33,6 +35,7 @@ CONF_CLOUDHOOK_CREATE_URL = "cloudhook_create_url"
|
||||||
CONF_REMOTE_API_URL = "remote_api_url"
|
CONF_REMOTE_API_URL = "remote_api_url"
|
||||||
CONF_ACME_DIRECTORY_SERVER = "acme_directory_server"
|
CONF_ACME_DIRECTORY_SERVER = "acme_directory_server"
|
||||||
CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url"
|
CONF_ALEXA_ACCESS_TOKEN_URL = "alexa_access_token_url"
|
||||||
|
CONF_GOOGLE_ACTIONS_REPORT_STATE_URL = "google_actions_report_state_url"
|
||||||
|
|
||||||
MODE_DEV = "development"
|
MODE_DEV = "development"
|
||||||
MODE_PROD = "production"
|
MODE_PROD = "production"
|
||||||
|
|
|
@ -1,6 +1,13 @@
|
||||||
"""Google config for Cloud."""
|
"""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.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||||
from homeassistant.components.google_assistant.helpers import AbstractConfig
|
from homeassistant.components.google_assistant.helpers import AbstractConfig
|
||||||
|
from homeassistant.helpers import entity_registry
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
PREF_SHOULD_EXPOSE,
|
PREF_SHOULD_EXPOSE,
|
||||||
|
@ -10,15 +17,31 @@ from .const import (
|
||||||
DEFAULT_DISABLE_2FA,
|
DEFAULT_DISABLE_2FA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CloudGoogleConfig(AbstractConfig):
|
class CloudGoogleConfig(AbstractConfig):
|
||||||
"""HA Cloud Configuration for Google Assistant."""
|
"""HA Cloud Configuration for Google Assistant."""
|
||||||
|
|
||||||
def __init__(self, config, prefs, cloud):
|
def __init__(self, hass, config, prefs, cloud):
|
||||||
"""Initialize the Alexa config."""
|
"""Initialize the Google config."""
|
||||||
|
super().__init__(hass)
|
||||||
self._config = config
|
self._config = config
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
self._cloud = cloud
|
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
|
@property
|
||||||
def agent_user_id(self):
|
def agent_user_id(self):
|
||||||
|
@ -35,16 +58,25 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return self._prefs.google_secure_devices_pin
|
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):
|
def should_expose(self, state):
|
||||||
"""If an entity should be exposed."""
|
"""If a state object should be exposed."""
|
||||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
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
|
return False
|
||||||
|
|
||||||
if not self._config["filter"].empty_filter:
|
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_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)
|
return entity_config.get(PREF_SHOULD_EXPOSE, DEFAULT_SHOULD_EXPOSE)
|
||||||
|
|
||||||
def should_2fa(self, state):
|
def should_2fa(self, state):
|
||||||
|
@ -52,3 +84,72 @@ class CloudGoogleConfig(AbstractConfig):
|
||||||
entity_configs = self._prefs.google_entity_configs
|
entity_configs = self._prefs.google_entity_configs
|
||||||
entity_config = entity_configs.get(state.entity_id, {})
|
entity_config = entity_configs.get(state.entity_id, {})
|
||||||
return not entity_config.get(PREF_DISABLE_2FA, DEFAULT_DISABLE_2FA)
|
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 aiohttp
|
||||||
import async_timeout
|
import async_timeout
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
from hass_nabucasa import Cloud
|
||||||
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
|
@ -28,6 +29,7 @@ from .const import (
|
||||||
InvalidTrustedNetworks,
|
InvalidTrustedNetworks,
|
||||||
InvalidTrustedProxies,
|
InvalidTrustedProxies,
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
|
PREF_GOOGLE_REPORT_STATE,
|
||||||
RequireRelink,
|
RequireRelink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -171,18 +173,9 @@ class GoogleActionsSyncView(HomeAssistantView):
|
||||||
async def post(self, request):
|
async def post(self, request):
|
||||||
"""Trigger a Google Actions sync."""
|
"""Trigger a Google Actions sync."""
|
||||||
hass = request.app["hass"]
|
hass = request.app["hass"]
|
||||||
cloud = hass.data[DOMAIN]
|
cloud: Cloud = hass.data[DOMAIN]
|
||||||
websession = hass.helpers.aiohttp_client.async_get_clientsession()
|
status = await cloud.client.google_config.async_sync_entities()
|
||||||
|
return self.json({}, status_code=status)
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class CloudLoginView(HomeAssistantView):
|
class CloudLoginView(HomeAssistantView):
|
||||||
|
@ -366,6 +359,7 @@ async def websocket_subscription(hass, connection, msg):
|
||||||
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
vol.Optional(PREF_ENABLE_GOOGLE): bool,
|
||||||
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
vol.Optional(PREF_ENABLE_ALEXA): bool,
|
||||||
vol.Optional(PREF_ALEXA_REPORT_STATE): 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),
|
vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "cloud",
|
"domain": "cloud",
|
||||||
"name": "Cloud",
|
"name": "Cloud",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
"documentation": "https://www.home-assistant.io/integrations/cloud",
|
||||||
"requirements": ["hass-nabucasa==0.17"],
|
"requirements": ["hass-nabucasa==0.22"],
|
||||||
"dependencies": ["http", "webhook"],
|
"dependencies": ["http", "webhook"],
|
||||||
"codeowners": ["@home-assistant/cloud"]
|
"codeowners": ["@home-assistant/cloud"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ from .const import (
|
||||||
PREF_ALEXA_ENTITY_CONFIGS,
|
PREF_ALEXA_ENTITY_CONFIGS,
|
||||||
PREF_ALEXA_REPORT_STATE,
|
PREF_ALEXA_REPORT_STATE,
|
||||||
DEFAULT_ALEXA_REPORT_STATE,
|
DEFAULT_ALEXA_REPORT_STATE,
|
||||||
|
PREF_GOOGLE_REPORT_STATE,
|
||||||
|
DEFAULT_GOOGLE_REPORT_STATE,
|
||||||
InvalidTrustedNetworks,
|
InvalidTrustedNetworks,
|
||||||
InvalidTrustedProxies,
|
InvalidTrustedProxies,
|
||||||
)
|
)
|
||||||
|
@ -74,6 +76,7 @@ class CloudPreferences:
|
||||||
google_entity_configs=_UNDEF,
|
google_entity_configs=_UNDEF,
|
||||||
alexa_entity_configs=_UNDEF,
|
alexa_entity_configs=_UNDEF,
|
||||||
alexa_report_state=_UNDEF,
|
alexa_report_state=_UNDEF,
|
||||||
|
google_report_state=_UNDEF,
|
||||||
):
|
):
|
||||||
"""Update user preferences."""
|
"""Update user preferences."""
|
||||||
for key, value in (
|
for key, value in (
|
||||||
|
@ -86,6 +89,7 @@ class CloudPreferences:
|
||||||
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
(PREF_GOOGLE_ENTITY_CONFIGS, google_entity_configs),
|
||||||
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
|
(PREF_ALEXA_ENTITY_CONFIGS, alexa_entity_configs),
|
||||||
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
|
(PREF_ALEXA_REPORT_STATE, alexa_report_state),
|
||||||
|
(PREF_GOOGLE_REPORT_STATE, google_report_state),
|
||||||
):
|
):
|
||||||
if value is not _UNDEF:
|
if value is not _UNDEF:
|
||||||
self._prefs[key] = value
|
self._prefs[key] = value
|
||||||
|
@ -164,6 +168,7 @@ class CloudPreferences:
|
||||||
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs,
|
||||||
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
|
PREF_ALEXA_ENTITY_CONFIGS: self.alexa_entity_configs,
|
||||||
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
|
PREF_ALEXA_REPORT_STATE: self.alexa_report_state,
|
||||||
|
PREF_GOOGLE_REPORT_STATE: self.google_report_state,
|
||||||
PREF_CLOUDHOOKS: self.cloudhooks,
|
PREF_CLOUDHOOKS: self.cloudhooks,
|
||||||
PREF_CLOUD_USER: self.cloud_user,
|
PREF_CLOUD_USER: self.cloud_user,
|
||||||
}
|
}
|
||||||
|
@ -196,6 +201,11 @@ class CloudPreferences:
|
||||||
"""Return if Google is enabled."""
|
"""Return if Google is enabled."""
|
||||||
return self._prefs[PREF_ENABLE_GOOGLE]
|
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
|
@property
|
||||||
def google_secure_devices_pin(self):
|
def google_secure_devices_pin(self):
|
||||||
"""Return if Google is allowed to unlock locks."""
|
"""Return if Google is allowed to unlock locks."""
|
||||||
|
|
|
@ -83,6 +83,13 @@ async def async_setup(hass: HomeAssistant, yaml_config: Dict[str, Any]):
|
||||||
try:
|
try:
|
||||||
with async_timeout.timeout(15):
|
with async_timeout.timeout(15):
|
||||||
agent_user_id = call.data.get("agent_user_id") or call.context.user_id
|
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(
|
res = await websession.post(
|
||||||
REQUEST_SYNC_BASE_URL,
|
REQUEST_SYNC_BASE_URL,
|
||||||
params={"key": api_key},
|
params={"key": api_key},
|
||||||
|
|
|
@ -3,7 +3,8 @@ from asyncio import gather
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from typing import List
|
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 (
|
from homeassistant.const import (
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
STATE_UNAVAILABLE,
|
STATE_UNAVAILABLE,
|
||||||
|
@ -22,10 +23,24 @@ from .const import (
|
||||||
)
|
)
|
||||||
from .error import SmartHomeError
|
from .error import SmartHomeError
|
||||||
|
|
||||||
|
SYNC_DELAY = 15
|
||||||
|
|
||||||
|
|
||||||
class AbstractConfig:
|
class AbstractConfig:
|
||||||
"""Hold the configuration for Google Assistant."""
|
"""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
|
@property
|
||||||
def agent_user_id(self):
|
def agent_user_id(self):
|
||||||
"""Return Agent User Id to use for query responses."""
|
"""Return Agent User Id to use for query responses."""
|
||||||
|
@ -41,6 +56,17 @@ class AbstractConfig:
|
||||||
"""Return entity config."""
|
"""Return entity config."""
|
||||||
return None
|
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:
|
def should_expose(self, state) -> bool:
|
||||||
"""Return if entity should be exposed."""
|
"""Return if entity should be exposed."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -50,11 +76,66 @@ class AbstractConfig:
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
return True
|
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:
|
class RequestData:
|
||||||
"""Hold data associated with a particular request."""
|
"""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."""
|
"""Initialize the request data."""
|
||||||
self.config = config
|
self.config = config
|
||||||
self.request_id = request_id
|
self.request_id = request_id
|
||||||
|
@ -71,7 +152,7 @@ def get_google_type(domain, device_class):
|
||||||
class GoogleEntity:
|
class GoogleEntity:
|
||||||
"""Adaptation of Entity expressed in Google's terms."""
|
"""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."""
|
"""Initialize a Google entity."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.config = config
|
self.config = config
|
||||||
|
@ -139,7 +220,7 @@ class GoogleEntity:
|
||||||
"name": {"name": name},
|
"name": {"name": name},
|
||||||
"attributes": {},
|
"attributes": {},
|
||||||
"traits": [trait.name for trait in traits],
|
"traits": [trait.name for trait in traits],
|
||||||
"willReportState": False,
|
"willReportState": self.config.should_report_state,
|
||||||
"type": device_type,
|
"type": device_type,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,10 +25,16 @@ _LOGGER = logging.getLogger(__name__)
|
||||||
class GoogleConfig(AbstractConfig):
|
class GoogleConfig(AbstractConfig):
|
||||||
"""Config for manual setup of Google."""
|
"""Config for manual setup of Google."""
|
||||||
|
|
||||||
def __init__(self, config):
|
def __init__(self, hass, config):
|
||||||
"""Initialize the config."""
|
"""Initialize the config."""
|
||||||
|
super().__init__(hass)
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def enabled(self):
|
||||||
|
"""Return if Google is enabled."""
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def agent_user_id(self):
|
def agent_user_id(self):
|
||||||
"""Return Agent User Id to use for query responses."""
|
"""Return Agent User Id to use for query responses."""
|
||||||
|
@ -77,7 +83,7 @@ class GoogleConfig(AbstractConfig):
|
||||||
@callback
|
@callback
|
||||||
def async_register_http(hass, cfg):
|
def async_register_http(hass, cfg):
|
||||||
"""Register HTTP views for Google Assistant."""
|
"""Register HTTP views for Google Assistant."""
|
||||||
hass.http.register_view(GoogleAssistantView(GoogleConfig(cfg)))
|
hass.http.register_view(GoogleAssistantView(GoogleConfig(hass, cfg)))
|
||||||
|
|
||||||
|
|
||||||
class GoogleAssistantView(HomeAssistantView):
|
class GoogleAssistantView(HomeAssistantView):
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
"name": "Google assistant",
|
"name": "Google assistant",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/google_assistant",
|
"documentation": "https://www.home-assistant.io/integrations/google_assistant",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"dependencies": [
|
"dependencies": ["http"],
|
||||||
"http"
|
"codeowners": ["@home-assistant/cloud"]
|
||||||
],
|
|
||||||
"codeowners": []
|
|
||||||
}
|
}
|
||||||
|
|
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")
|
@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.
|
"""Handle action.devices.DISCONNECT request.
|
||||||
|
|
||||||
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
|
https://developers.google.com/actions/smarthome/create#actiondevicesdisconnect
|
||||||
"""
|
"""
|
||||||
|
await data.config.async_deactivate_report_state()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ certifi>=2019.6.16
|
||||||
contextvars==2.4;python_version<"3.7"
|
contextvars==2.4;python_version<"3.7"
|
||||||
cryptography==2.7
|
cryptography==2.7
|
||||||
distro==1.4.0
|
distro==1.4.0
|
||||||
hass-nabucasa==0.17
|
hass-nabucasa==0.22
|
||||||
home-assistant-frontend==20191002.0
|
home-assistant-frontend==20191002.0
|
||||||
importlib-metadata==0.23
|
importlib-metadata==0.23
|
||||||
jinja2>=2.10.1
|
jinja2>=2.10.1
|
||||||
|
|
|
@ -607,7 +607,7 @@ habitipy==0.2.0
|
||||||
hangups==0.4.9
|
hangups==0.4.9
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.17
|
hass-nabucasa==0.22
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
hbmqtt==0.9.5
|
hbmqtt==0.9.5
|
||||||
|
|
|
@ -164,7 +164,7 @@ ha-ffmpeg==2.0
|
||||||
hangups==0.4.9
|
hangups==0.4.9
|
||||||
|
|
||||||
# homeassistant.components.cloud
|
# homeassistant.components.cloud
|
||||||
hass-nabucasa==0.17
|
hass-nabucasa==0.22
|
||||||
|
|
||||||
# homeassistant.components.mqtt
|
# homeassistant.components.mqtt
|
||||||
hbmqtt==0.9.5
|
hbmqtt==0.9.5
|
||||||
|
|
|
@ -59,7 +59,7 @@ async def test_alexa_config_invalidate_token(hass, cloud_prefs, aioclient_mock):
|
||||||
cloud_prefs,
|
cloud_prefs,
|
||||||
Mock(
|
Mock(
|
||||||
alexa_access_token_url="http://example/alexa_token",
|
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(),
|
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):
|
with patch_sync_helper() as (to_update, to_remove):
|
||||||
hass.bus.async_fire(
|
hass.bus.async_fire(
|
||||||
EVENT_ENTITY_REGISTRY_UPDATED,
|
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()
|
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()
|
@pytest.fixture()
|
||||||
def mock_auth():
|
def mock_auth():
|
||||||
"""Mock check token."""
|
"""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
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@ -357,6 +359,7 @@ async def test_websocket_status(
|
||||||
"google_secure_devices_pin": None,
|
"google_secure_devices_pin": None,
|
||||||
"alexa_entity_configs": {},
|
"alexa_entity_configs": {},
|
||||||
"alexa_report_state": False,
|
"alexa_report_state": False,
|
||||||
|
"google_report_state": False,
|
||||||
"remote_enabled": False,
|
"remote_enabled": False,
|
||||||
},
|
},
|
||||||
"alexa_entities": {
|
"alexa_entities": {
|
||||||
|
|
|
@ -28,6 +28,13 @@ async def test_constructor_loads_info_from_config(hass):
|
||||||
"user_pool_id": "test-user_pool_id",
|
"user_pool_id": "test-user_pool_id",
|
||||||
"region": "test-region",
|
"region": "test-region",
|
||||||
"relayer": "test-relayer",
|
"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.user_pool_id == "test-user_pool_id"
|
||||||
assert cl.region == "test-region"
|
assert cl.region == "test-region"
|
||||||
assert cl.relayer == "test-relayer"
|
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):
|
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."""
|
"""Fake config that always exposes everything."""
|
||||||
|
|
||||||
def __init__(
|
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."""
|
"""Initialize config."""
|
||||||
|
super().__init__(hass)
|
||||||
self._should_expose = should_expose
|
self._should_expose = should_expose
|
||||||
self._secure_devices_pin = secure_devices_pin
|
self._secure_devices_pin = secure_devices_pin
|
||||||
self._entity_config = entity_config or {}
|
self._entity_config = entity_config or {}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
"""The tests for google-assistant init."""
|
"""The tests for google-assistant init."""
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
|
from homeassistant.core import Context
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.components import google_assistant as ga
|
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
|
assert aioclient_mock.call_count == 0
|
||||||
yield from hass.services.async_call(
|
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
|
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):
|
async def test_query_disconnect(hass):
|
||||||
"""Test a disconnect message."""
|
"""Test a disconnect message."""
|
||||||
result = await sh.async_handle_message(
|
config = MockConfig(hass=hass)
|
||||||
hass,
|
config.async_enable_report_state()
|
||||||
BASIC_CONFIG,
|
assert config._unsub_report_state is not None
|
||||||
"test-agent",
|
with patch.object(
|
||||||
{"inputs": [{"intent": "action.devices.DISCONNECT"}], "requestId": REQ_ID},
|
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 result is None
|
||||||
|
assert len(mock_deactivate.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_trait_execute_adding_query_data(hass):
|
async def test_trait_execute_adding_query_data(hass):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue