There are two ways to bring OPC UA data in, depending on where the server is.
| Cloud connector | On site, push | |
|---|---|---|
| Server | Reachable from the internet (cloud-hosted, or published through a firewall rule or reverse proxy) | Inside the plant network, no inbound access |
| How | InduQT connects and samples the nodes you list | Node-RED or a Python script on a PC in the plant reads the server and posts to the ingest API |
| Plan | Plant Monitor or Enterprise | Every plan |
| Firewall | Inbound to the OPC UA port | Outbound HTTPS (443) only |
Most plants should use the on-site option: nothing in the plant is exposed to the internet.
Cloud connector
- Create the site and machines
Create them in the app or with
POST /v1/sitesandPOST /v1/machines. Set each machine'sexternalIdto the name your source uses (for exampleB-101), so messages can address it without knowing InduQT ids. - Find the node ids
Browse the server with UaExpert or a similar client and note the node id of each tag, e.g.
ns=2;s=Boiler1.SteamPressure. - Add the connector
In the app: open the site and choose OPC UA server. Or with the API:
POST /v1/connectorscurl -X POST "https://api.induqt.ai/v1/connectors" \ -H "Authorization: Bearer $INDUQT_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "siteId": "6f1c2c0e-4b1a-4c55-9d0e-1f2a3b4c5d6e", "type": "opcua", "name": "Boiler PLC", "config": { "endpoint": "opc.tcp://opcua.example.com:4840", "intervalMs": 5000, "nodes": [ { "nodeId": "ns=2;s=Boiler1.SteamPressure", "machine": "B-101", "signal": "steam_pressure" } ] } }'
| Name | Description |
|---|---|
endpointrequired | opc.tcp://host:port/path, reachable from the internet. For servers inside the plant network, use Node-RED or the ingest API from a machine on site. |
securityMode | None, Sign or SignAndEncrypt. Default None. |
username / password | Optional user identity; encrypted at rest. |
intervalMs | Sampling interval, 1,000–60,000 ms. Default 5,000. |
nodesrequired | List of { nodeId, machine, signal }: e.g. { "nodeId": "ns=2;s=Boiler1.SteamPressure", "machine": "B-101", "signal": "steam_pressure" }. |
Mapping. Each node names its machine (id or externalId) and signal key.
On site with Node-RED
- Install the OPC UA nodes
In Node-RED, Manage palette → Install
node-red-contrib-opcua, ornpm install node-red-contrib-opcuain the Node-RED user directory. - Create an API key
In Settings → API keys, create a key with the
ingestscope, limited to the site the data belongs to. See site-scoped keys. - Set the key in Node-RED's environment
For example
docker run -e INDUQT_API_KEY=iq_live_… nodered/node-red, orEnvironment=INDUQT_API_KEY=…in the systemd unit. - Import the flow
Menu → Import, paste the flow below, change the endpoint, node ids and machine, then Deploy. The debug node shows each response.
[
{
"id": "iq_tab_opcua",
"type": "tab",
"label": "OPC UA → InduQT",
"disabled": false,
"info": "Reads boiler tags from the plant OPC UA server every 5 seconds and posts them to the InduQT ingest API. Needs node-red-contrib-opcua and INDUQT_API_KEY in the environment."
},
{
"id": "iq_opc_inject",
"type": "inject",
"z": "iq_tab_opcua",
"name": "Every 5 s",
"props": [
{
"p": "payload"
}
],
"repeat": "5",
"crontab": "",
"once": true,
"onceDelay": "2",
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 140,
"wires": [
[
"iq_opc_item1",
"iq_opc_item2",
"iq_opc_item3"
]
]
},
{
"id": "iq_opc_item1",
"type": "OpcUa-Item",
"z": "iq_tab_opcua",
"item": "ns=2;s=Boiler1.SteamPressure",
"datatype": "Double",
"value": "",
"name": "ns=2;s=Boiler1.SteamPressure",
"x": 350,
"y": 80,
"wires": [
[
"iq_opc_client"
]
]
},
{
"id": "iq_opc_item2",
"type": "OpcUa-Item",
"z": "iq_tab_opcua",
"item": "ns=2;s=Boiler1.SteamFlow",
"datatype": "Double",
"value": "",
"name": "ns=2;s=Boiler1.SteamFlow",
"x": 350,
"y": 140,
"wires": [
[
"iq_opc_client"
]
]
},
{
"id": "iq_opc_item3",
"type": "OpcUa-Item",
"z": "iq_tab_opcua",
"item": "ns=2;s=Boiler1.FlueO2",
"datatype": "Double",
"value": "",
"name": "ns=2;s=Boiler1.FlueO2",
"x": 350,
"y": 200,
"wires": [
[
"iq_opc_client"
]
]
},
{
"id": "iq_opc_client",
"type": "OpcUa-Client",
"z": "iq_tab_opcua",
"endpoint": "iq_opc_endpoint",
"action": "read",
"deadbandtype": "a",
"deadbandvalue": 1,
"time": 10,
"timeUnit": "s",
"certificate": "n",
"localfile": "",
"localkeyfile": "",
"securitymode": "None",
"securitypolicy": "None",
"useTransport": false,
"maxChunkCount": 1,
"maxMessageSize": 8192,
"receiveBufferSize": 8192,
"sendBufferSize": 8192,
"name": "Read tags",
"x": 590,
"y": 140,
"wires": [
[
"iq_opc_map"
],
[]
]
},
{
"id": "iq_opc_map",
"type": "function",
"z": "iq_tab_opcua",
"name": "Map to InduQT",
"func": "// Map each OPC UA node to an InduQT signal key.\nconst signals = {\n 'ns=2;s=Boiler1.SteamPressure': 'steam_pressure',\n 'ns=2;s=Boiler1.SteamFlow': 'steam_flow',\n 'ns=2;s=Boiler1.FlueO2': 'flue_o2'\n};\nconst key = signals[msg.topic];\nif (!key) return null;\n\n// Depending on the node version, payload is the value or a DataValue.\nlet v = msg.payload;\nif (v && typeof v === 'object' && 'value' in v) v = v.value && typeof v.value === 'object' && 'value' in v.value ? v.value.value : v.value;\nif (typeof v === 'boolean') v = v ? 1 : 0;\nif (typeof v !== 'number' || !isFinite(v)) return null;\n\nmsg.headers = {\n 'Authorization': 'Bearer ' + env.get('INDUQT_API_KEY'),\n 'Content-Type': 'application/json'\n};\nmsg.payload = { machine: 'B-101', ts: new Date().toISOString(), values: { [key]: v } };\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 790,
"y": 140,
"wires": [
[
"iq_opc_post"
]
]
},
{
"id": "iq_opc_post",
"type": "http request",
"z": "iq_tab_opcua",
"name": "POST /v1/ingest",
"method": "POST",
"ret": "obj",
"paytoqs": "ignore",
"url": "https://api.induqt.ai/v1/ingest",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 990,
"y": 140,
"wires": [
[
"iq_opc_debug"
]
]
},
{
"id": "iq_opc_debug",
"type": "debug",
"z": "iq_tab_opcua",
"name": "InduQT response",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": true,
"complete": "payload",
"targetType": "msg",
"statusVal": "statusCode",
"statusType": "msg",
"x": 1190,
"y": 140,
"wires": []
},
{
"id": "iq_opc_endpoint",
"type": "OpcUa-Endpoint",
"endpoint": "opc.tcp://192.168.10.20:4840",
"secpol": "None",
"secmode": "None",
"none": true,
"login": false,
"usercert": false,
"usercertificate": "",
"userprivatekey": ""
}
]On site with Python
The same job as a short script with asyncua and requests. Run it as a service on a PC that can reach the server.
# pip install asyncua requests
import asyncio, os
from datetime import datetime, timezone
import requests
from asyncua import Client
ENDPOINT = "opc.tcp://192.168.10.20:4840" # the plant server, reachable from this PC
INGEST = "https://api.induqt.ai/v1/ingest"
KEY = os.environ["INDUQT_API_KEY"] # ingest-scoped, site-scoped key
MACHINE = "B-101" # machine externalId in InduQT
NODES = { # OPC UA node id -> InduQT signal key
"ns=2;s=Boiler1.SteamPressure": "steam_pressure",
"ns=2;s=Boiler1.SteamFlow": "steam_flow",
"ns=2;s=Boiler1.FlueO2": "flue_o2",
}
def post(values):
body = {"machine": MACHINE, "ts": datetime.now(timezone.utc).isoformat(), "values": values}
r = requests.post(INGEST, json=body, headers={"Authorization": f"Bearer {KEY}"}, timeout=10)
if r.status_code >= 300:
print("InduQT", r.status_code, r.text)
async def main():
async with Client(url=ENDPOINT) as client:
nodes = {key: client.get_node(node_id) for node_id, key in NODES.items()}
while True:
values = {}
for key, node in nodes.items():
v = await node.read_value()
if isinstance(v, (int, float, bool)):
values[key] = float(v)
if values:
await asyncio.to_thread(post, values)
await asyncio.sleep(5)
asyncio.run(main())Reading many tags? Batch several machines per request with readings; see the ingest API.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| Cannot connect (cloud) | The endpoint is not reachable from the internet, or the server advertises an internal hostname in its endpoint description. Configure the server to advertise its public name. |
| BadSecurityChecksFailed or certificate rejected | With Sign or SignAndEncrypt the server must trust the client certificate. Move it from the rejected to the trusted folder on the server. |
| BadNodeIdUnknown | The node id is wrong. Copy it exactly from the browser, including ns= and the identifier type (s=, i=, g=). |
| BadUserAccessDenied | Anonymous access is off or the user lacks read rights on the node. |
| Values arrive as 0/1 | Boolean tags are stored as 0 and 1. Strings and arrays are skipped. |
| Node-RED: 401 from InduQT | INDUQT_API_KEY is not set in the Node-RED process, so the header reads Bearer undefined. Restart Node-RED after setting it. |