Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/clear-seas-find.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@ecoflow-api/rest-client": minor
"@ecoflow-api/schemas": minor
---

add support for River 2 Pro
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The `@ecoflow-api` project is a TypeScript-based monorepo. Its primary objective
│ │ ├── __fixtures__/ # Test fixtures
│ │ ├── __test__/ # Shared tests
│ │ ├── lib/
│ │ │ ├── devices/ # Device implementations (Delta2, Glacier, PowerStream, etc.)
│ │ │ ├── devices/ # Device implementations (Delta2, Glacier, PowerStream, River2Pro, etc.)
│ │ │ └── signatureBuilder/ # Request signature building logic
│ │ └── index.ts # Main export entrypoint
│ ├── schemas/ # Zod schemas and TypeScript types (@ecoflow-api/schemas)
Expand All @@ -44,6 +44,7 @@ The `@ecoflow-api` project is a TypeScript-based monorepo. Its primary objective
│ │ ├── deltaPro/ # Delta Pro schemas
│ │ ├── glacier/ # Glacier schemas
│ │ ├── powerStream/ # PowerStream schemas
│ │ ├── river2Pro/ # River 2 Pro schemas
│ │ ├── shared/ # Shared/Common Zod schemas
│ │ ├── smartHomePanel/ # Smart Home Panel schemas
│ │ ├── smartPlug/ # Smart Plug schemas
Expand Down
257 changes: 257 additions & 0 deletions api-docs/river2pro.md

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions packages/rest-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ Depending on the serial number of the device, the client will return a specific

At the moment the following devices are supported with specific instances:

- Smart Plug
- PowerStream
- Delta 2
- Glacier
- Smart Home Panel
- Delta Pro
- Delta Pro 3
- Glacier
- PowerStream
- River 2 Pro
- Smart Home Panel
- SmartPlug
- Wave Air Conditioner

More devices to come.

Expand Down
8 changes: 8 additions & 0 deletions packages/rest-client/src/lib/devices/DeviceFactory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Glacier } from "./Glacier";
import { SmartHomePanel } from "./SmartHomePanel";
import { DeltaPro } from "./DeltaPro";
import { DeltaPro3 } from "./DeltaPro3";
import { River2Pro } from "./River2Pro";

describe("deviceFactory", () => {
let restClient: RestClient;
Expand Down Expand Up @@ -74,4 +75,11 @@ describe("deviceFactory", () => {
expect(device).toBeInstanceOf(DeltaPro3);
expect(device.sn).toBe(sn);
});

it("returns a River 2 Pro instance", () => {
const sn = "R621*****";
const device = deviceFactory(sn, restClient);
expect(device).toBeInstanceOf(River2Pro);
expect(device.sn).toBe(sn);
});
});
8 changes: 8 additions & 0 deletions packages/rest-client/src/lib/devices/DeviceFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
isDeltaProSerialNumber,
isGlacierSerialNumber,
isPowerStreamSerialNumber,
isRiver2ProSerialNumber,
isSmartHomePanelSerialNumber,
isSmartPlugSn,
PowerStreamSerialNumber,
River2ProSerialNumber,
SmartHomePanelSerialNumber,
SmartPlugSn,
} from "@ecoflow-api/schemas";
Expand All @@ -23,6 +25,7 @@ import { Glacier } from "./Glacier";
import { SmartHomePanel } from "./SmartHomePanel";
import { DeltaPro } from "./DeltaPro";
import { DeltaPro3 } from "./DeltaPro3";
import { River2Pro } from "./River2Pro";

// prettier-ignore
export type DeviceFactoryReturnType<T extends string> =
Expand All @@ -33,6 +36,7 @@ export type DeviceFactoryReturnType<T extends string> =
T extends SmartHomePanelSerialNumber ? SmartHomePanel :
T extends DeltaProSerialNumber ? DeltaPro :
T extends DeltaPro3SerialNumber ? DeltaPro3 :
T extends River2ProSerialNumber ? River2Pro :
UnknownDevice;

