mirror of
https://github.com/NickoScope/nickol-knx-mcp.git
synced 2026-09-29 19:31:12 +02:00
- device_library: NICKOL_KNX_CATALOG env points at a local device-library YAML file/dir; decompose_device returns the exact vendor object model (source: catalog-exact) and falls back to generic recipes (source: recipe-approximate). Env unset = behaviour unchanged. - appprog_parser (new) + MCP tool parse_devices_from_project: deterministic extraction of exact comm-object models from M-* application programs in a .knxproj/.knxprod (order number via nested <Product>, DPST-x-y -> x.00y, per-channel block/stride detection, coverage manifest). Read-only, PII-safe: never reads the client P-*/0.xml. Now 25 MCP tools. - tests: test_device_catalog.py + test_appprog_parser.py (synthetic, self-contained) - docs: README/README.ru/docs site/announcements synced to v0.7.0, 25 tools Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
3.1 KiB
Python
79 lines
3.1 KiB
Python
"""decompose_device: generic-recipe fallback + opt-in local exact catalog.
|
|
|
|
Self-contained: builds a tiny synthetic catalog in a temp dir, so the test does
|
|
NOT depend on any locally-harvested (unshipped) catalog data.
|
|
"""
|
|
import sys, os, tempfile
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
import nickol_knx_mcp.device_library as dl
|
|
|
|
SAMPLE_CATALOG = """\
|
|
schema_version: "0.1"
|
|
manufacturer: "TestVendor"
|
|
devices:
|
|
- order_number: "TV-DIM4"
|
|
name: "TestDimmer 4CH"
|
|
category: "actuator/dimmer"
|
|
application_program: { name: "TestDimmer", version: "v3" }
|
|
channels: { dimmer_channels: 4 }
|
|
object_counts: { master_catalog_total: 92 }
|
|
repeating_blocks:
|
|
- unit: "dimmer_channel"
|
|
instances: 4
|
|
objects_per_instance: 5
|
|
stride: 5
|
|
objects:
|
|
- { number: 1, name: "[C1] On/Off", dpt: "1.001", role: cmd, size_bits: 1 }
|
|
- { number: 2, name: "[C1] On/Off (Status)", dpt: "1.001", role: status, size_bits: 1 }
|
|
- { number: 3, name: "[C1] Relative Dimming", dpt: "3.007", role: cmd, size_bits: 4 }
|
|
- { number: 4, name: "[C1] Absolute Dimming", dpt: "5.001", role: cmd, size_bits: 8 }
|
|
- { number: 5, name: "[C1] Pattern", role: cmd, size_bits: 8 } # no dpt -> stays None
|
|
"""
|
|
|
|
|
|
def _reset(catalog_path=None):
|
|
if catalog_path:
|
|
os.environ["NICKOL_KNX_CATALOG"] = catalog_path
|
|
else:
|
|
os.environ.pop("NICKOL_KNX_CATALOG", None)
|
|
dl._CATALOG_INDEX = None # drop the module cache between modes
|
|
|
|
|
|
# 1) No catalog configured -> generic recipe, behaviour unchanged
|
|
_reset()
|
|
r = dl.decompose_device("ZIO-MB24", 2)
|
|
assert r["matched"] and r["source"] == "recipe-approximate", r
|
|
assert r["total_ga"] == r["objects_per_unit"] * 2, r
|
|
|
|
r = dl.decompose_device("no-such-device")
|
|
assert r["matched"] is False and r["source"] == "recipe-approximate", r
|
|
|
|
# 2) Local catalog configured -> exact model, matched by order number AND by name
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with open(os.path.join(d, "vendor.yaml"), "w", encoding="utf-8") as fh:
|
|
fh.write(SAMPLE_CATALOG)
|
|
_reset(d)
|
|
|
|
for q in ("TV-DIM4", "tv-dim4", "TestDimmer 4CH", "testdimmer 4ch"):
|
|
r = dl.decompose_device(q)
|
|
assert r["source"] == "catalog-exact", (q, r["source"])
|
|
assert r["order_number"] == "TV-DIM4", (q, r)
|
|
assert r["application_program"]["version"] == "v3", r
|
|
assert r["total_master_objects"] == 92, r
|
|
blk = r["blocks"][0]
|
|
assert blk["unit"] == "dimmer_channel" and blk["instances"] == 4, blk
|
|
assert blk["objects_per_instance"] == 5, blk
|
|
assert blk["first_instance_objects"][0]["dpt"] == "1.001", blk
|
|
assert blk["first_instance_objects"][4]["dpt"] is None, "no DPT must stay None, never guessed"
|
|
|
|
# a device NOT in the catalog still falls back to the generic recipe
|
|
r = dl.decompose_device("dimmer")
|
|
assert r["source"] == "recipe-approximate" and r["matched"], r
|
|
|
|
# 3) env cleared -> back to recipe-only, no stale cache
|
|
_reset()
|
|
assert dl.decompose_device("TV-DIM4")["matched"] is False
|
|
|
|
print("OK — device catalog wiring: recipe fallback + catalog-exact + name match + honest unverified DPT")
|