UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

1,123 lines (1,007 loc) 33 kB
import { CommandClasses, EncapsulationFlags, getCCName, ICommandClass, isZWaveError, IZWaveEndpoint, IZWaveNode, MessageOrCCLogEntry, MessageRecord, MulticastCC, MulticastDestination, NODE_ID_BROADCAST, parseCCId, SinglecastCC, ValueDB, ValueID, valueIdToString, ValueMetadata, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; import type { ZWaveApplicationHost, ZWaveHost } from "@zwave-js/host"; import { MessageOrigin } from "@zwave-js/serial"; import { buffer2hex, getEnumMemberName, JSONObject, num2hex, staticExtends, } from "@zwave-js/shared"; import { isArray } from "alcalzone-shared/typeguards"; import type { ValueIDProperties } from "./API"; import { getCCCommand, getCCCommandConstructor, getCCConstructor, getCCResponsePredicate, getCCValueProperties, getCCValues, getCommandClass, getExpectedCCResponse, getImplementedVersion, } from "./CommandClassDecorators"; import { EncapsulatingCommandClass, isEncapsulatingCommandClass, } from "./EncapsulatingCommandClass"; import { ICommandClassContainer, isCommandClassContainer, } from "./ICommandClassContainer"; import { CCValue, defaultCCValueOptions, DynamicCCValue, StaticCCValue, } from "./Values"; export type CommandClassDeserializationOptions = { data: Buffer; origin?: MessageOrigin; } & ( | { fromEncapsulation?: false; nodeId: number; } | { fromEncapsulation: true; encapCC: CommandClass; } ); export function gotDeserializationOptions( options: CommandClassOptions, ): options is CommandClassDeserializationOptions { return "data" in options && Buffer.isBuffer(options.data); } export interface CCCommandOptions { nodeId: number | MulticastDestination; endpoint?: number; } interface CommandClassCreationOptions extends CCCommandOptions { ccId?: number; // Used to overwrite the declared CC ID ccCommand?: number; // undefined = NoOp payload?: Buffer; origin?: undefined; } function gotCCCommandOptions(options: any): options is CCCommandOptions { return typeof options.nodeId === "number" || isArray(options.nodeId); } export type CommandClassOptions = | CommandClassCreationOptions | CommandClassDeserializationOptions; // @publicAPI export class CommandClass implements ICommandClass { // empty constructor to parse messages public constructor(host: ZWaveHost, options: CommandClassOptions) { this.host = host; // Extract the cc from declared metadata if not provided by the CC constructor this.ccId = ("ccId" in options && options.ccId) || getCommandClass(this); // Default to the root endpoint - Inherited classes may override this behavior this.endpointIndex = ("endpoint" in options ? options.endpoint : undefined) ?? 0; // We cannot use @ccValue for non-derived classes, so register interviewComplete as an internal value here // this.registerValue("interviewComplete", { internal: true }); if (gotDeserializationOptions(options)) { // For deserialized commands, try to invoke the correct subclass constructor const CCConstructor = getCCConstructor(CommandClass.getCommandClass(options.data)) ?? CommandClass; const ccCommand = CCConstructor.getCCCommand(options.data); if (ccCommand != undefined) { const CommandConstructor = getCCCommandConstructor( this.ccId, ccCommand, ); if ( CommandConstructor && (new.target as any) !== CommandConstructor ) { return new CommandConstructor(host, options); } } // If the constructor is correct or none was found, fall back to normal deserialization if (options.fromEncapsulation) { // Propagate the node ID and endpoint index from the encapsulating CC this.nodeId = options.encapCC.nodeId; if (!this.endpointIndex && options.encapCC.endpointIndex) { this.endpointIndex = options.encapCC.endpointIndex; } // And remember which CC encapsulates this CC this.encapsulatingCC = options.encapCC as any; } else { this.nodeId = options.nodeId; } ({ ccId: this.ccId, ccCommand: this.ccCommand, payload: this.payload, } = this.deserialize(options.data)); } else if (gotCCCommandOptions(options)) { const { nodeId, ccCommand = getCCCommand(this), payload = Buffer.allocUnsafe(0), } = options; this.nodeId = nodeId; this.ccCommand = ccCommand; this.payload = payload; } if (this instanceof InvalidCC) return; if ( options.origin !== MessageOrigin.Host && this.isSinglecast() && this.nodeId !== NODE_ID_BROADCAST ) { // For singlecast CCs, set the CC version as high as possible this.version = this.host.getSafeCCVersionForNode( this.ccId, this.nodeId, this.endpointIndex, ); // Send secure commands if necessary this.setEncapsulationFlag( EncapsulationFlags.Security, this.host.isCCSecure( this.ccId, this.nodeId, this.endpointIndex, ), ); } else { // For multicast and broadcast CCs, we just use the highest implemented version to serialize // Older nodes will ignore the additional fields this.version = getImplementedVersion(this.ccId); } } protected host: ZWaveHost; /** This CC's identifier */ public ccId: CommandClasses; public ccCommand?: number; public get ccName(): string { return getCCName(this.ccId); } /** The ID of the target node(s) */ public nodeId!: number | MulticastDestination; // Work around https://github.com/Microsoft/TypeScript/issues/27555 public payload!: Buffer; /** The version of the command class used */ // Work around https://github.com/Microsoft/TypeScript/issues/27555 public version!: number; /** Which endpoint of the node this CC belongs to. 0 for the root device. */ public endpointIndex: number; /** * Which encapsulation CCs this CC is/was/should be encapsulated with. * * Don't use this directly, this is used internally. */ public encapsulationFlags: EncapsulationFlags = EncapsulationFlags.None; /** Activates or deactivates the given encapsulation flag */ public setEncapsulationFlag( flag: EncapsulationFlags, active: boolean, ): void { if (active) { this.encapsulationFlags |= flag; } else { this.encapsulationFlags &= ~flag; } } /** Contains a reference to the encapsulating CC if this CC is encapsulated */ public encapsulatingCC?: EncapsulatingCommandClass; /** Returns true if this CC is an extended CC (0xF100..0xFFFF) */ public isExtended(): boolean { return this.ccId >= 0xf100; } /** Whether the interview for this CC was previously completed */ public isInterviewComplete(applHost: ZWaveApplicationHost): boolean { return !!this.getValueDB(applHost).getValue<boolean>({ commandClass: this.ccId, endpoint: this.endpointIndex, property: "interviewComplete", }); } /** Marks the interview for this CC as complete or not */ public setInterviewComplete( applHost: ZWaveApplicationHost, complete: boolean, ): void { this.getValueDB(applHost).setValue( { commandClass: this.ccId, endpoint: this.endpointIndex, property: "interviewComplete", }, complete, ); } /** * Deserializes a CC from a buffer that contains a serialized CC */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types protected deserialize(data: Buffer) { const ccId = CommandClass.getCommandClass(data); const ccIdLength = this.isExtended() ? 2 : 1; if (data.length > ccIdLength) { // This is not a NoOp CC (contains command and payload) const ccCommand = data[ccIdLength]; const payload = data.slice(ccIdLength + 1); return { ccId, ccCommand, payload, }; } else { // NoOp CC (no command, no payload) const payload = Buffer.allocUnsafe(0); return { ccId, payload }; } } /** * Serializes this CommandClass to be embedded in a message payload or another CC */ public serialize(): Buffer { // NoOp CCs have no command and no payload if (this.ccId === CommandClasses["No Operation"]) return Buffer.from([this.ccId]); else if (this.ccCommand == undefined) { throw new ZWaveError( "Cannot serialize a Command Class without a command", ZWaveErrorCodes.CC_Invalid, ); } const payloadLength = this.payload.length; const ccIdLength = this.isExtended() ? 2 : 1; const data = Buffer.allocUnsafe(ccIdLength + 1 + payloadLength); data.writeUIntBE(this.ccId, 0, ccIdLength); data[ccIdLength] = this.ccCommand; if (payloadLength > 0 /* implies payload != undefined */) { this.payload.copy(data, 1 + ccIdLength); } return data; } /** Extracts the CC id from a buffer that contains a serialized CC */ public static getCommandClass(data: Buffer): CommandClasses { return parseCCId(data).ccId; } /** Extracts the CC command from a buffer that contains a serialized CC */ public static getCCCommand(data: Buffer): number | undefined { if (data[0] === 0) return undefined; // NoOp const isExtendedCC = data[0] >= 0xf1; return isExtendedCC ? data[2] : data[1]; } /** * Retrieves the correct constructor for the CommandClass in the given Buffer. * It is assumed that the buffer only contains the serialized CC. This throws if the CC is not implemented. */ public static getConstructor(ccData: Buffer): CCConstructor<CommandClass> { // Encapsulated CCs don't have the two header bytes const cc = CommandClass.getCommandClass(ccData); const ret = getCCConstructor(cc); if (!ret) { const ccName = getCCName(cc); throw new ZWaveError( `The command class ${ccName} is not implemented`, ZWaveErrorCodes.CC_NotImplemented, ); } return ret; } /** * Creates an instance of the CC that is serialized in the given buffer */ public static from( host: ZWaveHost, options: CommandClassDeserializationOptions, ): CommandClass { // Fall back to unspecified command class in case we receive one that is not implemented const Constructor = CommandClass.getConstructor(options.data); try { const ret = new Constructor(host, options); return ret; } catch (e) { // Indicate invalid payloads with a special CC type if ( isZWaveError(e) && e.code === ZWaveErrorCodes.PacketFormat_InvalidPayload ) { const nodeId = options.fromEncapsulation ? options.encapCC.nodeId : options.nodeId; let ccName: string | undefined; const ccId = CommandClass.getCommandClass(options.data); const ccCommand = CommandClass.getCCCommand(options.data); if (ccCommand != undefined) { ccName = getCCCommandConstructor(ccId, ccCommand)?.name; } // Fall back to the unspecified CC if the command cannot be determined if (!ccName) { ccName = `${getCCName(ccId)} CC`; } // Preserve why the command was invalid let reason: string | ZWaveErrorCodes | undefined; if ( typeof e.context === "string" || (typeof e.context === "number" && ZWaveErrorCodes[e.context] != undefined) ) { reason = e.context; } const ret = new InvalidCC(host, { nodeId, ccId, ccName, reason, }); if (options.fromEncapsulation) { ret.encapsulatingCC = options.encapCC as any; } return ret; } throw e; } } /** * Create an instance of the given CC without checking whether it is supported. * If the CC is implemented, this returns an instance of the given CC which is linked to the given endpoint. * * **INTERNAL:** Applications should not use this directly. */ public static createInstanceUnchecked<T extends CommandClass>( host: ZWaveHost, endpoint: IZWaveEndpoint, cc: CommandClasses | CCConstructor<T>, ): T | undefined { const Constructor = typeof cc === "number" ? getCCConstructor(cc) : cc; if (Constructor) { return new Constructor(host, { nodeId: endpoint.nodeId, endpoint: endpoint.index, }) as T; } } /** Generates a representation of this CC for the log */ public toLogEntry(_applHost: ZWaveApplicationHost): MessageOrCCLogEntry { let tag = this.constructor.name; const message: MessageRecord = {}; if (this.constructor === CommandClass) { tag = `${getEnumMemberName( CommandClasses, this.ccId, )} CC (not implemented)`; if (this.ccCommand != undefined) { message.command = num2hex(this.ccCommand); } } if (this.payload.length > 0) { message.payload = buffer2hex(this.payload); } return { tags: [tag], message, }; } /** Generates the JSON representation of this CC */ public toJSON(): JSONObject { return this.toJSONInternal(); } private toJSONInternal(): JSONObject { const ret: JSONObject = { nodeId: this.nodeId, ccId: CommandClasses[this.ccId] || num2hex(this.ccId), }; if (this.ccCommand != undefined) { ret.ccCommand = num2hex(this.ccCommand); } if (this.payload.length > 0) { ret.payload = "0x" + this.payload.toString("hex"); } return ret; } protected throwMissingCriticalInterviewResponse(): never { throw new ZWaveError( `The node did not respond to a critical interview query in time.`, ZWaveErrorCodes.Controller_NodeTimeout, ); } /** * Performs the interview procedure for this CC according to SDS14223 */ public async interview(_applHost: ZWaveApplicationHost): Promise<void> { // This needs to be overwritten per command class. In the default implementation, don't do anything } /** * Refreshes all dynamic values of this CC */ public async refreshValues(_applHost: ZWaveApplicationHost): Promise<void> { // This needs to be overwritten per command class. In the default implementation, don't do anything } /** Determines which CC interviews must be performed before this CC can be interviewed */ public determineRequiredCCInterviews(): readonly CommandClasses[] { // By default, all CCs require the VersionCC interview // There are two exceptions to this rule: // * ManufacturerSpecific must be interviewed first // * VersionCC itself must be done after that // These exceptions are defined in the overrides of this method of each corresponding CC return [CommandClasses.Version]; } /** * Whether the endpoint interview may be skipped by a CC. Can be overwritten by a subclass. */ public skipEndpointInterview(): boolean { // By default no interview may be skipped return false; } /** * Maps a BasicCC value to a more specific CC implementation. Returns true if the value was mapped, false otherwise. * @param _value The value of the received BasicCC */ public setMappedBasicValue( _applHost: ZWaveApplicationHost, _value: number, ): boolean { // By default, don't map return false; } public isSinglecast(): this is SinglecastCC<this> { return typeof this.nodeId === "number"; } public isMulticast(): this is MulticastCC<this> { return isArray(this.nodeId); } /** * Returns the node this CC is linked to. Throws if the controller is not yet ready. */ public getNode(applHost: ZWaveApplicationHost): IZWaveNode | undefined { if (this.isSinglecast()) { return applHost.nodes.get(this.nodeId); } } /** * @internal * Returns the node this CC is linked to (or undefined if the node doesn't exist) */ public getNodeUnsafe( applHost: ZWaveApplicationHost, ): IZWaveNode | undefined { try { return this.getNode(applHost); } catch (e) { // This was expected if (isZWaveError(e) && e.code === ZWaveErrorCodes.Driver_NotReady) { return undefined; } // Something else happened throw e; } } public getEndpoint( applHost: ZWaveApplicationHost, ): IZWaveEndpoint | undefined { return this.getNode(applHost)?.getEndpoint(this.endpointIndex); } /** Returns the value DB for this CC's node */ protected getValueDB(applHost: ZWaveApplicationHost): ValueDB { if (this.isSinglecast()) { try { return applHost.getValueDB(this.nodeId); } catch { throw new ZWaveError( "The node for this CC does not exist or the driver is not ready yet", ZWaveErrorCodes.Driver_NotReady, ); } } throw new ZWaveError( "Cannot retrieve the value DB for non-singlecast CCs", ZWaveErrorCodes.CC_NoNodeID, ); } /** * Ensures that the metadata for the given CC value exists in the Value DB or creates it if it does not. * The endpoint index of the current CC instance is automatically taken into account. * @param meta Will be used in place of the predefined metadata when given */ protected ensureMetadata( applHost: ZWaveApplicationHost, ccValue: CCValue, meta?: ValueMetadata, ): void { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); if (!valueDB.hasMetadata(valueId)) { valueDB.setMetadata(valueId, meta ?? ccValue.meta); } } /** * Removes the metadata for the given CC value from the value DB. * The endpoint index of the current CC instance is automatically taken into account. */ protected removeMetadata( applHost: ZWaveApplicationHost, ccValue: CCValue, ): void { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); valueDB.setMetadata(valueId, undefined); } /** * Writes the metadata for the given CC value into the Value DB. * The endpoint index of the current CC instance is automatically taken into account. * @param meta Will be used in place of the predefined metadata when given */ protected setMetadata( applHost: ZWaveApplicationHost, ccValue: CCValue, meta?: ValueMetadata, ): void { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); valueDB.setMetadata(valueId, meta ?? ccValue.meta); } /** * Reads the metadata for the given CC value from the Value DB. * The endpoint index of the current CC instance is automatically taken into account. */ protected getMetadata<T extends ValueMetadata>( applHost: ZWaveApplicationHost, ccValue: CCValue, ): T | undefined { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); return valueDB.getMetadata(valueId) as any; } /** * Stores the given value under the value ID for the given CC value in the value DB. * The endpoint index of the current CC instance is automatically taken into account. */ protected setValue( applHost: ZWaveApplicationHost, ccValue: CCValue, value: unknown, ): void { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); valueDB.setValue(valueId, value); } /** * Removes the value for the given CC value from the value DB. * The endpoint index of the current CC instance is automatically taken into account. */ protected removeValue( applHost: ZWaveApplicationHost, ccValue: CCValue, ): void { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); valueDB.removeValue(valueId); } /** * Reads the value stored for the value ID of the given CC value from the value DB. * The endpoint index of the current CC instance is automatically taken into account. */ protected getValue<T>( applHost: ZWaveApplicationHost, ccValue: CCValue, ): T | undefined { const valueDB = this.getValueDB(applHost); const valueId = ccValue.endpoint(this.endpointIndex); return valueDB.getValue(valueId); } /** Returns the CC value definition for the current CC which matches the given value ID */ protected getCCValue( valueId: ValueID, ): StaticCCValue | DynamicCCValue | undefined { const ccValues = getCCValues(this); if (!ccValues) return; for (const value of Object.values(ccValues)) { if (value?.is(valueId)) { return value; } } } private getAllCCValues(): (StaticCCValue | DynamicCCValue)[] { return Object.values(getCCValues(this) ?? {}) as ( | StaticCCValue | DynamicCCValue )[]; } private getCCValueForValueId( properties: ValueIDProperties, ): StaticCCValue | DynamicCCValue | undefined { return this.getAllCCValues().find((value) => value.is({ commandClass: this.ccId, ...properties, }), ); } /** Returns a list of all value names that are defined for this CommandClass */ public getDefinedValueIDs(applHost: ZWaveApplicationHost): ValueID[] { // In order to compare value ids, we need them to be strings const ret = new Map<string, ValueID>(); const addValueId = ( property: string | number, propertyKey?: string | number, ): void => { const valueId: ValueID = { commandClass: this.ccId, endpoint: this.endpointIndex, property, propertyKey, }; const dbKey = valueIdToString(valueId); if (!ret.has(dbKey)) ret.set(dbKey, valueId); }; // Return all value IDs for this CC... const valueDB = this.getValueDB(applHost); // ...which either have metadata or a value const existingValueIds: ValueID[] = [ ...valueDB.getValues(this.ccId), ...valueDB.getAllMetadata(this.ccId), ]; // ...or which are statically defined using @ccValues(...) for (const value of Object.values(getCCValues(this) ?? {})) { // Skip dynamic CC values - they need a specific subclass instance to be evaluated if (!value || typeof value === "function") continue; // Skip those values that are only supported in higher versions of the CC if ( value.options.minVersion != undefined && value.options.minVersion > this.version ) { continue; } // Skip internal values if (value.options.internal) continue; // And determine if this value should be automatically "created" if ( value.options.autoCreate === false || (typeof value.options.autoCreate === "function" && !value.options.autoCreate( applHost, this.getEndpoint(applHost)!, )) ) { continue; } existingValueIds.push(value.endpoint(this.endpointIndex)); } // TODO: this is a bit awkward for the statically defined ones const ccValues = this.getAllCCValues(); for (const valueId of existingValueIds) { // ...belonging to the current endpoint if ((valueId.endpoint ?? 0) !== this.endpointIndex) continue; // Hard-coded: interviewComplete is always internal if (valueId.property === "interviewComplete") continue; // ... which don't have a CC value definition // ... or one that does not mark the value ID as internal const ccValue = ccValues.find((value) => value.is(valueId)); if (!ccValue || !ccValue.options.internal) { addValueId(valueId.property, valueId.propertyKey); } } return [...ret.values()]; } /** Determines if the given value is an internal value */ public isInternalValue(properties: ValueIDProperties): boolean { // Hard-coded: interviewComplete is always internal if (properties.property === "interviewComplete") return true; const ccValue = this.getCCValueForValueId(properties); return ccValue?.options.internal ?? defaultCCValueOptions.internal; } /** Determines if the given value is an secret value */ public isSecretValue(properties: ValueIDProperties): boolean { const ccValue = this.getCCValueForValueId(properties); return ccValue?.options.secret ?? defaultCCValueOptions.secret; } /** Determines if the given value should be persisted or represents an event */ public isStatefulValue(properties: ValueIDProperties): boolean { const ccValue = this.getCCValueForValueId(properties); return ccValue?.options.stateful ?? defaultCCValueOptions.stateful; } /** * Persists all values for this CC instance into the value DB which are annotated with @ccValue. * Returns `true` if the process succeeded, `false` if the value DB cannot be accessed. */ public persistValues(applHost: ZWaveApplicationHost): boolean { let valueDB: ValueDB; try { valueDB = this.getValueDB(applHost); } catch { return false; } // Get all properties of this CC which are annotated with a @ccValue decorator and store them. for (const [prop, _value] of getCCValueProperties(this)) { // Evaluate dynamic CC values first const value = typeof _value === "function" ? _value(this) : _value; // Skip those values that are only supported in higher versions of the CC if ( value.options.minVersion != undefined && value.options.minVersion > this.version ) { continue; } const valueId: ValueID = value.endpoint(this.endpointIndex); // Metadata always gets created for non-internal values, regardless of the actual value being defined if (!value.options.internal) { if (!valueDB.hasMetadata(valueId)) { valueDB.setMetadata(valueId, value.meta); } } // The value only gets written if it is not undefined const sourceValue = this[prop as keyof this]; if (sourceValue == undefined) continue; valueDB.setValue(valueId, sourceValue, { stateful: value.options.stateful, }); } return true; } /** * When a CC supports to be split into multiple partial CCs, this can be used to identify the * session the partial CCs belong to. * If a CC expects `mergePartialCCs` to be always called, you should return an empty object here. */ public getPartialCCSessionId(): Record<string, any> | undefined { return undefined; // Only select CCs support to be split } /** * When a CC supports to be split into multiple partial CCs, this indicates that the last report hasn't been received yet. * @param _session The previously received set of messages received in this partial CC session */ public expectMoreMessages(_session: CommandClass[]): boolean { return false; // By default, all CCs are monolithic } /** Include previously received partial responses into a final CC */ /* istanbul ignore next */ public mergePartialCCs( _applHost: ZWaveApplicationHost, _partials: CommandClass[], ): void { // This is highly CC dependent // Overwrite this in derived classes, by default do nothing } /** Tests whether this CC expects at least one command in return */ public expectsCCResponse(): boolean { let expected: | DynamicCCResponse<this> | ReturnType<DynamicCCResponse<this>> = getExpectedCCResponse(this); // Evaluate dynamic CC responses if ( typeof expected === "function" && !staticExtends(expected, CommandClass) ) { expected = expected(this); } if (expected === undefined) return false; if (isArray(expected)) { return expected.every((cc) => staticExtends(cc, CommandClass)); } else { return staticExtends(expected, CommandClass); } } public isExpectedCCResponse(received: CommandClass): boolean { if (received.nodeId !== this.nodeId) return false; let expected: | DynamicCCResponse<this> | ReturnType<DynamicCCResponse<this>> = getExpectedCCResponse(this); // Evaluate dynamic CC responses if ( typeof expected === "function" && !staticExtends(expected, CommandClass) ) { expected = expected(this); } if (expected == undefined) { // Fallback, should not happen if the expected response is defined correctly return false; } else if ( isArray(expected) && expected.every((cc) => staticExtends(cc, CommandClass)) ) { // The CC always expects a response from the given list, check if the received // message is in that list if (expected.every((base) => !(received instanceof base))) { return false; } } else if (staticExtends(expected, CommandClass)) { // The CC always expects the same single response, check if this is the one if (!(received instanceof expected)) return false; } // If the CC wants to test the response, let it const predicate = getCCResponsePredicate(this); const ret = predicate?.(this, received) ?? true; if (ret === "checkEncapsulated") { if ( isEncapsulatingCommandClass(this) && isEncapsulatingCommandClass(received) ) { return this.encapsulated.isExpectedCCResponse( received.encapsulated, ); } else { // Fallback, should not happen if the expected response is defined correctly return false; } } return ret; } /** * Translates a property identifier into a speaking name for use in an external API * @param property The property identifier that should be translated * @param _propertyKey The (optional) property key the translated name may depend on */ public translateProperty( _applHost: ZWaveApplicationHost, property: string | number, _propertyKey?: string | number, ): string { // Overwrite this in derived classes, by default just return the property key return property.toString(); } /** * Translates a property key into a speaking name for use in an external API * @param _property The property the key in question belongs to * @param propertyKey The property key for which the speaking name should be retrieved */ public translatePropertyKey( _applHost: ZWaveApplicationHost, _property: string | number, propertyKey: string | number, ): string | undefined { // Overwrite this in derived classes, by default just return the property key return propertyKey.toString(); } /** Returns the number of bytes that are added to the payload by this CC */ protected computeEncapsulationOverhead(): number { // Default is ccId (+ ccCommand): return (this.isExtended() ? 2 : 1) + 1; } /** Computes the maximum net payload size that can be transmitted inside this CC */ public getMaxPayloadLength(baseLength: number): number { let ret = baseLength; let cur: CommandClass | undefined = this; while (cur) { ret -= cur.computeEncapsulationOverhead(); cur = isEncapsulatingCommandClass(cur) ? cur.encapsulated : undefined; } return ret; } /** Checks whether this CC is encapsulated with one that has the given CC id and (optionally) CC Command */ public isEncapsulatedWith( ccId: CommandClasses, ccCommand?: number, ): boolean { let cc: CommandClass = this; // Check whether there was a S0 encapsulation while (cc.encapsulatingCC) { cc = cc.encapsulatingCC; if ( cc.ccId === ccId && (ccCommand === undefined || cc.ccCommand === ccCommand) ) { return true; } } return false; } /** Traverses the encapsulation stack of this CC and returns the one that has the given CC id and (optionally) CC Command if that exists. */ public getEncapsulatingCC( ccId: CommandClasses, ccCommand?: number, ): CommandClass | undefined { let cc: CommandClass = this; while (cc.encapsulatingCC) { cc = cc.encapsulatingCC; if ( cc.ccId === ccId && (ccCommand === undefined || cc.ccCommand === ccCommand) ) { return cc; } } } } export interface InvalidCCCreationOptions extends CommandClassCreationOptions { ccName: string; reason?: string | ZWaveErrorCodes; } export class InvalidCC extends CommandClass { public constructor(host: ZWaveHost, options: InvalidCCCreationOptions) { super(host, options); this._ccName = options.ccName; // Numeric reasons are used internally to communicate problems with a CC // without ignoring them entirely this.reason = options.reason; } private _ccName: string; public get ccName(): string { return this._ccName; } public readonly reason?: string | ZWaveErrorCodes; public toLogEntry(): MessageOrCCLogEntry { return { tags: [this.ccName, "INVALID"], message: this.reason != undefined ? { error: typeof this.reason === "string" ? this.reason : getEnumMemberName( ZWaveErrorCodes, this.reason, ), } : undefined, }; } } /** @publicAPI */ export function assertValidCCs(container: ICommandClassContainer): void { if (container.command instanceof InvalidCC) { if (typeof container.command.reason === "number") { throw new ZWaveError( "The message payload failed validation!", container.command.reason, ); } else { throw new ZWaveError( "The message payload is invalid!", ZWaveErrorCodes.PacketFormat_InvalidPayload, container.command.reason, ); } } else if (isCommandClassContainer(container.command)) { assertValidCCs(container.command); } } export type CCConstructor<T extends CommandClass> = typeof CommandClass & { // I don't like the any, but we need it to support half-implemented CCs (e.g. report classes) new (host: ZWaveHost, options: any): T; }; /** * @publicAPI * May be used to define different expected CC responses depending on the sent CC */ export type DynamicCCResponse< TSent extends CommandClass, TReceived extends CommandClass = CommandClass, > = ( sentCC: TSent, ) => CCConstructor<TReceived> | CCConstructor<TReceived>[] | undefined; /** @publicAPI */ export type CCResponseRole = | boolean // The response was either expected or unexpected | "checkEncapsulated"; // The response role depends on the encapsulated CC /** * @publicAPI * A predicate function to test if a received CC matches the sent CC */ export type CCResponsePredicate< TSent extends CommandClass, TReceived extends CommandClass = CommandClass, > = (sentCommand: TSent, receivedCommand: TReceived) => CCResponseRole;