water_purifier, range: drop invalid device_class='lock' from switches
SwitchDeviceClass only ever supported 'outlet'/'switch', not 'lock'. switch.py passes desc.device_class straight to SwitchDeviceClass(...), so any board with these hrefs raised ValueError during switch platform setup and lost every switch entity for the device, not just the lock ones (issue #349, TP2X_WATERPURIFIER_20K). KIDS_LOCK_GENERIC/_VS_FALLBACK dodged this same bug (issues #181/#183) by moving to a read-only BinarySensorDesc, but the water-purifier and cooktop locks are genuinely writable, so they stay SwitchDesc and just drop the invalid device_class (with an mdi:lock icon standing in for the one entity_category=config gave them for free). Added a registry-wide test that instantiates SwitchDeviceClass for every SwitchDesc.device_class across every by_type registry, so a future capability can't reintroduce the same crash.
This commit is contained in:
@@ -148,11 +148,13 @@ COOKTOP_STATUS = Capability(
|
||||
value_fn=lambda v: str(v).lower() == "on",
|
||||
),
|
||||
# Safe to write -- a lock toggle, not a heat control -- via a
|
||||
# direct single-field PUT, no RMW needed.
|
||||
# direct single-field PUT, no RMW needed. No device_class:
|
||||
# SwitchDeviceClass only has 'outlet'/'switch', not 'lock' --
|
||||
# passing it crashed switch platform setup for the whole device
|
||||
# (issue #349, same bug as water_purifier.py's lock switches).
|
||||
SwitchDesc(
|
||||
key="cooktop_child_lock",
|
||||
field="childLock",
|
||||
device_class="lock",
|
||||
entity_category="config",
|
||||
icon="mdi:lock",
|
||||
value_fn=lambda v: str(v).lower() == "on",
|
||||
|
||||
@@ -182,10 +182,15 @@ FAVORITE_HOTWATER = Capability(
|
||||
# adapter.flatten() only ever honors exists_fn, not entity.py's
|
||||
# implicit field-presence default -- without it, whichever
|
||||
# same-keyed descriptor is processed last would silently win.
|
||||
# No device_class: SwitchDeviceClass only has 'outlet'/'switch',
|
||||
# not 'lock' -- passing it crashed switch platform setup entirely
|
||||
# for the whole device (issue #349), same bug KIDS_LOCK_GENERIC
|
||||
# dodged by switching to BinarySensorDesc (issues #181/#183). This
|
||||
# entity stays a SwitchDesc since it's genuinely writable.
|
||||
SwitchDesc(
|
||||
key="hotwater_lock",
|
||||
field="x.com.samsung.da.switchHotwater",
|
||||
device_class="lock",
|
||||
icon="mdi:lock",
|
||||
entity_category="config",
|
||||
value_fn=lambda v: v != "Unlocked",
|
||||
exists_fn=lambda rep, resources: (
|
||||
@@ -354,11 +359,13 @@ LOCK = Capability(
|
||||
entities=(
|
||||
# Shares its key with FAVORITE_HOTWATER's switchHotwater fallback
|
||||
# above (issue #144); see the comment there. A stub rep ({}) still
|
||||
# counts as "present" here, matching entity.py's own default.
|
||||
# counts as "present" here, matching entity.py's own default. No
|
||||
# device_class on any of the three locks below -- see the
|
||||
# device_class note on FAVORITE_HOTWATER's hotwater_lock (issue #349).
|
||||
SwitchDesc(
|
||||
key="hotwater_lock",
|
||||
field="x.com.samsung.da.hotwaterLock",
|
||||
device_class="lock",
|
||||
icon="mdi:lock",
|
||||
entity_category="config",
|
||||
value_fn=lambda v: v != "Unlocked",
|
||||
exists_fn=lambda rep, resources: not rep or "x.com.samsung.da.hotwaterLock" in rep,
|
||||
@@ -370,7 +377,7 @@ LOCK = Capability(
|
||||
SwitchDesc(
|
||||
key="coldwater_lock",
|
||||
field="x.com.samsung.da.coldwaterLock",
|
||||
device_class="lock",
|
||||
icon="mdi:lock",
|
||||
entity_category="config",
|
||||
value_fn=lambda v: v != "Unlocked",
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
@@ -381,7 +388,7 @@ LOCK = Capability(
|
||||
SwitchDesc(
|
||||
key="buzz_lock",
|
||||
field="x.com.samsung.da.buzzLock",
|
||||
device_class="lock",
|
||||
icon="mdi:lock",
|
||||
entity_category="config",
|
||||
value_fn=lambda v: v != "Unlocked",
|
||||
write_fn=lambda p, rep, href=None: (
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Guards against a SwitchDesc.device_class HA's SwitchDeviceClass doesn't
|
||||
recognize -- switch.py passes it straight to SwitchDeviceClass(...), and an
|
||||
invalid value raises out of the whole switch platform's async_setup_entry,
|
||||
taking down every switch entity for the device, not just the offending one
|
||||
(issue #349: water_purifier.py's lock switches used device_class='lock',
|
||||
which SwitchDeviceClass only ever supported as 'outlet'/'switch').
|
||||
|
||||
Scans every by_type registry's declared capabilities rather than a specific
|
||||
fixture, so a new capability introducing the same mistake fails here instead
|
||||
of only surfacing as a live crash report.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
|
||||
from custom_components.localthings.registry import by_type
|
||||
from custom_components.localthings.registry.entities import SwitchDesc
|
||||
|
||||
|
||||
def _all_registries():
|
||||
for mod_info in pkgutil.iter_modules(by_type.__path__):
|
||||
if mod_info.name.startswith("_"):
|
||||
continue
|
||||
mod = importlib.import_module(
|
||||
f"custom_components.localthings.registry.by_type.{mod_info.name}"
|
||||
)
|
||||
reg = getattr(mod, "REGISTRY", None)
|
||||
if reg is not None:
|
||||
yield reg
|
||||
|
||||
|
||||
def test_every_switchdesc_device_class_is_valid_for_ha():
|
||||
bad = []
|
||||
for reg in _all_registries():
|
||||
caps = [c for cs in reg.capabilities.values() for c in cs] + list(reg.pattern_capabilities)
|
||||
for cap in caps:
|
||||
for entity in cap.entities:
|
||||
if isinstance(entity, SwitchDesc) and entity.device_class is not None:
|
||||
try:
|
||||
SwitchDeviceClass(entity.device_class)
|
||||
except ValueError:
|
||||
bad.append((reg.name, entity.key, entity.device_class))
|
||||
assert bad == []
|
||||
Reference in New Issue
Block a user