UNPKG

@bacnet-js/device

Version:

A TypeScript library for implementing BACnet IP devices in Node.js.

228 lines 9.01 kB
/** * 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