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 /tests/components/snmp/ @nmaggioni
/homeassistant/components/snooz/ @AustinBrunkhorst /homeassistant/components/snooz/ @AustinBrunkhorst
/tests/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst
/homeassistant/components/solaredge/ @frenck /homeassistant/components/solaredge/ @frenck @bdraco
/tests/components/solaredge/ @frenck /tests/components/solaredge/ @frenck @bdraco
/homeassistant/components/solaredge_local/ @drobtravels @scheric /homeassistant/components/solaredge_local/ @drobtravels @scheric
/homeassistant/components/solarlog/ @Ernst79 /homeassistant/components/solarlog/ @Ernst79
/tests/components/solarlog/ @Ernst79 /tests/components/solarlog/ @Ernst79

View File

@ -4,13 +4,14 @@ from __future__ import annotations
import socket import socket
from requests.exceptions import ConnectTimeout, HTTPError from aiohttp import ClientError
from solaredge import Solaredge from aiosolaredge import SolarEdge
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from .const import CONF_SITE_ID, DATA_API_CLIENT, DOMAIN, LOGGER 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: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up SolarEdge from a config entry.""" """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: try:
response = await hass.async_add_executor_job( response = await api.get_details(entry.data[CONF_SITE_ID])
api.get_details, entry.data[CONF_SITE_ID] except (TimeoutError, ClientError, socket.gaierror) as ex:
)
except (ConnectTimeout, HTTPError, socket.gaierror) as ex:
LOGGER.error("Could not retrieve details from SolarEdge API") LOGGER.error("Could not retrieve details from SolarEdge API")
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex

View File

