Fix hot/warm href tiers dropped for no-entity coverage capabilities
discover() only emitted a BoundEntity per capability *entity*, so a coverage-only Capability (entities=(), used to mark a href as handled elsewhere -- e.g. the AC climate card's wind/strength, wind/direction, temperature/control hrefs) produced zero rows. The coordinator computed its hot/warm href lists by walking `bound`, so every such href's poll_tier was silently discarded and it fell back to the ~30s summary poll only -- no sub-poll cadence and never attempted for OCF OBSERVE. This is the root cause of issue #166's "up to a minute" lag for remote-driven fan-speed changes: /wind/strength/vs/0 carries poll_tier 'warm' via airconditioner.COVERAGE but never reached _hot_hrefs/_warm_hrefs, so it wasn't in the OBSERVE-attempt href list and only refreshed on the summary poll. discover() now takes an optional tier_log(href, poll_tier) callback fired for every href a capability matches, entities or not. The coordinator uses it directly instead of deriving tiers from `bound`.
This commit is contained in:
@@ -366,14 +366,22 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
if reg is None:
|
||||
reg = for_device_by_resources(resources)
|
||||
unbound: list[str] = []
|
||||
hot, warm = set(), set()
|
||||
|
||||
def _tier_log(href: str, tier: str) -> None:
|
||||
if tier == 'hot':
|
||||
hot.add(href)
|
||||
elif tier == 'warm':
|
||||
warm.add(href)
|
||||
|
||||
if reg is not None:
|
||||
self._log.debug("device type: %s (oneUiVersion=%r)", reg.name, one_ui)
|
||||
bound = discover(resources, reg.capabilities, reg.pattern_capabilities,
|
||||
log=unbound.append)
|
||||
log=unbound.append, tier_log=_tier_log)
|
||||
self.device_type_name = reg.name
|
||||
else:
|
||||
self._log.warning("unknown device type oneUiVersion=%r; using common caps", one_ui)
|
||||
bound = discover(resources, CAPABILITIES, log=unbound.append)
|
||||
bound = discover(resources, CAPABILITIES, log=unbound.append, tier_log=_tier_log)
|
||||
self.device_type_name = None
|
||||
self.bound = bound
|
||||
self._unbound_hrefs = unbound
|
||||
@@ -398,13 +406,6 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
self._update_coverage_gap_issue(reg is None, unbound, name)
|
||||
|
||||
hot, warm = set(), set()
|
||||
for be in bound:
|
||||
tier = be.capability.poll_tier
|
||||
if tier == 'hot':
|
||||
hot.add(be.href)
|
||||
elif tier == 'warm':
|
||||
warm.add(be.href)
|
||||
self._hot_hrefs = sorted(hot)
|
||||
self._warm_hrefs = sorted(warm)
|
||||
|
||||
|
||||
@@ -73,7 +73,15 @@ def discover(
|
||||
registry: dict[str, list[Capability]],
|
||||
pattern_caps: Iterable[Capability] = (),
|
||||
log: Optional[Callable[[str], None]] = None,
|
||||
tier_log: Optional[Callable[[str, str], None]] = None,
|
||||
) -> list[BoundEntity]:
|
||||
"""`tier_log(href, poll_tier)` fires for every href a capability actually
|
||||
matches, even a no-entity "coverage-only" capability (see COVERAGE lists
|
||||
in capabilities/*.py) that `_bind()` turns into zero `BoundEntity` rows.
|
||||
Callers that need a href's poll cadence (the coordinator's hot/warm
|
||||
sub-poll and OBSERVE-attempt lists) must use this, not `bound` -- a
|
||||
coverage-only capability's `poll_tier` would otherwise be silently
|
||||
dropped since it never appears in `bound`."""
|
||||
out: list[BoundEntity] = []
|
||||
|
||||
for href, rep in resources.items():
|
||||
@@ -91,6 +99,8 @@ def discover(
|
||||
inst = instance_suffix(href)
|
||||
out.extend(_bind(cap, href, inst, _instance_name(cap, rep)))
|
||||
matched = True
|
||||
if tier_log is not None:
|
||||
tier_log(href, cap.poll_tier)
|
||||
|
||||
if matched:
|
||||
continue
|
||||
@@ -109,6 +119,8 @@ def discover(
|
||||
segs = [s for s in src.strip('/').split('/') if s and not s.isdigit() and s != 'vs']
|
||||
out.extend(_bind(cap, href, inst, _instance_name(cap, rep), '_'.join(segs)))
|
||||
matched = True
|
||||
if tier_log is not None:
|
||||
tier_log(href, cap.poll_tier)
|
||||
break
|
||||
|
||||
if not matched and not caps and log is not None:
|
||||
|
||||
@@ -209,3 +209,19 @@ def test_discover_rt_filter_gates_binding():
|
||||
resources = {'/mode/vs/0': {'rt': ['x.com.samsung.da.mode'], 'x.com.samsung.da.mode': 'Bake'}}
|
||||
bound = discover(resources, {'/mode/vs/0': [oven_mode_cap]})
|
||||
assert bound == []
|
||||
|
||||
|
||||
def test_discover_tier_log_fires_for_no_entity_coverage_cap():
|
||||
"""A coverage-only Capability (entities=(), used to mark a href as
|
||||
handled by e.g. a composite climate entity rather than its own HA
|
||||
entity) produces zero BoundEntity rows -- `bound` alone can't tell a
|
||||
caller its poll_tier. tier_log must still report it, or a consumer
|
||||
(the coordinator's hot/warm href lists) silently treats it as 'cold'
|
||||
regardless of what poll_tier the capability actually declares."""
|
||||
coverage_cap = Capability(href='/wind/strength/vs/0', poll_tier='warm')
|
||||
resources = {'/wind/strength/vs/0': {'x.com.samsung.da.modes': '0'}}
|
||||
seen = []
|
||||
bound = discover(resources, {coverage_cap.href: [coverage_cap]},
|
||||
tier_log=lambda href, tier: seen.append((href, tier)))
|
||||
assert bound == []
|
||||
assert seen == [('/wind/strength/vs/0', 'warm')]
|
||||
|
||||
Reference in New Issue
Block a user