diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index 4598d43b4dc..20309339451 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -13,7 +13,7 @@ from homeassistant import exceptions from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er +from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -41,37 +41,6 @@ def _get_bridge_devices(bridge: NukiBridge) -> tuple[list[NukiLock], list[NukiOp return bridge.locks, bridge.openers -def _update_devices(devices: list[NukiDevice]) -> dict[str, set[str]]: - """ - Update the Nuki devices. - - Returns: - A dict with the events to be fired. The event type is the key and the device ids are the value - """ - - events: dict[str, set[str]] = defaultdict(set) - - for device in devices: - for level in (False, True): - try: - if isinstance(device, NukiOpener): - last_ring_action_state = device.ring_action_state - - device.update(level) - - if not last_ring_action_state and device.ring_action_state: - events["ring"].add(device.nuki_id) - else: - device.update(level) - except RequestException: - continue - - if device.state not in ERROR_STATES: - break - - return events - - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up the Nuki entry.""" @@ -101,42 +70,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except RequestException as err: raise exceptions.ConfigEntryNotReady from err - async def async_update_data() -> None: - """Fetch data from Nuki bridge.""" - try: - # Note: asyncio.TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with async_timeout.timeout(10): - events = await hass.async_add_executor_job( - _update_devices, locks + openers - ) - except InvalidCredentialsException as err: - raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err - except RequestException as err: - raise UpdateFailed(f"Error communicating with Bridge: {err}") from err - - ent_reg = er.async_get(hass) - for event, device_ids in events.items(): - for device_id in device_ids: - entity_id = ent_reg.async_get_entity_id( - Platform.LOCK, DOMAIN, device_id - ) - event_data = { - "entity_id": entity_id, - "type": event, - } - hass.bus.async_fire("nuki_event", event_data) - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - # Name of the data. For logging purposes. - name="nuki devices", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=UPDATE_INTERVAL, + # Device registration for the bridge + info = bridge.info() + bridge_id = parse_id(info["ids"]["hardwareId"]) + dev_reg = device_registry.async_get(hass) + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, bridge_id)}, + manufacturer="Nuki Home Solutions GmbH", + name=f"Nuki Bridge {bridge_id}", + model="Hardware Bridge", + sw_version=info["versions"]["firmwareVersion"], ) + coordinator = NukiCoordinator(hass, bridge, locks, openers) + hass.data[DOMAIN][entry.entry_id] = { DATA_COORDINATOR: coordinator, DATA_BRIDGE: bridge, @@ -178,3 +126,94 @@ class NukiEntity(CoordinatorEntity[DataUpdateCoordinator[None]]): """Pass coordinator to CoordinatorEntity.""" super().__init__(coordinator) self._nuki_device = nuki_device + + @property + def device_info(self): + """Device info for Nuki entities.""" + return { + "identifiers": {(DOMAIN, parse_id(self._nuki_device.nuki_id))}, + "name": self._nuki_device.name, + "manufacturer": "Nuki Home Solutions GmbH", + "model": self._nuki_device.device_type_str.capitalize(), + "sw_version": self._nuki_device.firmware_version, + "via_device": (DOMAIN, self.coordinator.bridge_id), + } + + +class NukiCoordinator(DataUpdateCoordinator): + """Data Update Coordinator for the Nuki integration.""" + + def __init__(self, hass, bridge, locks, openers): + """Initialize my coordinator.""" + super().__init__( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="nuki devices", + # Polling interval. Will only be polled if there are subscribers. + update_interval=UPDATE_INTERVAL, + ) + self.bridge = bridge + self.locks = locks + self.openers = openers + + @property + def bridge_id(self): + """Return the parsed id of the Nuki bridge.""" + return parse_id(self.bridge.info()["ids"]["hardwareId"]) + + async def _async_update_data(self) -> None: + """Fetch data from Nuki bridge.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(10): + events = await self.hass.async_add_executor_job( + self.update_devices, self.locks + self.openers + ) + except InvalidCredentialsException as err: + raise UpdateFailed(f"Invalid credentials for Bridge: {err}") from err + except RequestException as err: + raise UpdateFailed(f"Error communicating with Bridge: {err}") from err + + ent_reg = entity_registry.async_get(self.hass) + for event, device_ids in events.items(): + for device_id in device_ids: + entity_id = ent_reg.async_get_entity_id( + Platform.LOCK, DOMAIN, device_id + ) + event_data = { + "entity_id": entity_id, + "type": event, + } + self.hass.bus.async_fire("nuki_event", event_data) + + def update_devices(self, devices: list[NukiDevice]) -> dict[str, set[str]]: + """ + Update the Nuki devices. + + Returns: + A dict with the events to be fired. The event type is the key and the device ids are the value + """ + + events: dict[str, set[str]] = defaultdict(set) + + for device in devices: + for level in (False, True): + try: + if isinstance(device, NukiOpener): + last_ring_action_state = device.ring_action_state + + device.update(level) + + if not last_ring_action_state and device.ring_action_state: + events["ring"].add(device.nuki_id) + else: + device.update(level) + except RequestException: + continue + + if device.state not in ERROR_STATES: + break + + return events diff --git a/homeassistant/components/nuki/binary_sensor.py b/homeassistant/components/nuki/binary_sensor.py index 6c03cef3664..ae861609e1a 100644 --- a/homeassistant/components/nuki/binary_sensor.py +++ b/homeassistant/components/nuki/binary_sensor.py @@ -36,11 +36,6 @@ class NukiDoorsensorEntity(NukiEntity, BinarySensorEntity): _attr_device_class = BinarySensorDeviceClass.DOOR - @property - def name(self): - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/homeassistant/components/nuki/lock.py b/homeassistant/components/nuki/lock.py index 4a8643e77aa..87cc6f40846 100644 --- a/homeassistant/components/nuki/lock.py +++ b/homeassistant/components/nuki/lock.py @@ -69,11 +69,6 @@ class NukiDeviceEntity(NukiEntity, LockEntity, ABC): _attr_supported_features = LockEntityFeature.OPEN - @property - def name(self) -> str | None: - """Return the name of the lock.""" - return self._nuki_device.name - @property def unique_id(self) -> str | None: """Return a unique ID."""