inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
352 lines (326 loc) • 9.17 kB
text/typescript
import { getImplementedVersion } from "@zwave-js/cc";
import { ConfigManager } from "@zwave-js/config";
import {
CommandClasses,
FLiRS,
InterviewStage,
IZWaveEndpoint,
IZWaveNode,
Maybe,
MessagePriority,
NodeStatus,
SecurityClass,
securityClassOrder,
unknownBoolean,
} from "@zwave-js/core";
import type { ZWaveHost } from "@zwave-js/host";
import {
expectedResponse,
FunctionType,
Message,
MessageType,
messageTypes,
priority,
} from "@zwave-js/serial";
import type { Driver } from "../driver/Driver";
import type { ZWaveNode } from "../node/Node";
import * as nodeUtils from "../node/utils";
import { SendDataRequest } from "../serialapi/transport/SendDataMessages";
const MockRequestMessageWithExpectation_FunctionType =
0xfa as unknown as FunctionType;
const MockRequestMessageWithoutExpectation_FunctionType =
0xfb as unknown as FunctionType;
const MockResponseMessage_FunctionType = 0xff as unknown as FunctionType;
(
MessageType.Request,
MockRequestMessageWithExpectation_FunctionType,
)
(MockResponseMessage_FunctionType)
(MessagePriority.Normal)
export class MockRequestMessageWithExpectation extends Message {}
(
MessageType.Request,
MockRequestMessageWithoutExpectation_FunctionType,
)
(MessagePriority.Normal)
export class MockRequestMessageWithoutExpectation extends Message {}
(MessageType.Response, MockResponseMessage_FunctionType)
export class MockResponseMessage extends Message {}
export const mockDriverDummyCallbackId = 0xfe;
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function createEmptyMockDriver() {
const ret = {
sendMessage: jest.fn().mockImplementation(() => Promise.resolve()),
sendCommand: jest.fn(),
getSafeCCVersionForNode: jest
.fn()
.mockImplementation(
(
ccId: CommandClasses,
nodeId: number,
endpointIndex: number = 0,
) => {
if (
ret.controller?.nodes instanceof Map &&
ret.controller.nodes.has(nodeId)
) {
const node: ZWaveNode =
ret.controller.nodes.get(nodeId);
const ccVersion = node
.getEndpoint(endpointIndex)!
.getCCVersion(ccId);
if (ccVersion > 0) return ccVersion;
}
// default to the implemented version
return getImplementedVersion(ccId);
},
),
isCCSecure: jest.fn().mockImplementation(() => false),
getNextCallbackId: jest
.fn()
.mockImplementation(() => mockDriverDummyCallbackId),
controller: {
nodes: new Map(),
ownNodeId: 1,
},
get nodes() {
return ret.controller.nodes;
},
get ownNodeId() {
return ret.controller.ownNodeId;
},
valueDB: new Map(),
getValueDB: (nodeId: number) => {
return ret.controller.nodes.get(nodeId).valueDB;
},
metadataDB: new Map(),
networkCache: new Map(),
cacheGet: jest.fn().mockImplementation(
<T>(
cacheKey: string,
options?: {
reviver?: (value: any) => T;
},
): T | undefined => {
let _ret = ret.networkCache.get(cacheKey);
if (
_ret !== undefined &&
typeof options?.reviver === "function"
) {
try {
_ret = options.reviver(_ret);
} catch {
// ignore, invalid entry
}
}
return _ret;
},
),
cacheSet: jest.fn().mockImplementation(
<T>(
cacheKey: string,
value: T | undefined,
options?: {
serializer?: (value: T) => any;
},
): void => {
if (
value !== undefined &&
typeof options?.serializer === "function"
) {
value = options.serializer(value);
}
if (value === undefined) {
ret.networkCache.delete(cacheKey);
} else {
ret.networkCache.set(cacheKey, value);
}
},
),
options: {
timeouts: {
ack: 1000,
byte: 150,
response: 1600,
report: 1600,
nonce: 5000,
sendDataCallback: 65000,
},
attempts: {
sendData: 3,
controller: 3,
},
},
driverLog: new Proxy(
{},
{
get() {
return () => {
/* intentionally empty */
};
},
},
),
controllerLog: new Proxy(
{},
{
get() {
return () => {
/* intentionally empty */
};
},
},
),
configManager: new ConfigManager(),
};
ret.sendCommand.mockImplementation(async (command, options) => {
const msg = new SendDataRequest(ret as unknown as Driver, {
command,
});
const resp = await ret.sendMessage(msg, options);
return resp?.command;
});
return ret;
}
export interface CreateTestNodeOptions {
id: number;
isListening?: boolean | undefined;
isFrequentListening?: FLiRS | undefined;
status?: NodeStatus;
interviewStage?: InterviewStage;
isSecure?: Maybe<boolean>;
numEndpoints?: number;
supportsCC?: (cc: CommandClasses) => boolean;
controlsCC?: (cc: CommandClasses) => boolean;
isCCSecure?: (cc: CommandClasses) => boolean;
getCCVersion?: (cc: CommandClasses) => number;
}
export interface TestNode extends IZWaveNode {
setEndpoint(endpoint: CreateTestEndpointOptions): void;
}
export function createTestNode(
host: ZWaveHost,
options: CreateTestNodeOptions,
): TestNode {
const endpointCache = new Map<number, IZWaveEndpoint>();
const securityClasses = new Map<SecurityClass, boolean>();
const ret: TestNode = {
id: options.id,
...createTestEndpoint(host, {
nodeId: options.id,
index: 0,
supportsCC: options.supportsCC,
controlsCC: options.controlsCC,
isCCSecure: options.isCCSecure,
getCCVersion: options.getCCVersion,
}),
isListening: options.isListening ?? true,
isFrequentListening: options.isFrequentListening ?? false,
get canSleep() {
if (ret.isListening == undefined) return undefined;
if (ret.isFrequentListening == undefined) return undefined;
return !ret.isListening && !ret.isFrequentListening;
},
status:
options.status ??
(options.isListening ? NodeStatus.Alive : NodeStatus.Asleep),
interviewStage: options.interviewStage ?? InterviewStage.Complete,
setEndpoint: (endpoint) => {
endpointCache.set(
endpoint.index,
createTestEndpoint(host, {
nodeId: options.id,
index: endpoint.index,
supportsCC: endpoint.supportsCC ?? options.supportsCC,
controlsCC: endpoint.controlsCC ?? options.controlsCC,
isCCSecure: endpoint.isCCSecure ?? options.isCCSecure,
getCCVersion: endpoint.getCCVersion ?? options.getCCVersion,
}),
);
},
getEndpoint: ((index: number) => {
// When the endpoint count is known, return undefined for non-existent endpoints
if (
options.numEndpoints != undefined &&
index > options.numEndpoints
) {
return undefined;
}
if (!endpointCache.has(index)) {
ret.setEndpoint(
createTestEndpoint(host, {
nodeId: options.id,
index,
supportsCC: options.supportsCC,
controlsCC: options.controlsCC,
isCCSecure: options.isCCSecure,
getCCVersion: options.getCCVersion,
}),
);
}
return endpointCache.get(index);
}) as IZWaveNode["getEndpoint"],
// These are copied from Node.ts
getHighestSecurityClass(): SecurityClass | undefined {
if (securityClasses.size === 0) return undefined;
let missingSome = false;
for (const secClass of securityClassOrder) {
if (securityClasses.get(secClass) === true) return secClass;
if (!securityClasses.has(secClass)) {
missingSome = true;
}
}
// If we don't have the info for every security class, we don't know the highest one yet
return missingSome ? undefined : SecurityClass.None;
},
hasSecurityClass(securityClass: SecurityClass): Maybe<boolean> {
return securityClasses.get(securityClass) ?? unknownBoolean;
},
setSecurityClass(securityClass: SecurityClass, granted: boolean): void {
securityClasses.set(securityClass, granted);
},
get isSecure(): Maybe<boolean> {
const securityClass = ret.getHighestSecurityClass();
if (securityClass == undefined) return unknownBoolean;
if (securityClass === SecurityClass.None) return false;
return true;
},
};
endpointCache.set(0, ret);
// If the number of endpoints are given, use them as the individual endpoint count
if (options.numEndpoints != undefined) {
nodeUtils.setIndividualEndpointCount(host, ret, options.numEndpoints);
nodeUtils.setAggregatedEndpointCount(host, ret, 0);
nodeUtils.setMultiChannelInterviewComplete(host, ret, true);
}
return ret;
}
export interface CreateTestEndpointOptions {
nodeId: number;
index: number;
supportsCC?: (cc: CommandClasses) => boolean;
controlsCC?: (cc: CommandClasses) => boolean;
isCCSecure?: (cc: CommandClasses) => boolean;
getCCVersion?: (cc: CommandClasses) => number;
}
export function createTestEndpoint(
host: ZWaveHost,
options: CreateTestEndpointOptions,
): IZWaveEndpoint {
const ret: IZWaveEndpoint = {
nodeId: options.nodeId,
index: options.index,
supportsCC: options.supportsCC ?? (() => true),
controlsCC: options.controlsCC ?? (() => false),
isCCSecure: options.isCCSecure ?? (() => false),
getCCVersion:
options.getCCVersion ??
((cc) =>
host.getSafeCCVersionForNode(
cc,
options.nodeId,
options.index,
)),
};
return ret;
}