diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index b1901fdf27f..7353ba6fd21 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -114,6 +114,10 @@ class CloudNotAvailable(HomeAssistantError): """Raised when an action requires the cloud but it's not available.""" +class CloudNotConnected(CloudNotAvailable): + """Raised when an action requires the cloud but it's not connected.""" + + @bind_hass @callback def async_is_logged_in(hass: HomeAssistant) -> bool: @@ -124,6 +128,13 @@ def async_is_logged_in(hass: HomeAssistant) -> bool: return DOMAIN in hass.data and hass.data[DOMAIN].is_logged_in +@bind_hass +@callback +def async_is_connected(hass: HomeAssistant) -> bool: + """Test if connected to the cloud.""" + return DOMAIN in hass.data and hass.data[DOMAIN].iot.connected + + @bind_hass @callback def async_active_subscription(hass: HomeAssistant) -> bool: @@ -134,6 +145,9 @@ def async_active_subscription(hass: HomeAssistant) -> bool: @bind_hass async def async_create_cloudhook(hass: HomeAssistant, webhook_id: str) -> str: """Create a cloudhook.""" + if not async_is_connected(hass): + raise CloudNotConnected + if not async_is_logged_in(hass): raise CloudNotAvailable diff --git a/homeassistant/components/dialogflow/strings.json b/homeassistant/components/dialogflow/strings.json index 0e7aa952c1a..4bd7ca461a3 100644 --- a/homeassistant/components/dialogflow/strings.json +++ b/homeassistant/components/dialogflow/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/geofency/strings.json b/homeassistant/components/geofency/strings.json index 855d3f43864..b42abcbfe6a 100644 --- a/homeassistant/components/geofency/strings.json +++ b/homeassistant/components/geofency/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/gpslogger/strings.json b/homeassistant/components/gpslogger/strings.json index 981fb49e60c..d75272fe33c 100644 --- a/homeassistant/components/gpslogger/strings.json +++ b/homeassistant/components/gpslogger/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/ifttt/strings.json b/homeassistant/components/ifttt/strings.json index d93879f6327..179d62b463c 100644 --- a/homeassistant/components/ifttt/strings.json +++ b/homeassistant/components/ifttt/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/locative/strings.json b/homeassistant/components/locative/strings.json index d7149122be2..ff1999306f4 100644 --- a/homeassistant/components/locative/strings.json +++ b/homeassistant/components/locative/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/mailgun/strings.json b/homeassistant/components/mailgun/strings.json index a26a908ff73..03446b50adf 100644 --- a/homeassistant/components/mailgun/strings.json +++ b/homeassistant/components/mailgun/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/traccar/strings.json b/homeassistant/components/traccar/strings.json index 89689ee43df..d8d100e1c2f 100644 --- a/homeassistant/components/traccar/strings.json +++ b/homeassistant/components/traccar/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/components/twilio/strings.json b/homeassistant/components/twilio/strings.json index ad91271f160..871711ff087 100644 --- a/homeassistant/components/twilio/strings.json +++ b/homeassistant/components/twilio/strings.json @@ -7,6 +7,7 @@ } }, "abort": { + "cloud_not_connected": "[%key:common::config_flow::abort::cloud_not_connected%]", "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", "webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]" }, diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6f07e5d665e..d7920f80941 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -215,6 +215,9 @@ class WebhookFlowHandler(config_entries.ConfigFlow): "cloud" in self.hass.config.components and self.hass.components.cloud.async_active_subscription() ): + if not self.hass.components.cloud.async_is_connected(): + return self.async_abort(reason="cloud_not_connected") + webhook_url = await self.hass.components.cloud.async_create_cloudhook( webhook_id ) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 39c041bb464..74ba965f6c5 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -73,7 +73,8 @@ "oauth2_authorize_url_timeout": "Timeout generating authorize URL.", "oauth2_no_url_available": "No URL available. For information about this error, [check the help section]({docs_url})", "reauth_successful": "Re-authentication was successful", - "unknown_authorize_url_generation": "Unknown error generating an authorize URL." + "unknown_authorize_url_generation": "Unknown error generating an authorize URL.", + "cloud_not_connected": "Not connected to Home Assistant Cloud." } } } diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 7f54ba99c8a..78b28a8ef10 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,5 +1,5 @@ """Tests for the Config Entry Flow helper.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock, PropertyMock, patch import pytest @@ -297,7 +297,7 @@ async def test_webhook_config_flow_registers_webhook(hass, webhook_flow_conf): async def test_webhook_create_cloudhook(hass, webhook_flow_conf): - """Test only a single entry is allowed.""" + """Test cloudhook will be created if subscribed.""" assert await setup.async_setup_component(hass, "cloud", {}) async_setup_entry = Mock(return_value=True) @@ -323,11 +323,15 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): "hass_nabucasa.cloudhooks.Cloudhooks.async_create", return_value={"cloudhook_url": "https://example.com"}, ) as mock_create, patch( - "homeassistant.components.cloud.async_active_subscription", return_value=True + "hass_nabucasa.Cloud.subscription_expired", + new_callable=PropertyMock(return_value=False), ), patch( - "homeassistant.components.cloud.async_is_logged_in", return_value=True + "hass_nabucasa.Cloud.is_logged_in", + new_callable=PropertyMock(return_value=True), + ), patch( + "hass_nabucasa.iot_base.BaseIoT.connected", + new_callable=PropertyMock(return_value=True), ): - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -346,6 +350,49 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): assert result["require_restart"] is False +async def test_webhook_create_cloudhook_aborts_not_connected(hass, webhook_flow_conf): + """Test cloudhook aborts if subscribed but not connected.""" + assert await setup.async_setup_component(hass, "cloud", {}) + + async_setup_entry = Mock(return_value=True) + async_unload_entry = Mock(return_value=True) + + mock_integration( + hass, + MockModule( + "test_single", + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + async_remove_entry=config_entry_flow.webhook_async_remove_entry, + ), + ) + mock_entity_platform(hass, "config_flow.test_single", None) + + result = await hass.config_entries.flow.async_init( + "test_single", context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "hass_nabucasa.cloudhooks.Cloudhooks.async_create", + return_value={"cloudhook_url": "https://example.com"}, + ), patch( + "hass_nabucasa.Cloud.subscription_expired", + new_callable=PropertyMock(return_value=False), + ), patch( + "hass_nabucasa.Cloud.is_logged_in", + new_callable=PropertyMock(return_value=True), + ), patch( + "hass_nabucasa.iot_base.BaseIoT.connected", + new_callable=PropertyMock(return_value=False), + ): + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cloud_not_connected" + + async def test_warning_deprecated_connection_class(hass, caplog): """Test that we log a warning when the connection_class is used.""" discovery_function = Mock()