Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for IKEA Tradfri."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, timedelta
6 from typing import Any
7 
8 from pytradfri import Gateway, RequestError
9 from pytradfri.api.aiocoap_api import APIFactory
10 from pytradfri.command import Command
11 from pytradfri.device import Device
12 
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP, Platform
15 from homeassistant.core import Event, HomeAssistant, callback
16 from homeassistant.exceptions import ConfigEntryNotReady
19  async_dispatcher_connect,
20  async_dispatcher_send,
21 )
22 from homeassistant.helpers.event import async_track_time_interval
23 
24 from .const import (
25  CONF_GATEWAY_ID,
26  CONF_IDENTITY,
27  CONF_KEY,
28  COORDINATOR,
29  COORDINATOR_LIST,
30  DOMAIN,
31  FACTORY,
32  KEY_API,
33  LOGGER,
34 )
35 from .coordinator import TradfriDeviceDataUpdateCoordinator
36 
37 PLATFORMS = [
38  Platform.COVER,
39  Platform.FAN,
40  Platform.LIGHT,
41  Platform.SENSOR,
42  Platform.SWITCH,
43 ]
44 SIGNAL_GW = "tradfri.gw_status"
45 TIMEOUT_API = 30
46 
47 
49  hass: HomeAssistant,
50  entry: ConfigEntry,
51 ) -> bool:
52  """Create a gateway."""
53  tradfri_data: dict[str, Any] = {}
54  hass.data.setdefault(DOMAIN, {})[entry.entry_id] = tradfri_data
55 
56  factory = await APIFactory.init(
57  entry.data[CONF_HOST],
58  psk_id=entry.data[CONF_IDENTITY],
59  psk=entry.data[CONF_KEY],
60  )
61  tradfri_data[FACTORY] = factory # Used for async_unload_entry
62 
63  async def on_hass_stop(event: Event) -> None:
64  """Close connection when hass stops."""
65  await factory.shutdown()
66 
67  # Setup listeners
68  entry.async_on_unload(
69  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
70  )
71 
72  api = factory.request
73  gateway = Gateway()
74 
75  try:
76  gateway_info = await api(gateway.get_gateway_info(), timeout=TIMEOUT_API)
77  devices_commands: Command = await api(
78  gateway.get_devices(), timeout=TIMEOUT_API
79  )
80  devices: list[Device] = await api(devices_commands, timeout=TIMEOUT_API)
81 
82  except RequestError as exc:
83  await factory.shutdown()
84  raise ConfigEntryNotReady from exc
85 
86  dev_reg = dr.async_get(hass)
87  dev_reg.async_get_or_create(
88  config_entry_id=entry.entry_id,
89  connections=set(),
90  identifiers={(DOMAIN, entry.data[CONF_GATEWAY_ID])},
91  manufacturer="IKEA of Sweden",
92  name="Gateway",
93  # They just have 1 gateway model. Type is not exposed yet.
94  model="E1526",
95  sw_version=gateway_info.firmware_version,
96  )
97 
98  remove_stale_devices(hass, entry, devices)
99 
100  # Setup the device coordinators
101  coordinator_data = {
102  CONF_GATEWAY_ID: gateway,
103  KEY_API: api,
104  COORDINATOR_LIST: [],
105  }
106 
107  for device in devices:
109  hass=hass, api=api, device=device
110  )
111  await coordinator.async_config_entry_first_refresh()
112 
113  entry.async_on_unload(
114  async_dispatcher_connect(hass, SIGNAL_GW, coordinator.set_hub_available)
115  )
116  coordinator_data[COORDINATOR_LIST].append(coordinator)
117 
118  tradfri_data[COORDINATOR] = coordinator_data
119 
120  async def async_keep_alive(now: datetime) -> None:
121  if hass.is_stopping:
122  return
123 
124  gw_status = True
125  try:
126  await api(gateway.get_gateway_info())
127  except RequestError:
128  LOGGER.error("Keep-alive failed")
129  gw_status = False
130 
131  async_dispatcher_send(hass, SIGNAL_GW, gw_status)
132 
133  entry.async_on_unload(
134  async_track_time_interval(hass, async_keep_alive, timedelta(seconds=60))
135  )
136 
137  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
138 
139  return True
140 
141 
142 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
143  """Unload a config entry."""
144  unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
145  if unload_ok:
146  tradfri_data = hass.data[DOMAIN].pop(entry.entry_id)
147  factory = tradfri_data[FACTORY]
148  await factory.shutdown()
149 
150  return unload_ok
151 
152 
153 @callback
155  hass: HomeAssistant, config_entry: ConfigEntry, devices: list[Device]
156 ) -> None:
157  """Remove stale devices from device registry."""
158  device_registry = dr.async_get(hass)
159  device_entries = dr.async_entries_for_config_entry(
160  device_registry, config_entry.entry_id
161  )
162  all_device_ids = {device.id for device in devices}
163 
164  for device_entry in device_entries:
165  device_id: str | None = None
166  gateway_id: str | None = None
167 
168  for identifier in device_entry.identifiers:
169  if identifier[0] != DOMAIN:
170  continue
171 
172  _id = identifier[1]
173 
174  # Identify gateway device.
175  if _id == config_entry.data[CONF_GATEWAY_ID]:
176  gateway_id = _id
177  break
178 
179  device_id = _id
180  break
181 
182  if gateway_id is not None:
183  # Do not remove gateway device entry.
184  continue
185 
186  if device_id is None or device_id not in all_device_ids:
187  # If device_id is None an invalid device entry was found for this config entry.
188  # If the device_id is not in existing device ids it's a stale device entry.
189  # Remove config entry from this device entry in either case.
190  device_registry.async_update_device(
191  device_entry.id, remove_config_entry_id=config_entry.entry_id
192  )
None remove_stale_devices(HomeAssistant hass, ConfigEntry config_entry, list[Device] devices)
Definition: __init__.py:156
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:142
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:51
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103
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