Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Zigbee Home Automation devices."""
2 
3 import contextlib
4 import logging
5 from zoneinfo import ZoneInfo
6 
7 import voluptuous as vol
8 from zha.application.const import BAUD_RATES, RadioType
9 from zha.application.gateway import Gateway
10 from zha.application.helpers import ZHAData
11 from zha.zigbee.device import get_device_automation_triggers
12 from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH
13 from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError
14 
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import (
17  CONF_TYPE,
18  EVENT_CORE_CONFIG_UPDATE,
19  EVENT_HOMEASSISTANT_STOP,
20  Platform,
21 )
22 from homeassistant.core import Event, HomeAssistant, callback
23 from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
24 from homeassistant.helpers import device_registry as dr
26 from homeassistant.helpers.dispatcher import async_dispatcher_send
27 from homeassistant.helpers.typing import ConfigType
28 
29 from . import repairs, websocket_api
30 from .const import (
31  CONF_BAUDRATE,
32  CONF_CUSTOM_QUIRKS_PATH,
33  CONF_DEVICE_CONFIG,
34  CONF_ENABLE_QUIRKS,
35  CONF_FLOW_CONTROL,
36  CONF_RADIO_TYPE,
37  CONF_USB_PATH,
38  CONF_ZIGPY,
39  DATA_ZHA,
40  DOMAIN,
41 )
42 from .helpers import (
43  SIGNAL_ADD_ENTITIES,
44  HAZHAData,
45  ZHAGatewayProxy,
46  create_zha_config,
47  get_zha_data,
48 )
49 from .radio_manager import ZhaRadioManager
50 from .repairs.network_settings_inconsistent import warn_on_inconsistent_network_settings
51 from .repairs.wrong_silabs_firmware import (
52  AlreadyRunningEZSP,
53  warn_on_wrong_silabs_firmware,
54 )
55 
56 DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({vol.Optional(CONF_TYPE): cv.string})
57 ZHA_CONFIG_SCHEMA = {
58  vol.Optional(CONF_BAUDRATE): cv.positive_int,
59  vol.Optional(CONF_DATABASE): cv.string,
60  vol.Optional(CONF_DEVICE_CONFIG, default={}): vol.Schema(
61  {cv.string: DEVICE_CONFIG_SCHEMA_ENTRY}
62  ),
63  vol.Optional(CONF_ENABLE_QUIRKS, default=True): cv.boolean,
64  vol.Optional(CONF_ZIGPY): dict,
65  vol.Optional(CONF_RADIO_TYPE): cv.enum(RadioType),
66  vol.Optional(CONF_USB_PATH): cv.string,
67  vol.Optional(CONF_CUSTOM_QUIRKS_PATH): cv.isdir,
68 }
69 CONFIG_SCHEMA = vol.Schema(
70  {
71  DOMAIN: vol.Schema(
72  vol.All(
73  cv.deprecated(CONF_USB_PATH),
74  cv.deprecated(CONF_BAUDRATE),
75  cv.deprecated(CONF_RADIO_TYPE),
76  ZHA_CONFIG_SCHEMA,
77  ),
78  ),
79  },
80  extra=vol.ALLOW_EXTRA,
81 )
82 
83 PLATFORMS = (
84  Platform.ALARM_CONTROL_PANEL,
85  Platform.BINARY_SENSOR,
86  Platform.BUTTON,
87  Platform.CLIMATE,
88  Platform.COVER,
89  Platform.DEVICE_TRACKER,
90  Platform.FAN,
91  Platform.LIGHT,
92  Platform.LOCK,
93  Platform.NUMBER,
94  Platform.SELECT,
95  Platform.SENSOR,
96  Platform.SIREN,
97  Platform.SWITCH,
98  Platform.UPDATE,
99 )
100 
101 
102 # Zigbee definitions
103 CENTICELSIUS = "C-100"
104 
105 # Internal definitions
106 _LOGGER = logging.getLogger(__name__)
107 
108 
109 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
110  """Set up ZHA from config."""
111  ha_zha_data = HAZHAData(yaml_config=config.get(DOMAIN, {}))
112  hass.data[DATA_ZHA] = ha_zha_data
113 
114  return True
115 
116 
117 async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
118  """Set up ZHA.
119 
120  Will automatically load components to support devices found on the network.
121  """
122  ha_zha_data: HAZHAData = get_zha_data(hass)
123  ha_zha_data.config_entry = config_entry
124  zha_lib_data: ZHAData = create_zha_config(hass, ha_zha_data)
125 
126  zha_gateway = await Gateway.async_from_config(zha_lib_data)
127 
128  # Load and cache device trigger information early
129  device_registry = dr.async_get(hass)
130  radio_mgr = ZhaRadioManager.from_config_entry(hass, config_entry)
131 
132  async with radio_mgr.connect_zigpy_app() as app:
133  for dev in app.devices.values():
134  dev_entry = device_registry.async_get_device(
135  identifiers={(DOMAIN, str(dev.ieee))},
136  connections={(dr.CONNECTION_ZIGBEE, str(dev.ieee))},
137  )
138 
139  if dev_entry is None:
140  continue
141 
142  zha_lib_data.device_trigger_cache[dev_entry.id] = (
143  str(dev.ieee),
144  get_device_automation_triggers(dev),
145  )
146  ha_zha_data.device_trigger_cache = zha_lib_data.device_trigger_cache
147 
148  _LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache)
149 
150  try:
151  await zha_gateway.async_initialize()
152  except NetworkSettingsInconsistent as exc:
154  hass,
155  config_entry=config_entry,
156  old_state=exc.old_state,
157  new_state=exc.new_state,
158  )
159  raise ConfigEntryError(
160  "Network settings do not match most recent backup"
161  ) from exc
162  except TransientConnectionError as exc:
163  raise ConfigEntryNotReady from exc
164  except Exception as exc:
165  _LOGGER.debug("Failed to set up ZHA", exc_info=exc)
166  device_path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH]
167 
168  if (
169  not device_path.startswith("socket://")
170  and RadioType[config_entry.data[CONF_RADIO_TYPE]] == RadioType.ezsp
171  ):
172  try:
173  # Ignore all exceptions during probing, they shouldn't halt setup
174  if await warn_on_wrong_silabs_firmware(hass, device_path):
175  raise ConfigEntryError("Incorrect firmware installed") from exc
176  except AlreadyRunningEZSP as ezsp_exc:
177  raise ConfigEntryNotReady from ezsp_exc
178 
179  raise ConfigEntryNotReady from exc
180 
181  repairs.async_delete_blocking_issues(hass)
182 
183  ha_zha_data.gateway_proxy = ZHAGatewayProxy(hass, config_entry, zha_gateway)
184 
185  manufacturer = zha_gateway.state.node_info.manufacturer
186  model = zha_gateway.state.node_info.model
187 
188  if manufacturer is None and model is None:
189  manufacturer = "Unknown"
190  model = "Unknown"
191 
192  device_registry.async_get_or_create(
193  config_entry_id=config_entry.entry_id,
194  connections={(dr.CONNECTION_ZIGBEE, str(zha_gateway.state.node_info.ieee))},
195  identifiers={(DOMAIN, str(zha_gateway.state.node_info.ieee))},
196  name="Zigbee Coordinator",
197  manufacturer=manufacturer,
198  model=model,
199  sw_version=zha_gateway.state.node_info.version,
200  )
201 
202  websocket_api.async_load_api(hass)
203 
204  async def async_shutdown(_: Event) -> None:
205  """Handle shutdown tasks."""
206  assert ha_zha_data.gateway_proxy is not None
207  await ha_zha_data.gateway_proxy.shutdown()
208 
209  config_entry.async_on_unload(
210  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown)
211  )
212 
213  @callback
214  def update_config(event: Event) -> None:
215  """Handle Core config update."""
216  zha_gateway.config.local_timezone = ZoneInfo(hass.config.time_zone)
217 
218  config_entry.async_on_unload(
219  hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_config)
220  )
221 
222  await ha_zha_data.gateway_proxy.async_initialize_devices_and_entities()
223  await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
224  async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES)
225  return True
226 
227 
228 async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
229  """Unload ZHA config entry."""
230  ha_zha_data = get_zha_data(hass)
231  ha_zha_data.config_entry = None
232 
233  if ha_zha_data.gateway_proxy is not None:
234  await ha_zha_data.gateway_proxy.shutdown()
235  ha_zha_data.gateway_proxy = None
236 
237  # clean up any remaining entity metadata
238  # (entities that have been discovered but not yet added to HA)
239  # suppress KeyError because we don't know what state we may
240  # be in when we get here in failure cases
241  with contextlib.suppress(KeyError):
242  for platform in PLATFORMS:
243  del ha_zha_data.platforms[platform]
244 
245  websocket_api.async_unload_api(hass)
246 
247  return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
248 
249 
250 async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
251  """Migrate old entry."""
252  _LOGGER.debug("Migrating from version %s", config_entry.version)
253 
254  if config_entry.version == 1:
255  data = {
256  CONF_RADIO_TYPE: config_entry.data[CONF_RADIO_TYPE],
257  CONF_DEVICE: {CONF_DEVICE_PATH: config_entry.data[CONF_USB_PATH]},
258  }
259 
260  baudrate = get_zha_data(hass).yaml_config.get(CONF_BAUDRATE)
261  if data[CONF_RADIO_TYPE] != RadioType.deconz and baudrate in BAUD_RATES:
262  data[CONF_DEVICE][CONF_BAUDRATE] = baudrate
263 
264  hass.config_entries.async_update_entry(config_entry, data=data, version=2)
265 
266  if config_entry.version == 2:
267  data = {**config_entry.data}
268 
269  if data[CONF_RADIO_TYPE] == "ti_cc":
270  data[CONF_RADIO_TYPE] = "znp"
271 
272  hass.config_entries.async_update_entry(config_entry, data=data, version=3)
273 
274  if config_entry.version == 3:
275  data = {**config_entry.data}
276 
277  if not data[CONF_DEVICE].get(CONF_BAUDRATE):
278  data[CONF_DEVICE][CONF_BAUDRATE] = {
279  "deconz": 38400,
280  "xbee": 57600,
281  "ezsp": 57600,
282  "znp": 115200,
283  "zigate": 115200,
284  }[data[CONF_RADIO_TYPE]]
285 
286  if not data[CONF_DEVICE].get(CONF_FLOW_CONTROL):
287  data[CONF_DEVICE][CONF_FLOW_CONTROL] = None
288 
289  hass.config_entries.async_update_entry(config_entry, data=data, version=4)
290 
291  _LOGGER.info("Migration to version %s successful", config_entry.version)
292  return True
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None update_config(str path, dict[str, Any] calendar)
Definition: __init__.py:346
ZHAData create_zha_config(HomeAssistant hass, HAZHAData ha_zha_data)
Definition: helpers.py:1228
HAZHAData get_zha_data(HomeAssistant hass)
Definition: helpers.py:1020
None warn_on_inconsistent_network_settings(HomeAssistant hass, ConfigEntry config_entry, NetworkBackup old_state, NetworkBackup new_state)
bool warn_on_wrong_silabs_firmware(HomeAssistant hass, str device)
bool async_unload_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:228
bool async_setup_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:117
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:109
bool async_migrate_entry(HomeAssistant hass, ConfigEntry config_entry)
Definition: __init__.py:250
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193