From 2e034c657c1cb4112ab6580f97d16e6b4b421712 Mon Sep 17 00:00:00 2001 From: Rebecca Borgqvist Date: Fri, 7 Jun 2024 14:24:43 +0000 Subject: [PATCH 1/4] wip --- homeassistant/components/nanogrid_air/api.py | 54 ------------------- .../components/nanogrid_air/config_flow.py | 13 +++-- .../components/nanogrid_air/manifest.json | 3 +- .../components/nanogrid_air/sensor.py | 6 +-- requirements_all.txt | 3 ++ requirements_test_all.txt | 3 ++ .../nanogrid_air/test_config_flow.py | 3 +- 7 files changed, 19 insertions(+), 66 deletions(-) delete mode 100644 homeassistant/components/nanogrid_air/api.py diff --git a/homeassistant/components/nanogrid_air/api.py b/homeassistant/components/nanogrid_air/api.py deleted file mode 100644 index 6c2dbb58e3e26..0000000000000 --- a/homeassistant/components/nanogrid_air/api.py +++ /dev/null @@ -1,54 +0,0 @@ -"""Fetch API for Nanogrid Air.""" - -import socket - -import aiohttp - -device_ip: str | None = None - - -async def get_ip(users_ip=None): - """Fetch IP address from hostname.""" - if users_ip: - globals()["device_ip"] = users_ip - return True - - try: - ip = socket.gethostbyname("ctek-ng-air.local") - if ip: - globals()["device_ip"] = ip - return True - except socket.gaierror: - return False - - -async def fetch_mac(): - """Fetch mac address from status API.""" - url_status = f"http://{device_ip}/status/" - - async with aiohttp.ClientSession() as session: - try: - async with session.get(url_status) as api_status_response: - api_status_response.raise_for_status() - mac = await api_status_response.json() - return mac["deviceInfo"]["mac"] - except aiohttp.ClientError: - return {} - except aiohttp.HttpProcessingError: - return {} - - -async def fetch_meter_data(): - """Fetch data from the API and return as a dictionary.""" - url_meter = f"http://{device_ip}/meter/" - - async with aiohttp.ClientSession() as session: - try: - async with session.get(url_meter) as api_meter_response: - api_meter_response.raise_for_status() - return await api_meter_response.json() - except aiohttp.ClientError: - await get_ip() - return {} - except aiohttp.HttpProcessingError: - return {} diff --git a/homeassistant/components/nanogrid_air/config_flow.py b/homeassistant/components/nanogrid_air/config_flow.py index 66a9d22a616ca..c875fb0648df0 100644 --- a/homeassistant/components/nanogrid_air/config_flow.py +++ b/homeassistant/components/nanogrid_air/config_flow.py @@ -2,15 +2,14 @@ from __future__ import annotations +from ctek import NanogridAir import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_URL -from .api import fetch_mac, get_ip from .const import DOMAIN -API_DEFAULT = "http://ctek-ng-air.local/meter/" TITLE = "Nanogrid Air" USER_DESC = "description" @@ -22,7 +21,7 @@ class NanogridAirConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._url = API_DEFAULT + self._url = NanogridAir().get_ip async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle a flow initiated by the user.""" @@ -37,8 +36,8 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult: # Attempt to automatically detect the device if user_input is None: try: - if await get_ip(): - mac_address = await fetch_mac() + if await NanogridAir().get_ip(): + mac_address = await NanogridAir().fetch_mac() if mac_address: unique_id = mac_address await self.async_set_unique_id(unique_id) @@ -61,8 +60,8 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult: # Handle user input try: - if await get_ip(user_input[CONF_URL]): - mac_address = await fetch_mac() + if await NanogridAir().get_ip(user_input[CONF_URL]): + mac_address = await NanogridAir().fetch_mac() if mac_address: unique_id = mac_address await self.async_set_unique_id(unique_id) diff --git a/homeassistant/components/nanogrid_air/manifest.json b/homeassistant/components/nanogrid_air/manifest.json index 45cea3820ba33..adfcf3a015a65 100644 --- a/homeassistant/components/nanogrid_air/manifest.json +++ b/homeassistant/components/nanogrid_air/manifest.json @@ -5,5 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nanogrid_air", "integration_type": "device", - "iot_class": "local_polling" + "iot_class": "local_polling", + "requirements": ["ctek==0.1.0"] } diff --git a/homeassistant/components/nanogrid_air/sensor.py b/homeassistant/components/nanogrid_air/sensor.py index 48229b2b9158a..ab1a343e7b0c3 100644 --- a/homeassistant/components/nanogrid_air/sensor.py +++ b/homeassistant/components/nanogrid_air/sensor.py @@ -2,6 +2,8 @@ from datetime import timedelta +from ctek import NanogridAir + from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -24,8 +26,6 @@ DataUpdateCoordinator, ) -from .api import fetch_meter_data - SENSOR = { "current_0": SensorEntityDescription( key="current_0", @@ -106,7 +106,7 @@ async def async_setup_entry( """Set up sensor entities for the integration entry.""" async def update_data(): - return await fetch_meter_data() + return await NanogridAir().fetch_meter_data() coordinator = DataUpdateCoordinator( hass, diff --git a/requirements_all.txt b/requirements_all.txt index 5fea8eca58c69..94bef98aac588 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -678,6 +678,9 @@ crownstone-sse==2.0.4 # homeassistant.components.crownstone crownstone-uart==2.1.0 +# homeassistant.components.nanogrid_air +ctek==0.1.0 + # homeassistant.components.datadog datadog==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05a922c872f34..62348875ab349 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -562,6 +562,9 @@ crownstone-sse==2.0.4 # homeassistant.components.crownstone crownstone-uart==2.1.0 +# homeassistant.components.nanogrid_air +ctek==0.1.0 + # homeassistant.components.datadog datadog==0.15.0 diff --git a/tests/components/nanogrid_air/test_config_flow.py b/tests/components/nanogrid_air/test_config_flow.py index 30f60eb0b9ec8..f6e2d5e64d830 100644 --- a/tests/components/nanogrid_air/test_config_flow.py +++ b/tests/components/nanogrid_air/test_config_flow.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +from ctek import NanogridAir import pytest from homeassistant import config_entries @@ -26,7 +27,7 @@ "00:11:22:33:44:55", FlowResultType.CREATE_ENTRY, "Nanogrid Air", - {CONF_URL: "http://ctek-ng-air.local/meter/"}, + {CONF_URL: NanogridAir().get_ip}, None, ), (False, None, FlowResultType.FORM, None, None, {"base": "cannot_connect"}), From 184c72732d9dabd0b2c8473257334718033b3caf Mon Sep 17 00:00:00 2001 From: Rebecca Borgqvist Date: Wed, 12 Jun 2024 09:28:02 +0000 Subject: [PATCH 2/4] requirements added --- .../components/nanogrid_air/config_flow.py | 11 ++- .../components/nanogrid_air/sensor.py | 14 +-- .../nanogrid_air/test_config_flow.py | 98 ++++++++----------- 3 files changed, 53 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/nanogrid_air/config_flow.py b/homeassistant/components/nanogrid_air/config_flow.py index c875fb0648df0..4782fc732586c 100644 --- a/homeassistant/components/nanogrid_air/config_flow.py +++ b/homeassistant/components/nanogrid_air/config_flow.py @@ -10,6 +10,7 @@ from .const import DOMAIN +API_DEFAULT = "http://ctek-ng-air.local/meter/" TITLE = "Nanogrid Air" USER_DESC = "description" @@ -21,7 +22,7 @@ class NanogridAirConfigFlow(ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize flow.""" - self._url = NanogridAir().get_ip + self._url = API_DEFAULT async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle a flow initiated by the user.""" @@ -37,7 +38,8 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult: if user_input is None: try: if await NanogridAir().get_ip(): - mac_address = await NanogridAir().fetch_mac() + status = await NanogridAir().fetch_status() + mac_address = status.device_info.mac if mac_address: unique_id = mac_address await self.async_set_unique_id(unique_id) @@ -60,8 +62,9 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult: # Handle user input try: - if await NanogridAir().get_ip(user_input[CONF_URL]): - mac_address = await NanogridAir().fetch_mac() + if await NanogridAir(device_ip=user_input[CONF_URL]).get_ip(): + status = await NanogridAir().fetch_status() + mac_address = status.device_info.mac if mac_address: unique_id = mac_address await self.async_set_unique_id(unique_id) diff --git a/homeassistant/components/nanogrid_air/sensor.py b/homeassistant/components/nanogrid_air/sensor.py index ab1a343e7b0c3..2b25210cc2d59 100644 --- a/homeassistant/components/nanogrid_air/sensor.py +++ b/homeassistant/components/nanogrid_air/sensor.py @@ -149,23 +149,23 @@ def __init__( def native_value(self) -> str | None: """Return the state of the sensor.""" data = self.coordinator.data - if data is None: + if data is None or isinstance(data, dict): return None if self._sensor_id.startswith("current_"): index = int(self._sensor_id.split("_")[-1]) - return data.get("current", [None, None, None])[index] + return getattr(data, "current", [None])[index] if self._sensor_id.startswith("voltage_"): index = int(self._sensor_id.split("_")[-1]) - return data.get("voltage", [None, None, None])[index] + return getattr(data, "voltage", [None])[index] if self._sensor_id == "power_in": - return data.get("activePowerIn", None) + return getattr(data, "active_power_in", None) if self._sensor_id == "power_out": - return data.get("activePowerOut", None) + return getattr(data, "active_power_out", None) if self._sensor_id == "total_energy_import": - return data.get("totalEnergyActiveImport", None) + return getattr(data, "total_energy_active_import", None) if self._sensor_id == "total_energy_export": - return data.get("totalEnergyActiveExport", None) + return getattr(data, "total_energy_active_export", None) return None @property diff --git a/tests/components/nanogrid_air/test_config_flow.py b/tests/components/nanogrid_air/test_config_flow.py index f6e2d5e64d830..377642d8aa3c7 100644 --- a/tests/components/nanogrid_air/test_config_flow.py +++ b/tests/components/nanogrid_air/test_config_flow.py @@ -2,7 +2,6 @@ from unittest.mock import AsyncMock, patch -from ctek import NanogridAir import pytest from homeassistant import config_entries @@ -12,10 +11,20 @@ from homeassistant.data_entry_flow import FlowResultType +class MockStatus: # noqa: D101 + def __init__(self, mac=None) -> None: # noqa: D107 + self.device_info = MockDeviceInfo(mac) + + +class MockDeviceInfo: # noqa: D101 + def __init__(self, mac) -> None: # noqa: D107 + self.mac = mac + + @pytest.mark.parametrize( ( "get_ip_return", - "fetch_mac_return", + "fetch_status_return", "result_type", "title", "data", @@ -24,17 +33,17 @@ [ ( True, - "00:11:22:33:44:55", + MockStatus(mac="00:11:22:33:44:55"), FlowResultType.CREATE_ENTRY, "Nanogrid Air", - {CONF_URL: NanogridAir().get_ip}, + {CONF_URL: "http://ctek-ng-air.local/meter/"}, None, ), (False, None, FlowResultType.FORM, None, None, {"base": "cannot_connect"}), - (True, None, FlowResultType.FORM, None, None, {"base": "invalid_auth"}), + (True, MockStatus(), FlowResultType.FORM, None, None, {"base": "invalid_auth"}), ( False, - "00:11:22:33:44:55", + MockStatus(mac="00:11:22:33:44:55"), FlowResultType.FORM, None, None, @@ -46,7 +55,7 @@ async def test_form_auto_detect( hass: HomeAssistant, mock_setup_entry: AsyncMock, get_ip_return, - fetch_mac_return, + fetch_status_return, result_type, title, data, @@ -54,14 +63,8 @@ async def test_form_auto_detect( ) -> None: """Test auto-detect scenarios with different return values.""" with ( - patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - return_value=get_ip_return, - ), - patch( - "homeassistant.components.nanogrid_air.config_flow.fetch_mac", - return_value=fetch_mac_return, - ), + patch("ctek.NanogridAir.get_ip", return_value=get_ip_return), + patch("ctek.NanogridAir.fetch_status", return_value=fetch_status_return), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -83,13 +86,10 @@ async def test_form_manual_entry_success( """Test manual entry success.""" user_input = {CONF_URL: "http://user-provided-url.local/meter/"} with ( + patch("ctek.NanogridAir.get_ip", return_value=True), patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - return_value=True, - ), - patch( - "homeassistant.components.nanogrid_air.config_flow.fetch_mac", - return_value="00:11:22:33:44:55", + "ctek.NanogridAir.fetch_status", + return_value=MockStatus(mac="00:11:22:33:44:55"), ), ): result = await hass.config_entries.flow.async_init( @@ -102,6 +102,7 @@ async def test_form_manual_entry_success( assert result["data"][CONF_URL] == "http://user-provided-url.local/meter/" assert len(mock_setup_entry.mock_calls) == 1 + # Additional checks for user inputs assert CONF_URL in result["data"] @@ -109,9 +110,7 @@ async def test_form_not_responding( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test device not responding.""" - with patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", return_value=False - ): + with patch("ctek.NanogridAir.get_ip", return_value=False): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -126,14 +125,8 @@ async def test_form_invalid_mac( ) -> None: """Test invalid MAC address handling.""" with ( - patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - return_value=True, - ), - patch( - "homeassistant.components.nanogrid_air.config_flow.fetch_mac", - return_value=None, - ), + patch("ctek.NanogridAir.get_ip", return_value=True), + patch("ctek.NanogridAir.fetch_status", return_value=MockStatus()), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -145,26 +138,20 @@ async def test_form_invalid_mac( @pytest.mark.parametrize( - ("get_ip_return", "fetch_mac_return", "expected_errors"), + ("get_ip_return", "fetch_status_return", "expected_errors"), [ (False, None, {"base": "cannot_connect"}), - (True, None, {"base": "invalid_auth"}), + (True, MockStatus(), {"base": "invalid_auth"}), ], ) async def test_form_error_handling( - hass: HomeAssistant, get_ip_return, fetch_mac_return, expected_errors + hass: HomeAssistant, get_ip_return, fetch_status_return, expected_errors ) -> None: """Test handling various error scenarios.""" user_input = {CONF_URL: "http://user-provided-url.local/meter/"} with ( - patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - return_value=get_ip_return, - ), - patch( - "homeassistant.components.nanogrid_air.config_flow.fetch_mac", - return_value=fetch_mac_return, - ), + patch("ctek.NanogridAir.get_ip", return_value=get_ip_return), + patch("ctek.NanogridAir.fetch_status", return_value=fetch_status_return), ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=user_input @@ -179,10 +166,7 @@ async def test_form_network_failure( hass: HomeAssistant, mock_setup_entry: AsyncMock ) -> None: """Test handling network failures.""" - with patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - side_effect=ConnectionError, - ): + with patch("ctek.NanogridAir.get_ip", side_effect=ConnectionError): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -199,13 +183,10 @@ async def test_form_user_input_exception( user_input = {CONF_URL: "http://user-provided-url.local/meter/"} with ( + patch("ctek.NanogridAir.get_ip", side_effect=Exception("Test exception")), patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - side_effect=Exception("Test exception"), - ), - patch( - "homeassistant.components.nanogrid_air.config_flow.fetch_mac", - return_value="00:11:22:33:44:55", + "ctek.NanogridAir.fetch_status", + return_value=MockStatus(mac="00:11:22:33:44:55"), ), ): result = await hass.config_entries.flow.async_init( @@ -213,6 +194,7 @@ async def test_form_user_input_exception( ) await hass.async_block_till_done() + # Check that the flow was aborted due to the exception assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_responding" @@ -224,13 +206,10 @@ async def test_form_user_input_connection_error( user_input = {CONF_URL: "http://user-provided-url.local/meter/"} with ( + patch("ctek.NanogridAir.get_ip", side_effect=ConnectionError), patch( - "homeassistant.components.nanogrid_air.config_flow.get_ip", - side_effect=ConnectionError, - ), - patch( - "homeassistant.components.nanogrid_air.config_flow.fetch_mac", - return_value="00:11:22:33:44:55", + "ctek.NanogridAir.fetch_status", + return_value=MockStatus(mac="00:11:22:33:44:55"), ), ): result = await hass.config_entries.flow.async_init( @@ -238,5 +217,6 @@ async def test_form_user_input_connection_error( ) await hass.async_block_till_done() + # Check that the flow was aborted due to the connection error assert result["type"] == FlowResultType.ABORT assert result["reason"] == "not_responding" From de8afba0fc98b110c9f82cd70466b33930d60ba0 Mon Sep 17 00:00:00 2001 From: Rebecca Borgqvist Date: Wed, 12 Jun 2024 13:10:26 +0000 Subject: [PATCH 3/4] ctek pypi update --- homeassistant/components/nanogrid_air/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nanogrid_air/manifest.json b/homeassistant/components/nanogrid_air/manifest.json index adfcf3a015a65..2ffdac0a7b45d 100644 --- a/homeassistant/components/nanogrid_air/manifest.json +++ b/homeassistant/components/nanogrid_air/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nanogrid_air", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["ctek==0.1.0"] + "requirements": ["ctek==0.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 94bef98aac588..1a008a60d174e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -679,7 +679,7 @@ crownstone-sse==2.0.4 crownstone-uart==2.1.0 # homeassistant.components.nanogrid_air -ctek==0.1.0 +ctek==0.2.0 # homeassistant.components.datadog datadog==0.15.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 62348875ab349..f39da504f8eee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -563,7 +563,7 @@ crownstone-sse==2.0.4 crownstone-uart==2.1.0 # homeassistant.components.nanogrid_air -ctek==0.1.0 +ctek==0.2.0 # homeassistant.components.datadog datadog==0.15.0 From aa25c7f885645211345e3efbda5a1939c1bd4b9a Mon Sep 17 00:00:00 2001 From: Rebecca Borgqvist Date: Thu, 13 Jun 2024 11:40:12 +0000 Subject: [PATCH 4/4] Removed unnecessary API url --- homeassistant/components/nanogrid_air/config_flow.py | 9 ++------- tests/components/nanogrid_air/test_config_flow.py | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/nanogrid_air/config_flow.py b/homeassistant/components/nanogrid_air/config_flow.py index 4782fc732586c..234a3c051f093 100644 --- a/homeassistant/components/nanogrid_air/config_flow.py +++ b/homeassistant/components/nanogrid_air/config_flow.py @@ -10,7 +10,6 @@ from .const import DOMAIN -API_DEFAULT = "http://ctek-ng-air.local/meter/" TITLE = "Nanogrid Air" USER_DESC = "description" @@ -20,10 +19,6 @@ class NanogridAirConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - def __init__(self) -> None: - """Initialize flow.""" - self._url = API_DEFAULT - async def async_step_user(self, user_input=None) -> ConfigFlowResult: """Handle a flow initiated by the user.""" self._async_abort_entries_match() @@ -31,7 +26,7 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult: errors = {} data_schema = vol.Schema( - {vol.Required(CONF_URL, default=self._url, description=USER_DESC): str} + {vol.Required(CONF_URL, default="", description=USER_DESC): str} ) # Attempt to automatically detect the device @@ -46,7 +41,7 @@ async def async_step_user(self, user_input=None) -> ConfigFlowResult: self._abort_if_unique_id_configured() return self.async_create_entry( title=TITLE, - data={CONF_URL: self._url}, + data={CONF_URL: ""}, ) errors["base"] = "invalid_auth" else: diff --git a/tests/components/nanogrid_air/test_config_flow.py b/tests/components/nanogrid_air/test_config_flow.py index 377642d8aa3c7..acefbf648eea7 100644 --- a/tests/components/nanogrid_air/test_config_flow.py +++ b/tests/components/nanogrid_air/test_config_flow.py @@ -36,7 +36,7 @@ def __init__(self, mac) -> None: # noqa: D107 MockStatus(mac="00:11:22:33:44:55"), FlowResultType.CREATE_ENTRY, "Nanogrid Air", - {CONF_URL: "http://ctek-ng-air.local/meter/"}, + {CONF_URL: ""}, None, ), (False, None, FlowResultType.FORM, None, None, {"base": "cannot_connect"}),