Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """DataUpdateCoordinator for Plugwise."""
2 
3 from datetime import timedelta
4 
5 from packaging.version import Version
6 from plugwise import PlugwiseData, Smile
7 from plugwise.exceptions import (
8  ConnectionFailedError,
9  InvalidAuthentication,
10  InvalidXMLError,
11  PlugwiseError,
12  ResponseError,
13  UnsupportedDeviceError,
14 )
15 
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
18 from homeassistant.core import HomeAssistant
19 from homeassistant.exceptions import ConfigEntryError
20 from homeassistant.helpers import device_registry as dr
21 from homeassistant.helpers.aiohttp_client import async_get_clientsession
22 from homeassistant.helpers.debounce import Debouncer
23 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
24 
25 from .const import DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, GATEWAY_ID, LOGGER
26 
27 
29  """Class to manage fetching Plugwise data from single endpoint."""
30 
31  _connected: bool = False
32 
33  config_entry: ConfigEntry
34 
35  def __init__(self, hass: HomeAssistant) -> None:
36  """Initialize the coordinator."""
37  super().__init__(
38  hass,
39  LOGGER,
40  name=DOMAIN,
41  update_interval=timedelta(seconds=60),
42  # Don't refresh immediately, give the device time to process
43  # the change in state before we query it.
44  request_refresh_debouncer=Debouncer(
45  hass,
46  LOGGER,
47  cooldown=1.5,
48  immediate=False,
49  ),
50  )
51 
52  self.apiapi = Smile(
53  host=self.config_entryconfig_entry.data[CONF_HOST],
54  username=self.config_entryconfig_entry.data.get(CONF_USERNAME, DEFAULT_USERNAME),
55  password=self.config_entryconfig_entry.data[CONF_PASSWORD],
56  port=self.config_entryconfig_entry.data.get(CONF_PORT, DEFAULT_PORT),
57  websession=async_get_clientsession(hass, verify_ssl=False),
58  )
59  self._current_devices_current_devices: set[str] = set()
60  self.new_devicesnew_devices: set[str] = set()
61 
62  async def _connect(self) -> None:
63  """Connect to the Plugwise Smile."""
64  version = await self.apiapi.connect()
65  self._connected_connected = isinstance(version, Version)
66  if self._connected_connected:
67  self.apiapi.get_all_gateway_entities()
68 
69  async def _async_update_data(self) -> PlugwiseData:
70  """Fetch data from Plugwise."""
71  data = PlugwiseData(devices={}, gateway={})
72  try:
73  if not self._connected_connected:
74  await self._connect_connect()
75  data = await self.apiapi.async_update()
76  except ConnectionFailedError as err:
77  raise UpdateFailed("Failed to connect") from err
78  except InvalidAuthentication as err:
79  raise ConfigEntryError("Authentication failed") from err
80  except (InvalidXMLError, ResponseError) as err:
81  raise UpdateFailed(
82  "Invalid XML data, or error indication received from the Plugwise Adam/Smile/Stretch"
83  ) from err
84  except PlugwiseError as err:
85  raise UpdateFailed("Data incomplete or missing") from err
86  except UnsupportedDeviceError as err:
87  raise ConfigEntryError("Device with unsupported firmware") from err
88  else:
89  self._async_add_remove_devices_async_add_remove_devices(data, self.config_entryconfig_entry)
90 
91  return data
92 
93  def _async_add_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
94  """Add new Plugwise devices, remove non-existing devices."""
95  # Check for new or removed devices
96  self.new_devicesnew_devices = set(data.devices) - self._current_devices_current_devices
97  removed_devices = self._current_devices_current_devices - set(data.devices)
98  self._current_devices_current_devices = set(data.devices)
99 
100  if removed_devices:
101  self._async_remove_devices_async_remove_devices(data, entry)
102 
103  def _async_remove_devices(self, data: PlugwiseData, entry: ConfigEntry) -> None:
104  """Clean registries when removed devices found."""
105  device_reg = dr.async_get(self.hasshass)
106  device_list = dr.async_entries_for_config_entry(
107  device_reg, self.config_entryconfig_entry.entry_id
108  )
109  # First find the Plugwise via_device
110  gateway_device = device_reg.async_get_device(
111  {(DOMAIN, data.gateway[GATEWAY_ID])}
112  )
113  assert gateway_device is not None
114  via_device_id = gateway_device.id
115 
116  # Then remove the connected orphaned device(s)
117  for device_entry in device_list:
118  for identifier in device_entry.identifiers:
119  if identifier[0] == DOMAIN:
120  if (
121  device_entry.via_device_id == via_device_id
122  and identifier[1] not in data.devices
123  ):
124  device_reg.async_update_device(
125  device_entry.id, remove_config_entry_id=entry.entry_id
126  )
127  LOGGER.debug(
128  "Removed %s device %s %s from device_registry",
129  DOMAIN,
130  device_entry.model,
131  identifier[1],
132  )
None _async_add_remove_devices(self, PlugwiseData data, ConfigEntry entry)
Definition: coordinator.py:93
None _async_remove_devices(self, PlugwiseData data, ConfigEntry entry)
Definition: coordinator.py:103
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)