The DTLS/CoAP transport code that made "ocf" an accurate name moved out to the smartthings-local package. What's left here (capability.py, entities.py, discovery.py, adapter.py, identity.py, capabilities/, by_type/, plus the /device/0 batch parser) is entirely the device capability registry, so name the package for what it does. Flattened the redundant ocf/registry/ nesting into a single top-level registry/ package and updated every import across the platform modules and test suite accordingly. Verified: full test suite (80/80) passes, and the Docker dev container reconnects to both live appliances and rediscovers their entities cleanly after the rename.
153 lines
5.8 KiB
Python
153 lines
5.8 KiB
Python
"""Unit tests for oven-family capabilities."""
|
|
from custom_components.localthings.registry.capabilities import oven
|
|
from custom_components.localthings.registry.discovery import discover
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OVEN_SETPOINT — NumberDesc with RMW write semantics
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_oven_setpoint_write_is_read_modify_write():
|
|
cap = oven.OVEN_SETPOINT
|
|
desc = cap.entities[0]
|
|
rep = {'x.com.samsung.da.items': [
|
|
{'x.com.samsung.da.id': 'Target', 'x.com.samsung.da.temperature': 0}]}
|
|
path, body = desc.write_fn(200, rep)
|
|
assert path[-1] == '0' # writes back to the resource
|
|
# the produced body preserves the items-array shape the oven expects
|
|
assert 'x.com.samsung.da.items' in body or 'x.com.samsung.da.temperature' in str(body)
|
|
|
|
|
|
def test_oven_setpoint_rmw_preserves_other_item_fields():
|
|
"""RMW must not drop sibling fields (e.g. x.com.samsung.da.current)."""
|
|
desc = oven.OVEN_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{
|
|
'x.com.samsung.da.current': '180',
|
|
'x.com.samsung.da.desired': '180',
|
|
}]}
|
|
path, body = desc.write_fn(200, rep)
|
|
item = body['x.com.samsung.da.items'][0]
|
|
assert item['x.com.samsung.da.desired'] == '200'
|
|
# sibling field preserved
|
|
assert item['x.com.samsung.da.current'] == '180'
|
|
|
|
|
|
def test_oven_setpoint_clamps_to_step():
|
|
"""Setpoint must be a multiple of SETPOINT_STEP_C."""
|
|
desc = oven.OVEN_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.desired': '0'}]}
|
|
_, body = desc.write_fn(202, rep)
|
|
# 202 → nearest 5 = 200
|
|
assert body['x.com.samsung.da.items'][0]['x.com.samsung.da.desired'] == '200'
|
|
|
|
|
|
def test_oven_setpoint_rejects_out_of_range():
|
|
desc = oven.OVEN_SETPOINT.entities[0]
|
|
rep = {'x.com.samsung.da.items': [{'x.com.samsung.da.desired': '100'}]}
|
|
assert desc.write_fn(10, rep) is None # below min (30)
|
|
assert desc.write_fn(300, rep) is None # above max (270)
|
|
|
|
|
|
def test_oven_setpoint_rejects_missing_items():
|
|
desc = oven.OVEN_SETPOINT.entities[0]
|
|
assert desc.write_fn(200, {}) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OVEN_MODE — SelectDesc with non-empty options
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_oven_mode_options_nonempty():
|
|
assert len(oven.OVEN_MODE.entities[0].options) > 0
|
|
|
|
|
|
def test_oven_mode_write_round_trips():
|
|
desc = oven.OVEN_MODE.entities[0]
|
|
valid_mode = desc.options[1] # e.g. 'Bake'
|
|
path, body = desc.write_fn(valid_mode, {})
|
|
assert path == ['mode', 'vs', '0']
|
|
assert body['x.com.samsung.da.modes'] == [valid_mode]
|
|
|
|
|
|
def test_oven_mode_rejects_unknown():
|
|
desc = oven.OVEN_MODE.entities[0]
|
|
assert desc.write_fn('SpaghettiMode', {}) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OVEN_MODE options-array RMW (lamp, sound, fast_preheat, natural_steam)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _mode_rep(*extra_opts):
|
|
return {'x.com.samsung.da.options': [
|
|
'UpperLamp_Off', 'Sound_On', 'fastpreheat_Off', *extra_opts,
|
|
]}
|
|
|
|
|
|
def test_lamp_write_replaces_slot():
|
|
desc = next(e for e in oven.OVEN_MODE.entities if e.key == 'lamp')
|
|
path, body = desc.write_fn('On', _mode_rep())
|
|
opts = body['x.com.samsung.da.options']
|
|
assert 'UpperLamp_On' in opts
|
|
assert 'UpperLamp_Off' not in opts
|
|
|
|
|
|
def test_lamp_write_requires_existing_options():
|
|
desc = next(e for e in oven.OVEN_MODE.entities if e.key == 'lamp')
|
|
assert desc.write_fn('On', {}) is None
|
|
|
|
|
|
def test_sound_write_preserves_other_options():
|
|
desc = next(e for e in oven.OVEN_MODE.entities if e.key == 'sound')
|
|
path, body = desc.write_fn('Off', _mode_rep())
|
|
opts = body['x.com.samsung.da.options']
|
|
assert 'Sound_Off' in opts
|
|
assert 'UpperLamp_Off' in opts # other slot unchanged
|
|
|
|
|
|
def test_natural_steam_appended_if_absent():
|
|
"""NaturalSteam slot is absent until first write — write_fn must append it."""
|
|
desc = next(e for e in oven.OVEN_MODE.entities if e.key == 'natural_steam')
|
|
path, body = desc.write_fn('On', _mode_rep()) # no NaturalSteam_* in rep
|
|
opts = body['x.com.samsung.da.options']
|
|
assert any(o.startswith('NaturalSteam_') for o in opts)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OVEN_OPERATIONAL_STATE — cycle_active BinarySensorDesc
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_cycle_active_true_when_running():
|
|
desc = next(e for e in oven.OVEN_OPERATIONAL_STATE.entities
|
|
if e.key == 'cycle_active')
|
|
assert desc.value_fn('Run') is True
|
|
assert desc.value_fn('Running') is True
|
|
|
|
|
|
def test_cycle_active_false_when_idle():
|
|
desc = next(e for e in oven.OVEN_OPERATIONAL_STATE.entities
|
|
if e.key == 'cycle_active')
|
|
assert desc.value_fn('Ready') is False
|
|
assert desc.value_fn(None) is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OVEN_OPERATIONAL_STATE — cook_time NumberDesc
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def test_cook_time_write_produces_hms():
|
|
desc = next(e for e in oven.OVEN_OPERATIONAL_STATE.entities
|
|
if e.key == 'cook_time')
|
|
path, body = desc.write_fn(90, {})
|
|
assert path == ['operational', 'state', 'vs', '0']
|
|
assert body['x.com.samsung.da.operationTime'] == '01:30:00'
|
|
assert body['x.com.samsung.da.remainingTime'] == '01:30:00'
|
|
|
|
|
|
def test_cook_time_rejects_out_of_range():
|
|
desc = next(e for e in oven.OVEN_OPERATIONAL_STATE.entities
|
|
if e.key == 'cook_time')
|
|
assert desc.write_fn(-1, {}) is None
|
|
assert desc.write_fn(1440, {}) is None
|
|
|