"""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