Ejemplo n.º 1
0
    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
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
    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
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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)
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
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
Ejemplo n.º 8
0
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
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
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
Ejemplo n.º 11
0
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)
Ejemplo n.º 12
0
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
Ejemplo n.º 13
0
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)
Ejemplo n.º 14
0
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)
Ejemplo n.º 15
0
                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