import crypto from 'crypto';
export interface EcoFlowConfig {
accessKey: string;
secretKey: string;
baseUrl?: string;
}
export interface EcoFlowDevice {
sn: string;
deviceName: string;
online: number;
productName?: string;
}
export interface DeviceQuota {
[key: string]: unknown;
}
export interface ApiResponse<T> {
code: string;
message: string;
data: T;
}
export class EcoFlowClient {
private accessKey: string;
private secretKey: string;
private baseUrl: string;
constructor(config: EcoFlowConfig) {
this.accessKey = config.accessKey;
this.secretKey = config.secretKey;
this.baseUrl = config.baseUrl || 'https://api.ecoflow.com';
}
/**
* Generate a random nonce (6 digits)
*/
private generateNonce(): string {
return String(100000 + Math.floor(Math.random() * 900000));
}
/**
* Get current timestamp in milliseconds
*/
private getTimestamp(): string {
return String(Date.now());
}
/**
* Flatten nested object parameters for signing
*/
private flattenParams(params: Record<string, unknown>, prefix = ''): string[] {
const result: string[] = [];
const keys = Object.keys(params).sort();
for (const key of keys) {
const value = params[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
if (value !== null && value !== undefined) {
if (typeof value === 'object' && !Array.isArray(value)) {
result.push(...this.flattenParams(value as Record<string, unknown>, fullKey));
} else if (Array.isArray(value)) {
// Handle arrays - convert to JSON string or indexed format
result.push(`${fullKey}=${JSON.stringify(value)}`);
} else {
result.push(`${fullKey}=${value}`);
}
}
}
return result;
}
/**
* Generate HMAC-SHA256 signature for API request
*/
private generateSignature(
params: Record<string, unknown>,
nonce: string,
timestamp: string
): string {
// Flatten and sort parameters
const flatParams = this.flattenParams(params);
// Build the sign string
const headerParams = [
`accessKey=${this.accessKey}`,
`nonce=${nonce}`,
`timestamp=${timestamp}`
];
// Combine params with headers
const allParams = [...flatParams, ...headerParams].sort();
const signStr = allParams.join('&');
// Generate HMAC-SHA256 signature
const hmac = crypto.createHmac('sha256', this.secretKey);
hmac.update(signStr);
return hmac.digest('hex');
}
/**
* Make authenticated API request
*/
private async request<T>(
method: 'GET' | 'POST' | 'PUT',
endpoint: string,
params: Record<string, unknown> = {}
): Promise<ApiResponse<T>> {
const nonce = this.generateNonce();
const timestamp = this.getTimestamp();
const sign = this.generateSignature(params, nonce, timestamp);
const headers: Record<string, string> = {
'Content-Type': 'application/json;charset=UTF-8',
'accessKey': this.accessKey,
'nonce': nonce,
'timestamp': timestamp,
'sign': sign
};
let url = `${this.baseUrl}${endpoint}`;
let body: string | undefined;
if (method === 'GET') {
const queryParams = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
queryParams.append(key, String(value));
}
}
const queryString = queryParams.toString();
if (queryString) {
url += `?${queryString}`;
}
} else {
body = JSON.stringify(params);
}
const response = await fetch(url, {
method,
headers,
body
});
if (!response.ok) {
throw new Error(`EcoFlow API error: ${response.status} ${response.statusText}`);
}
const data = await response.json() as ApiResponse<T>;
if (data.code !== '0') {
throw new Error(`EcoFlow API error: ${data.code} - ${data.message}`);
}
return data;
}
/**
* Get list of all devices
*/
async getDevices(): Promise<EcoFlowDevice[]> {
const response = await this.request<EcoFlowDevice[]>(
'GET',
'/iot-open/sign/device/list'
);
return response.data || [];
}
/**
* Get all quotas/parameters for a device
*/
async getDeviceQuotaAll(serialNumber: string): Promise<DeviceQuota> {
const response = await this.request<DeviceQuota>(
'GET',
'/iot-open/sign/device/quota/all',
{ sn: serialNumber }
);
return response.data || {};
}
/**
* Get specific quotas for a device
*/
async getDeviceQuota(serialNumber: string, quotas: string[]): Promise<DeviceQuota> {
const response = await this.request<DeviceQuota>(
'POST',
'/iot-open/sign/device/quota',
{
sn: serialNumber,
params: { quotas }
}
);
return response.data || {};
}
/**
* Set device parameters
*/
async setDeviceQuota(
serialNumber: string,
moduleType: number,
operateType: string,
params: Record<string, unknown>
): Promise<void> {
await this.request<unknown>(
'PUT',
'/iot-open/sign/device/quota',
{
sn: serialNumber,
moduleType,
operateType,
params
}
);
}
/**
* Control AC output (enable/disable)
* Works for DELTA/RIVER series power stations
*/
async setAcOutput(
serialNumber: string,
enabled: boolean,
options?: {
xboost?: boolean;
outVoltage?: number;
outFrequency?: number;
}
): Promise<void> {
const params: Record<string, unknown> = {
enabled: enabled ? 1 : 0
};
if (options?.xboost !== undefined) {
params.xboost = options.xboost ? 1 : 0;
}
if (options?.outVoltage !== undefined) {
params.out_voltage = options.outVoltage;
}
if (options?.outFrequency !== undefined) {
params.out_freq = options.outFrequency;
}
// Module type 1 is typically for inverter control
// Operate type varies by device - using common ones
await this.setDeviceQuota(serialNumber, 1, 'acOutCfg', params);
}
/**
* Control DC output (enable/disable)
* Works for DELTA/RIVER series power stations
*/
async setDcOutput(serialNumber: string, enabled: boolean): Promise<void> {
const params = {
enabled: enabled ? 1 : 0
};
await this.setDeviceQuota(serialNumber, 1, 'dcOutCfg', params);
}
/**
* Set charging settings
*/
async setChargingSettings(
serialNumber: string,
options: {
maxChargeSoc?: number; // Max charge level (0-100)
minDischargeSoc?: number; // Min discharge level (0-100)
chargingWatts?: number; // Charging power in watts
}
): Promise<void> {
const params: Record<string, unknown> = {};
if (options.maxChargeSoc !== undefined) {
params.maxChgSoc = options.maxChargeSoc;
}
if (options.minDischargeSoc !== undefined) {
params.minDsgSoc = options.minDischargeSoc;
}
if (options.chargingWatts !== undefined) {
params.chgWatts = options.chargingWatts;
}
await this.setDeviceQuota(serialNumber, 2, 'upsConfig', params);
}
/**
* Control USB output
*/
async setUsbOutput(serialNumber: string, enabled: boolean): Promise<void> {
const params = {
enabled: enabled ? 1 : 0
};
await this.setDeviceQuota(serialNumber, 1, 'usbOutCfg', params);
}
/**
* Set standby timeout
*/
async setStandbyTimeout(
serialNumber: string,
options: {
deviceTimeout?: number; // Device standby timeout in minutes (0 = never)
acTimeout?: number; // AC output standby timeout in minutes
dcTimeout?: number; // DC output standby timeout in minutes
lcdTimeout?: number; // LCD screen timeout in seconds
}
): Promise<void> {
const params: Record<string, unknown> = {};
if (options.deviceTimeout !== undefined) {
params.standbyMin = options.deviceTimeout;
}
if (options.acTimeout !== undefined) {
params.acStandbyMin = options.acTimeout;
}
if (options.dcTimeout !== undefined) {
params.dcStandbyMin = options.dcTimeout;
}
if (options.lcdTimeout !== undefined) {
params.lcdOffSec = options.lcdTimeout;
}
await this.setDeviceQuota(serialNumber, 1, 'standbyTime', params);
}
/**
* Set beeper/buzzer enabled state (quiet mode)
*/
async setBeeper(serialNumber: string, enabled: boolean): Promise<void> {
const params = {
enabled: enabled ? 1 : 0
};
await this.setDeviceQuota(serialNumber, 1, 'beepCfg', params);
}
}