Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Overkiz (by Somfy) integration."""
2 
3 from __future__ import annotations
4 
5 from collections import defaultdict
6 from dataclasses import dataclass
7 
8 from aiohttp import ClientError
9 from pyoverkiz.client import OverkizClient
10 from pyoverkiz.const import SUPPORTED_SERVERS
11 from pyoverkiz.enums import APIType, OverkizState, UIClass, UIWidget
12 from pyoverkiz.exceptions import (
13  BadCredentialsException,
14  MaintenanceException,
15  NotSuchTokenException,
16  TooManyRequestsException,
17 )
18 from pyoverkiz.models import Device, OverkizServer, Scenario
19 from pyoverkiz.utils import generate_local_server
20 
21 from homeassistant.config_entries import ConfigEntry
22 from homeassistant.const import (
23  CONF_HOST,
24  CONF_PASSWORD,
25  CONF_TOKEN,
26  CONF_USERNAME,
27  CONF_VERIFY_SSL,
28  Platform,
29 )
30 from homeassistant.core import HomeAssistant, callback
31 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
32 from homeassistant.helpers import device_registry as dr, entity_registry as er
33 from homeassistant.helpers.aiohttp_client import async_create_clientsession
34 
35 from .const import (
36  CONF_API_TYPE,
37  CONF_HUB,
38  DOMAIN,
39  LOGGER,
40  OVERKIZ_DEVICE_TO_PLATFORM,
41  PLATFORMS,
42  UPDATE_INTERVAL,
43  UPDATE_INTERVAL_ALL_ASSUMED_STATE,
44 )
45 from .coordinator import OverkizDataUpdateCoordinator
46 
47 
48 @dataclass
50  """Overkiz data stored in the Home Assistant data object."""
51 
52  coordinator: OverkizDataUpdateCoordinator
53  platforms: defaultdict[Platform, list[Device]]
54  scenarios: list[Scenario]
55 
56 
57 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
58  """Set up Overkiz from a config entry."""
59  client: OverkizClient | None = None
60  api_type = entry.data.get(CONF_API_TYPE, APIType.CLOUD)
61 
62  # Local API
63  if api_type == APIType.LOCAL:
64  client = create_local_client(
65  hass,
66  host=entry.data[CONF_HOST],
67  token=entry.data[CONF_TOKEN],
68  verify_ssl=entry.data[CONF_VERIFY_SSL],
69  )
70 
71  # Overkiz Cloud API
72  else:
73  client = create_cloud_client(
74  hass,
75  username=entry.data[CONF_USERNAME],
76  password=entry.data[CONF_PASSWORD],
77  server=SUPPORTED_SERVERS[entry.data[CONF_HUB]],
78  )
79 
80  await _async_migrate_entries(hass, entry)
81 
82  try:
83  await client.login()
84  setup = await client.get_setup()
85 
86  # Local API does expose scenarios, but they are not functional.
87  # Tracked in https://github.com/Somfy-Developer/Somfy-TaHoma-Developer-Mode/issues/21
88  if api_type == APIType.CLOUD:
89  scenarios = await client.get_scenarios()
90  else:
91  scenarios = []
92  except (BadCredentialsException, NotSuchTokenException) as exception:
93  raise ConfigEntryAuthFailed("Invalid authentication") from exception
94  except TooManyRequestsException as exception:
95  raise ConfigEntryNotReady("Too many requests, try again later") from exception
96  except (TimeoutError, ClientError) as exception:
97  raise ConfigEntryNotReady("Failed to connect") from exception
98  except MaintenanceException as exception:
99  raise ConfigEntryNotReady("Server is down for maintenance") from exception
100 
101  coordinator = OverkizDataUpdateCoordinator(
102  hass,
103  LOGGER,
104  name="device events",
105  client=client,
106  devices=setup.devices,
107  places=setup.root_place,
108  update_interval=UPDATE_INTERVAL,
109  config_entry_id=entry.entry_id,
110  )
111 
112  await coordinator.async_config_entry_first_refresh()
113 
114  if coordinator.is_stateless:
115  LOGGER.debug(
116  (
117  "All devices have an assumed state. Update interval has been reduced"
118  " to: %s"
119  ),
120  UPDATE_INTERVAL_ALL_ASSUMED_STATE,
121  )
122  coordinator.update_interval = UPDATE_INTERVAL_ALL_ASSUMED_STATE
123 
124  platforms: defaultdict[Platform, list[Device]] = defaultdict(list)
125 
126  hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantOverkizData(
127  coordinator=coordinator, platforms=platforms, scenarios=scenarios
128  )
129 
130  # Map Overkiz entities to Home Assistant platform
131  for device in coordinator.data.values():
132  LOGGER.debug(
133  (
134  "The following device has been retrieved. Report an issue if not"
135  " supported correctly (%s)"
136  ),
137  device,
138  )
139 
140  if platform := OVERKIZ_DEVICE_TO_PLATFORM.get(
141  device.widget
142  ) or OVERKIZ_DEVICE_TO_PLATFORM.get(device.ui_class):
143  platforms[platform].append(device)
144 
145  device_registry = dr.async_get(hass)
146 
147  for gateway in setup.gateways:
148  LOGGER.debug("Added gateway (%s)", gateway)
149 
150  device_registry.async_get_or_create(
151  config_entry_id=entry.entry_id,
152  identifiers={(DOMAIN, gateway.id)},
153  model=gateway.sub_type.beautify_name if gateway.sub_type else None,
154  manufacturer=client.server.manufacturer,
155  name=gateway.type.beautify_name if gateway.type else gateway.id,
156  sw_version=gateway.connectivity.protocol_version,
157  configuration_url=client.server.configuration_url,
158  )
159 
160  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
161 
162  return True
163 
164 
165 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
166  """Unload a config entry."""
167 
168  if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
169  hass.data[DOMAIN].pop(entry.entry_id)
170 
171  return unload_ok
172 
173 
175  hass: HomeAssistant, config_entry: ConfigEntry
176 ) -> bool:
177  """Migrate old entries to new unique IDs."""
178  entity_registry = er.async_get(hass)
179 
180  @callback
181  def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
182  # Python 3.11 treats (str, Enum) and StrEnum in a different way
183  # Since pyOverkiz switched to StrEnum, we need to rewrite the unique ids once to the new style
184  #
185  # io://xxxx-xxxx-xxxx/3541212-OverkizState.CORE_DISCRETE_RSSI_LEVEL -> io://xxxx-xxxx-xxxx/3541212-core:DiscreteRSSILevelState
186  # internal://xxxx-xxxx-xxxx/alarm/0-UIWidget.TSKALARM_CONTROLLER -> internal://xxxx-xxxx-xxxx/alarm/0-TSKAlarmController
187  # io://xxxx-xxxx-xxxx/xxxxxxx-UIClass.ON_OFF -> io://xxxx-xxxx-xxxx/xxxxxxx-OnOff
188  if (key := entry.unique_id.split("-")[-1]).startswith(
189  ("OverkizState", "UIWidget", "UIClass")
190  ):
191  state = key.split(".")[1]
192  new_key = ""
193 
194  if key.startswith("UIClass"):
195  new_key = UIClass[state]
196  elif key.startswith("UIWidget"):
197  new_key = UIWidget[state]
198  else:
199  new_key = OverkizState[state]
200 
201  new_unique_id = entry.unique_id.replace(key, new_key)
202 
203  LOGGER.debug(
204  "Migrating entity '%s' unique_id from '%s' to '%s'",
205  entry.entity_id,
206  entry.unique_id,
207  new_unique_id,
208  )
209 
210  if existing_entity_id := entity_registry.async_get_entity_id(
211  entry.domain, entry.platform, new_unique_id
212  ):
213  LOGGER.debug(
214  "Cannot migrate to unique_id '%s', already exists for '%s'. Entity will be removed",
215  new_unique_id,
216  existing_entity_id,
217  )
218  entity_registry.async_remove(entry.entity_id)
219 
220  return None
221 
222  return {
223  "new_unique_id": new_unique_id,
224  }
225 
226  return None
227 
228  await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
229 
230  return True
231 
232 
234  hass: HomeAssistant, host: str, token: str, verify_ssl: bool
235 ) -> OverkizClient:
236  """Create Overkiz local client."""
237  session = async_create_clientsession(hass, verify_ssl=verify_ssl)
238 
239  return OverkizClient(
240  username="",
241  password="",
242  token=token,
243  session=session,
244  server=generate_local_server(host=host),
245  verify_ssl=verify_ssl,
246  )
247 
248 
250  hass: HomeAssistant, username: str, password: str, server: OverkizServer
251 ) -> OverkizClient:
252  """Create Overkiz cloud client."""
253  # To allow users with multiple accounts/hubs, we create a new session so they have separate cookies
254  session = async_create_clientsession(hass)
255 
256  return OverkizClient(
257  username=username, password=password, session=session, server=server
258  )
dict[str, str]|None update_unique_id(er.RegistryEntry entity_entry, str unique_id)
Definition: __init__.py:168
bool _async_migrate_entries(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:176
OverkizClient create_cloud_client(HomeAssistant hass, str username, str password, OverkizServer server)
Definition: __init__.py:251
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:165
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:57
OverkizClient create_local_client(HomeAssistant hass, str host, str token, bool verify_ssl)
Definition: __init__.py:235
aiohttp.ClientSession async_create_clientsession()
Definition: coordinator.py:51