Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The ViCare integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Mapping
6 from contextlib import suppress
7 import logging
8 import os
9 from typing import Any
10 
11 from PyViCare.PyViCare import PyViCare
12 from PyViCare.PyViCareDeviceConfig import PyViCareDeviceConfig
13 from PyViCare.PyViCareUtils import (
14  PyViCareInvalidConfigurationError,
15  PyViCareInvalidCredentialsError,
16 )
17 
18 from homeassistant.components.climate import DOMAIN as DOMAIN_CLIMATE
19 from homeassistant.config_entries import ConfigEntry
20 from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME
21 from homeassistant.core import HomeAssistant
22 from homeassistant.exceptions import ConfigEntryAuthFailed
23 from homeassistant.helpers import device_registry as dr, entity_registry as er
24 from homeassistant.helpers.storage import STORAGE_DIR
25 
26 from .const import (
27  DEFAULT_CACHE_DURATION,
28  DEVICE_LIST,
29  DOMAIN,
30  PLATFORMS,
31  UNSUPPORTED_DEVICES,
32 )
33 from .types import ViCareDevice
34 from .utils import get_device, get_device_serial
35 
36 _LOGGER = logging.getLogger(__name__)
37 _TOKEN_FILENAME = "vicare_token.save"
38 
39 
40 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
41  """Set up from config entry."""
42  _LOGGER.debug("Setting up ViCare component")
43 
44  hass.data[DOMAIN] = {}
45  hass.data[DOMAIN][entry.entry_id] = {}
46 
47  try:
48  await hass.async_add_executor_job(setup_vicare_api, hass, entry)
49  except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError) as err:
50  raise ConfigEntryAuthFailed("Authentication failed") from err
51 
52  for device in hass.data[DOMAIN][entry.entry_id][DEVICE_LIST]:
53  # Migration can be removed in 2025.4.0
54  await async_migrate_devices_and_entities(hass, entry, device)
55 
56  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
57 
58  return True
59 
60 
62  hass: HomeAssistant,
63  entry_data: Mapping[str, Any],
64  cache_duration=DEFAULT_CACHE_DURATION,
65 ) -> PyViCare:
66  """Login via PyVicare API."""
67  vicare_api = PyViCare()
68  vicare_api.setCacheDuration(cache_duration)
69  vicare_api.initWithCredentials(
70  entry_data[CONF_USERNAME],
71  entry_data[CONF_PASSWORD],
72  entry_data[CONF_CLIENT_ID],
73  hass.config.path(STORAGE_DIR, _TOKEN_FILENAME),
74  )
75  return vicare_api
76 
77 
78 def setup_vicare_api(hass: HomeAssistant, entry: ConfigEntry) -> None:
79  """Set up PyVicare API."""
80  vicare_api = vicare_login(hass, entry.data)
81 
82  device_config_list = get_supported_devices(vicare_api.devices)
83  if (number_of_devices := len(device_config_list)) > 1:
84  cache_duration = DEFAULT_CACHE_DURATION * number_of_devices
85  _LOGGER.debug(
86  "Found %s devices, adjusting cache duration to %s",
87  number_of_devices,
88  cache_duration,
89  )
90  vicare_api = vicare_login(hass, entry.data, cache_duration)
91  device_config_list = get_supported_devices(vicare_api.devices)
92 
93  for device in device_config_list:
94  _LOGGER.debug(
95  "Found device: %s (online: %s)", device.getModel(), str(device.isOnline())
96  )
97 
98  hass.data[DOMAIN][entry.entry_id][DEVICE_LIST] = [
99  ViCareDevice(config=device_config, api=get_device(entry, device_config))
100  for device_config in device_config_list
101  ]
102 
103 
104 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
105  """Unload ViCare config entry."""
106  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
107  if unload_ok:
108  hass.data[DOMAIN].pop(entry.entry_id)
109 
110  with suppress(FileNotFoundError):
111  await hass.async_add_executor_job(
112  os.remove, hass.config.path(STORAGE_DIR, _TOKEN_FILENAME)
113  )
114 
115  return unload_ok
116 
117 
119  hass: HomeAssistant, entry: ConfigEntry, device: ViCareDevice
120 ) -> None:
121  """Migrate old entry."""
122  device_registry = dr.async_get(hass)
123  entity_registry = er.async_get(hass)
124 
125  gateway_serial: str = device.config.getConfig().serial
126  device_id = device.config.getId()
127  device_serial: str | None = await hass.async_add_executor_job(
128  get_device_serial, device.api
129  )
130  device_model = device.config.getModel()
131 
132  old_identifier = gateway_serial
133  new_identifier = (
134  f"{gateway_serial}_{device_serial if device_serial is not None else device_id}"
135  )
136 
137  # Migrate devices
138  for device_entry in dr.async_entries_for_config_entry(
139  device_registry, entry.entry_id
140  ):
141  if (
142  device_entry.identifiers == {(DOMAIN, old_identifier)}
143  and device_entry.model == device_model
144  ):
145  _LOGGER.debug(
146  "Migrating device %s to new identifier %s",
147  device_entry.name,
148  new_identifier,
149  )
150  device_registry.async_update_device(
151  device_entry.id,
152  serial_number=device_serial,
153  new_identifiers={(DOMAIN, new_identifier)},
154  )
155 
156  # Migrate entities
157  for entity_entry in er.async_entries_for_device(
158  entity_registry, device_entry.id, True
159  ):
160  if entity_entry.unique_id.startswith(new_identifier):
161  # already correct, nothing to do
162  continue
163  unique_id_parts = entity_entry.unique_id.split("-")
164  # replace old prefix `<gateway-serial>`
165  # with `<gateways-serial>_<device-serial>`
166  unique_id_parts[0] = new_identifier
167  # convert climate entity unique id
168  # from `<device_identifier>-<circuit_no>`
169  # to `<device_identifier>-heating-<circuit_no>`
170  if entity_entry.domain == DOMAIN_CLIMATE:
171  unique_id_parts[len(unique_id_parts) - 1] = (
172  f"{entity_entry.translation_key}-{unique_id_parts[len(unique_id_parts)-1]}"
173  )
174  entity_new_unique_id = "-".join(unique_id_parts)
175 
176  _LOGGER.debug(
177  "Migrating entity %s to new unique id %s",
178  entity_entry.name,
179  entity_new_unique_id,
180  )
181  entity_registry.async_update_entity(
182  entity_id=entity_entry.entity_id, new_unique_id=entity_new_unique_id
183  )
184 
185 
187  devices: list[PyViCareDeviceConfig],
188 ) -> list[PyViCareDeviceConfig]:
189  """Remove unsupported devices from the list."""
190  return [
191  device_config
192  for device_config in devices
193  if device_config.getModel() not in UNSUPPORTED_DEVICES
194  ]
DeviceEntry get_device(HomeAssistant hass, str unique_id)
Definition: util.py:12
PyViCare vicare_login(HomeAssistant hass, Mapping[str, Any] entry_data, cache_duration=DEFAULT_CACHE_DURATION)
Definition: __init__.py:65
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:40
None setup_vicare_api(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:78
None async_migrate_devices_and_entities(HomeAssistant hass, ConfigEntry entry, ViCareDevice device)
Definition: __init__.py:120
list[PyViCareDeviceConfig] get_supported_devices(list[PyViCareDeviceConfig] devices)
Definition: __init__.py:188
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:104