19 Commits
Author SHA1 Message Date
semantic-release 495fbe5dce chore(release): v1.1.1
Update Version / Update Version (push) Successful in 5s
2026-07-10 09:25:11 +00:00
sirmrak 6ccca63270 fix: Оформление
Update Version / Update Version (push) Successful in 7s
2026-07-10 12:25:05 +03:00
sirmrak f9cbcfe7f6 Добавить .github/workflows/release.yml
Update Version / Update Version (push) Successful in 5s
2026-07-10 12:14:02 +03:00
sirmrak cdba65f5f7 Добавить .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 5s
2026-07-10 12:12:55 +03:00
sirmrak c817a47e30 Добавить pyproject.toml 2026-07-10 12:11:38 +03:00
sirmrak 96ff3d4285 Обновить README.md 2026-07-08 13:29:45 +03:00
sirmrak 457ff1fa02 Обновить README.ru.md 2026-07-08 13:29:21 +03:00
sirmrak 33c170ca15 Обновить README.ru.md 2026-07-05 13:50:19 +03:00
sirmrak 2c473b157c Обновить README.md 2026-07-05 13:46:43 +03:00
sirmrak a1b63d2ee1 Обновить README.md 2026-07-05 13:46:11 +03:00
sirmrak 92ecd51e2e Обновить custom_components/motion_recorder/translations/ru.json 2026-07-05 13:36:31 +03:00
sirmrak e60a47ef48 Обновить custom_components/motion_recorder/translations/en.json 2026-07-05 13:36:10 +03:00
sirmrak ab3de76de5 Обновить custom_components/motion_recorder/__init__.py 2026-07-05 13:35:40 +03:00
sirmrak 84127a1229 Обновить custom_components/motion_recorder/config_flow.py 2026-07-05 13:34:29 +03:00
sirmrak 57d2441d3b Обновить custom_components/motion_recorder/strings.json 2026-07-05 13:33:56 +03:00
sirmrak ea1b6e3916 Обновить custom_components/motion_recorder/sensor.py 2026-07-05 13:33:27 +03:00
sirmrak 792a185204 Обновить custom_components/motion_recorder/manifest.json 2026-07-05 13:32:55 +03:00
sirmrak 015e3221df Обновить custom_components/motion_recorder/coordinator.py 2026-07-05 13:32:29 +03:00
sirmrak 02cb16325c Обновить custom_components/motion_recorder/const.py 2026-07-05 13:31:50 +03:00
15 changed files with 1669 additions and 1279 deletions
+69
View File
@@ -0,0 +1,69 @@
name: Update Version
on:
push:
branches:
- main
- master
env:
GIT_SSL_NO_VERIFY: true
jobs:
update-version:
name: Update Version
runs-on: ubuntu-latest
if: github.actor != 'gitea-actions[bot]' && github.actor != 'Gitea Actions Bot'
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.PAT_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install python-semantic-release
- name: Configure Git
run: |
git config --local user.email "actions@gitea.local"
git config --local user.name "Gitea Actions Bot"
- name: Run semantic-release
env:
GITEA_TOKEN: ${{ secrets.PAT_TOKEN }}
run: |
echo "🧹 Сбрасываем конфликтующие переменные Gitea Actions..."
unset GITHUB_SERVER_URL
unset GITHUB_API_URL
unset GITHUB_REPOSITORY
echo "🔐 Записываем оригинальный Root CA от mkcert из секретов..."
echo "${{ secrets.LOCAL_ROOT_CA }}" > /tmp/rootCA.crt
# Проверка на случай, если секрет забыли настроить
if [ ! -s /tmp/rootCA.crt ]; then
echo "❌ Ошибка: Секрет LOCAL_ROOT_CA пуст. Проверьте настройки репозитория!"
exit 1
fi
echo "✅ Root CA успешно прочитан."
# Находим, где конкретно Python (библиотека certifi) хранит свои сертификаты
CERTIFI_PATH=$(python3 -c "import certifi; print(certifi.where())")
echo "Путь к bundle Python: $CERTIFI_PATH"
# Внедряем корневой сертификат mkcert в конец доверенного списка Python
cat /tmp/rootCA.crt >> "$CERTIFI_PATH"
echo "✅ Root CA от mkcert добавлен в доверенные для Python!"
echo "🚀 Запускаем semantic-release..."
semantic-release version
+132
View File
@@ -0,0 +1,132 @@
name: Test and Release
on:
push:
tags:
- 'v*'
workflow_dispatch: # Позволяет запускать вручную
jobs:
# Шаг 1: Валидация структуры HA интеграции
validate:
name: Validate Integration
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Hassfest Validation
uses: home-assistant/actions/hassfest@master
- name: HACS Validation
uses: hacs/action@main
with:
category: "integration"
# Шаг 2: Запуск тестов (если есть)
test:
name: Run Tests
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-homeassistant-custom-component
if [ -f requirements_test.txt ]; then pip install -r requirements_test.txt; fi
- name: Run tests
run: |
if [ -d "tests" ]; then
pytest tests/ --cov=custom_components/motion_light --cov-report=xml
else
echo "No tests directory found, skipping tests"
fi
# Шаг 3: Проверка стиля кода
lint:
name: Code Quality
runs-on: ubuntu-latest
needs: validate
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install linters
run: |
pip install black ruff mypy
- name: Check code formatting with black
run: |
black --check custom_components/motion_light/ || echo "Code formatting issues found"
- name: Check code with ruff
run: |
ruff check custom_components/motion_light/ || echo "Ruff issues found"
# Шаг 4: Создание релиза (только если все проверки пройдены)
release:
name: Create Release
runs-on: ubuntu-latest
needs: [validate, test, lint]
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Get tag info
id: tag
run: |
TAG=${GITHUB_REF#refs/tags/}
echo "tag=$TAG" >> $GITHUB_OUTPUT
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
COMMITS=$(git log --pretty=format:"- %s" --reverse)
else
COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s" --reverse)
fi
echo "commits<<EOF" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
COMMITS="${{ steps.tag.outputs.commits }}"
echo "🚀 Создаём релиз $TAG в GitHub..."
cat > /tmp/release_notes.md << EOF
## Изменения в версии $TAG
$COMMITS
EOF
gh release create "$TAG" \
--title "Release $TAG" \
--notes-file /tmp/release_notes.md \
--target main
echo "✅ Релиз создан!"
+18
View File
@@ -0,0 +1,18 @@
# CHANGELOG
<!-- version list -->
## v1.1.1 (2026-07-10)
### Bug Fixes
- Оформление
([`6ccca63`](https://192.168.200.208:3000/sirmrak/motion_recorder/commit/6ccca6327021b53a8cd46e62bb0d740f0a609043))
## v1.1.0 (2026-07-05)
## v1.0.0 (2026-07-03)
- Initial Release
+32 -3
View File
@@ -1,4 +1,4 @@
# Motion Recorder for Home Assistant
# <img src="custom_components/motion_recorder/brand/icon.png" width="64"> </img> Motion Recorder for Home Assistant
English | [Русский](README.ru.md)
## A powerful Home Assistant integration for recording camera streams based on motion detection with advanced filtering and automated management.
@@ -19,6 +19,8 @@ English | [Русский](README.ru.md)
- **Force stop sensor** - Stop recording based on any entity state
- **Enable/disable switch** - Control recording with a toggle
- **Status sensor** - Monitor recording state with beautiful icons
- **Smart device control** - Automatically turn on/off lights or switches based on recording state
- **Auto-cleanup** - Delete old recordings by retention policy (daily at 03:00)
- **Multi-language support** - English and Russian translations
- **Hot reload** - Settings apply immediately without restart
@@ -66,6 +68,8 @@ When adding the integration, you'll configure:
| **Force Stop Sensor** | Entity to force stop recording | - | - |
| **Force Stop State** | State value to trigger stop | `off` | - |
| **Retention Days** | Auto-delete recordings older than | 7 days | 0-365 days |
| **Controlled Devices** | Lights/switches to auto-control | - | - |
| **Control States** | States when devices should be ON | `recording` | Multi-select |
| **Log Level** | Logging verbosity | INFO | - |
### Options (Post-Setup)
@@ -77,6 +81,8 @@ After initial setup, you can modify these settings via **Settings → Devices &
- Motion filter
- Off delay
- Retention days
- Controlled Devices
- Control States
- Log level
Changes apply immediately without restart.
@@ -217,6 +223,29 @@ Files older than the retention period are automatically deleted daily at 03:00.
Cleanup runs in the background and doesn't affect recording performance.
## Smart Device Control
Motion Recorder can automatically control lights and switches based on its current state.
### How It Works
1. Select one or more `light` or `switch` entities in settings
2. Choose which states should trigger the devices (e.g., `recording`, `delaying`)
3. When the integration enters a selected state → devices turn ON
4. When it leaves that state → devices turn OFF (only if integration turned them on)
### Safety Features
- **Non-intrusive**: If you manually turn on a light, the integration won't turn it off
- **Manual override respected**: If you turn off a controlled light during recording, it stays off
- **Shutdown cleanup**: All integration-controlled devices are turned off when HA restarts or integration reloads
### Example Use Cases
- Turn on porch light only while recording is active
- Activate alarm indicator LED during motion detection
- Turn on indoor lighting when camera detects movement at night
## Advanced Features
### Multiple Motion Sensors
@@ -308,6 +337,6 @@ This project is licensed under the MIT License - see the LICENSE file for detail
---
**Version**: 1.0.0
**Last Updated**: 2026-07-03
**Version**: 1.1.0
**Last Updated**: 2026-07-05
**Compatible with**: Home Assistant 2024.6+
+32 -3
View File
@@ -1,4 +1,4 @@
# Motion Recorder для Home Assistant
# <img src="custom_components/motion_recorder/brand/icon.png" width="64"> </img> Motion Recorder для Home Assistant
[English](README.md) | Русский
Мощная интеграция Home Assistant для записи камер по датчику движения с продвинутой фильтрацией и автоматическим управлением.
@@ -19,6 +19,8 @@
- 🔌 **Датчик принудительной остановки** - Остановка записи по состоянию любой сущности
- 🎛️ **Переключатель включения/выключения** - Управление записью одним кликом
- **Сенсор статуса** - Мониторинг состояния записи с красивыми иконками
- 💡 **Умное управление устройствами** - Автоматическое включение/выключение света и переключателей по состоянию записи
- 🗑️ **Автоочистка** - Удаление старых записей по политике хранения (ежедневно в 03:00)
- 🌍 **Многоязычность** - Переводы на русский и английский языки
- **Горячая перезагрузка** - Настройки применяются сразу без перезапуска
@@ -66,6 +68,8 @@
| **Датчик принуд. остановки** | Сущность для принудительной остановки | - | - |
| **Состояние остановки** | Значение состояния для запуска остановки | `off` | - |
| **Дни хранения** | Автоудаление записей старше | 7 дней | 0-365 дней |
| **Управляемые устройства** | Светильники/переключатели для автоуправления | - | - |
| **Состояния включения** | Состояния, при которых устройства включаются | `recording` | Множественный выбор |
| **Уровень логирования** | Детализация логов | INFO | - |
### Опции (после настройки)
@@ -77,6 +81,8 @@
- Фильтр движения
- Задержка остановки
- Дни хранения
- Управляемые устройства
- Состояния включения
- Уровень логирования
Изменения применяются сразу без перезапуска.
@@ -217,6 +223,29 @@ automation:
Очистка работает в фоновом режиме и не влияет на производительность записи.
## Умное управление устройствами
Motion Recorder может автоматически управлять светильниками и переключателями в зависимости от текущего состояния.
### Как это работает
1. Выберите одну или несколько сущностей `light` или `switch` в настройках
2. Отметьте состояния, при которых устройства должны включаться (например, `recording`, `delaying`)
3. При переходе в выбранное состояние → устройства включаются
4. При выходе из состояния → устройства выключаются (только если интеграция их включала)
### Безопасность
- **Не мешает пользователю**: Если вы вручную включили свет, интеграция его не выключит
- **Ручное управление приоритетно**: Если вы выключили управляемый свет во время записи, он останется выключенным
- **Очистка при остановке**: Все устройства, включённые интеграцией, выключаются при перезагрузке HA или перезагрузке интеграции
### Примеры использования
- Включать уличный фонарь только во время активной записи
- Активировать индикатор тревоги при обнаружении движения
- Включать освещение в помещении при детекции движения камерой ночью
## Продвинутые возможности
### Несколько датчиков движения
@@ -308,6 +337,6 @@ logger:
---
**Версия**: 1.0.0
**Последнее обновление**: 2026-07-03
**Версия**: 1.1.0
**Последнее обновление**: 2026-07-05
**Совместимо с**: Home Assistant 2024.6+
+15 -17
View File
@@ -11,25 +11,28 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = ["sensor", "switch"]
def _get_config_value(entry: ConfigEntry, key: str, default=None):
"""Get config value from options first, then from data, then default."""
return entry.options.get(key, entry.data.get(key, default))
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Motion Recorder from a config entry."""
# Устанавливаем уровень логирования для всего домена
log_level = _get_config_value(entry, CONF_LOG_LEVEL, DEFAULT_LOG_LEVEL)
logging.getLogger(f"custom_components.{DOMAIN}").setLevel(getattr(logging, log_level, logging.INFO))
_LOGGER.info("Motion Recorder integration loaded with log level: %s", log_level)
hass.data.setdefault(DOMAIN, {})
coordinator = MotionRecorderCoordinator(hass, entry)
await coordinator.async_setup()
hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Добавляем listener для автоматической перезагрузки при изменении опций
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
@@ -37,21 +40,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
await coordinator.async_shutdown()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update - reload the integration."""
_LOGGER.info("Options updated, reloading Motion Recorder integration")
await hass.config_entries.async_reload(entry.entry_id)
def _get_config_value(entry: ConfigEntry, key: str, default=None):
"""Get config value from options first, then from data, then default."""
return entry.options.get(key, entry.data.get(key, default))
await hass.config_entries.async_reload(entry.entry_id)
+290 -234
View File
@@ -1,235 +1,291 @@
"""Config flow for Motion Recorder."""
from homeassistant import config_entries
from homeassistant.core import callback
import voluptuous as vol
from homeassistant.helpers import selector
from .const import (
DOMAIN,
CONF_CAMERA_ENTITY,
CONF_SAVE_PATH,
CONF_FILENAME_TEMPLATE,
CONF_MAX_DURATION,
CONF_PREBUFFER,
CONF_MOTION_SENSORS,
CONF_MOTION_FILTER,
CONF_OFF_DELAY,
CONF_FORCE_STOP_SENSOR,
CONF_FORCE_STOP_STATE,
CONF_LOG_LEVEL,
CONF_RETENTION_DAYS,
DEFAULT_NAME,
DEFAULT_SAVE_PATH,
DEFAULT_FILENAME_TEMPLATE,
DEFAULT_MAX_DURATION,
DEFAULT_PREBUFFER,
DEFAULT_MOTION_FILTER,
DEFAULT_OFF_DELAY,
DEFAULT_FORCE_STOP_STATE,
DEFAULT_LOG_LEVEL,
DEFAULT_RETENTION_DAYS,
)
# Список шаблонов имени файла (все с секундами для уникальности)
FILENAME_TEMPLATE_OPTIONS = [
selector.SelectOptionDict(value="%d-%m-%Y_%H-%M-%S", label="ДД-ММ-ГГГГ_ЧЧ-ММ-СС (02-07-2026_20-30-45)"),
selector.SelectOptionDict(value="%Y-%m-%d_%H-%M-%S", label="ГГГГ-ММ-ДД_ЧЧ-ММ-СС (2026-07-02_20-30-45)"),
selector.SelectOptionDict(value="%d.%m.%Y_%H-%M-%S", label="ДД.ММ.ГГГГ_ЧЧ-ММ-СС (02.07.2026_20-30-45)"),
selector.SelectOptionDict(value="%Y%m%d_%H%M%S", label="ГГГГММДД_ЧЧММСС (20260702_203045)"),
selector.SelectOptionDict(value="%d.%m.%Y_%H.%M.%S", label="ДД.ММ.ГГГГ_ЧЧ.ММ.СС (все точки)"),
]
class MotionRecorderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Motion Recorder."""
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_CAMERA_ENTITY])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input.get("name", DEFAULT_NAME),
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required("name", default=DEFAULT_NAME): str,
vol.Required(CONF_CAMERA_ENTITY): selector.EntitySelector(
selector.EntitySelectorConfig(domain="camera")
),
vol.Required(CONF_SAVE_PATH, default=DEFAULT_SAVE_PATH): str,
vol.Required(CONF_FILENAME_TEMPLATE, default=DEFAULT_FILENAME_TEMPLATE): selector.SelectSelector(
selector.SelectSelectorConfig(
options=FILENAME_TEMPLATE_OPTIONS,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(CONF_MAX_DURATION, default=DEFAULT_MAX_DURATION): selector.NumberSelector(
selector.NumberSelectorConfig(
min=10, max=3600, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_PREBUFFER, default=DEFAULT_PREBUFFER): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_MOTION_SENSORS): selector.EntitySelector(
selector.EntitySelectorConfig(domain="binary_sensor", multiple=True)
),
vol.Required(CONF_MOTION_FILTER, default=DEFAULT_MOTION_FILTER): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=10, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_OFF_DELAY, default=DEFAULT_OFF_DELAY): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_RETENTION_DAYS, default=DEFAULT_RETENTION_DAYS): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=365, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="дн",
)
),
vol.Optional(CONF_FORCE_STOP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=["binary_sensor", "switch", "input_boolean"],
multiple=False,
)
),
vol.Required(CONF_FORCE_STOP_STATE, default=DEFAULT_FORCE_STOP_STATE): str,
vol.Required(CONF_LOG_LEVEL, default=DEFAULT_LOG_LEVEL): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value="DEBUG", label="DEBUG (подробно)"),
selector.SelectOptionDict(value="INFO", label="INFO (обычный)"),
selector.SelectOptionDict(value="WARNING", label="WARNING (только предупреждения)"),
selector.SelectOptionDict(value="ERROR", label="ERROR (только ошибки)"),
],
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
}),
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return MotionRecorderOptionsFlow()
class MotionRecorderOptionsFlow(config_entries.OptionsFlow):
"""Handle options flow for Motion Recorder."""
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
vol.Required(
CONF_FILENAME_TEMPLATE,
default=self.config_entry.options.get(CONF_FILENAME_TEMPLATE,
self.config_entry.data.get(CONF_FILENAME_TEMPLATE, DEFAULT_FILENAME_TEMPLATE))
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=FILENAME_TEMPLATE_OPTIONS,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(
CONF_MAX_DURATION,
default=self.config_entry.options.get(CONF_MAX_DURATION,
self.config_entry.data.get(CONF_MAX_DURATION, DEFAULT_MAX_DURATION))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=10, max=3600, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_PREBUFFER,
default=self.config_entry.options.get(CONF_PREBUFFER,
self.config_entry.data.get(CONF_PREBUFFER, DEFAULT_PREBUFFER))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_MOTION_FILTER,
default=self.config_entry.options.get(CONF_MOTION_FILTER,
self.config_entry.data.get(CONF_MOTION_FILTER, DEFAULT_MOTION_FILTER))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=10, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_OFF_DELAY,
default=self.config_entry.options.get(CONF_OFF_DELAY,
self.config_entry.data.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_RETENTION_DAYS,
default=self.config_entry.options.get(CONF_RETENTION_DAYS,
self.config_entry.data.get(CONF_RETENTION_DAYS, DEFAULT_RETENTION_DAYS))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=365, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="дн",
)
),
vol.Required(
CONF_LOG_LEVEL,
default=self.config_entry.options.get(CONF_LOG_LEVEL,
self.config_entry.data.get(CONF_LOG_LEVEL, DEFAULT_LOG_LEVEL))
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value="DEBUG", label="DEBUG (подробно)"),
selector.SelectOptionDict(value="INFO", label="INFO (обычный)"),
selector.SelectOptionDict(value="WARNING", label="WARNING (только предупреждения)"),
selector.SelectOptionDict(value="ERROR", label="ERROR (только ошибки)"),
],
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
}),
"""Config flow for Motion Recorder."""
from homeassistant import config_entries
from homeassistant.core import callback
import voluptuous as vol
from homeassistant.helpers import selector
from .const import (
DOMAIN,
CONF_CAMERA_ENTITY,
CONF_SAVE_PATH,
CONF_FILENAME_TEMPLATE,
CONF_MAX_DURATION,
CONF_PREBUFFER,
CONF_MOTION_SENSORS,
CONF_MOTION_FILTER,
CONF_OFF_DELAY,
CONF_FORCE_STOP_SENSOR,
CONF_FORCE_STOP_STATE,
CONF_LOG_LEVEL,
CONF_RETENTION_DAYS,
CONF_CONTROLLED_ENTITIES,
CONF_CONTROL_STATES,
DEFAULT_NAME,
DEFAULT_SAVE_PATH,
DEFAULT_FILENAME_TEMPLATE,
DEFAULT_MAX_DURATION,
DEFAULT_PREBUFFER,
DEFAULT_MOTION_FILTER,
DEFAULT_OFF_DELAY,
DEFAULT_FORCE_STOP_STATE,
DEFAULT_LOG_LEVEL,
DEFAULT_RETENTION_DAYS,
DEFAULT_CONTROL_STATES,
STATE_IDLE,
STATE_DETECTING,
STATE_RECORDING,
STATE_DELAYING,
STATE_FINALIZING,
STATE_ERROR,
STATE_DISABLED,
)
FILENAME_TEMPLATE_OPTIONS = [
selector.SelectOptionDict(value="%d-%m-%Y_%H-%M-%S", label="ДД-ММ-ГГГГ_ЧЧ-ММ-СС (02-07-2026_20-30-45)"),
selector.SelectOptionDict(value="%Y-%m-%d_%H-%M-%S", label="ГГГГ-ММ-ДД_ЧЧ-ММ-СС (2026-07-02_20-30-45)"),
selector.SelectOptionDict(value="%d.%m.%Y_%H-%M-%S", label="ДД.ММ.ГГГГ_ЧЧ-ММ-СС (02.07.2026_20-30-45)"),
selector.SelectOptionDict(value="%Y%m%d_%H%M%S", label="ГГГГММДД_ЧЧММСС (20260702_203045)"),
selector.SelectOptionDict(value="%d.%m.%Y_%H.%M.%S", label="ДД.ММ.ГГГГ_ЧЧ.ММ.СС (все точки)"),
]
CONTROL_STATE_OPTIONS = [
selector.SelectOptionDict(value=STATE_IDLE, label="Ожидание (idle)"),
selector.SelectOptionDict(value=STATE_DETECTING, label="Обнаружение (detecting)"),
selector.SelectOptionDict(value=STATE_RECORDING, label="Запись (recording)"),
selector.SelectOptionDict(value=STATE_DELAYING, label="Задержка (delaying)"),
selector.SelectOptionDict(value=STATE_FINALIZING, label="Завершение (finalizing)"),
selector.SelectOptionDict(value=STATE_ERROR, label="Ошибка (error)"),
selector.SelectOptionDict(value=STATE_DISABLED, label="Отключено (disabled)"),
]
class MotionRecorderConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Motion Recorder."""
VERSION = 1
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
await self.async_set_unique_id(user_input[CONF_CAMERA_ENTITY])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input.get("name", DEFAULT_NAME),
data=user_input,
)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema({
vol.Required("name", default=DEFAULT_NAME): str,
vol.Required(CONF_CAMERA_ENTITY): selector.EntitySelector(
selector.EntitySelectorConfig(domain="camera")
),
vol.Required(CONF_SAVE_PATH, default=DEFAULT_SAVE_PATH): str,
vol.Required(CONF_FILENAME_TEMPLATE, default=DEFAULT_FILENAME_TEMPLATE): selector.SelectSelector(
selector.SelectSelectorConfig(
options=FILENAME_TEMPLATE_OPTIONS,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(CONF_MAX_DURATION, default=DEFAULT_MAX_DURATION): selector.NumberSelector(
selector.NumberSelectorConfig(
min=10, max=3600, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_PREBUFFER, default=DEFAULT_PREBUFFER): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_MOTION_SENSORS): selector.EntitySelector(
selector.EntitySelectorConfig(domain="binary_sensor", multiple=True)
),
vol.Required(CONF_MOTION_FILTER, default=DEFAULT_MOTION_FILTER): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=10, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(CONF_OFF_DELAY, default=DEFAULT_OFF_DELAY): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Optional(CONF_FORCE_STOP_SENSOR): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=["binary_sensor", "switch", "input_boolean"],
multiple=False,
)
),
vol.Required(CONF_FORCE_STOP_STATE, default=DEFAULT_FORCE_STOP_STATE): str,
vol.Required(CONF_RETENTION_DAYS, default=DEFAULT_RETENTION_DAYS): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=365, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="дн",
)
),
vol.Optional(CONF_CONTROLLED_ENTITIES, default=[]): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=["light", "switch"],
multiple=True,
)
),
vol.Optional(CONF_CONTROL_STATES, default=DEFAULT_CONTROL_STATES): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONTROL_STATE_OPTIONS,
multiple=True,
mode=selector.SelectSelectorMode.LIST,
)
),
vol.Required(CONF_LOG_LEVEL, default=DEFAULT_LOG_LEVEL): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value="DEBUG", label="DEBUG (подробно)"),
selector.SelectOptionDict(value="INFO", label="INFO (обычный)"),
selector.SelectOptionDict(value="WARNING", label="WARNING (только предупреждения)"),
selector.SelectOptionDict(value="ERROR", label="ERROR (только ошибки)"),
],
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
}),
errors=errors,
)
@staticmethod
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return MotionRecorderOptionsFlow()
class MotionRecorderOptionsFlow(config_entries.OptionsFlow):
"""Handle options flow for Motion Recorder."""
async def async_step_init(self, user_input=None):
"""Manage the options."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_show_form(
step_id="init",
data_schema=vol.Schema({
vol.Required(
CONF_FILENAME_TEMPLATE,
default=self.config_entry.options.get(CONF_FILENAME_TEMPLATE,
self.config_entry.data.get(CONF_FILENAME_TEMPLATE, DEFAULT_FILENAME_TEMPLATE))
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=FILENAME_TEMPLATE_OPTIONS,
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
vol.Required(
CONF_MAX_DURATION,
default=self.config_entry.options.get(CONF_MAX_DURATION,
self.config_entry.data.get(CONF_MAX_DURATION, DEFAULT_MAX_DURATION))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=10, max=3600, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_PREBUFFER,
default=self.config_entry.options.get(CONF_PREBUFFER,
self.config_entry.data.get(CONF_PREBUFFER, DEFAULT_PREBUFFER))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_MOTION_FILTER,
default=self.config_entry.options.get(CONF_MOTION_FILTER,
self.config_entry.data.get(CONF_MOTION_FILTER, DEFAULT_MOTION_FILTER))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=10, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_OFF_DELAY,
default=self.config_entry.options.get(CONF_OFF_DELAY,
self.config_entry.data.get(CONF_OFF_DELAY, DEFAULT_OFF_DELAY))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=60, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="сек",
)
),
vol.Required(
CONF_RETENTION_DAYS,
default=self.config_entry.options.get(CONF_RETENTION_DAYS,
self.config_entry.data.get(CONF_RETENTION_DAYS, DEFAULT_RETENTION_DAYS))
): selector.NumberSelector(
selector.NumberSelectorConfig(
min=0, max=365, step=1,
mode=selector.NumberSelectorMode.SLIDER,
unit_of_measurement="дн",
)
),
vol.Optional(
CONF_CONTROLLED_ENTITIES,
default=self.config_entry.options.get(
CONF_CONTROLLED_ENTITIES,
self.config_entry.data.get(CONF_CONTROLLED_ENTITIES, [])
)
): selector.EntitySelector(
selector.EntitySelectorConfig(
domain=["light", "switch"],
multiple=True,
)
),
vol.Optional(
CONF_CONTROL_STATES,
default=self.config_entry.options.get(
CONF_CONTROL_STATES,
self.config_entry.data.get(CONF_CONTROL_STATES, DEFAULT_CONTROL_STATES)
)
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=CONTROL_STATE_OPTIONS,
multiple=True,
mode=selector.SelectSelectorMode.LIST,
)
),
vol.Required(
CONF_LOG_LEVEL,
default=self.config_entry.options.get(CONF_LOG_LEVEL,
self.config_entry.data.get(CONF_LOG_LEVEL, DEFAULT_LOG_LEVEL))
): selector.SelectSelector(
selector.SelectSelectorConfig(
options=[
selector.SelectOptionDict(value="DEBUG", label="DEBUG (подробно)"),
selector.SelectOptionDict(value="INFO", label="INFO (обычный)"),
selector.SelectOptionDict(value="WARNING", label="WARNING (только предупреждения)"),
selector.SelectOptionDict(value="ERROR", label="ERROR (только ошибки)"),
],
mode=selector.SelectSelectorMode.DROPDOWN,
)
),
}),
)
+50 -38
View File
@@ -1,38 +1,50 @@
"""Constants for Motion Recorder integration."""
DOMAIN = "motion_recorder"
# Configuration keys
CONF_CAMERA_ENTITY = "camera_entity"
CONF_SAVE_PATH = "save_path"
CONF_FILENAME_TEMPLATE = "filename_template"
CONF_MAX_DURATION = "max_duration"
CONF_PREBUFFER = "prebuffer"
CONF_MOTION_SENSORS = "motion_sensors"
CONF_MOTION_FILTER = "motion_filter"
CONF_OFF_DELAY = "off_delay"
CONF_FORCE_STOP_SENSOR = "force_stop_sensor"
CONF_FORCE_STOP_STATE = "force_stop_state"
CONF_LOG_LEVEL = "log_level"
CONF_RETENTION_DAYS = "retention_days"
# Defaults
DEFAULT_NAME = "Motion Recorder"
DEFAULT_SAVE_PATH = "{camera_name}"
DEFAULT_FILENAME_TEMPLATE = "%d-%m-%Y_%H-%M-%S"
DEFAULT_MAX_DURATION = 60
DEFAULT_PREBUFFER = 0
DEFAULT_MOTION_FILTER = 0
DEFAULT_OFF_DELAY = 10
DEFAULT_FORCE_STOP_STATE = "off"
DEFAULT_LOG_LEVEL = "INFO"
DEFAULT_RETENTION_DAYS = 7
# Sensor states
STATE_IDLE = "idle"
STATE_DETECTING = "detecting"
STATE_RECORDING = "recording"
STATE_DELAYING = "delaying"
STATE_FINALIZING = "finalizing"
STATE_COMPLETED = "completed"
STATE_ERROR = "error"
STATE_DISABLED = "disabled"
"""Constants for Motion Recorder integration."""
DOMAIN = "motion_recorder"
# Configuration keys
CONF_CAMERA_ENTITY = "camera_entity"
CONF_SAVE_PATH = "save_path"
CONF_FILENAME_TEMPLATE = "filename_template"
CONF_MAX_DURATION = "max_duration"
CONF_PREBUFFER = "prebuffer"
CONF_MOTION_SENSORS = "motion_sensors"
CONF_MOTION_FILTER = "motion_filter"
CONF_OFF_DELAY = "off_delay"
CONF_FORCE_STOP_SENSOR = "force_stop_sensor"
CONF_FORCE_STOP_STATE = "force_stop_state"
CONF_LOG_LEVEL = "log_level"
CONF_RETENTION_DAYS = "retention_days"
CONF_CONTROLLED_ENTITIES = "controlled_entities"
CONF_CONTROL_STATES = "control_states"
# Defaults
DEFAULT_NAME = "Motion Recorder"
DEFAULT_SAVE_PATH = "{camera_name}"
DEFAULT_FILENAME_TEMPLATE = "%d-%m-%Y_%H-%M-%S"
DEFAULT_MAX_DURATION = 60
DEFAULT_PREBUFFER = 0
DEFAULT_MOTION_FILTER = 0
DEFAULT_OFF_DELAY = 10
DEFAULT_FORCE_STOP_STATE = "off"
DEFAULT_LOG_LEVEL = "INFO"
DEFAULT_RETENTION_DAYS = 7
DEFAULT_CONTROL_STATES = ["recording"]
# Sensor states
STATE_IDLE = "idle"
STATE_DETECTING = "detecting"
STATE_RECORDING = "recording"
STATE_DELAYING = "delaying"
STATE_FINALIZING = "finalizing"
STATE_ERROR = "error"
STATE_DISABLED = "disabled"
ALL_STATES = [
STATE_IDLE,
STATE_DETECTING,
STATE_RECORDING,
STATE_DELAYING,
STATE_FINALIZING,
STATE_ERROR,
STATE_DISABLED,
]
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -1,12 +1,12 @@
{
"domain": "motion_recorder",
"name": "Motion Recorder",
"version": "1.0.0",
"documentation": "https://github.com/sirmrak/motion_recorder",
"issue_tracker": "https://github.com/sirmrak/motion_recorder/issues",
"dependencies": ["camera", "stream"],
"codeowners": ["@sirmrak"],
"requirements": [],
"iot_class": "local_push",
"config_flow": true
{
"domain": "motion_recorder",
"name": "Motion Recorder",
"codeowners": ["@sirmrak"],
"config_flow": true,
"dependencies": ["camera", "stream"],
"documentation": "https://github.com/sirmrak/motion_recorder",
"iot_class": "local_push",
"issue_tracker": "https://github.com/sirmrak/motion_recorder/issues",
"requirements": [],
"version": "1.1.1"
}
+66 -69
View File
@@ -1,70 +1,67 @@
"""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."""
"""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
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",
)
self._icon_map = {
"idle": "mdi:sleep",
"detecting": "mdi:motion-sensor",
"recording": "mdi:record-rec",
"delaying": "mdi:timer-sand",
"finalizing": "mdi:file-check",
"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()
+98 -90
View File
@@ -1,91 +1,99 @@
{
"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": "Запись включена"
}
}
}
{
"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 = без задержки)",
"force_stop_sensor": "Датчик принудительной остановки (binary_sensor, switch, input_boolean)",
"force_stop_state": "Состояние для остановки",
"retention_days": "Хранить записи (дней, 0 = не удалять)",
"controlled_entities": "Управляемые устройства (light/switch)",
"control_states": "Включать при состояниях",
"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": "Сколько секунд ждать после пропадания движения перед остановкой записи. Если движение вернётся — таймер сбросится",
"force_stop_sensor": "Необязательно. Сущность, при переходе в указанное состояние запись немедленно остановится",
"force_stop_state": "Значение состояния датчика принудительной остановки (обычно 'off' или 'on')",
"retention_days": "Автоматически удалять записи старше указанного количества дней. 0 = не удалять никогда. Очистка выполняется ежедневно в 3:00",
"controlled_entities": "Список светильников или переключателей, которые будут автоматически включаться/выключаться в зависимости от состояния интеграции",
"control_states": "Отметьте состояния, при которых управляемые устройства должны быть включены. В остальных состояниях они будут выключены (только если были включены интеграцией)",
"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 = не удалять)",
"controlled_entities": "Управляемые устройства (light/switch)",
"control_states": "Включать при состояниях",
"log_level": "Уровень логирования"
},
"data_description": {
"filename_template": "Выберите формат даты и времени для имён файлов записей",
"max_duration": "Максимальное время одной записи (10-3600 секунд)",
"prebuffer": "Сколько секунд до обнаружения движения включить в запись",
"motion_filter": "Задержка перед началом записи для защиты от ложных срабатываний",
"off_delay": "Сколько секунд ждать после пропадания движения перед остановкой записи",
"retention_days": "Автоматически удалять записи старше указанного количества дней. 0 = не удалять никогда",
"controlled_entities": "Список светильников или переключателей для автоматического управления",
"control_states": "Состояния, при которых устройства должны быть включены",
"log_level": "Уровень детализации логов"
}
}
}
},
"entity": {
"sensor": {
"motion_recorder_status": {
"name": "Статус",
"state": {
"idle": "Ожидание",
"detecting": "Обнаружение",
"recording": "Запись",
"delaying": "Задержка",
"finalizing": "Завершение",
"completed": "Завершено",
"error": "Ошибка",
"disabled": "Отключено"
}
}
},
"switch": {
"motion_recorder_enabled": {
"name": "Запись включена"
}
}
}
}
@@ -1,91 +1,98 @@
{
"config": {
"step": {
"user": {
"title": "Motion Recorder Settings",
"description": "Configure motion-based camera recording integration",
"data": {
"name": "Integration Name",
"camera_entity": "Camera to record",
"save_path": "Save path (relative to media)",
"filename_template": "Filename format",
"max_duration": "Maximum recording duration (seconds)",
"prebuffer": "Prebuffer (seconds, 0 = disabled)",
"motion_sensors": "Motion sensors",
"motion_filter": "Motion detection filter (seconds, 0 = no filter)",
"off_delay": "Delay after motion stops (seconds, 0 = no delay)",
"retention_days": "Keep recordings (days, 0 = never delete)",
"force_stop_sensor": "Force stop sensor (binary_sensor, switch, input_boolean)",
"force_stop_state": "Stop state value",
"log_level": "Log level"
},
"data_description": {
"name": "Name that will be displayed in the integration",
"camera_entity": "Select a camera from the list of available",
"save_path": "Folder for saving recordings relative to /media. Variables are supported, e.g. camera_name",
"filename_template": "Select date and time format for recording filenames. All formats include seconds to avoid file overwriting when multiple triggers occur in the same minute",
"max_duration": "Maximum time of one recording (10-3600 seconds). If motion does not stop, recording will stop by timeout",
"prebuffer": "How many seconds before motion detection to include in recording. Works only if stream is already active",
"motion_sensors": "You can select multiple sensors. Recording will start when any of them triggers",
"motion_filter": "Delay before starting recording. If motion stops earlier — recording will not start (false trigger protection)",
"off_delay": "How many seconds to wait after motion stops before stopping recording. If motion returns — timer resets",
"retention_days": "Automatically delete recordings older than specified days. 0 = never delete. Cleanup runs daily at 03:00",
"force_stop_sensor": "Optional. Entity that will immediately stop recording when it reaches the specified state",
"force_stop_state": "State value of the force stop sensor (usually 'off' or 'on')",
"log_level": "Log detail level. DEBUG — for debugging, INFO — normal mode"
}
}
},
"abort": {
"single_instance_allowed": "Only one configuration per camera is allowed"
}
},
"options": {
"step": {
"init": {
"title": "Motion Recorder Settings",
"description": "Change recording parameters",
"data": {
"filename_template": "Filename format",
"max_duration": "Maximum recording duration (seconds)",
"prebuffer": "Prebuffer (seconds, 0 = disabled)",
"motion_filter": "Motion detection filter (seconds, 0 = no filter)",
"off_delay": "Delay after motion stops (seconds, 0 = no delay)",
"retention_days": "Keep recordings (days, 0 = never delete)",
"log_level": "Log level"
},
"data_description": {
"filename_template": "Select date and time format for recording filenames",
"max_duration": "Maximum time of one recording (10-3600 seconds)",
"prebuffer": "How many seconds before motion detection to include in recording",
"motion_filter": "Delay before starting recording for false trigger protection",
"off_delay": "How many seconds to wait after motion stops before stopping recording",
"retention_days": "Automatically delete recordings older than specified days. 0 = never delete",
"log_level": "Log detail level"
}
}
}
},
"entity": {
"sensor": {
"motion_recorder_status": {
"name": "Status",
"state": {
"idle": "Idle",
"detecting": "Detecting",
"recording": "Recording",
"delaying": "Delaying",
"finalizing": "Finalizing",
"completed": "Completed",
"error": "Error",
"disabled": "Disabled"
}
}
},
"switch": {
"motion_recorder_enabled": {
"name": "Recording Enabled"
}
}
}
{
"config": {
"step": {
"user": {
"title": "Motion Recorder Settings",
"description": "Configure motion-based camera recording integration",
"data": {
"name": "Integration Name",
"camera_entity": "Camera to record",
"save_path": "Save path (relative to media)",
"filename_template": "Filename format",
"max_duration": "Maximum recording duration (seconds)",
"prebuffer": "Prebuffer (seconds, 0 = disabled)",
"motion_sensors": "Motion sensors",
"motion_filter": "Motion detection filter (seconds, 0 = no filter)",
"off_delay": "Delay after motion stops (seconds, 0 = no delay)",
"force_stop_sensor": "Force stop sensor (binary_sensor, switch, input_boolean)",
"force_stop_state": "Stop state value",
"retention_days": "Keep recordings (days, 0 = never delete)",
"controlled_entities": "Controlled devices (light/switch)",
"control_states": "Turn on at states",
"log_level": "Log level"
},
"data_description": {
"name": "Name that will be displayed in the integration",
"camera_entity": "Select a camera from the list of available",
"save_path": "Folder for saving recordings relative to /media. Variables are supported, e.g. camera_name",
"filename_template": "Select date and time format for recording filenames. All formats include seconds to avoid file overwriting when multiple triggers occur in the same minute",
"max_duration": "Maximum time of one recording (10-3600 seconds). If motion does not stop, recording will stop by timeout",
"prebuffer": "How many seconds before motion detection to include in recording. Works only if stream is already active",
"motion_sensors": "You can select multiple sensors. Recording will start when any of them triggers",
"motion_filter": "Delay before starting recording. If motion stops earlier — recording will not start (false trigger protection)",
"off_delay": "How many seconds to wait after motion stops before stopping recording. If motion returns — timer resets",
"force_stop_sensor": "Optional. Entity that will immediately stop recording when it reaches the specified state",
"force_stop_state": "State value of the force stop sensor (usually 'off' or 'on')",
"retention_days": "Automatically delete recordings older than specified days. 0 = never delete. Cleanup runs daily at 03:00",
"controlled_entities": "List of lights or switches that will be automatically turned on/off depending on the integration state",
"control_states": "Select states when controlled devices should be turned on. In other states they will be turned off (only if they were turned on by the integration)",
"log_level": "Log detail level. DEBUG — for debugging, INFO — normal mode"
}
}
},
"abort": {
"single_instance_allowed": "Only one configuration per camera is allowed"
}
},
"options": {
"step": {
"init": {
"title": "Motion Recorder Settings",
"description": "Change recording parameters",
"data": {
"filename_template": "Filename format",
"max_duration": "Maximum recording duration (seconds)",
"prebuffer": "Prebuffer (seconds, 0 = disabled)",
"motion_filter": "Motion detection filter (seconds, 0 = no filter)",
"off_delay": "Delay after motion stops (seconds, 0 = no delay)",
"retention_days": "Keep recordings (days, 0 = never delete)",
"controlled_entities": "Controlled devices (light/switch)",
"control_states": "Turn on at states",
"log_level": "Log level"
},
"data_description": {
"filename_template": "Select date and time format for recording filenames",
"max_duration": "Maximum time of one recording (10-3600 seconds)",
"prebuffer": "How many seconds before motion detection to include in recording",
"motion_filter": "Delay before starting recording for false trigger protection",
"off_delay": "How many seconds to wait after motion stops before stopping recording",
"retention_days": "Automatically delete recordings older than specified days. 0 = never delete",
"controlled_entities": "List of lights or switches for automatic control",
"control_states": "States when devices should be turned on",
"log_level": "Log detail level"
}
}
}
},
"entity": {
"sensor": {
"motion_recorder_status": {
"name": "Status",
"state": {
"idle": "Idle",
"detecting": "Detecting",
"recording": "Recording",
"delaying": "Delaying",
"finalizing": "Finalizing",
"error": "Error",
"disabled": "Disabled"
}
}
},
"switch": {
"motion_recorder_enabled": {
"name": "Recording Enabled"
}
}
}
}
@@ -1,91 +1,98 @@
{
"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": "Запись включена"
}
}
}
{
"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 = без задержки)",
"force_stop_sensor": "Датчик принудительной остановки (binary_sensor, switch, input_boolean)",
"force_stop_state": "Состояние для остановки",
"retention_days": "Хранить записи (дней, 0 = не удалять)",
"controlled_entities": "Управляемые устройства (light/switch)",
"control_states": "Включать при состояниях",
"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": "Сколько секунд ждать после пропадания движения перед остановкой записи. Если движение вернётся — таймер сбросится",
"force_stop_sensor": "Необязательно. Сущность, при переходе в указанное состояние запись немедленно остановится",
"force_stop_state": "Значение состояния датчика принудительной остановки (обычно 'off' или 'on')",
"retention_days": "Автоматически удалять записи старше указанного количества дней. 0 = не удалять никогда. Очистка выполняется ежедневно в 3:00",
"controlled_entities": "Список светильников или переключателей, которые будут автоматически включаться/выключаться в зависимости от состояния интеграции",
"control_states": "Отметьте состояния, при которых управляемые устройства должны быть включены. В остальных состояниях они будут выключены (только если были включены интеграцией)",
"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 = не удалять)",
"controlled_entities": "Управляемые устройства (light/switch)",
"control_states": "Включать при состояниях",
"log_level": "Уровень логирования"
},
"data_description": {
"filename_template": "Выберите формат даты и времени для имён файлов записей",
"max_duration": "Максимальное время одной записи (10-3600 секунд)",
"prebuffer": "Сколько секунд до обнаружения движения включить в запись",
"motion_filter": "Задержка перед началом записи для защиты от ложных срабатываний",
"off_delay": "Сколько секунд ждать после пропадания движения перед остановкой записи",
"retention_days": "Автоматически удалять записи старше указанного количества дней. 0 = не удалять никогда",
"controlled_entities": "Список светильников или переключателей для автоматического управления",
"control_states": "Состояния, при которых устройства должны быть включены",
"log_level": "Уровень детализации логов"
}
}
}
},
"entity": {
"sensor": {
"motion_recorder_status": {
"name": "Статус",
"state": {
"idle": "Ожидание",
"detecting": "Обнаружение",
"recording": "Запись",
"delaying": "Задержка",
"finalizing": "Завершение",
"error": "Ошибка",
"disabled": "Отключено"
}
}
},
"switch": {
"motion_recorder_enabled": {
"name": "Запись включена"
}
}
}
}
+25
View File
@@ -0,0 +1,25 @@
[tool.semantic_release]
tag_format = "v{version}"
version_variables = [
"custom_components/motion_recorder/manifest.json:version",
]
commit_message = "chore(release): v{version}"
# ВАЖНО: не создаём релиз в Gitea (релиз будет в GitHub)
upload_to_vcs_release = false
[tool.semantic_release.remote]
type = "gitea"
domain = "192.168.200.208:3000"
api_domain = "192.168.200.208:3000"
[tool.semantic_release.branches.main]
match = "(main|master)"
prerelease = false
[tool.semantic_release.commit_parser_options]
allowed_tags = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "revert"]
minor_tags = ["feat"]
patch_tags = ["fix", "perf"]