@bacnet-js/device
Version:
A TypeScript library for implementing BACnet IP devices in Node.js.
228 lines • 9.01 kB
JavaScript
/**
* BACnet object implementation module
*
* This module provides the base implementation for BACnet objects,
* which are the core components of BACnet devices.
*
* @module
*/
import assert from 'node:assert';
import { AsyncEventEmitter } from '../../events.js';
import { BDError } from '../../errors.js';
import { ASN1_MAX_INSTANCE, ErrorCode, ErrorClass, ObjectType, EventState, Reliability, ApplicationTag, PropertyIdentifier, StatusFlagsBitString, } from '@bacnet-js/client';
import { BDAbstractProperty, BDSingletProperty, BDPolledArrayProperty, } from '../../properties/index.js';
import { ensureArray } from '../../utils.js';
import { MAX_ARRAY_INDEX } from '../../constants.js';
import { TaskQueue } from '../../taskqueue.js';
import { getObjectUID } from '../../uids.js';
/**
* According to the BACnet specification, certain properties should not
* be included in the Property_List property of objects.
*
* @see section 12.1.1.4.1 "Property_List"
*/
const unlistedProperties = [
PropertyIdentifier.OBJECT_NAME,
PropertyIdentifier.OBJECT_TYPE,
PropertyIdentifier.OBJECT_IDENTIFIER,
PropertyIdentifier.PROPERTY_LIST,
];
/**
* Used to produce process-wide unique object identifiers.
*/
let objectCounter = 0;
/**
* Base class for all BACnet objects
*
* This class implements the core functionality required by all BACnet objects
* according to the BACnet specification. It manages object properties and
* handles property read/write operations and CoV notifications.
*
* @extends AsyncEventEmitter<BDObjectEvents>
*/
export class BDObject extends AsyncEventEmitter {
/** The unique identifier for this object (type and instance number) */
identifier;
uid;
/**
* The list of properties in this object (used for PROPERTY_LIST property)
* @private
*/
#propertyList;
/**
* Map of all properties in this object by their identifier
* @private
*/
#properties;
#queue;
objectName;
objectType;
objectIdentifier;
propertyList;
description;
outOfService;
statusFlags;
eventState;
reliability;
/**
* Creates a new BACnet object
*
*/
constructor(identifier, name, description = '') {
super();
assert(identifier.instance > 0, 'object instance cannot be zero or negative');
assert(identifier.instance <= ASN1_MAX_INSTANCE, `object instance cannot be greater than ${ASN1_MAX_INSTANCE}`);
this.#queue = new TaskQueue();
this.#properties = new Map();
this.#propertyList = [];
this.uid = getObjectUID(identifier);
this.identifier = Object.freeze(identifier);
this.objectName = this.addProperty(new BDSingletProperty(PropertyIdentifier.OBJECT_NAME, ApplicationTag.CHARACTER_STRING, false, name));
this.objectType = this.addProperty(new BDSingletProperty(PropertyIdentifier.OBJECT_TYPE, ApplicationTag.ENUMERATED, false, identifier.type));
this.objectIdentifier = this.addProperty(new BDSingletProperty(PropertyIdentifier.OBJECT_IDENTIFIER, ApplicationTag.OBJECTIDENTIFIER, false, this.identifier));
this.propertyList = this.addProperty(new BDPolledArrayProperty(PropertyIdentifier.PROPERTY_LIST, () => this.#propertyList));
this.description = this.addProperty(new BDSingletProperty(PropertyIdentifier.DESCRIPTION, ApplicationTag.CHARACTER_STRING, false, description));
this.outOfService = this.addProperty(new BDSingletProperty(PropertyIdentifier.OUT_OF_SERVICE, ApplicationTag.BOOLEAN, false, false));
this.statusFlags = this.addProperty(new BDSingletProperty(PropertyIdentifier.STATUS_FLAGS, ApplicationTag.BIT_STRING, false, new StatusFlagsBitString()));
this.eventState = this.addProperty(new BDSingletProperty(PropertyIdentifier.EVENT_STATE, ApplicationTag.ENUMERATED, false, EventState.NORMAL));
this.reliability = this.addProperty(new BDSingletProperty(PropertyIdentifier.RELIABILITY, ApplicationTag.ENUMERATED, false, Reliability.NO_FAULT_DETECTED));
}
/**
* Adds a property to this object
*
* This method registers a new property with the object and sets up
* event subscriptions for property value changes.
*
* @param property - The property to add
* @returns The added property
* @throws Error if a property with the same identifier already exists
* @typeParam T - The specific BACnet property type
*/
addProperty(property) {
if (this.#properties.has(property.identifier)) {
throw new Error('Cannot register property: duplicate property identifier');
}
this.#properties.set(property.identifier, property);
if (!unlistedProperties.includes(property.identifier)) {
this.#propertyList.push({ type: ApplicationTag.ENUMERATED, value: property.identifier });
}
property.on('aftercov', this.#onPropertyAfterCov);
return property;
}
transaction(task) {
return this.#queue.run(task);
}
/**
* Writes a value to a property
*
* This internal method is used to handle write operations from the BACnet network.
*
* @param identifier - The identifier of the property to write
* @param value - The value to write to the property
* @throws BACnetError if the property does not exist
* @internal
*/
async ___writeProperty(identifier, value) {
const property = this.#properties.get(identifier.id);
// TODO: test/validate value before setting it!
if (property) {
await property.___writeData(value);
}
else {
throw new BDError('unknown property', ErrorCode.UNKNOWN_PROPERTY, ErrorClass.PROPERTY);
}
}
/**
* Reads a value from a property
*
* This internal method is used to handle read operations from the BACnet network.
*
* @param req - The read property request
* @returns The property value
* @throws BACnetError if the property does not exist
* @internal
*/
async ___readProperty(identifier) {
const ctx = { date: new Date() };
const property = this.#properties.get(identifier.id);
if (property) {
return property.___readData(identifier.index, ctx);
}
throw new BDError('unknown property', ErrorCode.UNKNOWN_PROPERTY, ErrorClass.PROPERTY);
}
/**
* Reads all properties from this object
*
* This internal method is used to handle ReadPropertyMultiple operations
* when the ALL property identifier is used.
*
* @returns An object containing all property values
* @internal
*/
async ___readPropertyMultipleAll() {
const ctx = { date: new Date() };
const values = [];
for (const [identifier, property] of this.#properties.entries()) {
values.push({
property: {
id: identifier,
index: MAX_ARRAY_INDEX,
},
value: ensureArray(property.___readData(MAX_ARRAY_INDEX, ctx)),
});
}
return { objectId: this.identifier, values };
}
/**
* Reads multiple properties from this object
*
* This internal method is used to handle ReadPropertyMultiple operations
* from the BACnet network.
*
* @param identifiers - Array of property identifiers to read
* @returns An object containing the requested property values
* @internal
*/
async ___readPropertyMultiple(identifiers) {
if (identifiers.length === 1 && identifiers[0].id === PropertyIdentifier.ALL) {
return this.___readPropertyMultipleAll();
}
const ctx = { date: new Date() };
const values = [];
for (const identifier of identifiers) {
const property = this.#properties.get(identifier.id);
if (property) {
values.push({
property: identifier,
value: ensureArray(property.___readData(identifier.index, ctx))
});
}
}
return { objectId: this.identifier, values };
}
/**
*
* @internal
*/
___getPropertyOrThrow(identifier) {
const property = this.#properties.get(identifier);
if (property) {
return property;
}
throw new BDError('unknown property', ErrorCode.UNKNOWN_PROPERTY, ErrorClass.PROPERTY);
}
/**
* Handler for property 'aftercov' events
*
* This method is called after a property value changes and propagates
* the event to object subscribers.
*
* @param property - The property that changed
* @param nextValue - The new value that was set
* @private
*/
#onPropertyAfterCov = async (property, nextValue) => {
await this.___asyncEmitSeries(false, 'aftercov', this, property, nextValue);
};
}
//# sourceMappingURL=object.js.map