Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom_components/localthings/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"pyOpenSSL>=23.0",
"smartthings-local>=0.1.2"
],
"version": "0.21.0"
"version": "0.22.0"
}
6 changes: 4 additions & 2 deletions custom_components/localthings/registry/capabilities/range.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,13 @@ def _child_lock_write(p, rep, href=None):
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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,15 @@ def _status_lock_definitely_lacks_hotwater_field(resources: dict) -> bool:
# 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: (
Expand Down Expand Up @@ -354,11 +359,13 @@ def _status_lock_definitely_lacks_hotwater_field(resources: dict) -> bool:
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,
Expand All @@ -370,7 +377,7 @@ def _status_lock_definitely_lacks_hotwater_field(resources: dict) -> bool:
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: (
Expand All @@ -381,7 +388,7 @@ def _status_lock_definitely_lacks_hotwater_field(resources: dict) -> bool:
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: (
Expand Down
45 changes: 45 additions & 0 deletions tests/test_switch_device_class.py
Original file line number Diff line number Diff line change
@@ -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 == []
Loading