Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Fronius integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from datetime import datetime, timedelta
7 import logging
8 from typing import Final
9 
10 from pyfronius import Fronius, FroniusError
11 
12 from homeassistant.config_entries import ConfigEntry, ConfigEntryState
13 from homeassistant.const import ATTR_MODEL, ATTR_SW_VERSION, CONF_HOST, Platform
14 from homeassistant.core import HomeAssistant
15 from homeassistant.exceptions import ConfigEntryNotReady
16 from homeassistant.helpers import device_registry as dr
17 from homeassistant.helpers.aiohttp_client import async_get_clientsession
18 from homeassistant.helpers.device_registry import DeviceInfo
19 from homeassistant.helpers.dispatcher import async_dispatcher_send
20 from homeassistant.helpers.event import async_track_time_interval
21 
22 from .const import (
23  DOMAIN,
24  SOLAR_NET_DISCOVERY_NEW,
25  SOLAR_NET_ID_SYSTEM,
26  SOLAR_NET_RESCAN_TIMER,
27  FroniusDeviceInfo,
28 )
29 from .coordinator import (
30  FroniusCoordinatorBase,
31  FroniusInverterUpdateCoordinator,
32  FroniusLoggerUpdateCoordinator,
33  FroniusMeterUpdateCoordinator,
34  FroniusOhmpilotUpdateCoordinator,
35  FroniusPowerFlowUpdateCoordinator,
36  FroniusStorageUpdateCoordinator,
37 )
38 
39 _LOGGER: Final = logging.getLogger(__name__)
40 PLATFORMS: Final = [Platform.SENSOR]
41 
42 type FroniusConfigEntry = ConfigEntry[FroniusSolarNet]
43 
44 
45 async def async_setup_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool:
46  """Set up fronius from a config entry."""
47  host = entry.data[CONF_HOST]
48  fronius = Fronius(async_get_clientsession(hass), host)
49  solar_net = FroniusSolarNet(hass, entry, fronius)
50  await solar_net.init_devices()
51 
52  entry.runtime_data = solar_net
53  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
54  return True
55 
56 
57 async def async_unload_entry(hass: HomeAssistant, entry: FroniusConfigEntry) -> bool:
58  """Unload a config entry."""
59  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
60 
61 
63  hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
64 ) -> bool:
65  """Remove a config entry from a device."""
66  return True
67 
68 
70  """The FroniusSolarNet class routes received values to sensor entities."""
71 
72  def __init__(
73  self, hass: HomeAssistant, entry: ConfigEntry, fronius: Fronius
74  ) -> None:
75  """Initialize FroniusSolarNet class."""
76  self.hasshass = hass
77  self.config_entryconfig_entry = entry
78  self.coordinator_lockcoordinator_lock = asyncio.Lock()
79  self.froniusfronius = fronius
80  self.host: str = entry.data[CONF_HOST]
81  # entry.unique_id is either logger uid or first inverter uid if no logger available
82  # prepended by "solar_net_" to have individual device for whole system (power_flow)
83  self.solar_net_device_idsolar_net_device_id = f"solar_net_{entry.unique_id}"
84  self.system_device_infosystem_device_info: DeviceInfo | None = None
85 
86  self.inverter_coordinators: list[FroniusInverterUpdateCoordinator] = []
87  self.logger_coordinatorlogger_coordinator: FroniusLoggerUpdateCoordinator | None = None
88  self.meter_coordinatormeter_coordinator: FroniusMeterUpdateCoordinator | None = None
89  self.ohmpilot_coordinatorohmpilot_coordinator: FroniusOhmpilotUpdateCoordinator | None = None
90  self.power_flow_coordinatorpower_flow_coordinator: FroniusPowerFlowUpdateCoordinator | None = None
91  self.storage_coordinatorstorage_coordinator: FroniusStorageUpdateCoordinator | None = None
92 
93  async def init_devices(self) -> None:
94  """Initialize DataUpdateCoordinators for SolarNet devices."""
95  if self.config_entryconfig_entry.data["is_logger"]:
97  hass=self.hasshass,
98  solar_net=self,
99  logger=_LOGGER,
100  name=f"{DOMAIN}_logger_{self.host}",
101  )
102  await self.logger_coordinatorlogger_coordinator.async_config_entry_first_refresh()
103 
104  # _create_solar_net_device uses data from self.logger_coordinator when available
105  self.system_device_infosystem_device_info = await self._create_solar_net_device_create_solar_net_device()
106 
107  await self._init_devices_inverter_init_devices_inverter()
108 
109  self.meter_coordinatormeter_coordinator = await self._init_optional_coordinator(
111  hass=self.hasshass,
112  solar_net=self,
113  logger=_LOGGER,
114  name=f"{DOMAIN}_meters_{self.host}",
115  )
116  )
117 
118  self.ohmpilot_coordinatorohmpilot_coordinator = await self._init_optional_coordinator(
120  hass=self.hasshass,
121  solar_net=self,
122  logger=_LOGGER,
123  name=f"{DOMAIN}_ohmpilot_{self.host}",
124  )
125  )
126 
127  self.power_flow_coordinatorpower_flow_coordinator = await self._init_optional_coordinator(
129  hass=self.hasshass,
130  solar_net=self,
131  logger=_LOGGER,
132  name=f"{DOMAIN}_power_flow_{self.host}",
133  )
134  )
135 
136  self.storage_coordinatorstorage_coordinator = await self._init_optional_coordinator(
138  hass=self.hasshass,
139  solar_net=self,
140  logger=_LOGGER,
141  name=f"{DOMAIN}_storages_{self.host}",
142  )
143  )
144 
145  # Setup periodic re-scan
146  self.config_entryconfig_entry.async_on_unload(
148  self.hasshass,
149  self._init_devices_inverter_init_devices_inverter,
150  timedelta(minutes=SOLAR_NET_RESCAN_TIMER),
151  )
152  )
153 
154  async def _create_solar_net_device(self) -> DeviceInfo:
155  """Create a device for the Fronius SolarNet system."""
156  solar_net_device: DeviceInfo = DeviceInfo(
157  configuration_url=self.froniusfronius.url,
158  identifiers={(DOMAIN, self.solar_net_device_idsolar_net_device_id)},
159  manufacturer="Fronius",
160  name="SolarNet",
161  )
162  if self.logger_coordinatorlogger_coordinator:
163  _logger_info = self.logger_coordinatorlogger_coordinator.data[SOLAR_NET_ID_SYSTEM]
164  # API v0 doesn't provide product_type
165  solar_net_device[ATTR_MODEL] = _logger_info.get("product_type", {}).get(
166  "value", "Datalogger Web"
167  )
168  solar_net_device[ATTR_SW_VERSION] = _logger_info["software_version"][
169  "value"
170  ]
171 
172  device_registry = dr.async_get(self.hasshass)
173  device_registry.async_get_or_create(
174  config_entry_id=self.config_entryconfig_entry.entry_id,
175  **solar_net_device,
176  )
177  return solar_net_device
178 
179  async def _init_devices_inverter(self, _now: datetime | None = None) -> None:
180  """Get available inverters and set up coordinators for new found devices."""
181  _inverter_infos = await self._get_inverter_infos_get_inverter_infos()
182 
183  _LOGGER.debug("Processing inverters for: %s", _inverter_infos)
184  for _inverter_info in _inverter_infos:
185  _inverter_name = (
186  f"{DOMAIN}_inverter_{_inverter_info.solar_net_id}_{self.host}"
187  )
188 
189  # Add found inverter only not already existing
190  if _inverter_info.solar_net_id in [
191  inv.inverter_info.solar_net_id for inv in self.inverter_coordinators
192  ]:
193  continue
194 
195  _coordinator = FroniusInverterUpdateCoordinator(
196  hass=self.hasshass,
197  solar_net=self,
198  logger=_LOGGER,
199  name=_inverter_name,
200  inverter_info=_inverter_info,
201  )
202  if self.config_entryconfig_entry.state == ConfigEntryState.LOADED:
203  await _coordinator.async_refresh()
204  else:
205  await _coordinator.async_config_entry_first_refresh()
206  self.inverter_coordinators.append(_coordinator)
207 
208  # Only for re-scans. Initial setup adds entities through sensor.async_setup_entry
209  if self.config_entryconfig_entry.state == ConfigEntryState.LOADED:
210  async_dispatcher_send(self.hasshass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
211 
212  _LOGGER.debug(
213  "New inverter added (UID: %s)",
214  _inverter_info.unique_id,
215  )
216 
217  async def _get_inverter_infos(self) -> list[FroniusDeviceInfo]:
218  """Get information about the inverters in the SolarNet system."""
219  inverter_infos: list[FroniusDeviceInfo] = []
220 
221  try:
222  _inverter_info = await self.froniusfronius.inverter_info()
223  except FroniusError as err:
224  if self.config_entryconfig_entry.state == ConfigEntryState.LOADED:
225  # During a re-scan we will attempt again as per schedule.
226  _LOGGER.debug("Re-scan failed for %s", self.host)
227  return inverter_infos
228 
229  raise ConfigEntryNotReady from err
230 
231  for inverter in _inverter_info["inverters"]:
232  solar_net_id = inverter["device_id"]["value"]
233  unique_id = inverter["unique_id"]["value"]
234  device_info = DeviceInfo(
235  identifiers={(DOMAIN, unique_id)},
236  manufacturer=inverter["device_type"].get("manufacturer", "Fronius"),
237  model=inverter["device_type"].get(
238  "model", inverter["device_type"]["value"]
239  ),
240  name=inverter.get("custom_name", {}).get("value"),
241  via_device=(DOMAIN, self.solar_net_device_idsolar_net_device_id),
242  )
243  inverter_infos.append(
245  device_info=device_info,
246  solar_net_id=solar_net_id,
247  unique_id=unique_id,
248  )
249  )
250  _LOGGER.debug(
251  "Inverter found at %s (Device ID: %s, UID: %s)",
252  self.host,
253  solar_net_id,
254  unique_id,
255  )
256  return inverter_infos
257 
258  @staticmethod
259  async def _init_optional_coordinator[_FroniusCoordinatorT: FroniusCoordinatorBase](
260  coordinator: _FroniusCoordinatorT,
261  ) -> _FroniusCoordinatorT | None:
262  """Initialize an update coordinator and return it if devices are found."""
263  try:
264  await coordinator.async_config_entry_first_refresh()
265  except ConfigEntryNotReady:
266  # ConfigEntryNotReady raised form FroniusError / KeyError in
267  # DataUpdateCoordinator if request not supported by the Fronius device
268  return None
269  # if no device for the request is installed an empty dict is returned
270  if not coordinator.data:
271  return None
272  return coordinator
list[FroniusDeviceInfo] _get_inverter_infos(self)
Definition: __init__.py:217
None _init_devices_inverter(self, datetime|None _now=None)
Definition: __init__.py:179
None __init__(self, HomeAssistant hass, ConfigEntry entry, Fronius fronius)
Definition: __init__.py:74
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
bool async_setup_entry(HomeAssistant hass, FroniusConfigEntry entry)
Definition: __init__.py:45
bool async_remove_config_entry_device(HomeAssistant hass, ConfigEntry config_entry, dr.DeviceEntry device_entry)
Definition: __init__.py:64
bool async_unload_entry(HomeAssistant hass, FroniusConfigEntry entry)
Definition: __init__.py:57
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)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193
CALLBACK_TYPE async_track_time_interval(HomeAssistant hass, Callable[[datetime], Coroutine[Any, Any, None]|None] action, timedelta interval, *str|None name=None, bool|None cancel_on_shutdown=None)
Definition: event.py:1679