158 files
This commit is contained in:
@@ -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", [])
|
||||
|
||||
Reference in New Issue
Block a user