> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# BACnet/IP Route

> Connect Coreflux to building automation controllers over BACnet/IP — TAG polling, COV subscriptions, bidirectional writes, alarms, and on-demand ReadProperty/WriteProperty

## Connect Coreflux to Building Automation

The BACnet route connects Coreflux to building automation controllers (ASHRAE 135) using the unified OT TAG system. Define the BACnet configuration, add Tags for continuous polling or COV subscriptions, and handle alarm operations.

<Tip>
  BACnet is the dominant protocol for building management systems. If your controller supports BACnet/IP, this route maps each data point to an MQTT topic using the same TAG model as every other Coreflux industrial route.
</Tip>

### When to use this

Use the BACnet route when you need to integrate HVAC, lighting, metering, or access-control controllers that expose **BACnet/IP** — read live values on a schedule, subscribe to change notifications, write setpoints from MQTT, receive intrinsic alarms, or run ad-hoc reads and writes triggered by MQTT commands.

### Key capabilities

* **BACnet/IP over UDP** — standard port 47808 (0xBAC0) with broadcast or direct-IP discovery
* **TAG-based polling** — per-point MQTT topics with a configurable polling interval
* **Change-of-Value (COV)** — push subscriptions per tag (`WITH COV true`) without polling
* **Event alarms** — BACnet Event-Notifications to MQTT with per-tag or device-wide streams
* **Bidirectional writes** — MQTT-triggered writes to analog, binary, and value objects
* **On-demand events** — `ADD EVENT` with `READ`, `WRITE`, and `ACK` queries triggered by any MQTT publish
* **Operator ACK workflow** — auto-registered ACK handler per alarming tag
* **Direct IP mode** — connect to controllers that do not respond to Who-Is/I-Am
* **Docker and hostname support** — resolve container service names before opening the BACnet socket

***

## Quick Start

Pick the starting point that matches what you need. Each tag publishes to `{SOURCE_TOPIC}/{tagName}` unless the tag sets its own `SOURCE_TOPIC`.

