Updated apps
This commit is contained in:
@@ -19,6 +19,11 @@ from .base_trigger import BaseTrigger
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _norm_state(value: str | None) -> str | None:
|
||||
"""Case/whitespace-insensitive state form for from/to comparisons."""
|
||||
return value.strip().casefold() if isinstance(value, str) else value
|
||||
|
||||
|
||||
class StateChangeTrigger(BaseTrigger):
|
||||
"""Trigger that activates after counting state transitions.
|
||||
|
||||
@@ -35,8 +40,13 @@ class StateChangeTrigger(BaseTrigger):
|
||||
"""Initialize state change trigger."""
|
||||
super().__init__(hass, entity, trigger_config)
|
||||
|
||||
self._from_state: str | None = trigger_config.get("trigger_from_state")
|
||||
self._to_state: str | None = trigger_config.get("trigger_to_state")
|
||||
# Case-insensitive matching, like RuntimeTrigger's on_states: the
|
||||
# options flow lowercases these on save while the panel keeps the
|
||||
# user's casing, and HA states themselves can be capitalized
|
||||
# (input_select "Home") — normalizing BOTH sides at compare time is
|
||||
# the only variant that works for every surface combination.
|
||||
self._from_state: str | None = _norm_state(trigger_config.get("trigger_from_state"))
|
||||
self._to_state: str | None = _norm_state(trigger_config.get("trigger_to_state"))
|
||||
self._target_changes: int = trigger_config.get("trigger_target_changes", 1)
|
||||
# Restore persisted change count from config, default to 0
|
||||
self._change_count: int = trigger_config.get("trigger_change_count", 0)
|
||||
@@ -64,12 +74,27 @@ class StateChangeTrigger(BaseTrigger):
|
||||
|
||||
# Restore triggered state from persisted change count
|
||||
if self._change_count >= self._target_changes:
|
||||
self._triggered = True
|
||||
self.entity.async_update_trigger_state(
|
||||
is_triggered=True,
|
||||
current_value=float(self._change_count),
|
||||
trigger_entity_id=self.entity_id,
|
||||
)
|
||||
# Latch reconciliation: a single-shot state alarm (target_changes
|
||||
# == 1) that already left its alert state while we were down is no
|
||||
# longer active. Clear it quietly — the recovery transition was
|
||||
# never observed, so we must NOT auto-complete for it here (that
|
||||
# path only runs on a live off event, guarded against double-count).
|
||||
if (
|
||||
self._to_state is not None
|
||||
and self._target_changes == 1
|
||||
and _norm_state(state.state) != self._to_state
|
||||
):
|
||||
self._change_count = 0
|
||||
self._current_value = 0.0
|
||||
if self.hass.is_running:
|
||||
self.hass.async_create_task(self._persist_change_count())
|
||||
else:
|
||||
self._triggered = True
|
||||
self.entity.async_update_trigger_state(
|
||||
is_triggered=True,
|
||||
current_value=float(self._change_count),
|
||||
trigger_entity_id=self.entity_id,
|
||||
)
|
||||
|
||||
# Register state change listener (override base: we handle events differently)
|
||||
self._unsub_listener = async_track_state_change_event(self.hass, [self.entity_id], self._handle_state_transition)
|
||||
@@ -137,9 +162,9 @@ class StateChangeTrigger(BaseTrigger):
|
||||
|
||||
# Check if transition matches pattern
|
||||
matches = True
|
||||
if self._from_state is not None and effective_old != self._from_state:
|
||||
if self._from_state is not None and _norm_state(effective_old) != self._from_state:
|
||||
matches = False
|
||||
if self._to_state is not None and new_val != self._to_state:
|
||||
if self._to_state is not None and _norm_state(new_val) != self._to_state:
|
||||
matches = False
|
||||
|
||||
if matches and effective_old != new_val:
|
||||
@@ -166,6 +191,25 @@ class StateChangeTrigger(BaseTrigger):
|
||||
elif not is_triggered and was_triggered:
|
||||
self._on_trigger_deactivated(float(self._change_count))
|
||||
|
||||
# Latch recovery: a single-shot state alarm (target_changes == 1 — an
|
||||
# adopted problem sensor or an appliance event) clears when the entity
|
||||
# leaves its alert state. Reset the counter so the next occurrence can
|
||||
# fire again, and run the deactivation path — which auto-completes on
|
||||
# recovery when opted in. Multi-count triggers keep accumulating and
|
||||
# only reset on manual completion, so they are untouched here.
|
||||
elif (
|
||||
self._to_state is not None
|
||||
and self._target_changes == 1
|
||||
and self._triggered
|
||||
and _norm_state(new_val) != self._to_state
|
||||
):
|
||||
self._change_count = 0
|
||||
self._current_value = 0.0
|
||||
if self.hass.is_running:
|
||||
self.hass.async_create_task(self._persist_change_count())
|
||||
self._triggered = False
|
||||
self._on_trigger_deactivated(0.0)
|
||||
|
||||
self._last_state = new_val
|
||||
|
||||
def evaluate(self, value: float) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user