Skip to content

Commit fd4981f

Browse files
dontinellijoostlek
andauthored
Split up coordinators in solarlog (home-assistant#161169)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
1 parent ae1bedd commit fd4981f

12 files changed

Lines changed: 446 additions & 180 deletions

File tree

homeassistant/components/solarlog/__init__.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
"""Solar-Log integration."""
22

33
import logging
4+
from urllib.parse import ParseResult, urlparse
45

5-
from homeassistant.const import Platform
6+
from aiohttp import CookieJar
7+
from solarlog_cli.solarlog_connector import SolarLogConnector
8+
9+
from homeassistant.const import CONF_HOST, CONF_TIMEOUT, Platform
610
from homeassistant.core import HomeAssistant
711
from homeassistant.helpers import entity_registry as er
12+
from homeassistant.helpers.aiohttp_client import async_create_clientsession
813

914
from .const import CONF_HAS_PWD
10-
from .coordinator import SolarlogConfigEntry, SolarLogCoordinator
15+
from .coordinator import (
16+
SolarLogBasicDataCoordinator,
17+
SolarlogConfigEntry,
18+
SolarLogDeviceDataCoordinator,
19+
SolarLogLongtimeDataCoordinator,
20+
)
21+
from .models import SolarlogIntegrationData
1122

1223
_LOGGER = logging.getLogger(__name__)
1324

@@ -16,10 +27,57 @@
1627

1728
async def async_setup_entry(hass: HomeAssistant, entry: SolarlogConfigEntry) -> bool:
1829
"""Set up a config entry for solarlog."""
19-
coordinator = SolarLogCoordinator(hass, entry)
20-
await coordinator.async_config_entry_first_refresh()
21-
entry.runtime_data = coordinator
30+
31+
host_entry = entry.data[CONF_HOST]
32+
password = entry.data.get("password", "")
33+
34+
url = urlparse(host_entry, "http")
35+
netloc = url.netloc or url.path
36+
path = url.path if url.netloc else ""
37+
url = ParseResult("http", netloc, path, *url[3:])
38+
39+
solarlog = SolarLogConnector(
40+
url.geturl(),
41+
tz=hass.config.time_zone,
42+
password=password,
43+
session=async_create_clientsession(
44+
hass, cookie_jar=CookieJar(quote_cookie=False)
45+
),
46+
)
47+
48+
basic_coordinator = SolarLogBasicDataCoordinator(hass, entry, solarlog)
49+
50+
solarLogData = SolarlogIntegrationData(
51+
api=solarlog,
52+
basic_data_coordinator=basic_coordinator,
53+
)
54+
55+
await basic_coordinator.async_config_entry_first_refresh()
56+
57+
entry.runtime_data = solarLogData
58+
59+
if basic_coordinator.solarlog.extended_data:
60+
timeout = entry.data.get(CONF_TIMEOUT, 0)
61+
if timeout <= 150:
62+
# Increase timeout for next try, skip setup of LongtimeDataCoordinator,
63+
# if timeout was not the issue (assumed when timeout > 150)
64+
timeout = timeout + 30
65+
new = {**entry.data}
66+
new[CONF_TIMEOUT] = timeout
67+
hass.config_entries.async_update_entry(entry, data=new)
68+
69+
entry.runtime_data.longtime_data_coordinator = (
70+
SolarLogLongtimeDataCoordinator(hass, entry, solarlog, timeout)
71+
)
72+
await entry.runtime_data.longtime_data_coordinator.async_config_entry_first_refresh()
73+
74+
entry.runtime_data.device_data_coordinator = SolarLogDeviceDataCoordinator(
75+
hass, entry, solarlog
76+
)
77+
await entry.runtime_data.device_data_coordinator.async_config_entry_first_refresh()
78+
2279
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
80+
2381
return True
2482

2583

homeassistant/components/solarlog/coordinator.py

Lines changed: 137 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,41 @@
55
from collections.abc import Callable
66
from datetime import timedelta
77
import logging
8-
from urllib.parse import ParseResult, urlparse
98

10-
from aiohttp import CookieJar
119
from solarlog_cli.solarlog_connector import SolarLogConnector
1210
from solarlog_cli.solarlog_exceptions import (
1311
SolarLogAuthenticationError,
1412
SolarLogConnectionError,
1513
SolarLogUpdateError,
1614
)
17-
from solarlog_cli.solarlog_models import SolarlogData
15+
from solarlog_cli.solarlog_models import EnergyData, InverterData, SolarlogData
1816

1917
from homeassistant.config_entries import ConfigEntry
20-
from homeassistant.const import CONF_HOST
2118
from homeassistant.core import HomeAssistant
2219
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
2320
from homeassistant.helpers import device_registry as dr
24-
from homeassistant.helpers.aiohttp_client import async_create_clientsession
2521
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
2622
from homeassistant.util import slugify
2723

2824
from .const import DOMAIN
25+
from .models import SolarlogIntegrationData
2926

3027
_LOGGER = logging.getLogger(__name__)
3128

32-
type SolarlogConfigEntry = ConfigEntry[SolarLogCoordinator]
29+
type SolarlogConfigEntry = ConfigEntry[SolarlogIntegrationData]
3330

3431

35-
class SolarLogCoordinator(DataUpdateCoordinator[SolarlogData]):
36-
"""Get and update the latest data."""
32+
class SolarLogBasicDataCoordinator(DataUpdateCoordinator[SolarlogData]):
33+
"""Get and update the basic solarlog data."""
3734

3835
config_entry: SolarlogConfigEntry
3936

40-
def __init__(self, hass: HomeAssistant, config_entry: SolarlogConfigEntry) -> None:
37+
def __init__(
38+
self,
39+
hass: HomeAssistant,
40+
config_entry: SolarlogConfigEntry,
41+
api: SolarLogConnector,
42+
) -> None:
4143
"""Initialize the data object."""
4244
super().__init__(
4345
hass,
@@ -47,27 +49,8 @@ def __init__(self, hass: HomeAssistant, config_entry: SolarlogConfigEntry) -> No
4749
update_interval=timedelta(seconds=60),
4850
)
4951

50-
self.new_device_callbacks: list[Callable[[int], None]] = []
51-
self._devices_last_update: set[tuple[int, str]] = set()
52-
53-
host_entry = config_entry.data[CONF_HOST]
54-
password = config_entry.data.get("password", "")
55-
56-
url = urlparse(host_entry, "http")
57-
netloc = url.netloc or url.path
58-
path = url.path if url.netloc else ""
59-
url = ParseResult("http", netloc, path, *url[3:])
6052
self.unique_id = config_entry.entry_id
61-
self.host = url.geturl()
62-
63-
self.solarlog = SolarLogConnector(
64-
self.host,
65-
tz=hass.config.time_zone,
66-
password=password,
67-
session=async_create_clientsession(
68-
hass, cookie_jar=CookieJar(quote_cookie=False)
69-
),
70-
)
53+
self.solarlog = api
7154

7255
async def _async_setup(self) -> None:
7356
"""Do initialization logic."""
@@ -82,13 +65,10 @@ async def _async_setup(self) -> None:
8265

8366
async def _async_update_data(self) -> SolarlogData:
8467
"""Update the data from the SolarLog device."""
85-
_LOGGER.debug("Start data update")
68+
_LOGGER.debug("Start basic data update")
8669

8770
try:
88-
data = await self.solarlog.update_data()
89-
if self.solarlog.extended_data:
90-
await self.solarlog.update_device_list()
91-
data.inverter_data = await self.solarlog.update_inverter_data()
71+
data = await self.solarlog.update_basic_data()
9272
except SolarLogConnectionError as ex:
9373
raise ConfigEntryNotReady(
9474
translation_domain=DOMAIN,
@@ -112,26 +92,94 @@ async def _async_update_data(self) -> SolarlogData:
11292
translation_key="update_failed",
11393
) from ex
11494

115-
_LOGGER.debug("Data successfully updated")
116-
117-
if self.solarlog.extended_data:
118-
self._async_add_remove_devices(data)
119-
_LOGGER.debug("Add_remove_devices finished")
95+
_LOGGER.debug("Basic data successfully updated")
12096

12197
return data
12298

123-
def _async_add_remove_devices(self, data: SolarlogData) -> None:
99+
async def renew_authentication(self) -> bool:
100+
"""Renew access token for SolarLog API."""
101+
logged_in = False
102+
try:
103+
logged_in = await self.solarlog.login()
104+
except SolarLogAuthenticationError as ex:
105+
raise ConfigEntryAuthFailed(
106+
translation_domain=DOMAIN,
107+
translation_key="auth_failed",
108+
) from ex
109+
except (SolarLogConnectionError, SolarLogUpdateError) as ex:
110+
raise ConfigEntryNotReady(
111+
translation_domain=DOMAIN,
112+
translation_key="config_entry_not_ready",
113+
) from ex
114+
115+
_LOGGER.debug("Credentials successfully updated? %s", logged_in)
116+
117+
return logged_in
118+
119+
120+
class SolarLogDeviceDataCoordinator(DataUpdateCoordinator[dict[int, InverterData]]):
121+
"""Get and update the device data of solarlog."""
122+
123+
config_entry: SolarlogConfigEntry
124+
125+
def __init__(
126+
self,
127+
hass: HomeAssistant,
128+
config_entry: SolarlogConfigEntry,
129+
api: SolarLogConnector,
130+
) -> None:
131+
"""Initialize the data object."""
132+
super().__init__(
133+
hass,
134+
_LOGGER,
135+
config_entry=config_entry,
136+
name="SolarLogDevices",
137+
update_interval=timedelta(seconds=60),
138+
)
139+
140+
self.new_device_callbacks: list[Callable[[int], None]] = []
141+
self._devices_last_update: set[tuple[int, str]] = set()
142+
self.solarlog = api
143+
144+
async def _async_update_data(self) -> dict[int, InverterData]:
145+
"""Update the data from the SolarLog device."""
146+
_LOGGER.debug("Start device data update")
147+
148+
try:
149+
await self.solarlog.update_device_list()
150+
inverter_data = await self.solarlog.update_inverter_data()
151+
except SolarLogAuthenticationError as ex:
152+
raise ConfigEntryAuthFailed(
153+
translation_domain=DOMAIN,
154+
translation_key="auth_failed",
155+
) from ex
156+
except (SolarLogConnectionError, SolarLogUpdateError) as ex:
157+
raise UpdateFailed(
158+
translation_domain=DOMAIN,
159+
translation_key="update_failed",
160+
) from ex
161+
162+
_LOGGER.debug("Device data successfully updated")
163+
164+
self.data = inverter_data
165+
166+
self._async_add_remove_devices(inverter_data)
167+
168+
return inverter_data
169+
170+
def _async_add_remove_devices(self, inverter_data: dict[int, InverterData]) -> None:
124171
"""Add new devices, remove non-existing devices."""
172+
125173
if (
126174
current_devices := {
127-
(k, self.solarlog.device_name(k)) for k in data.inverter_data
175+
(k, self.solarlog.device_name(k)) for k in inverter_data
128176
}
129177
) == self._devices_last_update:
130178
return
131179

132180
# remove old devices
133181
if removed_devices := self._devices_last_update - current_devices:
134-
_LOGGER.debug("Removed device(s): %s", ", ".join(map(str, removed_devices)))
182+
_LOGGER.info("Removed device(s): %s", ", ".join(map(str, removed_devices)))
135183
device_registry = dr.async_get(self.hass)
136184

137185
for removed_device in removed_devices:
@@ -144,41 +192,76 @@ def _async_add_remove_devices(self, data: SolarlogData) -> None:
144192
identifiers={
145193
(
146194
DOMAIN,
147-
f"{self.unique_id}_{slugify(device_name)}",
195+
f"{self.config_entry.entry_id}_{slugify(device_name)}",
148196
)
149197
}
150198
):
151199
device_registry.async_update_device(
152200
device_id=device.id,
153-
remove_config_entry_id=self.unique_id,
201+
remove_config_entry_id=self.config_entry.entry_id,
154202
)
155-
_LOGGER.debug("Device removed from device registry: %s", device.id)
203+
_LOGGER.info("Device removed from device registry: %s", device.id)
156204

