158 files

This commit is contained in:
Home Assistant Version Control
2026-08-11 11:40:58 +00:00
parent c6db6ca558
commit 5388267a3e
158 changed files with 11840 additions and 698 deletions
@@ -88,6 +88,46 @@ TRIGGER_ENTITY_DOMAINS = [
]
def _apply_recovery_flag(tc: dict[str, Any], user_input: dict[str, Any]) -> None:
"""Write the #53 auto-complete-on-recovery flag from a form submission.
Editable since the dialog/flow parity round — before that the flow only
carried a previously stored value. Absence in trigger_config means off.
"""
if user_input.get("auto_complete_on_recovery"):
tc["auto_complete_on_recovery"] = True
else:
tc.pop("auto_complete_on_recovery", None)
def _recovery_default(tc: dict[str, Any] | None) -> bool:
return bool((tc or {}).get("auto_complete_on_recovery"))
def _state_selector(entity_id: str | None, *, multiple: bool = False) -> Any:
"""State field bound to the trigger entity (#129 follow-up).
Suggests the entity's known states instead of free text — typo'd states
were a real failure mode. Falls back to a plain text field when no entity
is available (defensive; the entity step always runs first).
"""
if entity_id:
return selector.StateSelector(
selector.StateSelectorConfig(entity_id=entity_id, multiple=multiple)
)
return selector.TextSelector(selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT))
def _parse_states(raw: Any) -> list[str]:
"""Normalize an on-states submission — list from the state selector,
comma string from the legacy text fallback."""
if isinstance(raw, list):
return [s.strip().lower() for s in raw if isinstance(s, str) and s.strip()]
if isinstance(raw, str):
return [s.strip().lower() for s in raw.split(",") if s.strip()]
return []
class TriggerConfigMixin:
"""Shared sensor trigger configuration logic for ConfigFlow and OptionsFlow.
@@ -381,6 +421,7 @@ class TriggerConfigMixin:
if below is not None:
tc[CONF_TRIGGER_BELOW] = below
tc[CONF_TRIGGER_FOR_MINUTES] = user_input.get(CONF_TRIGGER_FOR_MINUTES, 0)
_apply_recovery_flag(tc, user_input)
# Multi-entity: store entity_logic if multiple entities selected
entity_ids = tc.get("entity_ids", [])
@@ -419,6 +460,10 @@ class TriggerConfigMixin:
vol.Optional(CONF_TRIGGER_FOR_MINUTES, default=0): selector.NumberSelector(
selector.NumberSelectorConfig(min=0, max=1440, step=1, mode=selector.NumberSelectorMode.BOX)
),
vol.Optional(
"auto_complete_on_recovery",
default=_recovery_default(self._current_task.get("trigger_config")),
): selector.BooleanSelector(),
}
# Add entity_logic selector when multiple entities are selected
@@ -480,6 +525,13 @@ class TriggerConfigMixin:
tc = self._current_task["trigger_config"]
tc[CONF_TRIGGER_TARGET_VALUE] = user_input[CONF_TRIGGER_TARGET_VALUE]
tc[CONF_TRIGGER_DELTA_MODE] = user_input.get(CONF_TRIGGER_DELTA_MODE, False)
_apply_recovery_flag(tc, user_input)
# Counting start value (#102/#103): editable here since the parity
# round — an omitted field keeps the value the attribute step
# carried over; the backend clears stale Store state on change.
baseline = user_input.get("trigger_baseline_value")
if baseline is not None and baseline >= 0:
tc["trigger_baseline_value"] = baseline
# Multi-entity: store entity_logic if multiple entities selected
entity_ids = tc.get("entity_ids", [])
@@ -510,6 +562,12 @@ class TriggerConfigMixin:
else:
current_value = state.state
prev_tc = self._current_task.get("trigger_config") or {}
baseline_key = (
vol.Optional("trigger_baseline_value", default=prev_tc["trigger_baseline_value"])
if "trigger_baseline_value" in prev_tc
else vol.Optional("trigger_baseline_value")
)
schema_fields: dict[Any, Any] = {
vol.Required(CONF_TRIGGER_TARGET_VALUE): selector.NumberSelector(
selector.NumberSelectorConfig(
@@ -518,6 +576,17 @@ class TriggerConfigMixin:
)
),
vol.Optional(CONF_TRIGGER_DELTA_MODE, default=False): selector.BooleanSelector(),
baseline_key: selector.NumberSelector(
selector.NumberSelectorConfig(
min=0,
mode=selector.NumberSelectorMode.BOX,
step="any",
)
),
vol.Optional(
"auto_complete_on_recovery",
default=_recovery_default(prev_tc),
): selector.BooleanSelector(),
}
# Add entity_logic selector when multiple entities are selected
@@ -590,6 +659,7 @@ class TriggerConfigMixin:
if to_state:
tc[CONF_TRIGGER_TO_STATE] = to_state
tc[CONF_TRIGGER_TARGET_CHANGES] = user_input.get(CONF_TRIGGER_TARGET_CHANGES, 1)
_apply_recovery_flag(tc, user_input)
# Multi-entity: store entity_logic if multiple entities selected
entity_ids = tc.get("entity_ids", [])
@@ -608,12 +678,8 @@ class TriggerConfigMixin:
return on_complete()
schema_fields: dict[Any, Any] = {
vol.Optional(CONF_TRIGGER_FROM_STATE): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
vol.Optional(CONF_TRIGGER_TO_STATE): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
vol.Optional(CONF_TRIGGER_FROM_STATE): _state_selector(self._trigger_entity_id),
vol.Optional(CONF_TRIGGER_TO_STATE): _state_selector(self._trigger_entity_id),
vol.Required(CONF_TRIGGER_TARGET_CHANGES, default=1): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1,
@@ -622,6 +688,10 @@ class TriggerConfigMixin:
mode=selector.NumberSelectorMode.BOX,
)
),
vol.Optional(
"auto_complete_on_recovery",
default=_recovery_default(self._current_task.get("trigger_config")),
): selector.BooleanSelector(),
}
# Add entity_logic selector when multiple entities are selected
@@ -684,12 +754,12 @@ class TriggerConfigMixin:
tc = self._current_task["trigger_config"]
tc[CONF_TRIGGER_RUNTIME_HOURS] = user_input[CONF_TRIGGER_RUNTIME_HOURS]
# Parse comma-separated ON states
raw_states = user_input.get(CONF_TRIGGER_ON_STATES, "")
if raw_states and raw_states.strip():
tc[CONF_TRIGGER_ON_STATES] = [s.strip().lower() for s in raw_states.split(",") if s.strip()]
states = _parse_states(user_input.get(CONF_TRIGGER_ON_STATES))
if states:
tc[CONF_TRIGGER_ON_STATES] = states
else:
tc.pop(CONF_TRIGGER_ON_STATES, None)
_apply_recovery_flag(tc, user_input)
# Multi-entity: store entity_logic if multiple entities selected
entity_ids = tc.get("entity_ids", [])
@@ -709,8 +779,9 @@ class TriggerConfigMixin:
# Pre-fill existing custom states for editing
current_tc = self._current_task.get("trigger_config", {})
existing_states = current_tc.get(CONF_TRIGGER_ON_STATES)
default_states = ", ".join(existing_states) if existing_states else ""
existing_states = current_tc.get(CONF_TRIGGER_ON_STATES) or []
# The state selector takes/returns a LIST; the text fallback a comma string.
default_states: Any = list(existing_states) if self._trigger_entity_id else ", ".join(existing_states)
schema_fields: dict[Any, Any] = {
vol.Required(CONF_TRIGGER_RUNTIME_HOURS): selector.NumberSelector(
@@ -722,11 +793,13 @@ class TriggerConfigMixin:
unit_of_measurement="h",
)
),
vol.Optional(CONF_TRIGGER_ON_STATES, default=default_states): selector.TextSelector(
selector.TextSelectorConfig(
type=selector.TextSelectorType.TEXT,
)
vol.Optional(CONF_TRIGGER_ON_STATES, default=default_states): _state_selector(
self._trigger_entity_id, multiple=True
),
vol.Optional(
"auto_complete_on_recovery",
default=_recovery_default(current_tc),
): selector.BooleanSelector(),
}
# Add entity_logic selector when multiple entities are selected
@@ -791,15 +864,12 @@ class TriggerConfigMixin:
return cancel
logic = user_input.get(CONF_COMPOUND_LOGIC, "AND").upper()
# Carry over the panel-managed recovery flag (#53) across the
# compound rebuild, mirroring the flat-trigger path above.
prev_tc = self._current_task.get("trigger_config") or {}
self._current_task["trigger_config"] = {
"type": TriggerType.COMPOUND,
CONF_COMPOUND_LOGIC: logic,
CONF_COMPOUND_CONDITIONS: [],
**({"auto_complete_on_recovery": True} if prev_tc.get("auto_complete_on_recovery") else {}),
}
_apply_recovery_flag(self._current_task["trigger_config"], user_input)
if not hasattr(self, "_compound_conditions"):
self._compound_conditions: list[dict[str, Any]] = []
self._compound_conditions = []
@@ -823,6 +893,10 @@ class TriggerConfigMixin:
translation_key="compound_logic",
)
),
vol.Optional(
"auto_complete_on_recovery",
default=_recovery_default(self._current_task.get("trigger_config")),
): selector.BooleanSelector(),
}
return self.async_show_form(
step_id=step_id,
@@ -961,9 +1035,9 @@ class TriggerConfigMixin:
cond["trigger_target_changes"] = user_input.get(CONF_TRIGGER_TARGET_CHANGES, 1)
elif condition_type == TriggerType.RUNTIME:
cond["trigger_runtime_hours"] = user_input[CONF_TRIGGER_RUNTIME_HOURS]
raw_states = user_input.get(CONF_TRIGGER_ON_STATES, "")
if raw_states and raw_states.strip():
cond["trigger_on_states"] = [s.strip().lower() for s in raw_states.split(",") if s.strip()]
states = _parse_states(user_input.get(CONF_TRIGGER_ON_STATES))
if states:
cond["trigger_on_states"] = states
entity_ids = cond.get("entity_ids", [])
if len(entity_ids) > 1 and user_input.get(CONF_TRIGGER_ENTITY_LOGIC):
@@ -1000,13 +1074,10 @@ class TriggerConfigMixin:
vol.Optional(CONF_TRIGGER_DELTA_MODE, default=False): selector.BooleanSelector(),
}
elif condition_type == TriggerType.STATE_CHANGE:
cond_entity = cond.get("entity_id")
schema_fields = {
vol.Optional(CONF_TRIGGER_FROM_STATE): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
vol.Optional(CONF_TRIGGER_TO_STATE): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
vol.Optional(CONF_TRIGGER_FROM_STATE): _state_selector(cond_entity),
vol.Optional(CONF_TRIGGER_TO_STATE): _state_selector(cond_entity),
vol.Required(CONF_TRIGGER_TARGET_CHANGES, default=1): selector.NumberSelector(
selector.NumberSelectorConfig(
min=1,
@@ -1027,9 +1098,10 @@ class TriggerConfigMixin:
unit_of_measurement="h",
)
),
vol.Optional(CONF_TRIGGER_ON_STATES, default=""): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT)
),
vol.Optional(
CONF_TRIGGER_ON_STATES,
default=[] if cond.get("entity_id") else "",
): _state_selector(cond.get("entity_id"), multiple=True),
}
entity_ids = cond.get("entity_ids", [])