From b66a2c4d8b701453034a216e94f6c97b48631f19 Mon Sep 17 00:00:00 2001 From: sirmrak Date: Fri, 3 Jul 2026 15:10:03 +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/motion=5Frecorder=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/motion_recorder/sensor.py | 70 ++++++++++++++ .../motion_recorder/strings.json | 91 +++++++++++++++++++ custom_components/motion_recorder/switch.py | 85 +++++++++++++++++ 3 files changed, 246 insertions(+) create mode 100644 custom_components/motion_recorder/sensor.py create mode 100644 custom_components/motion_recorder/strings.json create mode 100644 custom_components/motion_recorder/switch.py diff --git a/custom_components/motion_recorder/sensor.py b/custom_components/motion_recorder/sensor.py new file mode 100644 index 0000000..7db322c --- /dev/null +++ b/custom_components/motion_recorder/sensor.py @@ -0,0 +1,70 @@ +"""Sensor platform for Motion Recorder.""" +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.device_registry import DeviceInfo + +from .const import DOMAIN, CONF_CAMERA_ENTITY + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Motion Recorder sensor.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([MotionRecorderStatusSensor(coordinator, entry)]) + + +class MotionRecorderStatusSensor(CoordinatorEntity, SensorEntity): + """Representation of a Motion Recorder status sensor.""" + + def __init__(self, coordinator, entry): + """Initialize the sensor.""" + super().__init__(coordinator) + self.entry = entry + self._attr_unique_id = f"{entry.entry_id}_status" + self._attr_name = f"{entry.title} Статус" + self._attr_translation_key = "motion_recorder_status" + + # Добавляем информацию об устройстве + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.title, + manufacturer="Motion Recorder", + model="Camera Recording Integration", + ) + + # Icon mapping + self._icon_map = { + "idle": "mdi:sleep", + "detecting": "mdi:motion-sensor", + "recording": "mdi:record-rec", + "delaying": "mdi:timer-sand", + "finalizing": "mdi:file-check", + "completed": "mdi:check-circle", + "error": "mdi:alert-circle", + "disabled": "mdi:video-off", + } + + @property + def native_value(self): + """Return the state of the sensor.""" + return self.coordinator.state + + @property + def extra_state_attributes(self): + """Return the state attributes.""" + return self.coordinator.attributes + + @property + def icon(self): + """Return the icon.""" + return self._icon_map.get(self.coordinator.state, "mdi:camera") + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() \ No newline at end of file diff --git a/custom_components/motion_recorder/strings.json b/custom_components/motion_recorder/strings.json new file mode 100644 index 0000000..90524c7 --- /dev/null +++ b/custom_components/motion_recorder/strings.json @@ -0,0 +1,91 @@ +{ + "config": { + "step": { + "user": { + "title": "Настройки Motion Recorder", + "description": "Настройка интеграции записи камер по движению", + "data": { + "name": "Имя интеграции", + "camera_entity": "Камера для записи", + "save_path": "Путь сохранения (относительно media)", + "filename_template": "Формат имени файла", + "max_duration": "Максимальная длительность записи (сек)", + "prebuffer": "Предбуфер (сек, 0 = отключено)", + "motion_sensors": "Датчики движения", + "motion_filter": "Фильтр обнаружения движения (сек, 0 = без фильтрации)", + "off_delay": "Задержка после пропадания движения (сек, 0 = без задержки)", + "retention_days": "Хранить записи (дней, 0 = не удалять)", + "force_stop_sensor": "Датчик принудительной остановки (binary_sensor, switch, input_boolean)", + "force_stop_state": "Состояние для остановки", + "log_level": "Уровень логирования" + }, + "data_description": { + "name": "Имя, которое будет отображаться в интеграции", + "camera_entity": "Выберите камеру из списка доступных", + "save_path": "Папка для сохранения записей относительно /media. Поддерживаются переменные, например camera_name", + "filename_template": "Выберите формат даты и времени для имён файлов записей. Все форматы содержат секунды, чтобы избежать перезаписи файлов при нескольких срабатываниях в одну минуту", + "max_duration": "Максимальное время одной записии (10-3600 секунд). Если движение не прекратится, запись остановится по таймауту", + "prebuffer": "Сколько секунд до обнаружения движения включить в запись. Работает только если стрим уже активен", + "motion_sensors": "Можно выбрать несколько датчиков. Запись начнётся при сработке любого из них", + "motion_filter": "Задержка перед началом записи. Если движение пропадёт раньше — запись не начнётся (защита от ложных срабатываний)", + "off_delay": "Сколько секунд ждать после пропадания движения перед остановкой записи. Если движение вернётся — таймер сбросится", + "retention_days": "Автоматически удалять записи старше указанного количества дней. 0 = не удалять никогда. Очистка выполняется ежедневно в 3:00", + "force_stop_sensor": "Необязательно. Сущность, при переходе в указанное состояние запись немедленно остановится", + "force_stop_state": "Значение состояния датчика принудительной остановки (обычно 'off' или 'on')", + "log_level": "Уровень детализации логов. DEBUG — для отладки, INFO — обычный режим" + } + } + }, + "abort": { + "single_instance_allowed": "Разрешена только одна конфигурация на камеру" + } + }, + "options": { + "step": { + "init": { + "title": "Настройки Motion Recorder", + "description": "Изменение параметров записи", + "data": { + "filename_template": "Формат имени файла", + "max_duration": "Максимальная длительность записи (сек)", + "prebuffer": "Предбуфер (сек, 0 = отключено)", + "motion_filter": "Фильтр обнаружения движения (сек, 0 = без фильтрации)", + "off_delay": "Задержка после пропадания движения (сек, 0 = без задержки)", + "retention_days": "Хранить записи (дней, 0 = не удалять)", + "log_level": "Уровень логирования" + }, + "data_description": { + "filename_template": "Выберите формат даты и времени для имён файлов записей", + "max_duration": "Максимальное время одной записии (10-3600 секунд)", + "prebuffer": "Сколько секунд до обнаружения движения включить в запись", + "motion_filter": "Задержка перед началом записи для защиты от ложных срабатываний", + "off_delay": "Сколько секунд ждать после пропадания движения перед остановкой записи", + "retention_days": "Автоматически удалять записи старше указанного количества дней. 0 = не удалять никогда", + "log_level": "Уровень детализации логов" + } + } + } + }, + "entity": { + "sensor": { + "motion_recorder_status": { + "name": "Статус", + "state": { + "idle": "Ожидание", + "detecting": "Обнаружение", + "recording": "Запись", + "delaying": "Задержка", + "finalizing": "Завершение", + "completed": "Завершено", + "error": "Ошибка", + "disabled": "Отключено" + } + } + }, + "switch": { + "motion_recorder_enabled": { + "name": "Запись включена" + } + } + } +} \ No newline at end of file diff --git a/custom_components/motion_recorder/switch.py b/custom_components/motion_recorder/switch.py new file mode 100644 index 0000000..215039f --- /dev/null +++ b/custom_components/motion_recorder/switch.py @@ -0,0 +1,85 @@ +"""Switch platform for Motion Recorder.""" +import logging +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_ON +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.device_registry import DeviceInfo + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Motion Recorder switch.""" + coordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities([MotionRecorderEnabledSwitch(coordinator, entry)]) + + +class MotionRecorderEnabledSwitch(CoordinatorEntity, SwitchEntity, RestoreEntity): + """Representation of a Motion Recorder enabled switch.""" + + def __init__(self, coordinator, entry): + """Initialize the switch.""" + super().__init__(coordinator) + self.entry = entry + self._attr_unique_id = f"{entry.entry_id}_enabled" + self._attr_name = f"{entry.title} Запись" + self._attr_translation_key = "motion_recorder_enabled" + self._attr_icon = "mdi:video" + + # Добавляем информацию об устройстве для привязки к Device Registry + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, entry.entry_id)}, + name=entry.title, + manufacturer="Motion Recorder", + model="Camera Recording Integration", + ) + + @property + def is_on(self) -> bool: + """Return True if recording is enabled.""" + return self.coordinator.is_enabled + + async def async_turn_on(self, **kwargs) -> None: + """Turn on recording.""" + _LOGGER.info("Switch turned ON, enabling recording") + await self.coordinator.set_enabled(True) + self.async_write_ha_state() # Принудительно обновляем UI + + async def async_turn_off(self, **kwargs) -> None: + """Turn off recording.""" + _LOGGER.info("Switch turned OFF, disabling recording") + await self.coordinator.set_enabled(False) + self.async_write_ha_state() # Принудительно обновляем UI + + async def async_added_to_hass(self) -> None: + """Handle entity which will be added.""" + await super().async_added_to_hass() + + # Восстанавливаем последнее состояние из базы данных HA + last_state = await self.async_get_last_state() + if last_state is not None: + is_enabled = last_state.state == STATE_ON + _LOGGER.info("Restored switch state: %s", last_state.state) + + # Синхронизируем состояние с координатором + if self.coordinator.is_enabled != is_enabled: + await self.coordinator.set_enabled(is_enabled) + + # Принудительно обновляем UI после восстановления + self.async_write_ha_state() + else: + _LOGGER.info("Switch added for the first time, defaulting to enabled") + + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self.async_write_ha_state() \ No newline at end of file