[build-system] requires = ["hatchling"] build-backend = "hatchling.build" [project] name = "iaiops" version = "0.28.0" description = "Governed, vendor-neutral OT data tap + intelligent troubleshooting for AI agents — 14 field protocols (OPC-UA / Modbus-TCP+RTU / S7comm / Mitsubishi MC / Omron FINS / MTConnect / MQTT-Sparkplug / EtherNet-IP / EtherCAT / SECS-GEM / PROFINET / BACnet / HART-IP / IO-Link) with a built-in governance harness (audit, budget, risk tiers, MOC), an AI downtime root-cause copilot, plus OEE, downtime, and asset-inventory analytics. Energy protocols (IEC-104 / DNP3 / IEC-61850) ship separately as iaiops-energy." readme = "README.md" license = "MIT" requires-python = ">=3.11" authors = [{ name = "wei", email = "zhouwei008@gmail.com" }] keywords = [ "opc-ua", "modbus", "s7comm", "siemens", "mitsubishi", "mtconnect", "mqtt", "sparkplug", "uns", "ethernet-ip", "rockwell", "allen-bradley", "ethercat", "pysoem", "soem", "secs-gem", "profinet", "bacnet", "hvac", "io-link", "iolink", "oee", "ot", "scada", "plc", "industrial", "ai-ops", "mcp", # Edge / ecosystem discoverability — iaiops is a governed, neutral *edge application* # (Margo alignment is roadmap; see docs/MARGO-ALIGNMENT.md — NOT compliant yet). "edge", "iiot", "edge-computing", "margo", "edge-interoperability", ] dependencies = [ "typer>=0.12,<1.0", "rich>=13.0,<16.0", "pyyaml>=6.0,<7.0", "python-dotenv>=1.0,<2.0", "cryptography>=42.0", "mcp[cli]>=1.10,<2.0", ] [project.optional-dependencies] # Per-protocol extras. Every protocol client library is imported LAZILY (inside the # connection session builders / connector ops), so the base package installs and # imports without any of these — a site installs only the 1-2 protocols it runs: # pip install iaiops[opcua] · pip install iaiops[opcua,modbus] # pyOpenSSL is asyncua's transitive dep, floored here as a RESOLVER GUARD, not # because this package uses it. asyncua asks for `pyOpenSSL` unbounded; a fresh # resolve prefers the newest `cryptography` (50.x), which no modern pyOpenSSL # accepts (26.3 caps it at <50), so the resolver backtracks pyOpenSSL all the way # to 22.0.0 — a 2022 release whose bounds are loose enough to "fit" and whose # bindings then fail against cryptography 50 at import # (`module 'lib' has no attribute 'GEN_EMAIL'`). The combination installs cleanly # and breaks at runtime on any OPC-UA cert path. The floor forces the resolver to # keep cryptography at 49 and pick a pyOpenSSL that actually works with it. opcua = ["asyncua>=2.0,<3", "pyopenssl>=25"] modbus = ["pymodbus>=3.5,<4", "pyserial>=3.5"] # pyserial for Modbus-RTU (serial) s7 = ["pyS7>=2.8,<3"] mc = ["pymcprotocol>=0.3,<1"] eip = ["pycomm3>=1.2,<2"] mtconnect = ["requests>=2.31,<3"] # IO-Link master JSON integration — pure HTTP, same lib as MTConnect (no new pins). iolink = ["iaiops[mtconnect]"] # BAS controller layer (vendor supervisory REST above BACnet — Metasys OpenBlue / # Niagara oBIX) — pure HTTP, same lib as MTConnect (no new HTTP dep). bas = ["iaiops[mtconnect]"] # Gateway MES/SCADA read layer (vendor SCADA/MES platform HTTP web API above its # OPC-UA server — READ-ONLY) — pure HTTP, same lib as MTConnect (no new HTTP dep). ignition = ["iaiops[mtconnect]"] sparkplug = ["paho-mqtt>=2.0,<3", "protobuf>=4.25,<8"] # SECS/GEM (semiconductor / display fab equipment ↔ host over HSMS). secsgem = ["secsgem>=0.3,<1"] # EtherCAT needs Linux + root/CAP_NET_RAW + a dedicated NIC + real slaves and has # NO software simulator (macOS unsupported) — hence optional like the rest. ethercat = ["pysoem>=1.1,<2"] # PROFINET-DCP (read-only discovery/identify) — layer-2 raw sockets (root/admin/ # CAP_NET_RAW on the NIC on the PROFINET subnet); discovery only, no RT cyclic. profinet = ["pnio-dcp>=1.1,<3"] # NOTE: the energy edition (IEC-104 / DNP3 / IEC-61850, substation/utility) moved to # its own package — `pip install iaiops-energy` (github.com/industrial-aiops/industrial-aiops-energy). # Building edition (read-only facility/HVAC): BACnet/IP via BAC0 (over bacpypes3). bacnet = ["BAC0>=2023.6,<2026"] # Process edition (read-only): HART-IP process instrumentation. hart-protocol does # the HART command codec (framing/parsing); the HART-IP UDP transport is in-tree # (待核实). Pure-Python, cross-platform. hart = ["hart-protocol>=2023.6,<2025"] # Omron FINS (CS/CJ/CP/NX-via-FINS): the client is IN-REPO and stdlib-only # (socket/struct), so the extra pins nothing — it exists so `pip install # iaiops[fins]` and the edition bundles stay uniform across protocols. fins = [] # 信创 / national time-series DB historian sinks (data egress to a domestic TSDB). tdengine = ["taospy>=2.7,<3"] # The native client above needs libtaos, a VENDOR DOWNLOAD that is not on PyPI — # which is why CI fetches a sha256-pinned tarball and why the sink is unusable on # a machine without it. These two transports need no native library at all and # talk to taosAdapter on 6041 instead; `taosrest` ships inside taospy. tdengine-rest = ["taospy>=2.7,<3"] tdengine-ws = ["taos-ws-py>=0.3,<1"] iotdb = ["apache-iotdb>=1.0,<3"] # Adapter belt — egress to a general-market TSDB / message bus / on-box LLM. All lightweight: # influxdb + ollama reuse the same `requests` pin as MTConnect (line protocol / HTTP, no heavy SDK). influxdb = ["requests>=2.31,<3"] # InfluxDB v1/v2 historian sink (line protocol over HTTP) nats = ["nats-py>=2.6,<3"] mqtt = ["iaiops[sparkplug]"] # MQTT/UNS telemetry publisher (paho + protobuf come from sparkplug) # NATS stream publisher (normalized points + RCA/alarm events) ollama = ["requests>=2.31,<3"] # on-box local LLM (air-gapped RCA narration; Ollama HTTP API) # Queryability layer: Parquet export from the local SQLite sink (CSV/SQLite are # stdlib-only; only `iaiops export parquet` / export_data(fmt="parquet") need this). export = ["pyarrow>=15,<22"] # Convenience: install every pip-installable connector (full installs + the test # matrix). The energy edition (IEC-104 / DNP3 / IEC-61850) ships separately as # `iaiops-energy`, so its extras are not part of this package's `all`. all = [ "asyncua>=2.0,<3", "pyopenssl>=25", # resolver guard — see the `opcua` extra above "pymodbus>=3.5,<4", "pyserial>=3.5", "pyS7>=2.8,<3", "pymcprotocol>=0.3,<1", "pycomm3>=1.2,<2", "requests>=2.31,<3", "paho-mqtt>=2.0,<3", "protobuf>=4.25,<8", "secsgem>=0.3,<1", "pysoem>=1.1,<2", "pnio-dcp>=1.1,<3", "BAC0>=2023.6,<2026", "hart-protocol>=2023.6,<2025", "nats-py>=2.6,<3", ] # Per-industry edition bundles — install only the protocols a vertical runs, and # matching the same-named IAIOPS_MCP profiles. (energy/building arrive with their # signature protocols: IEC-104/DNP3/61850, BACnet.) fab = ["iaiops[secsgem]", "iaiops[opcua]", "iaiops[s7]", "iaiops[modbus]"] factory = [ "iaiops[opcua]", "iaiops[modbus]", "iaiops[s7]", "iaiops[mc]", "iaiops[fins]", "iaiops[eip]", "iaiops[mtconnect]", "iaiops[sparkplug]", "iaiops[ethercat]", "iaiops[profinet]", "iaiops[iolink]", "iaiops[ignition]", ] process = ["iaiops[opcua]", "iaiops[modbus]", "iaiops[hart]"] # Energy edition (substation / utility) now ships separately: `pip install iaiops-energy`. # Building edition: facility / HVAC / 厂务 (matches IAIOPS_MCP=building). Includes # the BAS controller layer (vendor supervisory REST above BACnet). building = ["iaiops[bacnet]", "iaiops[modbus]", "iaiops[opcua]", "iaiops[iolink]", "iaiops[bas]"] # Phoenix Contact PLCnext vPLC (虚拟化 PLC) — rides the existing OPC-UA + Modbus # connectors (its built-in OPC-UA server + Modbus-TCP); no dedicated driver. plcnext = ["iaiops[opcua]", "iaiops[modbus]"] # Water treatment edition (水处理) — matches IAIOPS_MCP=water: Modbus field # devices + OPC-UA SCADA + HART-IP instrumentation. References the per-protocol # extras — no duplicated pins. water = ["iaiops[modbus]", "iaiops[opcua]", "iaiops[hart]"] # Renewables edition (光伏/风电): PV inverters + wind turbines over Modbus (SUN2000/Growatt # templates) + OPC-UA plant SCADA + MQTT-Sparkplug. Device-level monitoring + PdM (baseline/RCA); # grid/substation telecontrol (IEC-104/DNP3/61850) is the separate `iaiops-energy` package. renewables = ["iaiops[modbus]", "iaiops[opcua]", "iaiops[sparkplug]"] # Warehouse / intralogistics edition (仓储/物料搬运) — matches IAIOPS_MCP=warehouse: # conveyor/sorter drives over EtherNet/IP + PROFINET, VFD/energy meters over Modbus, # WMS/WCS gateways over OPC-UA, AMR/IoT telemetry over MQTT-Sparkplug. warehouse = [ "iaiops[eip]", "iaiops[profinet]", "iaiops[modbus]", "iaiops[opcua]", "iaiops[sparkplug]", ] # Clinical-facility edition (医疗设施) — matches IAIOPS_MCP=clinical: BACnet BMS + # Modbus gas-alarm panels / energy meters + OPC-UA plant SCADA. clinical = ["iaiops[bacnet]", "iaiops[modbus]", "iaiops[opcua]"] # Pharmaceutical-manufacturing edition (制药) — matches IAIOPS_MCP=pharma: BACnet # BMS/EMS (cleanroom cascade) + Modbus PW/WFI skids + HART transmitters (conductivity # / TOC) + OPC-UA DCS and bioreactors. No new protocol: pharma needed semantics. pharma = ["iaiops[bacnet]", "iaiops[modbus]", "iaiops[hart]", "iaiops[opcua]"] # 信创 / China entry: domestic TSDB historian sinks (compliance mapping is built-in). xinchuang = ["iaiops[tdengine]", "iaiops[iotdb]"] [dependency-groups] dev = [ "pytest>=8.0,<10.0", "ruff>=0.5,<1.0", ] [project.scripts] iaiops = "iaiops.cli:app" iaiops-mcp = "mcp_server.server:main" # Convenience named entry points — sugar over IAIOPS_MCP= iaiops-mcp. Each # targets a data-driven shim in mcp_server.entrypoints (kept in sync with the # PROTOCOL_MODULES / NAMED_PROFILES menu; see tests/test_entrypoints.py). # Brain-only server (B3): cross-protocol brain, zero protocols — pair with # IAIOPS_MCP_NO_BRAIN=1 protocol servers at multi-process sites. iaiops-mcp-brain = "mcp_server.entrypoints:main_brain" # Per-protocol: iaiops-mcp-opcua = "mcp_server.entrypoints:main_opcua" iaiops-mcp-modbus = "mcp_server.entrypoints:main_modbus" iaiops-mcp-s7 = "mcp_server.entrypoints:main_s7" iaiops-mcp-mc = "mcp_server.entrypoints:main_mc" iaiops-mcp-fins = "mcp_server.entrypoints:main_fins" iaiops-mcp-eip = "mcp_server.entrypoints:main_eip" iaiops-mcp-mtconnect = "mcp_server.entrypoints:main_mtconnect" iaiops-mcp-sparkplug = "mcp_server.entrypoints:main_sparkplug" iaiops-mcp-ethercat = "mcp_server.entrypoints:main_ethercat" iaiops-mcp-secsgem = "mcp_server.entrypoints:main_secsgem" iaiops-mcp-profinet = "mcp_server.entrypoints:main_profinet" iaiops-mcp-bacnet = "mcp_server.entrypoints:main_bacnet" iaiops-mcp-hart = "mcp_server.entrypoints:main_hart" iaiops-mcp-iolink = "mcp_server.entrypoints:main_iolink" # Per-edition named profiles: iaiops-mcp-fab = "mcp_server.entrypoints:main_fab" iaiops-mcp-factory = "mcp_server.entrypoints:main_factory" iaiops-mcp-process = "mcp_server.entrypoints:main_process" iaiops-mcp-building = "mcp_server.entrypoints:main_building" iaiops-mcp-plcnext = "mcp_server.entrypoints:main_plcnext" iaiops-mcp-water = "mcp_server.entrypoints:main_water" iaiops-mcp-renewables = "mcp_server.entrypoints:main_renewables" iaiops-mcp-warehouse = "mcp_server.entrypoints:main_warehouse" iaiops-mcp-clinical = "mcp_server.entrypoints:main_clinical" iaiops-mcp-pharma = "mcp_server.entrypoints:main_pharma" [tool.hatch.build.targets.wheel] packages = ["iaiops", "mcp_server"] [tool.ruff] line-length = 100 target-version = "py311" # Vendored, machine-generated Eclipse Tahu Sparkplug B protobuf module — kept # byte-for-byte as generated by protoc; never hand-edited, so exclude from lint. extend-exclude = ["iaiops/connectors/sparkplug/sparkplug_b_pb2.py"] [tool.ruff.lint] select = ["E", "F", "I", "N", "W", "UP"] [tool.ruff.lint.per-file-ignores] # Vendored governance harness — kept faithful to its source; the exception # names (PolicyDenied/BudgetExceeded) and long lines are intentional. "iaiops/core/governance/*.py" = ["E501", "N818"] # Discovery raises named *conditions*, not generic errors — SegmentUnhealthy / # HostBackedOff / UnknownWireKind read as what happened, the way the governance # harness's PolicyDenied does. Line length is still enforced here. "iaiops/core/discovery/*.py" = ["N818"] # Same rule for the scan store: ScanNotFound names the condition, and # ScanNotFoundError would read as an error about not-found-ness. "iaiops/core/sink/scan_store.py" = ["N818"] # MCP server files are reflected by FastMCP/Pydantic; PEP 604 unions (X | None) # crash on older mcp/pydantic, so they intentionally use Optional[X]. "mcp_server/**/*.py" = ["UP007", "UP045"] # The MCP entry keeps an explicit Python-version guard. "iaiops/cli/_root.py" = ["UP036"] [tool.pytest.ini_options] testpaths = ["tests"] markers = [ "unit: Unit tests", "integration: Integration tests (require a reachable OPC-UA server / Modbus device)", # Consulted by the IAIOPS_REQUIRE_LIVE gate in tests/conftest.py: a test that may # still skip on a live-promised CI step, because the workflow itself declares its # scaffolding non-fatal (today only the vendor-CDN libtaos client). "optional_live: may skip even under IAIOPS_REQUIRE_LIVE=1", ]