Live streams
Collector: BACnet/IP
Read building-automation devices over BACnet/IP: change-of-value subscriptions, polling, Who-Is discovery and Trend Log backfill.
BACnet/IP (building automation)
BACnet/IP is the standard of building management: HVAC controllers, chillers, meters and lighting. The collector acts as a read-only BACnet client. It reads points on a timer and, for the points you list in the points file, asks the device to send a value when it changes (a Change-Of-Value subscription, SubscribeCOV). It never writes a point or a setting, but a COV subscription is an entry in the device's own subscription list, and a device has a limited number of them. The collector therefore subscribes only the points of the points file (at most max_cov_per_device per device) and cancels them when it stops; points found by discovery are polled. Set cov: false to use no subscriptions at all. It uses the bacpypes3 library (MIT licence).
Configuration
sources:
- name: Building HVAC
type: bacnet
devices: [1001, 1002] # device instance numbers (found with Who-Is) or addresses
cov_lifetime_s: 300
- name: Boiler room (listed points)
type: bacnet
points: bacnet_points.csv
- name: Another subnet (through a BBMD)
type: bacnet
local_address: 10.20.0.15/24:47808
bbmd: 10.10.0.2:47808 # the BBMD on the building network: register as a foreign device
bbmd_ttl: 300
devices: [2001]| Name | Type | Description |
|---|---|---|
devices | list | Devices to read: a device instance number (found with a Who-Is) or an address (10.0.0.5 or 10.0.0.5:47808). Without a points file, every analog, binary and multi-state object of these devices is collected. With neither devices nor points, every device that answers a Who-Is is collected. |
points | path | The points list below, relative to collector.yaml. When set, only these points are collected. |
local_address | stringDefault: host | The interface and UDP port the collector uses: host (the main interface, port 47808), an IP address, or IP/mask:port such as 192.168.1.5/24:47808. Use the mask form on a machine with several interfaces: the mask sets the broadcast address of Who-Is. |
bbmd | string | The address of a BBMD (BACnet broadcast management device), with an optional :port. The collector registers as a foreign device, so Who-Is and I-Am cross routers. Needed when the devices are on another IP subnet. |
bbmd_ttl | numberDefault: 300 | Seconds the BBMD keeps the foreign-device registration; the collector renews it. |
cov | boolDefault: true | Subscribe to changes for the points of the points file (a point's own cov column wins). Discovered points are always polled. false polls every point and leaves the devices' subscription lists untouched. |
max_cov_per_device | numberDefault: 16 | Most subscriptions per device. Further points of that device are polled instead (counted in cov_capped, logged once per device), so the slots the building system's own front end uses stay free. |
cov_confirmed | boolDefault: true | Confirmed notifications: the collector acknowledges each one, so the device knows when it is gone. false uses unconfirmed notifications (less traffic, may be lost). |
cov_lifetime_s | numberDefault: 300 | Seconds a subscription lasts. The collector renews it at half the lifetime; a device that forgets it (a restart) or refuses a renewal falls back to polling and is subscribed again. |
poll_ms | numberDefault: 10000 | Interval of polled points that leave poll_ms empty. |
rpm_chunk | numberDefault: 20 | Objects per ReadPropertyMultiple request. Lower it for devices with small APDUs. |
timeout_s | numberDefault: 5 | Per BACnet request (the library retries internally; a request is abandoned after about twice this). |
who_is_wait_s | numberDefault: 3 | How long to listen for I-Am answers after a Who-Is. |
max_points | numberDefault: 5000 | Upper limit of points collected from devices without a points file. |
device_instance | numberDefault: 4194301 | The device number the collector announces itself with. Change it only if another device already uses it. |
timezone | string | IANA name (Europe/Madrid) of the devices' clock for Trend Log backfill. Default: the zone of this computer. |
The points list
name,device,type,instance,property,unit,cov,poll_ms,description
Supply air temp,1001,analog-input,1,,,,,
Fan status,1001,binary-input,3,,,,,
Mode,1001,multi-state-value,2,,,,,
Setpoint,10.0.0.5:47808,analog-value,12,,degC,false,30000,Polled every 30 s
Heating valve,1001,analog-output,4,,,,,Required: device, type and instance. The other columns are optional.
| Name | Type | Description |
|---|---|---|
name | stringDefault: device.type.instance | The channel name in Kymoscope. Unique within the source. |
devicerequired | string | The device instance number or its address, as in devices. |
typerequired | analog-input analog-output analog-value binary-input binary-output binary-value multi-state-input multi-state-output multi-state-value integer-value positive-integer-value large-analog-value | The BACnet object type. |
instancerequired | number | The object instance number. |
property | stringDefault: present-value | Another numeric property, such as cov-increment or relinquish-default. Properties other than present-value are polled. |
unit | string | Shown on the channel. Empty: read from the object's units property (degrees-celsius becomes °C). |
cov | true | false | This point's mode. Empty: the source's cov option. |
poll_ms | number | This point's polling interval (when it is polled). |
description | string | Shown on the channel. Empty: the object's description property. |
Without a points file, discover asks each device for its object list and names, units and state texts (bounded by max_points, with timeouts) and writes them as a tag list; the channel is named device name - object name. Numbers are floats for analog objects, 0 or 1 for binary objects (the active and inactive texts become labels) and the state number for multi-state objects (the state texts become labels).
$ kymoscope-collector discover --config collector.yaml --source "Building HVAC" --out bacnet_tags.csv
$ kymoscope-collector backfill --config collector.yaml --source "Building HVAC" --from 2026-10-01 --to 2026-10-08Quality of the values
| The device reports | Kymoscope shows |
|---|---|
| Normal; in-alarm | Good (an alarm is not a bad measurement) |
| Fault flag, or a reliability other than no-fault-detected | Bad (a gap): sensor failure for no-sensor, over-range, under-range, open-loop and shorted-loop; device failure otherwise |
| Out of service | Bad (a gap): the present value is a manual value |
| Overridden | Uncertain: the value is kept, flagged as a substitute |
| No answer from a polled device | Bad (a gap) until it answers again |
Change of value, polling and history
- A COV notification arrives only when the value changes by more than the device's
cov-increment(analog) or changes state (binary, multi-state), so a steady point yields few samples. BACnet carries no time stamp: the collector stamps the time it received the notification. Usecov: falsewith apoll_msfor a steady rate. - Devices have a limited number of subscriptions. If one refuses (or has run out), that point is polled with ReadPropertyMultiple instead, and subscribed again after
cov_lifetime_s. A device without ReadPropertyMultiple is read one property at a time. - Backfill: if a device logs a point in a Trend Log object (its log-device-object-property names the point),
backfillandbackfill_gapsread the log with ReadRange, page by page, for the requested period. The point's Trend Log must be on the same device. Trend Log time stamps are the device's local clock: settimezoneif it differs from the collector's. A point with no Trend Log has no history.
Network and firewall
Routed networks. A broadcast does not cross an IP router. For devices on another subnet either list them by address in devices (the Who-Is is then sent straight to them), or set bbmd to the BBMD of that network: the collector registers there as a foreign device and the BBMD forwards broadcasts and notifications. Ask the building contractor for the BBMD address and to allow foreign-device registrations from the collector (a BBMD may need to be set up to accept them).
Install
pip install "kymoscope-collector[bacnet]"Health
connected, devices_seen, cov_active (subscriptions in force), cov_renewals, cov_rejected (subscriptions refused or lost, then polled), cov_capped (points polled because the device is at max_cov_per_device), polls, poll_errors, overruns, values_per_s and history_values (read from Trend Logs).
Troubleshooting
| Symptom | Likely cause |
|---|---|
| "device N did not answer Who-Is" | A routed subnet (use the device's address or a bbmd), a wrong instance number, or UDP 47808 blocked in either direction. |
Polled values arrive, cov_active stays 0 | The device refuses SubscribeCOV, or notifications are blocked by a firewall (see above). cov_rejected counts refusals. |
| Address already in use | Another BACnet program holds UDP 47808: set another port in local_address. |
| Who-Is finds nothing on a computer with several interfaces | Set local_address with the mask of the building network (IP/mask:port). |
| A value never changes | COV sends only changes above the cov-increment. Use cov: false and a poll_ms for a steady rate. |
| No history for a point | The device has no Trend Log for it (or the Trend Log lives on another device). |