diff --git a/custom_components/nuki_ng/nuki.py b/custom_components/nuki_ng/nuki.py index 1a1c236..29dc76c 100644 --- a/custom_components/nuki_ng/nuki.py +++ b/custom_components/nuki_ng/nuki.py @@ -193,6 +193,41 @@ def can_web(self): def can_bridge(self): return True if self.token and self.bridge else False + async def web_get_last_log(self, dev_id: str): + lock_actions_map = { + 1: "unlock", + 2: "lock", + 3: "unlatch", + 4: "lock_n_go", + 5: "lock_n_go_unlatch", + } + device_actions_map = { + 0: lock_actions_map, + 2: { + 1: "activate_rto", + 2: "deactivate_rto", + 3: "electric_strike_actuation", + 6: "activate_continuous_mode", + 7: "deactivate_continuous_mode", + }, + 3: lock_actions_map, + 4: lock_actions_map, + } + device_actions_map[4] = device_actions_map[0] + response = await self.web_async_json( + lambda r, h: r.get(self.web_url(f"/smartlock/{dev_id}/log"), headers=h) + ) + _LOGGER.debug(f"web_get_last_log ({dev_id}): {response}") + for item in response: + actions_map = device_actions_map.get(item.get("deviceType"), 0) + if item.get("action") in actions_map.keys(): + return { + "name": item.get("name"), + "action": actions_map[item["action"]], + "timestamp": item["date"].replace("Z", "+00:00"), + } + return dict() + async def web_get_last_unlock_log(self, dev_id: str): actions_map = { 1: "unlock", @@ -202,8 +237,9 @@ async def web_get_last_unlock_log(self, dev_id: str): response = await self.web_async_json( lambda r, h: r.get(self.web_url(f"/smartlock/{dev_id}/log"), headers=h) ) + _LOGGER.debug(f"web_get_last_unlock_log ({dev_id}): {response}") for item in response: - if item.get("action") in (1, 3, 5): + if item.get("action") in actions_map.keys(): # unlock, unlatch, lock'n'go with unlatch return { "name": item.get("name"), @@ -232,19 +268,20 @@ async def web_list(self): 240: "removed", 255: "unknown", } + lock_state_map = { + 0: "uncalibrated", + 1: "locked", + 2: "unlocking", + 3: "unlocked", + 4: "locking", + 5: "unlatched", + 6: "unlocked (lock 'n' go)", + 7: "unlatching", + 254: "motor blocked", + 255: "undefined", + } device_state_map = { - 0: { - 0: "uncalibrated", - 1: "locked", - 2: "unlocking", - 3: "unlocked", - 4: "locking", - 5: "unlatched", - 6: "unlocked (lock 'n' go)", - 7: "unlatching", - 254: "motor blocked", - 255: "undefined", - }, + 0: lock_state_map, 2: { 0: "untrained", 1: "online", @@ -254,18 +291,8 @@ async def web_list(self): 253: "boot run", 255: "undefined", }, - 4: { - 0: "uncalibrated", - 1: "locked", - 2: "unlocking", - 3: "unlocked", - 4: "locking", - 5: "unlatched", - 6: "unlocked (lock 'n' go)", - 7: "unlatching", - 254: "motor blocked", - 255: "undefined", - }, + 3: lock_state_map, + 4: lock_state_map, } resp = await self.web_async_json( lambda r, h: r.get(self.web_url(f"/smartlock"), headers=h) @@ -425,14 +452,22 @@ def web_id_for_item(item): item["webId"] = web_id try: item["web_auth"] = await self.api.web_list_all_auths(web_id) - except HomeAssistantError: + except HomeAssistantError as err: + _LOGGER.warning("Despite being configured, Web API request has failed") + _LOGGER.exception(f"Error while fetching auth: {err}") + item["web_auth"] = self.device_data(dev_id).get("web_auth", {}) + try: + item["last_unlock_log"] = await self.api.web_get_last_unlock_log(web_id) + except HomeAssistantError as err: _LOGGER.warning("Despite being configured, Web API request has failed") - _LOGGER.exception("Error while fetching auth:") + _LOGGER.exception(f"Error while fetching last unlock log entry: {err}") + item["last_unlock_log"] = self.device_data(dev_id).get("last_unlock_log", {}) try: - item["last_log"] = await self.api.web_get_last_unlock_log(web_id) - except HomeAssistantError: + item["last_log"] = await self.api.web_get_last_log(web_id) + except HomeAssistantError as err: _LOGGER.warning("Despite being configured, Web API request has failed") - _LOGGER.exception("Error while fetching last log entry") + _LOGGER.exception(f"Error while fetching last log entry: {err}") + item["last_log"] = self.device_data(dev_id).get("last_log", {}) if web_list: item["config"] = web_list.get(web_id, {}).get("config") item["advancedConfig"] = web_list.get(web_id, {}).get("advancedConfig") @@ -507,7 +542,7 @@ def info_data(self): return self.data.get("info", {}) def is_lock(self, dev_id: str) -> bool: - return self.device_data(dev_id).get("deviceType") in (0, 4) + return self.device_data(dev_id).get("deviceType") in (0, 3, 4) def is_opener(self, dev_id: str) -> bool: return self.device_data(dev_id).get("deviceType") == 2 diff --git a/custom_components/nuki_ng/sensor.py b/custom_components/nuki_ng/sensor.py index 9052a7c..be1638b 100644 --- a/custom_components/nuki_ng/sensor.py +++ b/custom_components/nuki_ng/sensor.py @@ -30,6 +30,8 @@ async def async_setup_entry(hass, entry, async_add_entities): entities.append(DoorSensorState(coordinator, dev_id)) entities.append(DoorSecurityState(coordinator, dev_id)) if coordinator.info_field(dev_id, None, "last_log"): + entities.append(LastLog(coordinator, dev_id)) + if coordinator.info_field(dev_id, None, "last_unlock_log"): entities.append(LastUnlockUser(coordinator, dev_id)) async_add_entities(entities) @@ -199,6 +201,30 @@ def state(self): def entity_category(self): return EntityCategory.DIAGNOSTIC +class LastLog(NukiEntity, SensorEntity): + + def __init__(self, coordinator, device_id): + super().__init__(coordinator, device_id) + self.set_id("sensor", "last_log") + self.set_name("Last Log") + self._attr_icon = "mdi:history" + + @property + def state(self): + return self.coordinator.info_field(self.device_id, "Unknown", "last_log", "name") + + @property + def extra_state_attributes(self): + timestamp = self.coordinator.info_field(self.device_id, None, "last_log", "timestamp") + action = self.coordinator.info_field(self.device_id, "unknown", "last_log", "action") + return { + "timestamp": datetime.fromisoformat(timestamp) if isinstance(timestamp, str) else None, + "action": action, + } + + @property + def entity_category(self): + return EntityCategory.DIAGNOSTIC class LastUnlockUser(NukiEntity, SensorEntity): @@ -210,12 +236,12 @@ def __init__(self, coordinator, device_id): @property def state(self): - return self.coordinator.info_field(self.device_id, "Unknown", "last_log", "name") + return self.coordinator.info_field(self.device_id, "Unknown", "last_unlock_log", "name") @property def extra_state_attributes(self): - timestamp = self.coordinator.info_field(self.device_id, None, "last_log", "timestamp") - action = self.coordinator.info_field(self.device_id, "unknown", "last_log", "action") + timestamp = self.coordinator.info_field(self.device_id, None, "last_unlock_log", "timestamp") + action = self.coordinator.info_field(self.device_id, "unknown", "last_unlock_log", "action") return { "timestamp": datetime.fromisoformat(timestamp) if isinstance(timestamp, str) else None, "action": action,