smartpvms-api

SmartPVMS Northbound API Skill (v25.3.0)

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "smartpvms-api" with this command: npx skills add takzobye/smartpvms-northbound-api-skills/takzobye-smartpvms-northbound-api-skills-smartpvms-api

SmartPVMS Northbound API Skill (v25.3.0)

Quick Start

Before writing ANY code, read the relevant reference files:

Task Read First

Query plant/device data references/api-endpoints-query.md

Control battery/inverter references/api-endpoints-control.md

Know which fields exist references/data-fields.md

Handle errors & retries references/error-codes.md

Identify device types references/device-types.md

Architecture Pattern

Runtime: Bun | HTTP client: axios | Language: TypeScript

import axios, { AxiosInstance } from "axios";

interface SmartPVMSConfig { baseUrl: string; // e.g. "https://intl.fusionsolar.huawei.com" // API Account mode userName?: string; systemCode?: string; // OAuth Connect mode accessToken?: string; }

class SmartPVMSClient { private http: AxiosInstance; private xsrfToken: string | null = null; private tokenExpiresAt = 0; private config: SmartPVMSConfig;

constructor(config: SmartPVMSConfig) { this.config = config; this.http = axios.create({ baseURL: config.baseUrl, headers: { "Content-Type": "application/json" }, timeout: 30_000, }); }

Two Authentication Modes

Mode 1: API Account (XSRF-TOKEN)

  • Created by company admin in FusionSolar WebUI

  • Max 5 accounts per company

  • Token validity: 30 minutes (auto-extended on each call)

  • One online session per account — repeated login invalidates previous token

