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.
This commit is contained in:
147 files changed
+9977
-7584
No files matched your search
@@ -4,19 +4,20 @@
|
||||
HA-free like the rest of the suite: exercises the registry, discovery/
|
||||
flatten, and the zone/dhw mode and temperature 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, WaterHeaterDesc
|
||||
|
||||
from tests.conftest import _load_device
|
||||
|
||||
|
||||
def _ehs():
|
||||
resources = _load_device('ehs')
|
||||
info = resources['/information/vs/0']
|
||||
resources = _load_device("ehs")
|
||||
info = resources["/information/vs/0"]
|
||||
reg = for_device_by_model(
|
||||
info['x.com.samsung.da.modelNum'], info['x.com.samsung.da.description'],
|
||||
info["x.com.samsung.da.modelNum"],
|
||||
info["x.com.samsung.da.description"],
|
||||
)
|
||||
return reg, resources
|
||||
|
||||
@@ -38,7 +39,7 @@ def _desc(key):
|
||||
|
||||
def test_model_resolves_to_ehs_registry():
|
||||
reg, _ = _ehs()
|
||||
assert reg is not None and reg.name == 'ehs'
|
||||
assert reg is not None and reg.name == "ehs"
|
||||
|
||||
|
||||
def test_no_unbound_hrefs():
|
||||
@@ -53,72 +54,81 @@ def test_no_unbound_hrefs():
|
||||
|
||||
def test_expected_state_keys_present():
|
||||
state = _state()
|
||||
for key in ('zone_power', 'zone_mode', 'zone_temperature', 'zone_target_temperature',
|
||||
'water_heater', 'away_mode', 'mute_once', 'alarm_code', 'energy_kwh'):
|
||||
for key in (
|
||||
"zone_power",
|
||||
"zone_mode",
|
||||
"zone_temperature",
|
||||
"zone_target_temperature",
|
||||
"water_heater",
|
||||
"away_mode",
|
||||
"mute_once",
|
||||
"alarm_code",
|
||||
"energy_kwh",
|
||||
):
|
||||
assert key in state, key
|
||||
|
||||
|
||||
def test_zone_temperature_reads_current_value():
|
||||
state = _state()
|
||||
assert state['zone_temperature'] == 30.0
|
||||
assert state["zone_temperature"] == 30.0
|
||||
|
||||
|
||||
def test_zone_target_temperature_reads_desired_value():
|
||||
state = _state()
|
||||
assert state['zone_target_temperature'] == 5.0
|
||||
assert state["zone_target_temperature"] == 5.0
|
||||
|
||||
|
||||
def test_zone_mode_reads_first_mode():
|
||||
state = _state()
|
||||
assert state['zone_mode'] == 'Cool'
|
||||
assert state["zone_mode"] == "Cool"
|
||||
|
||||
|
||||
def test_zone_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 firmware with a different mode set is
|
||||
handled automatically."""
|
||||
desc = _desc('zone_mode')
|
||||
desc = _desc("zone_mode")
|
||||
assert isinstance(desc, SelectDesc)
|
||||
assert desc.options_field == 'x.com.samsung.da.supportedModes'
|
||||
assert desc.options_field == "x.com.samsung.da.supportedModes"
|
||||
assert desc.options == ()
|
||||
|
||||
|
||||
def test_zone_mode_write_contract():
|
||||
desc = _desc('zone_mode')
|
||||
path, body = desc.write_fn('Heat', {})
|
||||
assert path == ['mode', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.modes': ['Heat']}
|
||||
desc = _desc("zone_mode")
|
||||
path, body = desc.write_fn("Heat", {})
|
||||
assert path == ["mode", "vs", "0"]
|
||||
assert body == {"x.com.samsung.da.modes": ["Heat"]}
|
||||
|
||||
|
||||
def test_zone_power_reads_off():
|
||||
state = _state()
|
||||
assert state['zone_power'] is False
|
||||
assert state["zone_power"] is False
|
||||
|
||||
|
||||
def test_zone_power_write_contract():
|
||||
desc = _desc('zone_power')
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['power', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.power': 'On'}
|
||||
desc = _desc("zone_power")
|
||||
path, body = desc.write_fn("On", {})
|
||||
assert path == ["power", "vs", "0"]
|
||||
assert body == {"x.com.samsung.da.power": "On"}
|
||||
|
||||
|
||||
def test_zone_target_temperature_write_contract():
|
||||
desc = _desc('zone_target_temperature')
|
||||
desc = _desc("zone_target_temperature")
|
||||
assert isinstance(desc, NumberDesc)
|
||||
path, body = desc.write_fn('21.5', {})
|
||||
assert path == ['temperatures', 'indoor', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.desired': '21.5'}
|
||||
path, body = desc.write_fn("21.5", {})
|
||||
assert path == ["temperatures", "indoor", "vs", "0"]
|
||||
assert body == {"x.com.samsung.da.desired": "21.5"}
|
||||
|
||||
|
||||
def test_zone_target_temperature_bounds_read_live():
|
||||
"""min/max/step come from the device's own resource fields rather than
|
||||
a hardcoded constant -- see the adding-device-support skill's 'never
|
||||
hard-code the one dump's values' section."""
|
||||
desc = _desc('zone_target_temperature')
|
||||
rep = {'x.com.samsung.da.minimum': '5.0', 'x.com.samsung.da.maximum': '25.0'}
|
||||
desc = _desc("zone_target_temperature")
|
||||
rep = {"x.com.samsung.da.minimum": "5.0", "x.com.samsung.da.maximum": "25.0"}
|
||||
assert desc.native_min_fn(rep) == 5.0
|
||||
assert desc.native_max_fn(rep) == 25.0
|
||||
assert desc.step_fn({'x.com.samsung.da.increment': '0.5'}) == 0.5
|
||||
assert desc.step_fn({"x.com.samsung.da.increment": "0.5"}) == 0.5
|
||||
# No live field: falls back to a sane default rather than raising.
|
||||
assert desc.native_min_fn({}) == 5.0
|
||||
assert desc.native_max_fn({}) == 30.0
|
||||
@@ -129,16 +139,16 @@ def test_zone_target_temperature_bounds_fall_back_together():
|
||||
"""One end without the other is not a usable range: pairing a real
|
||||
device minimum with an invented default maximum looks plausible and is
|
||||
silently wrong, so a half-reported range falls back whole."""
|
||||
desc = _desc('zone_target_temperature')
|
||||
half = {'x.com.samsung.da.minimum': '10.0'}
|
||||
desc = _desc("zone_target_temperature")
|
||||
half = {"x.com.samsung.da.minimum": "10.0"}
|
||||
assert desc.native_min_fn(half) == 5.0
|
||||
assert desc.native_max_fn(half) == 30.0
|
||||
|
||||
|
||||
def test_zone_target_temperature_zero_increment_is_not_collapsed():
|
||||
"""`or` would turn a genuine 0 into the 0.5 default (issue #160)."""
|
||||
desc = _desc('zone_target_temperature')
|
||||
assert desc.step_fn({'x.com.samsung.da.increment': '0'}) == 0.0
|
||||
desc = _desc("zone_target_temperature")
|
||||
assert desc.step_fn({"x.com.samsung.da.increment": "0"}) == 0.0
|
||||
|
||||
|
||||
def test_water_heater_entity_is_bound():
|
||||
@@ -148,7 +158,7 @@ def test_water_heater_entity_is_bound():
|
||||
bound, _ = _bound()
|
||||
water_heaters = [b for b in bound if isinstance(b.desc, WaterHeaterDesc)]
|
||||
assert len(water_heaters) == 1
|
||||
assert water_heaters[0].href == '/mode/dhw/vs/0'
|
||||
assert water_heaters[0].href == "/mode/dhw/vs/0"
|
||||
|
||||
|
||||
def test_water_heater_reads_first_mode():
|
||||
@@ -156,23 +166,31 @@ def test_water_heater_reads_first_mode():
|
||||
the entity's current_operation is derived from -- see climate.py's
|
||||
_first_mode for the identical pattern on the AC side."""
|
||||
state = _state()
|
||||
assert state['water_heater'] == 'Eco'
|
||||
assert state["water_heater"] == "Eco"
|
||||
|
||||
|
||||
def test_water_heater_write_targets():
|
||||
"""DHW.entities[0].write_fn maps each (kind, value) command to the right
|
||||
vendor POST target and body -- power, mode and temperature only, no fan/
|
||||
swing/preset (the AC's climate.py has those; the DHW loop doesn't)."""
|
||||
write = _desc('water_heater').write_fn
|
||||
assert write(('power', True), {}) == (
|
||||
['power', 'dhw', 'vs', '0'], {'x.com.samsung.da.power': 'On'})
|
||||
assert write(('power', False), {}) == (
|
||||
['power', 'dhw', 'vs', '0'], {'x.com.samsung.da.power': 'Off'})
|
||||
assert write(('mode', 'Force'), {}) == (
|
||||
['mode', 'dhw', 'vs', '0'], {'x.com.samsung.da.modes': ['Force']})
|
||||
assert write(('temperature', 45.0), {}) == (
|
||||
['temperatures', 'dhw', 'vs', '0'], {'x.com.samsung.da.desired': '45.0'})
|
||||
assert write(('bogus', 1), {}) is None
|
||||
write = _desc("water_heater").write_fn
|
||||
assert write(("power", True), {}) == (
|
||||
["power", "dhw", "vs", "0"],
|
||||
{"x.com.samsung.da.power": "On"},
|
||||
)
|
||||
assert write(("power", False), {}) == (
|
||||
["power", "dhw", "vs", "0"],
|
||||
{"x.com.samsung.da.power": "Off"},
|
||||
)
|
||||
assert write(("mode", "Force"), {}) == (
|
||||
["mode", "dhw", "vs", "0"],
|
||||
{"x.com.samsung.da.modes": ["Force"]},
|
||||
)
|
||||
assert write(("temperature", 45.0), {}) == (
|
||||
["temperatures", "dhw", "vs", "0"],
|
||||
{"x.com.samsung.da.desired": "45.0"},
|
||||
)
|
||||
assert write(("bogus", 1), {}) is None
|
||||
|
||||
|
||||
def test_dhw_power_and_temperature_declared_as_coverage():
|
||||
@@ -181,7 +199,7 @@ def test_dhw_power_and_temperature_declared_as_coverage():
|
||||
their own entities -- declared as no-entity coverage caps so discover()
|
||||
reports no gap, same pattern as the AC's CLIMATE_CONSUMED_HREFS."""
|
||||
reg, _ = _ehs()
|
||||
for href in ('/power/dhw/vs/0', '/temperatures/dhw/vs/0'):
|
||||
for href in ("/power/dhw/vs/0", "/temperatures/dhw/vs/0"):
|
||||
caps = reg.capabilities.get(href)
|
||||
assert caps, href
|
||||
assert all(c.entities == () for c in caps), href
|
||||
@@ -189,11 +207,11 @@ def test_dhw_power_and_temperature_declared_as_coverage():
|
||||
|
||||
def test_away_mode_reads_off():
|
||||
state = _state()
|
||||
assert state['away_mode'] is False
|
||||
assert state["away_mode"] is False
|
||||
|
||||
|
||||
def test_away_mode_write_contract():
|
||||
desc = _desc('away_mode')
|
||||
path, body = desc.write_fn('On', {})
|
||||
assert path == ['option', 'outgoing', 'vs', '0']
|
||||
assert body == {'x.com.samsung.da.away': 'On'}
|
||||
desc = _desc("away_mode")
|
||||
path, body = desc.write_fn("On", {})
|
||||
assert path == ["option", "outgoing", "vs", "0"]
|
||||
assert body == {"x.com.samsung.da.away": "On"}
|
||||
Reference in new issue
Block a user