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.
107 lines
4.8 KiB
Python
107 lines
4.8 KiB
Python
"""Tests that a subdevice's LocalThingsClimate entity (issue #177) reads its
|
|
*own* power/mode/temperature -- not the master's, and not some mix of the
|
|
two -- and that the legacy-board test (is_legacy_board/_legacy_airflow) is
|
|
evaluated per subdevice rather than once globally.
|
|
|
|
Uses the issue #177 reporter's ARTIK051_DONGLE_FAC_18K fixture deliberately:
|
|
it's a legacy `/airflow/vs/<n>` board (no `/wind/*` at all) on *both* the
|
|
master and its materialized sibling, which is exactly the shape climate.py's
|
|
own comments warn is easy to get wrong if the canonical view leaks between
|
|
subdevices.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from homeassistant.components.climate import HVACMode
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from custom_components.localthings.climate import LocalThingsClimate
|
|
from custom_components.localthings.registry.entities import ClimateDesc
|
|
from tests.test_subdevice_discovery import _coordinator, _discover
|
|
|
|
|
|
def _climate_entities(coordinator):
|
|
"""{subdevice_key_or_None: LocalThingsClimate}, None standing for MAIN."""
|
|
from custom_components.localthings.registry.subdevices import MAIN
|
|
|
|
out = {}
|
|
for b in coordinator.bound:
|
|
if isinstance(b.desc, ClimateDesc):
|
|
key = None if b.subdevice == MAIN else b.subdevice.key
|
|
out[key] = LocalThingsClimate(coordinator, b)
|
|
return out
|
|
|
|
|
|
@pytest.fixture
|
|
async def climates(hass: HomeAssistant):
|
|
coordinator = _coordinator(hass)
|
|
await _discover(coordinator, "airconditioner_artik051_dongle_fac_18k")
|
|
return _climate_entities(coordinator)
|
|
|
|
|
|
async def test_subdevice_climate_reads_its_own_mode_and_power(climates):
|
|
"""Master reports 'Auto'; the bedroom subdevice (/device/1) reports 'Cool' --
|
|
confirmed distinct in the real captured fixture. If the subdevice
|
|
entity's _rep() weren't translating through its own subdevice, it would
|
|
read the master's /mode/vs/0 instead and report the master's mode."""
|
|
main, sub1 = climates[None], climates["1"]
|
|
assert main.hvac_mode == HVACMode.AUTO
|
|
assert sub1.hvac_mode == HVACMode.COOL
|
|
|
|
|
|
async def test_subdevice_climate_reads_its_own_temperature(climates):
|
|
"""Master: current 25.0 / desired 26.0. Subdevice 1: current 27.0 / desired
|
|
28.0 -- distinct values in the real fixture, so a href mix-up here
|
|
would show up as a wrong number, not just a wrong mode string."""
|
|
main, sub1 = climates[None], climates["1"]
|
|
assert main.current_temperature == 25.0
|
|
assert main.target_temperature == 26.0
|
|
assert sub1.current_temperature == 27.0
|
|
assert sub1.target_temperature == 28.0
|
|
|
|
|
|
async def test_subdevice_climate_reads_its_own_power_state(climates):
|
|
"""Both subdevices happen to report power On in this fixture -- this at
|
|
least confirms _is_on() reads the *subdevice's own* /power/vs/<n>, not a
|
|
hardcoded /power/vs/0, by checking the entity resolves without falling
|
|
back to OFF (which _is_on() would do if it silently read an absent
|
|
href instead of the subdevice's actual one)."""
|
|
main, sub1 = climates[None], climates["1"]
|
|
assert main.hvac_mode != HVACMode.OFF
|
|
assert sub1.hvac_mode != HVACMode.OFF
|
|
|
|
|
|
async def test_legacy_board_test_is_evaluated_per_subdevice(climates):
|
|
"""The reporter's board has no /wind/* resources at all on *either*
|
|
subdevice -- is_legacy_board(self._resources) must independently
|
|
evaluate True for the master's own canonical view and for the
|
|
subdevice's own canonical view. If is_legacy_board were fed the raw,
|
|
unpartitioned snapshot (or if canonical_view leaked one subdevice's
|
|
hrefs into the other's), this wouldn't distinguish "this subdevice is
|
|
legacy" from "some subdevice on this connection is legacy" -- and a
|
|
future board with one legacy + one modern subdevice sharing a
|
|
connection would silently read the wrong fan/swing channel on one
|
|
side."""
|
|
main, sub1 = climates[None], climates["1"]
|
|
assert main._legacy_airflow() != {}
|
|
assert sub1._legacy_airflow() != {}
|
|
# Both resolve to *some* fan mode via the legacy path rather than the
|
|
# /wind/strength/vs/0 channel (absent on this board) -- confirms
|
|
# is_legacy_board(self._resources) actually gated fan_mode's branch,
|
|
# not just that _legacy_airflow() itself returned something.
|
|
assert main.fan_mode is not None
|
|
assert sub1.fan_mode is not None
|
|
|
|
|
|
async def test_subdevice_climate_writes_are_scoped_to_its_own_bound_entity(climates):
|
|
"""A sanity check that the two entities are backed by genuinely
|
|
different BoundEntity objects (different hrefs), which is what makes
|
|
per-subdevice reads/writes possible at all -- see async_send_command's
|
|
translation test in test_coordinator_send_command.py for the write
|
|
side of this."""
|
|
main, sub1 = climates[None], climates["1"]
|
|
assert main._bound.href == "/mode/vs/0"
|
|
assert sub1._bound.href == "/mode/vs/1"
|
|
assert main._bound is not sub1._bound
|