157205
# add new devices
158206
if new_devices := current_devices - self._devices_last_update:
159-
_LOGGER.debug("New device(s) found: %s", ", ".join(map(str, new_devices)))
207+
_LOGGER.info("New device(s) found: %s", ", ".join(map(str, new_devices)))
160208
for device_id in new_devices:
161209
for callback in self.new_device_callbacks:
162210
callback(device_id[0])
163211

164212
self._devices_last_update = current_devices
165213

166-
async def renew_authentication(self) -> bool:
167-
"""Renew access token for SolarLog API."""
168-
logged_in = False
214+
215+
class SolarLogLongtimeDataCoordinator(DataUpdateCoordinator[EnergyData]):
216+
"""Get and update the solarlog longtime energy data."""
217+
218+
config_entry: SolarlogConfigEntry
219+
220+
def __init__(
221+
self,
222+
hass: HomeAssistant,
223+
config_entry: SolarlogConfigEntry,
224+
api: SolarLogConnector,
225+
timeout: float,
226+
) -> None:
227+
"""Initialize the data object."""
228+
super().__init__(
229+
hass,
230+
_LOGGER,
231+
config_entry=config_entry,
232+
name="SolarLogLongtimeEnergy",
233+
update_interval=timedelta(seconds=timeout * 2),
234+
)
235+
236+
self.solarlog = api
237+
self.connection_timeout = timeout
238+
239+
async def _async_update_data(self) -> EnergyData:
240+
"""Update the energy data from the SolarLog device."""
241+
_LOGGER.debug(
242+
"Start energy data update with timeout=%s", self.connection_timeout
243+
)
244+
169245
try:
170-
logged_in = await self.solarlog.login()
246+
energy_data: EnergyData | None = await self.solarlog.update_energy_data(
247+
timeout=self.connection_timeout
248+
)
171249
except SolarLogAuthenticationError as ex:
172250
raise ConfigEntryAuthFailed(
173251
translation_domain=DOMAIN,
174252
translation_key="auth_failed",
175253
) from ex
176254
except (SolarLogConnectionError, SolarLogUpdateError) as ex:
177-
raise ConfigEntryNotReady(
255+
raise UpdateFailed(
178256
translation_domain=DOMAIN,
179-
translation_key="config_entry_not_ready",
257+
translation_key="update_failed",
180258
) from ex
181259

182-
_LOGGER.debug("Credentials successfully updated? %s", logged_in)
260+
if energy_data is None:
261+
energy_data = EnergyData(None, None)
183262

184-
return logged_in
263+
self.config_entry.runtime_data.basic_data_coordinator.data.self_consumption_year = energy_data.self_consumption
264+
265+
_LOGGER.debug("Energy data successfully updated")
266+
267+
return energy_data

homeassistant/components/solarlog/diagnostics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ async def async_get_config_entry_diagnostics(
1919
hass: HomeAssistant, config_entry: SolarlogConfigEntry
2020
) -> dict[str, Any]:
2121
"""Return diagnostics for a config entry."""
22-
data = config_entry.runtime_data.data
22+
data = config_entry.runtime_data.basic_data_coordinator.data
2323

2424
return {
2525
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),

0 commit comments

Comments
 (0)