<Tabs>
  <Tab title="Poll sensor values">
    Connect to a controller and read a room temperature every five seconds:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
        ADD MAPPING RoomSensors
            WITH SOURCE_TOPIC "bacnet/bms"
            WITH EVERY 5 SECONDS
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH UNIT "degC"
    ```
  </Tab>

  <Tab title="Read and write">
    Read a setpoint and allow MQTT writes back to the device:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE HVAC WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "10.0.0.50"
            WITH DEVICE_ID 2000
            WITH WRITE_PRIORITY 8
        ADD MAPPING Setpoints
            WITH SOURCE_TOPIC "bacnet/hvac"
            WITH EVERY 10 SECONDS
            ADD TAG CoolingSetpoint
                WITH ADDRESS "AV:1"
                WITH DATA_TYPE "FLOAT"
                WITH UNIT "degC"
                WITH WRITABLE true
                WITH DESTINATION_TOPIC "bacnet/hvac/CoolingSetpoint/set"
    ```
  </Tab>

  <Tab title="Updates on change">
    Subscribe to Change-of-Value so the device pushes updates only when values change — no polling interval needed:

    ```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
            WITH COV_LIFETIME 600
        ADD MAPPING CovPoints
            WITH SOURCE_TOPIC "bacnet/bms/cov"
            ADD TAG RoomTempCov
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH COV true
    ```
  </Tab>

  <Tab title="Connect directly">
    When the controller does not answer Who-Is/I-Am, connect straight to its IP address:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "10.0.0.50"
            WITH PORT 47808
            WITH DEVICE_ID 1234
            WITH DIRECT_IP true
        ADD MAPPING RoomSensors
            WITH SOURCE_TOPIC "bacnet/bms"
            WITH EVERY 5 SECONDS
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH UNIT "degC"
    ```
  </Tab>

  <Tab title="On-demand read/write">
    Trigger a single BACnet read or write by publishing to an MQTT command topic:

    ```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
        ADD EVENT ReadProperty
            WITH SOURCE_TOPIC "bacnet/cmd/read"
            WITH DESTINATION_TOPIC "bacnet/cmd/read/result"
            WITH QUERY "READ:AI:0:PRESENT_VALUE"
        ADD EVENT WriteProperty
            WITH SOURCE_TOPIC "bacnet/cmd/write"
            WITH DESTINATION_TOPIC "bacnet/cmd/write/result"
            WITH QUERY "{payload}"
    ```
  </Tab>

  <Tab title="Per-tag alarms">
    Each alarming point gets its own MQTT alarm stream and auto-registered ACK topic:

    ```lot wrap  theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
            WITH LOCAL_DEVICE_ID 4194300
        ADD MAPPING LimitPoints
            WITH SOURCE_TOPIC "bacnet/bms"
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH COV true
                WITH ALARM true
    ```
  </Tab>
</Tabs>

***

## Protocol Overview

BACnet (Building Automation and Control networks) is an open protocol for building automation, standardized as ANSI/ASHRAE 135 and ISO 16484-5. It enables interoperability between devices from different manufacturers in HVAC, lighting, fire detection, access control, and energy management.

| Concept                | Description                                                                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Object model**       | Every data point is a typed object (Analog Input, Binary Value, etc.) identified by instance number, accessed by properties (Present Value, High Limit, Event State, …) |
| **Service model**      | Clients read and write properties using BACnet services (ReadProperty, WriteProperty, SubscribeCOV, AcknowledgeAlarm)                                                   |
| **Device discovery**   | Who-Is/I-Am broadcast locates devices by Device Instance without knowing the IP address                                                                                 |
| **Intrinsic alarming** | Controllers generate Event-Notifications when values cross configured limits — alarm logic lives in the device                                                          |

### BACnet variants

| Variant                  | Transport          | Port / Layer   | Notes                        |
| ------------------------ | ------------------ | -------------- | ---------------------------- |
| **BACnet/IP**            | UDP/IP             | 47808 (0xBAC0) | Most common; fully supported |
| **BACnet MSTP**          | Serial RS-485      | N/A            | Not supported                |
| **BACnet over Ethernet** | Ethernet (Annex H) | —              | Not supported                |
| **BACnet/SC**            | TLS/WebSocket      | —              | Not currently implemented    |

<Note>
  Coreflux supports **BACnet/IP** (UDP). MS/TP parameters are parsed but the transport is not validated. Use BACnet/IP for all production deployments.
</Note>

### Supported controllers and platforms

| Category                  | Examples                                                               |
| ------------------------- | ---------------------------------------------------------------------- |
| **HVAC controllers**      | Siemens Desigo, Johnson Controls Metasys, Honeywell WEBs, Carrier i-Vu |
| **DDC / field panels**    | Distech Controls ECB, Alerton Ascent, Delta Controls ORCAview          |
| **Meters and sensors**    | Veris BMeter, Onset HOBO, Accuenergy Acuvim                            |
| **Gateways**              | Contemporary Controls BASRT-B, Babel Buster (Modbus→BACnet)            |
| **Simulators**            | IOTech BACnet device simulator (Docker); VTS (Visual Test Shell)       |
| **Integration platforms** | Niagara (Tridium), EBO (Schneider)                                     |

***

## Configuration

Only `DEVICE_ID` is strictly required. For a working route, also set `IP` and add at least one `ADD MAPPING` with tags.

```lot wrap focus={2-4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
    ADD BACNET_CONFIG
        WITH IP "192.168.1.10"
        WITH DEVICE_ID 1234
    ADD MAPPING RoomSensors
        WITH SOURCE_TOPIC "bacnet/bms"
        WITH EVERY 5 SECONDS
        ADD TAG RoomTemp
            WITH ADDRESS "AI:0"
            WITH DATA_TYPE "FLOAT"
            WITH UNIT "degC"
```

### All configuration options

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
    ADD BACNET_CONFIG
        WITH IP "192.168.1.10"
        WITH PORT 47808
        WITH LOCAL_PORT 0
        WITH DEVICE_ID 1234
        WITH TIMEOUT 3
        WITH DISCOVERY_TIMEOUT 5
        WITH DIRECT_IP false
        WITH WRITE_PRIORITY 8
        WITH RETRIES 3
        WITH MAX_APDU 1476
        WITH COV_LIFETIME 300
        WITH COV_CONFIRMED false
        WITH LOCAL_DEVICE_ID 4194300
        WITH ALARMS false
        WITH ALARMS_TOPIC "bacnet/bms/alarms"
```

### Configuration parameters

| Setting                   | Required    | Default                     | Description                                                                                                                  |
| ------------------------- | ----------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `DEVICE_ID`               | **Yes**     | `1`                         | Target BACnet device instance (Who-Is/I-Am and ReadProperty)                                                                 |
| `IP` / `HOST` / `ADDRESS` | Conditional | `192.168.1.1`               | Remote device IPv4 or hostname. **Required** when `WITH DIRECT_IP true`; strongly recommended for Docker and routed networks |
| `PORT`                    | No          | `47808`                     | Remote device BACnet/IP UDP listen port                                                                                      |
| `LOCAL_PORT`              | No          | `0`                         | Local UDP port the broker binds on this host (`0` = OS ephemeral)                                                            |
| `TRANSPORT`               | No          | `IP`                        | `IP` or `MSTP` (MS/TP not validated — use BACnet/IP)                                                                         |
| `TIMEOUT`                 | No          | `3`                         | Read/write timeout in seconds                                                                                                |
| `DISCOVERY_TIMEOUT`       | No          | `5`                         | Who-Is/I-Am wait in seconds                                                                                                  |
| `DIRECT_IP`               | No          | `false`                     | Skip discovery; connect to `IP:PORT` and verify with ReadProperty                                                            |
| `WRITE_PRIORITY`          | No          | `8`                         | BACnet command priority (1 = highest, 16 = lowest)                                                                           |
| `RETRIES`                 | No          | `3`                         | Retry count on failure                                                                                                       |
| `MAX_APDU`                | No          | `1476`                      | Max APDU length (BACnet/IP)                                                                                                  |
| `COV_LIFETIME`            | No          | `300`                       | Default COV subscription lifetime in seconds (`0` = infinite)                                                                |
| `COV_CONFIRMED`           | No          | `false`                     | Request confirmed COV notifications by default                                                                               |
| `LOCAL_DEVICE_ID`         | Conditional | `0`                         | Local device instance for directed I-Am. **Required** when `WITH ALARMS true` or any TAG has `WITH ALARM true`               |
| `ALARMS`                  | No          | `false`                     | Publish all device Event-Notifications to `ALARMS_TOPIC`                                                                     |
| `ALARMS_TOPIC`            | No          | `bacnet/{routeName}/alarms` | MQTT topic for device-wide alarms when `WITH ALARMS true`                                                                    |
| `ALARM_TOPIC`             | No          | —                           | Alias for `ALARMS_TOPIC` on `BACNET_CONFIG`                                                                                  |

### Network setup: IP, PORT, and LOCAL\_PORT

BACnet/IP is UDP. Three settings control two different endpoints — the BACnet device (remote) and the Coreflux broker (local).

| Setting      | Side   | What it is                                            | Typical value                           |
| ------------ | ------ | ----------------------------------------------------- | --------------------------------------- |
| `IP`         | Remote | IPv4 address or hostname of the BACnet controller     | `192.168.1.10`, `coreflux-route-bacnet` |
| `PORT`       | Remote | UDP port where the device accepts BACnet/IP traffic   | `47808`                                 |
| `LOCAL_PORT` | Local  | UDP port the broker binds on before sending/receiving | `0` (OS-assigned)                       |

| Scenario                                         | `IP`                                            | `PORT`  | `LOCAL_PORT` |
| ------------------------------------------------ | ----------------------------------------------- | ------- | ------------ |
| Sandbox simulator on the same host               | `127.0.0.1`                                     | `47808` | `0`          |
| Simulator in Docker, broker in another container | `coreflux-route-bacnet` (docker container name) | `47808` | `0`          |
| Device on non-standard port                      | `10.0.0.5`                                      | `47809` | `0`          |
| Firewall requires fixed source port              | `10.0.0.5`                                      | `47808` | `47808`      |

<Tip>
  `WITH IP` accepts hostnames and Docker service names (for example `coreflux-route-bacnet`, `host.docker.internal`). The broker resolves them to IPv4 before opening the BACnet socket.
</Tip>

### Direct IP mode

Use when the controller does not respond to Who-Is/I-Am but accepts ReadProperty at a known UDP endpoint:

```lot wrap focus={6} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
    ADD BACNET_CONFIG
        WITH IP "10.0.0.50"
        WITH PORT 47808
        WITH DEVICE_ID 1234
        WITH DIRECT_IP true
```

### Write priority

Present-value writes use BACnet priority slots (1 = highest, 16 = lowest). If a device holds a value at a higher priority, a write at a lower priority will not override the active present value. The IOTech sandbox profile seeds `AO:0` at priority **1** — use `WITH WRITE_PRIORITY 1` for that test device.

<Warning>
  If a write appears to succeed but the present value does not change, the device may be holding the value at a higher priority. Try `WITH WRITE_PRIORITY 1` and check whether a BMS already owns the point.
</Warning>

***

## TAG System

### Mapping parameters

```lot wrap focus={1,2,3,4} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    ADD MAPPING RoomSensors
        WITH SOURCE_TOPIC "bacnet/bms"
        WITH EVERY 5 SECONDS
        ADD TAG RoomTemp
            WITH ADDRESS "AI:0"
            WITH DATA_TYPE "FLOAT"
            WITH UNIT "degC"
            WITH WRITABLE false
            WITH COV false
            WITH ALARM false
```

| Parameter      | Required    | Default | Description                                                                                                                    |
| -------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `SOURCE_TOPIC` | **Yes**\*   | —       | Base MQTT topic; each tag publishes to `{SOURCE_TOPIC}/{tagName}` unless the tag sets its own                                  |
| `EVERY`        | Conditional | —       | Polling interval (e.g. `1 SECONDS`, `200 MILLISECONDS`). Required when the mapping has polled tags; omit for COV-only mappings |
| `ADD TAG`      | **Yes**     | —       | At least one tag per mapping                                                                                                   |

\* You may set `SOURCE_TOPIC` on each tag instead of on the mapping, but one of the two is required.

### Tag parameters

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
ADD TAG RoomTemp
    WITH ADDRESS "AI:0"
    WITH DATA_TYPE "FLOAT"
    WITH UNIT "degC"
    WITH WRITABLE false
    WITH COV false
    WITH ALARM false
```

| Parameter                 | Required    | Default                            | Description                                                               |
| ------------------------- | ----------- | ---------------------------------- | ------------------------------------------------------------------------- |
| `ADDRESS`                 | **Yes**     | —                                  | BACnet object `TYPE:INSTANCE` (e.g. `AI:0`, `AV:3`, `BI:0`)               |
| `DATA_TYPE`               | **Yes**     | —                                  | `BOOL`, `INT16`, `UINT16`, `INT32`, `UINT32`, `FLOAT`, `DOUBLE`, `STRING` |
| `SOURCE_TOPIC`            | Conditional | `{mapping SOURCE_TOPIC}/{tagName}` | MQTT topic for tag data. Required on the tag **or** on the parent mapping |
| `DESTINATION_TOPIC`       | Conditional | —                                  | MQTT topic to subscribe for writes. Required when `WITH WRITABLE true`    |
| `ADDRESS_TYPE`            | No          | `PRESENT_VALUE`                    | BACnet property to read/write (e.g. `OBJECT_NAME`, `HIGH_LIMIT`)          |
| `STRING_SIZE`             | No          | `256`                              | Max string length when `DATA_TYPE` is `STRING`                            |
| `WRITABLE`                | No          | `false`                            | Allow MQTT writes to this object                                          |
| `SCALING`                 | No          | `1.0`                              | Multiply raw value                                                        |
| `OFFSET`                  | No          | `0.0`                              | Add after scaling                                                         |
| `UNIT`                    | No          | `""`                               | Engineering unit label (e.g. `degC`, `%RH`)                               |
| `DECIMAL_PLACES`          | No          | `2`                                | Rounding precision                                                        |
| `DEADBAND`                | No          | `0.0`                              | Minimum change before republishing                                        |
| `MIN_VALUE` / `MAX_VALUE` | No          | —                                  | Clamp published values                                                    |
| `PUBLISH_MODE`            | No          | `VALUE_ONLY`                       | `VALUE_ONLY` or `JSON`                                                    |
| `DESCRIPTION`             | No          | `""`                               | Documentation string only                                                 |
| `COV`                     | No          | `false`                            | Subscribe to BACnet Change-of-Value instead of polling                    |
| `COV_LIFETIME`            | No          | route `COV_LIFETIME`               | Per-tag COV subscription lifetime in seconds                              |
| `COV_CONFIRMED`           | No          | route `COV_CONFIRMED`              | Request confirmed COV for this tag                                        |
| `ALARM`                   | No          | `false`                            | Enable MQTT alarm stream on `{tag_topic}/alarms`                          |
| `ALARM_TOPIC`             | No          | `{tag_topic}/alarms`               | Override alarm MQTT topic (only when `WITH ALARM true`)                   |

### Object address format

| Shorthand | Full name          | Example |
| --------- | ------------------ | ------- |
| `AI`      | `ANALOG_INPUT`     | `AI:0`  |
| `AO`      | `ANALOG_OUTPUT`    | `AO:1`  |
| `AV`      | `ANALOG_VALUE`     | `AV:0`  |
| `BI`      | `BINARY_INPUT`     | `BI:0`  |
| `BO`      | `BINARY_OUTPUT`    | `BO:0`  |
| `BV`      | `BINARY_VALUE`     | `BV:0`  |
| `MSV`     | `MULTISTATE_VALUE` | `MSV:0` |

<Note>
  `ADDRESS_TYPE` is the **BACnet property** (e.g. `PRESENT_VALUE`, `OBJECT_NAME`), not the object type.
</Note>

### Change-of-Value (COV)

Tags with `WITH COV true` subscribe to BACnet Change-of-Value notifications instead of polling. A mapping with only COV tags does not need `WITH EVERY`. Mixed mappings poll non-COV tags on `EVERY` and receive COV tags via push.

<Note>
  COV requires support on the device side. If you receive no change notifications, confirm the controller supports COV and that `WITH COV true` is set on the tag.
</Note>

***

## Alarms and Event Notifications

The broker receives BACnet **Event-Notification** messages from the field device and publishes them to MQTT. Alarm limits and Notification Class membership are configured on the controller — the broker only needs the device to send events to its `LOCAL_DEVICE_ID`.

### Alarm message format

Every time the BACnet device reports an alarm or state change, the broker publishes **one JSON object** to the alarm topic. Subscribe to that topic to see events as they happen, or subscribe to `{alarm_topic}/active` for a retained snapshot of alarms that are still open.

**Example** — a room temperature crosses its high limit (`bacnet/bms/RoomTemp/alarms`):

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "route_name": "BMS",
  "device_id": 1234,
  "address": "AI:0",
  "tag_name": "RoomTemp",
  "tag_topic": "bacnet/bms/RoomTemp",
  "alarm_topic": "bacnet/bms/RoomTemp/alarms",
  "object_type": "ANALOG_INPUT",
  "instance": 0,
  "event_type": "EVENT_OUT_OF_RANGE",
  "from_state_short": "NORMAL",
  "to_state_short": "HIGH_LIMIT",
  "fundamental_state": "OFFNORMAL",
  "transition": "TO_OFFNORMAL",
  "event_state_to_ack": "HIGH_LIMIT",
  "ack_required": true,
  "present_value": 28.5,
  "bacnet_timestamp": "2026-06-10T15:28:21.000Z",
  "timestamp": "2026-06-10T16:28:21.003Z"
}
```

**Fields to know:**

| Field                                 | What it tells you                                                                         |
| ------------------------------------- | ----------------------------------------------------------------------------------------- |
| `address`                             | Which BACnet point raised the alarm (e.g. `AI:0` = Analog Input 0)                        |
| `tag_name` / `tag_topic`              | The route TAG and MQTT topic for live values on that point                                |
| `from_state_short` → `to_state_short` | State change just reported (e.g. `NORMAL` → `HIGH_LIMIT`)                                 |
| `transition`                          | Direction of the change: `TO_OFFNORMAL` (new alarm), `TO_NORMAL` (cleared), or `TO_FAULT` |
| `fundamental_state`                   | Overall condition: `OFFNORMAL` (limit alarm), `FAULT`, or `NORMAL`                        |
| `present_value`                       | Value on the device when the event fired (when provided)                                  |
| `ack_required`                        | If `true`, an operator must acknowledge the alarm on the device                           |
| `event_state_to_ack`                  | State to send when acknowledging — use this in ACK payloads                               |
| `bacnet_timestamp`                    | When the device recorded the event — include when acknowledging                           |

**Common `to_state_short` values:**

| Value        | Meaning                                    |
| ------------ | ------------------------------------------ |
| `HIGH_LIMIT` | Value rose above the configured high limit |
| `LOW_LIMIT`  | Value fell below the configured low limit  |
| `FAULT`      | Device reported a fault condition          |
| `NORMAL`     | Point returned to normal (alarm cleared)   |

### Setting Alarms on BACnet ROUTE

| Mode            | Configuration                         | Default MQTT topic                                  | Coverage                                 |
| --------------- | ------------------------------------- | --------------------------------------------------- | ---------------------------------------- |
| **Device-wide** | `WITH ALARMS true` on `BACNET_CONFIG` | `bacnet/{routeName}/alarms` (+ retained `…/active`) | Every Event-Notification from the device |
| **Per-tag**     | `WITH ALARM true` on TAG              | `{tag_topic}/alarms` (+ retained `…/active`)        | Only events matching the TAG's `ADDRESS` |

Both modes can coexist. When both are enabled for the same object, the event is published to both topics.

<Tabs>
  <Tab title="Device-wide">
    Publish every Event-Notification from the device to a single MQTT topic — useful for commissioning and catch-all monitoring:

    ```lot wrap focus={6-7} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
            WITH LOCAL_DEVICE_ID 4194300
            WITH ALARMS true
            WITH ALARMS_TOPIC "bacnet/bms/alarms"
        ADD MAPPING CovPoints
            WITH SOURCE_TOPIC "bacnet/bms"
            ADD TAG CovCounter
                WITH ADDRESS "AV:0"
                WITH DATA_TYPE "FLOAT"
                WITH COV true
    ```

    Alarm events appear on `bacnet/bms/alarms`. Operator ACK via `bacnet/bms/alarms/ack`.
  </Tab>

  <Tab title="Per-tag">
    Each alarming point gets its own MQTT alarm stream and ACK topic:

    ```lot wrap focus={12,17} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
            WITH LOCAL_DEVICE_ID 4194300
        ADD MAPPING LimitPoints
            WITH SOURCE_TOPIC "bacnet/bms"
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH COV true
                WITH ALARM true
            ADD TAG ChillerPressure
                WITH ADDRESS "AI:2"
                WITH DATA_TYPE "FLOAT"
                WITH EVERY 5 SECONDS
                WITH ALARM true
    ```

    | Data topic                   | Alarm topic                         | ACK topic                               |
    | ---------------------------- | ----------------------------------- | --------------------------------------- |
    | `bacnet/bms/RoomTemp`        | `bacnet/bms/RoomTemp/alarms`        | `bacnet/bms/RoomTemp/alarms/ack`        |
    | `bacnet/bms/ChillerPressure` | `bacnet/bms/ChillerPressure/alarms` | `bacnet/bms/ChillerPressure/alarms/ack` |
  </Tab>
</Tabs>

On connect, when alarms are enabled and `LOCAL_DEVICE_ID` is set, the broker sends a **directed I-Am** so Notification Class recipients learn the broker's UDP endpoint.

<Warning>
  Set a non-zero `WITH LOCAL_DEVICE_ID` before enabling alarms. Without it, Event-Notifications are unlikely to arrive because the field device does not know where to send them.
</Warning>

### Active alarm list

In addition to live events on `{alarm_topic}`, the broker maintains a **retained** snapshot on `{alarm_topic}/active`. Subscribe here to build an alarm panel — late subscribers receive the current state immediately without waiting for the next transition.

| Topic                  | Retain  | Use for                                                  |
| ---------------------- | ------- | -------------------------------------------------------- |
| `{alarm_topic}`        | No      | Live transitions as they happen (logging, notifications) |
| `{alarm_topic}/active` | **Yes** | Current open alarms (dashboards, alarm panels)           |

**How the list updates:**

| Event                                 | Effect on `/active`                                        |
| ------------------------------------- | ---------------------------------------------------------- |
| `TO_OFFNORMAL` or `TO_FAULT`          | Add or update the entry for that BACnet `address`          |
| `TO_NORMAL`                           | Remove the entry for that `address`                        |
| Successful ACK on `{alarm_topic}/ack` | Remove the entry matching `address` + `event_state_to_ack` |

Per-tag lists usually hold **0 or 1** entry (one BACnet object per tag). Device-wide lists hold **one entry per alarming object** on the device.

**Example** — device-wide active list on `bacnet/bms/alarms/active`:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "route_name": "BMS",
  "device_id": 1234,
  "alarm_topic": "bacnet/bms/alarms",
  "active_topic": "bacnet/bms/alarms/active",
  "updated_at": "2026-06-16T12:00:00.000Z",
  "count": 2,
  "alarms": [
    {
      "address": "AI:0",
      "tag_name": "RoomTemp",
      "to_state_short": "HIGH_LIMIT",
      "fundamental_state": "OFFNORMAL",
      "transition": "TO_OFFNORMAL",
      "event_state_to_ack": "HIGH_LIMIT",
      "ack_required": true,
      "present_value": 28.5,
      "bacnet_timestamp": "2026-06-10T15:28:21.000Z"
    },
    {
      "address": "AI:2",
      "tag_name": "ChillerPressure",
      "to_state_short": "HIGH_LIMIT",
      "fundamental_state": "OFFNORMAL",
      "transition": "TO_OFFNORMAL",
      "event_state_to_ack": "HIGH_LIMIT",
      "ack_required": false,
      "present_value": 95.2,
      "bacnet_timestamp": "2026-06-10T15:30:05.000Z"
    }
  ]
}
```

Each object in the `alarms` array uses the same fields as a single live alarm event. When no alarms are active, `count` is `0` and `alarms` is `[]` — the broker still publishes valid retained JSON.

### Acknowledging alarms

When a BACnet device raises an alarm, the broker publishes it to MQTT. To clear or acknowledge it on the device, an operator publishes to the **ACK topic**. The broker sends `AcknowledgeAlarm` to the controller and replies with a result.

**How it works:**

1. **Subscribe** to the alarm topic (live events) or `{alarm_topic}/active` (current alarm list).
2. **Publish** your acknowledgement to `{alarm_topic}/ack`.
3. **Check** `{alarm_topic}/ack/result` for `success: true` or `false`.

No additional configuration is needed for ACK — the broker registers the handler automatically when you enable device-wide alarms or per-tag `WITH ALARM true`.

| Topic                      | What you see                                  |
| -------------------------- | --------------------------------------------- |
| `{alarm_topic}`            | Live alarm events as they happen              |
| `{alarm_topic}/active`     | Retained list of alarms that are still active |
| `{alarm_topic}/ack`        | **Publish here** to acknowledge               |
| `{alarm_topic}/ack/result` | Success or failure of your acknowledgement    |

<Tabs>
  <Tab title="Recommended — echo the alarm">
    Copy the alarm JSON you received, add an `ack_text` note, and publish it to the ACK topic. This is the safest approach because you keep all fields the device expects (`event_state_to_ack`, `bacnet_timestamp`, and so on).

    **1. An alarm arrives** on `bacnet/bms/RoomTemp/alarms`:

    ```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    {
      "address": "AI:0",
      "tag_name": "RoomTemp",
      "to_state_short": "HIGH_LIMIT",
      "event_state_to_ack": "HIGH_LIMIT",
      "ack_required": true,
      "present_value": 28.5,
      "bacnet_timestamp": "2026-06-10T15:28:21.000Z"
    }
    ```

    **2. Publish to** `bacnet/bms/RoomTemp/alarms/ack` — same JSON, plus your note:

    ```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    {
      "address": "AI:0",
      "tag_name": "RoomTemp",
      "to_state_short": "HIGH_LIMIT",
      "event_state_to_ack": "HIGH_LIMIT",
      "ack_required": true,
      "present_value": 28.5,
      "bacnet_timestamp": "2026-06-10T15:28:21.000Z",
      "ack_text": "Checked by operator — HVAC team notified"
    }
    ```

    Use any MQTT client (such as MQTT Explorer) to publish the payload above to the ACK topic.

    **3. Result on** `bacnet/bms/RoomTemp/alarms/ack/result`:

    ```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    {
      "success": true,
      "route_name": "BMS",
      "operation": "ACK",
      "address": "AI:0",
      "event_state": "HIGH_LIMIT",
      "timestamp": "2026-06-10T16:30:00.000Z"
    }
    ```

    <Check>
      If `success` is `true`, the device accepted the acknowledgement. The alarm is removed from `{alarm_topic}/active`.
    </Check>
  </Tab>

  <Tab title="Explicit command">
    For scripts or integrations that do not store the full alarm JSON, publish a short text command to the ACK topic.

    **Publish to** `bacnet/bms/RoomTemp/alarms/ack`:

    ```text wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    ACK:AI:0:HIGH_LIMIT:Checked by operator
    ```

    Format: `ACK:OBJECT:INSTANCE:STATE:optional note`

    | Part     | Example               | Meaning                           |
    | -------- | --------------------- | --------------------------------- |
    | Object   | `AI`                  | Analog Input                      |
    | Instance | `0`                   | Object instance number            |
    | State    | `HIGH_LIMIT`          | Active alarm state to acknowledge |
    | Note     | `Checked by operator` | Optional operator comment         |

    Use the **state name** from the alarm (`HIGH_LIMIT`, `LOW_LIMIT`, `FAULT`, or `NORMAL`) — not labels like `OFFNORMAL`.

    **Result on** `bacnet/bms/RoomTemp/alarms/ack/result`:

    ```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    {
      "success": true,
      "route_name": "BMS",
      "operation": "ACK",
      "address": "AI:0",
      "event_state": "HIGH_LIMIT",
      "timestamp": "2026-06-10T16:30:00.000Z"
    }
    ```

    <Warning>
      If `success` is `false`, the state or timestamp may not match what the device expects. Prefer the recommended method when possible — echo the alarm JSON and add `ack_text`.
    </Warning>
  </Tab>
</Tabs>

<AccordionGroup>
  <Accordion title="Field device prerequisites">
    Before expecting MQTT alarms, confirm on the BMS or engineering tool:

    1. **Notification Class** — recipient list includes `LOCAL_DEVICE_ID`
    2. **Intrinsic alarming** on each object — `NOTIFICATION_CLASS`, `HIGH_LIMIT` / `LOW_LIMIT`, `EVENT_ENABLE`, `LIMIT_ENABLE`
    3. **Present value** actually crosses a configured limit

    COV working does **not** guarantee alarms — they use separate BACnet services.
  </Accordion>
</AccordionGroup>

***

## On-Demand Events

Use `ADD EVENT` when you need a single read, write, or alarm acknowledgement triggered by an MQTT publish — instead of continuous TAG polling.

| Parameter           | Required | Description                                              |
| ------------------- | -------- | -------------------------------------------------------- |
| `SOURCE_TOPIC`      | **Yes**  | MQTT topic that triggers the BACnet operation            |
| `DESTINATION_TOPIC` | **Yes**  | MQTT topic where the JSON result is published            |
| `QUERY`             | **Yes**  | BACnet operation: `READ:…`, `WRITE:…`, `ACK:…`, or JSON  |
| `EVERY`             | No       | Not supported on BACnet events — use TAG polling instead |

### Query formats

| Style                 | Example                                                                             | Notes                                                                         |
| --------------------- | ----------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Colon-separated read  | `READ:AI:0:PRESENT_VALUE`                                                           | Shorthand or full type name                                                   |
| Colon-separated write | `WRITE:AV:0:PRESENT_VALUE:72.5`                                                     | Value is everything after the property                                        |
| JSON                  | `{"operation":"READ","object_type":"AI","instance":"0","property":"PRESENT_VALUE"}` | Also supports `"operation":"WRITE"` with `"value"`                            |
| Topic placeholder     | `READ:{topic[0]}:0:PRESENT_VALUE`                                                   | `{topic[0]}` replaced from the matched MQTT topic segment                     |
| Dynamic payload       | `{payload}`                                                                         | Use the MQTT message body as the query (e.g. `WRITE:AV:0:PRESENT_VALUE:21.5`) |

Supported operations: **READ**, **WRITE**, and **ACK** (alarm acknowledgement).

```lot wrap focus={5-9} theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE BMS WITH TYPE BACNET
    ADD BACNET_CONFIG
        WITH IP "192.168.1.10"
        WITH DEVICE_ID 1234
    ADD EVENT ReadProperty
        WITH SOURCE_TOPIC "bacnet/cmd/read"
        WITH DESTINATION_TOPIC "bacnet/cmd/read/result"
        WITH QUERY "READ:AI:0:PRESENT_VALUE"
