Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """DataUpdateCoordinator for solarlog integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from datetime import timedelta
7 import logging
8 from typing import TYPE_CHECKING
9 from urllib.parse import ParseResult, urlparse
10 
11 from solarlog_cli.solarlog_connector import SolarLogConnector
12 from solarlog_cli.solarlog_exceptions import (
13  SolarLogAuthenticationError,
14  SolarLogConnectionError,
15  SolarLogUpdateError,
16 )
17 from solarlog_cli.solarlog_models import SolarlogData
18 
19 from homeassistant.const import CONF_HOST
20 from homeassistant.core import HomeAssistant
21 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
22 from homeassistant.helpers.aiohttp_client import async_get_clientsession
24 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
25 from homeassistant.util import slugify
26 
27 from .const import DOMAIN
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 if TYPE_CHECKING:
32  from . import SolarlogConfigEntry
33 
34 
36  """Get and update the latest data."""
37 
38  def __init__(self, hass: HomeAssistant, entry: SolarlogConfigEntry) -> None:
39  """Initialize the data object."""
40  super().__init__(
41  hass, _LOGGER, name="SolarLog", update_interval=timedelta(seconds=60)
42  )
43 
44  self.new_device_callbacks: list[Callable[[int], None]] = []
45  self._devices_last_update_devices_last_update: set[tuple[int, str]] = set()
46 
47  host_entry = entry.data[CONF_HOST]
48  password = entry.data.get("password", "")
49 
50  url = urlparse(host_entry, "http")
51  netloc = url.netloc or url.path
52  path = url.path if url.netloc else ""
53  url = ParseResult("http", netloc, path, *url[3:])
54  self.unique_idunique_id = entry.entry_id
55  self.namenamename = entry.title
56  self.hosthost = url.geturl()
57 
58  self.solarlogsolarlog = SolarLogConnector(
59  self.hosthost,
60  tz=hass.config.time_zone,
61  password=password,
62  session=async_get_clientsession(hass),
63  )
64 
65  async def _async_setup(self) -> None:
66  """Do initialization logic."""
67  _LOGGER.debug("Start async_setup")
68  logged_in = False
69  if self.solarlogsolarlog.password != "":
70  if logged_in := await self.renew_authenticationrenew_authentication():
71  await self.solarlogsolarlog.test_extended_data_available()
72  if logged_in or await self.solarlogsolarlog.test_extended_data_available():
73  device_list = await self.solarlogsolarlog.update_device_list()
74  self.solarlogsolarlog.set_enabled_devices({key: True for key in device_list})
75 
76  async def _async_update_data(self) -> SolarlogData:
77  """Update the data from the SolarLog device."""
78  _LOGGER.debug("Start data update")
79 
80  try:
81  data = await self.solarlogsolarlog.update_data()
82  if self.solarlogsolarlog.extended_data:
83  await self.solarlogsolarlog.update_device_list()
84  data.inverter_data = await self.solarlogsolarlog.update_inverter_data()
85  except SolarLogConnectionError as ex:
86  raise ConfigEntryNotReady(
87  translation_domain=DOMAIN,
88  translation_key="config_entry_not_ready",
89  ) from ex
90  except SolarLogAuthenticationError as ex:
91  if await self.renew_authenticationrenew_authentication():
92  # login was successful, update availability of extended data, retry data update
93  await self.solarlogsolarlog.test_extended_data_available()
94  raise ConfigEntryNotReady(
95  translation_domain=DOMAIN,
96  translation_key="config_entry_not_ready",
97  ) from ex
99  translation_domain=DOMAIN,
100  translation_key="auth_failed",
101  ) from ex
102  except SolarLogUpdateError as ex:
103  raise UpdateFailed(
104  translation_domain=DOMAIN,
105  translation_key="update_failed",
106  ) from ex
107 
108  _LOGGER.debug("Data successfully updated")
109 
110  if self.solarlogsolarlog.extended_data:
111  self._async_add_remove_devices_async_add_remove_devices(data)
112  _LOGGER.debug("Add_remove_devices finished")
113 
114  return data
115 
116  def _async_add_remove_devices(self, data: SolarlogData) -> None:
117  """Add new devices, remove non-existing devices."""
118  if (
119  current_devices := {
120  (k, self.solarlogsolarlog.device_name(k)) for k in data.inverter_data
121  }
122  ) == self._devices_last_update_devices_last_update:
123  return
124 
125  # remove old devices
126  if removed_devices := self._devices_last_update_devices_last_update - current_devices:
127  _LOGGER.debug("Removed device(s): %s", ", ".join(map(str, removed_devices)))
128  device_registry = dr.async_get(self.hasshass)
129 
130  for removed_device in removed_devices:
131  device_name = ""
132  for did, dn in self._devices_last_update_devices_last_update:
133  if did == removed_device[0]:
134  device_name = dn
135  break
136  if device := device_registry.async_get_device(
137  identifiers={
138  (
139  DOMAIN,
140  f"{self.unique_id}_{slugify(device_name)}",
141  )
142  }
143  ):
144  device_registry.async_update_device(
145  device_id=device.id,
146  remove_config_entry_id=self.unique_idunique_id,
147  )
148  _LOGGER.debug("Device removed from device registry: %s", device.id)
149 
150  # add new devices
151  if new_devices := current_devices - self._devices_last_update_devices_last_update:
152  _LOGGER.debug("New device(s) found: %s", ", ".join(map(str, new_devices)))
153  for device_id in new_devices:
154  for callback in self.new_device_callbacks:
155  callback(device_id[0])
156 
157  self._devices_last_update_devices_last_update = current_devices
158 
159  async def renew_authentication(self) -> bool:
160  """Renew access token for SolarLog API."""
161  logged_in = False
162  try:
163  logged_in = await self.solarlogsolarlog.login()
164  except SolarLogAuthenticationError as ex:
165  raise ConfigEntryAuthFailed(
166  translation_domain=DOMAIN,
167  translation_key="auth_failed",
168  ) from ex
169  except (SolarLogConnectionError, SolarLogUpdateError) as ex:
170  raise ConfigEntryNotReady(
171  translation_domain=DOMAIN,
172  translation_key="config_entry_not_ready",
173  ) from ex
174 
175  _LOGGER.debug("Credentials successfully updated? %s", logged_in)
176 
177  return logged_in
None __init__(self, HomeAssistant hass, SolarlogConfigEntry entry)
Definition: coordinator.py:38
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)