/**
Expand Down Expand Up @@ -72,5 +76,9 @@ export function deviceFactory<T extends string, R = DeviceFactoryReturnType<T>>(
return new DeltaPro3(restClient, sn) as R;
}

if (isRiver2ProSerialNumber(sn)) {
return new River2Pro(restClient, sn) as R;
}

return new UnknownDevice(restClient, sn) as R;
}
156 changes: 156 additions & 0 deletions packages/rest-client/src/lib/devices/River2Pro.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { RestClient, RestClientOptions } from "../RestClient";
import { River2Pro } from "./River2Pro";

describe("River2Pro", () => {
let river2Pro: River2Pro;
let restClient: RestClient;
const validSn = "R621xxxx";

beforeEach(() => {
const restClientOptions: RestClientOptions = {
host: "https://api-a.ecoflow.com",
accessKey: "fake_access",
secretKey: "fake_secret",
};

restClient = new RestClient(restClientOptions);
river2Pro = new River2Pro(restClient, validSn);

restClient.setCommandPlain = jest.fn<RestClient["setCommandPlain"]>();
});

describe("constructor", () => {
it("accepts a valid River2Pro serial number", () => {
expect.assertions(1);

expect(() => new River2Pro(restClient, validSn)).not.toThrow();
});

it("throws for an invalid River2Pro serial number", () => {
expect.assertions(1);

expect(() => new River2Pro(restClient, "R331xxxx" as never)).toThrowError(
"Invalid serial number for River 2 Pro device.",
);
});
});

describe(".enable12Vdc()", () => {
it("sets 12V DC output", async () => {
expect.assertions(1);
await river2Pro.enable12Vdc(1);

expect(restClient.setCommandPlain).toHaveBeenCalledWith({
id: 123456789,
version: "1.0",
moduleType: 5,
operateType: "mpptCar",
params: {
enabled: 1,
},
sn: validSn,
});
});
});

describe(".setAcOutCfg()", () => {
it("sets AC output configuration", async () => {
expect.assertions(1);
await river2Pro.setAcOutCfg({
enabled: 1,
xboost: 1,
out_voltage: 30,
out_freq: 50,
});

expect(restClient.setCommandPlain).toHaveBeenCalledWith({
id: 123456789,
version: "1.0",
moduleType: 5,
operateType: "acOutCfg",
params: {
enabled: 1,
xboost: 1,
out_voltage: 30,
out_freq: 50,
},
sn: validSn,
});
});
});

describe(".setWatthConfigSchema()", () => {
it("sets energy management configuration", async () => {
expect.assertions(1);
await river2Pro.setWatthConfigSchema({
isConfig: 1,
bpPowerSoc: 88,
minDsgSoc: 30,
minChgSoc: 88,
});

expect(restClient.setCommandPlain).toHaveBeenCalledWith({
id: 123456789,
version: "1.0",
moduleType: 1,
operateType: "watthConfig",
params: {
isConfig: 1,
bpPowerSoc: 88,
minDsgSoc: 30,
minChgSoc: 88,
},
sn: validSn,
});
});
});

describe(".setChargeLimit()", () => {
it("sets charge limit", async () => {
expect.assertions(1);
await river2Pro.setChargeLimit(50);

expect(restClient.setCommandPlain).toHaveBeenCalledWith({
id: 123456789,
version: "1.0",
moduleType: 2,
operateType: "upsConfig",
params: {
maxChgSoc: 50,
},
sn: validSn,
});
});

it("throws for invalid charge limit values", async () => {
expect.assertions(2);
await expect(river2Pro.setChargeLimit(101)).rejects.toThrowError();
await expect(river2Pro.setChargeLimit(-1)).rejects.toThrowError();
});
});

describe(".setDischargeLimit()", () => {
it("sets discharge limit", async () => {
expect.assertions(1);
await river2Pro.setDischargeLimit(40);

expect(restClient.setCommandPlain).toHaveBeenCalledWith({
id: 123456789,
version: "1.0",
moduleType: 2,
operateType: "dsgCfg",
params: {
minDsgSoc: 40,
},
sn: validSn,
});
});

it("throws for invalid discharge limit values", async () => {
expect.assertions(2);
await expect(river2Pro.setDischargeLimit(101)).rejects.toThrowError();
await expect(river2Pro.setDischargeLimit(-1)).rejects.toThrowError();
});
});
});
Loading