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

collector.yaml
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]
NameTypeDescription
deviceslistDevices 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.
pointspathThe points list below, relative to collector.yaml. When set, only these points are collected.
local_addressstring
Default: 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.
bbmdstringThe 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_ttlnumber
Default: 300
Seconds the BBMD keeps the foreign-device registration; the collector renews it.
covbool
Default: 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_devicenumber
Default: 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_confirmedbool
Default: 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_snumber
Default: 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_msnumber
Default: 10000
Interval of polled points that leave poll_ms empty.
rpm_chunknumber
Default: 20
Objects per ReadPropertyMultiple request. Lower it for devices with small APDUs.
timeout_snumber
Default: 5
Per BACnet request (the library retries internally; a request is abandoned after about twice this).
who_is_wait_snumber
Default: 3
How long to listen for I-Am answers after a Who-Is.
max_pointsnumber
Default: 5000
Upper limit of points collected from devices without a points file.
device_instancenumber
Default: 4194301
The device number the collector announces itself with. Change it only if another device already uses it.
timezonestringIANA name (Europe/Madrid) of the devices' clock for Trend Log backfill. Default: the zone of this computer.

The points list

bacnet_points.csv
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.

NameTypeDescription
namestring
Default: device.type.instance
The channel name in Kymoscope. Unique within the source.
devicerequiredstringThe device instance number or its address, as in devices.
typerequiredanalog-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-valueThe BACnet object type.
instancerequirednumberThe object instance number.
propertystring
Default: present-value
Another numeric property, such as cov-increment or relinquish-default. Properties other than present-value are polled.
unitstringShown on the channel. Empty: read from the object's units property (degrees-celsius becomes °C).
covtrue | falseThis point's mode. Empty: the source's cov option.
poll_msnumberThis point's polling interval (when it is polled).
descriptionstringShown 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).

Discovery and backfill
$ 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-08

Quality of the values

The device reportsKymoscope shows
Normal; in-alarmGood (an alarm is not a bad measurement)
Fault flag, or a reliability other than no-fault-detectedBad (a gap): sensor failure for no-sensor, over-range, under-range, open-loop and shorted-loop; device failure otherwise
Out of serviceBad (a gap): the present value is a manual value
OverriddenUncertain: the value is kept, flagged as a substitute
No answer from a polled deviceBad (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. Use cov: false with a poll_ms for 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), backfill and backfill_gaps read 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: set timezone if 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

shell
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

SymptomLikely 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 0The device refuses SubscribeCOV, or notifications are blocked by a firewall (see above). cov_rejected counts refusals.
Address already in useAnother BACnet program holds UDP 47808: set another port in local_address.
Who-Is finds nothing on a computer with several interfacesSet local_address with the mask of the building network (IP/mask:port).
A value never changesCOV sends only changes above the cov-increment. Use cov: false and a poll_ms for a steady rate.
No history for a pointThe device has no Trend Log for it (or the Trend Log lives on another device).