  • Supports: All query APIs + some control APIs

async login(): Promise<void> { const res = await this.http.post("/thirdData/login", { userName: this.config.userName, systemCode: this.config.systemCode, // NOT "password"! }); // Token is in response HEADER, not body const token = res.headers["xsrf-token"] || res.headers["set-cookie"] ?.find((c: string) => c.includes("XSRF-TOKEN")) ?.match(/XSRF-TOKEN=([^;]+)/)?.[1]; if (!token) throw new Error("No XSRF-TOKEN in response headers"); this.xsrfToken = token; this.tokenExpiresAt = Date.now() + 25 * 60 * 1000; // 25min safety margin }

private async ensureAuth(): Promise<void> { if (!this.xsrfToken || Date.now() >= this.tokenExpiresAt) { await this.login(); } }

Mode 2: OAuth Connect (Bearer Token)

  • For third-party app integration, owner authorizes access

  • OAuth server: https://oauth2.fusionsolar.huawei.com

  • Access token validity: 60 minutes

  • Refresh token used to obtain new access tokens

  • Scopes: pvms.openapi.basic , pvms.openapi.control

  • Required for ALL Control APIs (Section 5.2)

// Step 1: Build authorization URL (user opens in browser) const authUrl = https://oauth2.fusionsolar.huawei.com/rest/dp/uidm/oauth2/v1/authorize?response_type=code&#x26;client_id=${clientId}&#x26;redirect_uri=${encodeURIComponent(redirectUri)}&#x26;state=${state}&#x26;scope=pvms.openapi.basic%20pvms.openapi.control;

// Step 2: Exchange code for tokens (Content-Type: x-www-form-urlencoded!) const tokenRes = await axios.post( "https://oauth2.fusionsolar.huawei.com/rest/dp/uidm/oauth2/v1/token", new URLSearchParams({ grant_type: "authorization_code", code: authCode, client_id: clientId, client_secret: clientSecret, redirect_uri: redirectUri, }), { headers: { "Content-Type": "application/x-www-form-urlencoded" } } ); // Response: { access_token, refresh_token, expires_in: 3600, scope, token_type: "Bearer" }

// Step 3: Refresh when expired const refreshRes = await axios.post( "https://oauth2.fusionsolar.huawei.com/rest/dp/uidm/oauth2/v1/token", new URLSearchParams({ grant_type: "refresh_token", refresh_token: currentRefreshToken, client_id: clientId, client_secret: clientSecret, }), { headers: { "Content-Type": "application/x-www-form-urlencoded" } } );

// Usage: Add to every API request headers: { "Authorization": Bearer ${accessToken} }

Request Wrapper with Error Handling & Retry

class SmartPVMSError extends Error { constructor( public failCode: number, message: string, public params?: Record<string, unknown> ) { super(SmartPVMS Error ${failCode}: ${message}); this.name = "SmartPVMSError"; } get isRetryable(): boolean { return [407, 429, 20200, 20614].includes(this.failCode); } get isAuthError(): boolean { return [305, 20002, 20003].includes(this.failCode); } }

async request<T>(path: string, body: object, maxRetries = 3): Promise<T> { await this.ensureAuth(); for (let attempt = 0; attempt <= maxRetries; attempt++) { try { const headers: Record<string, string> = {}; if (this.xsrfToken) headers["XSRF-TOKEN"] = this.xsrfToken; if (this.config.accessToken) headers["Authorization"] = Bearer ${this.config.accessToken};

  const res = await this.http.post(path, body, { headers });
  const data = res.data;

  if (data.success === false || data.failCode !== 0) {
    const err = new SmartPVMSError(
      data.failCode ?? -1,
      data.message ?? "Unknown error",
      data.params
    );

    if (err.isAuthError) {
      await this.login();
      continue; // retry with new token
    }
    if (err.isRetryable &#x26;&#x26; attempt &#x3C; maxRetries) {
      const delay =
        err.failCode === 429
          ? 60_000 // system rate limit: wait 60s+
          : 1000 * Math.pow(2, attempt); // exponential backoff
      await Bun.sleep(delay);
      continue;
    }
    throw err;
  }
  // Extend token expiry on successful call
  this.tokenExpiresAt = Date.now() + 25 * 60 * 1000;
  return data.data as T;
} catch (err) {
  if (err instanceof SmartPVMSError) throw err;
  if (attempt &#x3C; maxRetries) {
    await Bun.sleep(1000 * Math.pow(2, attempt));
    continue;
  }
  throw err;
}

} throw new Error("Max retries exceeded"); }

CRITICAL Notes & Gotchas

All APIs are HTTPS POST with JSON body

  • Exception: OAuth authorization URL is GET (browser redirect)

  • Exception: OAuth token endpoint uses application/x-www-form-urlencoded

Timestamps are ALWAYS in milliseconds

const collectTime = Date.now(); // ms, not seconds!

Batch Size Limits

Resource Max per request

Plants (stationCodes) 100

Devices (sns/devIds) 100

Historical device data 1 device, 24 hours max

Control tasks (battery mode, params, power) 10 plants

Charge/discharge tasks 100 plants

Dispatch tasks 1 plant + 1 battery

Grid Meter Power Unit is WATTS, not kW!

The grid meter (devTypeId=17) and power sensor (devTypeId=47) return active_power in W (watts), unlike inverters which return in kW.

Report Data Time Logic

  • Hourly: Send any timestamp of the day → returns all hourly data for that day (up to 24 records)

  • Daily: Send any timestamp of the month → returns all daily data for that month (up to 31 records)

  • Monthly: Send any timestamp of the year → returns all monthly data for that year (up to 12 records)

  • Yearly: Send any timestamp → returns all yearly data available

Real-Time Data Collection Interval

  • Real-time plant/device data: refreshed every 5 minutes

  • Total revenue (total_income ): refreshed every 1 hour

inverter_power Ambiguity

The inverter_power key in report APIs is ambiguous. Use these instead:

  • PVYield — PV energy yield

  • inverterYield — Inverter energy yield

Plant ID Format

Plant IDs look like "NE=33554875" . Multiple IDs are comma-separated as a single string: "NE=33554875,NE=33554876" .

Flow Control (API Account Mode)

Read references/error-codes.md for detailed rate limit formulas. Key rules:

  • Real-time data APIs: ceil(count/100) calls per 5 minutes

  • Report/list APIs: ceil(count/100) + 24 calls per day

  • Historical data: ceil(devices/60/10) calls per second

  • Alarm API: ceil(count/100) calls per 30 minutes

Flow Control (OAuth Connect Mode)

  • Basic APIs: 1000 calls/day per owner

  • Control APIs: 100 calls/day per owner

Control APIs Are OAuth-Only

All Control APIs (battery charge/discharge, battery mode, battery params, inverter power, dispatch) require OAuth Connect mode. They are residential-scenario only.

Task Status Polling

After delivering a control task, query its status. Status values:

  • RUNNING — Task is still executing (updated every ~3 minutes)

  • SUCCESS — Task completed successfully

  • FAIL — Task failed (check message for: FAILURE , TIMEOUT , BUSY , INVALID , EXCEPTION )

  • Tasks timeout after 24 hours if not completed

Exception Responses (Non-standard)

Control APIs may return HTTP 400/500 with a different response format:

{ "exceptionId": "framwork.remote.Paramerror", "exceptionType": "ROA_EXFRAME_EXCEPTION", "descArgs": null, "reasonArgs": ["tasks"], "detailArgs": ["tasks size must be between 1 and 10"], "adviceArgs": null }

Always handle both { success, failCode, data } and exception format responses.

TypeScript Interface Templates

// Standard API response wrapper interface ApiResponse<T> { success: boolean; failCode: number; message: string | null; data: T; params?: Record<string, unknown>; }

// Plant from plant list interface Plant { plantCode: string; // "NE=33554875" plantName: string; plantAddress: string | null; longitude: number | null; latitude: number | null; capacity: number; // kWp contactPerson: string; contactMethod: string; gridConnectionDate: string; // ISO 8601 with timezone }

// Device from device list interface Device { id: number; devDn: string; // "NE=45112560" devName: string; stationCode: string; esnCode: string; // device SN devTypeId: number; model: string; softwareVersion: string; optimizerNumber: number; invType: string; // inverter model (inverters only) longitude: number | null; latitude: number | null; }

// Real-time data item interface DataItem { stationCode?: string; sn?: string; devDn?: string; collectTime?: number; dataItemMap: Record<string, number | string | null>; }

// Alarm interface Alarm { stationCode: string; stationName: string; alarmId: number; alarmName: string; alarmType: number; // 0:other, 1:transposition, 2:exception, 3:protection, 4:notification, 5:alarm_info alarmCause: string; causeId: number; repairSuggestion: string; devName: string; devTypeId: number; esnCode: string; lev: number; // 1:critical, 2:major, 3:minor, 4:warning status: number; // 1:active (not processed) raiseTime: number; // ms timestamp }

// Control task result interface TaskResult { plantCode: string; sn?: string; status: "RUNNING" | "SUCCESS" | "FAIL"; message: string | null; }

Code Generation Guidelines

  • Always use Bun.sleep() for delays (not setTimeout )

  • All timestamps in milliseconds (use Date.now() )

  • Add JSDoc comments explaining each API's constraints

  • Batch plant/device IDs respecting the 100 max limit

  • Include proper TypeScript types for all API responses

  • Handle both standard response format AND exception format

  • For polling tasks: check every 30-60s, respect the 3-minute update interval

  • Log rate limit errors (407/429) with remaining quota info from params

Reference Files Index

File Contents Lines

references/api-endpoints-query.md

All 14 query APIs: URLs, params, response fields, examples ~600

references/api-endpoints-control.md

All 10 control APIs: URLs, params, response fields, practices ~500

references/data-fields.md

Every data field per device type per API granularity ~800

references/error-codes.md

156 error codes + flow control rules + FAQs ~400

references/device-types.md

Device type IDs, inverter states, alarm types, extra devTypeIds ~200

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

Aoment Visuals

AI image and video generation service - supports text-to-image, image-to-image, and video generation

Registry SourceRecently Updated
General

Jira Task Creator

Create and manage Jira tasks using natural language input, smart user search, batch CSV import, and detailed task analytics with flexible field support.

Registry SourceRecently Updated
General

Cf Workers Logs

Query Cloudflare Workers Observability logs via API. Use when the user asks to check logs, debug Workers, look up errors, or investigate Worker/Durable Objec...

Registry SourceRecently Updated