feat: laundry firmware-flag gating, fridge vendor temp writes, course names
This commit is contained in:
1 parent
aff7647460
commit
00cf8d676d
8 files changed
+349
-12
No files matched your search
@@ -16,6 +16,7 @@ from custom_components.localthings.const import (
|
||||
from custom_components.localthings.coordinator import LocalThingsCoordinator
|
||||
from custom_components.localthings.registry.capabilities.common import (
|
||||
remote_control_enabled,
|
||||
remote_control_required_for_write,
|
||||
)
|
||||
from custom_components.localthings.observe import MODE_OBSERVE, MODE_POLL, PUSH_HEALTH_WINDOW_S
|
||||
|
||||
@@ -880,6 +881,38 @@ class TestRemoteControlEnabled:
|
||||
assert remote_control_enabled({}) is True
|
||||
|
||||
|
||||
class TestRemoteControlRequiredForWrite:
|
||||
"""isModelSettingWithoutSC auto-exempts settings writes, not cycle control."""
|
||||
|
||||
def test_without_flag_always_requires(self):
|
||||
resources = {}
|
||||
assert remote_control_required_for_write(resources, '/washer/vs/0') is True
|
||||
assert remote_control_required_for_write(
|
||||
resources, '/operational/state/vs/0') is True
|
||||
|
||||
def test_without_sc_exempts_settings_keeps_operational(self):
|
||||
resources = {
|
||||
'/wm/setinfo/vs/0': {
|
||||
'x.com.samsung.da.isModelSettingWithoutSC': 'true',
|
||||
},
|
||||
}
|
||||
assert remote_control_required_for_write(resources, '/washer/vs/0') is False
|
||||
assert remote_control_required_for_write(resources, '/course/vs/0') is False
|
||||
assert remote_control_required_for_write(resources, '/buzzersound/vs/0') is False
|
||||
assert remote_control_required_for_write(
|
||||
resources, '/operational/state/vs/0') is True
|
||||
assert remote_control_required_for_write(
|
||||
resources, '/operational/state/0') is True
|
||||
|
||||
def test_without_sc_false_still_requires(self):
|
||||
resources = {
|
||||
'/wm/setinfo/vs/0': {
|
||||
'x.com.samsung.da.isModelSettingWithoutSC': 'false',
|
||||
},
|
||||
}
|
||||
assert remote_control_required_for_write(resources, '/washer/vs/0') is True
|
||||
|
||||
|
||||
async def test_send_command_blocked_when_remote_control_disabled(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
@@ -1024,3 +1057,95 @@ async def test_send_command_bypasses_remote_control_when_option_enabled(
|
||||
await coordinator.async_send_command(bound, 5)
|
||||
|
||||
assert coordinator._cache.get('/some/path') == {'value': 5}
|
||||
|
||||
|
||||
async def test_send_command_settings_allowed_when_without_sc(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
"""Laundry boards with isModelSettingWithoutSC=true accept settings
|
||||
writes while remote control is off -- no options-flow bypass needed."""
|
||||
from custom_components.localthings.registry.discovery import BoundEntity
|
||||
from custom_components.localthings.registry.entities import SelectDesc
|
||||
|
||||
fake = mock_coordinator_observe_session
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||
coordinator._cache.apply_rep(
|
||||
'/remotectrl/vs/0',
|
||||
{'x.com.samsung.da.remoteControlEnabled': 'false'},
|
||||
source='test',
|
||||
)
|
||||
coordinator._cache.apply_rep(
|
||||
'/wm/setinfo/vs/0',
|
||||
{'x.com.samsung.da.isModelSettingWithoutSC': 'true'},
|
||||
source='test',
|
||||
)
|
||||
|
||||
def _write_fn(payload, rep, href=None):
|
||||
return (['washer', 'vs', '0'], {'x.com.samsung.da.spinLevel': payload})
|
||||
|
||||
desc = SelectDesc(key='spin_speed', field='x.com.samsung.da.spinLevel',
|
||||
write_fn=_write_fn)
|
||||
bound = BoundEntity(
|
||||
href='/washer/vs/0', capability=coordinator.bound[0].capability, desc=desc,
|
||||
)
|
||||
|
||||
with patch.object(fake, 'subscribe'):
|
||||
fake.post = lambda *a, **k: (0x44, b'')
|
||||
await coordinator.async_send_command(bound, '1000')
|
||||
|
||||
assert coordinator._cache.get('/washer/vs/0') == {
|
||||
'x.com.samsung.da.spinLevel': '1000',
|
||||
}
|
||||
|
||||
|
||||
async def test_send_command_operational_still_blocked_when_without_sc(
|
||||
hass: HomeAssistant, mock_entry, mock_coordinator_observe_session
|
||||
) -> None:
|
||||
"""Start/Pause/Stop stay gated on Smart Control even when settings
|
||||
are allowed without it -- isModelSettingWithoutSC is settings-only."""
|
||||
from custom_components.localthings.registry.discovery import BoundEntity
|
||||
from custom_components.localthings.registry.entities import ButtonDesc
|
||||
|
||||
fake = mock_coordinator_observe_session
|
||||
await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
coordinator: LocalThingsCoordinator = hass.data[DOMAIN][mock_entry.entry_id]
|
||||
coordinator._cache.apply_rep(
|
||||
'/remotectrl/vs/0',
|
||||
{'x.com.samsung.da.remoteControlEnabled': 'false'},
|
||||
source='test',
|
||||
)
|
||||
coordinator._cache.apply_rep(
|
||||
'/wm/setinfo/vs/0',
|
||||
{'x.com.samsung.da.isModelSettingWithoutSC': 'true'},
|
||||
source='test',
|
||||
)
|
||||
|
||||
def _write_fn(payload, rep, href=None):
|
||||
return (['operational', 'state', 'vs', '0'],
|
||||
{'x.com.samsung.da.state': payload})
|
||||
|
||||
desc = ButtonDesc(key='start', payload='Run', write_fn=_write_fn)
|
||||
bound = BoundEntity(
|
||||
href='/operational/state/vs/0',
|
||||
capability=coordinator.bound[0].capability,
|
||||
desc=desc,
|
||||
)
|
||||
|
||||
posted = False
|
||||
|
||||
def _post(*a, **k):
|
||||
nonlocal posted
|
||||
posted = True
|
||||
return (0x44, b'')
|
||||
|
||||
with patch.object(fake, 'subscribe'):
|
||||
fake.post = _post
|
||||
with pytest.raises(ServiceValidationError) as exc_info:
|
||||
await coordinator.async_send_command(bound, 'Run')
|
||||
|
||||
assert exc_info.value.translation_domain == DOMAIN
|
||||
assert exc_info.value.translation_key == 'remote_control_disabled'
|
||||
assert posted is False
|
||||
@@ -1,5 +1,7 @@
|
||||
from custom_components.localthings.registry.capabilities import common
|
||||
from custom_components.localthings.registry.discovery import discover
|
||||
from custom_components.localthings.registry.entities import BinarySensorDesc, SwitchDesc
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _reg():
|
||||
@@ -65,7 +67,7 @@ class TestMergeOptionsField:
|
||||
class TestPowerFallback:
|
||||
def test_generic_href_read_write(self):
|
||||
assert common.POWER_GENERIC.href == '/power/0'
|
||||
desc = common.POWER_GENERIC.entities[0]
|
||||
desc = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
assert desc.value_fn(True) is True
|
||||
assert desc.value_fn(False) is False
|
||||
path, body = desc.write_fn('On', {})
|
||||
@@ -80,13 +82,72 @@ class TestPowerFallback:
|
||||
{}, {'/power/0': {}, '/power/vs/0': {}}) is False
|
||||
|
||||
def test_vs_fallback_read_write(self):
|
||||
desc = common.POWER_VS_FALLBACK.entities[0]
|
||||
desc = next(e for e in common.POWER_VS_FALLBACK.entities if isinstance(e, SwitchDesc))
|
||||
assert desc.value_fn('On') is True
|
||||
assert desc.value_fn('Off') is False
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['power', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.power': 'On'}
|
||||
|
||||
def test_power_switch_hidden_when_firmware_disallows(self):
|
||||
switch = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
sensor = next(e for e in common.POWER_GENERIC.entities if isinstance(e, BinarySensorDesc))
|
||||
resources = {
|
||||
'/power/0': {'value': True},
|
||||
'/wm/setinfo/vs/0': {'x.com.samsung.da.isModelSettingPowerOnOff': 'false'},
|
||||
}
|
||||
assert switch.exists_fn(resources['/power/0'], resources) is False
|
||||
assert sensor.exists_fn(resources['/power/0'], resources) is True
|
||||
|
||||
def test_power_switch_writable_when_firmware_allows(self):
|
||||
switch = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
sensor = next(e for e in common.POWER_GENERIC.entities if isinstance(e, BinarySensorDesc))
|
||||
resources = {
|
||||
'/power/0': {'value': True},
|
||||
'/wm/setinfo/vs/0': {'x.com.samsung.da.isModelSettingPowerOnOff': 'true'},
|
||||
}
|
||||
assert switch.exists_fn(resources['/power/0'], resources) is True
|
||||
assert sensor.exists_fn(resources['/power/0'], resources) is False
|
||||
|
||||
def test_power_switch_default_writable_without_setinfo(self):
|
||||
switch = next(e for e in common.POWER_GENERIC.entities if isinstance(e, SwitchDesc))
|
||||
sensor = next(e for e in common.POWER_GENERIC.entities if isinstance(e, BinarySensorDesc))
|
||||
resources = {'/power/0': {'value': True}}
|
||||
assert switch.exists_fn(resources['/power/0'], resources) is True
|
||||
assert sensor.exists_fn(resources['/power/0'], resources) is False
|
||||
|
||||
|
||||
class TestWmSetinfoFlags:
|
||||
def test_washer_fixture_carries_setinfo_from_device0(self):
|
||||
"""/wm/setinfo/vs/0 lands in the seed snapshot -- no dedicated capability."""
|
||||
resources = _load_device('washer')
|
||||
assert '/wm/setinfo/vs/0' in resources
|
||||
assert common.model_allows_power_on_off(resources) is False
|
||||
assert common.model_setting_without_sc(resources) is True
|
||||
|
||||
def test_dishwasher_allows_power_on_off(self):
|
||||
resources = _load_device('dishwasher')
|
||||
assert common.model_allows_power_on_off(resources) is True
|
||||
assert common.model_setting_without_sc(resources) is False
|
||||
|
||||
def test_deleted_alarms_filtered(self):
|
||||
assert common._active_alarm_codes([
|
||||
{
|
||||
'x.com.samsung.da.code': 'ErrorCode',
|
||||
'x.com.samsung.da.state': 'Deleted',
|
||||
},
|
||||
]) == 'none'
|
||||
assert common._active_alarm_codes([
|
||||
{
|
||||
'x.com.samsung.da.code': 'LE',
|
||||
'x.com.samsung.da.state': 'Triggered',
|
||||
},
|
||||
{
|
||||
'x.com.samsung.da.code': 'ErrorCode',
|
||||
'x.com.samsung.da.state': 'Deleted',
|
||||
},
|
||||
]) == 'LE'
|
||||
|
||||
|
||||
class TestKidsLockFallback:
|
||||
def test_generic_read_write(self):
|
||||
|
||||
Reference in new issue
Block a user