```

Publish any message to `bacnet/cmd/read` and the result appears on `bacnet/cmd/read/result`:

```json wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{
  "success": true,
  "route_name": "BMS",
  "operation": "READ",
  "address": "AI:0",
  "property": "PRESENT_VALUE",
  "value": 22.5,
  "timestamp": "2026-05-31T10:00:00.000Z"
}
```

<Warning>
  The route must be connected before events run. If an event returns `"success": false`, check that the device is reachable and that the object and property in the query are correct.
</Warning>

### When to use what

| Need                         | Recommended approach                                                       |
| ---------------------------- | -------------------------------------------------------------------------- |
| Periodic telemetry           | `ADD MAPPING` + `ADD TAG` + `WITH EVERY`                                   |
| Write from MQTT              | `WITH WRITABLE true` + `WITH DESTINATION_TOPIC` on a tag                   |
| Ad-hoc read/write on command | `ADD EVENT` + `WITH QUERY`                                                 |
| Alarm acknowledgement        | Auto-registered handler on `{tag_topic}/alarms/ack`, or custom `ADD EVENT` |

***

## Examples

<Tabs>
  <Tab title="Simple polling">
    Reads two analog inputs and one analog value every five seconds:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
        ADD MAPPING RoomSensors
            WITH SOURCE_TOPIC "bacnet/bms"
            WITH EVERY 5 SECONDS
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH UNIT "degC"
            ADD TAG Humidity
                WITH ADDRESS "AI:1"
                WITH DATA_TYPE "FLOAT"
                WITH UNIT "%RH"
            ADD TAG Setpoint
                WITH ADDRESS "AV:0"
                WITH DATA_TYPE "FLOAT"
                WITH UNIT "degC"
    ```

    Publishes to `bacnet/bms/RoomTemp` → `22.5`, `bacnet/bms/Humidity` → `55.0`, `bacnet/bms/Setpoint` → `21.0`.
  </Tab>

  <Tab title="COV subscriptions">
    Subscribe to Change-of-Value for fast-changing points — no polling interval needed:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
            WITH COV_LIFETIME 600
        ADD MAPPING CovPoints
            WITH SOURCE_TOPIC "bacnet/bms/cov"
            ADD TAG RoomTempCov
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
                WITH COV true
            ADD TAG OccupancyStatus
                WITH ADDRESS "BI:0"
                WITH DATA_TYPE "BOOL"
                WITH COV true
    ```
  </Tab>

  <Tab title="Device-wide alarms">
    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
            WITH LOCAL_DEVICE_ID 4194300
            WITH ALARMS true
            WITH ALARMS_TOPIC "bacnet/bms/alarms"
        ADD MAPPING CovPoints
            WITH SOURCE_TOPIC "bacnet/bms"
            ADD TAG CovCounter
                WITH ADDRESS "AV:0"
                WITH DATA_TYPE "FLOAT"
                WITH COV true
    ```

    Alarm events appear on `bacnet/bms/alarms`. Operator ACK via `bacnet/bms/alarms/ack`.
  </Tab>

  <Tab title="Sandbox (Docker)">
    Connects to the IOTech BACnet Docker simulator when the broker runs in another container:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE Sandbox WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "coreflux-route-bacnet"
            WITH PORT 47808
            WITH DEVICE_ID 1234
            WITH DIRECT_IP true
        ADD MAPPING TestPoints
            WITH SOURCE_TOPIC "bacnet/sandbox"
            WITH EVERY 2 SECONDS
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
            ADD TAG AnalogOut
                WITH ADDRESS "AO:0"
                WITH DATA_TYPE "FLOAT"
                WITH WRITABLE true
                WITH DESTINATION_TOPIC "bacnet/sandbox/AnalogOut/set"
    ```
  </Tab>
</Tabs>

***

## Combining with Models and Actions

A BACnet route publishes each point to its own MQTT topic, so the rest of LoT can use that data like any other topic.

<Tabs>
  <Tab title="Aggregate into a model">
    First, the route publishes each point under `bacnet/bms`:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ROUTE BMS WITH TYPE BACNET
        ADD BACNET_CONFIG
            WITH IP "192.168.1.10"
            WITH DEVICE_ID 1234
        ADD MAPPING RoomSensors
            WITH SOURCE_TOPIC "bacnet/bms"
            WITH EVERY 5 SECONDS
            ADD TAG RoomTemp
                WITH ADDRESS "AI:0"
                WITH DATA_TYPE "FLOAT"
            ADD TAG Humidity
                WITH ADDRESS "AI:1"
                WITH DATA_TYPE "FLOAT"
            ADD TAG Setpoint
                WITH ADDRESS "AV:0"
                WITH DATA_TYPE "FLOAT"
    ```

    Then the model aggregates those topics into one object on `bacnet/bms/Info`:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE MODEL BacnetRoomSensors COLLAPSED WITH TOPIC "bacnet/bms/Info"
        ADD DOUBLE "RoomTemp" WITH TOPIC "bacnet/bms/RoomTemp" AS TRIGGER
        ADD DOUBLE "Humidity" WITH TOPIC "bacnet/bms/Humidity"
        ADD DOUBLE "Setpoint" WITH TOPIC "bacnet/bms/Setpoint"
        ADD STRING "TimeStamp" WITH TIMESTAMP "ISO"
    ```
  </Tab>

  <Tab title="Monitor alarm streams">
    An action can watch per-tag alarm topics and forward events:

    ```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
    DEFINE ACTION BmsAlarmMonitor
    ON TOPIC "bacnet/bms/+/alarms" DO
        PUBLISH TOPIC "site/alarm/log" WITH PAYLOAD
        PUBLISH TOPIC "site/alarm/last" WITH PAYLOAD
    ```
  </Tab>
</Tabs>

***

## Best Practices

<AccordionGroup>
  <Accordion title="Always set WITH IP explicitly">
    The default `192.168.1.1` is rarely correct. On Docker or routed networks, broadcast Who-Is may never reach the device. Set `WITH IP` to the controller's address or hostname.
  </Accordion>

  <Accordion title="Use Direct IP for unresponsive controllers">
    If a device does not answer Who-Is/I-Am, set `WITH DIRECT_IP true` to connect to `IP:PORT` and verify with ReadProperty. Otherwise the route waits on discovery timeout every connect.
  </Accordion>

  <Accordion title="Use COV for fast-changing points">
    Polling at one-second intervals wastes bandwidth on BACnet/IP. Use `WITH COV true` for occupancy sensors, fast process variables, and alarm-related points.
  </Accordion>

  <Accordion title="Set LOCAL_DEVICE_ID before enabling alarms">
    The broker must announce itself as a BACnet device via directed I-Am so the field device knows where to send Event-Notifications.
  </Accordion>

  <Accordion title="Choose write priority deliberately">
    If a BMS holds a point at priority 1, a write at priority 8 will not change the present value. Match priority to your application and verify on the device.
  </Accordion>

  <Accordion title="Omit WITH EVERY from COV-only mappings">
    COV-only mappings do not need a polling interval. Adding `WITH EVERY` on a COV-only mapping adds confusion without benefit.
  </Accordion>

  <Accordion title="Use per-tag alarms for production points">
    Per-tag alarms (`WITH ALARM true`) give each monitoring point its own ACK topic. Use device-wide (`WITH ALARMS true`) as a catch-all or for commissioning.
  </Accordion>

  <Accordion title="Echo alarm JSON when acknowledging">
    Include `event_state_to_ack` and `bacnet_timestamp` from the received alarm message. Wrong state or missing timestamp causes ACK to fail.
  </Accordion>
</AccordionGroup>

***

## Troubleshooting

### Connection check

Publish `-checkRouteConnection BMS` to `$SYS/Coreflux/Command`. The broker returns a JSON status for the named route indicating whether the BACnet device was reached.

<AccordionGroup>
  <Accordion title="Discovery and connection issues">
    | Symptom                            | Check                            | Solution                                                           |
    | ---------------------------------- | -------------------------------- | ------------------------------------------------------------------ |
    | Route never connects               | `IP` and `DEVICE_ID` correct?    | Verify device IP; use Wireshark to confirm Who-Is packet sent      |
    | Device not discovered              | Device answers broadcast Who-Is? | Set `WITH DIRECT_IP true` if the device does not respond to Who-Is |
    | Docker: route cannot reach device  | `WITH IP "127.0.0.1"` in Docker? | Use the container hostname (e.g. `coreflux-route-bacnet`)          |
    | Discovery timeout on every connect | `DISCOVERY_TIMEOUT` too low?     | Increase to 10+ seconds for slow devices                           |
  </Accordion>

  <Accordion title="Data issues">
    | Symptom                        | Check                     | Solution                                                          |
    | ------------------------------ | ------------------------- | ----------------------------------------------------------------- |
    | No MQTT values                 | `DEVICE_ID` match device? | Sandbox default is `1234` — confirm with device profile           |
    | Wrong object value             | Instance number correct?  | Verify object index in device BACnet browser or profile           |
    | Write has no effect            | Priority conflict?        | Try `WITH WRITE_PRIORITY 1`; check if BMS holds a higher priority |
    | COV notifications not arriving | Device supports COV?      | Check device profile; simulator may require `--cov` flag          |
  </Accordion>

  <Accordion title="Alarm issues">
    | Symptom                      | Check                                          | Solution                                                                    |
    | ---------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------- |
    | No MQTT alarms (COV works)   | `LOCAL_DEVICE_ID` set?                         | Add non-zero `LOCAL_DEVICE_ID`; verify device Notification Class recipient  |
    | Alarms on wrong topic        | `SOURCE_TOPIC` or `ALARM_TOPIC` misconfigured? | Default is `{SOURCE_TOPIC}/{tagName}/alarms`                                |
    | ACK returns `success: false` | Wrong `event_state_to_ack`?                    | Echo the received alarm JSON with `ack_text` field added                    |
    | Events only after reconnect  | Directed I-Am not sent?                        | Ensure `LOCAL_DEVICE_ID` is set and alarms are enabled before first connect |
  </Accordion>

  <Accordion title="Networking issues">
    | Symptom                          | Solution                                                                                       |
    | -------------------------------- | ---------------------------------------------------------------------------------------------- |
    | `PORT` vs `LOCAL_PORT` confusion | `PORT` is the device's listen port (47808). `LOCAL_PORT` is the broker's bind port (leave `0`) |
    | Event returns `success: false`   | Route not connected, or wrong object/property in `QUERY`; check broker logs                    |
    | `LOCAL_DEVICE_ID is 0` warning   | Set a non-zero device instance unique on the BACnet network                                    |
  </Accordion>
</AccordionGroup>

### Additional resources

* [ASHRAE 135 Standard](https://www.ashrae.org/technical-resources/bookstore/bacnet) — official BACnet standard
* [BACnet International](https://www.bacnetinternational.org/) — conformance testing and product listings

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Industrial Overview" icon="industry" href="./overview">
    Learn the patterns shared across all OT routes.
  </Card>

  <Card title="OPC UA Client" icon="sitemap" href="./opcua-client">
    Connect to cross-platform industrial servers with OPC UA.
  </Card>
</CardGroup>
