"""Synthetic test for cross-device parameter outlier detection (check_device_parameters).
Builds an in-memory .knxproj (a ZIP with a P-*/0.xml project part and a tiny
application program) containing 5 identical devices where one has an odd value,
one parameter that is balanced-split, and one that is uniform — then asserts the
tool flags the outlier, resolves its name, classifies the split, and stays quiet
on the uniform one.
"""
import io
import zipfile
from nickol_knx_mcp.param_check import check_device_parameters
HP = "M-TEST_H-X-1_HP-1111"
def _device(addr, hyst, mode, split):
pirs = "".join([
f'',
f'',
f'',
])
return (f''
f'{pirs}')
def _make_knxproj() -> str:
# 5 identical devices: hysteresis 5,5,5,5,10 (dev .10 is the outlier);
# mode uniform (2); split 1,1,1,2,2 (balanced 3/2)
devs = [
_device(1, 5, 2, 1), _device(2, 5, 2, 1), _device(3, 5, 2, 1),
_device(4, 5, 2, 2), _device(10, 10, 2, 2),
]
proj0 = (''
''
''
+ "".join(devs) +
'')
app = (''
''
''
''
''
''
''
''
''
''
'')
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z:
z.writestr("P-TEST/0.xml", proj0)
z.writestr("M-TEST/M-TEST_A-1111.xml", app)
path = "/tmp/_paramcheck_test.knxproj"
with open(path, "wb") as f:
f.write(buf.getvalue())
return path
def main():
path = _make_knxproj()
r = check_device_parameters(path, min_group=3)
assert "error" not in r, r
assert r["devices"] == 5, r["devices"]
assert r["identical_device_groups"] == 1, r
# clear outlier: hysteresis 4x5 vs 1x10, resolved name, odd device addr 10
hits = [o for o in r["clear_outliers"] if o["refid"].endswith("R-1")]
assert hits, f"hysteresis outlier not found: {r['clear_outliers']}"
h = hits[0]
assert h["parameter"] == "Hysteresis (K)", h["parameter"]
assert h["name_resolved"] is True
assert h["numeric"] is True
assert h["majority_value"] == "5"
assert [d["value"] for d in h["odd_devices"]] == ["10"], h["odd_devices"]
assert h["odd_devices"][0]["address"] == "10"
# significance: "Hysteresis (K)" is a config VALUE -> shows up in focus
assert h["significance"] == "config_value", h["significance"]
assert r["focus_count"] >= 1
assert any(o["refid"].endswith("R-1") for o in r["focus"]), r["focus"]
# uniform param (mode, all "2") must NOT appear anywhere
assert not any(o["refid"].endswith("R-2") for o in r["clear_outliers"] + r["split_configs"])
# balanced split (1,1,1,2,2) -> split_configs, name resolved
splits = [o for o in r["split_configs"] if o["refid"].endswith("R-3")]
assert splits, f"split not found: {r['split_configs']}"
assert splits[0]["parameter"] == "Zone role"
variants = {v["value"]: v["count"] for v in splits[0]["variants"]}
assert variants == {"1": 3, "2": 2}, variants
print("test_param_check: OK — outlier flagged (Hysteresis 4x5 vs 1x10 @ addr 10), "
"uniform param quiet, balanced split classified, names resolved.")
if __name__ == "__main__":
main()