Skip to content

Transports

A network is a configured protocol connection. It is declared once and shared across all devices that speak the same protocol on the same network endpoint.

The network owns the connection lifecycle: it opens it, keeps it alive, and closes it. Individual device reads and writes are dispatched through it without managing connectivity per device.


Connection lifecycle

Each transport goes through the following states:

State Description
idle Initialised, not yet connected
connecting Connection attempt in progress
connected Ready to read and write
connection_error Last connection attempt failed
closing Graceful shutdown in progress
closed Connection released

Every read and write is gated by a connected guard: if the transport is not in the connected state when an operation is issued, a connection attempt is made automatically before proceeding. If the attempt fails, the state moves to connection_error and the operation is still attempted — the error surfaces to the caller.

Updating a transport's config triggers an automatic reconnect — the transport closes the current connection and reopens it with the new settings.


Configuration per protocol

HTTP

HTTP is stateless — each read or write sends a new HTTP request. The transport holds a shared async HTTP client that is initialised on connect and reused across requests.

Field Required Default Description
request_timeout no 10 Timeout in seconds applied to every HTTP request

MQTT

MQTT maintains a persistent connection to a broker. It is push-based: on connect, the transport starts a background message loop that dispatches incoming messages to registered attribute listeners. Any message arriving on a topic that matches a registered attribute's read topic will be parsed through that attribute's codecs and used to update its value — regardless of whether the message was triggered by a read request. In practice, the MQTT transport largely works by listening to topics corresponding to registered device attributes.

Read flow — the transport publishes a request message to request.topic, subscribes to the response topic, and waits up to 10 seconds for a message to arrive. If no message is received within that window, the read times out. The request field in the transport address controls what is published and where.

Reply matching — some devices answer every request on one shared reply topic, with nothing in the frame tying it to the request. A read on such a topic would otherwise take the first frame that lands, which may answer another attribute's read or be an on-change push. The optional match field of a read address tells the transport which frame is the reply: a frame is accepted when the json_path expression finds at least one match in it, and every other frame is skipped until one matches or the 10-second window closes. Frames that are not JSON never match. The expression uses the same syntax as the json_path codec, so the driver typically repeats the codec's selector:

read:
  topic: updData/${mac}
  request:
    topic: ${mac}
    message: { data: Temperature, command: READ_DATA }
  match:
    json_path: $.data[?(@.name == "Temperature")]

match only affects reads. Listeners still receive every frame on the topic and decode it best-effort, as before.

Write flow — the transport publishes the rendered message to topic as defined in the write address.

Field Required Default Description
host yes Hostname or IP address of the MQTT broker
port no 1883 TCP port of the MQTT broker
tls no false Enables mqtts (TLS/mTLS)
ca_cert no PEM-encoded CA certificate used to validate the broker's certificate
client_cert no PEM-encoded client certificate presented to the broker (mTLS)
client_key no PEM-encoded private key matching client_cert
username no Username, for brokers combining mTLS with user auth
password no Password, for brokers combining mTLS with user auth

TLS brokers conventionally listen on 8883, not the plain-mqtt default of 1883port does not change automatically when tls: true is set. Connecting with tls: true to a plaintext port (or vice versa) fails as a connection timeout, not a clear error, so double-check the port matches the broker's actual listener.

transport:
  name: site-mqtts
  protocol: mqtt
  config:
    host: broker.example.com
    port: 8883
    tls: true
    ca_cert: |
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
    client_cert: |
      -----BEGIN CERTIFICATE-----
      ...
      -----END CERTIFICATE-----
    client_key: |
      -----BEGIN PRIVATE KEY-----
      ...
      -----END PRIVATE KEY-----

Modbus TCP

Modbus TCP maintains a persistent TCP connection to the Modbus server (PLC or gateway). Reads and writes are pull-based: the transport issues a Modbus function code request and waits for a response.

Field Required Default Description
host yes Hostname or IP address of the Modbus server
port no 502 TCP port of the Modbus server

M-Bus

M-Bus connects to an M-Bus/TCP gateway over RFC 2217. Reads are pull-based: the transport requests a meter's data by primary address and parses the response. It is read-only.

Field Required Default Description
host yes Hostname or IP address of the M-Bus/TCP gateway
port yes TCP port of the gateway
baud_rate no 2400 Serial baud rate negotiated with the gateway

BACnet

BACnet creates a local BACnet/IP application bound to the specified network interface. The ip_with_mask identifies that interface. The transport sends and receives BACnet packets over UDP.

Field Required Default Description
ip_with_mask yes Local interface IP with subnet mask (e.g. 192.168.1.100/24)
port no 47808 UDP port for BACnet/IP
local_device_instance no 990001 BACnet device instance of the Gridone client on the network
local_device_name no "GridOne BACnet Client" BACnet device name of the Gridone client
max_apdu_length no 1024 Maximum APDU length in bytes
vendor_identifier no 999 BACnet vendor identifier
segmentation_supported no noSegmentation Segmentation support level
discovery_timeout no 10.0 Timeout in seconds for device discovery
read_property_timeout no 5.0 Timeout in seconds for read operations
write_property_timeout no 5.0 Timeout in seconds for write operations
default_write_priority no 8 Default BACnet write priority (516) used when no priority is specified in the address

KNX

KNX uses the KNX/IP tunneling protocol to communicate with a KNX/IP gateway. It is push-based: on connect, a background listener processes all incoming telegrams. Any GroupValueResponse or GroupValueWrite received on a registered group address is immediately dispatched and updates the corresponding attribute value.

Read flow — sends a GroupValueRead telegram to the group address and awaits a GroupValueResponse. If no response is received within 5 seconds, the read times out.

Write flow — sends a GroupValueWrite telegram to the group address.

Field Required Default Description
gateway_ip yes Hostname or IP address of the KNX/IP gateway (no protocol prefix)
port no 3671 UDP or TCP port of the KNX/IP gateway
tunneling_mode no "udp" Tunneling transport: "udp" or "tcp"
secure_device_authentication_password no KNX IP-Secure device authentication password
secure_user_password no KNX IP-Secure user password
secure_user_id no 2 KNX IP-Secure tunnel user ID

KNX IP-Secure is enabled by setting both secure_device_authentication_password and secure_user_password (setting only one of them is a validation error). When enabled, the connection always uses TCP Secure regardless of tunneling_mode.


Webhook

Webhook is the HTTP-ingress counterpart of MQTT: instead of Gridone subscribing to a broker, external producers push messages to Gridone over HTTP. It is push-based and ingress-only — the transport cannot solicit data and does not support writes.

Messages enter through the API:

POST /transports/{transport_id}/ingress/{topic}

Everything after /ingress/ is the topic (it may contain slashes, e.g. room1/snapshot). The raw request body is the message payload. Topics are matched exactly against the read topics of registered device attributes — no MQTT-style wildcards. A push on a topic with no subscribed attribute returns 200 {"matched": 0}: this is a valid outcome (for example a push racing device provisioning), not an error.

Read flow — not supported. The transport cannot solicit data, so on-demand reads (refresh, read_device) fail with a 422; attribute values only move when a message is pushed. For the same reason webhook drivers must not poll: an explicit polling_enabled: true is rejected at driver validation, and a driver that omits the setting gets polling disabled automatically.

Write flow — not supported.

Authentication — the ingress endpoint is outside the API's user-authorization flow (this is device-level ingestion, like MQTT or BACnet credentials). The transport verifies each push itself, according to its config:

Scheme How to push
bearer Send Authorization: Bearer <secret>
hmac_sha256 Send x-signature-256: sha256=<hexdigest>, where the digest is HMAC-SHA256 of the raw request body keyed with secret (GitHub webhook convention)
none No credentials checked

A failed check returns 401 with a generic message. Request bodies are capped at 1 MiB.

Field Required Default Description
auth no bearer Authentication scheme: none, bearer or hmac_sha256
secret required unless auth: none Shared secret used to verify pushes

Device health — a webhook has no connection to monitor. Declare healthcheck.expected_push_interval in the driver: the device is marked degraded after 2× the interval without a push, then error after 3×.

transport:
  name: app-ingress
  protocol: webhook
  config:
    auth: bearer
    secret: my-shared-secret

Example push:

curl -X POST "https://gridone.example.com/api/transports/<transport_id>/ingress/room1/snapshot" \
  -H "Authorization: Bearer my-shared-secret" \
  -H "Content-Type: application/json" \
  -d '{"temperature": 21.5, "humidity": 55}'

OPC-UA

OPC-UA maintains a persistent session to a server. It is pull-based by default — reads go through the OPC-UA Read service with push available per attribute via subscriptions (push: true on the attribute, see General Layout): the transport opens one Subscription for the session and adds a MonitoredItem per subscribed NodeId, delivering data-change notifications instead of polling.

Read flow — a single address read uses the OPC-UA Read service against that NodeId. Multiple addresses in the same sweep are batched into one Read service call instead of one request per address.

Write flow — the transport reads the NodeId's server-declared variant type first, then writes the coerced value as that type.

Push flow (opt-in per attribute) — a subscribed NodeId is added as a MonitoredItem on the transport's Subscription. sampling_interval_ms and deadband control how the server reports changes; a Bad status quality drops the notification, Uncertain is still delivered.

Field Required Default Description
endpoint_url yes Server endpoint, e.g. opc.tcp://host:port/path
auth_mode no anonymous anonymous or username_password
username no Required if auth_mode is username_password
password no Required if auth_mode is username_password
connect_timeout no 10 Seconds to establish the session
request_timeout no 5 Seconds per Read/Write service call
keepalive_interval no 5 Seconds between session keepalive pings
sampling_interval_ms no 1000 Sampling interval, in milliseconds, requested for every MonitoredItem
deadband no 0 Absolute deadband applied to push notifications; 0 reports every change
security_policy no None None, Basic256Sha256, Aes128Sha256RsaOaep or Aes256Sha256RsaPss
security_mode no None None, Sign or SignAndEncrypt

security_policy and security_mode must both be None, or both set to a non-None value — one without the other is rejected.

transport:
  name: chiller-opcua
  protocol: opcua
  config:
    endpoint_url: opc.tcp://10.0.1.20:4840
    auth_mode: anonymous

Secure channel

Setting security_policy/security_mode enables OPC-UA's own mutual X.509 secure channel — a separate handshake from user auth (auth_mode composes freely with any policy/mode pair). Gridone generates and keeps one application-instance certificate for the whole instance under GRIDONE_OPCUA_PKI_DIR (default ~/.gridone/opcua-pki). The same certificate is used for every server, so this directory must be persisted across restarts — losing it means redoing the trust step on every server.

Server trust is pin-on-first-use: the certificate a server presents on first connect is written to the PKI directory and required byte-for-byte on every later connect. Because a server also has to trust gridone's certificate before it will accept a session, and nothing gridone does client-side can make that happen, connecting to a new server for the first time takes a manual step:

  1. Configure the transport and let it attempt to connect. The attempt fails and the transport parks in an error state — this failed attempt is what puts gridone's certificate in front of the server.
  2. On the server, find gridone's certificate in its rejected-certificates list and move it to the trusted list (vendor-specific UI/certificate store).
  3. Trigger a reconnect: POST /transports/{id}/reconnect. This step is required — a rejected secure channel is a terminal error and gridone will not retry on its own.

If a server later rotates its certificate, gridone refuses the new one (the pin no longer matches). Accepting it means deleting the pinned certificate file for that server under the PKI directory and reconnecting, which re-runs the first-use step above.

Basic128Rsa15 and Basic256 are not offered — the OPC Foundation withdrew both.