UNPKG

zigbee-herdsman

Version:

An open source ZigBee gateway solution with node.js.

1,285 lines (1,058 loc) 47.5 kB
import type {MockInstance} from "vitest"; import {SerialPort} from "../../../src/adapter/serialPort"; import {Constants as UnpiConstants, Frame as UnpiFrame} from "../../../src/adapter/z-stack/unpi"; import {Znp, ZpiObject} from "../../../src/adapter/z-stack/znp"; import BuffaloZnp from "../../../src/adapter/z-stack/znp/buffaloZnp"; import ParameterType from "../../../src/adapter/z-stack/znp/parameterType"; import {logger} from "../../../src/utils/logger"; import * as Zdo from "../../../src/zspec/zdo"; import {duplicateArray, ieeeaAddr1, ieeeaAddr2} from "../../testUtils"; const consoleLogger = logger; const mockSerialPortClose = vi.fn().mockImplementation((cb) => (cb ? cb() : null)); const mockSerialPortFlush = vi.fn().mockImplementation((cb) => cb()); const mockSerialPortAsyncFlushAndClose = vi.fn(); const mockSerialPortPipe = vi.fn(); const mockSerialPortList = vi.fn().mockReturnValue([]); const mockSerialPortOpen = vi.fn().mockImplementation((cb) => cb()); const mockSerialPortAsyncOpen = vi.fn(); const mockSerialPortConstructor = vi.fn(); const mockSerialPortOnce = vi.fn(); const mockSerialPortAsyncSet = vi.fn(); const mockSerialPortWrite = vi.fn((_buffer, cb) => cb()); let mockSerialPortIsOpen = false; vi.mock("../../../src/utils/wait", () => ({ wait: vi.fn(() => { return new Promise<void>((resolve) => resolve()); }), })); vi.mock("../../../src/adapter/serialPort", () => ({ SerialPort: vi.fn(() => ({ close: mockSerialPortClose, constructor: mockSerialPortConstructor, emit: () => {}, on: () => {}, once: mockSerialPortOnce, open: mockSerialPortOpen, pipe: mockSerialPortPipe, write: mockSerialPortWrite, flush: mockSerialPortFlush, isOpen: mockSerialPortIsOpen, asyncOpen: mockSerialPortAsyncOpen, asyncFlushAndClose: mockSerialPortAsyncFlushAndClose, asyncSet: mockSerialPortAsyncSet, })), })); const mockSocketSetNoDelay = vi.fn(); const mockSocketSetKeepAlive = vi.fn(); const mockSocketPipe = vi.fn(); const mockSocketOnce = vi.fn(); const mockSocketCallbacks = {}; const mockSocketConnect = vi.fn(() => { mockSocketCallbacks.connect(); mockSocketCallbacks.ready(); }); const mockSocketDestroy = vi.fn(); let requestSpy: MockInstance; vi.mock("node:net", async () => ({ Socket: vi.fn(() => ({ setNoDelay: mockSocketSetNoDelay, pipe: mockSocketPipe, connect: mockSocketConnect, on: (event, cb) => { mockSocketCallbacks[event] = cb; }, once: mockSocketOnce, destroy: mockSocketDestroy, setKeepAlive: mockSocketSetKeepAlive, })), })); SerialPort.list = mockSerialPortList; const mockUnpiParserOn = vi.fn(); vi.mock("../../../src/adapter/z-stack/unpi/parser", () => ({ Parser: vi.fn(() => ({ on: mockUnpiParserOn, })), })); const mockUnpiWriterWriteFrame = vi.fn(); const mockUnpiWriterWriteBuffer = vi.fn(); vi.mock("../../../src/adapter/z-stack/unpi/writer", () => ({ Writer: vi.fn(() => ({ writeFrame: mockUnpiWriterWriteFrame, writeBuffer: mockUnpiWriterWriteBuffer, pipe: vi.fn(), })), })); const mocks = [ mockSerialPortClose, mockSerialPortPipe, mockSerialPortConstructor, mockSerialPortOpen, mockSerialPortOnce, mockSerialPortWrite, SerialPort, mockUnpiParserOn, mockUnpiWriterWriteFrame, mockUnpiWriterWriteBuffer, mockSerialPortFlush, mockSerialPortAsyncFlushAndClose, mockSerialPortAsyncOpen, ]; describe("ZNP", () => { let znp: Znp; beforeAll(() => { vi.useFakeTimers(); }); afterAll(() => { vi.useRealTimers(); }); beforeEach(() => { for (const mock of mocks) { // @ts-ignore mock.mockClear(); } // @ts-ignore; make sure we always get a new instance znp = new Znp("/dev/ttyACM0", 100, true); requestSpy = vi.spyOn(znp, "request").mockImplementation(() => {}); }); afterEach(() => { requestSpy.mockRestore(); }); it("Open", async () => { await znp.open(); expect(SerialPort).toHaveBeenCalledTimes(1); expect(SerialPort).toHaveBeenCalledWith({path: "/dev/ttyACM0", autoOpen: false, baudRate: 100, rtscts: true}); expect(mockSerialPortPipe).toHaveBeenCalledTimes(1); expect(mockSerialPortAsyncOpen).toHaveBeenCalledTimes(1); expect(mockSerialPortOnce).toHaveBeenCalledTimes(2); expect(mockUnpiWriterWriteBuffer).toHaveBeenCalledTimes(0); }); it("Open - first ping fails should send reset bootloader", async () => { requestSpy.mockImplementation(() => { throw new Error("failed"); }); await znp.open(); expect(SerialPort).toHaveBeenCalledTimes(1); expect(SerialPort).toHaveBeenCalledWith({path: "/dev/ttyACM0", autoOpen: false, baudRate: 100, rtscts: true}); expect(mockSerialPortPipe).toHaveBeenCalledTimes(1); expect(mockSerialPortAsyncOpen).toHaveBeenCalledTimes(1); expect(mockUnpiWriterWriteBuffer).toHaveBeenCalledTimes(1); expect(mockSerialPortOnce).toHaveBeenCalledTimes(2); }); it("Open with defaults", async () => { znp = new Znp("/dev/ttyACM0", undefined, undefined); requestSpy = vi.spyOn(znp, "request").mockImplementation(() => {}); await znp.open(); expect(SerialPort).toHaveBeenCalledTimes(1); expect(SerialPort).toHaveBeenCalledWith({path: "/dev/ttyACM0", autoOpen: false, baudRate: 115200, rtscts: false}); expect(mockSerialPortPipe).toHaveBeenCalledTimes(1); expect(mockSerialPortAsyncOpen).toHaveBeenCalledTimes(1); expect(mockSerialPortOnce).toHaveBeenCalledTimes(2); }); it("Open and close tcp port", async () => { znp = new Znp("tcp://localhost:8080", 100, false); await znp.open(); expect(mockSocketConnect).toBeCalledTimes(1); expect(mockSocketConnect).toBeCalledWith(8080, "localhost"); expect(znp.isInitialized()).toBeTruthy(); expect(mockUnpiWriterWriteBuffer).toHaveBeenCalledTimes(1); await znp.close(); expect(mockSocketDestroy).toHaveBeenCalledTimes(1); }); it("Open tcp port with socket error", async () => { mockSocketConnect.mockImplementationOnce(() => { mockSocketCallbacks.error(); }); znp = new Znp("tcp://localhost:666", 100, false); let error = false; try { await znp.open(); } catch (e) { error = e; } expect(error).toStrictEqual(new Error("Error while opening socket")); expect(znp.isInitialized()).toBeFalsy(); }); it("Open with error", async () => { mockSerialPortAsyncOpen.mockImplementationOnce(() => { return new Promise((_resolve, reject) => { reject("failed!"); }); }); mockSerialPortIsOpen = true; let error = false; try { await znp.open(); } catch (e) { error = e; } expect(SerialPort).toHaveBeenCalledTimes(1); expect(SerialPort).toHaveBeenCalledWith({path: "/dev/ttyACM0", autoOpen: false, baudRate: 100, rtscts: true}); expect(error).toEqual("failed!"); expect(mockSerialPortPipe).toHaveBeenCalledTimes(1); expect(mockSerialPortAsyncOpen).toHaveBeenCalledTimes(1); expect(mockSerialPortClose).toHaveBeenCalledTimes(1); expect(mockUnpiWriterWriteBuffer).toHaveBeenCalledTimes(0); expect(mockSerialPortOnce).toHaveBeenCalledTimes(0); }); it("Open with error when serialport is not open", async () => { mockSerialPortAsyncOpen.mockImplementationOnce(() => { return new Promise((_resolve, reject) => { reject("failed!"); }); }); mockSerialPortIsOpen = false; let error = false; try { await znp.open(); } catch (e) { error = e; } expect(SerialPort).toHaveBeenCalledTimes(1); expect(SerialPort).toHaveBeenCalledWith({path: "/dev/ttyACM0", autoOpen: false, baudRate: 100, rtscts: true}); expect(error).toEqual("failed!"); expect(mockSerialPortPipe).toHaveBeenCalledTimes(1); expect(mockSerialPortAsyncOpen).toHaveBeenCalledTimes(1); expect(mockSerialPortClose).toHaveBeenCalledTimes(0); expect(mockUnpiWriterWriteBuffer).toHaveBeenCalledTimes(0); expect(mockSerialPortOnce).toHaveBeenCalledTimes(0); }); it("Open and close", async () => { const close = vi.fn(); znp.on("close", close); expect(znp.isInitialized()).toBeFalsy(); await znp.open(); expect(znp.isInitialized()).toBeTruthy(); await znp.close(); expect(znp.isInitialized()).toBeFalsy(); expect(mockSerialPortAsyncFlushAndClose).toHaveBeenCalledTimes(1); expect(close).toHaveBeenCalledTimes(1); }); it("Open and close error", async () => { const close = vi.fn(); znp.on("close", close); mockSerialPortAsyncFlushAndClose.mockImplementationOnce(() => { return new Promise((_resolve, reject) => { reject("failed!"); }); }); await znp.open(); let error; try { await znp.close(); } catch (e) { error = e; } expect(mockSerialPortAsyncFlushAndClose).toHaveBeenCalledTimes(1); expect(error).toEqual("failed!"); expect(close).toHaveBeenCalledTimes(1); }); it("Close without initialization", async () => { const close = vi.fn(); znp.on("close", close); mockSerialPortAsyncFlushAndClose.mockImplementationOnce(() => { return new Promise((_resolve, reject) => { reject("failed!"); }); }); await znp.close(); expect(mockSerialPortAsyncFlushAndClose).toHaveBeenCalledTimes(0); expect(close).toHaveBeenCalledTimes(1); }); it("Open and close by serialport event", async () => { let closeCb; mockSerialPortOnce.mockImplementation((event, cb) => { if (event === "close") { closeCb = cb; } }); const close = vi.fn(); znp.on("close", close); await znp.open(); closeCb(); expect(close).toHaveBeenCalledTimes(1); }); it("Serialport error (do nothing)", async () => { let errorCb; mockSerialPortOnce.mockImplementation((event, cb) => { if (event === "error") { errorCb = cb; } }); await znp.open(); errorCb(); }); it("znp receive", async () => { let parsedCb; const received = vi.fn(); znp.on("received", received); mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); znp.open(); parsedCb( new UnpiFrame( UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x02, Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x01, 0x01, 0x01, 0x01]), ), ); expect(received).toHaveBeenCalledTimes(1); const obj = received.mock.calls[0][0]; expect(obj.command.name).toBe("version"); expect(obj.command.ID).toBe(2); expect(obj.payload).toStrictEqual({maintrel: 5, majorrel: 3, minorrel: 4, product: 2, revision: 16843009, transportrev: 1}); expect(obj.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(obj.type).toBe(UnpiConstants.Type.SRSP); }); it("znp receive malformed", async () => { let parsedCb; const received = vi.fn(); znp.on("received", received); mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); znp.open(); parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x02, Buffer.from([0x01, 0x02, 0x03, 0x04]))); expect(received).toHaveBeenCalledTimes(0); }); it("znp request SREQ", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); }); await znp.open(); requestSpy.mockRestore(); const result = await znp.requestWithReply(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}); const frame = mockUnpiWriterWriteFrame.mock.calls[0][0]; expect(mockUnpiWriterWriteFrame).toHaveBeenCalledTimes(1); expect(frame.commandID).toBe(8); expect(frame.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(frame.type).toBe(UnpiConstants.Type.SREQ); expect(frame.data).toStrictEqual(Buffer.from([0x01, 0x00, 0x02])); expect(result.command.name).toBe("osalNvRead"); expect(result.command.ID).toBe(0x08); expect(result.payload).toStrictEqual({status: 0, len: 2, value: Buffer.from([0x01, 0x02])}); expect(result.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(result.type).toBe(UnpiConstants.Type.SRSP); }); it("znp request SREQ failed", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x01, 0x02, 0x01, 0x02]))); }); await znp.open(); requestSpy.mockRestore(); expect(znp.waitress.waiters.size).toBe(0); let error; try { await znp.request(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}); } catch (e) { expect(znp.waitress.waiters.size).toBe(0); error = e; } expect(error).toStrictEqual( new Error("--> 'SREQ: SYS - osalNvRead - {\"id\":1,\"offset\":2}' failed with status '(0x01: FAILURE)' (expected '(0x00: SUCCESS)')"), ); }); it("znp request SREQ failed should cancel waiter when provided", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x01, 0x02, 0x01, 0x02]))); }); await znp.open(); requestSpy.mockRestore(); expect(znp.waitress.waiters.size).toBe(0); const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, "osalNvRead"); expect(znp.waitress.waiters.size).toBe(1); let error; try { await znp.request(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}, waiter.ID); } catch (e) { expect(znp.waitress.waiters.size).toBe(0); error = e; } expect(error).toStrictEqual( new Error("--> 'SREQ: SYS - osalNvRead - {\"id\":1,\"offset\":2}' failed with status '(0x01: FAILURE)' (expected '(0x00: SUCCESS)')"), ); }); it("znp request SREQ with parsed in between", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.AF, 0x00, Buffer.from([0x00]))); parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); }); await znp.open(); requestSpy.mockRestore(); const result = await znp.request(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}); const frame = mockUnpiWriterWriteFrame.mock.calls[0][0]; expect(mockUnpiWriterWriteFrame).toHaveBeenCalledTimes(1); expect(frame.commandID).toBe(8); expect(frame.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(frame.type).toBe(UnpiConstants.Type.SREQ); expect(frame.data).toStrictEqual(Buffer.from([0x01, 0x00, 0x02])); expect(result.command.name).toBe("osalNvRead"); expect(result.command.ID).toBe(0x08); expect(result.payload).toStrictEqual({status: 0, len: 2, value: Buffer.from([0x01, 0x02])}); expect(result.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(result.type).toBe(UnpiConstants.Type.SRSP); }); it("znp request AREQ reset", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.SYS, 0x80, Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06]))); }); await znp.open(); requestSpy.mockRestore(); const result = await znp.request(UnpiConstants.Subsystem.SYS, "resetReq", {type: 1}); const frame = mockUnpiWriterWriteFrame.mock.calls[0][0]; expect(mockUnpiWriterWriteFrame).toHaveBeenCalledTimes(1); expect(frame.commandID).toBe(0); expect(frame.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(frame.type).toBe(UnpiConstants.Type.AREQ); expect(frame.data).toStrictEqual(Buffer.from([1])); expect(result.command.name).toBe("resetInd"); expect(result.command.ID).toBe(0x80); expect(result.payload).toStrictEqual({reason: 1, transportrev: 2, productid: 3, majorrel: 4, minorrel: 5, hwrev: 6}); expect(result.subsystem).toBe(UnpiConstants.Subsystem.SYS); expect(result.type).toBe(UnpiConstants.Type.AREQ); }); it("znp request AREQ", async () => { await znp.open(); requestSpy.mockRestore(); const result = await znp.request(UnpiConstants.Subsystem.SAPI, "startConfirm", {status: 1}); const frame = mockUnpiWriterWriteFrame.mock.calls[0][0]; expect(mockUnpiWriterWriteFrame).toHaveBeenCalledTimes(1); expect(frame.commandID).toBe(128); expect(frame.subsystem).toBe(UnpiConstants.Subsystem.SAPI); expect(frame.type).toBe(UnpiConstants.Type.AREQ); expect(frame.data).toStrictEqual(Buffer.from([1])); expect(result).toBe(undefined); }); it("znp request without init", async () => { let error; requestSpy.mockRestore(); try { await znp.request(UnpiConstants.Subsystem.SAPI, "startConfirm", {status: 1}); } catch (e) { error = e; } expect(error).toEqual(new Error("Cannot request when znp has not been initialized yet")); }); it("znp request with non-existing subsystem", async () => { await znp.open(); requestSpy.mockRestore(); let error; try { await znp.request(999, "startConfirm", {status: 1}); } catch (e) { error = e; } expect(error).toEqual(new Error("Subsystem '999' does not exist")); }); it("znp request with non-existing cmd", async () => { await znp.open(); requestSpy.mockRestore(); let error; try { await znp.request(UnpiConstants.Subsystem.SAPI, "nonExisting", {status: 1}); } catch (e) { error = e; } expect(error).toEqual(new Error("Command request 'nonExisting' from subsystem '6' not found")); }); it("znp request timeout", async () => { await znp.open(); requestSpy.mockRestore(); const result = znp.request(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}); vi.runAllTimers(); let error; try { await result; } catch (e) { error = e; } expect(error).toStrictEqual(new Error("SRSP - SYS - osalNvRead after 6000ms")); }); it("znp request timeout for startupFromApp is longer", async () => { await znp.open(); requestSpy.mockRestore(); const result = znp.request(UnpiConstants.Subsystem.ZDO, "startupFromApp", {startdelay: 100}); vi.advanceTimersByTime(30000); let error; try { vi.advanceTimersByTime(15000); await result; } catch (e) { error = e; } expect(error).toStrictEqual(new Error("SRSP - ZDO - startupFromApp after 40000ms")); }); it("znp request, responses comes after timeout", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); let result = znp.request(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}); vi.runAllTimers(); parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); let error; try { result = await result; } catch (e) { error = e; } expect(error).toStrictEqual(new Error("SRSP - SYS - osalNvRead after 6000ms")); }); it("znp request, waitFor", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, "osalNvRead"); znp.request(UnpiConstants.Subsystem.SYS, "osalNvRead", {id: 1, offset: 2}); parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); const object = await waiter.start().promise; expect(object.payload).toStrictEqual({len: 2, status: 0, value: Buffer.from([1, 2])}); }); it("znp request ZDO", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x00]))); }); await znp.open(); const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); const result = await znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, 1); const frame = mockUnpiWriterWriteFrame.mock.calls[0][0]; expect(mockUnpiWriterWriteFrame).toHaveBeenCalledTimes(1); expect(frame.commandID).toBe(2); expect(frame.subsystem).toBe(UnpiConstants.Subsystem.ZDO); expect(frame.type).toBe(UnpiConstants.Type.SREQ); expect(frame.data).toStrictEqual(zdoPayload); expect(result).toBe(undefined); }); it("znp request ZDO SUCCESS", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, "nodeDescReq"); const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, 1); parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x00]))); const object = await waiter.start().promise; expect(object.payload).toStrictEqual({status: 0x00}); }); it("znp request ZDO FAILURE", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x01]))); }); await znp.open(); const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); let error; try { await znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, undefined); } catch (e) { error = e; } expect(error).toStrictEqual( new Error(`--> 'SREQ: ZDO - NODE_DESCRIPTOR_REQUEST - ${zdoPayload.toString("hex")}' failed with status '(0x01: FAILURE)'`), ); }); it("znp request ZDO failed should cancel waiter when provided", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); mockUnpiWriterWriteFrame.mockImplementationOnce(() => { parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.ZDO, 2, Buffer.from([0x01]))); }); await znp.open(); expect(znp.waitress.waiters.size).toBe(0); const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, "nodeDescRsp"); expect(znp.waitress.waiters.size).toBe(1); const zdoPayload = Buffer.from([2 & 0xff, (2 >> 8) & 0xff, ...Zdo.Buffalo.buildRequest(false, Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, 2)]); let error; try { await znp.requestZdo(Zdo.ClusterId.NODE_DESCRIPTOR_REQUEST, zdoPayload, waiter.ID); } catch (e) { expect(znp.waitress.waiters.size).toBe(0); error = e; } expect(error).toStrictEqual( new Error(`--> 'SREQ: ZDO - NODE_DESCRIPTOR_REQUEST - ${zdoPayload.toString("hex")}' failed with status '(0x01: FAILURE)'`), ); }); it("znp waitFor with transid", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.AF, "dataConfirm", undefined, 123); parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.AF, 128, Buffer.from([0, 1, 123]))); const object = await waiter.start().promise; expect(object.payload).toStrictEqual({status: 0, endpoint: 1, transid: 123}); }); it("znp waitFor with target as network address", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, "activeEpRsp", 0x1234); parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 133, Buffer.from([0x34, 0x12, 0x00, 0x34, 0x12, 0x00]))); const object = await waiter.start().promise; expect(object.payload.zdo).toStrictEqual([ Zdo.Status.SUCCESS, { nwkAddress: 0x1234, endpointList: [], }, ]); }); it("znp waitFor with target as IEEE", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, "nwkAddrRsp", "0x0807060504030201"); parsedCb( new UnpiFrame( UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 128, Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, 0x00, 0x02, 0x10, 0x10, 0x11, 0x11]), ), ); const object = await waiter.start().promise; expect(object.payload.zdo).toStrictEqual([ Zdo.Status.SUCCESS, { assocDevList: [4112, 4369], eui64: "0x0807060504030201", // numassocdev: 2, nwkAddress: 257, startIndex: 0, }, ]); }); it("znp waitFor with target as IEEE forced to timeout because invalid ZDO status (no payload to match against)", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, "nwkAddrRsp", "0x0807060504030201").start(); parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 128, Buffer.from([Zdo.Status.INVALID_INDEX]))); vi.advanceTimersByTime(11000); await expect(waiter.promise).rejects.toThrow("AREQ - ZDO - nwkAddrRsp after 10000ms"); }); it("znp waitFor with state", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, "stateChangeInd", undefined, undefined, 9); parsedCb(new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 192, Buffer.from([9]))); const object = await waiter.start().promise; expect(object.payload).toStrictEqual({state: 9}); }); it("znp waitFor with payload mismatch", async () => { let parsedCb; mockUnpiParserOn.mockImplementationOnce((event, cb) => { if (event === "parsed") { parsedCb = cb; } }); await znp.open(); requestSpy.mockRestore(); const waiter = znp.waitFor(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, "osalNvRead", "abcd").start(); parsedCb(new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.SYS, 0x08, Buffer.from([0x00, 0x02, 0x01, 0x02]))); vi.advanceTimersByTime(11000); await expect(waiter.promise).rejects.toThrow("SRSP - SYS - osalNvRead after 10000ms"); }); it("znp requestWithReply should throw error when request as no reply", async () => { await znp.open(); try { await znp.requestWithReply(UnpiConstants.Subsystem.ZDO, "autoFindDestination", {}); fail("Should throw error"); } catch (error) { expect(error).toStrictEqual(new Error("Command autoFindDestination has no reply")); } }); it("ZpiObject throw error on missing write parser", async () => { // @ts-ignore; make sure we always get a new instance const obj = new ZpiObject(0, 0, "dummy", 0, {}, [{name: "nonExisting", parameterType: 9999999}]); expect(() => { obj.createPayloadBuffer(); }).toThrow(); }); it("ZpiObject throw error on unknown command", async () => { const frame = new UnpiFrame(UnpiConstants.Type.SREQ, UnpiConstants.Subsystem.AF, 99999, Buffer.alloc(0)); expect(() => { ZpiObject.fromUnpiFrame(frame); }).toThrow(); }); it("ZpiObject throw error on unknown parameters", async () => { const frame = new UnpiFrame(UnpiConstants.Type.SRSP, UnpiConstants.Subsystem.AF, 128, Buffer.alloc(0)); expect(() => { ZpiObject.fromUnpiFrame(frame); }).toThrow(); }); it("ZpiObject with cmd and non sapi is not reset command", () => { // @ts-ignore; make sure we always get a new instance const obj = new ZpiObject(UnpiConstants.Type.SREQ, UnpiConstants.Subsystem.AF, "systemReset", 0, {}, []); expect(obj.isResetCommand()).toBeFalsy(); }); it("ZpiObject parse payload for endDeviceAnnceInd", () => { const buffer = Buffer.from([0, 0, 0, 1, 1, 2, 3, 4, 5, 6, 7, 8, 5]); const frame = new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 193, buffer); const obj = ZpiObject.fromUnpiFrame(frame); expect(obj.payload.zdo).toStrictEqual([ Zdo.Status.SUCCESS, { capabilities: { allocateAddress: 0, alternatePANCoordinator: 1, deviceType: 0, powerSource: 1, reserved1: 0, reserved2: 0, rxOnWhenIdle: 0, securityCapability: 0, }, eui64: "0x0807060504030201", nwkAddress: 256, }, ]); }); it("ZpiObject parse payload for nwkAddrRsp", () => { const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01, 0x00, 0x02, 0x10, 0x10, 0x11, 0x11]); const frame = new UnpiFrame(UnpiConstants.Type.AREQ, UnpiConstants.Subsystem.ZDO, 128, buffer); const obj = ZpiObject.fromUnpiFrame(frame); expect(obj.payload.zdo).toStrictEqual([ Zdo.Status.SUCCESS, { assocDevList: [4112, 4369], eui64: "0x0807060504030201", // numassocdev: 2, nwkAddress: 257, startIndex: 0, }, ]); }); it("Cant read unsupported type", () => { expect(() => { const buffalo = new BuffaloZnp(Buffer.alloc(0)); // @ts-expect-error invalid typing buffalo.read(9999, {}); }).toThrow(new Error("Read for '9999' not available")); }); it("UINT8 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(3), 1); buffalo.write(ParameterType.UINT8, 240, {}); expect(buffalo.getPosition()).toEqual(2); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, 0xf0, 0x00])); }); it("UINT8 read", () => { const buffalo = new BuffaloZnp(Buffer.from([0x00, 0x03, 0x00, 0x00]), 1); const value = buffalo.read(ParameterType.UINT8, {}); expect(buffalo.getPosition()).toEqual(2); expect(value).toStrictEqual(3); }); it("INT8 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(3), 1); buffalo.write(ParameterType.INT8, 127, {}); expect(buffalo.getPosition()).toEqual(2); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, 0x7f, 0x00])); }); it("INT8 read", () => { const buffalo = new BuffaloZnp(Buffer.from([0x00, 0xf0, 0x00, 0x00]), 1); const value = buffalo.read(ParameterType.INT8, {}); expect(buffalo.getPosition()).toEqual(2); expect(value).toStrictEqual(-16); }); it("UINT16 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(3), 1); buffalo.write(ParameterType.UINT16, 1020, {}); expect(buffalo.getPosition()).toEqual(3); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, 0xfc, 0x03])); }); it("UINT16 read", () => { const buffalo = new BuffaloZnp(Buffer.from([0x00, 0x03, 0xff, 0x00]), 1); const value = buffalo.read(ParameterType.UINT16, {}); expect(buffalo.getPosition()).toEqual(3); expect(value).toStrictEqual(65283); }); it("UINT32 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(6), 2); buffalo.write(ParameterType.UINT32, 1065283, {}); expect(buffalo.getPosition()).toEqual(6); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, 0x00, 0x43, 0x41, 0x10, 0x00])); }); it("UINT32 read", () => { const buffalo = new BuffaloZnp(Buffer.from([0x01, 0x03, 0xff, 0xff])); const value = buffalo.read(ParameterType.UINT32, {}); expect(buffalo.getPosition()).toEqual(4); expect(value).toStrictEqual(4294902529); }); it("LIST_UINT8 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(4), 1); const payload = [200, 100]; buffalo.write(ParameterType.LIST_UINT8, payload, {}); expect(buffalo.getPosition()).toStrictEqual(3); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, 0xc8, 0x64, 0x00])); }); it("LIST_UINT8 read", () => { const buffalo = new BuffaloZnp(Buffer.from([0x00, 0x00, 0x04, 0x08]), 2); const value = buffalo.read(ParameterType.LIST_UINT8, {length: 2}); expect(buffalo.getPosition()).toStrictEqual(4); expect(value).toStrictEqual([4, 8]); }); it("LIST_UINT16 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(5), 1); const payload = [1024, 2048]; buffalo.write(ParameterType.LIST_UINT16, payload, {}); expect(buffalo.getPosition()).toStrictEqual(5); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, 0x00, 0x04, 0x00, 0x08])); }); it("LIST_UINT16 read", () => { const buffalo = new BuffaloZnp(Buffer.from([0x00, 0x00, 0x04, 0x00, 0x08]), 1); const value = buffalo.read(ParameterType.LIST_UINT16, {length: 2}); expect(buffalo.getPosition()).toStrictEqual(5); expect(value).toStrictEqual([1024, 2048]); }); it("LIST_NETWORK write", () => { expect(() => { const buffalo = new BuffaloZnp(Buffer.alloc(10)); buffalo.write(ParameterType.LIST_NETWORK, [], {}); }).toThrow(); }); it("LIST_NETWORK read", () => { const buffer = Buffer.from([0x05, 0x10, 0x10, 0x09, 0x31, 0x13, 0x01, 0x10, 0x10, 0x09, 0x31, 0x13, 0x00, 0x01]); const buffalo = new BuffaloZnp(buffer, 1); const value = buffalo.read(ParameterType.LIST_NETWORK, {length: 2}); expect(buffalo.getPosition()).toStrictEqual(13); expect(value).toStrictEqual([ { beaconOrder: 3, logicalChannel: 9, neightborPanId: 4112, permitJoin: 1, stackProfile: 1, superFrameOrder: 1, zigbeeVersion: 3, }, { beaconOrder: 3, logicalChannel: 9, neightborPanId: 4112, permitJoin: 0, stackProfile: 1, superFrameOrder: 1, zigbeeVersion: 3, }, ]); }); it("BUFFER8 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(9), 1); const payload = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); buffalo.write(ParameterType.BUFFER8, payload, {}); expect(buffalo.getPosition()).toStrictEqual(9); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, ...payload])); }); it("BUFFER8 write length consistent", () => { const buffalo = new BuffaloZnp(Buffer.alloc(9)); const payload = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); expect(() => { buffalo.write(ParameterType.BUFFER8, payload, {}); }).toThrow(); }); it("BUFFER8 read", () => { const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]); const buffalo = new BuffaloZnp(buffer, 2); const value = buffalo.read(ParameterType.BUFFER8, {}); expect(buffalo.getPosition()).toEqual(10); expect(value).toStrictEqual(buffer.subarray(2, 11)); }); it("BUFFER16 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(20), 1); const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; buffalo.write(ParameterType.BUFFER16, Buffer.from([...payload, ...payload]), {}); expect(buffalo.getPosition()).toStrictEqual(17); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, ...payload, ...payload, 0x00, 0x00, 0x00])); }); it("BUFFER16 read", () => { const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; const buffalo = new BuffaloZnp(Buffer.from([0x00, ...payload, ...payload]), 1); const value = buffalo.read(ParameterType.BUFFER16, {}); expect(buffalo.getPosition()).toEqual(17); expect(value).toStrictEqual(Buffer.from([...payload, ...payload])); }); it("BUFFER18 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(20), 1); const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; buffalo.write(ParameterType.BUFFER18, Buffer.from([...payload, ...payload]), {}); expect(buffalo.getPosition()).toStrictEqual(19); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, ...payload, ...payload, 0x00])); }); it("BUFFER18 read", () => { const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; const buffalo = new BuffaloZnp(Buffer.from([0x00, ...payload, ...payload]), 1); const value = buffalo.read(ParameterType.BUFFER18, {}); expect(buffalo.getPosition()).toStrictEqual(19); expect(value).toStrictEqual(Buffer.from([...payload, ...payload])); }); it("BUFFER32 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(34), 1); const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; buffalo.write(ParameterType.BUFFER32, Buffer.from([...payload, ...payload, ...payload, ...payload]), {}); expect(buffalo.getPosition()).toStrictEqual(33); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, ...payload, ...payload, ...payload, ...payload, 0x00])); }); it("BUFFER32 read", () => { const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; const buffalo = new BuffaloZnp(Buffer.from([0x00, ...payload, ...payload, ...payload, ...payload]), 1); const value = buffalo.read(ParameterType.BUFFER32, {}); expect(buffalo.getPosition()).toStrictEqual(33); expect(value).toStrictEqual(Buffer.from([...payload, ...payload, ...payload, ...payload])); }); it("BUFFER42 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(44), 1); const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; buffalo.write(ParameterType.BUFFER42, Buffer.from([...payload, ...payload, ...payload, ...payload, ...payload, 0x01, 0xff]), {}); expect(buffalo.getPosition()).toStrictEqual(43); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, ...payload, ...payload, ...payload, ...payload, ...payload, 0x01, 0xff, 0x00])); }); it("BUFFER42 read", () => { const payload = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]; const buffalo = new BuffaloZnp(Buffer.from([0x00, ...payload, ...payload, ...payload, ...payload, ...payload, 0x08, 0x09]), 1); const value = buffalo.read(ParameterType.BUFFER42, {}); expect(buffalo.getPosition()).toStrictEqual(43); expect(value).toStrictEqual(Buffer.from([...payload, ...payload, ...payload, ...payload, ...payload, 0x08, 0x09])); }); it("BUFFER100 write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(100), 0); const payload = duplicateArray(20, [0x00, 0x01, 0x02, 0x03, 0x04]); buffalo.write(ParameterType.BUFFER100, Buffer.from(payload), {}); expect(buffalo.getPosition()).toStrictEqual(100); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from(payload)); }); it("BUFFER100 read", () => { const payload = duplicateArray(20, [0x00, 0x01, 0x02, 0x03, 0x04]); const buffalo = new BuffaloZnp(Buffer.from([0x00, ...payload]), 1); const value = buffalo.read(ParameterType.BUFFER100, {}); expect(buffalo.getPosition()).toStrictEqual(101); expect(value).toStrictEqual(Buffer.from(payload)); }); it("BUFFER write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(9), 1); const payload = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]); buffalo.write(ParameterType.BUFFER, payload, {}); expect(buffalo.getPosition()).toStrictEqual(9); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from([0x00, ...payload])); }); it("BUFFER read", () => { const buffer = Buffer.from([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09]); const buffalo = new BuffaloZnp(buffer, 2); const value = buffalo.read(ParameterType.BUFFER, {length: 1}); expect(buffalo.getPosition()).toEqual(3); expect(value).toStrictEqual(buffer.subarray(2, 3)); }); it("IEEEADDR write", () => { const buffalo = new BuffaloZnp(Buffer.alloc(8)); buffalo.write(ParameterType.IEEEADDR, ieeeaAddr1.string, {}); expect(buffalo.getPosition()).toEqual(8); expect(buffalo.getBuffer()).toStrictEqual(Buffer.from(ieeeaAddr1.hex)); }); it("IEEEADDR read", () => { const buffalo = new BuffaloZnp(Buffer.from(ieeeaAddr2.hex)); const value = buffalo.read(ParameterType.IEEEADDR, {}); expect(buffalo.getPosition()).toEqual(8); expect(value).toStrictEqual(ieeeaAddr2.string); }); it.each([ParameterType.BUFFER, ParameterType.LIST_UINT8, ParameterType.LIST_UINT16, ParameterType.LIST_NETWORK])( "Throws when read is missing required length option - param %s", (type) => { expect(() => { const buffalo = new BuffaloZnp(Buffer.alloc(1)); buffalo.read(type, {}); }).toThrow(`Cannot read ${ParameterType[type]} without length option specified`); }, ); it("Coverage logger", async () => { consoleLogger.warning(() => "Test warning", "TestNS"); consoleLogger.error(() => "Test error", "TestNS"); }); });