BACnet Trend Log objects record timestamped property values—such as zone temperatures, damper positions, and setpoints—into an on-device buffer that you retrieve using the ReadRange service. To export that data for analysis or compliance reporting, use BAC0 in Python to connect to the device, iterate its trendlogs collection, call await trend.history to get a pandas Series, and then export to CSV or Excel with a single .to_csv() or .to_excel() call.
What Are BACnet Trend Logs
A Trend Log is a standard BACnet object type (Object Type 20) defined in ASHRAE 135 that records historical values of a monitored property over time. Think of it as a data logger embedded inside the controller itself. When a controller has a Trend Log object configured for, say, analogInput:1 presentValue, the controller periodically samples that value, timestamps the reading, and stores the record in an internal buffer. This happens entirely on the controller—no supervisory system or network traffic is required during the logging process.
Each record in the buffer is a BACnetLogRecord containing three elements: a timestamp (BACnetDateTime), a status flags field, and the logged value itself. The value can be a real number, an enumerated state, a boolean, or even a null entry indicating an error or gap. The buffer holds a fixed number of records determined by the Buffer_Size property, and records are retrieved over the network using the ReadRange service—not ReadProperty. This distinction matters because ReadProperty cannot return the ordered, sequenced buffer contents that trend analysis requires.
BACnet also defines a Trend Log Multiple object (Object Type 25) that monitors several properties simultaneously in a single buffer, producing correlated timestamps across multiple points. The principles in this guide apply to both object types, though Trend Log Multiple requires the client to parse multiple values per record.
Configuring Trend Log Objects
Before you can export trend data, the controller must be configured to log it. Trend Log objects are typically created during engineering using the manufacturer's programming tool, but understanding the key BACnet properties helps you verify and troubleshoot the configuration from any BACnet client.
Essential Configuration Properties
- Log_Enable (Boolean) — Master switch for the Trend Log. Logging occurs only when this property is
TRUE. AFALSEvalue overrides all other timing settings. Changes to this property are themselves recorded in the log buffer, so you can see when logging was started or stopped. - Log_DeviceObjectProperty (BACnetDeviceObjectPropertyReference) — Identifies exactly what is being trended. This property contains three fields: the device instance (or omitted if the monitored object is local), the object identifier (e.g.,
analogInput:1), and the property identifier (e.g.,presentValue). This is the "pointer" that tells the Trend Log which value to record. - Logging_Type (Enumerated) — Determines the acquisition method. The three options are
polled(samples at a fixed interval),cov(records on Change of Value), andtriggered(records when an external trigger fires). Polled logging is the most common and predictable for export workflows. - Log_Interval (Unsigned, in centiseconds) — When
Logging_Typeispolled, this property sets the sampling period. A value of 6000 means 60 seconds (6000 centiseconds). Common intervals range from 100 (1 second) for critical measurements to 30000 (5 minutes) for general monitoring. - Start_Time / Stop_Time (BACnetDateTime) — Optional time window for logging. If both are set to wildcard values (0xFF in all fields), logging runs continuously whenever
Log_EnableisTRUE. For compliance scenarios, you can set specific start and stop times to capture a defined test period. - Buffer_Size (Unsigned) — Maximum number of records the buffer can hold. This is a read-only property reflecting the controller's memory allocation. Typical values range from 100 records on small unitary controllers to 50,000 or more on supervisory controllers.
- Stop_When_Full (Boolean) — Controls buffer behavior when capacity is reached. When
TRUE, logging halts once the buffer fills—useful for capturing a fixed-duration test without overwriting. WhenFALSE(the default on most controllers), the buffer operates as a circular ring, overwriting the oldest records with new ones. - Record_Count (Unsigned) — The current number of valid records in the buffer. Writing zero to this property clears the buffer.
Verifying Configuration from a BACnet Client
You do not need the manufacturer's engineering tool to check these properties. Any BACnet explorer or scripting library can read them. Using BAC0:
import BAC0
import asyncio
async def check_trend_config():
async with BAC0.start(ip="192.168.1.10/24") as bacnet:
addr = "192.168.1.100"
# Read key Trend Log properties
enabled = await bacnet.read(f"{addr} trendLog 1 logEnable")
log_type = await bacnet.read(f"{addr} trendLog 1 loggingType")
interval = await bacnet.read(f"{addr} trendLog 1 logInterval")
buf_size = await bacnet.read(f"{addr} trendLog 1 bufferSize")
rec_count = await bacnet.read(f"{addr} trendLog 1 recordCount")
stop_full = await bacnet.read(f"{addr} trendLog 1 stopWhenFull")
print(f"Log Enable: {enabled}")
print(f"Logging Type: {log_type}")
print(f"Log Interval: {interval} centiseconds")
print(f"Buffer Size: {buf_size} records")
print(f"Record Count: {rec_count}")
print(f"Stop When Full: {stop_full}")
asyncio.run(check_trend_config())Reading Trend Logs with BAC0 Python
BAC0 provides first-class support for BACnet Trend Log objects. When you define a device using BAC0.device(), BAC0 automatically discovers all Trend Log objects on the controller and exposes them through the device.trendlogs collection. Each trend log entry wraps the ReadRange service calls and returns the buffered data as a pandas Series with a DateTimeIndex.
Discovering Available Trend Logs
import BAC0
import asyncio
async def list_trend_logs():
async with BAC0.start(ip="192.168.1.10/24") as bacnet:
# Connect to device instance 1234 at 192.168.1.100
controller = await BAC0.device(
"192.168.1.100", 1234, bacnet
)
# Iterate all discovered trend logs
for name, trend in controller.trendlogs.items():
props = trend.properties
print(
f"TrendLog: {name} | "
f"Object: {props.name} | "
f"Description: {props.description}"
)
asyncio.run(list_trend_logs())Retrieving Trend Log History
The trend.history property is asynchronous and returns a pandas Series. BAC0 handles the ReadRange requests, pagination across large buffers, and timestamp parsing internally.
import BAC0
import asyncio
async def read_trend_data():
async with BAC0.start(ip="192.168.1.10/24") as bacnet:
controller = await BAC0.device(
"192.168.1.100", 1234, bacnet
)
# Get a specific trend log by name
trend = controller.trendlogs["ZN-T-TrendLog"]
# Retrieve the full history as a pandas Series
history = await trend.history
print(history)
print(f"Records: {len(history)}")
print(f"First: {history.index[0]}")
print(f"Last: {history.index[-1]}")
print(f"Mean: {history.mean():.2f}")
asyncio.run(read_trend_data())Important: The trend.history call reads records directly from the controller's buffer. This is different from BAC0's live polling history (controller["pointName"].history), which stores values captured by BAC0 itself during the current session. Trend Log history retrieves data that the controller recorded independently—potentially days or weeks of data that existed before your script even started.
Exporting Trend Data to CSV
Because BAC0 returns trend data as pandas Series and DataFrames, exporting is straightforward. pandas supports CSV, Excel, JSON, Parquet, and dozens of other formats natively.
Single Trend Log to CSV
import BAC0
import asyncio
import pandas as pd
async def export_single_trend():
async with BAC0.start(ip="192.168.1.10/24") as bacnet:
controller = await BAC0.device(
"192.168.1.100", 1234, bacnet
)
trend = controller.trendlogs["ZN-T-TrendLog"]
history = await trend.history
# Export directly to CSV
history.to_csv("zone_temp_trend.csv", header=True)
print(f"Exported {len(history)} records to zone_temp_trend.csv")
asyncio.run(export_single_trend())Multiple Trend Logs to a Single CSV
async def export_multiple_trends():
async with BAC0.start(ip="192.168.1.10/24") as bacnet:
controller = await BAC0.device(
"192.168.1.100", 1234, bacnet
)
# Collect multiple trend logs into a DataFrame
trend_data = {}
for name, trend in controller.trendlogs.items():
history = await trend.history
trend_data[name] = history
df = pd.DataFrame(trend_data)
# Resample to uniform 1-minute intervals for alignment
df_resampled = df.resample("1min").mean().dropna()
df_resampled.to_csv("all_trends_1min.csv")
print(f"Exported {len(df_resampled)} rows across "
f"{len(df_resampled.columns)} trend logs")
asyncio.run(export_multiple_trends())Exporting to Excel with Multiple Sheets
For compliance deliverables where each controller gets its own workbook, export multiple devices to separate sheets in a single Excel file:
# Requires: pip install openpyxl
async def export_to_excel(device_list):
async with BAC0.start(ip="192.168.1.10/24") as bacnet:
with pd.ExcelWriter("trend_report.xlsx") as writer:
for addr, instance, sheet in device_list:
ctrl = await BAC0.device(addr, instance, bacnet)
trend_data = {}
for name, trend in ctrl.trendlogs.items():
trend_data[name] = await trend.history
df = pd.DataFrame(trend_data)
df.to_excel(writer, sheet_name=sheet)
print(f"Wrote sheet: {sheet}")
devices = [
("192.168.1.100", 1234, "AHU-1"),
("192.168.1.101", 1235, "AHU-2"),
("192.168.1.102", 1236, "VAV-101"),
]
asyncio.run(export_to_excel(devices))Trend Log Buffer Management
Understanding how the Trend Log buffer works is critical for reliable data collection. The buffer is a fixed-size array in the controller's memory, and its behavior depends on the Stop_When_Full property and the Buffer_Size allocation.
Circular vs. Fixed Buffers
When Stop_When_Full is FALSE (the default), the buffer operates as a circular ring. Once all slots are filled, the next record overwrites the oldest record. This means the buffer always contains the most recent N records, where N equals Buffer_Size. For ongoing monitoring, this is the correct mode—you never lose current data, though old data is silently discarded.
When Stop_When_Full is TRUE, logging halts once the buffer reaches capacity. No data is overwritten, but no new data is captured either. This mode is useful for bounded tests—for example, recording a 24-hour commissioning functional test where you need every single sample preserved. After the test, export the data and then clear the buffer by writing zero to Record_Count.
Calculating Buffer Duration
Before deploying trend logs for a compliance test, calculate how long the buffer will last at your configured interval:
Buffer_Size = 5000 records
Log_Interval = 6000 centiseconds (60 seconds)
Duration = Buffer_Size * (Log_Interval / 100) seconds
Duration = 5000 * 60 = 300,000 seconds
Duration = 83.3 hours (approximately 3.5 days)If you need 7 days of data at 60-second intervals, you need at least 10,080 buffer slots. If the controller only supports 5,000, you have three options: increase the log interval to 120 seconds (which doubles your coverage to 7 days), export the buffer periodically before it wraps, or use a supervisory system that polls and archives the trend data to its own storage.
Clearing the Buffer
To reset a trend log buffer, write zero to the Record_Count property. This clears all stored records and resets the Total_Record_Count. In BAC0:
# Clear the trend log buffer on the controller
await bacnet._write(
"192.168.1.100 trendLog 1 recordCount 0"
)Always export your data before clearing the buffer. Once zeroed, the records are gone permanently—there is no undo.
BUFFER_READY Notifications
BACnet Trend Log objects support intrinsic event reporting. When the Notification_Threshold property is configured, the controller sends a BUFFER_READY event notification to subscribed recipients once the specified number of new records accumulate. This allows a supervisory system to retrieve data on demand rather than on a fixed timer, reducing unnecessary network traffic and ensuring no records are missed between polling cycles.
Common BACnet Trending Mistakes
- Using ReadProperty instead of ReadRange to retrieve buffer data. The
Log_Bufferproperty of a Trend Log object is not accessible through the standard ReadProperty service. The BACnet specification requires the ReadRange service for sequential buffer access. If your BACnet client returns an error or empty result when you try to read the log buffer, confirm that it supports ReadRange. BAC0 handles this automatically, but lower-level tools or custom integrations may not. - Setting the log interval too aggressively on MS/TP trunks. Configuring a 1-second log interval on a controller behind an MS/TP network running at 38,400 baud works fine for the logging itself (it happens locally on the controller). The problem arises when you try to retrieve the data. An MS/TP trunk can only transfer approximately 10–20 ReadRange responses per second. A buffer with 86,400 one-second records (24 hours) could take over an hour to download through an MS/TP router. Use 60-second or longer intervals unless you genuinely need sub-minute resolution, and plan for longer retrieval times on serial links.
- Forgetting that circular buffers silently overwrite old data. With
Stop_When_Fullset toFALSE, the controller happily overwrites the oldest records once the buffer is full. If you configure a 5,000-record buffer with a 60-second interval and only export data once a week, you lose the first 3.5 days of data every cycle. Either increase the buffer size, reduce the log interval, or schedule automated exports before the buffer wraps. - Not accounting for controller clock drift. Trend Log timestamps come from the controller's internal real-time clock, which can drift several minutes per month on inexpensive controllers. When you correlate trend data from multiple controllers, the timestamps may not align even though the events occurred simultaneously. Synchronize controller clocks using BACnet's TimeSynchronization service or NTP before starting a compliance test.
- Confusing BAC0 live polling history with Trend Log history. BAC0 maintains two separate types of historical data. The
controller["pointName"].historyproperty contains values that BAC0 itself polled during the current script session. Thecontroller.trendlogs["name"].historyproperty retrieves records from the controller's on-device Trend Log buffer. The first exists only while your script is running and starts empty. The second contains data the controller recorded independently, potentially over days or weeks. For compliance reporting, you almost always want the Trend Log data, not the live polling data.
Platform Compatibility
BACnet Trend Log objects are defined in ASHRAE 135 and implemented across virtually all modern BAS controllers. The ability to read them depends on the client supporting the ReadRange service. The following table summarizes common platforms and their Trend Log support:
| Platform / Tool | Trend Log Support | Notes |
|---|---|---|
| BAC0 (Python) | Read and export | Requires Python 3.10+, pandas for history export. Handles ReadRange internally. |
| YABE (Yet Another BACnet Explorer) | Read and graph | Free Windows tool. Can display trend log contents graphically. No bulk CSV export. |
| Tridium Niagara (JACE / Supervisor) | Full (read, write, archive) | Can import BACnet Trend Logs into Niagara histories for long-term archival and export. |
| Johnson Controls Metasys (NAE / NCE) | Full (read, write, archive) | Trend Log data viewable in Metasys UI and exportable to CSV through the Trend Viewer. |
| Schneider Electric EcoStruxure Building Operation | Full (read, write, archive) | Trend Log list export to CSV available in the WorkStation interface. |
| Siemens Desigo CC | Full (read, write, archive) | Trend data accessible through the Desigo CC trend viewer and reporting engine. |
| WinCC OA | Read via ReadRange | BACnet driver supports TrendLog and TrendLogMultiple via Log_Buffer property reads. |
| Honeywell Niagara-based (CIPer, Spyder) | Read and archive | Controllers support on-device Trend Logs; supervisory level archives via Niagara histories. |
| Distech Controls ECLYPSE | Full (read, write, archive) | BACnet Trend Log objects configurable through the ECLYPSE web interface or Envysion. |
BACnet transport note: Trend Log objects exist on the controller regardless of the data link layer. If the controller sits on an MS/TP trunk behind a BACnet router, the data is still accessible from a BACnet/IP client—the router translates ReadRange requests transparently. However, retrieval speed is limited by the MS/TP baud rate (typically 38,400 or 76,800 baud). Plan for longer download times when pulling large buffers through MS/TP routers.
Python environment: BAC0 requires Python 3.10+ and depends on BACpypes3 for protocol handling. Install pandas alongside BAC0 for history and export support. BAC0 runs on Windows, Linux, and macOS.
Source Attribution
This guide draws on technical documentation from the following sources:
- BAC0 Documentation — Histories and Trending — official documentation for BAC0's trend log and history features (LGPL-3.0)
- Chipkin Automation Systems — What is a BACnet Trend Log Object — overview of Trend Log properties, buffer behavior, and configuration
- WinCC OA Documentation — BACnet TrendLog — ReadRange service usage and buffer reading mechanics
- David Fisher — Tutorial: Trend Log and Trend Log Multiple — in-depth technical paper on BACnet Trend Log object behavior and ReadRange sequencing
- ASHRAE Standard 135 — BACnet: A Data Communication Protocol for Building Automation and Control Networks (Clause 12.25: Trend Log Object Type)
Was this article helpful?
Related Articles
Need to do this remotely? SiteConduit provides Layer 2 access that preserves BACnet broadcasts — no BBMD needed for remote sessions. Join the waitlist.
SiteConduit Technical Team
Idea Networks Inc.
SiteConduit builds managed remote access for building automation. Our knowledge base is maintained by BAS professionals with hands-on experience deploying and troubleshooting BACnet, Niagara, Modbus, and Facility Explorer systems.