From e46e935ba0c6d2fd82e3257b723ffe46dc53e185 Mon Sep 17 00:00:00 2001 From: Marc Billow Date: Fri, 31 Jul 2026 02:09:58 +0000 Subject: [PATCH] fix(common): make the vendor kids-lock fallback read-only (#181, #183) KIDS_LOCK_VS_FALLBACK's write_fn wrote 'Enable', a value no dump in the fixture corpus (washers, dryers, dishwashers, ovens, ranges, microwaves, air purifiers, air dressers -- everything that lacks the OCF-standard /kidslock/0) has ever reported back; every one reports 'Ready' or 'Run'. It was never a confirmed write contract. #181's reporter tested directly: writing the *correct* value ('Run') still returns 4.05, and the SmartThings app itself has no control for kids lock either -- the resource is genuinely read-only on that hardware, not just wrong-valued. #183's reporter hit the identical symptom (toggle does nothing) on a different device family reporting the same Run/Ready vocabulary. Convert the entity to a read-only BinarySensorDesc. binary_sensor's 'lock' device class is inverted from the old switch reading (On means open/unlocked), so value_fn flips accordingly -- callers reading the flattened 'child_lock' state key need to account for the new polarity. KIDS_LOCK_GENERIC (the OCF-standard /kidslock/0 boolean) is untouched; nothing suggests that one is broken. --- .../registry/capabilities/common.py | 19 +++++++++++++------ .../localthings/translations/en.json | 3 +++ .../localthings/translations/nl.json | 3 +++ tests/test_common_capabilities.py | 18 ++++++++++++++++-- tests/test_range_ne6516a_capabilities.py | 9 ++++++--- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/custom_components/localthings/registry/capabilities/common.py b/custom_components/localthings/registry/capabilities/common.py index 45889b4..945903a 100644 --- a/custom_components/localthings/registry/capabilities/common.py +++ b/custom_components/localthings/registry/capabilities/common.py @@ -313,12 +313,19 @@ KIDS_LOCK_VS_FALLBACK = Capability( href='/kidslock/vs/0', match_fn=lambda rep, resources: '/kidslock/0' not in resources, entities=( - SwitchDesc(key='child_lock', field='x.com.samsung.da.kidsLock', - device_class='lock', - value_fn=lambda v: v != 'Ready', - write_fn=lambda p, rep, href=None: ( - ['kidslock', 'vs', '0'], - {'x.com.samsung.da.kidsLock': 'Enable' if p == 'On' else 'Ready'})), + # Read-only, not a SwitchDesc (issues #181/#183): the write side of + # this capability wrote 'Enable', a value no dump in the fixture + # corpus has ever reported back -- every one reports either 'Ready' + # or 'Run', so it was never a confirmed contract. #181's reporter + # confirmed this directly: writing the *correct* value ('Run') + # still 4.05s, and the SmartThings app itself has no control for + # it either -- the resource is genuinely read-only on this + # hardware, not just wrong-valued. binary_sensor's 'lock' device + # class is inverted from the switch reading below it used to be: + # On means open/unlocked, so value_fn flips to `v == 'Ready'`. + BinarySensorDesc(key='child_lock', field='x.com.samsung.da.kidsLock', + device_class='lock', + value_fn=lambda v: v == 'Ready'), ), ) diff --git a/custom_components/localthings/translations/en.json b/custom_components/localthings/translations/en.json index 5d718da..64b7809 100644 --- a/custom_components/localthings/translations/en.json +++ b/custom_components/localthings/translations/en.json @@ -16,6 +16,9 @@ "burner_pan_detected": { "name": "Burner {number} pan detected" }, + "child_lock": { + "name": "Child lock" + }, "cloud_connected": { "name": "Cloud connected" }, diff --git a/custom_components/localthings/translations/nl.json b/custom_components/localthings/translations/nl.json index 890ea12..b48539a 100644 --- a/custom_components/localthings/translations/nl.json +++ b/custom_components/localthings/translations/nl.json @@ -16,6 +16,9 @@ "burner_pan_detected": { "name": "Brander {number} pan gedetecteerd" }, + "child_lock": { + "name": "Kinderslot" + }, "cloud_connected": { "name": "Verbonden met cloud" }, diff --git a/tests/test_common_capabilities.py b/tests/test_common_capabilities.py index 37218cf..7b9741b 100644 --- a/tests/test_common_capabilities.py +++ b/tests/test_common_capabilities.py @@ -15,9 +15,13 @@ def _reg(): def test_kids_lock_vs_value_fn(): + """binary_sensor's 'lock' device class is inverted from a plain on/off + switch: On means open/unlocked (issues #181/#183 -- see + TestKidsLockFallback.test_vs_fallback_is_read_only for why this is a + binary_sensor at all).""" desc = common.KIDS_LOCK_VS_FALLBACK.entities[0] - assert desc.value_fn('Lock') is True - assert desc.value_fn('Ready') is False + assert desc.value_fn('Lock') is False + assert desc.value_fn('Ready') is True def test_common_caps_discover_on_dishwasher(dishwasher_resources): @@ -241,6 +245,16 @@ class TestKidsLockFallback: assert common.KIDS_LOCK_VS_FALLBACK.match_fn( {}, {'/kidslock/0': {}, '/kidslock/vs/0': {}}) is False + def test_vs_fallback_is_read_only(self): + """Issues #181/#183: writing this resource 4.05s on real hardware + even with the *correct* value ('Run'), and no dump in the fixture + corpus has ever reported the value this used to write ('Enable') + back -- every one reports 'Ready' or 'Run'. Never a confirmed write + contract, so it's a binary_sensor (no write_fn field at all), not + a switch.""" + desc = common.KIDS_LOCK_VS_FALLBACK.entities[0] + assert isinstance(desc, BinarySensorDesc) + class TestRemoteControlFallback: def test_generic_read(self): diff --git a/tests/test_range_ne6516a_capabilities.py b/tests/test_range_ne6516a_capabilities.py index ccf905c..c59c1cb 100644 --- a/tests/test_range_ne6516a_capabilities.py +++ b/tests/test_range_ne6516a_capabilities.py @@ -56,7 +56,10 @@ def test_energy_saving_and_cooktop_alert_present(): def test_child_lock_already_reads_correctly(): """issue #183 also reported child lock as unreadable -- but this dump's /kidslock/vs/0 ('Run') and /doors/vs/0's door lock field ('Lock') already - agree the door is locked, and the existing common.KIDS_LOCK_VS_FALLBACK - already reads that correctly with no code change needed.""" + agree the door is locked, and common.KIDS_LOCK_VS_FALLBACK already reads + that correctly with no further code change needed. Now a read-only + binary_sensor (issues #181/#183 -- the write side was never a confirmed + contract), whose 'lock' device class is inverted from a switch's plain + on/off: False means locked/closed, matching 'Run' here.""" state = _state() - assert state['child_lock'] is True + assert state['child_lock'] is False