From 81ad56b8ad9478739179701e6accfffb63771dff Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Wed, 2 Feb 2022 18:11:06 +0100 Subject: [PATCH] Add events on cloud connect and disconnect (#65215) * Add events on cloud connect and disconnect Signed-off-by: cgtobi * Use event capture helper Signed-off-by: cgtobi * Provide listener method instead of public event Signed-off-by: cgtobi * Add test for disconnect notification Signed-off-by: cgtobi * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Use Enum Signed-off-by: cgtobi * Add module level api Signed-off-by: cgtobi * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Clean up dead code Signed-off-by: cgtobi * Flake8 Signed-off-by: cgtobi * Clean up Co-authored-by: Martin Hjelmare --- homeassistant/components/cloud/__init__.py | 34 ++++++++++++++++++++++ tests/components/cloud/test_init.py | 19 ++++++++++++ 2 files changed, 53 insertions(+) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 7353ba6fd21..07c2898f204 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -1,5 +1,7 @@ """Component to integrate the Home Assistant cloud.""" import asyncio +from collections.abc import Callable +from enum import Enum from hass_nabucasa import Cloud import voluptuous as vol @@ -18,6 +20,10 @@ from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv, entityfilter from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from homeassistant.util.aiohttp import MockRequest @@ -52,6 +58,8 @@ DEFAULT_MODE = MODE_PROD SERVICE_REMOTE_CONNECT = "remote_connect" SERVICE_REMOTE_DISCONNECT = "remote_disconnect" +SIGNAL_CLOUD_CONNECTION_STATE = "CLOUD_CONNECTION_STATE" + ALEXA_ENTITY_SCHEMA = vol.Schema( { @@ -118,6 +126,13 @@ class CloudNotConnected(CloudNotAvailable): """Raised when an action requires the cloud but it's not connected.""" +class CloudConnectionState(Enum): + """Cloud connection state.""" + + CLOUD_CONNECTED = "cloud_connected" + CLOUD_DISCONNECTED = "cloud_disconnected" + + @bind_hass @callback def async_is_logged_in(hass: HomeAssistant) -> bool: @@ -135,6 +150,14 @@ def async_is_connected(hass: HomeAssistant) -> bool: return DOMAIN in hass.data and hass.data[DOMAIN].iot.connected +@callback +def async_listen_connection_change( + hass: HomeAssistant, target: Callable[[CloudConnectionState], None] +) -> Callable[[], None]: + """Notify on connection state changes.""" + return async_dispatcher_connect(hass, SIGNAL_CLOUD_CONNECTION_STATE, target) + + @bind_hass @callback def async_active_subscription(hass: HomeAssistant) -> bool: @@ -252,11 +275,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: Platform.TTS, DOMAIN, {}, config ) + async_dispatcher_send( + hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_CONNECTED + ) + + async def _on_disconnect(): + """Handle cloud disconnect.""" + async_dispatcher_send( + hass, SIGNAL_CLOUD_CONNECTION_STATE, CloudConnectionState.CLOUD_DISCONNECTED + ) + async def _on_initialized(): """Update preferences.""" await prefs.async_update(remote_domain=cloud.remote.instance_domain) cloud.iot.register_on_connect(_on_connect) + cloud.iot.register_on_disconnect(_on_disconnect) cloud.register_on_initialized(_on_initialized) await cloud.initialize() diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 4a513aff117..78a8f83eef6 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -137,6 +137,14 @@ async def test_on_connect(hass, mock_cloud_fixture): assert len(hass.states.async_entity_ids("binary_sensor")) == 0 + cloud_states = [] + + def handle_state(cloud_state): + nonlocal cloud_states + cloud_states.append(cloud_state) + + cloud.async_listen_connection_change(hass, handle_state) + assert "async_setup" in str(cl.iot._on_connect[-1]) await cl.iot._on_connect[-1]() await hass.async_block_till_done() @@ -149,6 +157,17 @@ async def test_on_connect(hass, mock_cloud_fixture): assert len(mock_load.mock_calls) == 0 + assert len(cloud_states) == 1 + assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_CONNECTED + + assert len(cl.iot._on_disconnect) == 2 + assert "async_setup" in str(cl.iot._on_disconnect[-1]) + await cl.iot._on_disconnect[-1]() + await hass.async_block_till_done() + + assert len(cloud_states) == 2 + assert cloud_states[-1] == cloud.CloudConnectionState.CLOUD_DISCONNECTED + async def test_remote_ui_url(hass, mock_cloud_fixture): """Test getting remote ui url."""