6 Commits
Author SHA1 Message Date
semantic-release 9fbb7cae18 chore(release): v1.1.3
Update Version / Update Version (push) Successful in 5s
2026-07-17 08:13:08 +00:00
sirmrak 0d64069548 fix: Test
Update Version / Update Version (push) Successful in 6s
2026-07-17 11:13:02 +03:00
sirmrak 03ea96b282 Обновить .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 5s
2026-07-17 11:11:00 +03:00
sirmrak cac3274bbb Обновить pyproject.toml
Update Version / Update Version (push) Has been cancelled
2026-07-17 10:48:18 +03:00
semantic-release b6d87cb049 chore(release): v1.1.2
Update Version / Update Version (push) Successful in 5s
2026-07-11 15:33:27 +00:00
sirmrak dc85e69e28 fix: FST
Update Version / Update Version (push) Successful in 8s
При выключении switch запись полностью останавливается и не продолжается
При активации force_stop запись мгновенно останавливается и блокируется до деактивации
При деактивации force_stop система возвращается в нормальный режим
2026-07-11 18:33:15 +03:00
6 changed files with 119 additions and 39 deletions
+1 -24
View File
@@ -6,13 +6,11 @@ on:
- main - main
- master - master
env:
GIT_SSL_NO_VERIFY: true
jobs: jobs:
update-version: update-version:
name: Update Version name: Update Version
runs-on: ubuntu-latest runs-on: ubuntu-latest
# Пропускаем запуск, если коммит сделал сам бот (избегаем бесконечного цикла)
if: github.actor != 'gitea-actions[bot]' && github.actor != 'Gitea Actions Bot' if: github.actor != 'gitea-actions[bot]' && github.actor != 'Gitea Actions Bot'
permissions: permissions:
contents: write contents: write
@@ -41,29 +39,8 @@ jobs:
env: env:
GITEA_TOKEN: ${{ secrets.PAT_TOKEN }} GITEA_TOKEN: ${{ secrets.PAT_TOKEN }}
run: | run: |
echo "🧹 Сбрасываем конфликтующие переменные Gitea Actions..."
unset GITHUB_SERVER_URL unset GITHUB_SERVER_URL
unset GITHUB_API_URL unset GITHUB_API_URL
unset GITHUB_REPOSITORY 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 semantic-release version
+16
View File
@@ -2,6 +2,22 @@
<!-- version list --> <!-- version list -->
## v1.1.3 (2026-07-17)
### Bug Fixes
- Test
([`0d64069`](https://git.darkmode.duckdns.org/sirmrak/motion_recorder/commit/0d64069548d980814ae3403c2b6b43ac4491b918))
## v1.1.2 (2026-07-11)
### Bug Fixes
- FST
([`dc85e69`](https://192.168.200.208:3000/sirmrak/motion_recorder/commit/dc85e69e289cb2f13ddb0e51a98bfc207c73a9d6))
## v1.1.1 (2026-07-10) ## v1.1.1 (2026-07-10)
### Bug Fixes ### Bug Fixes
@@ -1,4 +1,4 @@
"""The Motion Recorder integration.""" """The Motion Recorder integration. Test"""
import logging import logging
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -92,6 +92,8 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
# Enabled state # Enabled state
self._enabled = True self._enabled = True
self._force_stopped = False
# Listeners # Listeners
self._unsub_motion_listeners = [] self._unsub_motion_listeners = []
self._unsub_force_stop_listener = None self._unsub_force_stop_listener = None
@@ -146,6 +148,7 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
attrs["recordings_count"] = self._recordings_count attrs["recordings_count"] = self._recordings_count
attrs["total_duration"] = self._total_duration attrs["total_duration"] = self._total_duration
attrs["total_size"] = self._total_size attrs["total_size"] = self._total_size
attrs["force_stopped"] = self._force_stopped
return attrs return attrs
@property @property
@@ -159,17 +162,39 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
_LOGGER.info("Recording %s", "enabled" if enabled else "disabled") _LOGGER.info("Recording %s", "enabled" if enabled else "disabled")
if not enabled: if not enabled:
await self._cancel_all_timers()
# Останавливаем запись если идёт
if self._state == STATE_RECORDING: if self._state == STATE_RECORDING:
await self._stop_recording() await self._stop_recording()
# Переходим в disabled
await self._update_state(STATE_DISABLED) await self._update_state(STATE_DISABLED)
else: else:
await self._update_state(STATE_IDLE) # Включаем только если не заблокированы force_stop
if self._force_stopped:
_LOGGER.info("Cannot enable: force stop is active")
await self._update_state(STATE_DISABLED)
else:
await self._update_state(STATE_IDLE)
self.async_set_updated_data({ self.async_set_updated_data({
"state": self._state, "state": self._state,
"attributes": self.attributes "attributes": self.attributes
}) })
async def _cancel_all_timers(self) -> None:
"""Отменить все активные таймеры."""
if self._motion_detect_timer:
_LOGGER.debug("Cancelling motion detect timer")
self._motion_detect_timer()
self._motion_detect_timer = None
if self._off_delay_timer:
_LOGGER.debug("Cancelling off-delay timer")
self._off_delay_timer()
self._off_delay_timer = None
async def async_setup(self): async def async_setup(self):
"""Set up the coordinator.""" """Set up the coordinator."""
_LOGGER.info("Setting up Motion Recorder coordinator for %s", self.camera_entity) _LOGGER.info("Setting up Motion Recorder coordinator for %s", self.camera_entity)
@@ -364,9 +389,43 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
if not new_state: if not new_state:
return return
if new_state.state == self.force_stop_state: new_s = new_state.state
_LOGGER.info("Force stop triggered by %s", event.data.get("entity_id")) sensor_entity = event.data.get("entity_id")
self.hass.async_create_task(self._force_stop_recording())
_LOGGER.debug("Force stop sensor %s changed to: %s", sensor_entity, new_s)
if new_s == self.force_stop_state:
_LOGGER.info("Force stop ACTIVATED by %s", sensor_entity)
self.hass.async_create_task(self._activate_force_stop())
else:
_LOGGER.info("Force stop DEACTIVATED by %s", sensor_entity)
self.hass.async_create_task(self._deactivate_force_stop())
async def _activate_force_stop(self) -> None:
"""Активировать force stop блокировку."""
self._force_stopped = True
_LOGGER.info("Force stop blocking activated")
# Отменяем все таймеры
await self._cancel_all_timers()
# Останавливаем запись если идёт
if self._state == STATE_RECORDING:
await self._stop_recording()
# Переходим в disabled
await self._update_state(STATE_DISABLED)
async def _deactivate_force_stop(self) -> None:
"""Деактивировать force stop блокировку."""
self._force_stopped = False
_LOGGER.info("Force stop blocking deactivated")
# Возвращаемся в idle только если запись включена
if self._enabled:
await self._update_state(STATE_IDLE)
else:
await self._update_state(STATE_DISABLED)
def _is_any_motion_active(self) -> bool: def _is_any_motion_active(self) -> bool:
"""Check if any motion sensor is currently in 'on' state.""" """Check if any motion sensor is currently in 'on' state."""
@@ -382,6 +441,10 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
_LOGGER.debug("Recording disabled, ignoring motion") _LOGGER.debug("Recording disabled, ignoring motion")
return return
if self._force_stopped:
_LOGGER.debug("Force stop active, ignoring motion")
return
_LOGGER.debug("Motion detected by %s, current state: %s", triggered_by, self._state) _LOGGER.debug("Motion detected by %s, current state: %s", triggered_by, self._state)
if self._state == STATE_RECORDING: if self._state == STATE_RECORDING:
@@ -423,6 +486,12 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
async def _start_recording_after_filter(self, _=None): async def _start_recording_after_filter(self, _=None):
"""Start recording after motion filter.""" """Start recording after motion filter."""
if not self._enabled or self._force_stopped:
_LOGGER.debug("Cannot start recording: enabled=%s, force_stopped=%s",
self._enabled, self._force_stopped)
await self._update_state(STATE_IDLE if self._enabled else STATE_DISABLED)
return
_LOGGER.debug("Starting recording after filter") _LOGGER.debug("Starting recording after filter")
self._motion_detect_timer = None self._motion_detect_timer = None
triggered_by = self._attributes.get("triggered_by") triggered_by = self._attributes.get("triggered_by")
@@ -430,6 +499,10 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
async def _handle_motion_stopped(self): async def _handle_motion_stopped(self):
"""Handle motion stopped with off-delay.""" """Handle motion stopped with off-delay."""
if self._force_stopped:
_LOGGER.debug("Force stop active, ignoring motion stopped")
return
_LOGGER.debug("Motion stopped, current state: %s", self._state) _LOGGER.debug("Motion stopped, current state: %s", self._state)
if self._is_any_motion_active(): if self._is_any_motion_active():
@@ -465,10 +538,19 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
_LOGGER.debug("Cancelling motion filter timer") _LOGGER.debug("Cancelling motion filter timer")
self._motion_detect_timer() self._motion_detect_timer()
self._motion_detect_timer = None self._motion_detect_timer = None
await self._update_state(STATE_IDLE)
if not self._enabled:
await self._update_state(STATE_DISABLED)
else:
await self._update_state(STATE_IDLE)
async def _stop_recording_after_delay(self, _=None): async def _stop_recording_after_delay(self, _=None):
"""Stop recording after off-delay.""" """Stop recording after off-delay."""
if self._force_stopped or not self._enabled:
_LOGGER.debug("Cannot stop recording: enabled=%s, force_stopped=%s",
self._enabled, self._force_stopped)
return
_LOGGER.debug("Off-delay timer expired") _LOGGER.debug("Off-delay timer expired")
self._off_delay_timer = None self._off_delay_timer = None
@@ -482,6 +564,12 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
async def _start_recording(self, triggered_by): async def _start_recording(self, triggered_by):
"""Start recording the camera.""" """Start recording the camera."""
if not self._enabled or self._force_stopped:
_LOGGER.warning("Cannot start recording: enabled=%s, force_stopped=%s",
self._enabled, self._force_stopped)
await self._update_state(STATE_IDLE if self._enabled else STATE_DISABLED)
return
try: try:
if self._current_stream: if self._current_stream:
recorder_output = self._current_stream.outputs().get(_RECORDER_PROVIDER) recorder_output = self._current_stream.outputs().get(_RECORDER_PROVIDER)
@@ -578,13 +666,13 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
_LOGGER.error("Error stopping recording: %s", err, exc_info=True) _LOGGER.error("Error stopping recording: %s", err, exc_info=True)
await self._update_state(STATE_ERROR, error_message=str(err)) await self._update_state(STATE_ERROR, error_message=str(err))
await asyncio.sleep(5) await asyncio.sleep(5)
if not self._enabled: if not self._enabled or self._force_stopped:
await self._update_state(STATE_DISABLED) await self._update_state(STATE_DISABLED)
else: else:
await self._update_state(STATE_IDLE) await self._update_state(STATE_IDLE)
async def _force_stop_recording(self): async def _force_stop_recording(self):
"""Force stop recording.""" """Force stop recording (вызывается при активации force_stop)."""
_LOGGER.info("Force stop recording called") _LOGGER.info("Force stop recording called")
await self._stop_recording() await self._stop_recording()
@@ -628,9 +716,8 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
self._recording_start_time = None self._recording_start_time = None
self._current_filename = None self._current_filename = None
# Проверяем, не был ли выключен переключатель во время записи if not self._enabled or self._force_stopped:
if not self._enabled: _LOGGER.debug("Recording disabled or force stopped during finalization, transitioning to disabled")
_LOGGER.debug("Recording disabled during finalization, transitioning to disabled")
await self._update_state(STATE_DISABLED) await self._update_state(STATE_DISABLED)
else: else:
_LOGGER.debug("Transitioning to idle") _LOGGER.debug("Transitioning to idle")
@@ -8,5 +8,5 @@
"iot_class": "local_push", "iot_class": "local_push",
"issue_tracker": "https://github.com/sirmrak/motion_recorder/issues", "issue_tracker": "https://github.com/sirmrak/motion_recorder/issues",
"requirements": [], "requirements": [],
"version": "1.1.1" "version": "1.1.3"
} }
+2 -2
View File
@@ -12,8 +12,8 @@ upload_to_vcs_release = false
[tool.semantic_release.remote] [tool.semantic_release.remote]
type = "gitea" type = "gitea"
domain = "192.168.200.208:3000" domain = "git.darkmode.duckdns.org"
api_domain = "192.168.200.208:3000" api_domain = "git.darkmode.duckdns.org"
[tool.semantic_release.branches.main] [tool.semantic_release.branches.main]
match = "(main|master)" match = "(main|master)"