inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
341 lines (303 loc) • 9.2 kB
text/typescript
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, []),
]);
}