From 2ddeb0c013b859bc8d17585522570881188ef2d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Aug 2021 07:52:30 -0700 Subject: [PATCH] Ask for host because EAGLE mdns doesn't work in HA OS (#54905) --- .../rainforest_eagle/config_flow.py | 31 ++++++--- .../components/rainforest_eagle/data.py | 65 ++++++++++--------- .../components/rainforest_eagle/sensor.py | 2 + .../components/rainforest_eagle/strings.json | 1 + .../rainforest_eagle/translations/en.json | 1 + tests/components/rainforest_eagle/__init__.py | 63 ------------------ .../rainforest_eagle/test_config_flow.py | 25 +++++-- .../components/rainforest_eagle/test_init.py | 65 +++++++++++++++++++ .../rainforest_eagle/test_sensor.py | 4 +- 9 files changed, 146 insertions(+), 111 deletions(-) create mode 100644 tests/components/rainforest_eagle/test_init.py diff --git a/homeassistant/components/rainforest_eagle/config_flow.py b/homeassistant/components/rainforest_eagle/config_flow.py index acab5fc2070..be921c31bf7 100644 --- a/homeassistant/components/rainforest_eagle/config_flow.py +++ b/homeassistant/components/rainforest_eagle/config_flow.py @@ -7,7 +7,7 @@ from typing import Any import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.data_entry_flow import FlowResult from . import data @@ -15,12 +15,20 @@ from .const import CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, CONF_INSTALL_CODE, DOMA _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_CLOUD_ID): str, - vol.Required(CONF_INSTALL_CODE): str, - } -) + +def create_schema(user_input: dict[str, Any] | None) -> vol.Schema: + """Create user schema with passed in defaults if available.""" + if user_input is None: + user_input = {} + return vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, + vol.Required(CONF_CLOUD_ID, default=user_input.get(CONF_CLOUD_ID)): str, + vol.Required( + CONF_INSTALL_CODE, default=user_input.get(CONF_INSTALL_CODE) + ): str, + } + ) class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -34,7 +42,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" if user_input is None: return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA + step_id="user", data_schema=create_schema(user_input) ) await self.async_set_unique_id(user_input[CONF_CLOUD_ID]) @@ -42,7 +50,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: eagle_type, hardware_address = await data.async_get_type( - self.hass, user_input[CONF_CLOUD_ID], user_input[CONF_INSTALL_CODE] + self.hass, + user_input[CONF_CLOUD_ID], + user_input[CONF_INSTALL_CODE], + user_input[CONF_HOST], ) except data.CannotConnect: errors["base"] = "cannot_connect" @@ -59,7 +70,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", data_schema=create_schema(user_input), errors=errors ) async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: diff --git a/homeassistant/components/rainforest_eagle/data.py b/homeassistant/components/rainforest_eagle/data.py index e4cfe144a5e..07835212666 100644 --- a/homeassistant/components/rainforest_eagle/data.py +++ b/homeassistant/components/rainforest_eagle/data.py @@ -11,7 +11,7 @@ from requests.exceptions import ConnectionError as ConnectError, HTTPError, Time from uEagle import Eagle as Eagle100Reader from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import aiohttp_client @@ -27,7 +27,7 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -UPDATE_100_ERRORS = (ConnectError, HTTPError, Timeout, ValueError) +UPDATE_100_ERRORS = (ConnectError, HTTPError, Timeout) class RainforestError(HomeAssistantError): @@ -42,12 +42,37 @@ class InvalidAuth(RainforestError): """Error to indicate bad auth.""" -async def async_get_type(hass, cloud_id, install_code): +async def async_get_type(hass, cloud_id, install_code, host): """Try API call 'get_network_info' to see if target device is Eagle-100 or Eagle-200.""" - reader = Eagle100Reader(cloud_id, install_code) + # For EAGLE-200, fetch the hardware address of the meter too. + hub = aioeagle.EagleHub( + aiohttp_client.async_get_clientsession(hass), cloud_id, install_code, host=host + ) + + try: + with async_timeout.timeout(30): + meters = await hub.get_device_list() + except aioeagle.BadAuth as err: + raise InvalidAuth from err + except aiohttp.ClientError: + # This can happen if it's an eagle-100 + meters = None + + if meters is not None: + if meters: + hardware_address = meters[0].hardware_address + else: + hardware_address = None + + return TYPE_EAGLE_200, hardware_address + + reader = Eagle100Reader(cloud_id, install_code, host) try: response = await hass.async_add_executor_job(reader.get_network_info) + except ValueError as err: + # This could be invalid auth because it doesn't check 401 and tries to read JSON. + raise InvalidAuth from err except UPDATE_100_ERRORS as error: _LOGGER.error("Failed to connect during setup: %s", error) raise CannotConnect from error @@ -59,32 +84,7 @@ async def async_get_type(hass, cloud_id, install_code): ): return TYPE_EAGLE_100, None - # Branch to test if target is not an Eagle-200 Model - if ( - "Response" not in response - or response["Response"].get("Command") != "get_network_info" - ): - # We don't support this - return None, None - - # For EAGLE-200, fetch the hardware address of the meter too. - hub = aioeagle.EagleHub( - aiohttp_client.async_get_clientsession(hass), cloud_id, install_code - ) - - try: - meters = await hub.get_device_list() - except aioeagle.BadAuth as err: - raise InvalidAuth from err - except aiohttp.ClientError as err: - raise CannotConnect from err - - if meters: - hardware_address = meters[0].hardware_address - else: - hardware_address = None - - return TYPE_EAGLE_200, hardware_address + return None, None class EagleDataCoordinator(DataUpdateCoordinator): @@ -133,6 +133,7 @@ class EagleDataCoordinator(DataUpdateCoordinator): aiohttp_client.async_get_clientsession(self.hass), self.cloud_id, self.entry.data[CONF_INSTALL_CODE], + host=self.entry.data[CONF_HOST], ) self.eagle200_meter = aioeagle.ElectricMeter.create_instance( hub, self.hardware_address @@ -158,7 +159,9 @@ class EagleDataCoordinator(DataUpdateCoordinator): """Fetch and return the four sensor values in a dict.""" if self.eagle100_reader is None: self.eagle100_reader = Eagle100Reader( - self.cloud_id, self.entry.data[CONF_INSTALL_CODE] + self.cloud_id, + self.entry.data[CONF_INSTALL_CODE], + self.entry.data[CONF_HOST], ) out = {} diff --git a/homeassistant/components/rainforest_eagle/sensor.py b/homeassistant/components/rainforest_eagle/sensor.py index 67f61ffdc29..e3250dff30d 100644 --- a/homeassistant/components/rainforest_eagle/sensor.py +++ b/homeassistant/components/rainforest_eagle/sensor.py @@ -17,6 +17,7 @@ from homeassistant.components.sensor import ( ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( + CONF_HOST, CONF_IP_ADDRESS, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, @@ -85,6 +86,7 @@ async def async_setup_platform( DOMAIN, context={"source": SOURCE_IMPORT}, data={ + CONF_HOST: config[CONF_IP_ADDRESS], CONF_CLOUD_ID: config[CONF_CLOUD_ID], CONF_INSTALL_CODE: config[CONF_INSTALL_CODE], }, diff --git a/homeassistant/components/rainforest_eagle/strings.json b/homeassistant/components/rainforest_eagle/strings.json index d8e587c98ca..b32f38302f4 100644 --- a/homeassistant/components/rainforest_eagle/strings.json +++ b/homeassistant/components/rainforest_eagle/strings.json @@ -3,6 +3,7 @@ "step": { "user": { "data": { + "host": "[%key:common::config_flow::data::host%]", "cloud_id": "Cloud ID", "install_code": "Installation Code" } diff --git a/homeassistant/components/rainforest_eagle/translations/en.json b/homeassistant/components/rainforest_eagle/translations/en.json index 4307fc43a34..633d6551bd0 100644 --- a/homeassistant/components/rainforest_eagle/translations/en.json +++ b/homeassistant/components/rainforest_eagle/translations/en.json @@ -12,6 +12,7 @@ "user": { "data": { "cloud_id": "Cloud ID", + "host": "Host", "install_code": "Installation Code" } } diff --git a/tests/components/rainforest_eagle/__init__.py b/tests/components/rainforest_eagle/__init__.py index c5e41591789..df4f1749d49 100644 --- a/tests/components/rainforest_eagle/__init__.py +++ b/tests/components/rainforest_eagle/__init__.py @@ -1,64 +1 @@ """Tests for the Rainforest Eagle integration.""" -from unittest.mock import patch - -from homeassistant import config_entries, setup -from homeassistant.components.rainforest_eagle.const import ( - CONF_CLOUD_ID, - CONF_HARDWARE_ADDRESS, - CONF_INSTALL_CODE, - DOMAIN, - TYPE_EAGLE_200, -) -from homeassistant.const import CONF_TYPE -from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT -from homeassistant.setup import async_setup_component - - -async def test_import(hass: HomeAssistant) -> None: - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.rainforest_eagle.data.async_get_type", - return_value=(TYPE_EAGLE_200, "mock-hw"), - ), patch( - "homeassistant.components.rainforest_eagle.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - await async_setup_component( - hass, - "sensor", - { - "sensor": { - "platform": DOMAIN, - "ip_address": "192.168.1.55", - CONF_CLOUD_ID: "abcdef", - CONF_INSTALL_CODE: "123456", - } - }, - ) - await hass.async_block_till_done() - - entries = hass.config_entries.async_entries(DOMAIN) - assert len(entries) == 1 - entry = entries[0] - - assert entry.title == "abcdef" - assert entry.data == { - CONF_TYPE: TYPE_EAGLE_200, - CONF_CLOUD_ID: "abcdef", - CONF_INSTALL_CODE: "123456", - CONF_HARDWARE_ADDRESS: "mock-hw", - } - assert len(mock_setup_entry.mock_calls) == 1 - - # Second time we should get already_configured - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - data={CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, - context={"source": config_entries.SOURCE_IMPORT}, - ) - - assert result2["type"] == RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" diff --git a/tests/components/rainforest_eagle/test_config_flow.py b/tests/components/rainforest_eagle/test_config_flow.py index 0a294875f76..a54fbdac4db 100644 --- a/tests/components/rainforest_eagle/test_config_flow.py +++ b/tests/components/rainforest_eagle/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.rainforest_eagle.const import ( TYPE_EAGLE_200, ) from homeassistant.components.rainforest_eagle.data import CannotConnect, InvalidAuth -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM @@ -33,7 +33,11 @@ async def test_form(hass: HomeAssistant) -> None: ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, + { + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + CONF_HOST: "192.168.1.55", + }, ) await hass.async_block_till_done() @@ -41,6 +45,7 @@ async def test_form(hass: HomeAssistant) -> None: assert result2["title"] == "abcdef" assert result2["data"] == { CONF_TYPE: TYPE_EAGLE_200, + CONF_HOST: "192.168.1.55", CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456", CONF_HARDWARE_ADDRESS: "mock-hw", @@ -55,12 +60,16 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.rainforest_eagle.data.Eagle100Reader.get_network_info", + "aioeagle.EagleHub.get_device_list", side_effect=InvalidAuth, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, + { + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + CONF_HOST: "192.168.1.55", + }, ) assert result2["type"] == RESULT_TYPE_FORM @@ -74,12 +83,16 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) with patch( - "homeassistant.components.rainforest_eagle.data.Eagle100Reader.get_network_info", + "aioeagle.EagleHub.get_device_list", side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, + { + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + CONF_HOST: "192.168.1.55", + }, ) assert result2["type"] == RESULT_TYPE_FORM diff --git a/tests/components/rainforest_eagle/test_init.py b/tests/components/rainforest_eagle/test_init.py new file mode 100644 index 00000000000..0c3305732cb --- /dev/null +++ b/tests/components/rainforest_eagle/test_init.py @@ -0,0 +1,65 @@ +"""Tests for the Rainforest Eagle integration.""" +from unittest.mock import patch + +from homeassistant import config_entries, setup +from homeassistant.components.rainforest_eagle.const import ( + CONF_CLOUD_ID, + CONF_HARDWARE_ADDRESS, + CONF_INSTALL_CODE, + DOMAIN, + TYPE_EAGLE_200, +) +from homeassistant.const import CONF_HOST, CONF_TYPE +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT +from homeassistant.setup import async_setup_component + + +async def test_import(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.rainforest_eagle.data.async_get_type", + return_value=(TYPE_EAGLE_200, "mock-hw"), + ), patch( + "homeassistant.components.rainforest_eagle.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + await async_setup_component( + hass, + "sensor", + { + "sensor": { + "platform": DOMAIN, + "ip_address": "192.168.1.55", + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + } + }, + ) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + entry = entries[0] + + assert entry.title == "abcdef" + assert entry.data == { + CONF_TYPE: TYPE_EAGLE_200, + CONF_HOST: "192.168.1.55", + CONF_CLOUD_ID: "abcdef", + CONF_INSTALL_CODE: "123456", + CONF_HARDWARE_ADDRESS: "mock-hw", + } + assert len(mock_setup_entry.mock_calls) == 1 + + # Second time we should get already_configured + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + data={CONF_CLOUD_ID: "abcdef", CONF_INSTALL_CODE: "123456"}, + context={"source": config_entries.SOURCE_IMPORT}, + ) + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" diff --git a/tests/components/rainforest_eagle/test_sensor.py b/tests/components/rainforest_eagle/test_sensor.py index 46621eb5fdc..cf7e4a1d011 100644 --- a/tests/components/rainforest_eagle/test_sensor.py +++ b/tests/components/rainforest_eagle/test_sensor.py @@ -11,7 +11,7 @@ from homeassistant.components.rainforest_eagle.const import ( TYPE_EAGLE_100, TYPE_EAGLE_200, ) -from homeassistant.const import CONF_TYPE +from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -58,6 +58,7 @@ async def setup_rainforest_200(hass): domain="rainforest_eagle", data={ CONF_CLOUD_ID: MOCK_CLOUD_ID, + CONF_HOST: "192.168.1.55", CONF_INSTALL_CODE: "abcdefgh", CONF_HARDWARE_ADDRESS: "mock-hw-address", CONF_TYPE: TYPE_EAGLE_200, @@ -79,6 +80,7 @@ async def setup_rainforest_100(hass): domain="rainforest_eagle", data={ CONF_CLOUD_ID: MOCK_CLOUD_ID, + CONF_HOST: "192.168.1.55", CONF_INSTALL_CODE: "abcdefgh", CONF_HARDWARE_ADDRESS: None, CONF_TYPE: TYPE_EAGLE_100,