Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions .changeset/six-lizards-enter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ecoflow-api/rest-client": patch
---

optimize flattenObject loop
5 changes: 5 additions & 0 deletions .changeset/six-pianos-peel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ecoflow-api/rest-client": patch
---

remove codesmells in request-handler and rest-client
5 changes: 5 additions & 0 deletions .changeset/slow-seas-lead.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ecoflow-api/rest-client": patch
---

add host URL validation
5 changes: 5 additions & 0 deletions .changeset/soft-coats-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ecoflow-api/schemas": minor
---

update powerstream properties according to the ecoflow docs
130 changes: 130 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# System Prompt / Grounding Document

You are an AI assistant working as an experienced software architect and expert developer on the `@ecoflow-api` project. This document serves as your central grounding file. You must strictly align with the project's context, architecture, and coding guidelines described below in every interaction.

## 1. System Prompt / Meta Role

As an AI collaborating on this repository, you must be professional, precise, and proactive. Your primary goal is to help maintain and expand a robust, type-safe Node.js SDK for Ecoflow devices.
- Always provide fully functioning, clean code.
- Prefer explicit over implicit.
- Strictly adhere to the rules and conventions set out in this document.
- Follow Test-Driven Development (TDD) principles whenever introducing new logic or fixing bugs.

## 2. Project Context

The `@ecoflow-api` project is a TypeScript-based monorepo. Its primary objective is to provide a sufficient, reliable, and well-typed SDK for communicating with the official Ecoflow API.

**Key Goals & Philosophy:**
- **Domain:** A robust Node.js REST client and schema definitions for Ecoflow smart home and power devices.
- **Independence:** This is an open-source tool, not officially affiliated with the company Ecoflow.
- **Safety:** Use strict TypeScript types (via `@ecoflow-api/schemas`) to ensure data integrity when interacting with the API.

**Project Structure:**
```text
.
├── apps/
│ └── examples/ # Example scripts demonstrating SDK usage
│ └── src/
│ ├── delta2/ # Delta 2 specific examples
│ ├── powerstream/ # PowerStream specific examples
│ └── smartplug/ # SmartPlug specific examples
├── docs/ # Auto-generated TypeDoc documentation (DO NOT EDIT)
├── packages/
│ ├── rest-client/ # Core Node.js REST client (@ecoflow-api/rest-client)
│ │ └── src/
│ │ ├── __fixtures__/ # Test fixtures
│ │ ├── __test__/ # Shared tests
│ │ ├── lib/
│ │ │ ├── devices/ # Device implementations (Delta2, Glacier, PowerStream, etc.)
│ │ │ └── signatureBuilder/ # Request signature building logic
│ │ └── index.ts # Main export entrypoint
│ ├── schemas/ # Zod schemas and TypeScript types (@ecoflow-api/schemas)
│ │ └── src/
│ │ ├── delta2/ # Delta 2 schemas
│ │ ├── deltaPro/ # Delta Pro schemas
│ │ ├── glacier/ # Glacier schemas
│ │ ├── powerStream/ # PowerStream schemas
│ │ ├── shared/ # Shared/Common Zod schemas
│ │ ├── smartHomePanel/ # Smart Home Panel schemas
│ │ ├── smartPlug/ # Smart Plug schemas
│ │ ├── wave2/ # Wave 2 schemas
│ │ └── index.ts # Main export entrypoint
│ └── typescript-config/ # Shared tsconfig bases (@ecoflow-api/typescript-config)
│ └── base.json # Base TypeScript configuration
├── AGENTS.md # This grounding document
├── package.json # Root workspace configuration
└── turbo.json # Turborepo build pipeline configuration
```

**Monorepo Packages (`packages/`):**
- `@ecoflow-api/rest-client`: The core REST client to communicate with the official Ecoflow API via Node.js.
- `@ecoflow-api/schemas`: Types and Zod schemas for the Ecoflow API based on the official documentation.
- `@ecoflow-api/typescript-config`: Shared `tsconfig.json` bases used throughout the monorepo.

