inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
1,028 lines (926 loc) • 24.6 kB
text/typescript
import { pick } from "@zwave-js/shared";
import { CommandClasses } from "../capabilities/CommandClasses";
import { ZWaveErrorCodes } from "../error/ZWaveError";
import { assertZWaveError } from "../test/assertZWaveError";
import { ValueMetadata } from "../values/Metadata";
import { dbKeyToValueIdFast, ValueDB } from "./ValueDB";
import type { ValueID } from "./_Types";
describe("lib/node/ValueDB => ", () => {
let valueDB: ValueDB;
const onValueAdded = jest.fn();
const onValueUpdated = jest.fn();
const onValueRemoved = jest.fn();
const onMetadataUpdated = jest.fn();
function createValueDB(): void {
valueDB = new ValueDB(2, new Map() as any, new Map() as any)
.on("value added", onValueAdded)
.on("value updated", onValueUpdated)
.on("value removed", onValueRemoved)
.on("metadata updated", onMetadataUpdated);
}
beforeAll(() => createValueDB());
describe("setValue() (first add)", () => {
let cbArg: any;
beforeAll(() => {
valueDB.setValue(
{
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 4,
property: "foo",
},
"bar",
);
});
afterAll(() => {
onValueAdded.mockClear();
onValueUpdated.mockClear();
onValueRemoved.mockClear();
});
it("should emit the value added event", () => {
expect(onValueAdded).toBeCalled();
cbArg = onValueAdded.mock.calls[0][0];
});
it("The callback arg should contain the CC", () => {
expect(cbArg).toBeObject();
expect(cbArg.commandClass).toBe(CommandClasses["Alarm Sensor"]);
});
it("The callback arg should contain the endpoint", () => {
expect(cbArg.endpoint).toBe(4);
});
it("The callback arg should contain the property name", () => {
expect(cbArg.property).toBe("foo");
});
it("The callback arg should contain the new value", () => {
expect(cbArg.newValue).toBe("bar");
});
});
describe("setValue() (consecutive adds)", () => {
let cbArg: any;
beforeAll(() => {
const valueId = {
commandClass: CommandClasses["Wake Up"],
endpoint: 0,
property: "prop",
};
valueDB.setValue(valueId, "foo");
valueDB.setValue(valueId, "bar");
});
afterAll(() => {
onValueAdded.mockClear();
onValueUpdated.mockClear();
onValueRemoved.mockClear();
});
it("should emit the value updated event", () => {
expect(onValueUpdated).toBeCalled();
cbArg = onValueUpdated.mock.calls[0][0];
});
it("The callback arg should contain the CC", () => {
expect(cbArg).toBeObject();
expect(cbArg.commandClass).toBe(CommandClasses["Wake Up"]);
});
it("The callback arg should contain the endpoint", () => {
expect(cbArg.endpoint).toBe(0);
});
it("The callback arg should contain the property name", () => {
expect(cbArg.property).toBe("prop");
});
it("The callback arg should contain the previous value", () => {
expect(cbArg.prevValue).toBe("foo");
});
it("The callback arg should contain the new value", () => {
expect(cbArg.newValue).toBe("bar");
});
});
describe("setValue()/removeValue() (with the noEvent parameter set to true)", () => {
beforeAll(() => {
valueDB.setValue(
{
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 4,
property: "foo",
},
"bar",
{ noEvent: true },
);
valueDB.setValue(
{
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 4,
property: "foo",
},
"baz",
{ noEvent: true },
);
valueDB.removeValue(
{
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 4,
property: "foo",
},
{ noEvent: true },
);
});
afterAll(() => {
onValueAdded.mockClear();
onValueUpdated.mockClear();
onValueRemoved.mockClear();
});
it("should not emit any events", () => {
expect(onValueAdded).not.toBeCalled();
expect(onValueUpdated).not.toBeCalled();
expect(onValueRemoved).not.toBeCalled();
});
});
describe("values with a property key", () => {
const cc = CommandClasses["Wake Up"];
beforeAll(() => {
valueDB.setValue(
{
commandClass: cc,
endpoint: 0,
property: "prop",
propertyKey: "foo",
},
1,
);
valueDB.setValue(
{
commandClass: cc,
endpoint: 0,
property: "prop",
propertyKey: "bar",
},
2,
);
});
afterAll(() => {
onValueAdded.mockClear();
onValueUpdated.mockClear();
onValueRemoved.mockClear();
});
it("getValue()/setValue() should treat different property keys as distinct values", () => {
expect(
valueDB.getValue({
commandClass: cc,
endpoint: 0,
property: "prop",
propertyKey: "foo",
}),
).toBe(1);
expect(
valueDB.getValue({
commandClass: cc,
endpoint: 0,
property: "prop",
propertyKey: "bar",
}),
).toBe(2);
});
it("the value added callback should have been called for each added value", () => {
expect(onValueAdded).toBeCalled();
const getArg = (call: number) => onValueAdded.mock.calls[call][0];
expect(getArg(0).propertyKey).toBe("foo");
expect(getArg(1).propertyKey).toBe("bar");
});
it("after clearing the value DB, the values should all be removed", () => {
valueDB.clear();
expect(
valueDB.getValue({
commandClass: cc,
endpoint: 0,
property: "prop",
propertyKey: "foo",
}),
).toBeUndefined();
expect(
valueDB.getValue({
commandClass: cc,
endpoint: 0,
property: "prop",
propertyKey: "bar",
}),
).toBeUndefined();
});
});
describe("removeValue()", () => {
let cbArg: any;
beforeAll(() => {
valueDB.setValue(
{
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 1,
property: "bar",
},
"foo",
);
valueDB.removeValue({
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 1,
property: "bar",
});
});
afterAll(() => {
onValueAdded.mockClear();
onValueUpdated.mockClear();
onValueRemoved.mockClear();
});
it("should emit the value removed event", () => {
expect(onValueRemoved).toBeCalled();
cbArg = onValueRemoved.mock.calls[0][0];
});
it("The callback arg should contain the CC", () => {
expect(cbArg).toBeObject();
expect(cbArg.commandClass).toBe(CommandClasses["Alarm Sensor"]);
});
it("The callback arg should contain the endpoint", () => {
expect(cbArg.endpoint).toBe(1);
});
it("The callback arg should contain the property name", () => {
expect(cbArg.property).toBe("bar");
});
it("The callback arg should contain the previous value", () => {
expect(cbArg.prevValue).toBe("foo");
});
it("If the value was not in the DB, the value removed event should not be emitted", () => {
onValueRemoved.mockClear();
valueDB.removeValue({
commandClass: CommandClasses["Basic Tariff Information"],
endpoint: 9,
property: "test",
});
expect(onValueRemoved).not.toBeCalled();
});
it("should return true if a value was removed, false otherwise", () => {
const retValNotFound = valueDB.removeValue({
commandClass: CommandClasses["Basic Tariff Information"],
endpoint: 9,
property: "test",
});
expect(retValNotFound).toBeFalse();
valueDB.setValue(
{
commandClass: CommandClasses["Basic Tariff Information"],
endpoint: 0,
property: "test",
},
"value",
);
const retValFound = valueDB.removeValue({
commandClass: CommandClasses["Basic Tariff Information"],
endpoint: 0,
property: "test",
});
expect(retValFound).toBeTrue();
});
it("After removing a value, getValue should return undefined", () => {
const actual = valueDB.getValue({
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 1,
property: "bar",
});
expect(actual).toBeUndefined();
});
});
describe("clear()", () => {
let cbArgs: any[];
const valueId1 = {
commandClass: CommandClasses["Alarm Sensor"],
endpoint: 1,
property: "bar",
};
const valueId2 = {
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "prop",
};
beforeAll(() => {
createValueDB();
valueDB.setValue(valueId1, "foo");
valueDB.setValue(valueId2, "bar");
valueDB.clear();
});
afterAll(() => {
onValueAdded.mockClear();
onValueUpdated.mockClear();
onValueRemoved.mockClear();
});
it("should emit the value removed event for all stored values", () => {
expect(onValueRemoved).toBeCalledTimes(2);
cbArgs = onValueRemoved.mock.calls.map((args) => args[0]);
});
it("The callback should contain the removed values", () => {
expect(cbArgs[0]).toBeObject();
expect(cbArgs[0].prevValue).toBe("foo");
expect(cbArgs[1]).toBeObject();
expect(cbArgs[1].prevValue).toBe("bar");
});
it("After clearing, getValue should return undefined", () => {
let actual: unknown;
actual = valueDB.getValue(valueId1);
expect(actual).toBeUndefined();
actual = valueDB.setValue(valueId2, "bar");
expect(actual).toBeUndefined();
});
});
describe("getValue() / getValues()", () => {
beforeEach(() => createValueDB());
it("getValue() should return the value stored for the same combination of CC, endpoint and property", () => {
const tests = [
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
value: "1",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "foo",
value: "2",
},
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
value: "3",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "FOO",
value: "4",
},
];
for (const { value, ...valueId } of tests) {
valueDB.setValue(valueId, value);
}
for (const { value, ...valueId } of tests) {
expect(valueDB.getValue(valueId)).toBe(value);
}
});
it("getValues() should return all values stored for the given CC", () => {
const values = [
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
value: "1",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "foo",
value: "2",
},
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
value: "3",
},
{
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "FOO",
value: "4",
},
];
const requestedCC = CommandClasses.Basic;
const expected = values.filter(
(t) => t.commandClass === requestedCC,
);
for (const { value, ...valueId } of values) {
valueDB.setValue(valueId, value);
}
const actual = valueDB.getValues(requestedCC);
expect(actual).toHaveLength(expected.length);
expect(actual).toContainAllValues(expected);
});
it("getValues() should ignore values from another node", () => {
const values = [
{
nodeId: 2,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
value: "1",
},
{
nodeId: 2,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "foo",
value: "2",
},
{
nodeId: 1,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
value: "3",
},
{
nodeId: 1,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "FOO",
value: "4",
},
];
const requestedCC = CommandClasses.Basic;
const expected = values
.filter((t) => t.commandClass === requestedCC && t.nodeId === 2)
.map(({ nodeId, ...rest }) => rest);
for (const { value, ...valueId } of values) {
(valueDB as any)._db.set(JSON.stringify(valueId), value);
}
// we're bypassing the index, so we need to fix that
valueDB["_index"] = valueDB["buildIndex"]();
const actual = valueDB.getValues(requestedCC);
expect(actual).toHaveLength(expected.length);
expect(actual).toContainAllValues(expected);
});
});
describe("hasValue()", () => {
beforeEach(() => createValueDB());
it("should return false if no value is stored for the same combination of CC, endpoint and property", () => {
const tests = [
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
value: "1",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "foo",
value: "2",
},
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
value: "3",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "FOO",
value: "4",
},
];
for (const { value, ...valueId } of tests) {
expect(valueDB.hasValue(valueId)).toBeFalse();
valueDB.setValue(valueId, value);
expect(valueDB.hasValue(valueId)).toBeTrue();
}
});
});
describe("findValues()", () => {
beforeEach(() => createValueDB());
it("should return all values whose id matches the given predicate", () => {
const values = [
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
value: "1",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "foo",
value: "2",
},
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
value: "3",
},
{
commandClass: CommandClasses.Basic,
endpoint: 2,
property: "FOO",
value: "4",
},
];
for (const { value, ...valueId } of values) {
valueDB.setValue(valueId, value);
}
expect(valueDB.findValues((id) => id.endpoint === 2)).toEqual(
values.filter((v) => v.endpoint === 2),
);
});
it("should ignore values from another node", () => {
const values = [
{
nodeId: 2,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
value: "1",
},
{
nodeId: 2,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "foo",
value: "2",
},
{
nodeId: 1,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
value: "3",
},
{
nodeId: 1,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "FOO",
value: "4",
},
];
for (const { value, ...valueId } of values) {
(valueDB as any)._db.set(JSON.stringify(valueId), value);
}
// we're bypassing the index, so we need to fix that
valueDB["_index"] = valueDB["buildIndex"]();
// The node has nodeID 2
const { nodeId, ...expected } = values[1];
expect(valueDB.findValues((id) => id.endpoint === 2)).toEqual([
expected,
]);
});
});
describe("Metadata", () => {
beforeEach(() => createValueDB());
it("is assigned to a specific combination of endpoint, property name (and property key)", () => {
const valueId: ValueID = {
commandClass: 1,
endpoint: 2,
property: "3",
};
valueDB.setMetadata(valueId, ValueMetadata.Any);
expect(valueDB.hasMetadata(valueId)).toBeTrue();
expect(
valueDB.hasMetadata({ ...valueId, propertyKey: 4 }),
).toBeFalse();
expect(valueDB.getMetadata(valueId)).toBe(ValueMetadata.Any);
expect(
valueDB.getMetadata({ ...valueId, propertyKey: 4 }),
).toBeUndefined();
});
it("is cleared together with the values", () => {
const valueId: ValueID = {
commandClass: 1,
endpoint: 2,
property: "3",
};
valueDB.setMetadata(valueId, ValueMetadata.Any);
valueDB.clear();
expect(valueDB.hasMetadata(valueId)).toBeFalse();
});
describe("getAllMetadata()", () => {
it("returns all metadata for a given CC", () => {
expect(valueDB.getAllMetadata(1)).toHaveLength(0);
valueDB.setMetadata(
{
commandClass: 1,
endpoint: 2,
property: "3",
},
ValueMetadata.Any,
);
expect(valueDB.getAllMetadata(1)).toHaveLength(1);
valueDB.setMetadata(
{
commandClass: 5,
endpoint: 2,
property: "3",
},
ValueMetadata.Any,
);
expect(valueDB.getAllMetadata(1)).toHaveLength(1);
expect(valueDB.getAllMetadata(5)).toHaveLength(1);
valueDB.setMetadata(
{
commandClass: 5,
endpoint: 2,
property: "5",
},
ValueMetadata.Any,
);
expect(valueDB.getAllMetadata(1)).toHaveLength(1);
expect(valueDB.getAllMetadata(5)).toHaveLength(2);
});
it("should ignore values from another node", () => {
const metadata = [
{
nodeId: 2,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
meta: ValueMetadata.Any,
},
{
nodeId: 2,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "foo",
meta: ValueMetadata.Any,
},
{
nodeId: 1,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
meta: ValueMetadata.Any,
},
{
nodeId: 1,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "FOO",
meta: ValueMetadata.Any,
},
];
const requestedCC = CommandClasses.Basic;
const expected = metadata
.filter(
(t) => t.commandClass === requestedCC && t.nodeId === 2,
)
.map(({ nodeId, meta, ...rest }) => ({
...rest,
metadata: meta,
}));
for (const { meta, ...valueId } of metadata) {
(valueDB as any)._metadata.set(
JSON.stringify(valueId),
meta,
);
}
// we're bypassing the index, so we need to fix that
valueDB["_index"] = valueDB["buildIndex"]();
const actual = valueDB.getAllMetadata(requestedCC);
expect(actual).toHaveLength(expected.length);
expect(actual).toContainAllValues(expected);
});
});
describe("updating dynamic metadata", () => {
let cbArg: any;
beforeAll(() => {
valueDB.setMetadata(
{
commandClass: 1,
endpoint: 2,
property: "3",
},
ValueMetadata.Any,
);
});
afterAll(() => {
onMetadataUpdated.mockClear();
});
it(`should emit the "metadata updated" event`, () => {
expect(onMetadataUpdated).toBeCalled();
cbArg = onMetadataUpdated.mock.calls[0][0];
});
it("The callback arg should contain the CC", () => {
expect(cbArg).toBeObject();
expect(cbArg.commandClass).toBe(1);
});
it("The callback arg should contain the endpoint", () => {
expect(cbArg.endpoint).toBe(2);
});
it("The callback arg should contain the property", () => {
expect(cbArg.property).toBe("3");
});
it("The callback arg should contain the new metadata", () => {
expect(cbArg.metadata).toBe(ValueMetadata.Any);
});
});
});
describe("findMetadata()", () => {
beforeEach(() => createValueDB());
it("should return all metadata whose id matches the given predicate", () => {
const metadata = [
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
meta: ValueMetadata.Any,
},
{
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "foo",
meta: ValueMetadata.Any,
},
{
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
meta: ValueMetadata.Any,
},
{
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "FOO",
meta: ValueMetadata.Any,
},
];
for (const { meta, ...valueId } of metadata) {
valueDB.setMetadata(valueId, meta);
}
const expected = metadata
.filter((v) => v.endpoint === 2)
.map(({ meta, ...rest }) => ({
...rest,
metadata: meta,
}));
expect(valueDB.findMetadata((id) => id.endpoint === 2)).toEqual(
expected,
);
});
it("should ignore metadata from another node", () => {
const metadata = [
{
nodeId: 2,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "foo",
meta: ValueMetadata.Any,
},
{
nodeId: 2,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "foo",
meta: ValueMetadata.Any,
},
{
nodeId: 1,
commandClass: CommandClasses.Basic,
endpoint: 0,
property: "FOO",
meta: ValueMetadata.Any,
},
{
nodeId: 1,
commandClass: CommandClasses.Battery,
endpoint: 2,
property: "FOO",
meta: ValueMetadata.Any,
},
];
for (const { meta, ...valueId } of metadata) {
(valueDB as any)._metadata.set(JSON.stringify(valueId), meta);
}
// we're bypassing the index, so we need to fix that
valueDB["_index"] = valueDB["buildIndex"]();
// The node has nodeID 2
const expectedMeta = metadata[1];
const expected = {
...pick(expectedMeta, ["commandClass", "endpoint", "property"]),
metadata: expectedMeta.meta,
};
expect(valueDB.findMetadata((id) => id.endpoint === 2)).toEqual([
expected,
]);
});
});
describe("invalid value IDs should cause an error to be thrown", () => {
const invalidValueIDs = [
// missing required properties
{ commandClass: undefined, property: "test" },
{ commandClass: 1, property: undefined },
// wrong type
{ commandClass: "1", property: 5, propertyKey: 7, endpoint: 1 },
{ commandClass: 1, property: true, propertyKey: 7, endpoint: 1 },
{ commandClass: 1, property: 5, propertyKey: false, endpoint: 1 },
{ commandClass: 1, property: 5, propertyKey: 7, endpoint: "5" },
];
it("getValue()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(() => valueDB.getValue(valueId as any), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
});
}
});
it("setValue()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(() => valueDB.setValue(valueId as any, 0), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
});
}
});
it("hasValue()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(() => valueDB.hasValue(valueId as any), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
});
}
});
it("removeValue()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(() => valueDB.removeValue(valueId as any), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
});
}
});
it("getMetadata()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(() => valueDB.getMetadata(valueId as any), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
});
}
});
it("setMetadata()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(
() => valueDB.setMetadata(valueId as any, {} as any),
{
errorCode: ZWaveErrorCodes.Argument_Invalid,
},
);
}
});
it("hasMetadata()", () => {
for (const valueId of invalidValueIDs) {
assertZWaveError(() => valueDB.hasMetadata(valueId as any), {
errorCode: ZWaveErrorCodes.Argument_Invalid,
});
}
});
});
describe(`invalid value IDs should be ignored by the setXYZ methods when the "noThrow" parameter is true`, () => {
const invalidValueIDs = [
// missing required properties
{ commandClass: undefined, property: "test" },
{ commandClass: 1, property: undefined },
// wrong type
{ commandClass: "1", property: 5, propertyKey: 7, endpoint: 1 },
{ commandClass: 1, property: true, propertyKey: 7, endpoint: 1 },
{ commandClass: 1, property: 5, propertyKey: false, endpoint: 1 },
{ commandClass: 1, property: 5, propertyKey: 7, endpoint: "5" },
];
it("setValue()", () => {
for (const valueId of invalidValueIDs) {
expect(() =>
valueDB.setValue(valueId as any, 0, { noThrow: true }),
).not.toThrow();
}
});
it("setMetadata()", () => {
for (const valueId of invalidValueIDs) {
expect(() =>
valueDB.setMetadata(valueId as any, {} as any, {
noThrow: true,
}),
).not.toThrow();
}
});
});
describe("dbKeyToValueIdFast()", () => {
it("should work correctly", () => {
const tests: ({ nodeId: number } & ValueID)[] = [
{
nodeId: 1,
commandClass: 2,
endpoint: 3,
property: "4",
propertyKey: "5",
},
{
nodeId: 2,
commandClass: 4,
endpoint: 7,
property: "44",
propertyKey: 6,
},
{
nodeId: 3,
commandClass: 6,
endpoint: 11,
property: 48,
propertyKey: "8",
},
{
nodeId: 4,
commandClass: 9,
endpoint: 17,
property: 48,
propertyKey: 9,
},
{ nodeId: 6, commandClass: 13, endpoint: 0, property: "c" },
];
for (const test of tests) {
expect(dbKeyToValueIdFast(JSON.stringify(test))).toEqual(test);
}
});
});
});