UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

1,797 lines (1,597 loc) 55.2 kB
import { BasicCommand, BinarySwitchCommand, EntryControlCommand, EntryControlDataTypes, EntryControlEventTypes, getCCConstructor, WakeUpCommand, } from "@zwave-js/cc"; import { BasicCC, BasicCCValues } from "@zwave-js/cc/BasicCC"; import { BinarySwitchCCReport } from "@zwave-js/cc/BinarySwitchCC"; import { EntryControlCCNotification } from "@zwave-js/cc/EntryControlCC"; import { NoOperationCC } from "@zwave-js/cc/NoOperationCC"; import { WakeUpCC } from "@zwave-js/cc/WakeUpCC"; import { applicationCCs, assertZWaveError, CommandClasses, CommandClassInfo, getCCName, NodeType, nonApplicationCCs, ProtocolVersion, topologicalSort, ValueDB, ValueID, ValueMetadata, ZWaveErrorCodes, } from "@zwave-js/core"; import type { ThrowingMap } from "@zwave-js/shared"; import { MockController } from "@zwave-js/testing"; import { wait } from "alcalzone-shared/async"; import { createDefaultMockControllerBehaviors } from "../../Utils"; import type { Driver } from "../driver/Driver"; import { createAndStartTestingDriver } from "../driver/DriverMock"; import { GetNodeProtocolInfoRequest, GetNodeProtocolInfoResponse, } from "../serialapi/network-mgmt/GetNodeProtocolInfoMessages"; import { RequestNodeInfoRequest } from "../serialapi/network-mgmt/RequestNodeInfoMessages"; import { SendDataRequest } from "../serialapi/transport/SendDataMessages"; import { assertCC } from "../test/assertCC"; import { createEmptyMockDriver } from "../test/mocks"; import { DeviceClass } from "./DeviceClass"; import { ZWaveNode } from "./Node"; import { InterviewStage, NodeStatus, ZWaveNodeEvents } from "./_Types"; /** This is an ugly hack to be able to test the private methods without resorting to @internal */ class TestNode extends ZWaveNode { public async queryProtocolInfo(): Promise<void> { return super.queryProtocolInfo(); } public async ping(): Promise<boolean> { return super.ping(); } public async queryNodeInfo(): Promise<void> { return super["queryNodeInfo"](); } public async interviewCCs(): Promise<boolean> { return super.interviewCCs(); } // public async queryEndpoints(): Promise<void> { // return super.queryEndpoints(); // } // public async configureWakeup(): Promise<void> { // return super.configureWakeup(); // } public get implementedCommandClasses(): Map< CommandClasses, CommandClassInfo > { return super.implementedCommandClasses as any; } } describe("lib/node/Node", () => { beforeAll(async () => { // Load all CCs manually to populate the metadata await import("@zwave-js/cc/BatteryCC"); await import("@zwave-js/cc/ThermostatSetpointCC"); await import("@zwave-js/cc/VersionCC"); }); describe("constructor", () => { let driver: Driver; // let node2: ZWaveNode; let controller: MockController; beforeAll( async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); await driver.configManager.loadDeviceClasses(); }, // Loading configuration may take a while on CI 30000, ); afterAll(async () => { await driver.destroy(); }); afterEach(() => { driver.networkCache.clear(); }); it("stores the given Node ID", () => { const node1 = new ZWaveNode(1, driver); expect(node1.id).toBe(1); const node3 = new ZWaveNode(3, driver); expect(node3.id).toBe(3); node1.destroy(); node3.destroy(); }); it("stores the given device class", () => { function makeNode(cls: DeviceClass): ZWaveNode { return new ZWaveNode(1, driver, cls); } const nodeUndef = makeNode(undefined as any); expect(nodeUndef.deviceClass).toBeUndefined(); const devCls = new DeviceClass( driver.configManager, 0x02, 0x01, 0x03, ); const nodeWithClass = makeNode(devCls); expect(nodeWithClass.deviceClass).toBe(devCls); nodeUndef.destroy(); nodeWithClass.destroy(); }); it("remembers all given command classes", () => { function makeNode( supportedCCs: CommandClasses[] = [], controlledCCs: CommandClasses[] = [], ): ZWaveNode { return new ZWaveNode( 1, driver, undefined, supportedCCs, controlledCCs, ); } const tests: { supported: CommandClasses[]; controlled: CommandClasses[]; }[] = [ { supported: [CommandClasses["Anti-Theft"]], controlled: [CommandClasses.Basic], }, ]; for (const { supported, controlled } of tests) { const node = makeNode(supported, controlled); for (const supp of supported) { expect(node.supportsCC(supp)).toBeTrue(); } for (const ctrl of controlled) { expect(node.controlsCC(ctrl)).toBeTrue(); } node.destroy(); } }); it("initializes the node's value DB", () => { const node = new ZWaveNode(1, driver); expect(node.valueDB).toBeInstanceOf(ValueDB); node.destroy(); }); it("marks the mandatory CCs as supported/controlled", () => { // Portable Scene Controller const deviceClass = new DeviceClass( driver.configManager, 0x01, 0x01, 0x02, ); const node = new ZWaveNode(1, driver, deviceClass); expect(node.supportsCC(CommandClasses.Association)).toBeTrue(); expect( node.supportsCC( CommandClasses["Scene Controller Configuration"], ), ).toBeTrue(); expect( node.supportsCC(CommandClasses["Manufacturer Specific"]), ).toBeTrue(); expect(node.controlsCC(CommandClasses["Scene Activation"])); node.destroy(); }); }); describe("interview()", () => { let fakeDriver: ReturnType<typeof createEmptyMockDriver>; let node: ZWaveNode; beforeAll( async () => { fakeDriver = createEmptyMockDriver(); node = new ZWaveNode(2, fakeDriver as any); fakeDriver.controller.nodes.set(node.id, node); await fakeDriver.configManager.loadDeviceClasses(); }, // Loading configuration may take a while on CI 30000, ); afterAll(() => { node.destroy(); }); // We might need to persist the node state between stages, so // it shouldn't be created for each test describe(`queryProtocolInfo()`, () => { let expected: GetNodeProtocolInfoResponse; beforeAll(() => { fakeDriver.sendMessage.mockClear(); expected = { isListening: true, isFrequentListening: false, isRouting: true, supportedDataRates: [100000], supportsSecurity: false, protocolVersion: ProtocolVersion["4.5x / 6.0x"], supportsBeaming: false, nodeType: NodeType.Controller, basicDeviceClass: 0x01, genericDeviceClass: 0x03, specificDeviceClass: 0x02, } as unknown as GetNodeProtocolInfoResponse; fakeDriver.sendMessage.mockResolvedValue(expected); }); it("should send a GetNodeProtocolInfoRequest", async () => { await node["queryProtocolInfo"](); expect(fakeDriver.sendMessage).toBeCalled(); const request: GetNodeProtocolInfoRequest = fakeDriver.sendMessage.mock.calls[0][0]; expect(request).toBeInstanceOf(GetNodeProtocolInfoRequest); expect(request.requestedNodeId).toBe(node.id); }); // TODO: Do this with Mock Controller etc. it.skip("should remember all received information", () => { for (const prop of Object.keys( expected, ) as (keyof typeof expected)[]) { expect((node as any)[prop]).toBe(expected[prop]); } }); it("should set the interview stage to ProtocolInfo", () => { expect(node.interviewStage).toBe(InterviewStage.ProtocolInfo); }); it("if the node is a sleeping device, assume that it is asleep", async () => { for (const { isListening, isFrequentListening } of [ // Test 1-3: not sleeping { isListening: true, isFrequentListening: true, }, { isListening: false, isFrequentListening: true, }, { isListening: true, isFrequentListening: false, }, // Test 4: sleeping { isListening: false, isFrequentListening: false, }, ]) { Object.assign(expected, { isListening, isFrequentListening, }); await node["queryProtocolInfo"](); if (node.canSleep) { expect(node.status).toBe(NodeStatus.Asleep); } } }); }); describe(`ping()`, () => { beforeAll(() => fakeDriver.sendMessage.mockImplementation(() => Promise.resolve(), ), ); beforeEach(() => fakeDriver.sendMessage.mockClear()); it(`should not change the current interview stage`, async () => { node.interviewStage = InterviewStage.OverwriteConfig; await node.ping(); expect(node.interviewStage).toBe( InterviewStage.OverwriteConfig, ); }); it("should not send anything if the node is the controller", async () => { // Temporarily make this node the controller node fakeDriver.controller.ownNodeId = node.id; await node.ping(); expect(fakeDriver.sendMessage).not.toBeCalled(); fakeDriver.controller.ownNodeId = 1; }); it("should send a NoOperation CC and wait for the response", async () => { await node.ping(); expect(fakeDriver.sendMessage).toBeCalled(); const request: SendDataRequest = fakeDriver.sendMessage.mock.calls[0][0]; expect(request).toBeInstanceOf(SendDataRequest); expect(request.command).toBeInstanceOf(NoOperationCC); expect(request.getNodeId()).toBe(node.id); }); }); describe(`queryNodeInfo()`, () => { beforeAll(() => fakeDriver.sendMessage.mockImplementation(() => Promise.resolve(), ), ); beforeEach(() => fakeDriver.sendMessage.mockClear()); it(`should set the interview stage to "NodeInfo"`, async () => { await node["queryNodeInfo"](); expect(node.interviewStage).toBe(InterviewStage.NodeInfo); }); it("should not send anything if the node is the controller", async () => { // Temporarily make this node the controller node fakeDriver.controller.ownNodeId = node.id; await node["queryNodeInfo"](); expect(fakeDriver.sendMessage).not.toBeCalled(); fakeDriver.controller.ownNodeId = 1; }); it("should send a RequestNodeInfoRequest with the node's ID", async () => { await node["queryNodeInfo"](); expect(fakeDriver.sendMessage).toBeCalled(); const request: RequestNodeInfoRequest = fakeDriver.sendMessage.mock.calls[0][0]; expect(request).toBeInstanceOf(RequestNodeInfoRequest); expect(request.getNodeId()).toBe(node.id); }); // it.todo("Test the behavior when the request failed"); // // TODO: We need a real payload for this test // it.skip("should update its node information with the received data and mark the node as awake", async () => { // const nodeUpdate: NodeUpdatePayload = { // basic: 0x01, // generic: 0x03, // specific: 0x01, // supportedCCs: [CommandClasses["User Code"]], // controlledCCs: [CommandClasses["Window Covering"]], // nodeId: 2, // }; // const expected = new ApplicationUpdateRequest( // fakeDriver as any, // {} as any, // ); // (expected as any)._updateType = // ApplicationUpdateTypes.NodeInfo_Received; // (expected as any)._nodeInformation = nodeUpdate; // fakeDriver.sendMessage.mockResolvedValue(expected); // await node["queryNodeInfo"](); // for (const cc of nodeUpdate.supportedCCs) { // expect(node.supportsCC(cc)).toBeTrue(); // } // for (const cc of nodeUpdate.controlledCCs) { // expect(node.controlsCC(cc)).toBeTrue(); // } // expect(node.isAwake()).toBeTrue(); // }); }); describe(`interviewCCs()`, () => { beforeAll(() => fakeDriver.sendMessage.mockImplementation(() => Promise.resolve(), ), ); beforeEach(() => { fakeDriver.sendMessage.mockClear(); fakeDriver.networkCache.clear(); }); it.todo("test that the CC interview methods are called"); it("the CC interviews happen in the correct order", () => { require("@zwave-js/cc/index"); expect(getCCConstructor(49)).not.toBeUndefined(); const node = new ZWaveNode(2, fakeDriver as any); const CCs = [ CommandClasses["Z-Wave Plus Info"], CommandClasses["Device Reset Locally"], CommandClasses["Firmware Update Meta Data"], CommandClasses["CRC-16 Encapsulation"], CommandClasses["Multi Channel"], CommandClasses["Multilevel Switch"], CommandClasses.Configuration, CommandClasses["Multilevel Sensor"], CommandClasses.Meter, CommandClasses.Protection, CommandClasses.Association, CommandClasses["Multi Channel Association"], CommandClasses["Association Group Information"], CommandClasses.Notification, CommandClasses["Manufacturer Specific"], CommandClasses.Version, ]; for (const cc of CCs) { node.addCC(cc, { isSupported: true, version: 1 }); } const rootInterviewGraphPart1 = node.buildCCInterviewGraph([ CommandClasses.Security, CommandClasses["Security 2"], CommandClasses["Manufacturer Specific"], CommandClasses.Version, ...applicationCCs, ]); const rootInterviewGraphPart2 = node.buildCCInterviewGraph([ ...nonApplicationCCs, ]); const rootInterviewOrderPart1 = topologicalSort( rootInterviewGraphPart1, ); const rootInterviewOrderPart2 = topologicalSort( rootInterviewGraphPart2, ); expect( rootInterviewOrderPart1.map((cc) => getCCName(cc)), ).toEqual([ "Z-Wave Plus Info", "Device Reset Locally", "Firmware Update Meta Data", "CRC-16 Encapsulation", "Multi Channel", "Association", "Multi Channel Association", "Association Group Information", ]); expect( rootInterviewOrderPart2.map((cc) => getCCName(cc)), ).toEqual([ "Multilevel Switch", "Configuration", "Multilevel Sensor", "Meter", "Protection", "Notification", ]); }); // it("should not send anything if the node is the controller", async () => { // // Temporarily make this node the controller node // fakeDriver.controller.ownNodeId = node.id; // await node["queryNodeInfo"](); // expect(fakeDriver.sendMessage).not.toBeCalled(); // fakeDriver.controller.ownNodeId = 1; // }); // it("should send a RequestNodeInfoRequest with the node's ID", async () => { // await node["queryNodeInfo"](); // expect(fakeDriver.sendMessage).toBeCalled(); // const request: RequestNodeInfoRequest = // fakeDriver.sendMessage.mock.calls[0][0]; // expect(request).toBeInstanceOf(RequestNodeInfoRequest); // expect(request.getNodeId()).toBe(node.id); // }); }); // describe(`queryEndpoints()`, () => { // beforeAll(() => // fakeDriver.sendMessage.mockImplementation(() => // Promise.resolve({ command: {} }), // ), // ); // beforeEach(() => fakeDriver.sendMessage.mockClear()); // afterAll(() => // fakeDriver.sendMessage.mockImplementation(() => // Promise.resolve(), // ), // ); // it(`should set the interview stage to "Endpoints"`, async () => { // await node.queryEndpoints(); // expect(node.interviewStage).toBe(InterviewStage.Endpoints); // }); // it("should not send anything if the node does not support the Multi Channel CC", async () => { // node.addCC(CommandClasses["Multi Channel"], { // isSupported: false, // isControlled: false, // }); // await node.queryEndpoints(); // expect(fakeDriver.sendMessage).not.toBeCalled(); // }); // it("should send a MultiChannelCC.EndPointGet", async () => { // node.addCC(CommandClasses["Multi Channel"], { // isSupported: true, // }); // await node.queryEndpoints(); // expect(fakeDriver.sendMessage).toBeCalled(); // assertCC(fakeDriver.sendMessage.mock.calls[0][0], { // cc: MultiChannelCC, // nodeId: node.id, // ccValues: { // ccCommand: MultiChannelCommand.EndPointGet, // }, // }); // }); // it.todo("Test the behavior when the request failed"); // it.todo("Test the behavior when the request succeeds"); // }); // describe(`queryNeighbors()`, () => { // let expected: GetRoutingInfoResponse; // beforeAll(() => { // fakeDriver.sendMessage.mockClear(); // expected = { // nodeIds: [1, 4, 5], // } as GetRoutingInfoResponse; // fakeDriver.sendMessage.mockResolvedValue(expected); // }); // it("should send a GetRoutingInfoRequest", async () => { // await node["queryNeighbors"](); // expect(fakeDriver.sendMessage).toBeCalled(); // const request: GetRoutingInfoRequest = // fakeDriver.sendMessage.mock.calls[0][0]; // expect(request).toBeInstanceOf(GetRoutingInfoRequest); // expect(request.sourceNodeId).toBe(node.id); // }); // it("should remember the neighbor list", async () => { // await node["queryNeighbors"](); // expect(node.neighbors).toContainAllValues(expected.nodeIds); // }); // it("should set the interview stage to Neighbors", () => { // expect(node.interviewStage).toBe(InterviewStage.Neighbors); // }); // }); describe("interview sequence", () => { let originalMethods: Partial<Record<keyof TestNode, any>>; beforeAll(() => { const interviewStagesAfter: Record<string, InterviewStage> = { queryProtocolInfo: InterviewStage.ProtocolInfo, queryNodeInfo: InterviewStage.NodeInfo, interviewCCs: InterviewStage.CommandClasses, }; const returnValues: Partial<Record<keyof TestNode, any>> = { ping: true, interviewCCs: true, }; originalMethods = { queryProtocolInfo: node["queryProtocolInfo"].bind(node), queryNodeInfo: node["queryNodeInfo"].bind(node), interviewCCs: node["interviewCCs"].bind(node), }; for (const method of Object.keys( originalMethods, ) as (keyof TestNode)[]) { (node as any)[method] = jest .fn() .mockName(`${method} mock`) .mockImplementation(() => { if (method in interviewStagesAfter) node.interviewStage = interviewStagesAfter[method]; return method in returnValues ? Promise.resolve(returnValues[method]) : Promise.resolve(); }); } }); beforeEach(() => { for (const method of Object.keys(originalMethods)) { (node as any)[method].mockClear(); } }); afterAll(() => { for (const method of Object.keys( originalMethods, ) as (keyof TestNode)[]) { (node as any)[method] = originalMethods[method]; } }); it("should execute all the interview methods", async () => { node.interviewStage = InterviewStage.None; await node.interviewInternal(); for (const method of Object.keys(originalMethods)) { expect((node as any)[method]).toBeCalled(); } }); it("should not execute any interview method if the interview is completed", async () => { node.interviewStage = InterviewStage.Complete; await node.interviewInternal(); for (const method of Object.keys(originalMethods)) { expect((node as any)[method]).not.toBeCalled(); } }); it("should skip all methods that belong to an earlier stage", async () => { node.interviewStage = InterviewStage.NodeInfo; await node.interviewInternal(); const expectCalled = [ "interviewCCs", // "queryNodePlusInfo", // "queryManufacturerSpecific", // "queryCCVersions", // "queryEndpoints", // "requestStaticValues", // "configureWakeup", "queryNeighbors", ]; for (const method of Object.keys(originalMethods)) { if (expectCalled.indexOf(method) > -1) { expect((node as any)[method]).toBeCalled(); } else { expect((node as any)[method]).not.toBeCalled(); } } }); it.todo("Test restarting from cache"); }); }); // describe("isAwake() / setAwake()", () => { // const fakeDriver = createEmptyMockDriver(); // function makeNode(supportsWakeUp: boolean = false): ZWaveNode { // const node = new ZWaveNode(2, (fakeDriver as unknown) as Driver); // if (supportsWakeUp) // node.addCC(CommandClasses["Wake Up"], { isSupported: true }); // fakeDriver.controller.nodes.set(node.id, node); // return node; // } // it("newly created nodes should be assumed awake", () => { // const node = makeNode(); // expect(node.isAwake()).toBeTrue(); // node.destroy(); // }); // it("setAwake() should NOT throw if the node does not support Wake Up", () => { // const node = makeNode(); // expect(() => node.markAsAwake()).not.toThrow(); // node.destroy(); // }); // it("isAwake() should return the status set by setAwake()", () => { // const node = makeNode(true); // node.markAsAsleep(); // expect(node.isAwake()).toBeFalse(); // node.markAsAwake(); // expect(node.isAwake()).toBeTrue(); // node.destroy(); // }); // it(`setAwake() should emit the "wake up" event when the node wakes up and "sleep" when it goes to sleep`, () => { // const node = makeNode(true); // const wakeupSpy = jest.fn(); // const sleepSpy = jest.fn(); // node.on("wake up", wakeupSpy).on("sleep", sleepSpy); // for (const { state, expectWakeup, expectSleep } of [ // { state: false, expectSleep: true, expectWakeup: false }, // { state: true, expectSleep: false, expectWakeup: true }, // { state: true, expectSleep: false, expectWakeup: false }, // { state: false, expectSleep: true, expectWakeup: false }, // ]) { // wakeupSpy.mockClear(); // sleepSpy.mockClear(); // state ? node.markAsAwake() : node.markAsAsleep(); // expect(wakeupSpy).toBeCalledTimes(expectWakeup ? 1 : 0); // expect(sleepSpy).toBeCalledTimes(expectSleep ? 1 : 0); // } // node.destroy(); // }); // }); describe("updateNodeInfo()", () => { let driver: Driver; // let node2: ZWaveNode; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); function makeNode(canSleep: boolean = false): ZWaveNode { const node = new ZWaveNode(2, driver); node["isListening"] = !canSleep; node["isFrequentListening"] = false; // If the node doesn't support Z-Wave+ Info CC, the node instance // will try to poll the device for changes. We don't want this to happen in tests. node.addCC(CommandClasses["Z-Wave Plus Info"], { isSupported: true, }); // node.addCC(CommandClasses["Wake Up"], { isSupported: true }); (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).set( node.id, node, ); return node; } const emptyNodeInfo = { supportedCCs: [], controlledCCs: [], }; beforeEach(() => { driver.networkCache.clear(); }); it("marks a sleeping node as awake", () => { const node = makeNode(true); node.markAsAsleep(); node.updateNodeInfo(emptyNodeInfo as any); expect(node.status).toBe(NodeStatus.Awake); node.destroy(); }); it("does not throw when called on a non-sleeping node", () => { const node = makeNode(false); node.updateNodeInfo(emptyNodeInfo as any); node.destroy(); }); it("remembers all received CCs", () => { const node = makeNode(); node.addCC(CommandClasses.Battery, { isControlled: true, }); node.addCC(CommandClasses.Configuration, { isControlled: true, }); node.updateNodeInfo({ supportedCCs: [ CommandClasses.Battery, CommandClasses.Configuration, ], } as any); expect(node.supportsCC(CommandClasses.Battery)).toBeTrue(); expect(node.supportsCC(CommandClasses.Configuration)).toBeTrue(); node.destroy(); }); it("ignores the data in an NIF if it was received already", () => { const node = makeNode(); node.interviewStage = InterviewStage.Complete; node.updateNodeInfo({ controlledCCs: [CommandClasses.Configuration], supportedCCs: [CommandClasses.Battery], } as any); expect(node.supportsCC(CommandClasses.Battery)).toBeFalse(); expect(node.controlsCC(CommandClasses.Configuration)).toBeFalse(); node.destroy(); }); }); describe(`sendNoMoreInformation()`, () => { const fakeDriver = createEmptyMockDriver(); function makeNode(): ZWaveNode { const node = new ZWaveNode(2, fakeDriver as unknown as Driver); node["isListening"] = false; node["isFrequentListening"] = false; node.addCC(CommandClasses["Wake Up"], { isSupported: true }); fakeDriver.controller.nodes.set(node.id, node); return node; } beforeEach(() => fakeDriver.sendMessage.mockClear()); it("should not do anything and return false if the node is asleep", async () => { const node = makeNode(); node.markAsAsleep(); expect(await node.sendNoMoreInformation()).toBeFalse(); expect(fakeDriver.sendMessage).not.toBeCalled(); node.destroy(); }); it("should not do anything and return false if the node interview is not complete", async () => { const node = makeNode(); node.interviewStage = InterviewStage.CommandClasses; expect(await node.sendNoMoreInformation()).toBeFalse(); expect(fakeDriver.sendMessage).not.toBeCalled(); node.destroy(); }); it("should not send anything if the node should be kept awake", async () => { const node = makeNode(); node.markAsAwake(); node.keepAwake = true; expect(await node.sendNoMoreInformation()).toBeFalse(); expect(fakeDriver.sendMessage).not.toBeCalled(); node.destroy(); }); it("should send a WakeupCC.NoMoreInformation otherwise", async () => { const node = makeNode(); node.interviewStage = InterviewStage.Complete; node.markAsAwake(); expect(await node.sendNoMoreInformation()).toBeTrue(); expect(fakeDriver.sendMessage).toBeCalled(); assertCC(fakeDriver.sendMessage.mock.calls[0][0], { cc: WakeUpCC, nodeId: node.id, ccValues: { ccCommand: WakeUpCommand.NoMoreInformation, }, }); node.destroy(); }); it.todo("Test send failures"); }); describe("getCCVersion()", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); it("should return 0 if a command class is not supported", () => { const node = new ZWaveNode(2, driver); expect(node.getCCVersion(CommandClasses["Anti-Theft"])).toBe(0); node.destroy(); }); it("should return the supported version otherwise", () => { const node = new ZWaveNode(2, driver); node.addCC(CommandClasses["Anti-Theft"], { isSupported: true, version: 5, }); expect(node.getCCVersion(CommandClasses["Anti-Theft"])).toBe(5); node.destroy(); }); }); describe("removeCC()", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); it("should mark a CC as not supported", () => { const node = new ZWaveNode(2, driver); node.addCC(CommandClasses["Anti-Theft"], { isSupported: true, version: 7, }); expect(node.getCCVersion(CommandClasses["Anti-Theft"])).toBe(7); node.removeCC(CommandClasses["Anti-Theft"]); expect(node.getCCVersion(CommandClasses["Anti-Theft"])).toBe(0); node.destroy(); }); }); describe("createCCInstance()", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); it("should throw if the CC is not supported", () => { const node = new ZWaveNode(2, driver); assertZWaveError( () => node.createCCInstance(CommandClasses.Basic), { errorCode: ZWaveErrorCodes.CC_NotSupported, messageMatches: "unsupported", }, ); node.destroy(); }); it("should return a linked instance of the correct CC", () => { const node = new ZWaveNode(2, driver); (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).set( node.id, node, ); node.addCC(CommandClasses.Basic, { isSupported: true }); const cc = node.createCCInstance(BasicCC)!; expect(cc).toBeInstanceOf(BasicCC); expect(cc.getNode(driver)).toBe(node); node.destroy(); }); }); describe("getEndpoint()", () => { let driver: Driver; let node: ZWaveNode; let controller: MockController; beforeAll( async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); await driver.configManager.loadDeviceClasses(); }, // Loading configuration may take a while on CI 30000, ); afterAll(async () => { await driver.destroy(); }); beforeEach(async () => { node = new ZWaveNode( 2, driver, new DeviceClass(driver.configManager, 0x04, 0x01, 0x01), // Portable Remote Controller ); (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).set( node.id, node, ); }); afterEach(() => { (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).delete( node.id, ); node.valueDB.clear(); node.destroy(); driver.networkCache.clear(); }); it("throws when a negative endpoint index is requested", () => { assertZWaveError(() => node.getEndpoint(-1), { errorCode: ZWaveErrorCodes.Argument_Invalid, messageMatches: "must be positive", }); }); it("returns the node itself when endpoint 0 is requested", () => { expect(node.getEndpoint(0)).toBe(node); }); it("returns a new endpoint with the correct endpoint index otherwise", () => { // interviewComplete needs to be true for getEndpoint to work node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], property: "interviewComplete", }, true, ); node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], property: "individualCount", }, 5, ); const actual = node.getEndpoint(5)!; expect(actual.index).toBe(5); expect(actual.nodeId).toBe(2); }); it("caches the created endpoint instances", () => { // interviewComplete needs to be true for getEndpoint to work node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], property: "interviewComplete", }, true, ); node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], property: "individualCount", }, 5, ); const first = node.getEndpoint(5); const second = node.getEndpoint(5); expect(first).not.toBeUndefined(); expect(first).toBe(second); }); it("returns undefined if a non-existent endpoint is requested", () => { const actual = node.getEndpoint(5); expect(actual).toBeUndefined(); }); it("sets the correct device class for the endpoint", async () => { // interviewComplete needs to be true for getEndpoint to work node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], property: "interviewComplete", }, true, ); node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], property: "individualCount", }, 5, ); node.valueDB.setValue( { commandClass: CommandClasses["Multi Channel"], endpoint: 5, property: "deviceClass", }, { generic: 0x03, specific: 0x12, // Doorbell }, ); const actual = node.getEndpoint(5); expect(actual?.deviceClass?.specific.label).toBe("Doorbell"); }); }); it.todo("serialize() / deserialize()"); // describe("serialize() / deserialize()", () => { // const fakeDriver = createEmptyMockDriver() as unknown as Driver; // beforeAll(async () => { // // Loading configuration may take a while on CI // if (process.env.CI) jest.setTimeout(30000); // await fakeDriver.configManager.loadDeviceClasses(); // }); // const serializedTestNode = { // id: 1, // interviewStage: "NodeInfo", // deviceClass: { // basic: 2, // generic: 2, // specific: 1, // }, // isListening: true, // isFrequentListening: false, // isRouting: false, // supportedDataRates: [40000], // supportsSecurity: false, // supportsBeaming: true, // protocolVersion: 3, // nodeType: "Controller", // securityClasses: { // S2_AccessControl: false, // S2_Authenticated: true, // S2_Unauthenticated: true, // S0_Legacy: false, // }, // dsk: "00000-00001-00002-00003-00004-00005-00006-00007", // commandClasses: { // "0x25": { // name: "Binary Switch", // endpoints: { // 0: { // isSupported: false, // isControlled: true, // secure: false, // version: 3, // }, // }, // }, // "0x26": { // name: "Multilevel Switch", // endpoints: { // 0: { // isSupported: false, // isControlled: true, // secure: false, // version: 4, // }, // }, // }, // }, // }; // it("serializing a deserialized node should result in the original object", () => { // const node = new ZWaveNode(1, fakeDriver); // // @ts-ignore We need write access to the map // fakeDriver.controller.nodes.set(1, node); // node.deserialize(serializedTestNode); // expect(node.serialize()).toEqual(serializedTestNode); // node.destroy(); // }); // it("deserializing a legacy node object should have the correct properties", () => { // const node = new ZWaveNode(1, fakeDriver); // // @ts-ignore We need write access to the map // fakeDriver.controller.nodes.set(1, node); // const legacy = { // ...serializedTestNode, // version: 4, // version 4 -> protocolVersion 3 // isFrequentListening: true, // --> 1000ms // isBeaming: true, // maxBaudRate: 40000, // isSecure: true, // --> securityClasses.S0_Legacy: true // }; // // @ts-expect-error We want to test this! // delete legacy.protocolVersion; // // @ts-expect-error We want to test this! // delete legacy.securityClasses; // node.deserialize(legacy); // const expected = { // ...serializedTestNode, // isFrequentListening: "1000ms", // securityClasses: { // S0_Legacy: true, // // S2 classes are not granted when deserializing legacy caches // S2_AccessControl: false, // S2_Authenticated: false, // S2_Unauthenticated: false, // }, // }; // expect(node.serialize()).toEqual(expected); // node.destroy(); // }); // it("a changed interview stage is reflected in the cache", () => { // const node = new ZWaveNode(1, fakeDriver); // // @ts-ignore We need write access to the map // fakeDriver.controller.nodes.set(1, node); // node.deserialize(serializedTestNode); // node.interviewStage = InterviewStage.Complete; // expect(node.serialize().interviewStage).toEqual( // InterviewStage[InterviewStage.Complete], // ); // node.destroy(); // }); // it("deserialize() should correctly read values and metadata", () => { // const input = { ...serializedTestNode }; // const valueId1 = { // endpoint: 1, // property: "targetValue", // }; // const valueId2 = { // endpoint: 2, // property: "targetValue", // }; // (input.commandClasses as any)["0x20"] = { // name: "Basic", // isSupported: false, // isControlled: true, // version: 1, // values: [{ ...valueId1, value: 12 }], // metadata: [ // { // ...valueId2, // metadata: ValueMetadata.ReadOnlyInt32, // }, // ], // }; // const node = new ZWaveNode(1, fakeDriver); // // @ts-ignore We need write access to the map // fakeDriver.controller.nodes.set(1, node); // node.deserialize(input); // expect( // node.valueDB.getValue({ // ...valueId1, // commandClass: CommandClasses.Basic, // }), // ).toBe(12); // expect( // node.valueDB.getMetadata({ // ...valueId2, // commandClass: CommandClasses.Basic, // }), // ).toBe(ValueMetadata.ReadOnlyInt32); // node.destroy(); // }); // it("deserialize() should also accept numbers for the interview stage", () => { // const input = { // ...serializedTestNode, // interviewStage: InterviewStage.Complete, // }; // const node = new ZWaveNode(1, fakeDriver); // node.deserialize(input); // expect(node.interviewStage).toBe(InterviewStage.Complete); // node.destroy(); // }); // it("deserialize() should skip the deviceClass if it is malformed", () => { // const node = new ZWaveNode(1, fakeDriver); // const brokenDeviceClasses = [ // // not an object // undefined, // 1, // "foo", // // incomplete // {}, // { basic: 1 }, // { generic: 2 }, // { specific: 3 }, // { basic: 1, generic: 2 }, // { basic: 1, specific: 3 }, // { generic: 2, specific: 3 }, // // wrong type // { basic: "1", generic: 2, specific: 3 }, // { basic: 1, generic: true, specific: 3 }, // { basic: 1, generic: 2, specific: {} }, // ]; // for (const dc of brokenDeviceClasses) { // const input = { // ...serializedTestNode, // deviceClass: dc, // }; // (node as any)._deviceClass = undefined; // node.deserialize(input); // expect(node.deviceClass).toBeUndefined(); // } // node.destroy(); // }); // it("deserialize() should skip any primitive properties that have the wrong type or format", () => { // const node = new ZWaveNode(1, fakeDriver); // const wrongInputs: [string, any][] = [ // ["isListening", 1], // ["isFrequentListening", 2], // ["isRouting", {}], // ["supportedDataRates", true], // ["supportsSecurity", 3], // ["supportsSecurity", "3"], // ["protocolVersion", false], // ["dsk", "foo"], // ]; // for (const [prop, val] of wrongInputs) { // const input = { // ...serializedTestNode, // [prop]: val, // }; // (node as any)["_" + prop] = undefined; // node.deserialize(input); // expect((node as any)[prop]).toBeUndefined(); // } // node.destroy(); // }); // it("deserialize() should skip command classes that don't have a HEX key", () => { // const node = new ZWaveNode(1, fakeDriver); // const input = { // ...serializedTestNode, // commandClasses: { // "Binary Switch": { // name: "Binary Switch", // isSupported: false, // isControlled: true, // version: 3, // }, // }, // }; // node.deserialize(input); // expect(node.implementedCommandClasses.size).toBe(0); // node.destroy(); // }); // it("deserialize() should skip command classes that are not known to this library", () => { // const node = new ZWaveNode(1, fakeDriver); // const input = { // ...serializedTestNode, // commandClasses: { // "0x001122ff": { // name: "Binary Switch", // isSupported: false, // isControlled: true, // version: 3, // }, // }, // }; // node.deserialize(input); // expect(node.implementedCommandClasses.size).toBe(0); // node.destroy(); // }); // it("deserialize() should not parse any malformed CC properties", () => { // const node = new ZWaveNode(1, fakeDriver); // const input = { // ...serializedTestNode, // commandClasses: { // "0x25": { // isSupported: 1, // }, // "0x26": { // isControlled: "", // }, // "0x27": { // isSupported: true, // version: "5", // }, // }, // }; // node.deserialize(input); // expect(node.supportsCC(0x25)).toBeFalse(); // expect(node.controlsCC(0x26)).toBeFalse(); // expect(node.getCCVersion(0x27)).toBe(0); // node.destroy(); // }); // it("deserialize() should set the node status to Unknown if the node can sleep", () => { // const input = { // ...serializedTestNode, // isListening: false, // isFrequentListening: false, // }; // const node = new ZWaveNode(1, fakeDriver); // node.deserialize(input); // expect(node.status).toBe(NodeStatus.Unknown); // node.destroy(); // }); // it("deserialize() should set the node status to Unknown if the node is a listening node", () => { // const input = { // ...serializedTestNode, // isListening: true, // isFrequentListening: false, // }; // const node = new ZWaveNode(1, fakeDriver); // node.deserialize(input); // expect(node.status).toBe(NodeStatus.Unknown); // node.destroy(); // }); // it("deserialize() should set the node status to Unknown if the node is a frequent listening node", () => { // const input = { // ...serializedTestNode, // isListening: false, // isFrequentListening: true, // }; // const node = new ZWaveNode(1, fakeDriver); // node.deserialize(input); // expect(node.status).toBe(NodeStatus.Unknown); // node.destroy(); // }); // }); it.todo( "deserialize() should mark a sleeping node as ready if it was interviewed completely", ); describe("the emitted events", () => { let node: ZWaveNode; let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); const onValueAdded = jest.fn(); const onValueUpdated = jest.fn(); const onValueRemoved = jest.fn(); function createNode(): void { node = new ZWaveNode(1, driver) .on("value added", onValueAdded) .on("value updated", onValueUpdated) .on("value removed", onValueRemoved); (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).set( node.id, node, ); } beforeEach(() => { createNode(); onValueAdded.mockClear(); onValueUpdated.mockClear(); onValueRemoved.mockClear(); }); afterEach(() => { node.destroy(); (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).delete( node.id, ); }); it("should contain a speaking name for the CC", () => { const cc = CommandClasses["Wake Up"]; const ccName = CommandClasses[cc]; const valueId: ValueID = { commandClass: cc, property: "fooProp", }; node.valueDB.setValue(valueId, 1); expect(onValueAdded).toBeCalled(); node.valueDB.setValue(valueId, 3); expect(onValueUpdated).toBeCalled(); node.valueDB.removeValue(valueId); expect(onValueRemoved).toBeCalled(); for (const method of [ onValueAdded, onValueUpdated, onValueRemoved, ]) { const cbArg = method.mock.calls[0][1]; expect(cbArg.commandClassName).toBe(ccName); } }); it("should contain a speaking name for the propertyKey", () => { node.valueDB.setValue( { commandClass: CommandClasses["Thermostat Setpoint"], property: "setpoint", propertyKey: 1 /* Heating */, }, 5, ); expect(onValueAdded).toBeCalled(); const cbArg = onValueAdded.mock.calls[0][1]; expect(cbArg.propertyKeyName).toBe("Heating"); }); it("should not be emitted for internal values", () => { node.valueDB.setValue( { commandClass: CommandClasses.Battery, property: "interviewComplete", // interviewCompleted is an internal value }, true, ); expect(onValueAdded).not.toBeCalled(); }); }); describe("changing the node status", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); interface TestOptions { targetStatus: NodeStatus; expectedEvent: ZWaveNodeEvents; expectCall?: boolean; // default true } function performTest(options: TestOptions): void { const node = new ZWaveNode(1, driver); node["_status"] = undefined as any; const spy = jest.fn(); node.on(options.expectedEvent, spy); node["onStatusChange"](options.targetStatus); node.destroy(); if (options.expectCall !== false) { expect(spy).toBeCalled(); } else { expect(spy).not.toBeCalled(); } node.destroy(); } it("Changing the status to awake should raise the wake up event", () => { performTest({ targetStatus: NodeStatus.Awake, expectedEvent: "wake up", }); }); it("Changing the status to asleep should raise the sleep event", () => { performTest({ targetStatus: NodeStatus.Asleep, expectedEvent: "sleep", }); }); it("Changing the status to dead should raise the dead event", () => { performTest({ targetStatus: NodeStatus.Dead, expectedEvent: "dead", }); }); it("Changing the status to alive should raise the alive event", () => { performTest({ targetStatus: NodeStatus.Alive, expectedEvent: "alive", }); }); }); describe("getValue()", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); it("returns the values stored in the value DB", () => { const node = new ZWaveNode(1, driver); const valueId: ValueID = { commandClass: CommandClasses.Version, endpoint: 2, property: "3", }; node.valueDB.setValue(valueId, 4); expect(node.getValue(valueId)).toBe(4); node.destroy(); }); }); describe("setValue()", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); it("issues the correct xyzCCSet command", async () => { // We test with a BasicCC const node = new ZWaveNode(1, driver); node.addCC(CommandClasses.Basic, { isSupported: true }); // Since setValue also issues a get, we need to mock a response driver.sendMessage = jest .fn() .mockResolvedValueOnce(undefined) // For some reason this is called twice?! .mockResolvedValue({ command: {} }); const result = await node.setValue( { commandClass: CommandClasses.Basic, property: "targetValue", }, 5, ); expect(result).toBeTrue(); expect(driver.sendMessage).toBeCalled(); assertCC((driver.sendMessage as jest.Mock).mock.calls[0][0], { cc: BasicCC, nodeId: node.id, ccValues: { ccCommand: BasicCommand.Set, }, }); node.destroy(); }); it("returns false if the CC is not implemented", async () => { const node = new ZWaveNode(1, driver); const result = await node.setValue( { commandClass: 0xbada55, // this is guaranteed to not be implemented property: "test", }, 1, ); expect(result).toBeFalse(); node.destroy(); }); }); describe("getValueMetadata()", () => { let driver: Driver; let controller: MockController; beforeAll(async () => { ({ driver } = await createAndStartTestingDriver({ skipNodeInterview: true, loadConfiguration: false, beforeStartup(mockPort) { controller = new MockController({ serial: mockPort }); controller.defineBehavior( ...createDefaultMockControllerBehaviors(), ); }, })); }); afterAll(async () => { await driver.destroy(); }); let node: ZWaveNode; const valueId = BasicCCValues.currentValue.id; beforeEach(() => { node = new ZWaveNode(1, driver); (driver.controller.nodes as ThrowingMap<number, ZWaveNode>).set( node.id, node, ); }); afterEach(() => { node.destroy(); }); it("retur