From a86176b49f0793ab52361196c3e3816e085a70ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Apr 2024 15:46:17 -0500 Subject: [PATCH 1/3] Convert solaredge to asyncio with aiosolaredge https://github.com/bdraco/aiosolaredge fixes #115588 --- CODEOWNERS | 4 +-- .../components/solaredge/__init__.py | 14 ++++---- .../components/solaredge/config_flow.py | 19 +++++----- .../components/solaredge/coordinator.py | 35 ++++++++----------- .../components/solaredge/manifest.json | 6 ++-- requirements_all.txt | 6 ++-- requirements_test_all.txt | 6 ++-- 7 files changed, 43 insertions(+), 47 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6e7b7e6f8f417d..aa7bd3fa7897fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1275,8 +1275,8 @@ build.json @home-assistant/supervisor /tests/components/snmp/ @nmaggioni /homeassistant/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst -/homeassistant/components/solaredge/ @frenck -/tests/components/solaredge/ @frenck +/homeassistant/components/solaredge/ @frenck @bdraco +/tests/components/solaredge/ @frenck @bdraco /homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solarlog/ @Ernst79 /tests/components/solarlog/ @Ernst79 diff --git a/homeassistant/components/solaredge/__init__.py b/homeassistant/components/solaredge/__init__.py index 69e02c1875c93d..64f76372e91d86 100644 --- a/homeassistant/components/solaredge/__init__.py +++ b/homeassistant/components/solaredge/__init__.py @@ -4,13 +4,14 @@ import socket -from requests.exceptions import ConnectTimeout, HTTPError -from solaredge import Solaredge +from aiohttp import ClientError +from aiosolaredge import SolarEdge from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY, Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER @@ -22,13 +23,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up SolarEdge from a config entry.""" - api = Solaredge(entry.data[CONF_API_KEY]) + session = async_get_clientsession(hass) + api = SolarEdge(entry.data[CONF_API_KEY], session) try: - response = await hass.async_add_executor_job( - api.get_details, entry.data[CONF_SITE_ID] - ) - except (ConnectTimeout, HTTPError, socket.gaierror) as ex: + response = await api.get_details(entry.data[CONF_SITE_ID]) + except (TimeoutError, ClientError, socket.gaierror) as ex: LOGGER.error("Could not retrieve details from SolarEdge API") raise ConfigEntryNotReady from ex diff --git a/homeassistant/components/solaredge/config_flow.py b/homeassistant/components/solaredge/config_flow.py index b75af866549fb7..6235e22400ff13 100644 --- a/homeassistant/components/solaredge/config_flow.py +++ b/homeassistant/components/solaredge/config_flow.py @@ -2,15 +2,17 @@ from __future__ import annotations +import socket from typing import Any -from requests.exceptions import ConnectTimeout, HTTPError -import solaredge +from aiohttp import ClientError +import aiosolaredge import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util import slugify from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN @@ -38,15 +40,16 @@ def _site_in_configuration_exists(self, site_id: str) -> bool: """Return True if site_id exists in configuration.""" return site_id in self._async_current_site_ids() - def _check_site(self, site_id: str, api_key: str) -> bool: + async def _async_check_site(self, site_id: str, api_key: str) -> bool: """Check if we can connect to the soleredge api service.""" - api = solaredge.Solaredge(api_key) + session = async_get_clientsession(self.hass) + api = aiosolaredge.SolarEdge(api_key, session) try: - response = api.get_details(site_id) + response = await api.get_details(site_id) if response["details"]["status"].lower() != "active": self._errors[CONF_SITE_ID] = "site_not_active" return False - except (ConnectTimeout, HTTPError): + except (TimeoutError, ClientError, socket.gaierror): self._errors[CONF_SITE_ID] = "could_not_connect" return False except KeyError: @@ -66,9 +69,7 @@ async def async_step_user( else: site = user_input[CONF_SITE_ID] api = user_input[CONF_API_KEY] - can_connect = await self.hass.async_add_executor_job( - self._check_site, site, api - ) + can_connect = await self._async_check_site(site, api) if can_connect: return self.async_create_entry( title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api} diff --git a/homeassistant/components/solaredge/coordinator.py b/homeassistant/components/solaredge/coordinator.py index d2da99820d7870..0c264c1c514562 100644 --- a/homeassistant/components/solaredge/coordinator.py +++ b/homeassistant/components/solaredge/coordinator.py @@ -6,7 +6,7 @@ from datetime import date, datetime, timedelta from typing import Any -from solaredge import Solaredge +from aiosolaredge import SolarEdge from stringcase import snakecase from homeassistant.core import HomeAssistant, callback @@ -27,7 +27,7 @@ class SolarEdgeDataService(ABC): coordinator: DataUpdateCoordinator[None] - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the data object.""" self.api = api self.site_id = site_id @@ -54,12 +54,8 @@ def update_interval(self) -> timedelta: """Update interval.""" @abstractmethod - def update(self) -> None: - """Update data in executor.""" - async def async_update_data(self) -> None: """Update data.""" - await self.hass.async_add_executor_job(self.update) class SolarEdgeOverviewDataService(SolarEdgeDataService): @@ -70,10 +66,10 @@ def update_interval(self) -> timedelta: """Update interval.""" return OVERVIEW_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_overview(self.site_id) + data = await self.api.get_overview(self.site_id) overview = data["overview"] except KeyError as ex: raise UpdateFailed("Missing overview data, skipping update") from ex @@ -113,11 +109,11 @@ def update_interval(self) -> timedelta: """Update interval.""" return DETAILS_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_details(self.site_id) + data = await self.api.get_details(self.site_id) details = data["details"] except KeyError as ex: raise UpdateFailed("Missing details data, skipping update") from ex @@ -157,10 +153,10 @@ def update_interval(self) -> timedelta: """Update interval.""" return INVENTORY_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_inventory(self.site_id) + data = await self.api.get_inventory(self.site_id) inventory = data["Inventory"] except KeyError as ex: raise UpdateFailed("Missing inventory data, skipping update") from ex @@ -178,7 +174,7 @@ def update(self) -> None: class SolarEdgeEnergyDetailsService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) @@ -189,17 +185,16 @@ def update_interval(self) -> timedelta: """Update interval.""" return ENERGY_DETAILS_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: now = datetime.now() today = date.today() midnight = datetime.combine(today, datetime.min.time()) - data = self.api.get_energy_details( + data = await self.api.get_energy_details( self.site_id, midnight, - now.strftime("%Y-%m-%d %H:%M:%S"), - meters=None, + now, time_unit="DAY", ) energy_details = data["energyDetails"] @@ -239,7 +234,7 @@ def update(self) -> None: class SolarEdgePowerFlowDataService(SolarEdgeDataService): """Get and update the latest power flow data.""" - def __init__(self, hass: HomeAssistant, api: Solaredge, site_id: str) -> None: + def __init__(self, hass: HomeAssistant, api: SolarEdge, site_id: str) -> None: """Initialize the power flow data service.""" super().__init__(hass, api, site_id) @@ -250,10 +245,10 @@ def update_interval(self) -> timedelta: """Update interval.""" return POWER_FLOW_UPDATE_DELAY - def update(self) -> None: + async def async_update_data(self) -> None: """Update the data from the SolarEdge Monitoring API.""" try: - data = self.api.get_current_power_flow(self.site_id) + data = await self.api.get_current_power_flow(self.site_id) power_flow = data["siteCurrentPowerFlow"] except KeyError as ex: raise UpdateFailed("Missing power flow data, skipping update") from ex diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 22759b1be7c3d9..02f96c0211f69c 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -1,7 +1,7 @@ { "domain": "solaredge", "name": "SolarEdge", - "codeowners": ["@frenck"], + "codeowners": ["@frenck", "@bdraco"], "config_flow": true, "dhcp": [ { @@ -12,6 +12,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge", "integration_type": "device", "iot_class": "cloud_polling", - "loggers": ["solaredge"], - "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"] + "loggers": ["aiosolaredge"], + "requirements": ["aiosolaredge==0.2.0", "stringcase==1.2.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 14f5749a02abc4..7ccbefa6f4e7fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -376,6 +376,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==3.0.0 +# homeassistant.components.solaredge +aiosolaredge==0.2.0 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -2577,9 +2580,6 @@ soco==0.30.2 # homeassistant.components.solaredge_local solaredge-local==0.2.3 -# homeassistant.components.solaredge -solaredge==0.0.2 - # homeassistant.components.solax solax==3.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a58259c096cdb4..b21749b5f52e7c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -349,6 +349,9 @@ aioskybell==22.7.0 # homeassistant.components.slimproto aioslimproto==3.0.0 +# homeassistant.components.solaredge +aiosolaredge==0.2.0 + # homeassistant.components.steamist aiosteamist==0.3.2 @@ -1984,9 +1987,6 @@ snapcast==2.3.6 # homeassistant.components.sonos soco==0.30.2 -# homeassistant.components.solaredge -solaredge==0.0.2 - # homeassistant.components.solax solax==3.1.0 From 678ceb89dac786de0b3d052786d52a8182459dd3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Apr 2024 16:15:22 -0500 Subject: [PATCH 2/3] update tests --- tests/components/solaredge/test_config_flow.py | 15 +++++++++------ tests/components/solaredge/test_coordinator.py | 18 ++++++++++-------- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 9ff605a871d6d4..759a4d6b421a6a 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,9 +1,9 @@ """Tests for the SolarEdge config flow.""" -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch +from aiohttp import ClientError import pytest -from requests.exceptions import ConnectTimeout, HTTPError from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER @@ -22,8 +22,11 @@ def mock_controller(): """Mock a successful Solaredge API.""" api = Mock() - api.get_details.return_value = {"details": {"status": "active"}} - with patch("solaredge.Solaredge", return_value=api): + api.get_details = AsyncMock(return_value={"details": {"status": "active"}}) + with patch( + "homeassistant.components.solaredge.config_flow.aiosolaredge.SolarEdge", + return_value=api, + ): yield api @@ -117,7 +120,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout - test_api.get_details.side_effect = ConnectTimeout() + test_api.get_details = AsyncMock(side_effect=TimeoutError()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, @@ -127,7 +130,7 @@ async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError - test_api.get_details.side_effect = HTTPError() + test_api.get_details = AsyncMock(side_effect=ClientError()) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index b1496d18d9309d..7a6b3af1cde0b5 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -1,6 +1,6 @@ """Tests for the SolarEdge coordinator services.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -25,7 +25,7 @@ def enable_all_entities(entity_registry_enabled_by_default): """Make sure all entities are enabled.""" -@patch("homeassistant.components.solaredge.Solaredge") +@patch("homeassistant.components.solaredge.SolarEdge") async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge, hass: HomeAssistant, freezer: FrozenDateTimeFactory ) -> None: @@ -35,7 +35,9 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( title=DEFAULT_NAME, data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ) - mock_solaredge().get_details.return_value = {"details": {"status": "active"}} + mock_solaredge().get_details = AsyncMock( + return_value={"details": {"status": "active"}} + ) mock_config_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -50,7 +52,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( "currentPower": {"power": 0.0}, } } - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -60,7 +62,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lifeTimeData energy is lower than last year, month or day. mock_overview_data["overview"]["lifeTimeData"]["energy"] = 0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -71,7 +73,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # New valid energy values update mock_overview_data["overview"]["lifeTimeData"]["energy"] = 100001 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -82,7 +84,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( # Invalid energy values, lastYearData energy is lower than last month or day. mock_overview_data["overview"]["lastYearData"]["energy"] = 0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) @@ -100,7 +102,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_overview_data["overview"]["lastYearData"]["energy"] = 0.0 mock_overview_data["overview"]["lastMonthData"]["energy"] = 0.0 mock_overview_data["overview"]["lastDayData"]["energy"] = 0.0 - mock_solaredge().get_overview.return_value = mock_overview_data + mock_solaredge().get_overview = AsyncMock(return_value=mock_overview_data) freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) await hass.async_block_till_done(wait_background_tasks=True) From dd4303deaeffaa1cc279901c5cc2151f330cbd80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 14 Apr 2024 16:30:38 -0500 Subject: [PATCH 3/3] fix import --- homeassistant/components/solaredge/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/solaredge/sensor.py b/homeassistant/components/solaredge/sensor.py index 5ec65a3b9a5e6b..b3345d5dc86720 100644 --- a/homeassistant/components/solaredge/sensor.py +++ b/homeassistant/components/solaredge/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import Any -from solaredge import Solaredge +from aiosolaredge import SolarEdge from homeassistant.components.sensor import ( SensorDeviceClass, @@ -205,7 +205,7 @@ async def async_setup_entry( ) -> None: """Add an solarEdge entry.""" # Add the needed sensors to hass - api: Solaredge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] + api: SolarEdge = hass.data[DOMAIN][entry.entry_id][DATA_API_CLIENT] sensor_factory = SolarEdgeSensorFactory(hass, entry.data[CONF_SITE_ID], api) for service in sensor_factory.all_services: @@ -223,7 +223,7 @@ async def async_setup_entry( class SolarEdgeSensorFactory: """Factory which creates sensors based on the sensor_key.""" - def __init__(self, hass: HomeAssistant, site_id: str, api: Solaredge) -> None: + def __init__(self, hass: HomeAssistant, site_id: str, api: SolarEdge) -> None: """Initialize the factory.""" details = SolarEdgeDetailsDataService(hass, api, site_id)