@ -2,15 +2,17 @@
from __future__ import annotations from __future__ import annotations
import socket
from typing import Any from typing import Any
from requests.exceptions import ConnectTimeout, HTTPError from aiohttp import ClientError
import solaredge import aiosolaredge
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import slugify from homeassistant.util import slugify
from .const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN 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 True if site_id exists in configuration."""
return site_id in self._async_current_site_ids() 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.""" """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: try:
response = api.get_details(site_id) response = await api.get_details(site_id)
if response["details"]["status"].lower() != "active": if response["details"]["status"].lower() != "active":
self._errors[CONF_SITE_ID] = "site_not_active" self._errors[CONF_SITE_ID] = "site_not_active"
return False return False
except (ConnectTimeout, HTTPError): except (TimeoutError, ClientError, socket.gaierror):
self._errors[CONF_SITE_ID] = "could_not_connect" self._errors[CONF_SITE_ID] = "could_not_connect"
return False return False
except KeyError: except KeyError:
@ -66,9 +69,7 @@ class SolarEdgeConfigFlow(ConfigFlow, domain=DOMAIN):
else: else:
site = user_input[CONF_SITE_ID] site = user_input[CONF_SITE_ID]
api = user_input[CONF_API_KEY] api = user_input[CONF_API_KEY]
can_connect = await self.hass.async_add_executor_job( can_connect = await self._async_check_site(site, api)
self._check_site, site, api
)
if can_connect: if can_connect:
return self.async_create_entry( return self.async_create_entry(
title=name, data={CONF_SITE_ID: site, CONF_API_KEY: api} 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 datetime import date, datetime, timedelta
from typing import Any from typing import Any
from solaredge import Solaredge from aiosolaredge import SolarEdge
from stringcase import snakecase from stringcase import snakecase
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -27,7 +27,7 @@ class SolarEdgeDataService(ABC):
coordinator: DataUpdateCoordinator[None] 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.""" """Initialize the data object."""
self.api = api self.api = api
self.site_id = site_id self.site_id = site_id
@ -54,12 +54,8 @@ class SolarEdgeDataService(ABC):
"""Update interval.""" """Update interval."""
@abstractmethod @abstractmethod
def update(self) -> None:
"""Update data in executor."""
async def async_update_data(self) -> None: async def async_update_data(self) -> None:
"""Update data.""" """Update data."""
await self.hass.async_add_executor_job(self.update)
class SolarEdgeOverviewDataService(SolarEdgeDataService): class SolarEdgeOverviewDataService(SolarEdgeDataService):
@ -70,10 +66,10 @@ class SolarEdgeOverviewDataService(SolarEdgeDataService):
"""Update interval.""" """Update interval."""
return OVERVIEW_UPDATE_DELAY return OVERVIEW_UPDATE_DELAY
def update(self) -> None: async def async_update_data(self) -> None:
"""Update the data from the SolarEdge Monitoring API.""" """Update the data from the SolarEdge Monitoring API."""
try: try:
data = self.api.get_overview(self.site_id) data = await self.api.get_overview(self.site_id)
overview = data["overview"] overview = data["overview"]
except KeyError as ex: except KeyError as ex:
raise UpdateFailed("Missing overview data, skipping update") from ex raise UpdateFailed("Missing overview data, skipping update") from ex
@ -113,11 +109,11 @@ class SolarEdgeDetailsDataService(SolarEdgeDataService):
"""Update interval.""" """Update interval."""
return DETAILS_UPDATE_DELAY return DETAILS_UPDATE_DELAY
def update(self) -> None: async def async_update_data(self) -> None:
"""Update the data from the SolarEdge Monitoring API.""" """Update the data from the SolarEdge Monitoring API."""
try: try:
data = self.api.get_details(self.site_id) data = await self.api.get_details(self.site_id)
details = data["details"] details = data["details"]
except KeyError as ex: except KeyError as ex:
raise UpdateFailed("Missing details data, skipping update") from ex raise UpdateFailed("Missing details data, skipping update") from ex
@ -157,10 +153,10 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService):
"""Update interval.""" """Update interval."""
return INVENTORY_UPDATE_DELAY return INVENTORY_UPDATE_DELAY
def update(self) -> None: async def async_update_data(self) -> None:
"""Update the data from the SolarEdge Monitoring API.""" """Update the data from the SolarEdge Monitoring API."""
try: try:
data = self.api.get_inventory(self.site_id) data = await self.api.get_inventory(self.site_id)
inventory = data["Inventory"] inventory = data["Inventory"]
except KeyError as ex: except KeyError as ex:
raise UpdateFailed("Missing inventory data, skipping update") from ex raise UpdateFailed("Missing inventory data, skipping update") from ex
@ -178,7 +174,7 @@ class SolarEdgeInventoryDataService(SolarEdgeDataService):
class SolarEdgeEnergyDetailsService(SolarEdgeDataService): class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
"""Get and update the latest power flow data.""" """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.""" """Initialize the power flow data service."""
super().__init__(hass, api, site_id) super().__init__(hass, api, site_id)
@ -189,17 +185,16 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
"""Update interval.""" """Update interval."""
return ENERGY_DETAILS_DELAY return ENERGY_DETAILS_DELAY
def update(self) -> None: async def async_update_data(self) -> None:
"""Update the data from the SolarEdge Monitoring API.""" """Update the data from the SolarEdge Monitoring API."""
try: try:
now = datetime.now() now = datetime.now()
today = date.today() today = date.today()
midnight = datetime.combine(today, datetime.min.time()) midnight = datetime.combine(today, datetime.min.time())
data = self.api.get_energy_details( data = await self.api.get_energy_details(
self.site_id, self.site_id,
midnight, midnight,
now.strftime("%Y-%m-%d %H:%M:%S"), now,
meters=None,
time_unit="DAY", time_unit="DAY",
) )
energy_details = data["energyDetails"] energy_details = data["energyDetails"]
@ -239,7 +234,7 @@ class SolarEdgeEnergyDetailsService(SolarEdgeDataService):
class SolarEdgePowerFlowDataService(SolarEdgeDataService): class SolarEdgePowerFlowDataService(SolarEdgeDataService):
"""Get and update the latest power flow data.""" """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.""" """Initialize the power flow data service."""
super().__init__(hass, api, site_id) super().__init__(hass, api, site_id)
@ -250,10 +245,10 @@ class SolarEdgePowerFlowDataService(SolarEdgeDataService):
"""Update interval.""" """Update interval."""
return POWER_FLOW_UPDATE_DELAY return POWER_FLOW_UPDATE_DELAY
def update(self) -> None: async def async_update_data(self) -> None:
"""Update the data from the SolarEdge Monitoring API.""" """Update the data from the SolarEdge Monitoring API."""
try: 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"] power_flow = data["siteCurrentPowerFlow"]
except KeyError as ex: except KeyError as ex:
raise UpdateFailed("Missing power flow data, skipping update") from ex raise UpdateFailed("Missing power flow data, skipping update") from ex

View File

