429 files
This commit is contained in:
1 parent
472c19bceb
commit
963cce81a7
566 files changed
+50311
-47616
No files matched your search
@@ -9,7 +9,7 @@ from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
@@ -59,6 +59,7 @@ from ..const import (
|
||||
CONF_QUIET_HOURS_END,
|
||||
CONF_QUIET_HOURS_START,
|
||||
CONF_REMINDER_LEAD_DAYS,
|
||||
CONF_SHOPPING_LIST_ENTITY,
|
||||
CONF_SNOOZE_DURATION_HOURS,
|
||||
CONF_WARRANTY_REMINDER_DAYS,
|
||||
CONF_WARRANTY_REMINDER_ENABLED,
|
||||
@@ -149,6 +150,8 @@ def _build_full_settings(options: Mapping[str, Any], *, notify_targets: list[str
|
||||
"default_warning_days": options.get(CONF_DEFAULT_WARNING_DAYS, DEFAULT_WARNING_DAYS),
|
||||
"notifications_enabled": options.get(CONF_NOTIFICATIONS_ENABLED, False),
|
||||
"notify_service": options.get(CONF_NOTIFY_SERVICE, ""),
|
||||
# v2.67: buy-task shopping sync target ("" = off).
|
||||
"shopping_list_entity": options.get(CONF_SHOPPING_LIST_ENTITY, ""),
|
||||
# Shared pickable-target list so the panel picker can't drift from
|
||||
# the options-flow dropdown (both go through build_notify_targets).
|
||||
"notify_targets": notify_targets or [],
|
||||
@@ -327,7 +330,7 @@ async def ws_subscribe(
|
||||
|
||||
deltas: bool = msg.get("deltas", False)
|
||||
compact: bool = msg.get("compact", False)
|
||||
attached_entry_ids: set[str] = set()
|
||||
attached_entry_ids: dict[str, tuple[Any, Callable[[], None]]] = {}
|
||||
unsub_callbacks: list[Callable[[], None]] = []
|
||||
dirty: set[str] = set()
|
||||
last_hash: dict[str, int] = {}
|
||||
@@ -405,18 +408,33 @@ async def ws_subscribe(
|
||||
debounce_unsub = async_call_later(hass, 1.0, _send_now)
|
||||
|
||||
def _attach_entry(entry_id: str) -> None:
|
||||
"""Attach a coordinator listener for a specific entry."""
|
||||
if entry_id in attached_entry_ids:
|
||||
return
|
||||
"""Attach a coordinator listener for a specific entry.
|
||||
|
||||
Every entry RELOAD (task create/edit/delete, options flow, ...) builds
|
||||
a brand-new coordinator and re-announces the entry via
|
||||
SIGNAL_NEW_OBJECT_ENTRY. An "already attached" early return kept this
|
||||
subscription bound to the dead coordinator, so other clients froze
|
||||
for that object until a page reload (bug audit 2026-08-29). Track
|
||||
the coordinator instance and re-attach when it changed.
|
||||
"""
|
||||
rd = _get_runtime_data(hass, entry_id)
|
||||
if rd and rd.coordinator:
|
||||
coordinator = rd.coordinator if rd else None
|
||||
if coordinator is None:
|
||||
return
|
||||
previous = attached_entry_ids.get(entry_id)
|
||||
if previous is not None:
|
||||
if previous[0] is coordinator:
|
||||
return
|
||||
previous[1]()
|
||||
unsub_callbacks.remove(previous[1])
|
||||
|
||||
@callback
|
||||
def _on_update(eid: str = entry_id) -> None:
|
||||
_forward_update(eid)
|
||||
@callback
|
||||
def _on_update(eid: str = entry_id) -> None:
|
||||
_forward_update(eid)
|
||||
|
||||
unsub_callbacks.append(rd.coordinator.async_add_listener(_on_update))
|
||||
attached_entry_ids.add(entry_id)
|
||||
unsub = coordinator.async_add_listener(_on_update)
|
||||
unsub_callbacks.append(unsub)
|
||||
attached_entry_ids[entry_id] = (coordinator, unsub)
|
||||
|
||||
# Register listeners on all existing coordinators
|
||||
entries = _get_object_entries(hass)
|
||||
@@ -677,6 +695,17 @@ def sanitize_settings_input(settings_input: dict[str, Any]) -> tuple[dict[str, A
|
||||
tids.append(v)
|
||||
filtered[CONF_DISABLED_TEMPLATE_IDS] = tids
|
||||
|
||||
# Validate shopping_list_entity if provided: "" clears; otherwise it must
|
||||
# be a todo.* entity id. Format-only — NO existence check (the list's
|
||||
# integration may load later; the sync self-heals on first appearance).
|
||||
if CONF_SHOPPING_LIST_ENTITY in filtered:
|
||||
raw_ent = (filtered[CONF_SHOPPING_LIST_ENTITY] or "").strip()
|
||||
if raw_ent and not valid_entity_id(raw_ent):
|
||||
return filtered, "invalid_shopping_list_entity"
|
||||
if raw_ent and not raw_ent.startswith("todo."):
|
||||
return filtered, "invalid_shopping_list_entity"
|
||||
filtered[CONF_SHOPPING_LIST_ENTITY] = raw_ent
|
||||
|
||||
# Validate notify_service if provided
|
||||
if CONF_NOTIFY_SERVICE in filtered:
|
||||
from ..config_flow_options_global import validate_notify_service
|
||||
|
||||
Reference in new issue
Block a user