diff --git a/.changeset/six-lizards-enter.md b/.changeset/six-lizards-enter.md new file mode 100644 index 0000000..2d0523d --- /dev/null +++ b/.changeset/six-lizards-enter.md @@ -0,0 +1,5 @@ +--- +"@ecoflow-api/rest-client": patch +--- + +optimize flattenObject loop diff --git a/.changeset/six-pianos-peel.md b/.changeset/six-pianos-peel.md new file mode 100644 index 0000000..4a22dd8 --- /dev/null +++ b/.changeset/six-pianos-peel.md @@ -0,0 +1,5 @@ +--- +"@ecoflow-api/rest-client": patch +--- + +remove codesmells in request-handler and rest-client diff --git a/.changeset/slow-seas-lead.md b/.changeset/slow-seas-lead.md new file mode 100644 index 0000000..5e673b9 --- /dev/null +++ b/.changeset/slow-seas-lead.md @@ -0,0 +1,5 @@ +--- +"@ecoflow-api/rest-client": patch +--- + +add host URL validation diff --git a/.changeset/soft-coats-report.md b/.changeset/soft-coats-report.md new file mode 100644 index 0000000..3de0194 --- /dev/null +++ b/.changeset/soft-coats-report.md @@ -0,0 +1,5 @@ +--- +"@ecoflow-api/schemas": minor +--- + +update powerstream properties according to the ecoflow docs diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9491220 --- /dev/null +++ b/AGENTS.md @@ -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). diff --git a/packages/rest-client/src/__fixtures__/powerStreamProperties.ts b/packages/rest-client/src/__fixtures__/powerStreamProperties.ts index f1b9f36..a9e3267 100644 --- a/packages/rest-client/src/__fixtures__/powerStreamProperties.ts +++ b/packages/rest-client/src/__fixtures__/powerStreamProperties.ts @@ -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, @@ -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", @@ -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", diff --git a/packages/rest-client/src/lib/RequestHandler.ts b/packages/rest-client/src/lib/RequestHandler.ts index bab64f7..93f8c69 100644 --- a/packages/rest-client/src/lib/RequestHandler.ts +++ b/packages/rest-client/src/lib/RequestHandler.ts @@ -54,7 +54,7 @@ export class RequestHandler { headers: this.#createRequestHeaders(signature), }; - if (typeof payload !== "undefined") { + if (payload !== undefined) { options.body = JSON.stringify(payload); } diff --git a/packages/rest-client/src/lib/RestClient.test.ts b/packages/rest-client/src/lib/RestClient.test.ts new file mode 100644 index 0000000..8d265d7 --- /dev/null +++ b/packages/rest-client/src/lib/RestClient.test.ts @@ -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"); + }); +}); diff --git a/packages/rest-client/src/lib/RestClient.ts b/packages/rest-client/src/lib/RestClient.ts index 6bb864e..41ab8fe 100644 --- a/packages/rest-client/src/lib/RestClient.ts +++ b/packages/rest-client/src/lib/RestClient.ts @@ -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( @@ -154,7 +163,7 @@ export class RestClient { const { port } = parsedResult.data; return { ...parsedResult.data, - port: parseInt(port), + port: Number.parseInt(port), }; } diff --git a/packages/rest-client/src/lib/signatureBuilder/flattenObject.ts b/packages/rest-client/src/lib/signatureBuilder/flattenObject.ts index 3155831..000b24c 100644 --- a/packages/rest-client/src/lib/signatureBuilder/flattenObject.ts +++ b/packages/rest-client/src/lib/signatureBuilder/flattenObject.ts @@ -36,7 +36,8 @@ export function flattenObject( ) { let flattened: Record = {}; - 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; diff --git a/packages/schemas/src/delta2/getProperties.ts b/packages/schemas/src/delta2/getProperties.ts index 5de7bad..43623c1 100644 --- a/packages/schemas/src/delta2/getProperties.ts +++ b/packages/schemas/src/delta2/getProperties.ts @@ -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, diff --git a/packages/schemas/src/powerStream/getProperties.ts b/packages/schemas/src/powerStream/getProperties.ts index 8078ebb..9b8d7cd 100644 --- a/packages/schemas/src/powerStream/getProperties.ts +++ b/packages/schemas/src/powerStream/getProperties.ts @@ -25,9 +25,8 @@ export const powerStreamQuotaAllSchema = z "20_1.pv1InputWatts": integer.nonnegative(), // PV1 output voltage: 0.1 V "20_1.pv1OpVolt": integer.nonnegative(), - - // @todo: Not documented in the official docs. - "20_1.pv1CtrlMpptOffFlag": integer, + // PV1 on/off status: 0: off; 1: on + "20_1.pv1CtrlMpptOffFlag": zeroOrOne, /********** * PV 2 @@ -51,9 +50,8 @@ export const powerStreamQuotaAllSchema = z "20_1.pv2InputWatts": integer.nonnegative(), // PV2 input voltage: 0.1 V "20_1.pv2InputVolt": integer.nonnegative(), - - // @todo: Not documented in the official docs. - "20_1.pv2CtrlMpptOffFlag": integer, + // PV2 on/off status: 0: off; 1: on + "20_1.pv2CtrlMpptOffFlag": zeroOrOne, /********** * LLC @@ -71,9 +69,8 @@ export const powerStreamQuotaAllSchema = z "20_1.llcInputVolt": integer.nonnegative(), // LLC warning code "20_1.llcWarningCode": integer, - - // @todo: Not documented in the official docs. - "20_1.llcOffFlag": integer, + // LLC on/off status: 0: off; 1: on + "20_1.llcOffFlag": zeroOrOne, /********** * BAT @@ -99,17 +96,16 @@ export const powerStreamQuotaAllSchema = z "20_1.batTemp": integer, // BAT input current: 0.1 A; positive for discharging and negative for charging "20_1.batInputCur": integer, - - // @todo: Not documented in the official docs. + // Limited DC output power when DC power is low. Unit: 0.1W "20_1.batErrorInvLoadLimit": integer.nonnegative(), - // @todo: Not documented in the official docs. - "20_1.batLoadLimitFlag": integer, - // @todo: Not documented in the official docs. + // Whether the BAT module is derated.0: no; 1: yes + "20_1.batLoadLimitFlag": zeroOrOne, + // BAT power after derating. Unit: 0.1W "20_1.batOutputLoadLimit": integer, - // @todo: Not documented in the official docs. - "20_1.batOffFlag": integer, - // @todo: Not documented in the official docs. - "20_1.batSystem": integer, + // Battery on/off status: 0: off; 1: on + "20_1.batOffFlag": zeroOrOne, + // Whether PowerStream is connected to a power station or an EV 0: power station; 1: EV + "20_1.batSystem": zeroOrOne, /********** * INV @@ -143,15 +139,14 @@ export const powerStreamQuotaAllSchema = z "20_1.invRelayStatus": integer, // Micro-inverter AC warning code "20_1.invWarnCode": integer, - - // @todo: Not documented in the official docs. + // PV power after derating. Unit: 0.1W "20_1.invOutputLoadLimit": integer, // @todo: Not documented in the official docs. "20_1.invToPlugWatts": integer, // @todo: Not documented in the official docs. "20_1.invDemandWatts": integer, - // @todo: Not documented in the official docs. - "20_1.invLoadLimitFlag": integer, + // Whether the PV module is derated.0: no; 1: yes + "20_1.invLoadLimitFlag": zeroOrOne, // @todo: Not documented in the official docs. "20_1.invToOtherWatts": integer, @@ -200,79 +195,79 @@ export const powerStreamQuotaAllSchema = z // Charge Level "20_1.upperLimit": integer.nonnegative(), - // @todo: Not documented in the official docs. - "20_1.mqttTlsLastErr": integer, - // @todo: Not documented in the official docs. + // MQTT network error code + "20_1.mqttTlsLastErr": integer.nonnegative(), + // Number of devices consuming power "20_1.consNum": integer, - // @todo: Not documented in the official docs. - "20_1.feedProtect": integer, - // @todo: Not documented in the official docs. + // Feed-in control: 0: off; 1: on + "20_1.feedProtect": zeroOrOne, + // MQTT network error code "20_1.mqttSockErrno": integer.nonnegative(), // @todo: Not documented in the official docs. "20_1.pvToInvWatts": integer, - // @todo: Not documented in the official docs. + // Number of devices generating power "20_1.geneNum": integer, - // @todo: Not documented in the official docs. + // Mesh ID "20_1.meshId": integer, - // @todo: Not documented in the official docs. + // MQTT network error code "20_1.mqttTlsStackErr": integer, - // @todo: Not documented in the official docs. - "20_1.acOffFlag": integer, - // @todo: Not documented in the official docs. + // INV on/off status: 0: off; 1: on + "20_1.acOffFlag": zeroOrOne, + // Timestamp of the MQQT error code "20_1.mqttErrTime": integer, - // @todo: Not documented in the official docs - looks like, can be negative + // Static IP address, for determining whether devices are in the same LAN "20_1.staIpAddr": integer, - // @todo: Not documented in the official docs. - "20_1.uwlowLightFlag": integer, - // @todo: Not documented in the official docs - looks like, can be negative + // Whether anti-backflow is triggered due to low light.0: no; 1: yes + "20_1.uwlowLightFlag": zeroOrOne, + // Power consumed by Smart Plugs "20_1.consWatt": integer, - // @todo: Not documented in the official docs. + // Mesh layer "20_1.meshLayel": integer, - // @todo: Not documented in the official docs. - "20_1.uwloadLimitFlag": integer, - // @todo: Not documented in the official docs. + // Whether the INV module is derated.0: no; 1: yes + "20_1.uwloadLimitFlag": zeroOrOne, + // INV power after derating. Unit: 0.1W "20_1.floadLimitOut": integer, // @todo: Not documented in the official docs. "20_134.updateTime": z.string(), - // @todo: Not documented in the official docs. + // Timestamp of the Wi-Fi error code "20_1.wifiErrTime": integer.nonnegative(), - // @todo: Not documented in the official docs. + // Minimal stack space remaining "20_1.stackMinFree": integer, - // @todo: Not documented in the official docs. + // Number of restarts "20_1.resetCount": integer, - // @todo: Not documented in the official docs. - "20_1.uwsocFlag": integer, + // Whether anti-backflow is triggered by the battery.0: no; 1: yes + "20_1.uwsocFlag": zeroOrOne, // @todo: Not documented in the official docs. "20_1.plugTotalWatts": integer.nonnegative(), - // @todo: Not documented in the official docs. + // Cause of restart "20_1.resetReason": integer, - // @todo: Not documented in the official docs. + // Stack space remaining "20_1.stackFree": integer, - // @todo: Not documented in the official docs. + // MAC address "20_1.selfMac": integer, // @todo: Not documented in the official docs. "20_1.gridConsWatts": integer, - // @todo: Not documented in the official docs. + // Power generated "20_1.geneWatt": integer, - // @todo: Not documented in the official docs. + // BMS requesting voltage "20_1.bmsReqChgVol": integer, - // @todo: Not documented in the official docs. + // Port connection flag: bit0: AC connected; bit1: BAT connected; bit2: PV1connected; bit3: PV2 connected "20_1.interfaceConnFlag": integer, // @todo: Not documented in the official docs. "20_1.spaceDemandWatts": integer.nonnegative(), - // @todo: Not documented in the official docs. + // BMS requesting current "20_1.bmsReqChgAmp": integer, - // @todo: Not documented in the official docs. - "20_1.antiBackFlowFlag": integer, - // @todo: Not documented in the official docs. + // Whether anti-backflow is triggered.0: no; 1: yes + "20_1.antiBackFlowFlag": zeroOrOne, + // MQQT error code "20_1.mqttErr": integer, - // @todo: Not documented in the official docs. + // Wi-Fi error code "20_1.wifiErr": integer, - // @todo: Not documented in the official docs. + // Wi-Fi signal strength of the parent node "20_1.wifiRssi": integer, - // @todo: Not documented in the official docs. + // Limited AC output power when PV power is low. Unit: 0.1W "20_1.pvPowerLimitAcPower": integer, - // @todo: Not documented in the official docs. + // MAC address of the parent node "20_1.parentMac": integer, }) .passthrough();