#!/usr/bin/env python3 """ Load hc900_map_master from the per-controller register maps (register-map-c{n}.json). hc900_map_master is the DB table the web UI "태그 관리" (Tag Management) tab reads and the realtime service uses to know which tags to poll (it sends ``hc900_tag`` to the gateway's ReadTags). So ``hc900_tag`` must match the gateway's register-map tag — i.e. the Experion point name (e.g. FICQ-6101.PV). We set ``tagname == hc900_tag`` so the realtime_table (keyed by tagname) lines up with both. Usage: python3 load_map_master.py --controller C3 --map docs/register-map-c3.json python3 load_map_master.py --from-config config/gateway-config.json # all enabled """ import re import json import argparse from pathlib import Path import psycopg2 DSN = dict(host="localhost", port=5432, dbname="iiot_platform", user="postgres", password="postgres") # Experion point-parameter suffix → param_type used by the UI for grouping/colours. _PARAM_ALIAS = {"MD": "MODE"} def classify(entry: dict) -> tuple[str, int | None]: """Return (param_type, loop_no) for a register entry.""" tag = entry["tag"] desc = entry.get("description", "") if desc.startswith("LOOP"): m = re.search(r"#(\d+)", desc) loop = int(m.group(1)) if m else None suffix = tag.rsplit(".", 1)[1] if "." in tag else "LOOP" return _PARAM_ALIAS.get(suffix, suffix), loop if "Signal Tag" in desc: return "SIG", None if "Variable" in desc: return "VAR", None if "Custom" in desc: return "RAW", None suffix = tag.rsplit(".", 1)[1] if "." in tag else "OTHER" return _PARAM_ALIAS.get(suffix, suffix), None def load_controller(cur, controller: str, map_path: Path, active: bool) -> int: data = json.loads(map_path.read_text(encoding="utf-8")) cur.execute("DELETE FROM hc900_map_master WHERE controller_id = %s", (controller,)) n = 0 for e in data["registers"]: param_type, loop_no = classify(e) access = "R/W" if e.get("access") == "RW" else "R" # Single consistent name everywhere: tagname == hc900_tag == the register tag # (Experion ItemName, e.g. FICQ-6101.PV) — used by the gateway, map_master and # realtime_table alike. No case conversion. cur.execute( """INSERT INTO hc900_map_master (tagname, hc900_tag, modbus_addr, data_type, access, is_active, loop_no, param_type, controller_id) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)""", (e["tag"], e["tag"], e["addr"], e["type"], access, active, loop_no, param_type, controller)) n += 1 return n def main(): p = argparse.ArgumentParser(description="Load hc900_map_master from register maps") p.add_argument("--controller", help="controller id, e.g. C3") p.add_argument("--map", help="register-map json for --controller") p.add_argument("--from-config", help="gateway-config.json: load every enabled controller") p.add_argument("--inactive", action="store_true", help="insert rows as is_active=false (default: active)") args = p.parse_args() jobs: list[tuple[str, Path]] = [] if args.from_config: cfg = json.loads(Path(args.from_config).read_text(encoding="utf-8")) for c in cfg.get("controllers", []): if c.get("enabled", True): jobs.append((c["id"], Path(c["registerMapPath"]))) elif args.controller and args.map: jobs.append((args.controller, Path(args.map))) else: p.error("provide --from-config OR (--controller and --map)") conn = psycopg2.connect(**DSN) cur = conn.cursor() cur.execute("SET search_path TO hc900") total = 0 for ctrl, mp in jobs: if not mp.exists(): print(f" ⚠ {ctrl}: map not found: {mp} — skipping") continue n = load_controller(cur, ctrl, mp, not args.inactive) total += n print(f" {ctrl}: {n} rows ← {mp.name}") conn.commit() conn.close() print(f"✓ hc900_map_master loaded: {total} rows total") if __name__ == "__main__": main()