inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
624 lines (552 loc) • 19.1 kB
text/typescript
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);
}