UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

341 lines (303 loc) 9.2 kB
import { sum } from "@zwave-js/shared/safe"; import { validatePayload } from "../util/misc"; import { CommandClasses } from "./CommandClasses"; export interface ApplicationNodeInformation { genericDeviceClass: number; specificDeviceClass: number; supportedCCs: CommandClasses[]; } export function parseApplicationNodeInformation( nif: Buffer, ): ApplicationNodeInformation { validatePayload(nif.length >= 2); return { genericDeviceClass: nif[0], specificDeviceClass: nif[1], supportedCCs: parseCCList(nif.slice(2)).supportedCCs, }; } export interface NodeUpdatePayload extends ApplicationNodeInformation { nodeId: number; basicDeviceClass: number; } export function parseNodeUpdatePayload(nif: Buffer): NodeUpdatePayload { const nodeId = nif[0]; const remainingLength = nif[1]; validatePayload(nif.length >= 2 + remainingLength); return { nodeId, basicDeviceClass: nif[2], ...parseApplicationNodeInformation(nif.slice(3, 2 + remainingLength)), }; } export function encodeNodeUpdatePayload(nif: NodeUpdatePayload): Buffer { const ccList = encodeCCList(nif.supportedCCs, []); return Buffer.concat([ Buffer.from([ nif.nodeId, 3 + ccList.length, nif.basicDeviceClass, nif.genericDeviceClass, nif.specificDeviceClass, ]), ccList, ]); } export function isExtendedCCId(ccId: CommandClasses): boolean { return ccId >= 0xf1; } /** * Reads a CC id from the given buffer, returning the parsed CC id and the number of bytes read * @param offset The offset at which the CC id is located */ export function parseCCId( payload: Buffer, offset: number = 0, ): { ccId: CommandClasses; bytesRead: number } { const isExtended = isExtendedCCId(payload[offset]); validatePayload(payload.length >= offset + (isExtended ? 2 : 1)); if (isExtended) { return { ccId: payload.readUInt16BE(offset), bytesRead: 2 }; } else { return { ccId: payload.readUInt8(offset), bytesRead: 1 }; } } /** * Writes the given CC id into the given buffer at the given location * @returns The number of bytes written */ export function encodeCCId( ccId: CommandClasses, payload: Buffer, offset: number = 0, ): number { if (isExtendedCCId(ccId)) { payload.writeUInt16BE(ccId, offset); return 2; } else { payload.writeUInt8(ccId, offset); return 1; } } export function parseCCList(payload: Buffer): { supportedCCs: CommandClasses[]; controlledCCs: CommandClasses[]; } { const ret = { supportedCCs: [] as CommandClasses[], controlledCCs: [] as CommandClasses[], }; let offset = 0; let isAfterMark = false; while (offset < payload.length) { // Read either the normal or extended ccId const { ccId: cc, bytesRead } = parseCCId(payload, offset); offset += bytesRead; // CCs before the support/control mark are supported // CCs after the support/control mark are controlled if (cc === CommandClasses["Support/Control Mark"]) { isAfterMark = true; continue; } (isAfterMark ? ret.controlledCCs : ret.supportedCCs).push(cc); } return ret; } export function encodeCCList( supportedCCs: readonly CommandClasses[], controlledCCs: readonly CommandClasses[], ): Buffer { const bufferLength = sum(supportedCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))) + (controlledCCs.length > 0 ? 1 : 0) + // support/control mark sum(controlledCCs.map((cc) => (isExtendedCCId(cc) ? 2 : 1))); const ret = Buffer.allocUnsafe(bufferLength); let offset = 0; for (const cc of supportedCCs) { offset += encodeCCId(cc, ret, offset); } if (controlledCCs.length > 0) { ret[offset++] = CommandClasses["Support/Control Mark"]; for (const cc of controlledCCs) { offset += encodeCCId(cc, ret, offset); } } return ret; } export enum ProtocolVersion { "unknown" = 0, "2.0" = 1, "4.2x / 5.0x" = 2, "4.5x / 6.0x" = 3, } export type FLiRS = false | "250ms" | "1000ms"; export type DataRate = 9600 | 40000 | 100000; export enum NodeType { Controller, /** @deprecated Use `NodeType["End Node"]` instead */ "Routing End Node", "End Node" = 1, } export interface NodeProtocolInfo { /** Whether this node is always listening or not */ isListening: boolean; /** Indicates the wakeup interval if this node is a FLiRS node. `false` if it isn't. */ isFrequentListening: FLiRS; /** Whether the node supports routing/forwarding messages. */ isRouting: boolean; supportedDataRates: DataRate[]; protocolVersion: ProtocolVersion; /** Whether this node supports additional CCs besides the mandatory minimum */ optionalFunctionality: boolean; /** Whether this node is a controller (can calculate routes) or an end node (relies on route info) */ nodeType: NodeType; /** Whether this node supports (legacy) network security */ supportsSecurity: boolean; /** Whether this node can issue wakeup beams to FLiRS nodes */ supportsBeaming: boolean; /** Whether this node's device class has the specific part */ hasSpecificDeviceClass: boolean; } export interface NodeProtocolInfoAndDeviceClass extends Omit<NodeProtocolInfo, "hasSpecificDeviceClass"> { basicDeviceClass: number; genericDeviceClass: number; specificDeviceClass: number; } export type NodeInformationFrame = NodeProtocolInfoAndDeviceClass & ApplicationNodeInformation; export function parseNodeProtocolInfo( buffer: Buffer, offset: number, ): NodeProtocolInfo { validatePayload(buffer.length >= offset + 3); const isListening = !!(buffer[offset] & 0b10_000_000); const isRouting = !!(buffer[offset] & 0b01_000_000); const supportedDataRates: DataRate[] = []; const maxSpeed = buffer[offset] & 0b00_011_000; const speedExtension = buffer[offset + 2] & 0b111; if (maxSpeed & 0b00_010_000) { supportedDataRates.push(40000); } if (maxSpeed & 0b00_001_000) { supportedDataRates.push(9600); } if (speedExtension & 0b001) { supportedDataRates.push(100000); } if (supportedDataRates.length === 0) { supportedDataRates.push(9600); } const protocolVersion = buffer[offset] & 0b111; const capability = buffer[offset + 1]; const optionalFunctionality = !!(capability & 0b1000_0000); let isFrequentListening: FLiRS; switch (capability & 0b0110_0000) { case 0b0100_0000: isFrequentListening = "1000ms"; break; case 0b0010_0000: isFrequentListening = "250ms"; break; default: isFrequentListening = false; } const supportsBeaming = !!(capability & 0b0001_0000); let nodeType: NodeType; switch (capability & 0b1010) { case 0b1000: nodeType = NodeType["End Node"]; break; case 0b0010: default: nodeType = NodeType.Controller; break; } const hasSpecificDeviceClass = !!(capability & 0b100); const supportsSecurity = !!(capability & 0b1); return { isListening, isFrequentListening, isRouting, supportedDataRates, protocolVersion, optionalFunctionality, nodeType, supportsSecurity, supportsBeaming, hasSpecificDeviceClass, }; } export function encodeNodeProtocolInfo(info: NodeProtocolInfo): Buffer { const ret = Buffer.alloc(3, 0); // Byte 0 and 2 if (info.isListening) ret[0] |= 0b10_000_000; if (info.isRouting) ret[0] |= 0b01_000_000; if (info.supportedDataRates.includes(40000)) ret[0] |= 0b00_010_000; if (info.supportedDataRates.includes(9600)) ret[0] |= 0b00_001_000; if (info.supportedDataRates.includes(100000)) ret[2] |= 0b001; ret[0] |= info.protocolVersion & 0b111; // Byte 1 if (info.optionalFunctionality) ret[1] |= 0b1000_0000; if (info.isFrequentListening === "1000ms") ret[1] |= 0b0100_0000; else if (info.isFrequentListening === "250ms") ret[1] |= 0b0010_0000; if (info.supportsBeaming) ret[1] |= 0b0001_0000; if (info.supportsSecurity) ret[1] |= 0b1; if (info.nodeType === NodeType["End Node"]) ret[1] |= 0b1000; else ret[1] |= 0b0010; // Controller if (info.hasSpecificDeviceClass) ret[1] |= 0b100; return ret; } export function parseNodeProtocolInfoAndDeviceClass(buffer: Buffer): { info: NodeProtocolInfoAndDeviceClass; bytesRead: number; } { validatePayload(buffer.length >= 5); const protocolInfo = parseNodeProtocolInfo(buffer, 0); let offset = 3; const basic = buffer[offset++]; const generic = buffer[offset++]; let specific = 0; if (protocolInfo.hasSpecificDeviceClass) { validatePayload(buffer.length >= offset + 1); specific = buffer[offset++]; } return { info: { ...protocolInfo, basicDeviceClass: basic, genericDeviceClass: generic, specificDeviceClass: specific, }, bytesRead: offset, }; } export function encodeNodeProtocolInfoAndDeviceClass( info: NodeProtocolInfoAndDeviceClass, ): Buffer { return Buffer.concat([ encodeNodeProtocolInfo({ ...info, hasSpecificDeviceClass: true }), Buffer.from([ info.basicDeviceClass, info.genericDeviceClass, info.specificDeviceClass, ]), ]); } export function parseNodeInformationFrame( buffer: Buffer, ): NodeInformationFrame { const { info, bytesRead: offset } = parseNodeProtocolInfoAndDeviceClass(buffer); const supportedCCs = parseCCList(buffer.slice(offset)).supportedCCs; return { ...info, supportedCCs, }; } export function encodeNodeInformationFrame(info: NodeInformationFrame): Buffer { return Buffer.concat([ encodeNodeProtocolInfoAndDeviceClass(info), encodeCCList(info.supportedCCs, []), ]); }