Add cooktop and range hood support
This commit is contained in:
1 parent
bedc662b47
commit
c64f58aaa5
25 files changed
+1399
-14
No files matched your search
Vendored
+73
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": [
|
||||
"x.com.samsung.devcol",
|
||||
"oic.wk.col"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.ll",
|
||||
"oic.if.b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.power": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"rt": [
|
||||
"x.com.samsung.da.mode"
|
||||
],
|
||||
"if": [
|
||||
"oic.if.baseline",
|
||||
"oic.if.a"
|
||||
],
|
||||
"x.com.samsung.da.options": [
|
||||
"DeviceType_NA9300K-/AA1",
|
||||
"MainTimerCurrent_0",
|
||||
"MainTimerSet_0",
|
||||
"MainTimerState_Ready",
|
||||
"OperationState0_Ready",
|
||||
"OperationState1_Ready",
|
||||
"OperationState3_Ready",
|
||||
"OperationState4_Ready",
|
||||
"OperationState5_Ready"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/connected/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.connected": "On"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {}
|
||||
},
|
||||
{
|
||||
"href": "/bluetooth/hood/status/vs/0",
|
||||
"rep": {
|
||||
"connectionState": "disconnected",
|
||||
"micomModelId": "",
|
||||
"firmwareVersion": "",
|
||||
"power": "off",
|
||||
"fanSpeed": 0,
|
||||
"lampState": "off",
|
||||
"timer": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"any_burner_active",
|
||||
"burner_0_state",
|
||||
"burner_1_state",
|
||||
"burner_3_state",
|
||||
"burner_4_state",
|
||||
"burner_5_state",
|
||||
"cloud_connected",
|
||||
"firmware_update",
|
||||
"main_timer_current",
|
||||
"main_timer_state",
|
||||
"paired_hood_connected",
|
||||
"paired_hood_fan_speed",
|
||||
"paired_hood_firmware",
|
||||
"paired_hood_light",
|
||||
"paired_hood_model",
|
||||
"paired_hood_power",
|
||||
"power_state"
|
||||
]
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"state_keys": [
|
||||
"air_sensing_state",
|
||||
"alarm_code",
|
||||
"auto_ventilation_action",
|
||||
"automatic_operation",
|
||||
"automatic_ventilation_state",
|
||||
"clean_level",
|
||||
"energy_kwh",
|
||||
"fan",
|
||||
"firmware_update",
|
||||
"hood_filter_capacity",
|
||||
"hood_filter_status",
|
||||
"hood_filter_usage",
|
||||
"lamp",
|
||||
"lamp_brightness",
|
||||
"last_air_sensing_level",
|
||||
"last_air_sensing_time",
|
||||
"periodic_air_sensing",
|
||||
"dust",
|
||||
"fine_dust",
|
||||
"super_fine_dust"
|
||||
]
|
||||
}
|
||||
+199
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"device0": [
|
||||
{
|
||||
"rt": ["x.com.samsung.devcol", "oic.wk.col"],
|
||||
"if": ["oic.if.baseline", "oic.if.ll", "oic.if.b"]
|
||||
},
|
||||
{
|
||||
"href": "/personality/presence/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "",
|
||||
"x.com.samsung.da.deviceId": "TEST-DEVICE-ID",
|
||||
"x.com.samsung.da.value": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/realtimenotiforclient/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.timeforshortnoti": "0",
|
||||
"x.com.samsung.da.longnotisubscription": "false",
|
||||
"x.com.samsung.da.periodicnotisubscription": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/airlevelcheck/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.periodicSensingActivationState": "Off",
|
||||
"x.com.samsung.da.periodicSensingInterval": "3600",
|
||||
"x.com.samsung.da.startSensingOnce": "Off",
|
||||
"x.com.samsung.da.sensingState": "NonProcessing",
|
||||
"x.com.samsung.da.lastSensingTime": "1784651378",
|
||||
"x.com.samsung.da.lastSensingLevel": "Kr2",
|
||||
"x.com.samsung.da.autoExeState": "Sensing",
|
||||
"x.com.samsung.da.supportedAutoExeState": ["Airpurify", "Alarm"],
|
||||
"x.com.samsung.da.periodicSensingSkipStatus": "Off",
|
||||
"x.com.samsung.da.periodicSensingSkipTime": "00000000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/alarms/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Alarm",
|
||||
"x.com.samsung.da.alarmType": "Device",
|
||||
"x.com.samsung.da.code": "ErrorCode_OFF",
|
||||
"x.com.samsung.da.triggeredTime": "2026-07-21T20:18:51",
|
||||
"x.com.samsung.da.state": "Deleted"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/energy/consumption/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.cumulativePower": "165620",
|
||||
"x.com.samsung.da.cumulativeSavedPower": "0",
|
||||
"x.com.samsung.da.cumulativeDate": "1784647130",
|
||||
"x.com.samsung.da.cumulativeDateUTC": "1784665130",
|
||||
"x.com.samsung.da.cumulativeUnit": "Wh"
|
||||
}
|
||||
},
|
||||
{"href": "/energy/consumption/0", "rep": {}},
|
||||
{
|
||||
"href": "/mode/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.options": ["OptionCode_19322", "UpdateAllow_NotAllowed"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/power/vs/0",
|
||||
"rep": {"x.com.samsung.da.power": "Off"}
|
||||
},
|
||||
{"href": "/power/0", "rep": {"value": false}},
|
||||
{
|
||||
"href": "/sensors/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.items": [
|
||||
{
|
||||
"x.com.samsung.da.id": "0",
|
||||
"x.com.samsung.da.description": "Sensor for CleanLevel",
|
||||
"x.com.samsung.da.type": "CleanLevel",
|
||||
"x.com.samsung.da.value": ["2"]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "2",
|
||||
"x.com.samsung.da.description": "Sensor for Dust",
|
||||
"x.com.samsung.da.type": "Dust",
|
||||
"x.com.samsung.da.value": ["18", "1"]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "3",
|
||||
"x.com.samsung.da.description": "Sensor for FineDust",
|
||||
"x.com.samsung.da.type": "FineDust",
|
||||
"x.com.samsung.da.value": ["14", "1"]
|
||||
},
|
||||
{
|
||||
"x.com.samsung.da.id": "4",
|
||||
"x.com.samsung.da.description": "Sensor for SuperFineDust",
|
||||
"x.com.samsung.da.type": "SuperFineDust",
|
||||
"x.com.samsung.da.value": ["9", "1"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/information/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.modelNum": "AHD-WW-TP1-22-COMMON|20136141|7800006B001713C44D00090001030000",
|
||||
"x.com.samsung.da.description": "AHD-WW-TP1-22-COMMON",
|
||||
"x.com.samsung.da.serialNum": "TEST-HOOD-SERIAL",
|
||||
"x.com.samsung.da.diagProtocolType": "WIFI_HTTPS",
|
||||
"x.com.samsung.da.diagSetupid": "KH0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/file/information/vs/0",
|
||||
"rep": {"x.com.samsung.timeoffset": "-05:00"}
|
||||
},
|
||||
{
|
||||
"href": "/configuration/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.region": "0000000000",
|
||||
"x.com.samsung.da.airconOptionList": ["DR"],
|
||||
"x.com.samsung.da.countryCode": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/0",
|
||||
"rep": {
|
||||
"DRLevel": 0,
|
||||
"start": "1970-01-01T00:00:00Z",
|
||||
"durationminutes": 0,
|
||||
"override": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/drlc/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.drlcLevel": "0",
|
||||
"x.com.samsung.da.durationminutes": "0",
|
||||
"x.com.samsung.da.start": "1970-01-01T00:00:00Z",
|
||||
"x.com.samsung.da.override": "Off",
|
||||
"x.com.samsung.da.realSaving": "Off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/availablecontrolsets/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.sets": "0000000000012000000000000000",
|
||||
"x.com.samsung.da.id": "HOOD",
|
||||
"x.com.samsung.da.version": "1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/da/softreset/vs/0",
|
||||
"rep": {"x.com.samsung.da.softwarereset": "false"}
|
||||
},
|
||||
{
|
||||
"href": "/filter/hoodfilter/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.filterUsage": "100",
|
||||
"x.com.samsung.da.filterUsageResolution": "1",
|
||||
"x.com.samsung.da.filterStatus": "wash",
|
||||
"x.com.samsung.da.filterCapacity": "60",
|
||||
"x.com.samsung.da.filterCapacityUnit": "Hour",
|
||||
"x.com.samsung.da.filterResetType": ["replaceable", "washable"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/hood/fanspeed/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.hood.autoOperation": "Off",
|
||||
"x.com.samsung.da.hood.fanSpeed": "14",
|
||||
"x.com.samsung.da.hood.supportedFanSpeed": ["14", "15", "16", "17"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"href": "/hood/lamp/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.lamp.power": "Off",
|
||||
"x.com.samsung.lamp.current": "2",
|
||||
"x.com.samsung.lamp.range": ["1", "2"]
|
||||
}
|
||||
},
|
||||
{"href": "/autoventilation/vs/0", "rep": {"action": "Start"}},
|
||||
{
|
||||
"href": "/otninformation/vs/0",
|
||||
"rep": {
|
||||
"x.com.samsung.da.target": "",
|
||||
"x.com.samsung.da.newVersionAvailable": "false"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -134,6 +134,31 @@ def test_run_discovery_detects_washer_via_model_fallback(
|
||||
assert coordinator.device_type_name == 'washer'
|
||||
|
||||
|
||||
def test_run_discovery_detects_cooktop_via_resource_signature(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
"""NA9300K omits oneUiVersion and information but has burner options."""
|
||||
from tests.conftest import _load_device
|
||||
|
||||
coordinator = LocalThingsCoordinator(hass, mock_entry)
|
||||
coordinator._run_discovery(_load_device('cooktop'))
|
||||
|
||||
assert coordinator.device_type_name == 'cooktop'
|
||||
assert coordinator._unbound_hrefs == []
|
||||
|
||||
|
||||
def test_run_discovery_detects_range_hood_via_model(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
from tests.conftest import _load_device
|
||||
|
||||
coordinator = LocalThingsCoordinator(hass, mock_entry)
|
||||
coordinator._run_discovery(_load_device('range_hood'))
|
||||
|
||||
assert coordinator.device_type_name == 'range_hood'
|
||||
assert coordinator._unbound_hrefs == []
|
||||
|
||||
|
||||
def test_update_coverage_gap_issue_creates_issue_for_unknown_type(
|
||||
hass: HomeAssistant, mock_entry
|
||||
) -> None:
|
||||
|
||||
@@ -41,6 +41,16 @@ class TestForDevice:
|
||||
assert isinstance(registry, DeviceRegistry)
|
||||
assert registry.name == 'refrigerator'
|
||||
|
||||
def test_for_device_returns_cooktop_registry(self):
|
||||
registry = for_device('7.0 Cooktop')
|
||||
assert registry is not None
|
||||
assert registry.name == 'cooktop'
|
||||
|
||||
def test_for_device_returns_range_hood_registry(self):
|
||||
registry = for_device('7.0 Range Hood')
|
||||
assert registry is not None
|
||||
assert registry.name == 'range_hood'
|
||||
|
||||
|
||||
class TestDeviceRegistries:
|
||||
"""Tests for device registries themselves."""
|
||||
@@ -180,6 +190,25 @@ class TestForDeviceByModel:
|
||||
assert reg is not None
|
||||
assert reg.name == 'airconditioner'
|
||||
|
||||
def test_cooktop_via_legacy_model_description(self):
|
||||
"""Older cooktops identify themselves as ARTIK051_GLOBAL_COOKTOP."""
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'ARTIK051_GB_CT_001|40424141|50000204001211000200000000000000',
|
||||
'ARTIK051_GLOBAL_COOKTOP',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'cooktop'
|
||||
|
||||
def test_range_hood_via_ahd_model(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model(
|
||||
'AHD-WW-TP1-22-COMMON|20136141|7800006B001713C44D00090001030000',
|
||||
'AHD-WW-TP1-22-COMMON',
|
||||
)
|
||||
assert reg is not None
|
||||
assert reg.name == 'range_hood'
|
||||
|
||||
def test_unknown_model_returns_none(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
reg = for_device_by_model('SOME-UNKNOWN-BOARD', 'SOME-UNKNOWN-BOARD')
|
||||
@@ -188,3 +217,38 @@ class TestForDeviceByModel:
|
||||
def test_empty_inputs_return_none(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
assert for_device_by_model('', '') is None
|
||||
|
||||
|
||||
class TestForDeviceByResources:
|
||||
def test_na9300k_without_one_ui_or_information_is_cooktop(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
from tests.conftest import _load_device
|
||||
|
||||
reg = for_device_by_resources(_load_device('cooktop'))
|
||||
|
||||
assert reg is not None
|
||||
assert reg.name == 'cooktop'
|
||||
|
||||
def test_unrelated_mode_options_are_not_cooktop(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
|
||||
resources = {
|
||||
'/mode/vs/0': {
|
||||
'x.com.samsung.da.options': [
|
||||
'DeviceType_SOME_OVEN',
|
||||
'UpperLamp_Off',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
assert for_device_by_resources(resources) is None
|
||||
|
||||
def test_hood_resource_signature(self):
|
||||
from custom_components.localthings.registry.by_type import for_device_by_resources
|
||||
resources = {
|
||||
'/hood/fanspeed/vs/0': {},
|
||||
'/hood/lamp/vs/0': {},
|
||||
}
|
||||
reg = for_device_by_resources(resources)
|
||||
assert reg is not None
|
||||
assert reg.name == 'range_hood'
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Tests for the read-only cooktop capability profile."""
|
||||
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import cooktop
|
||||
from custom_components.localthings.registry.capabilities.cooktop import (
|
||||
COOKTOP_MODE,
|
||||
)
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _state(resources):
|
||||
bound = discover(
|
||||
resources,
|
||||
cooktop.REGISTRY.capabilities,
|
||||
cooktop.REGISTRY.pattern_capabilities,
|
||||
)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_real_cooktop_fixture_has_expected_idle_state():
|
||||
state = _state(_load_device('cooktop'))
|
||||
|
||||
assert state['power_state'] == 'On'
|
||||
assert state['any_burner_active'] is False
|
||||
assert state['burner_0_state'] == 'Ready'
|
||||
assert state['burner_5_state'] == 'Ready'
|
||||
assert state['main_timer_state'] == 'Ready'
|
||||
assert state['main_timer_current'] == 0
|
||||
assert state['cloud_connected'] is True
|
||||
assert state['paired_hood_connected'] is False
|
||||
assert state['paired_hood_power'] is False
|
||||
assert state['paired_hood_fan_speed'] == 0
|
||||
assert state['paired_hood_light'] is False
|
||||
|
||||
|
||||
def test_run_operation_marks_any_burner_active():
|
||||
resources = _load_device('cooktop')
|
||||
options = resources['/mode/vs/0']['x.com.samsung.da.options']
|
||||
options[options.index('OperationState3_Ready')] = 'OperationState3_Run'
|
||||
|
||||
state = _state(resources)
|
||||
|
||||
assert state['burner_3_state'] == 'Run'
|
||||
assert state['any_burner_active'] is True
|
||||
|
||||
|
||||
def test_cooktop_profile_has_no_write_functions():
|
||||
"""A local integration must never expose unverified remote ignition."""
|
||||
descriptions = [
|
||||
desc
|
||||
for capabilities in cooktop.REGISTRY.capabilities.values()
|
||||
for capability in capabilities
|
||||
for desc in capability.entities
|
||||
]
|
||||
|
||||
assert descriptions
|
||||
assert all(not hasattr(desc, 'write_fn') or desc.write_fn is None
|
||||
for desc in descriptions)
|
||||
|
||||
|
||||
def test_known_na9300k_slots_are_present():
|
||||
keys = {desc.key for desc in COOKTOP_MODE.entities}
|
||||
|
||||
assert {
|
||||
'burner_0_state', 'burner_1_state', 'burner_3_state',
|
||||
'burner_4_state', 'burner_5_state',
|
||||
} <= keys
|
||||
@@ -1,6 +1,6 @@
|
||||
from custom_components.localthings.registry.entities import (
|
||||
SensorDesc, BinarySensorDesc, SelectDesc, SwitchDesc, ButtonDesc,
|
||||
NumberDesc, TimeDesc, ClimateDesc, PLATFORM_OF,
|
||||
NumberDesc, TimeDesc, ClimateDesc, FanDesc, PLATFORM_OF,
|
||||
)
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ def test_platform_mapping_covers_all_subclasses():
|
||||
assert PLATFORM_OF[NumberDesc] == 'number'
|
||||
assert PLATFORM_OF[TimeDesc] == 'time'
|
||||
assert PLATFORM_OF[ClimateDesc] == 'climate'
|
||||
assert PLATFORM_OF[FanDesc] == 'fan'
|
||||
|
||||
|
||||
def test_select_carries_options_and_write_fn():
|
||||
|
||||
@@ -7,7 +7,9 @@ GOLDEN = Path(__file__).parent / 'fixtures' / 'golden'
|
||||
|
||||
|
||||
def _new_state_keys(name, resources):
|
||||
from custom_components.localthings.registry.by_type import for_device, for_device_by_model
|
||||
from custom_components.localthings.registry.by_type import (
|
||||
for_device, for_device_by_model, for_device_by_resources,
|
||||
)
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
otn = resources.get('/otninformation/vs/0', {})
|
||||
@@ -19,6 +21,8 @@ def _new_state_keys(name, resources):
|
||||
info.get('x.com.samsung.da.modelNum', ''),
|
||||
info.get('x.com.samsung.da.description', ''),
|
||||
)
|
||||
if reg is None:
|
||||
reg = for_device_by_resources(resources)
|
||||
if reg is None:
|
||||
from custom_components.localthings.registry.registry import CAPABILITIES
|
||||
caps, pats = CAPABILITIES, []
|
||||
@@ -81,6 +85,30 @@ def test_registry_reproduces_golden_state_keys_for_airconditioner():
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_cooktop():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('cooktop')
|
||||
golden = json.loads((GOLDEN / 'cooktop.json').read_text())
|
||||
state_keys = _new_state_keys('cooktop', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_registry_reproduces_golden_state_keys_for_range_hood():
|
||||
from tests.conftest import _load_device
|
||||
resources = _load_device('range_hood')
|
||||
golden = json.loads((GOLDEN / 'range_hood.json').read_text())
|
||||
state_keys = _new_state_keys('range_hood', resources)
|
||||
assert set(state_keys) == set(golden['state_keys']), (
|
||||
f"state_keys mismatch:\n"
|
||||
f" extra: {sorted(set(state_keys) - set(golden['state_keys']))}\n"
|
||||
f" missing: {sorted(set(golden['state_keys']) - set(state_keys))}"
|
||||
)
|
||||
|
||||
|
||||
def test_resources_from_batch_preferred_over_flat():
|
||||
from tests.conftest import _resources_from_dump
|
||||
dump = {
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""Tests for the AHD-WW-TP1-22 range-hood profile."""
|
||||
|
||||
from custom_components.localthings.registry.adapter import flatten
|
||||
from custom_components.localthings.registry.by_type import for_device_by_model
|
||||
from custom_components.localthings.registry.capabilities import range_hood
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import FanDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _hood():
|
||||
resources = _load_device('range_hood')
|
||||
info = resources['/information/vs/0']
|
||||
registry = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'],
|
||||
info['x.com.samsung.da.description'],
|
||||
)
|
||||
return registry, resources
|
||||
|
||||
|
||||
def _state():
|
||||
registry, resources = _hood()
|
||||
bound = discover(
|
||||
resources, registry.capabilities, registry.pattern_capabilities,
|
||||
)
|
||||
return flatten(bound, resources)
|
||||
|
||||
|
||||
def test_ahd_model_resolves_to_range_hood_registry():
|
||||
registry, _ = _hood()
|
||||
assert registry is not None
|
||||
assert registry.name == 'range_hood'
|
||||
|
||||
|
||||
def test_range_hood_has_no_unbound_hrefs():
|
||||
registry, resources = _hood()
|
||||
unbound = []
|
||||
discover(
|
||||
resources,
|
||||
registry.capabilities,
|
||||
registry.pattern_capabilities,
|
||||
log=unbound.append,
|
||||
)
|
||||
assert unbound == []
|
||||
|
||||
|
||||
def test_range_hood_fixture_values():
|
||||
state = _state()
|
||||
assert state['fan'] == '14'
|
||||
assert state['alarm_code'] == 'none'
|
||||
assert state['automatic_operation'] is False
|
||||
assert state['lamp'] is False
|
||||
assert state['lamp_brightness'] == '2'
|
||||
assert state['hood_filter_usage'] == 100
|
||||
assert state['hood_filter_status'] == 'wash'
|
||||
assert state['energy_kwh'] == 165.62
|
||||
assert state['clean_level'] == 2
|
||||
assert state['dust'] == 18
|
||||
assert state['fine_dust'] == 14
|
||||
assert state['super_fine_dust'] == 9
|
||||
assert state['periodic_air_sensing'] is False
|
||||
assert state['air_sensing_state'] == 'NonProcessing'
|
||||
assert state['last_air_sensing_level'] == 'Kr2'
|
||||
|
||||
|
||||
def test_one_composite_fan_is_bound():
|
||||
registry, resources = _hood()
|
||||
bound = discover(
|
||||
resources, registry.capabilities, registry.pattern_capabilities,
|
||||
)
|
||||
fans = [entity for entity in bound if isinstance(entity.desc, FanDesc)]
|
||||
assert len(fans) == 1
|
||||
assert fans[0].href == '/hood/fanspeed/vs/0'
|
||||
|
||||
|
||||
def test_fan_write_contract():
|
||||
desc = range_hood.HOOD_FAN.entities[0]
|
||||
rep = {
|
||||
'x.com.samsung.da.hood.supportedFanSpeed': ['14', '15', '16', '17'],
|
||||
}
|
||||
assert desc.write_fn(('power', True), rep) == (
|
||||
['power', '0'], {'value': True},
|
||||
)
|
||||
assert desc.write_fn(('power', False), rep) == (
|
||||
['power', '0'], {'value': False},
|
||||
)
|
||||
assert desc.write_fn(('speed', '16'), rep) == (
|
||||
['hood', 'fanspeed', 'vs', '0'],
|
||||
{'x.com.samsung.da.hood.fanSpeed': '16'},
|
||||
)
|
||||
assert desc.write_fn(('speed', '99'), rep) is None
|
||||
|
||||
|
||||
def test_lamp_write_contract():
|
||||
lamp, brightness = range_hood.HOOD_LAMP.entities
|
||||
rep = {'x.com.samsung.lamp.range': ['1', '2']}
|
||||
assert lamp.write_fn('On', rep) == (
|
||||
['hood', 'lamp', 'vs', '0'], {'x.com.samsung.lamp.power': 'On'},
|
||||
)
|
||||
assert lamp.write_fn('Off', rep) == (
|
||||
['hood', 'lamp', 'vs', '0'], {'x.com.samsung.lamp.power': 'Off'},
|
||||
)
|
||||
assert brightness.write_fn('1', rep) == (
|
||||
['hood', 'lamp', 'vs', '0'], {'x.com.samsung.lamp.current': '1'},
|
||||
)
|
||||
assert brightness.write_fn('2', rep) == (
|
||||
['hood', 'lamp', 'vs', '0'], {'x.com.samsung.lamp.current': '2'},
|
||||
)
|
||||
assert brightness.write_fn('Unsupported', rep) is None
|
||||
@@ -0,0 +1,61 @@
|
||||
"""HA fan-entity mapping tests for the range hood."""
|
||||
|
||||
from custom_components.localthings.fan import LocalThingsRangeHoodFan
|
||||
from custom_components.localthings.registry.by_type import range_hood
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import FanDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
class _FakeCoordinator:
|
||||
device_serial = 'TEST-HOOD-SERIAL'
|
||||
device_info = {}
|
||||
data = {}
|
||||
|
||||
def __init__(self, resources):
|
||||
self.last_resources = resources
|
||||
|
||||
def resource(self, href):
|
||||
return self.last_resources.get(href, {})
|
||||
|
||||
|
||||
def _entity(resources):
|
||||
bound = discover(
|
||||
resources,
|
||||
range_hood.REGISTRY.capabilities,
|
||||
range_hood.REGISTRY.pattern_capabilities,
|
||||
)
|
||||
fan_bound = next(item for item in bound if isinstance(item.desc, FanDesc))
|
||||
return LocalThingsRangeHoodFan(_FakeCoordinator(resources), fan_bound)
|
||||
|
||||
|
||||
def test_power_off_maps_to_zero_percent_and_four_retained_speeds():
|
||||
entity = _entity(_load_device('range_hood'))
|
||||
assert entity.is_on is False
|
||||
assert entity.speed_count == 4
|
||||
assert entity.percentage == 0
|
||||
|
||||
|
||||
def test_active_codes_map_to_ordered_percentages():
|
||||
resources = _load_device('range_hood')
|
||||
resources['/power/0']['value'] = True
|
||||
|
||||
resources['/hood/fanspeed/vs/0'][
|
||||
'x.com.samsung.da.hood.fanSpeed'
|
||||
] = '14'
|
||||
assert _entity(resources).percentage == 25
|
||||
|
||||
resources['/hood/fanspeed/vs/0'][
|
||||
'x.com.samsung.da.hood.fanSpeed'
|
||||
] = '15'
|
||||
assert _entity(resources).percentage == 50
|
||||
|
||||
resources['/hood/fanspeed/vs/0'][
|
||||
'x.com.samsung.da.hood.fanSpeed'
|
||||
] = '16'
|
||||
assert _entity(resources).percentage == 75
|
||||
|
||||
resources['/hood/fanspeed/vs/0'][
|
||||
'x.com.samsung.da.hood.fanSpeed'
|
||||
] = '17'
|
||||
assert _entity(resources).percentage == 100
|
||||
Reference in new issue
Block a user