257 files
This commit is contained in:
@@ -7,7 +7,7 @@ import math
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Coroutine
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any, Protocol
|
||||
|
||||
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, State, callback
|
||||
from homeassistant.helpers.event import (
|
||||
@@ -15,10 +15,6 @@ from homeassistant.helpers.event import (
|
||||
async_track_state_change_event,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...coordinator import MaintenanceCoordinator
|
||||
from ...sensor import MaintenanceSensor
|
||||
|
||||
from ...const import (
|
||||
EVENT_TRIGGER_ACTIVATED,
|
||||
EVENT_TRIGGER_DEACTIVATED,
|
||||
@@ -28,6 +24,49 @@ from ...helpers.managed_timer import ManagedTimer
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TriggerCoordinator(Protocol):
|
||||
"""What a trigger needs from its coordinator: the object's, or a compound
|
||||
condition's proxy that routes these per condition."""
|
||||
|
||||
async def async_add_trigger_history_entry(self, task_id: str, trigger_value: float | None = None) -> None: ...
|
||||
|
||||
async def async_auto_complete_on_recovery(self, task_id: str, trigger_value: float) -> None: ...
|
||||
|
||||
async def async_persist_trigger_runtime(
|
||||
self,
|
||||
task_id: str,
|
||||
runtime_data: dict[str, Any],
|
||||
entity_id: str | None = None,
|
||||
*,
|
||||
immediate: bool = False,
|
||||
) -> None: ...
|
||||
|
||||
async def async_request_refresh(self) -> None: ...
|
||||
|
||||
def note_trigger_edge(self, task_id: str, *, recovered: bool = True) -> None: ...
|
||||
|
||||
|
||||
class TriggerHost(Protocol):
|
||||
"""What a trigger needs from the entity it drives: the task's sensor, or
|
||||
a compound condition's proxy entity."""
|
||||
|
||||
@property
|
||||
def entity_id(self) -> str: ...
|
||||
|
||||
@property
|
||||
def _task_id(self) -> str: ...
|
||||
|
||||
@property
|
||||
def coordinator(self) -> TriggerCoordinator: ...
|
||||
|
||||
def async_update_trigger_state(
|
||||
self,
|
||||
is_triggered: bool,
|
||||
current_value: float | None = None,
|
||||
trigger_entity_id: str | None = None,
|
||||
) -> bool | None: ...
|
||||
|
||||
# The initial-evaluation retry: an entity that is unknown/unavailable at
|
||||
# setup is re-checked this often, this many times, before the state-change
|
||||
# listener alone is trusted to catch its recovery (issue #1 family).
|
||||
@@ -45,7 +84,7 @@ class BaseTrigger(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entity: MaintenanceSensor,
|
||||
entity: TriggerHost,
|
||||
trigger_config: dict[str, Any],
|
||||
) -> None:
|
||||
"""Initialize the trigger."""
|
||||
@@ -75,7 +114,7 @@ class BaseTrigger(ABC):
|
||||
self._logged_unavailable = False # Log-once pattern for unavailable
|
||||
|
||||
@property
|
||||
def _coordinator(self) -> MaintenanceCoordinator:
|
||||
def _coordinator(self) -> TriggerCoordinator:
|
||||
"""Get the coordinator from the entity."""
|
||||
return self.entity.coordinator
|
||||
|
||||
@@ -84,6 +123,14 @@ class BaseTrigger(ABC):
|
||||
"""Get the task ID from the entity."""
|
||||
return self.entity._task_id
|
||||
|
||||
def _event_ids(self) -> dict[str, Any]:
|
||||
"""The task's ids for the trigger events — EXAMPLES.md tells
|
||||
automations to deep-link with them (`?entry_id=…&task_id=…`), but
|
||||
only the completed / skipped / reset events carried them (audit
|
||||
2026-09-28)."""
|
||||
entry = getattr(self._coordinator, "entry", None)
|
||||
return {"entry_id": getattr(entry, "entry_id", None), "task_id": self._task_id}
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Set up the trigger: validate entity and register listener.
|
||||
|
||||
@@ -307,6 +354,12 @@ class BaseTrigger(ABC):
|
||||
"""
|
||||
|
||||
|
||||
@property
|
||||
def _is_compound_condition(self) -> bool:
|
||||
"""This trigger watches one condition of a compound (its entity is
|
||||
the condition's proxy), not the task itself."""
|
||||
return getattr(self.entity, "is_compound_condition", False) is True
|
||||
|
||||
def _request_coordinator_refresh(self) -> None:
|
||||
"""#175: a trigger flip must reach the coordinator now, not on the
|
||||
next 5-minute tick. Notifications (and the ``maintenance_supporter_
|
||||
@@ -332,6 +385,12 @@ class BaseTrigger(ABC):
|
||||
current_value=value,
|
||||
trigger_entity_id=self.entity_id,
|
||||
)
|
||||
if self._is_compound_condition:
|
||||
# One condition of a compound: the compound decides whether the
|
||||
# TASK activated and announces it. Firing here sent
|
||||
# maintenance_supporter_trigger_activated with the task's ids when
|
||||
# one half of an AND flipped (bug audit 2026-09-29).
|
||||
return
|
||||
|
||||
# Add history entry for the trigger activation
|
||||
self._track(self._coordinator.async_add_trigger_history_entry(self._task_id, trigger_value=value))
|
||||
@@ -343,6 +402,7 @@ class BaseTrigger(ABC):
|
||||
EVENT_TRIGGER_ACTIVATED,
|
||||
{
|
||||
"entity_id": self.entity.entity_id,
|
||||
**self._event_ids(),
|
||||
"trigger_entity": self.entity_id,
|
||||
"trigger_attribute": self.attribute,
|
||||
"trigger_value": value,
|
||||
@@ -367,6 +427,8 @@ class BaseTrigger(ABC):
|
||||
current_value=value,
|
||||
trigger_entity_id=self.entity_id,
|
||||
)
|
||||
if self._is_compound_condition:
|
||||
return # the compound announces the task's recovery (see activation)
|
||||
|
||||
self._request_coordinator_refresh()
|
||||
|
||||
@@ -375,6 +437,7 @@ class BaseTrigger(ABC):
|
||||
EVENT_TRIGGER_DEACTIVATED,
|
||||
{
|
||||
"entity_id": self.entity.entity_id,
|
||||
**self._event_ids(),
|
||||
"trigger_entity": self.entity_id,
|
||||
"trigger_attribute": self.attribute,
|
||||
"trigger_value": value,
|
||||
|
||||
Reference in New Issue
Block a user