UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

326 lines (287 loc) 8.29 kB
import { validatePayload, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core/safe"; import { getEnumMemberName, TypedClassDecorator } from "@zwave-js/shared/safe"; import "reflect-metadata"; enum S2ExtensionType { SPAN = 0x01, MPAN = 0x02, MGRP = 0x03, MOS = 0x04, } const METADATA_S2ExtensionMap = Symbol("S2ExtensionMap"); const METADATA_S2Extension = Symbol("S2Extension"); type S2ExtensionMap = Map< S2ExtensionType, Security2ExtensionConstructor<Security2Extension> >; export type Security2ExtensionConstructor<T extends Security2Extension> = typeof Security2Extension & { new (options: Security2ExtensionOptions): T; }; /** * Looks up the S2 extension constructor for a given S2 extension type */ export function getS2ExtensionConstructor( type: S2ExtensionType, ): Security2ExtensionConstructor<Security2Extension> | undefined { // Retrieve the constructor map from the CommandClass class const map = Reflect.getMetadata( METADATA_S2ExtensionMap, Security2Extension, ) as S2ExtensionMap | undefined; return map?.get(type); } /** * Defines the command class associated with a Z-Wave message */ export function extensionType( type: S2ExtensionType, ): TypedClassDecorator<Security2Extension> { return (extensionClass) => { Reflect.defineMetadata(METADATA_S2Extension, type, extensionClass); const map: S2ExtensionMap = Reflect.getMetadata(METADATA_S2ExtensionMap, Security2Extension) || new Map(); map.set( type, extensionClass as unknown as Security2ExtensionConstructor<Security2Extension>, ); Reflect.defineMetadata( METADATA_S2ExtensionMap, map, Security2Extension, ); }; } /** * Retrieves the command class defined for a Z-Wave message class */ export function getExtensionType<T extends Security2Extension>( ext: T, ): S2ExtensionType { // get the class constructor const constr = ext.constructor; // retrieve the current metadata const ret: S2ExtensionType | undefined = Reflect.getMetadata( METADATA_S2Extension, constr, ); if (ret == undefined) { throw new ZWaveError( `No S2 extension type defined for ${constr.name}!`, ZWaveErrorCodes.CC_Invalid, ); } return ret; } interface Security2ExtensionCreationOptions { critical: boolean; payload?: Buffer; } interface Security2ExtensionDeserializationOptions { data: Buffer; } type Security2ExtensionOptions = | Security2ExtensionCreationOptions | Security2ExtensionDeserializationOptions; function gotDeserializationOptions( options: Record<string, any>, ): options is Security2ExtensionDeserializationOptions { return "data" in options && Buffer.isBuffer(options.data); } export class Security2Extension { public constructor(options: Security2ExtensionOptions) { if (gotDeserializationOptions(options)) { validatePayload(options.data.length >= 2); const totalLength = options.data[0]; validatePayload(options.data.length >= totalLength); const controlByte = options.data[1]; this.moreToFollow = !!(controlByte & 0b1000_0000); this.critical = !!(controlByte & 0b0100_0000); this.type = controlByte & 0b11_1111; this.payload = options.data.slice(2, totalLength); } else { this.type = getExtensionType(this); this.critical = options.critical; this.payload = options.payload ?? Buffer.allocUnsafe(0); } } public type: S2ExtensionType; public critical: boolean; public readonly moreToFollow?: boolean; public payload: Buffer; public isEncrypted(): boolean { return false; } public serialize(moreToFollow: boolean): Buffer { return Buffer.concat([ Buffer.from([ 2 + this.payload.length, (moreToFollow ? 0b1000_0000 : 0) | (this.critical ? 0b0100_0000 : 0) | (this.type & 0b11_1111), ]), this.payload, ]); } /** Returns the number of bytes the first extension in the buffer occupies */ public static getExtensionLength(data: Buffer): number { return data[0]; } /** Returns the number of bytes the serialized extension will occupy */ public computeLength(): number { return 2 + this.payload.length; } /** * Retrieves the correct constructor for the next extension in the given Buffer. * It is assumed that the buffer has been checked beforehand */ public static getConstructor( data: Buffer, ): Security2ExtensionConstructor<Security2Extension> { const type = data[1] & 0b11_1111; return getS2ExtensionConstructor(type) ?? Security2Extension; } /** Creates an instance of the S2 extension that is serialized in the given buffer */ public static from(data: Buffer): Security2Extension { const Constructor = Security2Extension.getConstructor(data); const ret = new Constructor({ data }); return ret; } public toLogEntry(): string { let ret = ` · type: ${getEnumMemberName(S2ExtensionType, this.type)}`; if (this.payload.length > 0) { ret += ` payload: 0x${this.payload.toString("hex")}`; } return ret; } } interface SPANExtensionOptions { senderEI: Buffer; } @extensionType(S2ExtensionType.SPAN) export class SPANExtension extends Security2Extension { public constructor( options: | Security2ExtensionDeserializationOptions | SPANExtensionOptions, ) { if (gotDeserializationOptions(options)) { super(options); validatePayload(this.payload.length === 16); this.senderEI = this.payload; } else { super({ critical: true }); if (options.senderEI.length !== 16) { throw new ZWaveError( "The sender's entropy must be a 16-byte buffer!", ZWaveErrorCodes.Argument_Invalid, ); } this.senderEI = options.senderEI; } } public senderEI: Buffer; public serialize(moreToFollow: boolean): Buffer { this.payload = this.senderEI; return super.serialize(moreToFollow); } public toLogEntry(): string { let ret = super.toLogEntry().replace(/^ payload:.+$/m, ""); ret += ` sender EI: 0x${this.senderEI.toString("hex")}`; return ret; } } interface MPANExtensionOptions { groupId: number; innerMPANState: Buffer; } @extensionType(S2ExtensionType.MPAN) export class MPANExtension extends Security2Extension { public constructor( options: | Security2ExtensionDeserializationOptions | MPANExtensionOptions, ) { if (gotDeserializationOptions(options)) { super(options); validatePayload(this.payload.length === 17); this.groupId = this.payload[0]; this.innerMPANState = this.payload.slice(1); } else { if (options.innerMPANState.length !== 16) { throw new ZWaveError( "The inner MPAN state must be a 16-byte buffer!", ZWaveErrorCodes.Argument_Invalid, ); } super({ critical: true }); this.groupId = options.groupId; this.innerMPANState = options.innerMPANState; } } public groupId: number; public innerMPANState: Buffer; public isEncrypted(): boolean { return true; } public serialize(moreToFollow: boolean): Buffer { this.payload = Buffer.concat([ Buffer.from([this.groupId]), this.innerMPANState, ]); return super.serialize(moreToFollow); } public toLogEntry(): string { let ret = super.toLogEntry().replace(/^ payload:.+$/m, ""); ret += ` group ID: ${this.groupId} MPAN state: 0x${this.innerMPANState.toString("hex")}`; return ret; } } interface MGRPExtensionOptions { groupId: number; } @extensionType(S2ExtensionType.MGRP) export class MGRPExtension extends Security2Extension { public constructor( options: | Security2ExtensionDeserializationOptions | MGRPExtensionOptions, ) { if (gotDeserializationOptions(options)) { super(options); validatePayload(this.payload.length === 1); this.groupId = this.payload[0]; } else { super({ critical: true }); this.groupId = options.groupId; } } public groupId: number; public serialize(moreToFollow: boolean): Buffer { this.payload = Buffer.from([this.groupId]); return super.serialize(moreToFollow); } public toLogEntry(): string { let ret = super.toLogEntry().replace(/^ payload:.+$/m, ""); ret += ` group ID: ${this.groupId}`; return ret; } } @extensionType(S2ExtensionType.MOS) export class MOSExtension extends Security2Extension { public constructor(options?: Security2ExtensionDeserializationOptions) { if (options && gotDeserializationOptions(options)) { super(options); } else { super({ critical: false }); } } }