Skip to content

Commit b3875f9

Browse files
committed
test(diag): firmware<->HACS bit-contract test + QA Layer 3; bump integration submodule
Add firmware/test/test_diag_contract.py: it parses the HISENSE_FAULT1_*/HISENSE_FEAT1_* macros in hisense_rs485.h and the HACS integration's const.py bit lists and asserts they agree. They live in separate repos (the integration is a submodule), so nothing else stops them drifting. Wired as QA Layer 3 in run_tests.sh (skips cleanly if the submodule is absent). Point the hisense-unified-ac submodule at the diagnostics-entities commit (hisense-unified-ac PR #2). Firmware binary is unchanged from 1.3.19 (test/tooling + submodule pointer only), so this bypasses the local already-flashed-version gate; CI still checks version vs the PR base. Assisted-by: AI
1 parent 33d5712 commit b3875f9

3 files changed

Lines changed: 95 additions & 1 deletion

File tree

firmware/test/run_tests.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,9 @@ print("== ROUND-TRIP OK ==" if ok else "== ROUND-TRIP FAILED ==")
4848
sys.exit(0 if ok else 1)
4949
PY
5050

51+
echo
52+
echo "== Layer 3: diagnostics bitmap contract (firmware <-> HACS integration) =="
53+
python3 "$(dirname "$0")/test_diag_contract.py"
54+
5155
echo
5256
echo "ALL QA LAYERS PASSED"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
#!/usr/bin/env python3
2+
"""Contract test (docs/14): the HACS integration's Faults1/Features1 bit layout MUST match
3+
the firmware packers' HISENSE_FAULT1_* / HISENSE_FEAT1_* macros in hisense_rs485.h.
4+
5+
The two live in separate repos (the integration is a submodule), so nothing but this test
6+
stops them drifting. Exits non-zero on any mismatch; skips cleanly if the submodule is absent.
7+
"""
8+
9+
import pathlib
10+
import re
11+
import sys
12+
13+
ROOT = pathlib.Path(__file__).resolve().parents[2]
14+
HDR = ROOT / "firmware/src/rs485-driver/hisense_rs485.h"
15+
CONST = (
16+
ROOT
17+
/ "integrations/hisense-unified-ac/custom_components/hisense_unified_ac/const.py"
18+
)
19+
20+
21+
def firmware_macros() -> dict[str, int]:
22+
text = HDR.read_text()
23+
return {
24+
m: int(v)
25+
for m, v in re.findall(
26+
r"#define\s+(HISENSE_(?:FAULT1|FEAT1)_\w+)\s+(\d+)", text
27+
)
28+
}
29+
30+
31+
def const_namespace() -> dict:
32+
ns: dict = {}
33+
# const.py is pure data (no imports), so it execs standalone without Home Assistant.
34+
exec(compile(CONST.read_text(), str(CONST), "exec"), ns) # noqa: S102
35+
return ns
36+
37+
38+
def main() -> None:
39+
if not CONST.exists():
40+
print("[diag contract] SKIP: HACS submodule not checked out")
41+
return
42+
fw = firmware_macros()
43+
ns = const_namespace()
44+
errors: list[str] = []
45+
46+
faults = ns["FAULT1_BITS"]
47+
if len(faults) != 18:
48+
errors.append(f"FAULT1_BITS has {len(faults)} entries, want 18")
49+
for bit, key, _name in faults:
50+
macro = "HISENSE_FAULT1_" + key.upper()
51+
if fw.get(macro) != bit:
52+
errors.append(
53+
f"fault {key}: const bit {bit} != firmware {macro}={fw.get(macro)}"
54+
)
55+
56+
feats = ns["FEAT1_BITS"]
57+
for bit, key, _name, _ext in feats:
58+
macro = "HISENSE_FEAT1_" + key.upper()
59+
if fw.get(macro) != bit:
60+
errors.append(
61+
f"feat {key}: const bit {bit} != firmware {macro}={fw.get(macro)}"
62+
)
63+
64+
meta = [
65+
("FAULTS1_VALID_BIT", "HISENSE_FAULT1_VALID"),
66+
("FAULTS1_ANY_BIT", "HISENSE_FAULT1_ANY"),
67+
("FEATURES1_VALID_BIT", "HISENSE_FEAT1_VALID"),
68+
("FEATURES1_EXT_VALID_BIT", "HISENSE_FEAT1_EXT_VALID"),
69+
("FEATURES1_POWER_DISPLAY_SHIFT", "HISENSE_FEAT1_POWER_DISPLAY_SHIFT"),
70+
("FEATURES1_DEMAND_RESP_SHIFT", "HISENSE_FEAT1_DEMAND_RESP_SHIFT"),
71+
]
72+
for const_name, macro in meta:
73+
if ns[const_name] != fw.get(macro):
74+
errors.append(
75+
f"{const_name} ({ns[const_name]}) != {macro} ({fw.get(macro)})"
76+
)
77+
78+
if errors:
79+
print("[diag contract] FAIL")
80+
for e in errors:
81+
print(" -", e)
82+
sys.exit(1)
83+
print(
84+
f"[diag contract] OK: 18 fault bits + {len(feats)} feature flags + meta "
85+
"match hisense_rs485.h"
86+
)
87+
88+
89+
if __name__ == "__main__":
90+
main()

0 commit comments

Comments
 (0)