mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
feat(param_check): significance layer — a focus list of config-value outliers
Rank findings by significance: config_value (setpoint/hysteresis/time/threshold — an odd device is usually a real mistake) vs config_flag (mode/type) vs label (per-room text, often intentional). New focus list surfaces the config-value clear outliers first. On a real 275-device project this turns 422 raw outliers into a 9-item focus that includes the thermostats whose init-setpoint differs from their siblings. Multilingual name hints (EN/DE/RU). Test updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
81799169c5
commit
1e924ede6f
+6
-2
@@ -23,8 +23,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
flags the odd one out: `clear_outliers` (a strong majority with a small minority — e.g. one
|
||||
thermostat with a different setpoint/hysteresis, one presence detector with a different
|
||||
detection time) and `split_configs` (balanced 2+ variants — review, often two zones).
|
||||
Parameter names resolved from the app-program. Read-only; no ETS/bus; encrypted projects are
|
||||
skipped honestly. Validated on real 42–275-device projects; synthetic `tests/test_param_check.py`.
|
||||
Parameter names resolved from the app-program. Findings are ranked by **significance**: a
|
||||
`focus` list of *config-value* outliers (setpoint/hysteresis/time/threshold — where an odd device
|
||||
is usually a real mistake), separated from mode/type flags and per-room text labels — on a real
|
||||
275-device project this turns 422 raw outliers into a 9-item focus (incl. the thermostats whose
|
||||
init-setpoint differs from their siblings). Read-only; no ETS/bus; encrypted projects are skipped
|
||||
honestly. Validated on real 42–275-device projects; synthetic `tests/test_param_check.py`.
|
||||
Community-driven (asked for in Discussions). Tool count 25 → 26.
|
||||
|
||||
- **`skills/ha-git-backup`** — an ops-companion skill for the engineer package: a two-circuit
|
||||
|
||||
@@ -28,6 +28,25 @@ from typing import Any, Optional
|
||||
|
||||
_NUMERIC_RE = re.compile(r"^-?\d+$")
|
||||
|
||||
# Parameter-name hints for a meaningful numeric CONFIG VALUE — the kind of setting
|
||||
# where an odd value is usually a real mistake (a different hysteresis / setpoint /
|
||||
# timeout on one device). Multilingual (EN / DE / RU). Everything numeric but not a
|
||||
# value is a "config_flag" (mode/type/enable); non-numeric is a "label" (often a
|
||||
# deliberate per-room name, lowest priority).
|
||||
_VALUE_HINTS = (
|
||||
"setpoint", "hyster", "temperatur", "threshold", "delay", "duration", "timeout",
|
||||
"time", "period", "level", "speed", "offset", "value", "brightness", "scaling",
|
||||
"уставк", "гистерез", "температур", "порог", "задержк", "время", "период",
|
||||
"длительн", "уровень", "скорост", "яркост", "значени",
|
||||
)
|
||||
|
||||
|
||||
def _significance(name: str, numeric: bool) -> str:
|
||||
if not numeric:
|
||||
return "label"
|
||||
low = (name or "").lower()
|
||||
return "config_value" if any(h in low for h in _VALUE_HINTS) else "config_flag"
|
||||
|
||||
|
||||
def _localname(tag: str) -> str:
|
||||
return tag.rsplit("}", 1)[-1]
|
||||
@@ -150,16 +169,26 @@ def check_device_parameters(path: str, password: Optional[str] = None,
|
||||
|
||||
names = _resolve_names(zf, refids_needed)
|
||||
|
||||
_SIG_RANK = {"config_value": 0, "config_flag": 1, "label": 2}
|
||||
|
||||
def _decorate(rec: dict[str, Any]) -> dict[str, Any]:
|
||||
rec["parameter"] = names.get(rec["refid"], rec["refid"])
|
||||
rec["name_resolved"] = rec["refid"] in names
|
||||
rec["significance"] = _significance(rec["parameter"], rec["numeric"])
|
||||
return rec
|
||||
|
||||
# numeric config outliers first (time/setpoint/hysteresis-like), then the rest
|
||||
# most meaningful first: config_value (setpoint/hysteresis/time…) > flag > label
|
||||
clear = [_decorate(r) for r in clear]
|
||||
splits = [_decorate(r) for r in splits]
|
||||
clear.sort(key=lambda r: (not r["numeric"], len(r["odd_devices"]), -r["total"]))
|
||||
splits.sort(key=lambda r: (not r["numeric"], -r["total"]))
|
||||
clear.sort(key=lambda r: (_SIG_RANK[r["significance"]], len(r["odd_devices"]), -r["total"]))
|
||||
splits.sort(key=lambda r: (_SIG_RANK[r["significance"]], -r["total"]))
|
||||
|
||||
def _by_sig(items: list[dict[str, Any]]) -> dict[str, int]:
|
||||
c = Counter(r["significance"] for r in items)
|
||||
return {k: c.get(k, 0) for k in ("config_value", "config_flag", "label")}
|
||||
|
||||
# the short list an integrator should actually look at first
|
||||
focus = [r for r in clear if r["significance"] == "config_value"]
|
||||
|
||||
grp_sizes = sorted((len(v) for v in groups.values() if len(v) >= min_group), reverse=True)
|
||||
return {
|
||||
@@ -168,13 +197,18 @@ def check_device_parameters(path: str, password: Optional[str] = None,
|
||||
"largest_groups": grp_sizes[:8],
|
||||
"clear_outliers_count": len(clear),
|
||||
"split_configs_count": len(splits),
|
||||
"focus_count": len(focus),
|
||||
"focus": focus[:max_findings],
|
||||
"clear_outliers_by_significance": _by_sig(clear),
|
||||
"split_configs_by_significance": _by_sig(splits),
|
||||
"clear_outliers": clear[:max_findings],
|
||||
"split_configs": splits[:max_findings],
|
||||
"names_unresolved": sum(1 for r in (clear + splits) if not r["name_resolved"]),
|
||||
"note": "clear_outliers = a device whose value differs from its N identical "
|
||||
"siblings (likely a mistake). split_configs = the group splits into "
|
||||
"balanced variants (review — often two zones/roles, not an error). "
|
||||
"Numeric config parameters (times/setpoints/hysteresis) are listed first. "
|
||||
"Read-only; parameter values come from P-*/0.xml (xknxproject does not "
|
||||
"expose them). Some module-definition parameter names may stay as RefIds.",
|
||||
"note": "START WITH `focus` — clear outliers on a config VALUE parameter "
|
||||
"(setpoint/hysteresis/time/threshold…), where an odd device is usually a "
|
||||
"real mistake. `clear_outliers` also includes config_flag (mode/type) and "
|
||||
"label (per-room text, often intentional) — see *_by_significance. "
|
||||
"split_configs = the group splits into balanced variants (review — often two "
|
||||
"zones, not an error). Read-only; values come from P-*/0.xml (xknxproject does "
|
||||
"not expose them). Some module-definition parameter names may stay as RefIds.",
|
||||
}
|
||||
|
||||
@@ -75,6 +75,10 @@ def main():
|
||||
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"])
|
||||
|
||||
Reference in New Issue
Block a user