from __future__ import annotations from typing import Any, Dict, List, Tuple TOP_KEYS = ["ui", "hmis", "plcs", "sensors", "actuators", "hils", "serial_networks", "ip_networks"] def validate_basic(cfg: dict[str, Any]) -> List[str]: errors: List[str] = [] if not isinstance(cfg, dict): return ["Top-level JSON is not an object"] for k in TOP_KEYS: if k not in cfg: errors.append(f"Missing top-level key: {k}") if errors: return errors if not isinstance(cfg["ui"], dict): errors.append("ui must be an object") for k in ["hmis", "plcs", "sensors", "actuators", "hils", "serial_networks", "ip_networks"]: if not isinstance(cfg[k], list): errors.append(f"{k} must be an array") if errors: return errors ui = cfg.get("ui", {}) if not isinstance(ui, dict): errors.append("ui must be an object") return errors uinet = ui.get("network") if not isinstance(uinet, dict): errors.append("ui.network must be an object") return errors for req in ["ip", "port", "docker_network"]: if req not in uinet: errors.append(f"ui.network missing key: {req}") names: List[str] = [] for section in ["hmis", "plcs", "sensors", "actuators", "hils"]: for dev in cfg.get(section, []): if isinstance(dev, dict) and isinstance(dev.get("name"), str): names.append(dev["name"]) dup = {n for n in set(names) if names.count(n) > 1} if dup: errors.append(f"Duplicate device names: {sorted(list(dup))}") seen: Dict[Tuple[str, str], str] = {} def check_net(dev: dict[str, Any]) -> None: net = dev.get("network") or {} dn = net.get("docker_network") ip = net.get("ip") name = dev.get("name", "") if not isinstance(dn, str) or not isinstance(ip, str): return key = (dn, ip) if key in seen: errors.append(f"Duplicate IP {ip} on docker_network {dn} (devices: {seen[key]} and {name})") else: seen[key] = str(name) for section in ["hmis", "plcs", "sensors", "actuators"]: for dev in cfg.get(section, []): if isinstance(dev, dict): check_net(dev) def uses_rtu(dev: dict[str, Any]) -> bool: for c in (dev.get("inbound_connections") or []): if isinstance(c, dict) and c.get("type") == "rtu": return True for c in (dev.get("outbound_connections") or []): if isinstance(c, dict) and c.get("type") == "rtu": return True return False any_rtu = False for section in ["hmis", "plcs", "sensors", "actuators"]: for dev in cfg.get(section, []): if isinstance(dev, dict) and uses_rtu(dev): any_rtu = True serial_nets = cfg.get("serial_networks", []) if any_rtu and len(serial_nets) == 0: errors.append("RTU used but serial_networks is empty") if (not any_rtu) and len(serial_nets) != 0: errors.append("serial_networks must be empty when RTU is not used") return errors