core/homeassistant/components/rainbird/__init__.py

217 lines
7.8 KiB
Python

"""Support for Rain Bird Irrigation system LNK WiFi Module."""
from __future__ import annotations
import logging
from pyrainbird.async_client import AsyncRainbirdClient, AsyncRainbirdController
from pyrainbird.exceptions import RainbirdApiException
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from .const import CONF_SERIAL_NUMBER
from .coordinator import RainbirdData
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.SWITCH,
Platform.SENSOR,
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.CALENDAR,
]
DOMAIN = "rainbird"
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the config entry for Rain Bird."""
hass.data.setdefault(DOMAIN, {})
controller = AsyncRainbirdController(
AsyncRainbirdClient(
async_get_clientsession(hass),
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
)
if not (await _async_fix_unique_id(hass, controller, entry)):
return False
if mac_address := entry.data.get(CONF_MAC):
_async_fix_entity_unique_id(
hass,
er.async_get(hass),
entry.entry_id,
format_mac(mac_address),
str(entry.data[CONF_SERIAL_NUMBER]),
)
_async_fix_device_id(
hass,
dr.async_get(hass),
entry.entry_id,
format_mac(mac_address),
str(entry.data[CONF_SERIAL_NUMBER]),
)
try:
model_info = await controller.get_model_and_version()
except RainbirdApiException as err:
raise ConfigEntryNotReady from err
data = RainbirdData(hass, entry, controller, model_info)
await data.coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = data
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def _async_fix_unique_id(
hass: HomeAssistant, controller: AsyncRainbirdController, entry: ConfigEntry
) -> bool:
"""Update the config entry with a unique id based on the mac address."""
_LOGGER.debug("Checking for migration of config entry (%s)", entry.unique_id)
if not (mac_address := entry.data.get(CONF_MAC)):
try:
wifi_params = await controller.get_wifi_params()
except RainbirdApiException as err:
_LOGGER.warning("Unable to fix missing unique id: %s", err)
return True
if (mac_address := wifi_params.mac_address) is None:
_LOGGER.warning("Unable to fix missing unique id (mac address was None)")
return True
new_unique_id = format_mac(mac_address)
if entry.unique_id == new_unique_id and CONF_MAC in entry.data:
_LOGGER.debug("Config entry already in correct state")
return True
entries = hass.config_entries.async_entries(DOMAIN)
for existing_entry in entries:
if existing_entry.unique_id == new_unique_id:
_LOGGER.warning(
"Unable to fix missing unique id (already exists); Removing duplicate entry"
)
hass.async_create_background_task(
hass.config_entries.async_remove(entry.entry_id),
"Remove rainbird config entry",
)
return False
_LOGGER.debug("Updating unique id to %s", new_unique_id)
hass.config_entries.async_update_entry(
entry,
unique_id=new_unique_id,
data={
**entry.data,
CONF_MAC: mac_address,
},
)
return True
def _async_fix_entity_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
config_entry_id: str,
mac_address: str,
serial_number: str,
) -> None:
"""Migrate existing entity if current one can't be found and an old one exists."""
entity_entries = er.async_entries_for_config_entry(entity_registry, config_entry_id)
for entity_entry in entity_entries:
unique_id = str(entity_entry.unique_id)
if unique_id.startswith(mac_address):
continue
if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id:
new_unique_id = f"{mac_address}{suffix}"
_LOGGER.debug("Updating unique id from %s to %s", unique_id, new_unique_id)
entity_registry.async_update_entity(
entity_entry.entity_id, new_unique_id=new_unique_id
)
def _async_device_entry_to_keep(
old_entry: dr.DeviceEntry, new_entry: dr.DeviceEntry
) -> dr.DeviceEntry:
"""Determine which device entry to keep when there are duplicates.
As we transitioned to new unique ids, we did not update existing device entries
and as a result there are devices with both the old and new unique id format. We
have to pick which one to keep, and preferably this can repair things if the
user previously renamed devices.
"""
# Prefer the new device if the user already gave it a name or area. Otherwise,
# do the same for the old entry. If no entries have been modified then keep the new one.
if new_entry.disabled_by is None and (
new_entry.area_id is not None or new_entry.name_by_user is not None
):
return new_entry
if old_entry.disabled_by is None and (
old_entry.area_id is not None or old_entry.name_by_user is not None
):
return old_entry
return new_entry if new_entry.disabled_by is None else old_entry
def _async_fix_device_id(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
config_entry_id: str,
mac_address: str,
serial_number: str,
) -> None:
"""Migrate existing device identifiers to the new format.
This will rename any device ids that are prefixed with the serial number to be prefixed
with the mac address. This also cleans up from a bug that allowed devices to exist
in both the old and new format.
"""
device_entries = dr.async_entries_for_config_entry(device_registry, config_entry_id)
device_entry_map = {}
migrations = {}
for device_entry in device_entries:
unique_id = str(next(iter(device_entry.identifiers))[1])
device_entry_map[unique_id] = device_entry
if (suffix := unique_id.removeprefix(str(serial_number))) != unique_id:
migrations[unique_id] = f"{mac_address}{suffix}"
for unique_id, new_unique_id in migrations.items():
old_entry = device_entry_map[unique_id]
if (new_entry := device_entry_map.get(new_unique_id)) is not None:
# Device entries exist for both the old and new format and one must be removed
entry_to_keep = _async_device_entry_to_keep(old_entry, new_entry)
if entry_to_keep == new_entry:
_LOGGER.debug("Removing device entry %s", unique_id)
device_registry.async_remove_device(old_entry.id)
continue
# Remove new entry and update old entry to new id below
_LOGGER.debug("Removing device entry %s", new_unique_id)
device_registry.async_remove_device(new_entry.id)
_LOGGER.debug("Updating device id from %s to %s", unique_id, new_unique_id)
device_registry.async_update_device(
old_entry.id, new_identifiers={(DOMAIN, new_unique_id)}
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok