# MQTT and the UNS topics

> Subscribe to live tag values on the MQTT broker built into every edge gateway, with the Unified Namespace topic layout, the JSON payloads and Sparkplug B.

Source: https://proxus.io/docs/develop/mqtt

At the end of this guide, an MQTT client of yours receives every tag value of a gateway's devices as it is read,
on topics that follow the plant's Unified Namespace.

## Before you start

You need:

- The address of the edge gateway whose devices you want. Each gateway runs its own broker and publishes only its
  own devices. The hub's gateway, **SYSTEM**, publishes the devices assigned to it.
- The gateway's **MasterToken**, from the `[MQTTBroker]` section of that gateway's configuration file. If it is
  empty when the gateway starts, the gateway creates one and writes it into the file.
- Network access to the broker port: `8883` for TLS, or `1883` without TLS (`MQTTBrokerPort`).
- For TLS: the platform CA, from **Security > Certificate Store** with **Download CA**.
- Any MQTT 3.1.1 or 5 client: MQTT Explorer, `mosquitto_sub`, or a library such as Python `paho-mqtt`.

> **Note:**
The device's place in the plant (enterprise, site, area, line or cell, equipment) decides its topic. Set it on the
device, as described in [Unified Namespace](https://proxus.io/docs/concepts/unified-namespace).

## Step 1: Connect

| Setting | Value |
| --- | --- |
| Host | the gateway's address |
| Port | `8883` with TLS, or `1883` (or `MQTTBrokerPort`) without TLS |
| User name | the gateway's `MasterToken` |
| Password | empty |
| TLS | on port 8883: verify the broker against the platform CA. See [Connect with TLS](#connect-with-tls). |

The broker accepts a connection when the user name is the `MasterToken`, when it is the **Token** of a device on
that gateway (shown on the device page), or when `AllowAnonymous = true`. Any other connection is refused with
`Not authorized`.

> **Warning:**
`AllowAnonymous = true` lets anyone on the network read every value of the gateway. Leave it `false` and give the
`MasterToken` only to systems that need it. The broker has no per-topic rights: a connected client can subscribe to
every topic.

### Connect with TLS

The broker accepts TLS on port **8883** with the gateway's own certificate, which the hub issues from the
[Certificate Store](https://proxus.io/docs/administer/certificates). The port opens once the hub has issued the certificate.

1. In **Security > Certificate Store**, select **Download CA**. The file holds the CA certificates (PEM), no private
   key.
2. In your client, set port `8883` and use that file as the trusted CA. Connect to a host name or address that the
   gateway's certificate covers, because the name must match.

```bash
mosquitto_sub -h <gateway-host> -p 8883 --cafile proxus-ca.pem -u "<MasterToken>" -t 'v1/#' -v
```

In the Python example below, call `client.tls_set(ca_certs="proxus-ca.pem")` and use port `8883`.

Port **1883** stays open for devices without TLS. To accept TLS only, set `[MQTTBroker] PlainEnabled = false`.
When the gateway's certificate is renewed, the broker restarts for a few seconds and clients connect again.

## Step 2: Subscribe

Subscribe to `v1/#` for everything, or to a part of the tree. For example:

| Subscription | Receives |
| --- | --- |
| `v1/#` | every topic of the gateway |
| `v1/Meridian/Lyon_Foods_Plant/#` | everything of one site |
| `v1/+/+/+/+/+/LYS_PACKER_04/telemetry/+` | each tag of one device, when its place has all five levels |

```python
import os
import paho.mqtt.client as mqtt

BROKER = "localhost"
PORT = 1883
TOKEN = os.environ["PROXUS_MQTT_TOKEN"]  # the gateway's MasterToken

def on_connect(client, userdata, flags, reason_code, properties):
    print("connected:", reason_code)
    client.subscribe("v1/#")

def on_message(client, userdata, msg):
    print(msg.topic, msg.retain, msg.payload.decode())

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="uns-reader")
client.username_pw_set(TOKEN)
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT)
client.loop_forever()
```

Run it with `PROXUS_MQTT_TOKEN=<token> python3 uns_reader.py`.

## The topic layout

```text
v1/<Enterprise>/<Site>/<Area>/<Line or cell>/<Equipment>/<Device>/<category>/<name>
```

- `v1` is the namespace version.
- The place levels come from the device. A level that is empty is left out, so the topic is shorter. A device with
  only a site and an area publishes under `v1/Lyon_Foods_Plant/Packaging/LYS_PACKER_04/...`; a device with no place
  at all publishes under `v1/<Device>/...`.
- Every character that is not a letter, a digit, `_` or `-` becomes `_`. `Lyon Foods Plant` becomes
  `Lyon_Foods_Plant`, `Line 1` becomes `Line_1`.
- `<category>` is `telemetry` for values and `state` for the device's state.

| Topic | Retained | Sent |
| --- | --- | --- |
| `.../telemetry/<tag>` | no | every time the tag is read |
| `.../telemetry/_summary` | no | all tags of the device in one message, at most every `SummaryIntervalMs` (5 s) |
| `.../state/_lastSeen` | yes | the time of the latest reading, at most every `LastSeenIntervalMs` (5 s) |
| `.../state/_status` | yes | `Online` when values arrive, `Offline` when the device is disabled or deleted. For a device that publishes its own values (see below), also `Offline` when its client disconnects and `Timeout` after five minutes without values. |

Retained messages are kept by the broker and sent to a new subscriber at once, so a client that connects later
still learns the status of every device. Set `UNSRetainMessages = false` to turn retaining off.

## The payloads

One tag value, on `v1/Lyon_Foods_Plant/Packaging/LYS_PACKER_04/telemetry/Temperature`:

```json
{"ts":"2026-10-15T19:01:43.450Z","value":25.4,"dataType":"Double","unit":"°C","quality":"Good","source":"Proxus"}
```

| Field | Meaning |
| --- | --- |
| `ts` | When the value was read, UTC, ISO 8601 with milliseconds. |
| `value` | The value as a JSON number, `true`/`false` or string. |
| `dataType` | `Bool`, `Int`, `Long`, `Float`, `Double`, `DateTime` or `String`. It is worked out from the value itself, so a `UShort` tag arrives as `Int`. |
| `unit` | The tag's unit. Left out when the tag has none. |
| `quality` | Always `Good` on this topic. Values that could not be read are not published here; see the device page for read errors. |
| `source` | Always `Proxus`. |

With `TelemetryThinPayload = false`, each tag message also carries a `context` object with `enterprise`, `site`,
`area`, `processCell`, `equipmentModule`, `deviceId`, `deviceName` and `metric`.

The summary, on `.../telemetry/_summary`:

```json
{
  "ts": "2026-10-15T19:01:46.955Z",
  "deviceId": "48",
  "deviceName": "MFB_LYS_MIXER_3",
  "metrics": {
    "Speed": {"value": 386, "dataType": "Int", "unit": "rpm", "quality": "Good"},
    "Running": {"value": true, "dataType": "Bool", "quality": "Good"}
  },
  "context": {"enterprise": "Meridian", "site": "Lyon Foods Plant", "area": "Processing",
              "processCell": "Dough", "equipmentModule": "_none"},
  "source": "Proxus"
}
```

`context` is left out when `SummaryIncludeContext = false`. `_none` means the device has no equipment level.

The state topics:

```json
{"ts":"2026-10-15T19:01:39.551Z"}
{"status":"Online","ts":"2026-10-15T18:14:07.999Z"}
{"status":"Timeout","ts":"2026-10-15T19:20:00.000Z","reason":"Device timeout"}
```

## Publish values from a device

A device or program can send its own values to the broker. It connects with the device's **Token** as user name
and publishes JSON to the topic `telemetry` (or any topic under `telemetry/`). Each field becomes a tag of that
device:

```json
{"Temperature": 21.7, "DoorOpen": false, "Pressure": {"value": 2.4, "unit": "bar"}}
```

Nested objects become dotted names (`Line.Speed`), and arrays get an index (`Axis[0]`). The values then go through
the same path as values the gateway reads itself: storage, pipelines, outbound targets and the UNS topics.

Writing to a device through MQTT, by publishing to a `command` topic, is not supported with the JSON format. Use a
[rule action](https://proxus.io/docs/automate/rule-actions) or a [pipeline](https://proxus.io/docs/automate/pipelines) to write values.

## Sparkplug B

Set `UNSPayloadFormat = "SparkplugB"` in the `[UNS]` section of the gateway's configuration and restart the gateway.
The gateway then publishes Sparkplug B 3.0 messages on its own broker instead of the JSON topics above:

- Topic namespace `spBv1.0/<UNSGroupId>/...`, with the gateway as edge node `<UNSEdgeNodeId>`.
- Each device is a Sparkplug device: a `DBIRTH` with its name, status, last-seen time and place, then `DDATA`
  with its tag values. Each metric carries `Source`, `Quality` and `Unit` properties.
- The node answers the node commands `Node Control/Rebirth` (sends the births again), `discovery` and
  `refresh_metadata`.

Sparkplug host applications connect with the `MasterToken` as user name, the same as JSON clients.

## Broker and UNS settings

These keys are in the gateway's configuration file. See [Configuration keys](https://proxus.io/docs/reference/configuration) for the
environment-variable form.

| Key | Default | Meaning |
| --- | --- | --- |
| `[MQTTBroker] Enabled` | `true` | Runs the broker on this gateway. With `false` there is no broker and no UNS publishing. |
| `[MQTTBroker] MQTTBrokerPort` | `1883` | The broker port. If it is taken, the gateway picks a free port and logs a warning, unless `AllowPortFallback = false`. |
| `[MQTTBroker] TlsPort` | `8883` | The TLS port, with the gateway's certificate. `0` turns TLS off. |
| `[MQTTBroker] PlainEnabled` | `true` | Accept connections without TLS on `MQTTBrokerPort`. `false` accepts TLS only. |
| `[MQTTBroker] AllowAnonymous` | `false` | Accept clients without a user name. |
| `[MQTTBroker] MasterToken` | created at first start | The user name for UNS and Sparkplug clients. |
| `[MQTTBroker] EnableMQTTHooks` | `true` | Publishes the values the gateway reads to the UNS topics. With `false`, only values that devices publish themselves appear. |
| `[UNS] UNSPayloadFormat` | `"JSON"` | `JSON` or `SparkplugB`. |
| `[UNS] UNSRetainMessages` | `true` | Retain the state topics. |
| `[UNS] UNSGroupId`, `UNSEdgeNodeId` | `"ProxusGroup"`, `"ProxusEdge"` | Sparkplug group and edge node. |
| `[UNS.Optimizations] TelemetryThinPayload` | `true` | Leave `context` out of each tag message. |
| `[UNS.Optimizations] SummaryIntervalMs` | `5000` | Least time between two summaries of a device. `0` sends one with every reading. |
| `[UNS.Optimizations] SummaryIncludeContext` | `true` | Put `context` in the summary. |
| `[UNS.Optimizations] SummaryOnlyOnChange` | `false` | Send a summary only when a value changed. |
| `[UNS.Optimizations] LastSeenIntervalMs` | `5000` | Least time between two `_lastSeen` messages. |
| `[UNS.Optimizations] StatusOnChangeOnly` | `true` | Send `_status` only when it changes. |

The QoS levels are `0` for telemetry and `1` for state messages.

## What's next

- [Unified Namespace](https://proxus.io/docs/concepts/unified-namespace): how the plant hierarchy is built.
- [UNS Explorer](https://proxus.io/docs/organize/uns-explorer): browse the same tree in the hub.
- [Outbound targets](https://proxus.io/docs/connect/outbound-targets): send values to your own MQTT broker instead.
