AC: quantize subdevice temperature writes against their own step (Opus review)
async_send_command handed write_fn/validate_fn the raw cache snapshot (real, on-the-wire hrefs), not a subdevice-scoped view. Every other consumer of a full resources dict (exists_fn, rep_fn, is_legacy_board, ...) reads through coordinator.canonical_resources() specifically to avoid this; write_fn/validate_fn didn't, so on a composite AC (issue #177) _temperature_step's resources.get(HREF_TEMP_CONTROL) saw the master's /temperature/control/vs/0 instead of the subdevice's own /temperature/control/vs/1, silently rounding a subdevice's 0.5-degree write to a whole degree. The remote-control gate stays on the raw snapshot -- /remotectrl/* is a shared, MAIN-only resource a subdevice's owned-hrefs-only canonical view would drop entirely. climate.py's target_temperature_step duplicated this same read-in-order logic; pointed it at airconditioner._temperature_step so the read and write paths can't drift again. Also, from the same review: - _quantize_temperature rejects non-finite floats (nan/inf survive float() but raise out of round()/division, escaping write_fn's documented None-on-bad-payload contract). - Deduplicated the quantize-and-check block shared by the temperature_ocf/temperature branches of _climate_write. - Added the /temperatures/vs/0 items[]-fallback test that was previously unreachable (every increment-carrying fixture also has /temperature/control/vs/0, which _temperature_step checks first). - Added a coordinator-level test seeding an indexed subdevice with its own step, distinct from the master's, covering the fix above. - The Towels/Bedding regression test now checks all 7 locale catalogs, not just English -- the bug is a code-mapping error, and test_every_language_mirrors_the_english_catalog only checks key topology, not values.
This commit is contained in:
6 files changed
+94
-23
No files matched your search
@@ -181,6 +181,26 @@ def test_climate_write_preserves_half_degree_temperature_steps():
|
||||
)
|
||||
|
||||
|
||||
def test_temperature_step_falls_back_to_temps_vs_items_when_no_control_resource():
|
||||
"""Isolates the /temperatures/vs/0 fallback: every fixture that carries
|
||||
an increment there also carries /temperature/control/vs/0, which
|
||||
_temperature_step checks first -- so without dropping that resource,
|
||||
this fallback branch is never actually exercised, and reinstating the
|
||||
original bug (reading the increment off /temperatures/vs/0's top level
|
||||
instead of unwrapping its items[0]) would still pass every other test."""
|
||||
resources = dict(_load_device("airconditioner_cac"))
|
||||
del resources[airconditioner.HREF_TEMP_CONTROL]
|
||||
assert airconditioner._temperature_step(resources) == 0.5
|
||||
assert airconditioner._climate_write(("temperature", 24.5), {}, None, resources) == (
|
||||
["temperatures", "vs", "0"],
|
||||
{
|
||||
"x.com.samsung.da.items": [
|
||||
{"x.com.samsung.da.id": "0", "x.com.samsung.da.desired": "24.5"}
|
||||
]
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_climate_write_rounds_to_whole_degree_with_no_advertised_increment():
|
||||
"""ARTIK051 boards have neither /temperature/control/vs/0 nor an
|
||||
increment field on /temperatures/vs/0's item -- target_temperature_step
|
||||
|
||||
@@ -201,3 +201,33 @@ async def test_main_climate_write_unaffected_by_subdevice_translation(coordinato
|
||||
|
||||
posted_path, _ = coordinator._session.post_calls[0]
|
||||
assert posted_path == ["power", "vs", "0"]
|
||||
|
||||
|
||||
async def test_indexed_subdevice_climate_write_quantizes_with_its_own_temperature_step(
|
||||
coordinator,
|
||||
) -> None:
|
||||
"""A composite AC's write_fn (airconditioner._climate_write) reads the
|
||||
temperature step off write_fn's own `resources` argument -- if
|
||||
async_send_command handed it the raw cache snapshot (real, on-the-wire
|
||||
hrefs) instead of this subdevice's canonical_resources() view, the
|
||||
subdevice's own /temperature/control/vs/1 would be invisible under the
|
||||
canonical HREF_TEMP_CONTROL key, and the write would quantize against
|
||||
the master's step (or none at all) instead of its own."""
|
||||
sub1 = Subdevice(kind="indexed", key="1", seed_path=("device", "1"))
|
||||
coordinator._observe.apply(
|
||||
"/temperature/control/vs/0",
|
||||
{"x.com.samsung.da.increment": "1"},
|
||||
source="poll",
|
||||
)
|
||||
coordinator._observe.apply(
|
||||
"/temperature/control/vs/1",
|
||||
{"x.com.samsung.da.increment": "0.5"},
|
||||
source="poll",
|
||||
)
|
||||
bound = _climate_bound("/mode/vs/1", sub1)
|
||||
|
||||
await coordinator.async_send_command(bound, ("temperature_ocf", 24.5))
|
||||
|
||||
posted_path, posted_bytes = coordinator._session.post_calls[0]
|
||||
assert posted_path == ["temperature", "desired", "1"]
|
||||
assert cbor2.loads(posted_bytes) == {"temperature": 24.5}
|
||||
@@ -164,15 +164,23 @@ def test_confirmed_washer_table_02_towels_bedding_are_not_swapped():
|
||||
"""Issue #343: DA_WM_TP1_21_COMMON's Table_02 had 24/33 transposed --
|
||||
selecting 'Towels' in HA ran the washer's Bedding cycle and vice versa.
|
||||
24 must agree with the 69/6A-79/88 family's own Bedding/Towels pair
|
||||
(6f/70), not with each other."""
|
||||
states = _load("en")["entity"]["select"]["washer_cycle_table_02"]["state"]
|
||||
assert states["24"] == states["6f"] == "Bedding"
|
||||
assert states["33"] == states["70"] == "Towels"
|
||||
(6f/70), not with each other.
|
||||
|
||||
Checked in every locale catalog, not just English: the underlying bug
|
||||
is a device-code mapping error, not a wording issue, and
|
||||
test_every_language_mirrors_the_english_catalog only checks key
|
||||
topology -- a locale-specific 24/33 swap (the exact shape of #343)
|
||||
would still pass that test."""
|
||||
for language in _languages():
|
||||
states = _load(language)["entity"]["select"]["washer_cycle_table_02"]["state"]
|
||||
assert states["24"] == states["6f"], language
|
||||
assert states["33"] == states["70"], language
|
||||
|
||||
|
||||
def test_confirmed_washer_table_02_missing_course_names():
|
||||
"""Issue #342: 06/08/a0 had no translation and fell back to the raw
|
||||
device code in the UI."""
|
||||
device code in the UI; 74 was already translated by the time this
|
||||
landed and is pinned here only as a "didn't regress" anchor."""
|
||||
states = _load("en")["entity"]["select"]["washer_cycle_table_02"]["state"]
|
||||
assert {code: states[code] for code in ("06", "08", "74", "a0")} == {
|
||||
"06": "XXL Laundry",
|
||||
|
||||
Reference in new issue
Block a user