From 3e0d2e730c20273557c180aa9b0bc4e3a410a8c2 Mon Sep 17 00:00:00 2001 From: Felix Hofmann Date: Fri, 17 Oct 2025 22:32:19 +0200 Subject: [PATCH 1/2] chore: add `getProperty` method for retrieving specific device properties and update tests across device modules --- .../src/__fixtures__/delta2Properties.ts | 4 +- .../src/__fixtures__/deltaProProperties.ts | 4 +- .../src/__fixtures__/glacierProperties.ts | 4 +- .../src/__fixtures__/powerStreamProperties.ts | 4 +- .../src/__fixtures__/shpProperties.ts | 4 +- .../src/__fixtures__/smartPlugProperties.ts | 4 +- .../src/lib/devices/Delta2.test.ts | 22 +++++---- .../src/lib/devices/DeltaPro.test.ts | 22 +++++---- .../rest-client/src/lib/devices/Device.ts | 10 ++++ .../src/lib/devices/Glacier.test.ts | 22 +++++---- .../src/lib/devices/PowerStream.test.ts | 22 +++++---- .../src/lib/devices/SmartHomePanel.test.ts | 22 +++++---- .../src/lib/devices/SmartPlug.test.ts | 31 ++++++------- .../src/lib/devices/UnkownDevice.test.ts | 46 +++++++++++++++++++ 14 files changed, 153 insertions(+), 68 deletions(-) create mode 100644 packages/rest-client/src/lib/devices/UnkownDevice.test.ts diff --git a/packages/rest-client/src/__fixtures__/delta2Properties.ts b/packages/rest-client/src/__fixtures__/delta2Properties.ts index 55ec642..a9e6aa2 100644 --- a/packages/rest-client/src/__fixtures__/delta2Properties.ts +++ b/packages/rest-client/src/__fixtures__/delta2Properties.ts @@ -1,6 +1,6 @@ export const propertiesFixture = { - code: "0", - message: "Success", + code: "0" as const, + message: "Success" as const, data: { "mppt.faultCode": 0, "bms_emsStatus.maxChargeSoc": 80, diff --git a/packages/rest-client/src/__fixtures__/deltaProProperties.ts b/packages/rest-client/src/__fixtures__/deltaProProperties.ts index d9fcf55..3c85d43 100644 --- a/packages/rest-client/src/__fixtures__/deltaProProperties.ts +++ b/packages/rest-client/src/__fixtures__/deltaProProperties.ts @@ -1,6 +1,6 @@ export const propertiesFixture = { - code: "0", - message: "Success", + code: "0" as const, + message: "Success" as const, data: { "pd.iconWifiMode": 0, "mppt.faultCode": 0, diff --git a/packages/rest-client/src/__fixtures__/glacierProperties.ts b/packages/rest-client/src/__fixtures__/glacierProperties.ts index cff5fb5..b281806 100644 --- a/packages/rest-client/src/__fixtures__/glacierProperties.ts +++ b/packages/rest-client/src/__fixtures__/glacierProperties.ts @@ -1,8 +1,8 @@ // Note: This is a dummy data created based on the documents, it's not taken from a real device // Maybe someone can provide the real data for this device. export const glacierProperties = { - code: "0", - message: "Success", + code: "0" as const, + message: "Success" as const, data: { "bms_bmsStatus.amp": 20, "bms_bmsStatus.bmsFault": 20, diff --git a/packages/rest-client/src/__fixtures__/powerStreamProperties.ts b/packages/rest-client/src/__fixtures__/powerStreamProperties.ts index 90b63e3..f1b9f36 100644 --- a/packages/rest-client/src/__fixtures__/powerStreamProperties.ts +++ b/packages/rest-client/src/__fixtures__/powerStreamProperties.ts @@ -1,6 +1,6 @@ export const propertiesFixture = { - code: "0", - message: "Success", + code: "0" as const, + message: "Success" as const, data: { "20_1.pv2Temp": 240, "20_1.invOutputWatts": 1710, diff --git a/packages/rest-client/src/__fixtures__/shpProperties.ts b/packages/rest-client/src/__fixtures__/shpProperties.ts index 7807df6..ca40c6e 100644 --- a/packages/rest-client/src/__fixtures__/shpProperties.ts +++ b/packages/rest-client/src/__fixtures__/shpProperties.ts @@ -1,6 +1,6 @@ export const propertiesFixture = { - code: "0", - message: "Success", + code: "0" as const, + message: "Success" as const, data: { "heartbeat.errorCodes": [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/packages/rest-client/src/__fixtures__/smartPlugProperties.ts b/packages/rest-client/src/__fixtures__/smartPlugProperties.ts index 3533d7b..4919acd 100644 --- a/packages/rest-client/src/__fixtures__/smartPlugProperties.ts +++ b/packages/rest-client/src/__fixtures__/smartPlugProperties.ts @@ -341,7 +341,7 @@ export const smartPlugProperties = { }; export const devicePropertiesResponse = { - code: "0", - message: "success", + code: "0" as const, + message: "Success" as const, data: smartPlugProperties, }; diff --git a/packages/rest-client/src/lib/devices/Delta2.test.ts b/packages/rest-client/src/lib/devices/Delta2.test.ts index 0ea6160..ab12f6e 100644 --- a/packages/rest-client/src/lib/devices/Delta2.test.ts +++ b/packages/rest-client/src/lib/devices/Delta2.test.ts @@ -19,8 +19,10 @@ describe("Delta2", () => { restClient = new RestClient(restClientOptions); delta2 = new Delta2(restClient, validSn); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue(propertiesFixture); }); it("Should be able to construct an instance of Delta2", () => { @@ -35,15 +37,19 @@ describe("Delta2", () => { }); it("Should return data if api response could be parsed", async () => { - // @ts-ignore - restClient.getDevicePropertiesPlain = jest - .fn() - // @ts-ignore - .mockResolvedValue(propertiesFixture); - await expect(delta2.getProperties()).resolves.toBeDefined(); }); + it("returns the requested property", async () => { + await expect(delta2.getProperty("bms_bmsStatus.f32ShowSoc")).resolves.toBe( + propertiesFixture.data["bms_bmsStatus.f32ShowSoc"], + ); + }); + + it("returns undefined for non existing property", async () => { + await expect(delta2.getProperty("foobar")).resolves.toBeUndefined(); + }); + it("Should throw an error for invalid data received from api", async () => { await getPropertiesFailsOnInvalidResponse(restClient, delta2); }); diff --git a/packages/rest-client/src/lib/devices/DeltaPro.test.ts b/packages/rest-client/src/lib/devices/DeltaPro.test.ts index d79581e..cec63b8 100644 --- a/packages/rest-client/src/lib/devices/DeltaPro.test.ts +++ b/packages/rest-client/src/lib/devices/DeltaPro.test.ts @@ -19,8 +19,10 @@ describe("Delta Pro", () => { restClient = new RestClient(restClientOptions); device = new DeltaPro(restClient, validSn); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue(propertiesFixture); }); it("Should be able to construct an instance of Delta Pro", () => { @@ -39,15 +41,19 @@ describe("Delta Pro", () => { }); it("Should return data if api response could be parsed", async () => { - // @ts-ignore - restClient.getDevicePropertiesPlain = jest - .fn() - // @ts-ignore - .mockResolvedValue(propertiesFixture); - await expect(device.getProperties()).resolves.toBeDefined(); }); + it("returns the requested property", async () => { + await expect(device.getProperty("bmsMaster.soc")).resolves.toBe( + propertiesFixture.data["bmsMaster.soc"], + ); + }); + + it("returns undefined for non existing property", async () => { + await expect(device.getProperty("foobar")).resolves.toBeUndefined(); + }); + it("should enable xboost", async () => { const enabled = 1; const xboost = 1; diff --git a/packages/rest-client/src/lib/devices/Device.ts b/packages/rest-client/src/lib/devices/Device.ts index 2b41d3f..273f84f 100644 --- a/packages/rest-client/src/lib/devices/Device.ts +++ b/packages/rest-client/src/lib/devices/Device.ts @@ -33,4 +33,14 @@ export abstract class Device { const response = await this.restClient.getDevicePropertiesPlain(this.sn); return this.parseProperties(response.data); } + + /** + * Retrieves the specified property of a device, if present. + */ + async getProperty( + property: K, + ): Promise { + const response = await this.restClient.getDevicePropertiesPlain(this.sn); + return this.parseProperties(response.data)[property]; + } } diff --git a/packages/rest-client/src/lib/devices/Glacier.test.ts b/packages/rest-client/src/lib/devices/Glacier.test.ts index 8b4d0e9..ad6b9f0 100644 --- a/packages/rest-client/src/lib/devices/Glacier.test.ts +++ b/packages/rest-client/src/lib/devices/Glacier.test.ts @@ -19,8 +19,10 @@ describe("Glacier", () => { restClient = new RestClient(restClientOptions); glacier = new Glacier(restClient, validSn); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue(glacierProperties); }); it("Should be able to construct an instance of Glacier", () => { @@ -34,6 +36,16 @@ describe("Glacier", () => { }).toThrowError("Invalid serial number for Glacier device."); }); + it("returns the requested property", async () => { + await expect(glacier.getProperty("bms_emsStatus.dsgCmd")).resolves.toBe( + glacierProperties.data["bms_emsStatus.dsgCmd"], + ); + }); + + it("returns undefined for non existing property", async () => { + await expect(glacier.getProperty("foobar")).resolves.toBeUndefined(); + }); + it("should set temperature for right, left and middle zones", async () => { expect.assertions(1); await glacier.setTemperature({ @@ -422,12 +434,6 @@ describe("Glacier", () => { }); it("Should return data if api response could be parsed", async () => { - // @ts-ignore - restClient.getDevicePropertiesPlain = jest - .fn() - // @ts-ignore - .mockResolvedValue(glacierProperties); - await expect(glacier.getProperties()).resolves.toBeDefined(); }); }); diff --git a/packages/rest-client/src/lib/devices/PowerStream.test.ts b/packages/rest-client/src/lib/devices/PowerStream.test.ts index edd4d52..ec95dde 100644 --- a/packages/rest-client/src/lib/devices/PowerStream.test.ts +++ b/packages/rest-client/src/lib/devices/PowerStream.test.ts @@ -19,8 +19,10 @@ describe("PowerStream", () => { restClient = new RestClient(restClientOptions); powerStream = new PowerStream(restClient, validSn); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue(propertiesFixture); }); it("Should be able to construct an instance of PowerStream", () => { @@ -34,6 +36,16 @@ describe("PowerStream", () => { }).toThrowError("Invalid serial number for powerStream device."); }); + it("returns the requested property", async () => { + await expect(powerStream.getProperty("20_1.pv1InputWatts")).resolves.toBe( + propertiesFixture.data["20_1.pv1InputWatts"], + ); + }); + + it("returns undefined for non existing property", async () => { + await expect(powerStream.getProperty("foobar")).resolves.toBeUndefined(); + }); + it("should set power supply priority", async () => { expect.assertions(1); const priority = "powerSupply"; @@ -164,12 +176,6 @@ describe("PowerStream", () => { }); it("Should return data if api response could be parsed", async () => { - // @ts-ignore - restClient.getDevicePropertiesPlain = jest - .fn() - // @ts-ignore - .mockResolvedValue(propertiesFixture); - await expect(powerStream.getProperties()).resolves.toBeDefined(); }); }); diff --git a/packages/rest-client/src/lib/devices/SmartHomePanel.test.ts b/packages/rest-client/src/lib/devices/SmartHomePanel.test.ts index f8e5193..bdff6e8 100644 --- a/packages/rest-client/src/lib/devices/SmartHomePanel.test.ts +++ b/packages/rest-client/src/lib/devices/SmartHomePanel.test.ts @@ -19,8 +19,10 @@ describe("SmartHomePanel", () => { restClient = new RestClient(restClientOptions); device = new SmartHomePanel(restClient, validSn); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue(propertiesFixture); }); it("Should be able to construct an instance of SmartHomePanel", () => { @@ -39,15 +41,19 @@ describe("SmartHomePanel", () => { }); it("Should return data if api response could be parsed", async () => { - // @ts-ignore - restClient.getDevicePropertiesPlain = jest - .fn() - // @ts-ignore - .mockResolvedValue(propertiesFixture); - await expect(device.getProperties()).resolves.toBeDefined(); }); + it("returns the requested property", async () => { + await expect(device.getProperty("areaInfo.cmdSet")).resolves.toBe( + propertiesFixture.data["areaInfo.cmdSet"], + ); + }); + + it("returns undefined for non existing property", async () => { + await expect(device.getProperty("foobar")).resolves.toBeUndefined(); + }); + it("should update rtc time", async () => { const rtcTime = { sec: 0, diff --git a/packages/rest-client/src/lib/devices/SmartPlug.test.ts b/packages/rest-client/src/lib/devices/SmartPlug.test.ts index 9cf7e18..08e2b30 100644 --- a/packages/rest-client/src/lib/devices/SmartPlug.test.ts +++ b/packages/rest-client/src/lib/devices/SmartPlug.test.ts @@ -18,6 +18,11 @@ describe("SmartPlug", () => { restClient = new RestClient(restClientOptions); smartPlug = new SmartPlug(restClient, validSn); + + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue(devicePropertiesResponse); }); it("Should be able to construct an instance of SmartPlug", () => { @@ -33,8 +38,6 @@ describe("SmartPlug", () => { it("Should be able to turn the smart plug on", async () => { expect.assertions(1); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); await smartPlug.switchOn(); @@ -49,8 +52,6 @@ describe("SmartPlug", () => { // it("Should be able to turn the smart plug off", async () => { expect.assertions(1); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); await smartPlug.switchOff(); expect(restClient.setCommandPlain).toHaveBeenCalledWith({ @@ -64,8 +65,6 @@ describe("SmartPlug", () => { it("Should be able to delete Task", async () => { expect.assertions(1); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); await smartPlug.deleteTask(42); expect(restClient.setCommandPlain).toHaveBeenCalledWith({ @@ -82,19 +81,21 @@ describe("SmartPlug", () => { }); it("Should return data if api response could be parsed", async () => { - // @ts-ignore - restClient.getDevicePropertiesPlain = jest - .fn() - // @ts-ignore - .mockResolvedValue(devicePropertiesResponse); - await expect(smartPlug.getProperties()).resolves.toBeDefined(); }); + it("returns the requested property", async () => { + await expect(smartPlug.getProperty("2_1.temp")).resolves.toBe( + devicePropertiesResponse.data["2_1.temp"], + ); + }); + + it("returns undefined for non existing property", async () => { + await expect(smartPlug.getProperty("foobar")).resolves.toBeUndefined(); + }); + it("should set led brightness", async () => { expect.assertions(1); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); const brightness = 42; await smartPlug.setLedBrightness(brightness); @@ -109,8 +110,6 @@ describe("SmartPlug", () => { it("should throw for an invalid brightness value", async () => { expect.assertions(2); - // @ts-ignore - restClient.setCommandPlain = jest.fn(); await expect(smartPlug.setLedBrightness(5000)).rejects.toThrowError(); await expect(smartPlug.setLedBrightness(-1)).rejects.toThrowError(); }); diff --git a/packages/rest-client/src/lib/devices/UnkownDevice.test.ts b/packages/rest-client/src/lib/devices/UnkownDevice.test.ts new file mode 100644 index 0000000..920237c --- /dev/null +++ b/packages/rest-client/src/lib/devices/UnkownDevice.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { RestClient, RestClientOptions } from "../RestClient"; +import { UnknownDevice } from "./UnknownDevice"; + +describe("UnkownDevice", () => { + let device: UnknownDevice; + let restClient: RestClient; + const sn = "some-sn"; + + beforeEach(() => { + const restClientOptions: RestClientOptions = { + host: "https://api-a.ecoflow.com", + accessKey: "fake_access", + secretKey: "fake_secret", + }; + + restClient = new RestClient(restClientOptions); + device = new UnknownDevice(restClient, sn); + + restClient.setCommandPlain = jest.fn(); + restClient.getDevicePropertiesPlain = jest + .fn() + .mockResolvedValue({ + code: "0" as const, + message: "Success" as const, + data: { foo: "bar" }, + }); + }); + + it("Should be able to construct an instance of SmartPlug", () => { + expect(device).toBeTruthy(); + expect(device).toBeInstanceOf(UnknownDevice); + }); + + it("Should return data if api response could be parsed", async () => { + await expect(device.getProperties()).resolves.toBeDefined(); + }); + + it("returns the requested property", async () => { + await expect(device.getProperty("foo")).resolves.toBe("bar"); + }); + + it("returns undefined for non existing property", async () => { + await expect(device.getProperty("bar")).resolves.toBeUndefined(); + }); +}); From b86b2522565721452736081a5294b9d310f6c3b2 Mon Sep 17 00:00:00 2001 From: Felix Hofmann Date: Fri, 17 Oct 2025 22:34:43 +0200 Subject: [PATCH 2/2] chore: add changeset for `getProperty` method in rest-client and schemas --- .changeset/seven-crabs-enter.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/seven-crabs-enter.md diff --git a/.changeset/seven-crabs-enter.md b/.changeset/seven-crabs-enter.md new file mode 100644 index 0000000..3e8c00c --- /dev/null +++ b/.changeset/seven-crabs-enter.md @@ -0,0 +1,7 @@ +--- +"@ecoflow-api/rest-client": minor +"@ecoflow-api/schemas": minor +"examples": minor +--- + +add getProperty method on the device to query for a specific property value