def async_disable_local_sdk(self): """Disable the local SDK.""" if not self._local_sdk_active: return webhook.async_unregister(self.hass, self.local_sdk_webhook_id) self._local_sdk_active = False
async def unload_smartapp_endpoint(hass: HomeAssistant): """Tear down the component configuration.""" if DOMAIN not in hass.data: return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url and hass.components.cloud.async_is_logged_in(): await hass.components.cloud.async_delete_cloudhook( hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save({ CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID], CONF_WEBHOOK_ID: hass.data[DOMAIN][CONF_WEBHOOK_ID], CONF_CLOUDHOOK_URL: None, }) _LOGGER.debug("Cloudhook '%s' was removed", cloudhook_url) # Remove the webhook webhook.async_unregister(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Disconnect all brokers for broker in hass.data[DOMAIN][DATA_BROKERS].values(): broker.disconnect() # Remove all handlers from manager hass.data[DOMAIN][DATA_MANAGER].dispatcher.disconnect_all() # Remove the component data hass.data.pop(DOMAIN)
async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" # Circular dep # pylint: disable=import-outside-toplevel from . import smart_home self._local_last_active = utcnow() payload = await request.json() if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Received local message from %s (JS %s):\n%s\n", request.remote, request.headers.get("HA-Cloud-Version", "unknown"), pprint.pformat(payload), ) if not self.enabled: return json_response(smart_home.turned_off_response(payload)) if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None: # No agent user linked to this webhook, means that the user has somehow unregistered # removing webhook and stopping processing of this request. _LOGGER.error( "Cannot process request for webhook %s as no linked agent user is found:\n%s\n", webhook_id, pprint.pformat(payload), ) webhook.async_unregister(self.hass, webhook_id) return None
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload an OwnTracks config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) hass.data[DOMAIN]["unsub"]() return unload_ok
async def unload_smartapp_endpoint(hass: HomeAssistantType): """Tear down the component configuration.""" if DOMAIN not in hass.data: return # Remove the cloudhook if it was created cloudhook_url = hass.data[DOMAIN][CONF_CLOUDHOOK_URL] if cloudhook_url and cloud.async_is_logged_in(hass): await cloud.async_delete_cloudhook( hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Remove cloudhook from storage store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) await store.async_save({ CONF_INSTANCE_ID: hass.data[DOMAIN][CONF_INSTANCE_ID], CONF_WEBHOOK_ID: hass.data[DOMAIN][CONF_WEBHOOK_ID], CONF_CLOUDHOOK_URL: None }) _LOGGER.debug("Cloudhook '%s' was removed", cloudhook_url) # Remove the webhook webhook.async_unregister(hass, hass.data[DOMAIN][CONF_WEBHOOK_ID]) # Disconnect all brokers for broker in hass.data[DOMAIN][DATA_BROKERS].values(): broker.disconnect() # Remove all handlers from manager hass.data[DOMAIN][DATA_MANAGER].dispatcher.disconnect_all() # Remove the component data hass.data.pop(DOMAIN)
def async_disable_local_sdk(self): """Disable the local SDK.""" if not self._local_sdk_active: return for agent_user_id in self._store.agent_user_ids: webhook.async_unregister(self.hass, self.get_local_webhook_id(agent_user_id)) self._local_sdk_active = False
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) session = hass.data[DOMAIN].pop(entry.entry_id) await session.remove_webhook() unload_ok = await hass.config_entries.async_unload_platforms( entry, PLATFORMS) if not hass.data[DOMAIN]: hass.data.pop(DOMAIN) return unload_ok
async def test_unregistering_webhook(hass, mock_client): """Test unregistering a webhook.""" hooks = [] webhook_id = webhook.async_generate_id() async def handle(*args): """Handle webhook.""" hooks.append(args) webhook.async_register(hass, "test", "Test hook", webhook_id, handle) resp = await mock_client.post(f"/api/webhook/{webhook_id}") assert resp.status == HTTPStatus.OK assert len(hooks) == 1 webhook.async_unregister(hass, webhook_id) resp = await mock_client.post(f"/api/webhook/{webhook_id}") assert resp.status == HTTPStatus.OK assert len(hooks) == 1
async def _handle_local_webhook(self, hass, webhook_id, request): """Handle an incoming local SDK message.""" # Circular dep # pylint: disable=import-outside-toplevel from . import smart_home self._local_last_active = utcnow() # Check version local SDK. version = request.headers.get("HA-Cloud-Version") if not self._local_sdk_version_warn and ( not version or AwesomeVersion(version) < LOCAL_SDK_MIN_VERSION): _LOGGER.warning( "Local SDK version is too old (%s), check documentation on how to update to the latest version", version, ) self._local_sdk_version_warn = True payload = await request.json() if _LOGGER.isEnabledFor(logging.DEBUG): _LOGGER.debug( "Received local message from %s (JS %s):\n%s\n", request.remote, request.headers.get("HA-Cloud-Version", "unknown"), pprint.pformat(payload), ) if (agent_user_id := self.get_local_agent_user_id(webhook_id)) is None: # No agent user linked to this webhook, means that the user has somehow unregistered # removing webhook and stopping processing of this request. _LOGGER.error( "Cannot process request for webhook %s as no linked agent user is found:\n%s\n", webhook_id, pprint.pformat(payload), ) webhook.async_unregister(self.hass, webhook_id) return None
class AbstractConfig(ABC): """Hold the configuration for Google Assistant.""" _unsub_report_state = None def __init__(self, hass): """Initialize abstract config.""" self.hass = hass self._store = None self._google_sync_unsub = {} self._local_sdk_active = False self._local_last_active: datetime | None = None async def async_initialize(self): """Perform async initialization of config.""" self._store = GoogleConfigStore(self.hass) await self._store.async_initialize() if not self.enabled: return async def sync_google(_): """Sync entities to Google.""" await self.async_sync_entities_all() start.async_at_start(self.hass, sync_google) @property def enabled(self): """Return if Google is enabled.""" return False @property def entity_config(self): """Return entity config.""" return {} @property def secure_devices_pin(self): """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 is_local_sdk_active(self): """Return if we're actively accepting local messages.""" return self._local_sdk_active @property def should_report_state(self): """Return if states should be proactively reported.""" return False @property def is_local_connected(self) -> bool: """Return if local is connected.""" return (self._local_last_active is not None # We get a reachable devices intent every minute. and self._local_last_active > utcnow() - timedelta(seconds=70)) def get_local_agent_user_id(self, webhook_id): """Return the user ID to be used for actions received via the local SDK. Return None is no agent user id is found. """ found_agent_user_id = None for agent_user_id, agent_user_data in self._store.agent_user_ids.items( ): if agent_user_data[STORE_GOOGLE_LOCAL_WEBHOOK_ID] == webhook_id: found_agent_user_id = agent_user_id break return found_agent_user_id def get_local_webhook_id(self, agent_user_id): """Return the webhook ID to be used for actions for a given agent user id via the local SDK.""" return self._store.agent_user_ids[agent_user_id][ STORE_GOOGLE_LOCAL_WEBHOOK_ID] @abstractmethod def get_agent_user_id(self, context): """Get agent user ID from context.""" @abstractmethod def should_expose(self, state) -> bool: """Return if entity should be exposed.""" def should_2fa(self, state): """If an entity should have 2FA checked.""" return True async def async_report_state(self, message, agent_user_id: str): """Send a state report to Google.""" raise NotImplementedError async def async_report_state_all(self, message): """Send a state report to Google for all previously synced users.""" jobs = [ self.async_report_state(message, agent_user_id) for agent_user_id in self._store.agent_user_ids ] await gather(*jobs) @callback def async_enable_report_state(self): """Enable proactive mode.""" # Circular dep # pylint: disable=import-outside-toplevel 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) @callback 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, agent_user_id: str): """Sync all entities to Google.""" # Remove any pending sync self._google_sync_unsub.pop(agent_user_id, lambda: None)() status = await self._async_request_sync_devices(agent_user_id) if status == HTTPStatus.NOT_FOUND: await self.async_disconnect_agent_user(agent_user_id) return status async def async_sync_entities_all(self): """Sync all entities to Google for all registered agents.""" res = await gather(*(self.async_sync_entities(agent_user_id) for agent_user_id in self._store.agent_user_ids)) return max(res, default=204) @callback def async_schedule_google_sync(self, agent_user_id: str): """Schedule a sync.""" async def _schedule_callback(_now): """Handle a scheduled sync callback.""" self._google_sync_unsub.pop(agent_user_id, None) await self.async_sync_entities(agent_user_id) self._google_sync_unsub.pop(agent_user_id, lambda: None)() self._google_sync_unsub[agent_user_id] = async_call_later( self.hass, SYNC_DELAY, _schedule_callback) @callback def async_schedule_google_sync_all(self): """Schedule a sync for all registered agents.""" for agent_user_id in self._store.agent_user_ids: self.async_schedule_google_sync(agent_user_id) async def _async_request_sync_devices(self, agent_user_id: str) -> int: """Trigger a sync with Google. Return value is the HTTP status code of the sync request. """ raise NotImplementedError async def async_connect_agent_user(self, agent_user_id: str): """Add a synced and known agent_user_id. Called before sending a sync response to Google. """ self._store.add_agent_user_id(agent_user_id) async def async_disconnect_agent_user(self, agent_user_id: str): """Turn off report state and disable further state reporting. Called when: - The user disconnects their account from Google. - When the cloud configuration is initialized - When sync entities fails with 404 """ self._store.pop_agent_user_id(agent_user_id) @callback def async_enable_local_sdk(self): """Enable the local SDK.""" setup_successful = True setup_webhook_ids = [] # Don't enable local SDK if ssl is enabled if self.hass.config.api and self.hass.config.api.use_ssl: self._local_sdk_active = False return for user_agent_id, _ in self._store.agent_user_ids.items(): if (webhook_id := self.get_local_webhook_id(user_agent_id)) is None: setup_successful = False break try: webhook.async_register( self.hass, DOMAIN, "Local Support for " + user_agent_id, webhook_id, self._handle_local_webhook, local_only=True, ) setup_webhook_ids.append(webhook_id) except ValueError: _LOGGER.warning( "Webhook handler %s for agent user id %s is already defined!", webhook_id, user_agent_id, ) setup_successful = False break if not setup_successful: _LOGGER.warning( "Local fulfillment failed to setup, falling back to cloud fulfillment" ) for setup_webhook_id in setup_webhook_ids: webhook.async_unregister(self.hass, setup_webhook_id) self._local_sdk_active = setup_successful
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) return True
def async_unregister_webhook(hass: HomeAssistant, entry: ConfigEntry) -> None: """Unregister a webhook.""" webhook_id: str = entry.data[CONF_WEBHOOK_ID] webhook.async_unregister(hass, webhook_id)
async def async_unload_webhook(hass: HomeAssistant, entry: ConfigEntry): """Unload webhook based entry.""" if entry.data[CONF_WEBHOOK_ID] is not None: webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID]) return await async_unload_platforms(hass, entry, PLATFORMS)
setup_webhook_ids.append(webhook_id) except ValueError: _LOGGER.warning( "Webhook handler %s for agent user id %s is already defined!", webhook_id, user_agent_id, ) setup_successful = False break if not setup_successful: _LOGGER.warning( "Local fulfillment failed to setup, falling back to cloud fulfillment" ) for setup_webhook_id in setup_webhook_ids: webhook.async_unregister(self.hass, setup_webhook_id) self._local_sdk_active = setup_successful @callback def async_disable_local_sdk(self): """Disable the local SDK.""" if not self._local_sdk_active: return for agent_user_id in self._store.agent_user_ids: webhook.async_unregister(self.hass, self.get_local_webhook_id(agent_user_id)) self._local_sdk_active = False