Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Rain Bird Irrigation system LNK WiFi Module."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 import aiohttp
9 from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController
10 from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
11 
12 from homeassistant.config_entries import ConfigEntry
13 from homeassistant.const import (
14  CONF_HOST,
15  CONF_MAC,
16  CONF_PASSWORD,
17  EVENT_HOMEASSISTANT_CLOSE,
18  Platform,
19 )
20 from homeassistant.core import HomeAssistant
21 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
22 from homeassistant.helpers import device_registry as dr, entity_registry as er
23 from homeassistant.helpers.device_registry import format_mac
24 
25 from .const import CONF_SERIAL_NUMBER
26 from .coordinator import (
27  RainbirdScheduleUpdateCoordinator,
28  RainbirdUpdateCoordinator,
29  async_create_clientsession,
30 )
31 from .types import RainbirdConfigEntry, RainbirdData
32 
33 _LOGGER = logging.getLogger(__name__)
34 
35 PLATFORMS = [
36  Platform.BINARY_SENSOR,
37  Platform.CALENDAR,
38  Platform.NUMBER,
39  Platform.SENSOR,
40  Platform.SWITCH,
41 ]
42 
43 
44 DOMAIN = "rainbird"
45 
46 
48  hass: HomeAssistant,
49  entry: ConfigEntry,
50  clientsession: aiohttp.ClientSession,
51 ) -> None:
52  """Register cleanup hooks for the clientsession."""
53 
54  async def _async_close_websession(*_: Any) -> None:
55  """Close websession."""
56  await clientsession.close()
57 
58  unsub = hass.bus.async_listen_once(
59  EVENT_HOMEASSISTANT_CLOSE, _async_close_websession
60  )
61  entry.async_on_unload(unsub)
62  entry.async_on_unload(_async_close_websession)
63 
64 
65 async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) -> bool:
66  """Set up the config entry for Rain Bird."""
67 
68  hass.data.setdefault(DOMAIN, {})
69 
70  clientsession = async_create_clientsession()
71  _async_register_clientsession_shutdown(hass, entry, clientsession)
72 
73  controller = AsyncRainbirdController(
74  AsyncRainbirdClient(
75  clientsession,
76  entry.data[CONF_HOST],
77  entry.data[CONF_PASSWORD],
78  )
79  )
80 
81  if not (await _async_fix_unique_id(hass, controller, entry)):
82  return False
83  if mac_address := entry.data.get(CONF_MAC):
85  hass,
86  er.async_get(hass),
87  entry.entry_id,
88  format_mac(mac_address),
89  str(entry.data[CONF_SERIAL_NUMBER]),
90  )
92  hass,
93  dr.async_get(hass),
94  entry.entry_id,
95  format_mac(mac_address),
96  str(entry.data[CONF_SERIAL_NUMBER]),
97  )
98 
99  try:
100  model_info = await controller.get_model_and_version()
101  except RainbirdAuthException as err:
102  raise ConfigEntryAuthFailed from err
103  except RainbirdApiException as err:
104  raise ConfigEntryNotReady from err
105 
106  data = RainbirdData(
107  controller,
108  model_info,
109  coordinator=RainbirdUpdateCoordinator(
110  hass,
111  name=entry.title,
112  controller=controller,
113  unique_id=entry.unique_id,
114  model_info=model_info,
115  ),
116  schedule_coordinator=RainbirdScheduleUpdateCoordinator(
117  hass,
118  name=f"{entry.title} Schedule",
119  controller=controller,
120  ),
121  )
122  await data.coordinator.async_config_entry_first_refresh()
123 
124  entry.runtime_data = data
125  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
126 
127  return True
128 
129 
131  hass: HomeAssistant, controller: AsyncRainbirdController, entry: ConfigEntry
132 ) -> bool:
133  """Update the config entry with a unique id based on the mac address."""
134  _LOGGER.debug("Checking for migration of config entry (%s)", entry.unique_id)
135  if not (mac_address := entry.data.get(CONF_MAC)):
136  try:
137  wifi_params = await controller.get_wifi_params()
138  except RainbirdApiException as err:
139  _LOGGER.warning("Unable to fix missing unique id: %s", err)
140  return True
141 
142  if (mac_address := wifi_params.mac_address) is None:
143  _LOGGER.warning("Unable to fix missing unique id (mac address was None)")
144  return True
145 
146  new_unique_id = format_mac(mac_address)
147  if entry.unique_id == new_unique_id and CONF_MAC in entry.data:
148  _LOGGER.debug("Config entry already in correct state")
149  return True
150 
151  entries = hass.config_entries.async_entries(DOMAIN)
152  for existing_entry in entries:
153  if existing_entry.unique_id == new_unique_id:
154  _LOGGER.warning(
155  "Unable to fix missing unique id (already exists); Removing duplicate entry"
156  )
157  hass.async_create_background_task(
158  hass.config_entries.async_remove(entry.entry_id),
159  "Remove rainbird config entry",
160  )
161  return False
162 
163  _LOGGER.debug("Updating unique id to %s", new_unique_id)
164  hass.config_entries.async_update_entry(
165  entry,
166  unique_id=new_unique_id,
167  data={
168  **entry.data,
169  CONF_MAC: mac_address,
170  },
171  )
172  return True
173 
174 
176  hass: HomeAssistant,
177  entity_registry: er.EntityRegistry,
178  config_entry_id: str,
179  mac_address: str,
180  serial_number: str,
181 ) -> None:
182  """Migrate existing entity if current one can't be found and an old one exists."""
183  entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id)
184  for entity_entry in entity_entries:
185  unique_id = str(entity_entry.unique_id)
186  if unique_id.startswith(mac_address):
187  continue
188  if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id:
189  new_unique_id = f"{mac_address}{suffix}"
190  _LOGGER.debug("Updating unique id from %s to %s", unique_id, new_unique_id)
191  entity_registry.async_update_entity(
192  entity_entry.entity_id, new_unique_id=new_unique_id
193  )
194 
195 
197  old_entry: dr.DeviceEntry, new_entry: dr.DeviceEntry
198 ) -> dr.DeviceEntry:
199  """Determine which device entry to keep when there are duplicates.
200 
201  As we transitioned to new unique ids, we did not update existing device entries
202  and as a result there are devices with both the old and new unique id format. We
203  have to pick which one to keep, and preferably this can repair things if the
204  user previously renamed devices.
205  """
206  # Prefer the new device if the user already gave it a name or area. Otherwise,
207  # do the same for the old entry. If no entries have been modified then keep the new one.
208  if new_entry.disabled_by is None and (
209  new_entry.area_id is not None or new_entry.name_by_user is not None
210  ):
211  return new_entry
212  if old_entry.disabled_by is None and (
213  old_entry.area_id is not None or old_entry.name_by_user is not None
214  ):
215  return old_entry
216  return new_entry if new_entry.disabled_by is None else old_entry
217 
218 
220  hass: HomeAssistant,
221  device_registry: dr.DeviceRegistry,
222  config_entry_id: str,
223  mac_address: str,
224  serial_number: str,
225 ) -> None:
226  """Migrate existing device identifiers to the new format.
227 
228  This will rename any device ids that are prefixed with the serial number to be prefixed
229  with the mac address. This also cleans up from a bug that allowed devices to exist
230  in both the old and new format.
231  """
232  device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id)
233  device_entry_map = {}
234  migrations = {}
235  for device_entry in device_entries:
236  unique_id = str(next(iter(device_entry.identifiers))[1])
237  device_entry_map[unique_id] = device_entry
238  if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id:
239  migrations[unique_id] = f"{mac_address}{suffix}"
240 
241  for unique_id, new_unique_id in migrations.items():
242  old_entry = device_entry_map[unique_id]
243  if (new_entry := device_entry_map.get(new_unique_id)) is not None:
244  # Device entries exist for both the old and new format and one must be removed
245  entry_to_keep = _async_device_entry_to_keep(old_entry, new_entry)
246  if entry_to_keep == new_entry:
247  _LOGGER.debug("Removing device entry %s", unique_id)
248  device_registry.async_remove_device(old_entry.id)
249  continue
250  # Remove new entry and update old entry to new id below
251  _LOGGER.debug("Removing device entry %s", new_unique_id)
252  device_registry.async_remove_device(new_entry.id)
253 
254  _LOGGER.debug("Updating device id from %s to %s", unique_id, new_unique_id)
255  device_registry.async_update_device(
256  old_entry.id, new_identifiers={(DOMAIN, new_unique_id)}
257  )
258 
259 
260 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
261  """Unload a config entry."""
262  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
aiohttp.ClientSession async_create_clientsession()
Definition: coordinator.py:51
None _async_register_clientsession_shutdown(HomeAssistant hass, ConfigEntry entry, aiohttp.ClientSession clientsession)
Definition: __init__.py:51
None _async_fix_device_id(HomeAssistant hass, dr.DeviceRegistry device_registry, str config_entry_id, str mac_address, str serial_number)
Definition: __init__.py:225
bool _async_fix_unique_id(HomeAssistant hass, AsyncRainbirdController controller, ConfigEntry entry)
Definition: __init__.py:132
None _async_fix_entity_unique_id(HomeAssistant hass, er.EntityRegistry entity_registry, str config_entry_id, str mac_address, str serial_number)
Definition: __init__.py:181
bool async_setup_entry(HomeAssistant hass, RainbirdConfigEntry entry)
Definition: __init__.py:65
dr.DeviceEntry _async_device_entry_to_keep(dr.DeviceEntry old_entry, dr.DeviceEntry new_entry)
Definition: __init__.py:198
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:260