inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
508 lines (470 loc) • 13.7 kB
text/typescript
import { AssociationCommand } from "@zwave-js/cc";
import { AssociationCCReport } from "@zwave-js/cc/AssociationCC";
import { BasicCCSet } from "@zwave-js/cc/BasicCC";
import { FirmwareUpdateMetaDataCC } from "@zwave-js/cc/FirmwareUpdateMetaDataCC";
import { MultiChannelCCCommandEncapsulation } from "@zwave-js/cc/MultiChannelCC";
import { MultiCommandCCCommandEncapsulation } from "@zwave-js/cc/MultiCommandCC";
import { SecurityCCCommandEncapsulation } from "@zwave-js/cc/SecurityCC";
import { WakeUpCCIntervalSet } from "@zwave-js/cc/WakeUpCC";
import {
CommandClasses,
EncapsulationFlags,
TransmitOptions,
ValueID,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import { MockController, MockNode } from "@zwave-js/testing";
import { createDefaultMockControllerBehaviors } from "../../Utils";
import { ZWaveNode } from "../node/Node";
import { ApplicationCommandRequest } from "../serialapi/application/ApplicationCommandRequest";
import { SendDataRequest } from "../serialapi/transport/SendDataMessages";
import type { Driver } from "./Driver";
import { createAndStartTestingDriver } from "./DriverMock";
describe("lib/driver/Driver", () => {
describe("receiving messages", () => {
let driver: Driver;
let controller: MockController;
beforeEach(async () => {
({ driver } = await createAndStartTestingDriver({
loadConfiguration: false,
// We don't need a real interview for this
skipControllerIdentification: true,
skipNodeInterview: true,
beforeStartup(mockPort) {
controller = new MockController({ serial: mockPort });
},
}));
}, 30000);
afterEach(async () => {
await driver.destroy();
driver.removeAllListeners();
});
it("should not crash if a message is received that cannot be deserialized", async () => {
const req = new ApplicationCommandRequest(driver, {
command: new WakeUpCCIntervalSet(driver, {
nodeId: 1,
controllerNodeId: 2,
wakeUpInterval: 5,
}),
});
controller.serial.emitData(req.serialize());
await controller.expectHostACK(1000);
});
it("should correctly handle multiple messages in the receive buffer", async () => {
// This buffer contains a SendData transmit report and a ManufacturerSpecific report
const data = Buffer.from(
"010700130f000002e6010e000400020872050086000200828e",
"hex",
);
controller.serial.emitData(data);
// Ensure the driver ACKed two messages
await controller.expectHostACK(1000);
await controller.expectHostACK(1000);
});
});
describe("getNextCallbackId()", () => {
let driver: Driver;
beforeEach(async () => {
({ driver } = await createAndStartTestingDriver({
loadConfiguration: false,
skipControllerIdentification: true,
skipNodeInterview: true,
}));
}, 30000);
afterEach(async () => {
await driver.destroy();
driver.removeAllListeners();
});
it("the automatically created callback ID should be incremented and wrap from 0xff back to 1", () => {
let lastCallbackId: number | undefined;
for (let i = 0; i <= 300; i++) {
if (i === 300) {
throw new Error(
"incrementing the callback ID does not work somehow",
);
}
const nextCallbackId = driver.getNextCallbackId();
if (lastCallbackId === 0xff) {
expect(nextCallbackId).toBe(1);
break;
} else if (lastCallbackId != null) {
expect(nextCallbackId).toBe(lastCallbackId + 1);
}
lastCallbackId = nextCallbackId;
}
});
});
describe("computeNetCCPayloadSize()", () => {
let driver: Driver;
let controller: MockController;
beforeEach(async () => {
({ driver } = await createAndStartTestingDriver({
loadConfiguration: false,
skipNodeInterview: true,
securityKeys: {
S0_Legacy: Buffer.alloc(16, 0xff),
},
beforeStartup(mockPort) {
controller = new MockController({ serial: mockPort });
controller.defineBehavior(
...createDefaultMockControllerBehaviors(),
);
},
}));
}, 30000);
afterEach(async () => {
await driver.destroy();
driver.removeAllListeners();
});
it("should compute the correct net payload sizes", () => {
const testMsg1 = new SendDataRequest(driver, {
command: new SecurityCCCommandEncapsulation(driver, {
nodeId: 2,
encapsulated: {} as any,
}),
transmitOptions: TransmitOptions.DEFAULT,
});
testMsg1.command.encapsulated = undefined as any;
expect(driver.computeNetCCPayloadSize(testMsg1)).toBe(26);
const multiChannelCC = new MultiChannelCCCommandEncapsulation(
driver,
{
nodeId: 2,
destination: 1,
encapsulated: {} as any,
},
);
const testMsg2 = new SendDataRequest(driver, {
command: new SecurityCCCommandEncapsulation(driver, {
nodeId: 2,
encapsulated: multiChannelCC,
}),
transmitOptions: TransmitOptions.NoRoute,
});
multiChannelCC.encapsulated = undefined as any;
expect(driver.computeNetCCPayloadSize(testMsg2)).toBe(54 - 20 - 4);
const testMsg3 = new FirmwareUpdateMetaDataCC(driver, {
nodeId: 2,
});
testMsg3.setEncapsulationFlag(EncapsulationFlags.Security, true);
expect(driver.computeNetCCPayloadSize(testMsg3)).toBe(46 - 20 - 2);
});
});
describe("assemblePartialCCs()", () => {
let driver: Driver;
let controller: MockController;
beforeEach(async () => {
({ driver } = await createAndStartTestingDriver({
loadConfiguration: false,
skipNodeInterview: true,
securityKeys: {
S0_Legacy: Buffer.alloc(16, 0xff),
},
beforeStartup(mockPort) {
controller = new MockController({ serial: mockPort });
controller.defineBehavior(
...createDefaultMockControllerBehaviors(),
);
const node2 = new MockNode({
id: 2,
controller,
});
controller.nodes.set(node2.id, node2);
},
}));
}, 30000);
afterEach(async () => {
await driver.destroy();
driver.removeAllListeners();
});
it("returns true when a non-partial CC is received", async () => {
const cc = new BasicCCSet(driver, { nodeId: 2, targetValue: 50 });
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeTrue();
});
it("returns true when a partial CC is received that expects no more reports", async () => {
const cc = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
1,
2,
3,
]),
});
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeTrue();
});
it("returns false when a partial CC is received that expects more reports", async () => {
const cc = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
1, // reportsFollow
1,
2,
3,
]),
});
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeFalse();
});
it("returns true when the final partial CC is received and merges its data", async () => {
const cc1 = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
1, // reportsFollow
1,
2,
3,
]),
});
const cc2 = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
4,
5,
6,
]),
});
const msg1 = new ApplicationCommandRequest(driver, {
command: cc1,
});
expect(driver["assemblePartialCCs"](msg1)).toBeFalse();
const msg2 = new ApplicationCommandRequest(driver, {
command: cc2,
});
expect(driver["assemblePartialCCs"](msg2)).toBeTrue();
expect((msg2.command as AssociationCCReport).nodeIds).toEqual([
1, 2, 3, 4, 5, 6,
]);
});
it("does not crash when receiving a Multi Command CC", async () => {
const cc1 = new BasicCCSet(driver, { nodeId: 2, targetValue: 25 });
const cc2 = new BasicCCSet(driver, { nodeId: 2, targetValue: 50 });
const cc = new MultiCommandCCCommandEncapsulation(driver, {
nodeId: 2,
encapsulated: [cc1, cc2],
});
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeTrue();
});
it("supports nested partial/non-partial CCs", async () => {
const cc1 = new BasicCCSet(driver, { nodeId: 2, targetValue: 25 });
const cc = new SecurityCCCommandEncapsulation(driver, {
nodeId: 2,
encapsulated: {} as any,
});
cc.encapsulated = undefined as any;
cc["decryptedCCBytes"] = cc1.serialize();
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeTrue();
});
it("supports nested partial/partial CCs (part 1)", async () => {
const cc = new SecurityCCCommandEncapsulation(driver, {
nodeId: 2,
encapsulated: {} as any,
});
cc.encapsulated = undefined as any;
cc["decryptedCCBytes"] = Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
1, // reportsFollow
1,
2,
3,
]);
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeFalse();
});
it("supports nested partial/partial CCs (part 2)", async () => {
const cc = new SecurityCCCommandEncapsulation(driver, {
nodeId: 2,
encapsulated: {} as any,
});
cc.encapsulated = undefined as any;
cc["decryptedCCBytes"] = Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
1,
2,
3,
]);
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeTrue();
});
it("returns false when a partial CC throws Deserialization_NotImplemented during merging", async () => {
const cc = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
1,
2,
3,
]),
});
cc.mergePartialCCs = () => {
throw new ZWaveError(
"not implemented",
ZWaveErrorCodes.Deserialization_NotImplemented,
);
};
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeFalse();
});
it("returns false when a partial CC throws CC_NotImplemented during merging", async () => {
const cc = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
1,
2,
3,
]),
});
cc.mergePartialCCs = () => {
throw new ZWaveError(
"not implemented",
ZWaveErrorCodes.CC_NotImplemented,
);
};
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeFalse();
});
it("returns false when a partial CC throws PacketFormat_InvalidPayload during merging", async () => {
const cc = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
1,
2,
3,
]),
});
cc.mergePartialCCs = () => {
throw new ZWaveError(
"not implemented",
ZWaveErrorCodes.PacketFormat_InvalidPayload,
);
};
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(driver["assemblePartialCCs"](msg)).toBeFalse();
});
it("passes other errors during merging through", async () => {
const cc = new AssociationCCReport(driver, {
nodeId: 2,
data: Buffer.from([
CommandClasses.Association,
AssociationCommand.Report,
1,
2,
0, // reportsFollow
1,
2,
3,
]),
});
cc.mergePartialCCs = () => {
throw new ZWaveError(
"invalid",
ZWaveErrorCodes.Argument_Invalid,
);
};
const msg = new ApplicationCommandRequest(driver, {
command: cc,
});
expect(() => driver["assemblePartialCCs"](msg)).toThrow("invalid");
});
});
describe("hasPendingMessages()", () => {
let driver: Driver;
let node2: ZWaveNode;
let controller: MockController;
beforeEach(async () => {
({ driver } = await createAndStartTestingDriver({
loadConfiguration: false,
skipNodeInterview: true,
beforeStartup(mockPort) {
controller = new MockController({ serial: mockPort });
controller.defineBehavior(
...createDefaultMockControllerBehaviors(),
);
},
}));
node2 = new ZWaveNode(2, driver);
(driver.controller.nodes as any as Map<any, any>).set(
node2.id,
node2,
);
}, 30000);
afterEach(async () => {
await driver.destroy();
driver.removeAllListeners();
});
it("should return true when there is a poll scheduled for a node", () => {
expect(driver["hasPendingMessages"](node2)).toBeFalse();
const valueId: ValueID = {
commandClass: CommandClasses.Basic,
property: "currentValue",
};
node2.schedulePoll(valueId, { timeoutMs: 1000 });
expect(driver["hasPendingMessages"](node2)).toBeTrue();
node2.cancelScheduledPoll(valueId);
expect(driver["hasPendingMessages"](node2)).toBeFalse();
});
});
});