9 Commits
Author SHA1 Message Date
sirmrak 5016165c10 Update .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 5s
2026-07-24 13:57:05 +03:00
sirmrak e56f297f6d Update pyproject.toml
Update Version / Update Version (push) Successful in 5s
2026-07-24 13:42:20 +03:00
sirmrak 0860d19813 Update .github/workflows/release.yml
Update Version / Update Version (push) Successful in 5s
2026-07-24 13:41:28 +03:00
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
7 changed files with 164 additions and 52 deletions
+3 -26
View File
@@ -5,14 +5,13 @@ on:
branches: branches:
- main - main
- master - master
- dev
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 +40,7 @@ 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
semantic-release version
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
+36 -11
View File
@@ -4,10 +4,9 @@ on:
push: push:
tags: tags:
- 'v*' - 'v*'
workflow_dispatch: # Позволяет запускать вручную workflow_dispatch:
jobs: jobs:
# Шаг 1: Валидация структуры HA интеграции
validate: validate:
name: Validate Integration name: Validate Integration
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -23,7 +22,6 @@ jobs:
with: with:
category: "integration" category: "integration"
# Шаг 2: Запуск тестов (если есть)
test: test:
name: Run Tests name: Run Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -51,7 +49,6 @@ jobs:
echo "No tests directory found, skipping tests" echo "No tests directory found, skipping tests"
fi fi
# Шаг 3: Проверка стиля кода
lint: lint:
name: Code Quality name: Code Quality
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -77,7 +74,6 @@ jobs:
run: | run: |
ruff check custom_components/motion_light/ || echo "Ruff issues found" ruff check custom_components/motion_light/ || echo "Ruff issues found"
# Шаг 4: Создание релиза (только если все проверки пройдены)
release: release:
name: Create Release name: Create Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -97,6 +93,15 @@ jobs:
TAG=${GITHUB_REF#refs/tags/} TAG=${GITHUB_REF#refs/tags/}
echo "tag=$TAG" >> $GITHUB_OUTPUT echo "tag=$TAG" >> $GITHUB_OUTPUT
# Проверяем, это beta или stable релиз
if [[ "$TAG" == *"beta"* ]]; then
echo "is_beta=true" >> $GITHUB_OUTPUT
PRERELEASE_FLAG="--prerelease"
else
echo "is_beta=false" >> $GITHUB_OUTPUT
PRERELEASE_FLAG=""
fi
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then if [ -z "$PREV_TAG" ]; then
@@ -108,25 +113,45 @@ jobs:
echo "commits<<EOF" >> $GITHUB_OUTPUT echo "commits<<EOF" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release - name: Create GitHub Release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
TAG="${{ steps.tag.outputs.tag }}" TAG="${{ steps.tag.outputs.tag }}"
COMMITS="${{ steps.tag.outputs.commits }}" COMMITS="${{ steps.tag.outputs.commits }}"
IS_BETA="${{ steps.tag.outputs.is_beta }}"
echo "🚀 Создаём релиз $TAG в GitHub..." echo "🚀 Создаём релиз $TAG в GitHub..."
echo "Тип релиза: IS_BETA=$IS_BETA"
# Определяем заголовок
if [ "$IS_BETA" = "true" ]; then
TITLE="Beta Release $TAG"
PRERELEASE_FLAG="--prerelease"
else
TITLE="Release $TAG"
PRERELEASE_FLAG=""
fi
cat > /tmp/release_notes.md << EOF cat > /tmp/release_notes.md << EOF
## Изменения в версии $TAG ## $TITLE
$COMMITS $COMMITS
EOF EOF
gh release create "$TAG" \ # Создаём релиз
--title "Release $TAG" \ if [ -n "$PRERELEASE_FLAG" ]; then
--notes-file /tmp/release_notes.md \ gh release create "$TAG" \
--target main --title "$TITLE" \
--notes-file /tmp/release_notes.md \
--target main \
--prerelease
else
gh release create "$TAG" \
--title "$TITLE" \
--notes-file /tmp/release_notes.md \
--target main
fi
echo "✅ Релиз создан!" echo "✅ Релиз создан!"
+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
@@ -91,6 +91,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 = []
@@ -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."""
@@ -381,6 +440,10 @@ class MotionRecorderCoordinator(DataUpdateCoordinator):
if not self._enabled: if not self._enabled:
_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)
@@ -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"
} }
+10 -3
View File
@@ -7,18 +7,25 @@ version_variables = [
commit_message = "chore(release): v{version}" commit_message = "chore(release): v{version}"
# ВАЖНО: не создаём релиз в Gitea (релиз будет в GitHub) # Отключаем создание релиза через API Gitea (релиз будет создаваться в GitHub)
upload_to_vcs_release = false 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"
# Настройки для stable релизов (ветка main)
[tool.semantic_release.branches.main] [tool.semantic_release.branches.main]
match = "(main|master)" match = "(main|master)"
prerelease = false prerelease = false
# Настройки для beta релизов (ветка dev)
[tool.semantic_release.branches.dev]
match = "dev"
prerelease = true
prerelease_token = "beta"
[tool.semantic_release.commit_parser_options] [tool.semantic_release.commit_parser_options]
allowed_tags = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "revert"] allowed_tags = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "revert"]
minor_tags = ["feat"] minor_tags = ["feat"]