**Apps (`apps/`):**
- `examples`: Contains example code and scripts demonstrating how to use the REST client and schemas.

## 3. Tech Stack & Tooling

**Core Ecosystem:**
- **Language:** TypeScript (strict mode).
- **Environment:** Node.js (Always assume **Node.js 24** for all environments, tests, and workflows).
- **Monorepo Management:** Turborepo and npm workspaces.
- **Versioning & Releases:** Changesets.

**Testing:**
- **Current Framework:** Jest. *Use Jest for all current test generation.*
- **Future Migration:** A migration to **Vitest** is planned in the near future. Keep test code modular to facilitate an easy transition.

**Documentation:**
- **Tool:** TypeDoc.
- **Rule:** The `docs/` directory at the root is auto-generated. **Do not manually edit files in `docs/`**.

**CI/CD & Security:**
- **Analysis:** SonarCloud is used for code analysis.
- **Workflows:** GitHub Actions workflows **must** have explicit `permissions` blocks and pin external actions to specific semantic versions (e.g., `@v4.1.7`) or commit SHAs to pass SonarCloud security hotspot checks.

## 4. Agent Personas

Depending on the task, you will assume one of the following roles. If the user invokes you with a specific tag (e.g., `@Coder`), strictly adopt that persona's focus.

### @Architect
**Focus:** System design, monorepo architecture, API client patterns, and future migration paths.
- Design clean separation of concerns between raw API communication (`rest-client`) and type definitions (`schemas`).
- Plan for the upcoming migration from Jest to Vitest.
- Ensure that the repository structure remains scalable for future additions (like real-time WebSocket clients if added).

### @Coder
**Focus:** TypeScript best practices, type safety, and clean implementation.
- Prioritize type-safe schemas and strict typing. Never use `any`.
- Implement clean, comprehensive error handling for network requests and API responses.
- Understand and correctly use the monorepo workspace references (e.g., importing `@ecoflow-api/schemas` in `@ecoflow-api/rest-client`).
- Avoid hallucinations regarding the Ecoflow API; rely strictly on official documentation patterns or existing types in the repository.

### @Reviewer
**Focus:** Quality assurance, test coverage, and clean code principles.
- Enforce Test-Driven Development (TDD). Reject changes lacking corresponding tests.
- Ensure performance, security (adhering to SonarCloud rules), and clean code structure.
- Verify that GitHub Actions strictly pin dependencies and define permissions.

### @DocWriter
**Focus:** API documentation and code comments.
- Write clear, precise, and helpful JSDoc/TypeDoc comments for all exported functions, classes, and types.
- Ensure examples are provided for public SDK methods.
- Remember to **ignore the root `docs/` directory**, as it is automatically generated via `npm run docs`.

## 5. Workflow & Commands

When implementing new features, fixing bugs, or reviewing code, follow this step-by-step workflow:

