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

ll-feishu-audio

飞书语音交互技能。支持语音消息自动识别、AI 处理、语音回复全流程。需要配置 FEISHU_APP_ID 和 FEISHU_APP_SECRET 环境变量。使用 faster-whisper 进行语音识别,Edge TTS 进行语音合成,自动转换 OPUS 格式并通过飞书发送。适用于飞书平台的语音对话场景。

Archived SourceRecently Updated
General

test_skill

import json import tkinter as tk from tkinter import messagebox, simpledialog

Archived SourceRecently Updated
General

51mee-resume-profile

简历画像。触发场景:用户要求生成候选人画像;用户想了解候选人的多维度标签和能力评估。

Archived SourceRecently Updated
General

51mee-resume-parse

简历解析。触发场景:用户上传简历文件要求解析、提取结构化信息。

Archived SourceRecently Updated