Convert solaredge to asyncio with aiosolaredge (#115599)

This commit is contained in:
J. Nick Koston 2024-04-23 22:07:16 +02:00 committed by GitHub
parent d08bb96d00
commit fd08b7281e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 65 additions and 64 deletions

View File

@ -1286,8 +1286,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

View File

@ -4,13 +4,14 @@ from __future__ import annotations
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 @@ PLATFORMS = [Platform.SENSOR]
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

View File

@ -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 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN):
"""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 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN):
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}

View File

@ -6,7 +6,7 @@ from abc import ABC, abstractmethod
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 @@ class SolarEdgeDataService(ABC):
"""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 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService):
"""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 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService):
"""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 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService):
"""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 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService):
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 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
"""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 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
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 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService):
"""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

View File

@ -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"]
}

View File

@ -5,7 +5,7 @@ from __future__ import annotations
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)

View File

@ -367,6 +367,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
@ -2574,9 +2577,6 @@ soco==0.30.3
# homeassistant.components.solaredge_local
solaredge-local==0.2.3
# homeassistant.components.solaredge
solaredge==0.0.2
# homeassistant.components.solax
solax==3.1.0

View File

@ -340,6 +340,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
@ -1990,9 +1993,6 @@ snapcast==2.3.6
# homeassistant.components.sonos
soco==0.30.3
# homeassistant.components.solaredge
solaredge==0.0.2
# homeassistant.components.solax
solax==3.1.0

View File

@ -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 @@ API_KEY = "a1b2c3d4e5f6g7h8"
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},

View File

@ -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)