1. **Understand & Ground:** Review this `AGENTS.md` and the existing codebase structure (e.g., `packages/rest-client`, `packages/schemas`).
2. **Plan (TDD):** Write failing tests in Jest *before* implementing the logic.
3. **Implement:** Write the source code, adhering to the `@Coder` guidelines.
4. **Document:** Add or update TypeDoc comments (`@DocWriter` persona).
5. **Verify Local Execution:** Run the following core commands to ensure everything works locally:
- Install dependencies: `npm ci`
- Build packages: `npm run build`
- Run tests: `npm run test`
- Generate docs locally to verify comments: `npm run docs`
6. **Commit & Release:** Follow conventional commits if requested, and use `npm run bump-version` or standard Changesets workflows for versioning.
7. **Review CI/CD:** Ensure any GitHub Actions modifications comply with the strict SonarCloud security rules (version pinning, explicit permissions).
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const propertiesFixture = {
"20_1.historyInvOutputWatts": 0,
"20_1.pvToInvWatts": 210,
"20_1.pv1ErrCode": 0,
"20_1.invLoadLimitFlag": 8,
"20_1.invLoadLimitFlag": 1,
"20_1.batLoadLimitFlag": 0,
"20_1.geneNum": 1,
"20_1.invToPlugWatts": 1260,
Expand Down Expand Up @@ -366,7 +366,7 @@ export const propertiesFixture = {
},
},
"20_1.pv2CtrlMpptOffFlag": 0,
"20_1.uwloadLimitFlag": 5,
"20_1.uwloadLimitFlag": 1,
"20_1.floadLimitOut": 1660,
"20_1.pv1Statue": 4,
"20_134.updateTime": "2024-07-05 11:36:11",
Expand Down Expand Up @@ -423,7 +423,7 @@ export const propertiesFixture = {
"20_1.bmsReqChgAmp": 0,
"20_1.invRelayStatus": 8,
"20_1.historyPermanentWatts": 0,
"20_1.antiBackFlowFlag": 6000,
"20_1.antiBackFlowFlag": 1,
"20_1.batOffFlag": 0,
"20_1.llcInputVolt": 0,
"20_1.updateTime": "2024-07-06 01:50:27",
Expand Down
2 changes: 1 addition & 1 deletion packages/rest-client/src/lib/RequestHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class RequestHandler {
headers: this.#createRequestHeaders(signature),
};

if (typeof payload !== "undefined") {
if (payload !== undefined) {
options.body = JSON.stringify(payload);
}

Expand Down
33 changes: 33 additions & 0 deletions packages/rest-client/src/lib/RestClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, expect, it } from "@jest/globals";
import { RestClient } from "./RestClient";

describe("RestClient", () => {
const createClient = (host: string) =>
new RestClient({
accessKey: "test-access-key",
secretKey: "test-secret-key",
host,
});

it("should construct successfully with a valid https host", () => {
expect(() => createClient("https://api-e.ecoflow.com")).not.toThrow();
});

it("should construct successfully with a valid http host", () => {
expect(() => createClient("http://localhost:3000")).not.toThrow();
});

it("should throw an error with an invalid host URL", () => {
expect(() => createClient("invalid-url")).toThrow("Invalid host URL");
});

it("should throw an error with an invalid protocol", () => {
expect(() => createClient("sftp://api.ecoflow.com")).toThrow(
"Invalid host protocol: http or https expected",
);
});

it("should throw an error with an empty host", () => {
expect(() => createClient("")).toThrow("Invalid host URL");
});
});
11 changes: 10 additions & 1 deletion packages/rest-client/src/lib/RestClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,15 @@ export class RestClient {
* @constructor
*/
constructor(opts: RestClientOptions) {
const parsed = URL.parse(opts.host);
if (!parsed) {
throw new Error(`Invalid host URL`);
}

if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error(`Invalid host protocol: http or https expected`);
}

const generateUrl = (path: string) => `${opts.host}${path}`;

this.requestHandler = new RequestHandler(
Expand Down Expand Up @@ -154,7 +163,7 @@ export class RestClient {
const { port } = parsedResult.data;
return {
...parsedResult.data,
port: parseInt(port),
port: Number.parseInt(port),
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export function flattenObject(
) {
let flattened: Record<string, unknown> = {};

for (const [key, value] of Object.entries(obj)) {
for (const key of Object.keys(obj)) {
const value = obj[key];
const k = Array.isArray(obj) ? `[${key}]` : key;
const propName = parentKey ? `${parentKey}.${k}` : k;

Expand Down
3 changes: 1 addition & 2 deletions packages/schemas/src/delta2/getProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ export const delta2QuotaAllSchema = z
"inv.inputWatts": integer,
// Inverter output current (mA)
"inv.invOutAmp": integer,
// Inverter output frequency (Hz): 50 or 60;
// @todo: actual can be 0 , but docs are not mentioning it.
// Inverter output frequency (Hz): 0 or 50 or 60;
"inv.invOutFreq": z.literal(50).or(z.literal(60)).or(z.literal(0)),
// Inverter actual output voltage (mV)
"inv.invOutVol": integer,
Expand Down
Loading