UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

624 lines (552 loc) 19.1 kB
import { createReflectionDecorator, getNodeTag, highResTimestamp, IZWaveNode, MessageOrCCLogEntry, MessagePriority, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; import type { ZWaveApplicationHost, ZWaveHost } from "@zwave-js/host"; import type { JSONObject, TypedClassDecorator } from "@zwave-js/shared/safe"; import { num2hex, staticExtends } from "@zwave-js/shared/safe"; import { MessageHeaders } from "../MessageHeaders"; import { FunctionType, MessageType } from "./Constants"; import { isNodeQuery } from "./INodeQuery"; export type MessageConstructor<T extends Message> = new ( host: ZWaveHost, options?: MessageOptions, ) => T; export type DeserializingMessageConstructor<T extends Message> = new ( host: ZWaveHost, options: MessageDeserializationOptions, ) => T; /** Where a serialized message originates from, to distinguish how certain messages need to be deserialized */ export enum MessageOrigin { Controller, Host, } export interface MessageDeserializationOptions { data: Buffer; origin?: MessageOrigin; /** Whether CCs should be parsed immediately (only affects messages that contain CCs). Default: `true` */ parseCCs?: boolean; } /** * Tests whether the given message constructor options contain a buffer for deserialization */ export function gotDeserializationOptions( options: Record<any, any> | undefined, ): options is MessageDeserializationOptions { return options != undefined && Buffer.isBuffer(options.data); } export interface MessageBaseOptions { callbackId?: number; } export interface MessageCreationOptions extends MessageBaseOptions { type?: MessageType; functionType?: FunctionType; expectedResponse?: FunctionType | typeof Message | ResponsePredicate; expectedCallback?: FunctionType | typeof Message | ResponsePredicate; payload?: Buffer; } export type MessageOptions = | MessageCreationOptions | MessageDeserializationOptions; /** * Represents a Z-Wave message for communication with the serial interface */ export class Message { public constructor( protected host: ZWaveHost, options: MessageOptions = {}, ) { // decide which implementation we follow if (gotDeserializationOptions(options)) { // #1: deserialize from payload const payload = options.data; // SOF, length, type, commandId and checksum must be present if (!payload.length || payload.length < 5) { throw new ZWaveError( "Could not deserialize the message because it was truncated", ZWaveErrorCodes.PacketFormat_Truncated, ); } // the packet has to start with SOF if (payload[0] !== MessageHeaders.SOF) { throw new ZWaveError( "Could not deserialize the message because it does not start with SOF", ZWaveErrorCodes.PacketFormat_Invalid, ); } // check the length again, this time with the transmitted length const messageLength = Message.getMessageLength(payload); if (payload.length < messageLength) { throw new ZWaveError( "Could not deserialize the message because it was truncated", ZWaveErrorCodes.PacketFormat_Truncated, ); } // check the checksum const expectedChecksum = computeChecksum( payload.slice(0, messageLength), ); if (payload[messageLength - 1] !== expectedChecksum) { throw new ZWaveError( "Could not deserialize the message because the checksum didn't match", ZWaveErrorCodes.PacketFormat_Checksum, ); } this.type = payload[2]; this.functionType = payload[3]; const payloadLength = messageLength - 5; this.payload = payload.slice(4, 4 + payloadLength); } else { // Try to determine the message type if (options.type == undefined) options.type = getMessageType(this); if (options.type == undefined) { throw new ZWaveError( "A message must have a given or predefined message type", ZWaveErrorCodes.Argument_Invalid, ); } this.type = options.type; if (options.functionType == undefined) options.functionType = getFunctionType(this); if (options.functionType == undefined) { throw new ZWaveError( "A message must have a given or predefined function type", ZWaveErrorCodes.Argument_Invalid, ); } this.functionType = options.functionType; // Fall back to decorated response/callback types if none is given this.expectedResponse = options.expectedResponse ?? getExpectedResponse(this); this.expectedCallback = options.expectedCallback ?? getExpectedCallback(this); this._callbackId = options.callbackId; this.payload = options.payload || Buffer.allocUnsafe(0); } } public type: MessageType; public functionType: FunctionType; public expectedResponse: | FunctionType | typeof Message | ResponsePredicate | undefined; public expectedCallback: | FunctionType | typeof Message | ResponsePredicate | undefined; public payload: Buffer; // TODO: Length limit 255 private _callbackId: number | undefined; /** * Used to map requests to responses. * * WARNING: Accessing this property will generate a new callback ID if this message had none. * If you want to compare the callback ID, use `hasCallbackId()` beforehand to check if the callback ID is already defined. */ public get callbackId(): number { if (this._callbackId == undefined) { this._callbackId = this.host.getNextCallbackId(); } return this._callbackId; } public set callbackId(v: number | undefined) { this._callbackId = v; } /** * Tests whether this message's callback ID is defined */ public hasCallbackId(): boolean { return this._callbackId != undefined; } /** * Tests whether this message needs a callback ID to match its response */ public needsCallbackId(): boolean { return true; } /** Returns the callback timeout for this message in case the default settings do not apply. */ public getCallbackTimeout(): number | undefined { // Use default timeout by default return; } /** Serializes this message into a Buffer */ public serialize(): Buffer { const ret = Buffer.allocUnsafe(this.payload.length + 5); ret[0] = MessageHeaders.SOF; // length of the following data, including the checksum ret[1] = this.payload.length + 3; // write the remaining data ret[2] = this.type; ret[3] = this.functionType; this.payload.copy(ret, 4); // followed by the checksum ret[ret.length - 1] = computeChecksum(ret); return ret; } /** Returns the number of bytes the first message in the buffer occupies */ public static getMessageLength(data: Buffer): number { const remainingLength = data[1]; return remainingLength + 2; } /** * Checks if there's enough data in the buffer to deserialize */ public static isComplete(data?: Buffer): boolean { if (!data || !data.length || data.length < 5) return false; // not yet const messageLength = Message.getMessageLength(data); if (data.length < messageLength) return false; // not yet return true; // probably, but the checksum may be wrong } /** * Retrieves the correct constructor for the next message in the given Buffer. * It is assumed that the buffer has been checked beforehand */ public static getConstructor(data: Buffer): MessageConstructor<Message> { return getMessageConstructor(data[2], data[3]) || Message; } /** Creates an instance of the message that is serialized in the given buffer */ public static from( host: ZWaveHost, options: MessageDeserializationOptions, ): Message { const Constructor = Message.getConstructor(options.data); const ret = new Constructor(host, options); return ret; } /** Returns the slice of data which represents the message payload */ public static extractPayload(data: Buffer): Buffer { const messageLength = Message.getMessageLength(data); const payloadLength = messageLength - 5; return data.slice(4, 4 + payloadLength); } /** Generates a representation of this Message for the log */ public toLogEntry(): MessageOrCCLogEntry { const tags = [ this.type === MessageType.Request ? "REQ" : "RES", FunctionType[this.functionType], ]; const nodeId = this.getNodeId(); if (nodeId) tags.unshift(getNodeTag(nodeId)); return { tags, message: this.payload.length > 0 ? { payload: `0x${this.payload.toString("hex")}` } : undefined, }; } /** Generates the JSON representation of this Message */ public toJSON(): JSONObject { return this.toJSONInternal(); } private toJSONInternal(): JSONObject { const ret: JSONObject = { name: this.constructor.name, type: MessageType[this.type], functionType: FunctionType[this.functionType] || num2hex(this.functionType), }; if (this.expectedResponse != null) ret.expectedResponse = FunctionType[this.functionType]; ret.payload = this.payload.toString("hex"); return ret; } private testMessage( msg: Message, predicate: Message["expectedResponse"], ): boolean { if (predicate == undefined) return false; if (typeof predicate === "number") { return msg.functionType === predicate; } if (staticExtends(predicate, Message)) { // predicate is a Message constructor return msg instanceof predicate; } else { // predicate is a ResponsePredicate return predicate(this, msg); } } /** Tests whether this message expects a response from the controller */ public expectsResponse(): boolean { return !!this.expectedResponse; } /** Tests whether this message expects a callback from the controller */ public expectsCallback(): boolean { // A message expects a callback... return ( // ...when it has a callback id that is not 0 (no callback) ((this.hasCallbackId() && this.callbackId !== 0) || // or the message type does not need a callback id to match the response !this.needsCallbackId()) && // and the expected callback is defined !!this.expectedCallback ); } /** Tests whether this message expects an update from the target node to finalize the transaction */ public expectsNodeUpdate(): boolean { // Most messages don't expect an update by default return false; } /** Checks if a message is an expected response for this message */ public isExpectedResponse(msg: Message): boolean { return ( msg.type === MessageType.Response && this.testMessage(msg, this.expectedResponse) ); } /** Checks if a message is an expected callback for this message */ public isExpectedCallback(msg: Message): boolean { if (msg.type !== MessageType.Request) return false; // If a received request included a callback id, enforce that the response contains the same if ( this.hasCallbackId() && (!msg.hasCallbackId() || this._callbackId !== msg._callbackId) ) { return false; } return this.testMessage(msg, this.expectedCallback); } /** Checks if a message is an expected node update for this message */ // eslint-disable-next-line @typescript-eslint/no-unused-vars public isExpectedNodeUpdate(msg: Message): boolean { // Most messages don't expect an update by default return false; } /** Finds the ID of the target or source node in a message, if it contains that information */ public getNodeId(): number | undefined { if (isNodeQuery(this)) return this.nodeId; // Override this in subclasses if a different behavior is desired } /** * Returns the node this message is linked to or undefined */ public getNodeUnsafe( applHost: ZWaveApplicationHost, ): IZWaveNode | undefined { const nodeId = this.getNodeId(); if (nodeId != undefined) return applHost.nodes.get(nodeId); } private _transmissionTimestamp: number | undefined; /** The timestamp when this message was (last) transmitted (in nanoseconds) */ public get transmissionTimestamp(): number | undefined { return this._transmissionTimestamp; } /** Marks this message as sent and sets the transmission timestamp */ public markAsSent(): void { this._transmissionTimestamp = highResTimestamp(); this._completedTimestamp = undefined; } private _completedTimestamp: number | undefined; public get completedTimestamp(): number | undefined { return this._completedTimestamp; } /** Marks this message as completed and sets the corresponding timestamp */ public markAsCompleted(): void { this._completedTimestamp = highResTimestamp(); } /** Returns the round trip time of this message from transmission until completion. */ public get rtt(): number | undefined { if (this._transmissionTimestamp == undefined) return undefined; if (this._completedTimestamp == undefined) return undefined; return this._completedTimestamp - this._transmissionTimestamp; } } /** Computes the checksum for a serialized message as defined in the Z-Wave specs */ function computeChecksum(message: Buffer): number { let ret = 0xff; // exclude SOF and checksum byte from the computation for (let i = 1; i < message.length - 1; i++) { ret ^= message[i]; } return ret; } // ======================= // use decorators to link function types to message classes export type ResponseRole = | "unexpected" // a message that does not belong to this transaction | "confirmation" // a confirmation response, e.g. controller reporting that a message was sent | "final" // a final response (leading to a resolved transaction) | "fatal_controller" // a response from the controller that leads to a rejected transaction | "fatal_node"; // a response or (lack thereof) from the node that leads to a rejected transaction/** /** * A predicate function to test if a received message matches to the sent message */ export type ResponsePredicate<TSent extends Message = Message> = ( sentMessage: TSent, receivedMessage: Message, ) => boolean; function getMessageTypeMapKey( messageType: MessageType, functionType: FunctionType, ): string { return JSON.stringify({ messageType, functionType }); } const messageTypesDecorator = createReflectionDecorator< Message, [messageType: MessageType, functionType: FunctionType], { messageType: MessageType; functionType: FunctionType }, MessageConstructor<Message> >({ name: "messageTypes", valueFromArgs: (messageType, functionType) => ({ messageType, functionType, }), constructorLookupKey(target, messageType, functionType) { return getMessageTypeMapKey(messageType, functionType); }, }); /** * Defines the message and function type associated with a Z-Wave message */ export const messageTypes = messageTypesDecorator.decorator; /** * Retrieves the message type defined for a Z-Wave message class */ export function getMessageType<T extends Message>( messageClass: T, ): MessageType | undefined { return messageTypesDecorator.lookupValue(messageClass)?.messageType; } /** * Retrieves the message type defined for a Z-Wave message class */ export function getMessageTypeStatic<T extends MessageConstructor<Message>>( classConstructor: T, ): MessageType | undefined { return messageTypesDecorator.lookupValueStatic(classConstructor) ?.messageType; } /** * Retrieves the function type defined for a Z-Wave message class */ export function getFunctionType<T extends Message>( messageClass: T, ): FunctionType | undefined { return messageTypesDecorator.lookupValue(messageClass)?.functionType; } /** * Retrieves the function type defined for a Z-Wave message class */ export function getFunctionTypeStatic<T extends MessageConstructor<Message>>( classConstructor: T, ): FunctionType | undefined { return messageTypesDecorator.lookupValueStatic(classConstructor) ?.functionType; } /** * Looks up the message constructor for a given message type and function type */ function getMessageConstructor( messageType: MessageType, functionType: FunctionType, ): MessageConstructor<Message> | undefined { return messageTypesDecorator.lookupConstructorByKey( getMessageTypeMapKey(messageType, functionType), ); } const expectedResponseDecorator = createReflectionDecorator< Message, [typeOrPredicate: FunctionType | typeof Message | ResponsePredicate], FunctionType | typeof Message | ResponsePredicate, MessageConstructor<Message> >({ name: "expectedResponse", valueFromArgs: (typeOrPredicate) => typeOrPredicate, constructorLookupKey: false, }); /** * Defines the expected response function type or message class for a Z-Wave message */ export const expectedResponse = expectedResponseDecorator.decorator; /** * Retrieves the expected response function type or message class defined for a Z-Wave message class */ export function getExpectedResponse<T extends Message>( messageClass: T, ): FunctionType | typeof Message | ResponsePredicate | undefined { return expectedResponseDecorator.lookupValue(messageClass); } /** * Retrieves the function type defined for a Z-Wave message class */ export function getExpectedResponseStatic< T extends MessageConstructor<Message>, >( classConstructor: T, ): FunctionType | typeof Message | ResponsePredicate | undefined { return expectedResponseDecorator.lookupValueStatic(classConstructor); } const expectedCallbackDecorator = createReflectionDecorator< Message, [typeOrPredicate: FunctionType | typeof Message | ResponsePredicate], FunctionType | typeof Message | ResponsePredicate, MessageConstructor<Message> >({ name: "expectedCallback", valueFromArgs: (typeOrPredicate) => typeOrPredicate, constructorLookupKey: false, }); /** * Defines the expected callback function type or message class for a Z-Wave message */ export function expectedCallback<TSent extends Message>( typeOrPredicate: FunctionType | typeof Message | ResponsePredicate<TSent>, ): TypedClassDecorator<Message> { return expectedCallbackDecorator.decorator(typeOrPredicate as any); } /** * Retrieves the expected callback function type or message class defined for a Z-Wave message class */ export function getExpectedCallback<T extends Message>( messageClass: T, ): FunctionType | typeof Message | ResponsePredicate | undefined { return expectedCallbackDecorator.lookupValue(messageClass); } /** * Retrieves the function type defined for a Z-Wave message class */ export function getExpectedCallbackStatic< T extends MessageConstructor<Message>, >( classConstructor: T, ): FunctionType | typeof Message | ResponsePredicate | undefined { return expectedCallbackDecorator.lookupValueStatic(classConstructor); } const priorityDecorator = createReflectionDecorator< Message, [prio: MessagePriority], MessagePriority >({ name: "priority", valueFromArgs: (priority) => priority, constructorLookupKey: false, }); /** * Defines the default priority associated with a Z-Wave message */ export const priority = priorityDecorator.decorator; /** * Retrieves the default priority defined for a Z-Wave message class */ export function getDefaultPriority<T extends Message>( messageClass: T, ): MessagePriority | undefined { return priorityDecorator.lookupValue(messageClass); } /** * Retrieves the default priority defined for a Z-Wave message class */ export function getDefaultPriorityStatic<T extends MessageConstructor<Message>>( classConstructor: T, ): MessagePriority | undefined { return priorityDecorator.lookupValueStatic(classConstructor); }