The Trend Log object is the standard BACnet mechanism for local data logging.
It watches a single property (any object, local or remote, referenced via
log_device_object_property) and records timestamped samples into a circular or
growing buffer. Logging can be triggered periodically (log_interval), on COV of
the monitored property, or by an explicit trigger.
The log supports clock-aligned start times, a logging window (start/stop time),
enable/disable, and a buffer_size that determines how many records are kept
before old ones are overwritten. Records are retrieved with the
BACnet.Protocol.Services.ReadRange service.
When intrinsic_reporting: true the object can emit a BUFFER_READY event so that
a workstation knows new trend data is available for harvesting.
Object Description (ASHRAE 135)
A Trend Log object monitors a property of a referenced object and, when predefined conditions are met, saves ("logs") the value of the property and a timestamp in an internal buffer for subsequent retrieval.
Trend Log objects that support intrinsic reporting shall apply the BUFFER_READY event algorithm.
Behaviour and Operation
Trend Log objects are active data loggers. A background task (driven by a timer
or by COV notifications from the monitored property) periodically or on trigger
reads the property referenced by log_device_object_property (local or remote)
and appends a BACnet.Protocol.LogRecord to the internal buffer.
The application / stack must implement the actual sampling and buffering logic
(respecting log_interval, start_time/stop_time, stop_when_full, buffer
size, etc.). Clients retrieve historical records with ReadRange.
When intrinsic_reporting: true the log can emit a BUFFER_READY notification
when the number of records since last notification reaches notification_threshold,
telling a client that new trend data is ready to be harvested.
Developer Implementation Notes (geared to device server / application authors)
The generated code handles storage + basic mechanics (validation, implicit_relationships,
readonly annotations as hints to your server, etc.). You must drive "special" live
properties and side effects yourself, analogous to maintaining present_value on
inputs via update_property/3 (never direct mutation). Read notes below + generated
tables for details.
Special / live properties and expected developer behaviour
log_device_object_property: The property (local or remote) being logged. Dev must: Your logging engine task must read its current value (ReadProperty or COV sub for remote) on the schedule dictated by logging_type / log_interval / COV, and include theEncoding+ status in theLogRecord.log_buffer: The circular or append-only history. Dev must: The core of the engine. On each sample, buildLogRecord, thenupdate_property/3the buffer (append/overwrite respectingstop_when_full) or manage buffer yourself and write whole on change. The object enforces size, enable, start/stop windows.record_count,total_record_count: Counters. Dev must: Bumprecord_counton appends; total is ever-increasing. Special update paths allow resettingrecord_count=0 in some cases.trigger: Manual sample trigger (for triggeredlogging_type). Dev must: BACnet client writestrueto force an immediate sample + log.logging_type,log_interval,cov_resubscription_interval,client_cov_increment,align_intervals/interval_offset: Sampling config. Dev must: Your task reacts to changes. Re-schedule timers or COV subs. For aligned, compute sample times from midnight + offset.buffer_size,stop_when_full,enable,start_time/stop_time: Logging control. Dev must: Respect in your sampler (don't log if disabled or outside time window).notification_threshold+ intrinsic event fields: For BUFFER_READY. Dev must: On every append, manage the "since last" counter; when it hits threshold, emit BUFFER_READY notification and reset. Tells clients "new data to harvest with ReadRange".reliability,status_flags: Source or buffer problems (e.g. full +stop_when_full). Dev must: Set when referenced properties unreachable or other issues. Note thatin_alarm/fault/out_of_servicebits ofstatus_flagsare auto-managed by the object.
Trend Log (and TrendLogMultiple) are the standard way to keep a local circular or append-only history of any property so that a workstation can later harvest it with ReadRange without having to poll 24/7.
You own the logging engine: Nothing in the object automatically samples. You must have a task that:
- wakes on a timer (for
log_interval/ polled mode) or is notified when the watched property changes (for COV-triggered or "on change" logging_type) - reads the current value of the (possibly remote)
log_device_object_propertyand builds aLogRecord. - Update
record_countproperty and run the event algorithm, if intrinsic. - Maintain the buffer yourself and make it available through ReadRange service.
Clock-aligned logging: If the align_intervals flag (or equivalent) is set
you must compute the next sample time as a multiple of the interval from
midnight / from the interval_offset. This is so that logs from many devices
line up nicely when plotted.
BUFFER_READY intrinsic event: When intrinsic_reporting: true the object
carries a notification_threshold and the normal event fields. Every time you
append a record you (or a central event helper) must increment a "records since
last notification" counter. When it crosses the threshold you emit a
BUFFER_READY event (using the object's own notification_class) and reset the
counter. This tells a historian "there is new data worth harvesting with
ReadRange" without the historian having to poll the enable / record count
fields.
Remote logging source: When the reference points at another device you must perform the ReadProperty (or subscribe to COVs on the remote property) on the schedule the log wants. If the remote read fails you should still append a record with a null / error value and the appropriate status bits so that the gap is visible in the history.
Trend logs are one of the heaviest users of the "active object + your engine drives the side effects" pattern. Treat the object as a smart configuration + buffer container; the real work (sampling, time keeping, buffer management, BUFFER_READY emission) lives in your code.
The BACnet.Stack.TrendLogger module does a lot of this work.
Intrinsic Reporting
When intrinsic_reporting: true is passed to create/4, BUFFER_READY intrinsic
reporting is enabled (notifications when the buffer reaches notification_threshold).
Examples
Creating a basic Trend Log (polled logging):
iex> alias BACnet.Protocol.{DeviceObjectPropertyRef, ObjectIdentifier}
iex> ref = %DeviceObjectPropertyRef{object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1}, property_identifier: :present_value, property_array_index: nil, device_identifier: nil}
iex> {:ok, tl} = BACnet.Protocol.ObjectTypes.TrendLog.create(900, "TempTrend", %{log_device_object_property: ref, log_interval: 60, buffer_size: 100, logging_type: :polled}); tl.object_name
"TempTrend"With clock-aligned logging and intrinsic reporting:
iex> alias BACnet.Protocol.{DeviceObjectPropertyRef, ObjectIdentifier}
iex> ref = %DeviceObjectPropertyRef{object_identifier: %ObjectIdentifier{type: :analog_input, instance: 1}, property_identifier: :present_value, property_array_index: nil, device_identifier: nil}
iex> {:ok, tl} = BACnet.Protocol.ObjectTypes.TrendLog.create(901, "AlignedTrend", %{log_device_object_property: ref, log_interval: 300, buffer_size: 50, logging_type: :polled, align_intervals: false, interval_offset: 0}, intrinsic_reporting: true, clock_aligned_logging: true); tl.object_name
"AlignedTrend"See Also
BACnet.Protocol.LogRecordBACnet.Protocol.EventAlgorithms.BufferReadyBACnet.Protocol.Services.ReadRangeBACnet.Stack.TrendLogger
The following part has been automatically generated.
Click to expand
This module defines a BACnet object of the type `trend_log`. The following properties are defined: | Property | Revision | Required | Readonly | Protected | Intrinsic | |----------|----------|----------|----------|-----------|-----------| | acked_transitions | | | X | | X | | align_intervals | | | | | | | buffer_size | | X | X | | | | client_cov_increment | | | | | | | cov_resubscription_interval | | | | | | | description | | | | | | | enable | | X | | | | | event_algorithm_inhibit | | | | | X | | event_algorithm_inhibit_ref | | | | | X | | event_detection_enable | | | | | X | | event_enable | | | | | X | | event_message_texts | | | X | | X | | event_message_texts_config | | | | | X | | event_state | | X | | | | | event_timestamps | | | X | | X | | interval_offset | | | | | | | last_notify_record | | | | | X | | limit_enable | | | | | X | | log_buffer | | X | | | | | log_device_object_property | | X | | | | | log_interval | | X | | | | | logging_type | | X | | | | | notification_class | | | | | X | | notification_threshold | | | | | X | | notify_type | | | | | X | | object_instance | | X | X | | | | object_name | | X | X | | | | profile_location | 19 | | | | | | profile_name | | | | | | | record_count | | X | | | | | records_since_notification | | | | | X | | reliability | | | | | | | reliability_evaluation_inhibit | | | | | | | start_time | | X | | | | | status_flags | | X | X | | | | stop_time | | X | | | | | stop_when_full | | X | | | | | tags | 19 | | | | | | time_delay | | | | | X | | time_delay_normal | | | | | X | | total_record_count | | X | X | | | | trigger | | | | | | The following properties have additional semantics: | Property | Has Default | Has Init | Implicit Relationships | Validators | Annotations | |----------|-------------|----------|------------------------|------------|-------------| | align_intervals | | | | | `required_when: {:opts, :clock_aligned_logging}` | | buffer_size | | | | Fun/Type | | | cov_resubscription_interval | | | | Type | | | enable | X | | | | | | event_algorithm_inhibit_ref | | | event_algorithm_inhibit | | | | event_state | X | | | | | | interval_offset | | | | | `required_when: {:opts, :clock_aligned_logging}` | | last_notify_record | X | | | Type | | | log_buffer | X | | | Fun | | | logging_type | X | | | | | | notification_threshold | X | | | Type | | | profile_location | | | | Fun | `revision: 19` | | record_count | X | | | Type | | | records_since_notification | X | | | Type | | | reliability | | | reliability_evaluation_inhibit | | | | start_time | X | | | | | | stop_time | X | | | | | | tags | | | | | `revision: 19` | | total_record_count | X | | | Type | | The following table shows the default values and/or init functions: | Property | Default Value | Init Function | |----------|---------------|---------------| | enable | `true` | | | event_state | `:normal` | | | last_notify_record | `0` | | | log_buffer | `[]` | | | logging_type | `:cov` | | | notification_threshold | `0` | | | record_count | `0` | | | records_since_notification | `0` | | | start_time | `%BACnet.Protocol.BACnetDateTime{...}` | | | stop_time | `%BACnet.Protocol.BACnetDateTime{...}` | | | total_record_count | `0` | |Summary
Types
Common object options for creation - all are optional.
Options accepted when creating or configuring a Trend Log object.
Available property names for this object.
The structure for property errors.
Represents a Trend Log object. All keys should be treated as read-only,
all updates should go only through update_property/3.
Functions
Adds an optional property to an object. Remote objects can not be mutated using this operation.
Creates a new object struct with the defined properties. Optional properties are not
created when not given, only required, given and dependency properties are created.
Properties with a value of nil are ignored.
Auto generated function to get the names of all properties this object supports.
Auto generated function to get the annotations for the given property name.
Auto generated function to get the list of annotations for each property.
Auto generated function to get the names of properties used for COV reporting.
Auto generated function to get the names of intrinsic properties.
Get the BACnet object identifier.
Auto generated function to get the names of optional properties.
Get the list of properties the object has.
Auto generated function to get a map of property name to type.
Get a property's value from an object.
Auto generated function to get the names of protected properties.
Auto generated function to get the names of readonly properties.
Auto generated function to get the names of required properties.
Checks if the given object has the given property.
Checks if the given object has Intrinsic Reporting enabled.
Checks if the given property is writable.
Removes an optional property from an object. This function is idempotent. Remote objects can not be mutated using this operation.
Auto generated function to check whether the object type supports intrinsic reporting.
Updates a property of an object.
Types
@type common_object_opts() :: {:allow_numeric_constants, boolean()} | {:allow_unknown_properties, boolean()} | {:ignore_unknown_properties, boolean()} | {:revision, BACnet.Protocol.Constants.protocol_revision()} | {:skip_property_validation_remote_object, boolean() | :value}
Common object options for creation - all are optional.
allow_numeric_constants- Constants are atoms and thus unknown constants or vendor extensions are integers and thus are rejected. Enabling this option will allow integers (non_neg_integer()) for properties with aConstants.type()spec.allow_unknown_properties- Properties that are unknown to the object implementation are usually rejected. With this option, unknown properties (numeric identifiers usually means we dont know them) are accepted and put into a separate map. This does mean we can not validate or write them. Types of the values can be anything at this point. While you can read unknown properties with atom or integer as property identifier, you can only remove numeric unknown property identifiers from an object. Property identifiers of typeatomare only accepted, if it is a remote object (object implementation is only enforced if it is a local object). Numeric property identifiers are accepted regardless of remote object or not. For remote objects, this means you have to write "raw values" (usuallyEncodingstructs).ignore_unknown_properties- Properties that are unknown to the object implementation are usually rejected. With this option, unknown properties get ignored, as if they were not specified.revision- The BACnet protocol revision to check required properties against. Optional properties are regardless of revision available. SeeBACnet.Protocol.Constants.protocol_revision/0for the available revisions.skip_property_validation_remote_object- Skips property validation for remote objects. Sometimes it is possible that the value is invalid as per BACnet specification (i.e. value 0 for a multistate object), but you still want those to be represented. Valuetrueneither type nor value are validated. Value:valuemeans the type is still validated and only the value validator is not run (if present). The property'svalidator_funwill also be skipped.
@type object_opts() :: {:intrinsic_reporting, boolean()} | common_object_opts() | {:clock_aligned_logging, boolean()}
Options accepted when creating or configuring a Trend Log object.
In addition to the common options, Trend Log supports:
clock_aligned_logging- Enables clock-aligned logging intervals.intrinsic_reporting- Enables BUFFER_READY intrinsic reporting (notifications when Notification_Threshold records have been collected).
@type property_name() ::
:acked_transitions
| :align_intervals
| :buffer_size
| :client_cov_increment
| :cov_resubscription_interval
| :description
| :enable
| :event_algorithm_inhibit
| :event_algorithm_inhibit_ref
| :event_detection_enable
| :event_enable
| :event_message_texts
| :event_message_texts_config
| :event_state
| :event_timestamps
| :interval_offset
| :last_notify_record
| :limit_enable
| :log_buffer
| :log_device_object_property
| :log_interval
| :logging_type
| :notification_class
| :notification_threshold
| :notify_type
| :object_instance
| :object_name
| :profile_location
| :profile_name
| :record_count
| :records_since_notification
| :reliability
| :reliability_evaluation_inhibit
| :start_time
| :status_flags
| :stop_time
| :stop_when_full
| :tags
| :time_delay
| :time_delay_normal
| :total_record_count
| :trigger
Available property names for this object.
@type property_update_error() :: {:error, {error :: atom(), property :: BACnet.Protocol.Constants.property_identifier()}}
The structure for property errors.
@type t() :: %BACnet.Protocol.ObjectTypes.TrendLog{ _metadata: internal_metadata(), _unknown_properties: %{ optional(atom() | non_neg_integer()) => term() | BACnet.Protocol.ApplicationTags.Encoding.t() | [BACnet.Protocol.ApplicationTags.Encoding.t()] }, acked_transitions: BACnet.Protocol.EventTransitionBits.t() | nil, align_intervals: boolean() | nil, buffer_size: BACnet.Protocol.ApplicationTags.unsigned32(), client_cov_increment: (float() | nil) | nil, cov_resubscription_interval: pos_integer() | nil, description: String.t() | nil, enable: boolean(), event_algorithm_inhibit: boolean() | nil, event_algorithm_inhibit_ref: BACnet.Protocol.ObjectPropertyRef.t() | nil, event_detection_enable: boolean() | nil, event_enable: BACnet.Protocol.EventTransitionBits.t() | nil, event_message_texts: BACnet.Protocol.EventMessageTexts.t() | nil, event_message_texts_config: BACnet.Protocol.EventMessageTexts.t() | nil, event_state: BACnet.Protocol.Constants.event_state() | (reserved_or_vendor_extension :: non_neg_integer()), event_timestamps: BACnet.Protocol.EventTimestamps.t() | nil, interval_offset: non_neg_integer() | nil, last_notify_record: BACnet.Protocol.ApplicationTags.unsigned32() | nil, limit_enable: BACnet.Protocol.LimitEnable.t() | nil, log_buffer: [BACnet.Protocol.LogRecord.t()], log_device_object_property: BACnet.Protocol.DeviceObjectPropertyRef.t(), log_interval: non_neg_integer(), logging_type: BACnet.Protocol.Constants.logging_type() | (reserved_or_vendor_extension :: non_neg_integer()), notification_class: non_neg_integer() | nil, notification_threshold: BACnet.Protocol.ApplicationTags.unsigned32() | nil, notify_type: BACnet.Protocol.Constants.notify_type() | (reserved_or_vendor_extension :: non_neg_integer()) | nil, object_instance: non_neg_integer(), object_name: String.t(), profile_location: String.t() | nil, profile_name: String.t() | nil, record_count: BACnet.Protocol.ApplicationTags.unsigned32(), records_since_notification: BACnet.Protocol.ApplicationTags.unsigned32() | nil, reliability: BACnet.Protocol.Constants.reliability() | (reserved_or_vendor_extension :: non_neg_integer()) | nil, reliability_evaluation_inhibit: boolean() | nil, start_time: BACnet.Protocol.BACnetDateTime.t(), status_flags: BACnet.Protocol.StatusFlags.t(), stop_time: BACnet.Protocol.BACnetDateTime.t(), stop_when_full: boolean(), tags: BACnet.Protocol.BACnetArray.t(BACnet.Protocol.NameValue.t()) | nil, time_delay: non_neg_integer() | nil, time_delay_normal: non_neg_integer() | nil, total_record_count: BACnet.Protocol.ApplicationTags.unsigned32(), trigger: boolean() | nil }
Represents a Trend Log object. All keys should be treated as read-only,
all updates should go only through update_property/3.
Properties which are for Intrinsic Reporting are nil, if disabled. If Intrinsic Reporting is enabled on the object, then the properties can not be nil.
Functions
@spec add_property(t(), BACnet.Protocol.Constants.property_identifier(), term()) :: {:ok, t()} | property_update_error()
Adds an optional property to an object. Remote objects can not be mutated using this operation.
Please note that properties of services can not be dynamically added and instead
the object must be newly created using create/4.
@spec create( non_neg_integer(), String.t(), %{optional(property_name() | atom() | non_neg_integer()) => term()}, [object_opts() | internal_metadata()] ) :: {:ok, t()} | property_update_error()
Creates a new object struct with the defined properties. Optional properties are not
created when not given, only required, given and dependency properties are created.
Properties with a value of nil are ignored.
Only properties that are required for specific services (i.e. Intrinsic Reporting) are automatically created.
@spec get_all_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of all properties this object supports.
@spec get_annotation(property_name()) :: [term()]
Auto generated function to get the annotations for the given property name.
@spec get_annotations() :: [{name :: property_name(), values :: [term()]}]
Auto generated function to get the list of annotations for each property.
@spec get_cov_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of properties used for COV reporting.
@spec get_intrinsic_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of intrinsic properties.
@spec get_object_identifier(t()) :: BACnet.Protocol.ObjectIdentifier.t()
Get the BACnet object identifier.
@spec get_optional_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of optional properties.
@spec get_properties(t()) :: [BACnet.Protocol.Constants.property_identifier()]
Get the list of properties the object has.
@spec get_properties_type_map() :: map()
Auto generated function to get a map of property name to type.
@spec get_property( t(), BACnet.Protocol.Constants.property_identifier() | non_neg_integer() ) :: {:ok, term()} | property_update_error()
Get a property's value from an object.
@spec get_protected_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of protected properties.
Protected is an annotation and the object modules prevent writing to this property directly in code. The protected properties are either written on creation or updated automatically depending on other properties being written to. Some properties are only written once at creation and never updated.
@spec get_readonly_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of readonly properties.
Readonly is only an annotation that the property should be write protected
on the BACnet side, there is no actual write protection in the object.
This is a hint to the device server. If you need actual write protection, see protected.
@spec get_required_properties() :: [BACnet.Protocol.Constants.property_identifier()]
Auto generated function to get the names of required properties.
@spec has_property?(t(), BACnet.Protocol.Constants.property_identifier()) :: boolean()
Checks if the given object has the given property.
See BACnet.Protocol.ObjectsUtility.has_property?/2 for implementation details.
Checks if the given object has Intrinsic Reporting enabled.
@spec property_writable?(t(), BACnet.Protocol.Constants.property_identifier()) :: boolean()
Checks if the given property is writable.
Check BACnet.Protocol.ObjectsUtility.property_writable?/2 for a basic run-down.
@spec remove_property( t(), BACnet.Protocol.Constants.property_identifier() | non_neg_integer() ) :: {:ok, t()} | property_update_error()
Removes an optional property from an object. This function is idempotent. Remote objects can not be mutated using this operation.
Please note that properties of services can not be dynamically removed and instead
the object must be newly created using create/4. Required properties can not be removed.
@spec supports_intrinsic() :: boolean()
Auto generated function to check whether the object type supports intrinsic reporting.
@spec update_property(t(), BACnet.Protocol.Constants.property_identifier(), term()) :: {:ok, t()} | property_update_error()
Updates a property of an object.