Files
localthings/tests/test_dehumidifier_capabilities.py
T
Marc Billow daf7e3787f Add ruff (lint + format) and ty (type checking) to the project
Adds [tool.ruff] and [tool.ty] config to pyproject.toml with a curated
ruff rule set (E, F, W, I, UP, B, C4, SIM, RUF, ASYNC, LOG, G, PIE, RET,
PERF, N), pins ruff/ty in requirements-dev.txt, reformats the whole tree
with `ruff format`, and fixes the pre-existing lint and type-check debt
those tools surfaced so both run clean.

Production-code type fixes include: HA's ConfigFlowResult vs. the
generic FlowResult in config_flow.py, narrowing BoundEntity.desc to its
platform-specific subclass (SelectDesc/NumberDesc/SensorDesc/etc.) via
cast() instead of an unchecked annotation, converting HA device_class
strings to their proper enum types, a resolve_registry callback typed
as `object` instead of `DeviceRegistry | None`, and a couple of other
narrow correctness fixes (CA key type validation, an index-out-of-bounds
false positive from an empty-tuple fallback, a bool/dict argument swap).

Test-file fixes are mechanical: narrowing SamsungEntityDescription to
the correct subclass via isinstance()/cast() before accessing
subclass-only fields, and asserting Optional write_fn/unit_fn fields
are set before calling them.
2026-08-02 23:56:38 +00:00

127 lines
4.0 KiB
Python

"""Tests for Samsung dehumidifier support (issue #88, AY18CG7500GED).
HA-free like the rest of the suite: exercises the registry, discovery/
flatten, and the target-humidity/operating-mode write contracts.
"""
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.discovery import discover
from custom_components.localthings.registry.entities import NumberDesc, SelectDesc
from tests.conftest import _load_device
def _dehumidifier():
resources = _load_device("dehumidifier")
info = resources["/information/vs/0"]
reg = for_device_by_model(
info["x.com.samsung.da.modelNum"],
info["x.com.samsung.da.description"],
)
return reg, resources
def _bound():
reg, resources = _dehumidifier()
return discover(resources, reg.capabilities, reg.pattern_capabilities), resources
def _state():
bound, resources = _bound()
return flatten(bound, resources)
def _desc(key):
bound, _ = _bound()
return next(b.desc for b in bound if b.desc.key == key)
def test_model_resolves_to_dehumidifier_registry():
reg, _ = _dehumidifier()
assert reg is not None and reg.name == "dehumidifier"
def test_no_unbound_hrefs():
"""Every resource in the issue #88 dump binds or is covered -- clears
the coverage-gap repair."""
reg, resources = _dehumidifier()
unbound = []
discover(resources, reg.capabilities, reg.pattern_capabilities, log=unbound.append)
assert unbound == []
def test_expected_state_keys_present():
state = _state()
for key in (
"humidity",
"target_humidity",
"operating_mode",
"power_switch",
"auto_clean",
"air_filter_status",
"mute_once",
):
assert key in state, key
def test_humidity_sensor_reads_current_value():
state = _state()
assert state["humidity"] == 47
def test_target_humidity_number_reads_desired_value():
state = _state()
assert state["target_humidity"] == 50
def test_target_humidity_write_contract():
desc = _desc("target_humidity")
assert isinstance(desc, NumberDesc)
path, body = desc.write_fn("55", {})
assert path == ["humidity", "vs", "0"]
assert body == {"x.com.samsung.da.desiredHumidity": "55"}
def test_target_humidity_step_reads_live_increment():
"""Step comes from the device's own `increment` field rather than a
hardcoded constant -- see the adding-device-support skill's 'never
hard-code the one dump's values' section."""
desc = _desc("target_humidity")
assert desc.step_fn({"increment": "5"}) == 5
assert desc.step_fn({"increment": "10"}) == 10
# No live field: falls back to a sane default rather than raising.
assert desc.step_fn({}) == 1
def test_target_humidity_has_no_hardcoded_bounds():
"""No range field is present in any dump seen so far -- native_min/max
are deliberately left unset (falls back to HA's own 0-100 default for a
percentage field) rather than a bound guessed from a spec sheet."""
desc = _desc("target_humidity")
assert desc.native_min is None
assert desc.native_max is None
assert desc.native_min_fn is None
assert desc.native_max_fn is None
def test_operating_mode_select_options_come_from_live_supported_modes():
"""Options are read live from x.com.samsung.da.supportedModes, not a
hardcoded tuple -- so a future device with a different mode set is
handled automatically."""
desc = _desc("operating_mode")
assert isinstance(desc, SelectDesc)
assert desc.options_field == "x.com.samsung.da.supportedModes"
assert desc.options == ()
def test_operating_mode_write_contract():
desc = _desc("operating_mode")
path, body = desc.write_fn("Quiet", {})
assert path == ["mode", "vs", "0"]
assert body == {"x.com.samsung.da.modes": ["Quiet"]}
def test_operating_mode_reads_first_mode():
state = _state()
assert state["operating_mode"] == "Smart"