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.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 EVENTwithREAD,WRITE, andACKqueries 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.
- Poll sensor values
- Read and write
- Updates on change
- Connect directly
- On-demand read/write
- Per-tag alarms
Connect to a controller and read a room temperature every five seconds:
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 |
Coreflux supports BACnet/IP (UDP). MS/TP parameters are parsed but the transport is not validated. Use BACnet/IP for all production deployments.
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
OnlyDEVICE_ID is strictly required. For a working route, also set IP and add at least one ADD MAPPING with tags.
All configuration options
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 |
Direct IP mode
Use when the controller does not respond to Who-Is/I-Am but accepts ReadProperty at a known UDP endpoint: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 seedsAO:0 at priority 1 — use WITH WRITE_PRIORITY 1 for that test device.
TAG System
Mapping parameters
| 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 |
SOURCE_TOPIC on each tag instead of on the mapping, but one of the two is required.
Tag parameters
| 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 |
ADDRESS_TYPE is the BACnet property (e.g. PRESENT_VALUE, OBJECT_NAME), not the object type.Change-of-Value (COV)
Tags withWITH 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.
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.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 itsLOCAL_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):
| 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 |
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 |
- Device-wide
- Per-tag
Publish every Event-Notification from the device to a single MQTT topic — useful for commissioning and catch-all monitoring:Alarm events appear on
bacnet/bms/alarms. Operator ACK via bacnet/bms/alarms/ack.LOCAL_DEVICE_ID is set, the broker sends a directed I-Am so Notification Class recipients learn the broker’s UDP endpoint.
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) |
| 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 |
bacnet/bms/alarms/active:
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 sendsAcknowledgeAlarm to the controller and replies with a result.
How it works:
- Subscribe to the alarm topic (live events) or
{alarm_topic}/active(current alarm list). - Publish your acknowledgement to
{alarm_topic}/ack. - Check
{alarm_topic}/ack/resultforsuccess: trueorfalse.
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 |
- Recommended — echo the alarm
- Explicit command
Copy the alarm JSON you received, add an 2. Publish to Use any MQTT client (such as MQTT Explorer) to publish the payload above to the ACK topic.3. Result on
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:bacnet/bms/RoomTemp/alarms/ack — same JSON, plus your note:bacnet/bms/RoomTemp/alarms/ack/result:If
success is true, the device accepted the acknowledgement. The alarm is removed from {alarm_topic}/active.Field device prerequisites
Field device prerequisites
Before expecting MQTT alarms, confirm on the BMS or engineering tool:
- Notification Class — recipient list includes
LOCAL_DEVICE_ID - Intrinsic alarming on each object —
NOTIFICATION_CLASS,HIGH_LIMIT/LOW_LIMIT,EVENT_ENABLE,LIMIT_ENABLE - Present value actually crosses a configured limit
On-Demand Events
UseADD 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) |
bacnet/cmd/read and the result appears on bacnet/cmd/read/result:
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
- Simple polling
- COV subscriptions
- Device-wide alarms
- Sandbox (Docker)
Reads two analog inputs and one analog value every five seconds:Publishes to
bacnet/bms/RoomTemp → 22.5, bacnet/bms/Humidity → 55.0, bacnet/bms/Setpoint → 21.0.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.- Aggregate into a model
- Monitor alarm streams
First, the route publishes each point under Then the model aggregates those topics into one object on
bacnet/bms:bacnet/bms/Info:Best Practices
Always set WITH IP explicitly
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.Use Direct IP for unresponsive controllers
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.Use COV for fast-changing points
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.Set LOCAL_DEVICE_ID before enabling alarms
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.
Choose write priority deliberately
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.
Omit WITH EVERY from COV-only mappings
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.Use per-tag alarms for production points
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.Echo alarm JSON when acknowledging
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.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.
Discovery and connection issues
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 |
Data issues
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 |
Alarm issues
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 |
Networking issues
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 |
Additional resources
- ASHRAE 135 Standard — official BACnet standard
- BACnet International — conformance testing and product listings
Next Steps
Industrial Overview
Learn the patterns shared across all OT routes.
OPC UA
Connect to cross-platform industrial servers with OPC UA.

