From fbd4e61fcdd584e4983956cbafaf10a562e3b81d Mon Sep 17 00:00:00 2001 From: sirmrak Date: Sun, 28 Jun 2026 13:41:32 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?custom=5Fcomponents/rtkey=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/rtkey/image.py | 243 ++++++++++++++++++++++++++ custom_components/rtkey/manifest.json | 12 ++ custom_components/rtkey/sensor.py | 122 +++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 custom_components/rtkey/image.py create mode 100644 custom_components/rtkey/manifest.json create mode 100644 custom_components/rtkey/sensor.py diff --git a/custom_components/rtkey/image.py b/custom_components/rtkey/image.py new file mode 100644 index 0000000..d9f90a9 --- /dev/null +++ b/custom_components/rtkey/image.py @@ -0,0 +1,243 @@ +from datetime import datetime, timedelta +from typing import Any + +from homeassistant.components.image import ImageEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.entity import EntityCategory +from zoneinfo import ZoneInfo + +from . import CONF_CAMERA_IMAGE_REFRESH_INTERVAL, DOMAIN, RTKeyCamerasApi, _LOGGER + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Any, +) -> None: + cameras_api: RTKeyCamerasApi = hass.data[DOMAIN][config_entry.entry_id]["cameras_api"] + cameras_info = await cameras_api.get_cameras_info() + + # Проверяем, включены ли обычные скриншоты + interval = config_entry.options.get(CONF_CAMERA_IMAGE_REFRESH_INTERVAL, 60) + + entities = [] + + # Обычные скриншоты (если interval > 0) + if interval > 0: + entities.extend([ + RTKeyCameraImageEntity(hass, config_entry, cameras_api, camera_info) + for camera_info in cameras_info["data"]["items"] + ]) + else: + _LOGGER.info("Обычные скриншоты отключены (interval=0)") + + # Скриншоты событий + if cameras_api.event_screenshot_enabled: + intercoms_info = await cameras_api.get_intercoms_info() + for intercom_info in intercoms_info.get("data", {}).get("devices", []): + entities.append( + RTKeyEventImage(hass, config_entry, cameras_api, intercom_info) + ) + else: + _LOGGER.info("Скриншоты событий отключены") + + async_add_entities(entities) + + +class RTKeyCameraImageEntity(ImageEntity): + """Периодический скриншот камеры.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + cameras_api: RTKeyCamerasApi, + camera_info: dict, + ) -> None: + super().__init__(hass) + + self.hass = hass + self.cameras_api = cameras_api + self.camera_id = camera_info["id"] + + self.device_name = cameras_api.build_device_name(camera_info.get("title", "Устройство")) + + self._device_model = camera_info.get("model", "") + self._device_vendor = camera_info.get("vendor", "") + self._device_serial = camera_info.get("serial_number", "") + self._device_status_title = camera_info.get("status_title", "Неизвестно") + + self._attr_unique_id = f"image_{self.camera_id}" + self._attr_name = "Скриншот" + + self.camera_image_refresh_interval = config_entry.options.get( + CONF_CAMERA_IMAGE_REFRESH_INTERVAL, 60 + ) + + self._attr_image_last_updated = datetime.now() + self._cached_image: bytes | None = None + self._unsub_timer: Any = None + + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + + self._unsub_timer = async_track_time_interval( + self.hass, + self._async_update_image, + timedelta(seconds=self.camera_image_refresh_interval), + ) + await self._async_update_image() + + async def async_will_remove_from_hass(self) -> None: + if self._unsub_timer: + self._unsub_timer() + await super().async_will_remove_from_hass() + + async def _async_update_image(self, now: datetime | None = None) -> None: + """Продолжаем опрашивать API даже если камера недоступна, + чтобы узнать, когда она снова появится.""" + try: + image = await self.cameras_api.get_camera_image(self.camera_id) + if image: + self._cached_image = image + self._attr_image_last_updated = datetime.now() + self.async_write_ha_state() + _LOGGER.debug(f"Обновлено изображение: {self.device_name}") + except Exception as e: + _LOGGER.error(f"Ошибка обновления изображения {self.device_name}: {e}") + + async def async_image(self) -> bytes | None: + return self._cached_image + + @property + def available(self) -> bool: + """Сущность доступна, если камера не в списке недоступных.""" + return self.cameras_api.is_camera_available(self.camera_id) + + @property + def extra_state_attributes(self) -> dict[str, Any]: + """Атрибуты с динамической проверкой доступности камеры.""" + if self.cameras_api.is_camera_available(self.camera_id): + status = self._device_status_title + else: + status = "❌ Нет связи" + + attrs = {"Статус": status} + if self._device_model: + attrs["Модель"] = self._device_model + if self._device_serial: + attrs["Серийный номер"] = self._device_serial + return attrs + + @property + def device_info(self) -> DeviceInfo: + info = { + "identifiers": {(DOMAIN, self.camera_id)}, + "name": self.device_name, + "manufacturer": self._device_vendor or "RT Key", + } + if self._device_model: + info["model"] = self._device_model + if self._device_serial: + info["serial_number"] = self._device_serial + return info + + +class RTKeyEventImage(ImageEntity): + """Скриншот на момент последнего события домофона.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + cameras_api: RTKeyCamerasApi, + intercom_info: dict, + ) -> None: + super().__init__(hass) + + self.hass = hass + self.cameras_api = cameras_api + self.intercom_id = str(intercom_info["id"]) + self.camera_id = intercom_info.get("camera_id") + + self.device_name = cameras_api.build_device_name( + intercom_info.get("name_by_user") + or intercom_info.get("description") + or intercom_info.get("name_by_company", "Домофон") + ) + + self._attr_unique_id = f"event_image_{self.intercom_id}" + self._attr_name = "Скриншот события" + + self._attr_image_last_updated = None + self._cached_image: bytes | None = None + self._event_description = None + self._event_time = None + + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + self.cameras_api.register_event_listener(self._handle_event_update) + + if self.intercom_id in self.cameras_api.last_events: + self._update_from_event() + + async def async_will_remove_from_hass(self) -> None: + self.cameras_api.unregister_event_listener(self._handle_event_update) + await super().async_will_remove_from_hass() + + @callback + def _handle_event_update(self, intercom_id: str): + if intercom_id == self.intercom_id: + self._update_from_event() + + def _update_from_event(self): + event_info = self.cameras_api.last_events.get(self.intercom_id) + if not event_info: + return + + screenshot_data = event_info.get("screenshot_data") + if screenshot_data: + self._cached_image = screenshot_data + self._event_description = event_info.get("description") + self._event_time = event_info.get("local_time") + + # ✅ Используем время события, а не текущее время + event_timestamp = event_info.get("timestamp") + if event_timestamp: + self._attr_image_last_updated = datetime.fromtimestamp( + event_timestamp, + tz=ZoneInfo(self.hass.config.time_zone), + ) + else: + self._attr_image_last_updated = datetime.now() + + self.async_write_ha_state() + _LOGGER.debug(f"Обновлён скриншот события для {self.device_name}") + + async def async_image(self) -> bytes | None: + return self._cached_image + + @property + def extra_state_attributes(self) -> dict[str, Any]: + attrs = {} + if self._event_description: + attrs["Событие"] = self._event_description + if self._event_time: + attrs["Время"] = self._event_time + return attrs + + @property + def device_info(self) -> DeviceInfo: + device_id = self.camera_id or self.intercom_id + return { + "identifiers": {(DOMAIN, device_id)}, + "name": self.device_name, + "manufacturer": "RT Key", + "model": "Домофон", + } \ No newline at end of file diff --git a/custom_components/rtkey/manifest.json b/custom_components/rtkey/manifest.json new file mode 100644 index 0000000..ecb436f --- /dev/null +++ b/custom_components/rtkey/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "rtkey", + "name": "RT Key", + "documentation": "https://github.com/sirmrak/RTKey", + "issue_tracker": "https://github.com/sirmrak/RTKey/issues", + "iot_class": "cloud_polling", + "version": "1.0.1", + "requirements": ["transliterate", "pyjwt"], + "config_flow": true, + "codeowners": ["@sirmrak"] +} + diff --git a/custom_components/rtkey/sensor.py b/custom_components/rtkey/sensor.py new file mode 100644 index 0000000..f27759f --- /dev/null +++ b/custom_components/rtkey/sensor.py @@ -0,0 +1,122 @@ +from typing import Any + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity import EntityCategory + +from . import DOMAIN, RTKeyCamerasApi, _LOGGER + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: Any, +) -> None: + cameras_api: RTKeyCamerasApi = hass.data[DOMAIN][config_entry.entry_id]["cameras_api"] + + if not cameras_api.event_sensor_enabled: + _LOGGER.info("Сенсоры событий отключены") + return + + intercoms_info = await cameras_api.get_intercoms_info() + + entities = [ + RTKeyEventSensor(hass, config_entry, cameras_api, intercom_info) + for intercom_info in intercoms_info.get("data", {}).get("devices", []) + ] + async_add_entities(entities) + + +class RTKeyEventSensor(SensorEntity): + """Сенсор последнего события домофона.""" + + _attr_should_poll = False + _attr_entity_category = EntityCategory.DIAGNOSTIC + + def __init__( + self, + hass: HomeAssistant, + config_entry: ConfigEntry, + cameras_api: RTKeyCamerasApi, + intercom_info: dict, + ) -> None: + self.hass = hass + self.cameras_api = cameras_api + self.intercom_id = str(intercom_info["id"]) + self.camera_id = intercom_info.get("camera_id") + + self.device_name = cameras_api.build_device_name( + intercom_info.get("name_by_user") or + intercom_info.get("description") or + intercom_info.get("name_by_company", "Домофон") + ) + + self._attr_unique_id = f"event_sensor_{self.intercom_id}" + self._attr_name = "Последнее событие" + self._attr_icon = "mdi:bell-ring" + + self._attr_native_value = "Нет событий" + self._event_data = None + + async def async_added_to_hass(self) -> None: + await super().async_added_to_hass() + self.cameras_api.register_event_listener(self._handle_event_update) + + if self.intercom_id in self.cameras_api.last_events: + self._update_from_event() + + async def async_will_remove_from_hass(self) -> None: + self.cameras_api.unregister_event_listener(self._handle_event_update) + await super().async_will_remove_from_hass() + + @callback + def _handle_event_update(self, intercom_id: str): + if intercom_id == self.intercom_id: + self._update_from_event() + + def _update_from_event(self): + event_info = self.cameras_api.last_events.get(self.intercom_id) + if not event_info: + return + + new_value = event_info["description"] + + # ✅ Обновляем только если значение изменилось + if self._attr_native_value != new_value: + self._event_data = event_info + self._attr_native_value = new_value + self.async_write_ha_state() + _LOGGER.debug(f"Обновлён сенсор события для {self.device_name}: {self._attr_native_value}") + + @property + def extra_state_attributes(self) -> dict[str, Any]: + if not self._event_data: + return {} + + attrs = { + "Время": self._event_data.get("local_time", ""), + "Тип события": self._event_data.get("event_type", ""), + } + + archive_url = self._event_data.get("archive_url") + if archive_url: + attrs["Архив видео"] = archive_url + + event = self._event_data.get("event", {}) + rfid = event.get("rfid") + if rfid: + attrs["RFID"] = rfid + + return attrs + + @property + def device_info(self) -> DeviceInfo: + device_id = self.camera_id or self.intercom_id + return { + "identifiers": {(DOMAIN, device_id)}, + "name": self.device_name, + "manufacturer": "RT Key", + "model": "Домофон", + } \ No newline at end of file