Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a44a996b1 | ||
|
|
e1f1ff89d8 | ||
|
|
75d985f64a | ||
|
|
8b65cb43eb | ||
|
|
02a3d821e3 | ||
|
|
0e07bc8c9d | ||
|
|
8472b91cd1 | ||
|
|
ff5406828c | ||
|
|
c121261f3d | ||
|
|
15a97be8e2 |
@@ -1,3 +1,4 @@
|
|||||||
|
<img src="custom_components/rtkey/icon.png" width="128"> </img>
|
||||||
# RT Key Integration for Home Assistant
|
# RT Key Integration for Home Assistant
|
||||||
|
|
||||||
[](https://www.home-assistant.io/)
|
[](https://www.home-assistant.io/)
|
||||||
|
|||||||
+461
-377
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
|||||||
import datetime
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.event import async_track_time_interval
|
|
||||||
from homeassistant.helpers.entity import EntityCategory
|
from homeassistant.helpers.entity import EntityCategory
|
||||||
from . import _LOGGER, DOMAIN, RTKeyCamerasApi, TOKEN_REFRESH_BUFFER
|
|
||||||
|
from . import _LOGGER, DOMAIN, RTKeyCamerasApi
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -17,13 +17,12 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
cameras_api: RTKeyCamerasApi = hass.data[DOMAIN][config_entry.entry_id]["cameras_api"]
|
cameras_api: RTKeyCamerasApi = hass.data[DOMAIN][config_entry.entry_id]["cameras_api"]
|
||||||
cameras_info = await cameras_api.get_cameras_info()
|
cameras_info = await cameras_api.get_cameras_info()
|
||||||
|
|
||||||
entities = [
|
entities = [
|
||||||
RTKeyCamera(hass, config_entry, cameras_api, camera_info)
|
RTKeyCamera(hass, config_entry, cameras_api, camera_info)
|
||||||
for camera_info in cameras_info["data"]["items"]
|
for camera_info in cameras_info["data"]["items"]
|
||||||
]
|
]
|
||||||
|
|
||||||
# 🆕 Добавляем архивные камеры только если archive_copies > 0
|
|
||||||
if cameras_api.archive_copies > 0:
|
if cameras_api.archive_copies > 0:
|
||||||
intercoms_info = await cameras_api.get_intercoms_info()
|
intercoms_info = await cameras_api.get_intercoms_info()
|
||||||
for intercom_info in intercoms_info.get("data", {}).get("devices", []):
|
for intercom_info in intercoms_info.get("data", {}).get("devices", []):
|
||||||
@@ -39,6 +38,8 @@ async def async_setup_entry(
|
|||||||
class RTKeyCamera(Camera):
|
class RTKeyCamera(Camera):
|
||||||
"""Live-камера (для всех устройств — и камер, и домофонов)."""
|
"""Live-камера (для всех устройств — и камер, и домофонов)."""
|
||||||
|
|
||||||
|
_attr_supported_features = CameraEntityFeature.STREAM
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -63,38 +64,6 @@ class RTKeyCamera(Camera):
|
|||||||
|
|
||||||
self._attr_unique_id = f"camera_{self.camera_id}"
|
self._attr_unique_id = f"camera_{self.camera_id}"
|
||||||
self._attr_name = self.device_name
|
self._attr_name = self.device_name
|
||||||
self._attr_supported_features = CameraEntityFeature.STREAM
|
|
||||||
|
|
||||||
self._unsub_stream_refresh: Any = None
|
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
|
|
||||||
self._unsub_stream_refresh = async_track_time_interval(
|
|
||||||
self.hass,
|
|
||||||
self._stream_refresh,
|
|
||||||
datetime.timedelta(seconds=TOKEN_REFRESH_BUFFER),
|
|
||||||
)
|
|
||||||
_LOGGER.debug(f"Таймер обновления стрима запущен для {self.device_name}")
|
|
||||||
|
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
|
||||||
if self._unsub_stream_refresh:
|
|
||||||
self._unsub_stream_refresh()
|
|
||||||
self._unsub_stream_refresh = None
|
|
||||||
await super().async_will_remove_from_hass()
|
|
||||||
|
|
||||||
async def _stream_refresh(self, now: datetime.datetime) -> None:
|
|
||||||
try:
|
|
||||||
if self.cameras_api._tokens_invalid:
|
|
||||||
_LOGGER.debug(f"Обнаружены протухшие токены, обновляем для {self.device_name}")
|
|
||||||
await self.cameras_api.refresh_tokens()
|
|
||||||
|
|
||||||
url = await self.stream_source()
|
|
||||||
if url and self.stream and getattr(self.stream, "source", None) != url:
|
|
||||||
_LOGGER.debug(f"Обновляем URL стрима для {self.device_name}")
|
|
||||||
self.stream.update_source(url)
|
|
||||||
except Exception as e:
|
|
||||||
_LOGGER.error(f"Ошибка обновления стрима для {self.camera_id}: {e}")
|
|
||||||
|
|
||||||
async def stream_source(self) -> str | None:
|
async def stream_source(self) -> str | None:
|
||||||
return await self.cameras_api.get_camera_stream_url(self.camera_id)
|
return await self.cameras_api.get_camera_stream_url(self.camera_id)
|
||||||
@@ -113,7 +82,7 @@ class RTKeyCamera(Camera):
|
|||||||
if self.cameras_api.is_camera_available(self.camera_id):
|
if self.cameras_api.is_camera_available(self.camera_id):
|
||||||
status = self._device_status_title
|
status = self._device_status_title
|
||||||
else:
|
else:
|
||||||
status = "❌ Нет связи"
|
status = "Нет связи"
|
||||||
|
|
||||||
attrs = {"Статус": status}
|
attrs = {"Статус": status}
|
||||||
if self._device_model:
|
if self._device_model:
|
||||||
@@ -141,10 +110,11 @@ class RTKeyCamera(Camera):
|
|||||||
|
|
||||||
|
|
||||||
class RTKeyEventCamera(Camera):
|
class RTKeyEventCamera(Camera):
|
||||||
"""Камера для просмотра архивного видео на момент последнего события (локальный файл)."""
|
"""Архив события: проигрывание сохранённого видео."""
|
||||||
|
|
||||||
_attr_should_poll = False
|
_attr_should_poll = False
|
||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
_attr_supported_features = CameraEntityFeature.STREAM
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -168,7 +138,6 @@ class RTKeyEventCamera(Camera):
|
|||||||
|
|
||||||
self._attr_unique_id = f"event_camera_{self.intercom_id}"
|
self._attr_unique_id = f"event_camera_{self.intercom_id}"
|
||||||
self._attr_name = "Архив события"
|
self._attr_name = "Архив события"
|
||||||
self._attr_supported_features = CameraEntityFeature.STREAM
|
|
||||||
|
|
||||||
self._event_description = None
|
self._event_description = None
|
||||||
self._event_time = None
|
self._event_time = None
|
||||||
@@ -198,32 +167,34 @@ class RTKeyEventCamera(Camera):
|
|||||||
|
|
||||||
archive_path = event_info.get("archive_path")
|
archive_path = event_info.get("archive_path")
|
||||||
archive_error = event_info.get("archive_error")
|
archive_error = event_info.get("archive_error")
|
||||||
|
|
||||||
if archive_path:
|
if archive_path:
|
||||||
self._archive_path = archive_path
|
self._archive_path = archive_path
|
||||||
self._archive_error = None
|
self._archive_error = None
|
||||||
self._event_description = event_info.get("description")
|
self._event_description = event_info.get("description")
|
||||||
self._event_time = event_info.get("local_time")
|
self._event_time = event_info.get("local_time")
|
||||||
|
|
||||||
if self.stream:
|
|
||||||
_LOGGER.debug(f"Обновляем путь архива для {self.device_name}")
|
|
||||||
# Для локального файла stream не обновляем через update_source
|
|
||||||
# stream_source() будет вызван при запросе
|
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
elif archive_error:
|
elif archive_error:
|
||||||
self._archive_error = archive_error
|
self._archive_error = archive_error
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def stream_source(self) -> str | None:
|
async def stream_source(self) -> str | None:
|
||||||
"""Возвращаем путь к локальному файлу."""
|
|
||||||
if self._archive_path and Path(self._archive_path).exists():
|
if self._archive_path and Path(self._archive_path).exists():
|
||||||
return f"file://{self._archive_path}"
|
return f"file://{self._archive_path}"
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def async_camera_image(
|
||||||
|
self, width: int | None = None, height: int | None = None
|
||||||
|
) -> bytes | None:
|
||||||
|
event_info = self.cameras_api.last_events.get(self.intercom_id)
|
||||||
|
if event_info and event_info.get("screenshot_path"):
|
||||||
|
path = Path(event_info["screenshot_path"])
|
||||||
|
if path.exists():
|
||||||
|
return await self.hass.async_add_executor_job(path.read_bytes)
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Доступна, если есть локальный файл."""
|
|
||||||
return self._archive_path is not None and Path(self._archive_path).exists()
|
return self._archive_path is not None and Path(self._archive_path).exists()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -247,4 +218,4 @@ class RTKeyEventCamera(Camera):
|
|||||||
"name": self.device_name,
|
"name": self.device_name,
|
||||||
"manufacturer": "RT Key",
|
"manufacturer": "RT Key",
|
||||||
"model": "Домофон",
|
"model": "Домофон",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from . import (
|
|||||||
CONF_EVENT_SENSOR_ENABLED,
|
CONF_EVENT_SENSOR_ENABLED,
|
||||||
CONF_ARCHIVE_PATH,
|
CONF_ARCHIVE_PATH,
|
||||||
CONF_ARCHIVE_COPIES,
|
CONF_ARCHIVE_COPIES,
|
||||||
|
CONF_ARCHIVE_DURATION,
|
||||||
CONF_SCREENSHOT_COPIES,
|
CONF_SCREENSHOT_COPIES,
|
||||||
DATA_SCHEMA,
|
DATA_SCHEMA,
|
||||||
OPTIONS_SCHEMA,
|
OPTIONS_SCHEMA,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"documentation": "https://github.com/sirmrak/RTKey",
|
"documentation": "https://github.com/sirmrak/RTKey",
|
||||||
"issue_tracker": "https://github.com/sirmrak/RTKey/issues",
|
"issue_tracker": "https://github.com/sirmrak/RTKey/issues",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"version": "1.0.1",
|
"version": "1.2.3",
|
||||||
"requirements": ["transliterate", "pyjwt"],
|
"requirements": [],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"codeowners": ["@sirmrak"]
|
"codeowners": ["@sirmrak"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ class RTKeyEventSensor(SensorEntity):
|
|||||||
"Тип события": self._event_data.get("event_type", ""),
|
"Тип события": self._event_data.get("event_type", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
archive_url = self._event_data.get("archive_url")
|
archive_path = self._event_data.get("archive_path")
|
||||||
if archive_url:
|
if archive_path:
|
||||||
attrs["Архив видео"] = archive_url
|
attrs["Архив видео"] = archive_path
|
||||||
|
|
||||||
event = self._event_data.get("event", {})
|
event = self._event_data.get("event", {})
|
||||||
rfid = event.get("rfid")
|
rfid = event.get("rfid")
|
||||||
|
|||||||
@@ -18,13 +18,15 @@
|
|||||||
"event_sensor_enabled": "Create event sensor entity",
|
"event_sensor_enabled": "Create event sensor entity",
|
||||||
"archive_path": "Archive storage path",
|
"archive_path": "Archive storage path",
|
||||||
"archive_copies": "Number of video copies (0 = disabled)",
|
"archive_copies": "Number of video copies (0 = disabled)",
|
||||||
|
"archive_duration": "Archive video duration (sec)",
|
||||||
"screenshot_copies": "Number of screenshot copies (0 = disabled)"
|
"screenshot_copies": "Number of screenshot copies (0 = disabled)"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"token": "Get token via browser developer tools (F12) on key.rt.ru",
|
"token": "Get token via browser developer tools (F12) on key.rt.ru",
|
||||||
"camera_image_refresh_interval": "0 — disable periodic screenshots",
|
"camera_image_refresh_interval": "0 — disable periodic screenshots",
|
||||||
"archive_path": "Relative to /media/ folder (e.g.: RTKey)",
|
"archive_path": "Subfolder name inside HA media folder (e.g.: RTKey)",
|
||||||
"archive_copies": "Maximum number of saved videos per intercom",
|
"archive_copies": "Maximum number of saved videos per intercom",
|
||||||
|
"archive_duration": "Duration of archived video fragment per event",
|
||||||
"screenshot_copies": "Maximum number of saved screenshots per intercom"
|
"screenshot_copies": "Maximum number of saved screenshots per intercom"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,13 +51,15 @@
|
|||||||
"event_sensor_enabled": "Create event sensor entity",
|
"event_sensor_enabled": "Create event sensor entity",
|
||||||
"archive_path": "Archive storage path",
|
"archive_path": "Archive storage path",
|
||||||
"archive_copies": "Number of video copies (0 = disabled)",
|
"archive_copies": "Number of video copies (0 = disabled)",
|
||||||
|
"archive_duration": "Archive video duration (sec)",
|
||||||
"screenshot_copies": "Number of screenshot copies (0 = disabled)"
|
"screenshot_copies": "Number of screenshot copies (0 = disabled)"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"token": "Get token via browser developer tools (F12) on key.rt.ru",
|
"token": "Get token via browser developer tools (F12) on key.rt.ru",
|
||||||
"camera_image_refresh_interval": "0 — disable periodic screenshots",
|
"camera_image_refresh_interval": "0 — disable periodic screenshots",
|
||||||
"archive_path": "Relative to /media/ folder (e.g.: RTKey)",
|
"archive_path": "Subfolder name inside HA media folder (e.g.: RTKey)",
|
||||||
"archive_copies": "Maximum number of saved videos per intercom",
|
"archive_copies": "Maximum number of saved videos per intercom",
|
||||||
|
"archive_duration": "Duration of archived video fragment per event",
|
||||||
"screenshot_copies": "Maximum number of saved screenshots per intercom"
|
"screenshot_copies": "Maximum number of saved screenshots per intercom"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,13 +18,15 @@
|
|||||||
"event_sensor_enabled": "Создавать сенсор последнего события",
|
"event_sensor_enabled": "Создавать сенсор последнего события",
|
||||||
"archive_path": "Путь для сохранения архивов",
|
"archive_path": "Путь для сохранения архивов",
|
||||||
"archive_copies": "Количество копий видео (0 = отключено)",
|
"archive_copies": "Количество копий видео (0 = отключено)",
|
||||||
|
"archive_duration": "Длительность видео архива (сек)",
|
||||||
"screenshot_copies": "Количество копий скриншотов (0 = отключено)"
|
"screenshot_copies": "Количество копий скриншотов (0 = отключено)"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"token": "Получите токен в браузере через инструменты разработчика (F12) на сайте key.rt.ru",
|
"token": "Получите токен в браузере через инструменты разработчика (F12) на сайте key.rt.ru",
|
||||||
"camera_image_refresh_interval": "0 — отключить периодические скриншоты",
|
"camera_image_refresh_interval": "0 — отключить периодические скриншоты",
|
||||||
"archive_path": "Относительно папки /media/ (например: RTKey)",
|
"archive_path": "Имя подпапки внутри медиа-папки HA (например: RTKey)",
|
||||||
"archive_copies": "Максимальное количество сохранённых видео на домофон",
|
"archive_copies": "Максимальное количество сохранённых видео на домофон",
|
||||||
|
"archive_duration": "Длительность фрагмента видео архива на событие",
|
||||||
"screenshot_copies": "Максимальное количество сохранённых скриншотов на домофон"
|
"screenshot_copies": "Максимальное количество сохранённых скриншотов на домофон"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,13 +51,15 @@
|
|||||||
"event_sensor_enabled": "Создавать сенсор последнего события",
|
"event_sensor_enabled": "Создавать сенсор последнего события",
|
||||||
"archive_path": "Путь для сохранения архивов",
|
"archive_path": "Путь для сохранения архивов",
|
||||||
"archive_copies": "Количество копий видео (0 = отключено)",
|
"archive_copies": "Количество копий видео (0 = отключено)",
|
||||||
|
"archive_duration": "Длительность видео архива (сек)",
|
||||||
"screenshot_copies": "Количество копий скриншотов (0 = отключено)"
|
"screenshot_copies": "Количество копий скриншотов (0 = отключено)"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"token": "Получите токен в браузере через инструменты разработчика (F12) на сайте key.rt.ru",
|
"token": "Получите токен в браузере через инструменты разработчика (F12) на сайте key.rt.ru",
|
||||||
"camera_image_refresh_interval": "0 — отключить периодические скриншоты",
|
"camera_image_refresh_interval": "0 — отключить периодические скриншоты",
|
||||||
"archive_path": "Относительно папки /media/ (например: RTKey)",
|
"archive_path": "Имя подпапки внутри медиа-папки HA (например: RTKey)",
|
||||||
"archive_copies": "Максимальное количество сохранённых видео на домофон",
|
"archive_copies": "Максимальное количество сохранённых видео на домофон",
|
||||||
|
"archive_duration": "Длительность фрагмента видео архива на событие",
|
||||||
"screenshot_copies": "Максимальное количество сохранённых скриншотов на домофон"
|
"screenshot_copies": "Максимальное количество сохранённых скриншотов на домофон"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user