@ -1,7 +1,7 @@
{ {
"domain": "solaredge", "domain": "solaredge",
"name": "SolarEdge", "name": "SolarEdge",
"codeowners": ["@frenck"], "codeowners": ["@frenck", "@bdraco"],
"config_flow": true, "config_flow": true,
"dhcp": [ "dhcp": [
{ {
@ -12,6 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/solaredge", "documentation": "https://www.home-assistant.io/integrations/solaredge",
"integration_type": "device", "integration_type": "device",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["solaredge"], "loggers": ["aiosolaredge"],
"requirements": ["solaredge==0.0.2", "stringcase==1.2.0"] "requirements": ["aiosolaredge==0.2.0", "stringcase==1.2.0"]
} }

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from solaredge import Solaredge from aiosolaredge import SolarEdge
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,
@ -205,7 +205,7 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Add an solarEdge entry.""" """Add an solarEdge entry."""
# Add the needed sensors to hass # 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) sensor_factory = SolarEdgeSensorFactory(hass, entry.data[CONF_SITE_ID], api)
for service in sensor_factory.all_services: for service in sensor_factory.all_services:
@ -223,7 +223,7 @@ async def async_setup_entry(
class SolarEdgeSensorFactory: class SolarEdgeSensorFactory:
"""Factory which creates sensors based on the sensor_key.""" """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.""" """Initialize the factory."""
details = SolarEdgeDetailsDataService(hass, api, site_id) details = SolarEdgeDetailsDataService(hass, api, site_id)

View File

@ -367,6 +367,9 @@ aioskybell==22.7.0
# homeassistant.components.slimproto # homeassistant.components.slimproto
aioslimproto==3.0.0 aioslimproto==3.0.0
# homeassistant.components.solaredge
aiosolaredge==0.2.0
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.2 aiosteamist==0.3.2
@ -2574,9 +2577,6 @@ soco==0.30.3
# homeassistant.components.solaredge_local # homeassistant.components.solaredge_local
solaredge-local==0.2.3 solaredge-local==0.2.3
# homeassistant.components.solaredge
solaredge==0.0.2
# homeassistant.components.solax # homeassistant.components.solax
solax==3.1.0 solax==3.1.0

View File

@ -340,6 +340,9 @@ aioskybell==22.7.0
# homeassistant.components.slimproto # homeassistant.components.slimproto
aioslimproto==3.0.0 aioslimproto==3.0.0
# homeassistant.components.solaredge
aiosolaredge==0.2.0
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.2 aiosteamist==0.3.2
@ -1990,9 +1993,6 @@ snapcast==2.3.6
# homeassistant.components.sonos # homeassistant.components.sonos
soco==0.30.3 soco==0.30.3
# homeassistant.components.solaredge
solaredge==0.0.2
# homeassistant.components.solax # homeassistant.components.solax
solax==3.1.0 solax==3.1.0

View File

@ -1,9 +1,9 @@
"""Tests for the SolarEdge config flow.""" """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 import pytest
from requests.exceptions import ConnectTimeout, HTTPError
from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN
from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER
@ -22,8 +22,11 @@ API_KEY = "a1b2c3d4e5f6g7h8"
def mock_controller(): def mock_controller():
"""Mock a successful Solaredge API.""" """Mock a successful Solaredge API."""
api = Mock() api = Mock()
api.get_details.return_value = {"details": {"status": "active"}} api.get_details = AsyncMock(return_value={"details": {"status": "active"}})
with patch("solaredge.Solaredge", return_value=api): with patch(
"homeassistant.components.solaredge.config_flow.aiosolaredge.SolarEdge",
return_value=api,
):
yield 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"} assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"}
# test with ConnectionTimeout # 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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_USER}, 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"} assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"}
# test with HTTPError # 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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},

View File

@ -1,6 +1,6 @@
"""Tests for the SolarEdge coordinator services.""" """Tests for the SolarEdge coordinator services."""
from unittest.mock import patch from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -25,7 +25,7 @@ def enable_all_entities(entity_registry_enabled_by_default):
"""Make sure all entities are enabled.""" """Make sure all entities are enabled."""
@patch("homeassistant.components.solaredge.Solaredge") @patch("homeassistant.components.solaredge.SolarEdge")
async def test_solaredgeoverviewdataservice_energy_values_validity( async def test_solaredgeoverviewdataservice_energy_values_validity(
mock_solaredge, hass: HomeAssistant, freezer: FrozenDateTimeFactory mock_solaredge, hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None: ) -> None:
@ -35,7 +35,9 @@ async def test_solaredgeoverviewdataservice_energy_values_validity(
title=DEFAULT_NAME, title=DEFAULT_NAME,
data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, 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) mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id) 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}, "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) freezer.tick(OVERVIEW_UPDATE_DELAY)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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. # Invalid energy values, lifeTimeData energy is lower than last year, month or day.
mock_overview_data["overview"]["lifeTimeData"]["energy"] = 0 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) freezer.tick(OVERVIEW_UPDATE_DELAY)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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 # New valid energy values update
mock_overview_data["overview"]["lifeTimeData"]["energy"] = 100001 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) freezer.tick(OVERVIEW_UPDATE_DELAY)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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. # Invalid energy values, lastYearData energy is lower than last month or day.
mock_overview_data["overview"]["lastYearData"]["energy"] = 0 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) freezer.tick(OVERVIEW_UPDATE_DELAY)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) 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"]["lastYearData"]["energy"] = 0.0
mock_overview_data["overview"]["lastMonthData"]["energy"] = 0.0 mock_overview_data["overview"]["lastMonthData"]["energy"] = 0.0
mock_overview_data["overview"]["lastDayData"]["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) freezer.tick(OVERVIEW_UPDATE_DELAY)
async_fire_time_changed(hass) async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True) await hass.async_block_till_done(wait_background_tasks=True)