Merge pull request #351 from mbillow/claude/triage-version-bump-x60dym

water_purifier, range: drop invalid device_class='lock' from switches
This commit is contained in:
Marc Billow
2026-08-10 12:37:38 -04:00
committed by GitHub
4 changed files with 62 additions and 8 deletions
+1 -1
View File
@@ -12,5 +12,5 @@
"pyOpenSSL>=23.0",
"smartthings-local>=0.1.2"
],
"version": "0.21.0"
"version": "0.22.0"
}
@@ -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: (
+45
View File
@@ -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 == []