Compare commits

...
42 Commits
Author SHA1 Message Date
semantic-release 584d7e6669 chore(release): v1.2.0-beta.1
Update Version / Update Version (push) Successful in 5s
2026-07-22 06:30:58 +00:00
sirmrak 3d5fd50c95 feat: major FSM improvements, optimization and bug fixes
Update Version / Update Version (push) Successful in 6s
- Fix manual override detection using context prefix matching
- Add force stop sensor with STATE_FORCE_STOPPED mode
- Implement smart manual mode (turn on remaining lights on motion)
- Fix lux_cooldown activation on all light turn-offs
- Fix extend sensor re-triggering in STATE_ACTIVE
- Add conditional lux entities creation
- Allow manual_idle_timeout = 0 to disable timer
- Optimize code with universal timer and sensor methods
- Add integration name prefix to logs
- Support light domain in addition to switch
2026-07-22 09:30:51 +03:00
sirmrak 283a16082b Merge pull request 'Main' (#2) from main into dev
Update Version / Update Version (push) Successful in 6s
Reviewed-on: #2
2026-07-22 09:16:42 +03:00
semantic-release cfe316cca1 chore(release): v1.1.0
Update Version / Update Version (push) Successful in 5s
2026-07-22 06:14:11 +00:00
sirmrak fcc6073298 Merge pull request 'Dev' (#1) from dev into main
Update Version / Update Version (push) Successful in 7s
Reviewed-on: #1
2026-07-22 09:14:01 +03:00
sirmrak 489a868a4d ci: support beta releases in GitHub workflow
Update Version / Update Version (push) Successful in 6s
2026-07-22 09:06:46 +03:00
semantic-release 7afe64d14c chore(release): v1.1.0-beta.1
Update Version / Update Version (push) Successful in 5s
2026-07-22 06:05:53 +00:00
sirmrak c366bc457a ci: add dev branch support for beta releases
Update Version / Update Version (push) Successful in 9s
2026-07-22 09:05:42 +03:00
sirmrak fa4d133d50 feat: add dev branch for beta releases 2026-07-22 09:04:38 +03:00
semantic-release f129191a48 chore(release): v1.0.17
Update Version / Update Version (push) Successful in 5s
2026-07-17 08:07:41 +00:00
sirmrak 31237e76d9 fix: Test
Update Version / Update Version (push) Successful in 6s
2026-07-17 10:58:44 +03:00
sirmrak fc59a786ed Обновить pyproject.toml
Update Version / Update Version (push) Successful in 5s
2026-07-17 10:57:53 +03:00
sirmrak 71cbd70f9e Обновить .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 8s
2026-07-17 10:57:16 +03:00
semantic-release f12371e519 chore(release): v1.0.16
Update Version / Update Version (push) Successful in 5s
2026-07-10 09:09:57 +00:00
sirmrak eae1c9cef2 fix: Test new secrets
Update Version / Update Version (push) Successful in 6s
2026-07-10 12:09:50 +03:00
semantic-release 8dff505bb4 chore(release): v1.0.15
Update Version / Update Version (push) Successful in 5s
2026-07-10 06:49:05 +00:00
sirmrak 342a98327d fix: lux_cooldown_active
Update Version / Update Version (push) Successful in 7s
2026-07-10 09:48:56 +03:00
sirmrak d10e83c750 Добавить LICENSE
Update Version / Update Version (push) Successful in 5s
2026-07-09 22:28:37 +03:00
semantic-release 0deb61f920 chore(release): v1.0.14
Update Version / Update Version (push) Successful in 5s
2026-07-09 19:22:51 +00:00
sirmrak 563095cbcf fix: Удалены переводы атрибутов из custom_components/motion_light/translations
Update Version / Update Version (push) Successful in 6s
2026-07-09 22:22:43 +03:00
sirmrak 952feac482 fixЖ перевод атрибутов удален
Update Version / Update Version (push) Successful in 5s
2026-07-09 22:20:23 +03:00
sirmrak 7c229b37f2 Обновить custom_components/motion_light/translations/ru.json
Update Version / Update Version (push) Successful in 5s
2026-07-09 22:11:42 +03:00
sirmrak 19546a12e6 Обновить custom_components/motion_light/translations/en.json
Update Version / Update Version (push) Successful in 5s
2026-07-09 22:09:15 +03:00
sirmrak 7b9b1030f2 Обновить custom_components/motion_light/translations/en.json
Update Version / Update Version (push) Successful in 5s
2026-07-09 22:08:45 +03:00
sirmrak 13df970b21 Обновить custom_components/motion_light/manifest.json
Update Version / Update Version (push) Successful in 5s
2026-07-09 22:05:41 +03:00
semantic-release 8069a5d635 chore(release): v1.0.13
Update Version / Update Version (push) Successful in 5s
2026-07-09 19:00:28 +00:00
sirmrak 356b61e6e6 fix: Test validation
Update Version / Update Version (push) Successful in 6s
2026-07-09 22:00:19 +03:00
sirmrak 3b93a6a16b Добавление тестов
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:59:37 +03:00
semantic-release aa7eb0a9d9 chore(release): v1.0.12
Update Version / Update Version (push) Successful in 5s
2026-07-09 18:47:22 +00:00
sirmrak 0888c63b80 fix: test
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:47:14 +03:00
sirmrak c6e7b332cf Обновить .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:45:52 +03:00
semantic-release 7b4fdfe629 chore(release): v1.0.11
Update Version / Update Version (push) Successful in 5s
2026-07-09 18:35:55 +00:00
sirmrak 15dd7e3880 fix: Test
Update Version / Update Version (push) Failing after 6s
2026-07-09 21:35:49 +03:00
sirmrak 28811370e9 Обновить .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:35:26 +03:00
sirmrak a1fca2f4d1 Обновить pyproject.toml
Update Version / Update Version (push) Successful in 6s
2026-07-09 21:34:44 +03:00
semantic-release d479bdb583 chore(release): v1.0.10
Update Version / Update Version (push) Successful in 6s
2026-07-09 18:26:23 +00:00
sirmrak 010a1144c5 fix: Test
Update Version / Update Version (push) Failing after 7s
2026-07-09 21:26:14 +03:00
sirmrak 6cf92d83f0 Обновить .gitea/workflows/update_version.yml
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:24:56 +03:00
sirmrak 9f7199beab Обновить pyproject.toml
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:23:53 +03:00
semantic-release a02ae4106b chore(release): v1.0.9
Update Version / Update Version (push) Successful in 4s
2026-07-09 18:08:02 +00:00
sirmrak 6a411cea91 fix: test
Update Version / Update Version (push) Failing after 7s
2026-07-09 21:07:53 +03:00
sirmrak 4ff72fdb07 Обновить pyproject.toml
Update Version / Update Version (push) Successful in 5s
2026-07-09 21:07:03 +03:00
10 changed files with 562 additions and 713 deletions
+6 -7
View File
@@ -5,14 +5,14 @@ on:
branches:
- main
- master
env:
GIT_SSL_NO_VERIFY: true
- dev
jobs:
update-version:
name: Update Version
runs-on: ubuntu-latest
# Пропускаем запуск, если коммит сделал сам бот (избегаем бесконечного цикла)
if: github.actor != 'gitea-actions[bot]' && github.actor != 'Gitea Actions Bot'
permissions:
contents: write
@@ -39,9 +39,8 @@ jobs:
- name: Run semantic-release
env:
GITEA_TOKEN: ${{ secrets.PAT_TOKEN }}
PYTHONHTTPSVERIFY: "0"
REQUESTS_CA_BUNDLE: ""
CURL_CA_BUNDLE: ""
run: |
echo "🚀 Запускаем semantic-release..."
unset GITHUB_SERVER_URL
unset GITHUB_API_URL
unset GITHUB_REPOSITORY
semantic-release version
+109 -10
View File
@@ -1,14 +1,83 @@
name: Create Release
name: Test and Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
release:
name: Create GitHub Release
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"
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
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"
release:
name: Create Release
runs-on: ubuntu-latest
needs: [validate, test, lint]
permissions:
contents: write
@@ -24,9 +93,25 @@ jobs:
TAG=${GITHUB_REF#refs/tags/}
echo "tag=$TAG" >> $GITHUB_OUTPUT
TAG_MSG=$(git tag -l --format='%(contents)' $TAG 2>/dev/null || git log -1 --pretty=%B)
echo "message<<EOF" >> $GITHUB_OUTPUT
echo "$TAG_MSG" >> $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 "")
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
@@ -34,13 +119,27 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.tag.outputs.tag }}"
MSG="${{ steps.tag.outputs.message }}"
COMMITS="${{ steps.tag.outputs.commits }}"
IS_BETA="${{ steps.tag.outputs.is_beta }}"
echo "🚀 Создаём релиз $TAG в GitHub..."
if [ "$IS_BETA" = "true" ]; then
TITLE="Beta Release $TAG"
else
TITLE="Release $TAG"
fi
cat > /tmp/release_notes.md << EOF
## $TITLE
$COMMITS
EOF
gh release create "$TAG" \
--title "Release $TAG" \
--notes "$MSG" \
--target main
--title "$TITLE" \
--notes-file /tmp/release_notes.md \
--target main \
${IS_BETA:+--prerelease}
echo "✅ Релиз создан!"
+91
View File
@@ -2,6 +2,97 @@
<!-- version list -->
## v1.2.0-beta.1 (2026-07-22)
### Features
- Major FSM improvements, optimization and bug fixes
([`3d5fd50`](https://git.darkmode.duckdns.org/sirmrak/motion_light/commit/3d5fd50c951c3e255e3dc73ea9f1cec87da879fe))
## v1.1.0 (2026-07-22)
## v1.1.0-beta.1 (2026-07-22)
### Features
- Add dev branch for beta releases
([`fa4d133`](https://git.darkmode.duckdns.org/sirmrak/motion_light/commit/fa4d133d50e896428883919c98ef830780aacce2))
## v1.0.17 (2026-07-17)
### Bug Fixes
- Test
([`31237e7`](https://git.darkmode.duckdns.org/sirmrak/motion_light/commit/31237e76d9c770dae4e04fad6d7a0196340a60c5))
## v1.0.16 (2026-07-10)
### Bug Fixes
- Test new secrets
([`eae1c9c`](https://192.168.200.208:3000/sirmrak/motion_light/commit/eae1c9cef21aa9285a6e5b2174949b6bf2942154))
## v1.0.15 (2026-07-10)
### Bug Fixes
- Lux_cooldown_active
([`342a983`](https://192.168.200.208:3000/sirmrak/motion_light/commit/342a98327d2297977323e12f84e0dc9bc8b3a49d))
## v1.0.14 (2026-07-09)
### Bug Fixes
- Удалены переводы атрибутов из custom_components/motion_light/translations
([`563095c`](https://192.168.200.208:3000/sirmrak/motion_light/commit/563095cbcf77d22d02a7176faf5aec6837d18b37))
## v1.0.13 (2026-07-09)
### Bug Fixes
- Test validation
([`356b61e`](https://192.168.200.208:3000/sirmrak/motion_light/commit/356b61e6e618a423503d44d2f0d9ef96fee0da0d))
## v1.0.12 (2026-07-09)
### Bug Fixes
- Test
([`0888c63`](https://192.168.200.208:3000/sirmrak/motion_light/commit/0888c63b804be1e5bedd7e72fbfd142dd4f88287))
## v1.0.11 (2026-07-09)
### Bug Fixes
- Test
([`15dd7e3`](https://192.168.200.208:3000/sirmrak/motion_light/commit/15dd7e3880dad782f502eb70db6aa8ca23f803bc))
## v1.0.10 (2026-07-09)
### Bug Fixes
- Test
([`010a114`](https://192.168.200.208:3000/sirmrak/motion_light/commit/010a1144c5dc5252831126bfc9592036a7ca2518))
## v1.0.9 (2026-07-09)
### Bug Fixes
- Test
([`6a411ce`](https://192.168.200.208:3000/sirmrak/motion_light/commit/6a411cea91047e9714519dbb190d5d92252b024a))
## v1.0.8 (2026-07-09)
### Bug Fixes
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 sirmrak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1 -1
View File
@@ -1,4 +1,4 @@
"""The Motion Light integration. Test"""
"""The Motion Light integration. Test"""
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
+155 -486
View File
@@ -35,15 +35,18 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
class PrefixLoggerAdapter(logging.LoggerAdapter):
"""Logger adapter that adds a prefix to log messages."""
def process(self, msg, kwargs):
return f"[{self.extra.get('name', 'Unknown')}] {msg}", kwargs
def _get_config_value(entry, key, default=None):
"""Get config value from options first, then from data, then default."""
return entry.options.get(key, entry.data.get(key, default))
class MotionLightCoordinator(DataUpdateCoordinator):
"""Motion Light coordinator."""
@@ -52,14 +55,9 @@ class MotionLightCoordinator(DataUpdateCoordinator):
self.entry = entry
self.logger = PrefixLoggerAdapter(_LOGGER, {"name": entry.title})
super().__init__(
hass,
self.logger,
name=f"Motion Light {entry.title}",
)
super().__init__(hass, self.logger, name=f"Motion Light {entry.title}")
self.hass = hass
self.switch_entities = _get_config_value(entry, CONF_SWITCH_ENTITIES, [])
self.main_sensors = _get_config_value(entry, CONF_MAIN_SENSORS, [])
self.extend_sensors = _get_config_value(entry, CONF_EXTEND_SENSORS, [])
@@ -95,39 +93,37 @@ class MotionLightCoordinator(DataUpdateCoordinator):
self._manual_idle_timer = None
self._manual_off_cooldown_timer = None
# Context & Flags
# Flags
self._lux_cooldown_active = False
@property
def state(self):
"""Return current state."""
return self._state
@property
def attributes(self):
"""Return current attributes."""
attrs = self._attributes.copy()
attrs["switch_entities"] = self.switch_entities
attrs["main_sensors"] = self.main_sensors
attrs["extend_sensors"] = self.extend_sensors
attrs["lux_sensor"] = self.lux_sensor
attrs["force_stop_sensor"] = self.force_stop_sensor
attrs["force_stop_state"] = self.force_stop_state
attrs["force_stop_active"] = self._state == STATE_FORCE_STOPPED
attrs["active_main_sensors"] = self._get_active_main_sensors()
attrs["active_extend_sensors"] = self._get_active_extend_sensors()
attrs["lux_value"] = self._get_current_lux()
attrs["lux_cooldown_active"] = self._lux_cooldown_active
attrs["lux_check_active"] = self._lux_check_timer is not None
attrs.update({
"switch_entities": self.switch_entities,
"main_sensors": self.main_sensors,
"extend_sensors": self.extend_sensors,
"lux_sensor": self.lux_sensor,
"force_stop_sensor": self.force_stop_sensor,
"force_stop_state": self.force_stop_state,
"force_stop_active": self._state == STATE_FORCE_STOPPED,
"active_main_sensors": self._get_active_sensors(self.main_sensors),
"active_extend_sensors": self._get_active_sensors(self.extend_sensors),
"lux_value": self._get_current_lux(),
"lux_cooldown_active": self._lux_cooldown_active,
"lux_check_active": self._lux_check_timer is not None,
})
return attrs
@property
def is_enabled(self) -> bool:
"""Return if integration is enabled."""
return self._enabled
async def set_enabled(self, enabled: bool) -> None:
"""Set enabled state."""
self._enabled = enabled
self.logger.info("Motion Light %s", "enabled" if enabled else "disabled")
if not enabled:
@@ -135,9 +131,8 @@ class MotionLightCoordinator(DataUpdateCoordinator):
await self._turn_off_switches()
self._stop_lux_check_timer()
await self._update_state(STATE_DISABLED)
else:
if self._state == STATE_DISABLED:
await self._update_state(STATE_IDLE)
elif self._state == STATE_DISABLED:
await self._update_state(STATE_IDLE)
def set_motion_filter(self, value: float) -> None:
self.motion_filter = int(value)
@@ -158,10 +153,8 @@ class MotionLightCoordinator(DataUpdateCoordinator):
def set_manual_idle_timeout(self, value: float) -> None:
self.manual_idle_timeout = int(value)
self.logger.info("Manual idle timeout set to %d sec", self.manual_idle_timeout)
if self.manual_idle_timeout <= 0 and self._manual_idle_timer:
self.logger.info("Manual idle timeout set to 0, cancelling active timer")
self._manual_idle_timer()
self._manual_idle_timer = None
if self.manual_idle_timeout <= 0:
self._cancel_timer("_manual_idle_timer")
def set_manual_off_cooldown(self, value: float) -> None:
self.manual_off_cooldown = int(value)
@@ -172,24 +165,17 @@ class MotionLightCoordinator(DataUpdateCoordinator):
self.logger.info("Respect manual set to %s", value)
async def async_setup(self):
"""Set up the coordinator."""
self.logger.info("Setting up Motion Light coordinator")
self.logger.info("Switches: %s, Main sensors: %s, Extend sensors: %s, Lux: %s",
self.switch_entities, self.main_sensors, self.extend_sensors, self.lux_sensor)
# Проверяем состояние force_stop_sensor при старте
if self.force_stop_sensor:
force_stop_state_obj = self.hass.states.get(self.force_stop_sensor)
if force_stop_state_obj and force_stop_state_obj.state == self.force_stop_state:
state_obj = self.hass.states.get(self.force_stop_sensor)
if state_obj and state_obj.state == self.force_stop_state:
self.logger.info("Force stop sensor is already in stop state at startup")
await self._update_state(STATE_FORCE_STOPPED)
await self._start_listening()
return
any_on = any(
self.hass.states.get(s) and self.hass.states.get(s).state == "on"
for s in self.switch_entities
)
any_on = any(self.hass.states.get(s) and self.hass.states.get(s).state == "on" for s in self.switch_entities)
if any_on:
self.logger.info("Some switches are ON at startup, entering manual_on")
await self._enter_manual_on()
@@ -199,644 +185,327 @@ class MotionLightCoordinator(DataUpdateCoordinator):
await self._start_listening()
async def async_shutdown(self):
"""Shut down the coordinator."""
self.logger.info("Shutting down Motion Light coordinator")
await self._stop_listening()
self._cancel_all_timers()
async def _update_state(self, state, **attributes):
"""Update state and attributes."""
old_state = self._state
self._state = state
self._attributes.update(attributes)
self.logger.debug("State changed: %s → %s", old_state, state)
self.async_set_updated_data({
"state": state,
"attributes": self.attributes,
})
self.async_set_updated_data({"state": state, "attributes": self.attributes})
async def _start_listening(self):
"""Start listening to sensors."""
self.logger.info("Starting to listen to main sensors: %s", self.main_sensors)
for sensor in self.main_sensors:
unsub = async_track_state_change_event(
self.hass, [sensor], self._main_sensor_state_changed
)
self._unsub_main_listeners.append(unsub)
self.logger.info("Starting to listen to extend sensors: %s", self.extend_sensors)
self._unsub_main_listeners.append(async_track_state_change_event(self.hass, [sensor], self._main_sensor_state_changed))
for sensor in self.extend_sensors:
unsub = async_track_state_change_event(
self.hass, [sensor], self._extend_sensor_state_changed
)
self._unsub_extend_listeners.append(unsub)
self.logger.info("Starting to listen to target switches: %s", self.switch_entities)
self._unsub_extend_listeners.append(async_track_state_change_event(self.hass, [sensor], self._extend_sensor_state_changed))
for switch in self.switch_entities:
unsub = async_track_state_change_event(
self.hass, [switch], self._target_state_changed
)
self._unsub_target_listeners.append(unsub)
self._unsub_target_listeners.append(async_track_state_change_event(self.hass, [switch], self._target_state_changed))
if self.force_stop_sensor:
self.logger.info("Force stop sensor: %s → %s",
self.force_stop_sensor, self.force_stop_state)
self._unsub_force_stop_listener = async_track_state_change_event(
self.hass, [self.force_stop_sensor], self._force_stop_state_changed
)
self._unsub_force_stop_listener = async_track_state_change_event(self.hass, [self.force_stop_sensor], self._force_stop_state_changed)
async def _stop_listening(self):
"""Stop listening to sensors."""
for unsub in self._unsub_main_listeners:
for unsub in self._unsub_main_listeners + self._unsub_extend_listeners + self._unsub_target_listeners:
unsub()
self._unsub_main_listeners = []
for unsub in self._unsub_extend_listeners:
unsub()
self._unsub_extend_listeners = []
for unsub in self._unsub_target_listeners:
unsub()
self._unsub_target_listeners = []
self._unsub_main_listeners = self._unsub_extend_listeners = self._unsub_target_listeners = []
if self._unsub_force_stop_listener:
self._unsub_force_stop_listener()
self._unsub_force_stop_listener = None
@callback
def _main_sensor_state_changed(self, event):
"""Handle main sensor state change."""
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
if not new_state:
return
sensor_entity = event.data.get("entity_id")
old_s = old_state.state if old_state else None
if not new_state: return
old_s = event.data.get("old_state").state if event.data.get("old_state") else None
new_s = new_state.state
self.logger.debug("Main sensor event: %s %s → %s", sensor_entity, old_s, new_s)
if new_s == "on" and old_s != "on":
self.logger.debug("Main sensor FRONT UP: %s", sensor_entity)
self.hass.async_create_task(self._handle_main_sensor_on(sensor_entity))
self.hass.async_create_task(self._handle_sensor_on(event.data["entity_id"], is_main=True))
elif old_s == "on" and new_s != "on":
self.logger.debug("Main sensor FRONT DOWN: %s", sensor_entity)
self.hass.async_create_task(self._handle_main_sensor_off())
@callback
def _extend_sensor_state_changed(self, event):
"""Handle extend sensor state change."""
old_state = event.data.get("old_state")
new_state = event.data.get("new_state")
if not new_state:
return
sensor_entity = event.data.get("entity_id")
old_s = old_state.state if old_state else None
if not new_state: return
old_s = event.data.get("old_state").state if event.data.get("old_state") else None
new_s = new_state.state
self.logger.debug("Extend sensor event: %s %s → %s", sensor_entity, old_s, new_s)
if new_s == "on" and old_s != "on":
self.logger.debug("Extend sensor FRONT UP: %s", sensor_entity)
self.hass.async_create_task(self._handle_extend_sensor_on(sensor_entity))
self.hass.async_create_task(self._handle_sensor_on(event.data["entity_id"], is_main=False))
async def _handle_main_sensor_on(self, triggered_by):
"""Handle main sensor ON - can turn on light."""
if not self._enabled:
self.logger.debug("Integration disabled, ignoring")
async def _handle_sensor_on(self, triggered_by: str, is_main: bool):
"""Universal handler for sensor ON events (main or extend)."""
if not self._enabled or self._state == STATE_FORCE_STOPPED or self._state == STATE_MANUAL_OFF_COOLDOWN:
return
# БЛОКИРОВЩИК: Игнорируем движение, если активна принудительная остановка
if self._state == STATE_FORCE_STOPPED:
self.logger.debug("Ignoring during force_stopped")
return
self.logger.debug("Main sensor ON by %s, current state: %s", triggered_by, self._state)
if self._state == STATE_MANUAL_OFF_COOLDOWN:
self.logger.debug("Ignoring during manual_off_cooldown")
return
sensor_type = "Main" if is_main else "Extend"
self.logger.debug("%s sensor ON by %s, current state: %s", sensor_type, triggered_by, self._state)
if self._state == STATE_MANUAL_ON:
self.logger.debug("During manual_on, resetting idle timer")
if is_main:
all_on = all(self.hass.states.get(s) and self.hass.states.get(s).state == "on" for s in self.switch_entities)
if not all_on:
self.logger.info("Motion during manual_on, turning on remaining switches and switching to auto mode.")
self._cancel_timer("_manual_idle_timer")
await self._turn_on_switches()
await self._update_state(STATE_ON, triggered_by=triggered_by)
return
if self.manual_idle_timeout > 0:
self._reset_manual_idle_timer()
return
if self._state in (STATE_ON, STATE_ACTIVE):
self.logger.debug("Already on/active, updating time")
if self._state == STATE_ON:
self._attributes["last_motion_time"] = dt_util.utcnow().isoformat()
return
if self._state == STATE_ACTIVE:
self._attributes["last_motion_time"] = dt_util.utcnow().isoformat()
self._cancel_timer("_off_delay_timer")
self._start_timer("_off_delay_timer", self.off_delay, self._turn_off_after_delay)
return
if self._state == STATE_DETECTING:
self.logger.debug("Already detecting, ignoring")
return
if self._state == STATE_DELAYING:
self.logger.info("Main sensor ON during off-delay, extending")
if self._off_delay_timer:
self._off_delay_timer()
self._off_delay_timer = None
self.logger.info("%s sensor ON during off-delay, extending", sensor_type)
self._cancel_timer("_off_delay_timer")
await self._update_state(STATE_ACTIVE)
if self.off_delay > 0:
self._off_delay_timer = async_call_later(
self.hass,
self.off_delay,
self._turn_off_after_delay,
)
self._start_timer("_off_delay_timer", self.off_delay, self._turn_off_after_delay)
return
if self._off_delay_timer:
self._off_delay_timer()
self._off_delay_timer = None
# IDLE state
if not is_main:
return # Extend sensors cannot turn on light from idle
await self._update_state(STATE_DETECTING, triggered_by=triggered_by)
if self.motion_filter > 0:
self.logger.debug("Starting motion filter timer for %d seconds", self.motion_filter)
try:
self._motion_detect_timer = async_call_later(
self.hass,
self.motion_filter,
self._turn_on_after_filter,
)
except Exception as err:
self.logger.error("Failed to create motion filter timer: %s", err)
await self._turn_on_after_filter(None)
self._start_timer("_motion_detect_timer", self.motion_filter, self._turn_on_after_filter)
else:
await self._turn_on_after_filter(None)
async def _handle_main_sensor_off(self):
"""Handle main sensor OFF - start delay."""
self.logger.debug("Main sensor OFF, current state: %s", self._state)
if self._is_any_main_sensor_active():
self.logger.debug("Some main sensor still active, ignoring")
if self._is_any_sensor_from_list_active(self.main_sensors):
return
self.logger.debug("All main sensors are off")
self._stop_lux_check_timer()
if self._state in (STATE_ON, STATE_ACTIVE):
self.logger.debug("Transitioning to DELAYING state")
await self._update_state(STATE_DELAYING)
if self._off_delay_timer:
self._off_delay_timer()
self._off_delay_timer = None
if self.off_delay > 0:
self.logger.debug("Starting off-delay timer for %d seconds", self.off_delay)
try:
self._off_delay_timer = async_call_later(
self.hass,
self.off_delay,
self._turn_off_after_delay,
)
except Exception as err:
self.logger.error("Failed to create off-delay timer: %s", err)
await self._turn_off_after_delay(None)
else:
self.logger.debug("No off-delay, turning off immediately")
await self._turn_off_after_delay(None)
self._cancel_timer("_off_delay_timer")
self._start_timer("_off_delay_timer", self.off_delay, self._turn_off_after_delay)
elif self._state == STATE_DETECTING:
if self._motion_detect_timer:
self._motion_detect_timer()
self._motion_detect_timer = None
self._cancel_timer("_motion_detect_timer")
await self._update_state(STATE_IDLE)
async def _handle_extend_sensor_on(self, triggered_by):
"""Handle extend sensor ON - only extend, never turn on."""
if not self._enabled:
self.logger.debug("Integration disabled, ignoring")
return
# БЛОКИРОВЩИК: Игнорируем движение, если активна принудительная остановка
if self._state == STATE_FORCE_STOPPED:
self.logger.debug("Ignoring during force_stopped")
return
self.logger.debug("Extend sensor ON by %s, current state: %s", triggered_by, self._state)
if self._state == STATE_IDLE:
self.logger.debug("Extend sensor ignored: in idle state")
return
if self._state == STATE_MANUAL_OFF_COOLDOWN:
self.logger.debug("Ignoring during manual_off_cooldown")
return
if self._state == STATE_MANUAL_ON:
self.logger.debug("During manual_on, resetting idle timer")
if self.manual_idle_timeout > 0:
self._reset_manual_idle_timer()
return
if self._state in (STATE_ON, STATE_ACTIVE):
self.logger.debug("Already on/active, updating time")
self._attributes["last_motion_time"] = dt_util.utcnow().isoformat()
return
if self._state == STATE_DETECTING:
self.logger.debug("Already detecting, ignoring")
return
if self._state == STATE_DELAYING:
self.logger.info("Extend sensor ON during off-delay, extending")
if self._off_delay_timer:
self._off_delay_timer()
self._off_delay_timer = None
await self._update_state(STATE_ACTIVE)
if self.off_delay > 0:
self._off_delay_timer = async_call_later(
self.hass,
self.off_delay,
self._turn_off_after_delay,
)
return
@callback
def _target_state_changed(self, event):
"""Handle target switch state change."""
if not self.respect_manual:
if not self.respect_manual or self._state == STATE_FORCE_STOPPED:
return
# БЛОКИРОВЩИК: Игнорируем ручные переключения, если активна принудительная остановка
if self._state == STATE_FORCE_STOPPED:
self.logger.debug("Ignoring manual change during force_stopped")
return
new_state = event.data.get("new_state")
old_state = event.data.get("old_state")
if not new_state or not old_state:
return
if new_state.state == old_state.state:
if not new_state or not old_state or new_state.state == old_state.state:
return
ctx = new_state.context
if ctx and ctx.id and ctx.id.startswith(f"motion_light_{self.entry.entry_id}_"):
self.logger.debug("State change from our context (%s), ignoring", ctx.id)
return
entity_id = event.data["entity_id"]
self.logger.info("Manual change detected on %s: %s → %s",
entity_id, old_state.state, new_state.state)
self.logger.info("Manual change detected on %s: %s → %s", entity_id, old_state.state, new_state.state)
if new_state.state == "off" and old_state.state == "on":
if self._state in (STATE_ON, STATE_DELAYING, STATE_ACTIVE, STATE_MANUAL_ON):
self.logger.info("Manual OFF during our control → entering cooldown")
self._cancel_off_delay()
self._cancel_timer("_off_delay_timer")
self._cancel_timer("_manual_idle_timer")
self._stop_lux_check_timer()
if self._manual_idle_timer:
self._manual_idle_timer()
self._manual_idle_timer = None
self.hass.async_create_task(self._enter_manual_off_cooldown())
return
if new_state.state == "on" and old_state.state == "off":
elif new_state.state == "on" and old_state.state == "off":
if self._state in (STATE_IDLE, STATE_DETECTING, STATE_MANUAL_OFF_COOLDOWN):
self.logger.info("Manual ON → entering manual_on with idle timeout")
self._cancel_motion_filter()
self._cancel_timer("_motion_detect_timer")
self._stop_lux_check_timer()
self.hass.async_create_task(self._enter_manual_on())
return
@callback
def _force_stop_state_changed(self, event):
"""Handle force stop sensor state change."""
new_state = event.data.get("new_state")
if not new_state:
return
# Если датчик перешел в блокирующее состояние
if not new_state: return
if new_state.state == self.force_stop_state:
self.logger.info("Force stop triggered by %s", event.data.get("entity_id"))
self.hass.async_create_task(self._enter_force_stop())
# Если датчик вышел из блокирующего состояния
else:
self.logger.info("Force stop released by %s", event.data.get("entity_id"))
self.hass.async_create_task(self._exit_force_stop())
async def _enter_force_stop(self):
"""Enter force_stopped state."""
self.logger.info("Entering force_stopped state")
self._cancel_all_timers()
if self._state in (STATE_ON, STATE_DELAYING, STATE_ACTIVE, STATE_MANUAL_ON):
await self._turn_off_switches()
await self._update_state(STATE_FORCE_STOPPED)
async def _exit_force_stop(self):
"""Exit force_stopped state to idle."""
self.logger.info("Exiting force_stopped state")
await self._update_state(STATE_IDLE)
# --- Универсальные методы для сенсоров ---
def _is_any_sensor_from_list_active(self, sensors: list) -> bool:
return any(self.hass.states.get(s) and self.hass.states.get(s).state == "on" for s in sensors)
def _get_active_sensors(self, sensors: list) -> list:
return [s for s in sensors if self.hass.states.get(s) and self.hass.states.get(s).state == "on"]
def _is_any_main_sensor_active(self) -> bool:
"""Check if any main sensor is currently in 'on' state."""
for sensor in self.main_sensors:
state = self.hass.states.get(sensor)
if state and state.state == "on":
return True
return False
return self._is_any_sensor_from_list_active(self.main_sensors)
def _is_any_extend_sensor_active(self) -> bool:
"""Check if any extend sensor is currently in 'on' state."""
for sensor in self.extend_sensors:
state = self.hass.states.get(sensor)
if state and state.state == "on":
return True
return False
return self._is_any_sensor_from_list_active(self.extend_sensors)
def _is_any_sensor_active(self) -> bool:
"""Check if any sensor (main or extend) is active."""
return self._is_any_main_sensor_active() or self._is_any_extend_sensor_active()
def _get_active_main_sensors(self) -> list:
"""Return list of active main sensors."""
return [
s for s in self.main_sensors
if self.hass.states.get(s) and self.hass.states.get(s).state == "on"
]
def _get_active_extend_sensors(self) -> list:
"""Return list of active extend sensors."""
return [
s for s in self.extend_sensors
if self.hass.states.get(s) and self.hass.states.get(s).state == "on"
]
# --- Lux и таймеры ---
def _get_current_lux(self) -> float | None:
"""Get current lux value from sensor."""
if not self.lux_sensor:
return None
if not self.lux_sensor: return None
state = self.hass.states.get(self.lux_sensor)
if not state or state.state in ("unknown", "unavailable"):
return None
try:
return float(state.state)
except (ValueError, TypeError):
return None
if not state or state.state in ("unknown", "unavailable"): return None
try: return float(state.state)
except (ValueError, TypeError): return None
async def _should_turn_on_by_lux(self) -> bool:
"""Check if we should turn on based on lux."""
if not self.lux_sensor:
return True
if self._lux_cooldown_active:
self.logger.debug("Lux cooldown active, ignoring sensor")
return True
if not self.lux_sensor or self._lux_cooldown_active: return True
lux = self._get_current_lux()
if lux is None:
return True
return lux < self.lux_threshold
return lux is None or lux < self.lux_threshold
def _start_lux_check_timer(self):
"""Start periodic lux check timer."""
if not self.lux_sensor:
self.logger.debug("No lux sensor, not starting lux check timer")
return
if self._lux_check_timer:
self.logger.debug("Lux check timer already running")
return
self.logger.info("Starting lux check timer (interval=%d sec)", LUX_CHECK_INTERVAL)
self._lux_check_timer = async_call_later(
self.hass,
LUX_CHECK_INTERVAL,
self._check_lux_periodically,
)
self.async_set_updated_data({
"state": self._state,
"attributes": self.attributes,
})
if not self.lux_sensor or self._lux_check_timer: return
self._lux_check_timer = async_call_later(self.hass, LUX_CHECK_INTERVAL, self._check_lux_periodically)
self.async_set_updated_data({"state": self._state, "attributes": self.attributes})
def _stop_lux_check_timer(self):
"""Stop periodic lux check timer."""
if self._lux_check_timer:
self.logger.debug("Stopping lux check timer")
self._lux_check_timer()
self._lux_check_timer = None
self.async_set_updated_data({
"state": self._state,
"attributes": self.attributes,
})
self._cancel_timer("_lux_check_timer")
self.async_set_updated_data({"state": self._state, "attributes": self.attributes})
def _start_lux_cooldown(self):
self._lux_cooldown_active = True
if self.lux_cooldown > 0:
self._cancel_timer("_lux_cooldown_timer")
self._lux_cooldown_timer = async_call_later(self.hass, self.lux_cooldown, self._lux_cooldown_expired)
self.async_set_updated_data({"state": self._state, "attributes": self.attributes})
def _cancel_lux_cooldown(self):
if self._lux_cooldown_active:
self._lux_cooldown_active = False
self._cancel_timer("_lux_cooldown_timer")
self.async_set_updated_data({"state": self._state, "attributes": self.attributes})
@callback
def _check_lux_periodically(self, _=None):
"""Check lux periodically."""
self._lux_check_timer = None
if not self._is_any_sensor_active():
self.logger.debug("No sensors active, not checking lux")
if not self._is_any_sensor_active() or self._state not in (STATE_IDLE, STATE_DETECTING):
return
if self._state not in (STATE_IDLE, STATE_DETECTING):
self.logger.debug("State is %s, not checking lux", self._state)
return
lux = self._get_current_lux()
if lux is None:
self.logger.debug("Lux value unavailable, restarting timer")
self._start_lux_check_timer()
return
self.logger.debug("[Lux check] lux=%.1f, threshold=%d", lux, self.lux_threshold)
if lux < self.lux_threshold:
self.logger.info("[Lux check] Dark enough, turning on lights")
self.hass.async_create_task(self._turn_on_after_lux_check())
else:
self.logger.debug("[Lux check] Still bright, restarting timer")
self._start_lux_check_timer()
async def _turn_on_after_lux_check(self):
"""Turn on lights after periodic lux check."""
if self._lux_cooldown_active:
self.logger.debug("Lux cooldown active, not turning on")
if self._lux_cooldown_active or self._state not in (STATE_IDLE, STATE_DETECTING) or not self._is_any_sensor_active():
return
if self._state not in (STATE_IDLE, STATE_DETECTING):
self.logger.debug("State changed to %s, not turning on", self._state)
return
if not self._is_any_sensor_active():
self.logger.debug("No sensors active, not turning on")
return
triggered_by = self._attributes.get("triggered_by")
self._cancel_lux_cooldown()
await self._turn_on_switches()
await self._update_state(STATE_ON, triggered_by=triggered_by)
await self._update_state(STATE_ON, triggered_by=self._attributes.get("triggered_by"))
async def _turn_on_after_filter(self, _=None):
"""Turn on after motion filter."""
self.logger.debug("Turning on after filter")
self._motion_detect_timer = None
self._cancel_timer("_motion_detect_timer")
if not await self._should_turn_on_by_lux():
self.logger.debug("Too bright, not turning on — starting lux check timer")
await self._update_state(STATE_IDLE)
self._start_lux_check_timer()
return
triggered_by = self._attributes.get("triggered_by")
self._cancel_lux_cooldown()
await self._turn_on_switches()
await self._update_state(STATE_ON, triggered_by=triggered_by)
await self._update_state(STATE_ON, triggered_by=self._attributes.get("triggered_by"))
async def _turn_off_after_delay(self, _=None):
"""Turn off after off-delay."""
self.logger.debug("Off-delay timer expired")
self._off_delay_timer = None
self._cancel_timer("_off_delay_timer")
if self._is_any_sensor_active():
self.logger.info("Sensor is still active — staying on")
await self._update_state(STATE_ON)
return
self.logger.info("No sensors active, turning off")
await self._turn_off_switches()
self._lux_cooldown_active = True
if self.lux_cooldown > 0:
self._lux_cooldown_timer = async_call_later(
self.hass,
self.lux_cooldown,
self._lux_cooldown_expired,
)
self.logger.info("Lux cooldown started for %d sec", self.lux_cooldown)
await self._start_lux_cooldown()
await self._update_state(STATE_OFF)
async_call_later(self.hass, 1, self._transition_to_idle)
@callback
def _transition_to_idle(self, _=None):
"""Transition from off to idle."""
if self._state == STATE_OFF:
self.logger.debug("Transitioning from off to idle")
self.hass.async_create_task(self._update_state(STATE_IDLE))
# --- Управление переключателями ---
async def _turn_on_switches(self) -> None:
"""Turn on switches/lights with our context."""
ctx = Context(
id=f"motion_light_{self.entry.entry_id}_{dt_util.utcnow().timestamp()}"
)
self.logger.info("Turning ON entities: %s", self.switch_entities)
await self.hass.services.async_call(
"homeassistant",
"turn_on",
{"entity_id": self.switch_entities},
context=ctx,
)
ctx = Context(id=f"motion_light_{self.entry.entry_id}_{dt_util.utcnow().timestamp()}")
await self.hass.services.async_call("homeassistant", "turn_on", {"entity_id": self.switch_entities}, context=ctx)
async def _turn_off_switches(self) -> None:
"""Turn off switches/lights with our context."""
ctx = Context(
id=f"motion_light_{self.entry.entry_id}_{dt_util.utcnow().timestamp()}"
)
self.logger.info("Turning OFF entities: %s", self.switch_entities)
await self.hass.services.async_call(
"homeassistant",
"turn_off",
{"entity_id": self.switch_entities},
context=ctx,
)
ctx = Context(id=f"motion_light_{self.entry.entry_id}_{dt_util.utcnow().timestamp()}")
await self.hass.services.async_call("homeassistant", "turn_off", {"entity_id": self.switch_entities}, context=ctx)
# --- Ручное управление ---
async def _enter_manual_on(self):
"""Enter manual_on state."""
self._cancel_lux_cooldown()
await self._update_state(STATE_MANUAL_ON)
if self.manual_idle_timeout > 0:
self._reset_manual_idle_timer()
else:
self.logger.info("Manual idle timeout is 0. 'Forgot to turn off' timer is disabled.")
def _reset_manual_idle_timer(self):
"""Reset manual idle timer."""
if self.manual_idle_timeout <= 0:
return
if self._manual_idle_timer:
self._manual_idle_timer()
self._manual_idle_timer = async_call_later(
self.hass,
self.manual_idle_timeout,
self._manual_idle_timeout_expired,
)
self.logger.debug("Manual idle timer reset for %d sec", self.manual_idle_timeout)
if self.manual_idle_timeout <= 0: return
self._cancel_timer("_manual_idle_timer")
self._manual_idle_timer = async_call_later(self.hass, self.manual_idle_timeout, self._manual_idle_timeout_expired)
@callback
def _manual_idle_timeout_expired(self, _=None):
"""Manual idle timeout expired — turn off."""
self._manual_idle_timer = None
self.logger.info("Manual idle timeout expired, turning off")
self.hass.async_create_task(self._turn_off_switches_manual())
async def _turn_off_switches_manual(self):
"""Turn off switches/lights in manual mode."""
self.logger.info("Turning OFF entities (manual timeout): %s", self.switch_entities)
await self.hass.services.async_call(
"homeassistant",
"turn_off",
{"entity_id": self.switch_entities},
)
await self.hass.services.async_call("homeassistant", "turn_off", {"entity_id": self.switch_entities})
await self._start_lux_cooldown()
await self._update_state(STATE_OFF)
async_call_later(self.hass, 1, self._transition_to_idle)
async def _enter_manual_off_cooldown(self):
"""Enter manual_off_cooldown state."""
await self._start_lux_cooldown()
await self._update_state(STATE_MANUAL_OFF_COOLDOWN)
if self._manual_off_cooldown_timer:
self._manual_off_cooldown_timer()
if self.manual_off_cooldown > 0:
self._manual_off_cooldown_timer = async_call_later(
self.hass,
self.manual_off_cooldown,
self._manual_off_cooldown_expired,
)
self.logger.info("Manual off cooldown started for %d sec",
self.manual_off_cooldown)
self._start_timer("_manual_off_cooldown_timer", self.manual_off_cooldown, self._manual_off_cooldown_expired)
else:
await self._update_state(STATE_IDLE)
@callback
def _manual_off_cooldown_expired(self, _=None):
"""Manual off cooldown expired."""
self._manual_off_cooldown_timer = None
self.logger.info("Manual off cooldown expired → idle")
self.hass.async_create_task(self._update_state(STATE_IDLE))
@callback
def _lux_cooldown_expired(self, _=None):
"""Lux cooldown expired."""
self._lux_cooldown_active = False
self._lux_cooldown_timer = None
self.logger.info("Lux cooldown expired")
self.async_set_updated_data({
"state": self._state,
"attributes": self.attributes,
})
self._cancel_timer("_lux_cooldown_timer")
self.async_set_updated_data({"state": self._state, "attributes": self.attributes})
# --- Универсальные методы управления таймерами ---
def _cancel_timer(self, timer_attr: str):
timer = getattr(self, timer_attr, None)
if timer:
timer()
setattr(self, timer_attr, None)
def _start_timer(self, timer_attr: str, delay: float, callback):
if delay > 0:
setattr(self, timer_attr, async_call_later(self.hass, delay, callback))
def _cancel_all_timers(self):
"""Cancel all timers."""
if self._motion_detect_timer:
self._motion_detect_timer()
self._motion_detect_timer = None
if self._off_delay_timer:
self._off_delay_timer()
self._off_delay_timer = None
if self._lux_cooldown_timer:
self._lux_cooldown_timer()
self._lux_cooldown_timer = None
if self._lux_check_timer:
self._lux_check_timer()
self._lux_check_timer = None
if self._manual_idle_timer:
self._manual_idle_timer()
self._manual_idle_timer = None
if self._manual_off_cooldown_timer:
self._manual_off_cooldown_timer()
self._manual_off_cooldown_timer = None
def _cancel_motion_filter(self):
"""Cancel motion filter timer."""
if self._motion_detect_timer:
self._motion_detect_timer()
self._motion_detect_timer = None
def _cancel_off_delay(self):
"""Cancel off-delay timer."""
if self._off_delay_timer:
self._off_delay_timer()
self._off_delay_timer = None
self._cancel_timer("_motion_detect_timer")
self._cancel_timer("_off_delay_timer")
self._cancel_timer("_lux_cooldown_timer")
self._cancel_timer("_lux_check_timer")
self._cancel_timer("_manual_idle_timer")
self._cancel_timer("_manual_off_cooldown_timer")
+10 -10
View File
@@ -1,12 +1,12 @@
{
"domain": "motion_light",
"name": "Motion Light",
"version": "1.0.8",
"documentation": "https://github.com/sirmrak/motion_light",
"issue_tracker": "https://github.com/sirmrak/motion_light/issues",
"dependencies": [],
"codeowners": ["@sirmrak"],
"requirements": [],
"iot_class": "local_push",
"config_flow": true
"domain": "motion_light",
"name": "Motion Light",
"codeowners": ["@sirmrak"],
"config_flow": true,
"dependencies": [],
"documentation": "https://github.com/sirmrak/motion_light",
"iot_class": "local_push",
"issue_tracker": "https://github.com/sirmrak/motion_light/issues",
"requirements": [],
"version": "1.2.0-beta.1"
}
@@ -1,101 +1,85 @@
{
"config": {
"step": {
"user": {
"title": "Motion Light",
"description": "Motion-based lighting control setup",
"data": {
"name": "Name",
"main_sensors": "Main triggers",
"extend_sensors": "Extend sensors (optional)",
"switch_entities": "Controlled switches",
"lux_sensor": "Illuminance sensor (optional)",
"force_stop_sensor": "Force stop sensor (optional)",
"force_stop_state": "Force stop sensor state",
"log_level": "Log level"
"config": {
"step": {
"user": {
"title": "Motion Light",
"description": "Motion-based lighting control setup",
"data": {
"name": "Name",
"main_sensors": "Main triggers",
"extend_sensors": "Extend sensors (optional)",
"switch_entities": "Controlled switches",
"lux_sensor": "Illuminance sensor (optional)",
"force_stop_sensor": "Force stop sensor (optional)",
"force_stop_state": "Force stop sensor state",
"log_level": "Log level"
}
}
}
}
}
},
"options": {
"step": {
"init": {
"title": "Motion Light Options",
"description": "Change integration parameters",
"data": {
"main_sensors": "Main triggers",
"extend_sensors": "Extend sensors (optional)",
"switch_entities": "Controlled switches",
"lux_sensor": "Illuminance sensor (optional)",
"force_stop_sensor": "Force stop sensor (optional)",
"force_stop_state": "Force stop sensor state",
"log_level": "Log level"
}
}
}
},
"entity": {
"switch": {
"motion_light_enabled": {
"name": "Control"
},
"motion_light_priority_manual": {
"name": "Manual priority"
}
},
"sensor": {
"motion_light_status": {
"name": "Status",
"state": {
"idle": "Idle",
"detecting": "Detecting",
"on": "On",
"delaying": "Delaying",
"active": "Extended",
"off": "Off",
"manual_on": "Manual on",
"manual_off_cooldown": "Manual off cooldown",
"force_stopped": "Force stopped",
"error": "Error",
"disabled": "Disabled"
"options": {
"step": {
"init": {
"title": "Motion Light Options",
"description": "Change integration parameters",
"data": {
"main_sensors": "Main triggers",
"extend_sensors": "Extend sensors (optional)",
"switch_entities": "Controlled switches",
"lux_sensor": "Illuminance sensor (optional)",
"force_stop_sensor": "Force stop sensor (optional)",
"force_stop_state": "Force stop sensor state",
"log_level": "Log level"
}
}
}
},
"entity": {
"switch": {
"motion_light_enabled": {
"name": "Control"
},
"motion_light_priority_manual": {
"name": "Manual priority"
}
},
"attributes": {
"switch_entities": "Controlled switches",
"main_sensors": "Main triggers",
"extend_sensors": "Extend sensors",
"lux_sensor": "Illuminance sensor",
"force_stop_sensor": "Force stop sensor",
"force_stop_state": "Force stop state",
"force_stop_active": "Force stop active",
"active_main_sensors": "Active main triggers",
"active_extend_sensors": "Active extend sensors",
"lux_value": "Current illuminance",
"lux_cooldown_active": "Lux cooldown active",
"lux_check_active": "Lux check active",
"triggered_by": "Triggered by",
"last_motion_time": "Last motion time"
"sensor": {
"motion_light_status": {
"name": "Status",
"state": {
"idle": "Idle",
"detecting": "Detecting",
"on": "On",
"delaying": "Delaying",
"active": "Extended",
"off": "Off",
"manual_on": "Manual on",
"manual_off_cooldown": "Manual off cooldown",
"force_stopped": "Force stopped",
"error": "Error",
"disabled": "Disabled"
}
}
},
"number": {
"motion_light_motion_filter": {
"name": "Motion filter"
},
"motion_light_off_delay": {
"name": "Off delay"
},
"motion_light_lux_threshold": {
"name": "Lux threshold"
},
"motion_light_lux_cooldown": {
"name": "Lux cooldown"
},
"motion_light_manual_idle_timeout": {
"name": "'Forgot to turn off' timer"
},
"motion_light_manual_off_cooldown": {
"name": "Manual off cooldown"
}
}
}
},
"number": {
"motion_light_motion_filter": {
"name": "Motion filter"
},
"motion_light_off_delay": {
"name": "Off delay"
},
"motion_light_lux_threshold": {
"name": "Lux threshold"
},
"motion_light_lux_cooldown": {
"name": "Lux cooldown"
},
"motion_light_manual_idle_timeout": {
"name": "'Forgot to turn off' timer"
},
"motion_light_manual_off_cooldown": {
"name": "Manual off cooldown"
}
}
}
}
@@ -1,101 +1,85 @@
{
"config": {
"step": {
"user": {
"title": "Motion Light",
"description": "Настройка управления освещением по движению",
"data": {
"name": "Название",
"main_sensors": "Главные триггеры",
"extend_sensors": "Датчики продления (опционально)",
"switch_entities": "Управляемые выключатели",
"lux_sensor": "Датчик освещённости (опционально)",
"force_stop_sensor": "Датчик принудительной остановки (опционально)",
"force_stop_state": "Состояние датчика принудительной остановки",
"log_level": "Уровень логирования"
"config": {
"step": {
"user": {
"title": "Motion Light",
"description": "Настройка управления освещением по движению",
"data": {
"name": "Название",
"main_sensors": "Главные триггеры",
"extend_sensors": "Датчики продления (опционально)",
"switch_entities": "Управляемые выключатели",
"lux_sensor": "Датчик освещённости (опционально)",
"force_stop_sensor": "Датчик принудительной остановки (опционально)",
"force_stop_state": "Состояние датчика принудительной остановки",
"log_level": "Уровень логирования"
}
}
}
}
}
},
"options": {
"step": {
"init": {
"title": "Настройки Motion Light",
"description": "Изменение параметров интеграции",
"data": {
"main_sensors": "Главные триггеры",
"extend_sensors": "Датчики продления (опционально)",
"switch_entities": "Управляемые выключатели",
"lux_sensor": "Датчик освещённости (опционально)",
"force_stop_sensor": "Датчик принудительной остановки (опционально)",
"force_stop_state": "Состояние датчика принудительной остановки",
"log_level": "Уровень логирования"
}
}
}
},
"entity": {
"switch": {
"motion_light_enabled": {
"name": "Управление"
},
"motion_light_priority_manual": {
"name": "Приоритет ручного"
}
},
"sensor": {
"motion_light_status": {
"name": "Статус",
"state": {
"idle": "Ожидание",
"detecting": "Обнаружение",
"on": "Включено",
"delaying": "Задержка выключения",
"active": "Продлено",
"off": "Выключено",
"manual_on": "Ручное включение",
"manual_off_cooldown": "Пауза после ручного выкл.",
"force_stopped": "Принудительная остановка",
"error": "Ошибка",
"disabled": "Отключено"
"options": {
"step": {
"init": {
"title": "Настройки Motion Light",
"description": "Изменение параметров интеграции",
"data": {
"main_sensors": "Главные триггеры",
"extend_sensors": "Датчики продления (опционально)",
"switch_entities": "Управляемые выключатели",
"lux_sensor": "Датчик освещённости (опционально)",
"force_stop_sensor": "Датчик принудительной остановки (опционально)",
"force_stop_state": "Состояние датчика принудительной остановки",
"log_level": "Уровень логирования"
}
}
}
},
"entity": {
"switch": {
"motion_light_enabled": {
"name": "Управление"
},
"motion_light_priority_manual": {
"name": "Приоритет ручного"
}
},
"attributes": {
"switch_entities": "Управляемые выключатели",
"main_sensors": "Главные триггеры",
"extend_sensors": "Датчики продления",
"lux_sensor": "Датчик освещённости",
"force_stop_sensor": "Датчик принудительной остановки",
"force_stop_state": "Состояние принудительной остановки",
"force_stop_active": "Принудительная остановка активна",
"active_main_sensors": "Активные главные триггеры",
"active_extend_sensors": "Активные датчики продления",
"lux_value": "Текущая освещённость",
"lux_cooldown_active": "Cooldown люкса активен",
"lux_check_active": "Проверка люкса активна",
"triggered_by": "Активировано",
"last_motion_time": "Последнее движение"
"sensor": {
"motion_light_status": {
"name": "Статус",
"state": {
"idle": "Ожидание",
"detecting": "Обнаружение",
"on": "Включено",
"delaying": "Задержка выключения",
"active": "Продлено",
"off": "Выключено",
"manual_on": "Ручное включение",
"manual_off_cooldown": "Пауза после ручного выкл.",
"force_stopped": "Принудительная остановка",
"error": "Ошибка",
"disabled": "Отключено"
}
}
},
"number": {
"motion_light_motion_filter": {
"name": "Фильтр движения"
},
"motion_light_off_delay": {
"name": "Задержка выключения"
},
"motion_light_lux_threshold": {
"name": "Порог освещённости"
},
"motion_light_lux_cooldown": {
"name": "Cooldown люкса"
},
"motion_light_manual_idle_timeout": {
"name": "Таймер 'забыл выключить'"
},
"motion_light_manual_off_cooldown": {
"name": "Cooldown ручного выкл."
}
}
}
},
"number": {
"motion_light_motion_filter": {
"name": "Фильтр движения"
},
"motion_light_off_delay": {
"name": "Задержка выключения"
},
"motion_light_lux_threshold": {
"name": "Порог освещённости"
},
"motion_light_lux_cooldown": {
"name": "Cooldown люкса"
},
"motion_light_manual_idle_timeout": {
"name": "Таймер 'забыл выключить'"
},
"motion_light_manual_off_cooldown": {
"name": "Cooldown ручного выкл."
}
}
}
}
+12 -10
View File
@@ -1,29 +1,31 @@
[tool.semantic_release]
# Формат тега С префиксом v (для совместимости с существующими тегами)
tag_format = "v{version}"
# Где обновлять версию при релизе
version_variables = [
"custom_components/motion_light/manifest.json:version",
]
# Сообщение коммита
commit_message = "chore(release): v{version}"
# Настройки для Gitea
# Отключаем создание релиза через API Gitea (релиз будет создаваться в GitHub)
upload_to_vcs_release = false
[tool.semantic_release.remote]
type = "gitea"
# URL вашей Gitea (без trailing slash)
domain = "https://192.168.200.208:3000"
# Имя пользователя и репозитория
api_path = "/api/v1"
domain = "git.darkmode.duckdns.org"
api_domain = "git.darkmode.duckdns.org"
# Настройки веток
# Настройки для stable релизов (ветка main)
[tool.semantic_release.branches.main]
match = "(main|master)"
prerelease = false
# Настройки парсера коммитов
# Настройки для beta релизов (ветка dev)
[tool.semantic_release.branches.dev]
match = "dev"
prerelease = true
prerelease_token = "beta"
[tool.semantic_release.commit_parser_options]
allowed_tags = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "chore", "revert"]
minor_tags = ["feat"]