173 lines
4.8 KiB
Markdown
173 lines
4.8 KiB
Markdown
# SolarEdge internal optimizer API
|
|
|
|
> Status: experimental and unsupported. This document describes calls observed
|
|
> in the SolarEdge Monitoring web portal on 2026-08-09. SolarEdge can change or
|
|
> remove them without notice.
|
|
|
|
## Why a second client is required
|
|
|
|
The official API at `https://monitoringapi.solaredge.com` does not expose
|
|
per-optimizer measurements. Its API key is rejected by the internal portal
|
|
endpoints with HTTP 401.
|
|
|
|
The Monitoring portal instead uses an OAuth2 authorization-code flow with PKCE:
|
|
|
|
1. Load the hosted SolarEdge/AWS Cognito login form.
|
|
2. Submit the configured portal username and password together with the CSRF
|
|
token and login cookies.
|
|
3. Exchange the returned authorization code for OAuth tokens.
|
|
4. Exchange those tokens at `/services/auth/token?legacy=false` for a Monitoring
|
|
portal session.
|
|
5. Send both the bearer access token and session cookies to internal services.
|
|
6. Refresh the session through `POST /services/auth/refresh` when necessary.
|
|
|
|
`SolarEdgePortalClient` performs this flow with HTTP requests and a memory-only
|
|
cookie jar. It does not start or depend on a browser. Tokens and cookies are not
|
|
persisted to disk.
|
|
|
|
## Configuration
|
|
|
|
Keep all values on the server:
|
|
|
|
```env
|
|
SOLAREDGE_SITE_ID=...
|
|
SOLAREDGE_USERNAME=owner@example.com
|
|
SOLAREDGE_PASSWORD=...
|
|
```
|
|
|
|
Do not use the `EXPO_PUBLIC_` prefix. Values with that prefix are embedded in
|
|
the client application and are not secret.
|
|
|
|
```ts
|
|
import { createSolarEdgePortalClientFromEnv } from "@solar-dash/solaredgeapi";
|
|
|
|
const portal = createSolarEdgePortalClientFromEnv();
|
|
await portal.authenticate();
|
|
```
|
|
|
|
## Observed endpoints
|
|
|
|
### Logical layout including optimizers
|
|
|
|
```http
|
|
GET /services/layout/logical/generic/v2/site/{siteId}?include-optimizers=true
|
|
```
|
|
|
|
```ts
|
|
const layout = await portal.getLogicalLayout();
|
|
const optimizerSerials = await portal.listOptimizerSerials();
|
|
const optimizerMappings = await portal.listOptimizerMappings();
|
|
```
|
|
|
|
The response contains the site's logical device tree and optimizer serials.
|
|
`listOptimizerSerials()` traverses that tree and returns only nodes whose type
|
|
is `OPTIMIZER`. The raw layout is deliberately typed as a generic object because
|
|
this undocumented structure is more likely to change.
|
|
|
|
`listOptimizerMappings()` preserves SolarEdge's logical position fields:
|
|
|
|
```ts
|
|
type SolarEdgeOptimizerMapping = {
|
|
serial: string;
|
|
inverterId?: string; // e.g. "1"
|
|
stringId?: string; // e.g. "1.1"
|
|
optimizerId?: string; // e.g. "1.1.16"
|
|
inverterOrder?: number;
|
|
stringOrder?: number;
|
|
optimizerOrder?: number;
|
|
};
|
|
```
|
|
|
|
The IDs come from the portal's `displayOrder` properties and are therefore
|
|
preferable to deriving positions from array indexes.
|
|
|
|
### Optimizer information
|
|
|
|
```http
|
|
POST /services/layout/information/optimizers
|
|
Content-Type: application/json
|
|
|
|
["OPTIMIZER_SERIAL_1", "OPTIMIZER_SERIAL_2"]
|
|
```
|
|
|
|
Observed response container:
|
|
|
|
```ts
|
|
type SolarEdgeOptimizerInformation = {
|
|
basicInformationList: Array<{
|
|
serial: string;
|
|
type_?: string;
|
|
model?: string;
|
|
bundleType?: string;
|
|
multiOptimizers?: boolean;
|
|
modules?: Array<{
|
|
orientation?: string;
|
|
tilt?: number;
|
|
azimuth?: number;
|
|
manufacturer?: string;
|
|
model?: string;
|
|
}>;
|
|
[property: string]: unknown;
|
|
}>;
|
|
serialToLiveData: Record<string, {
|
|
lastMeasurement?: string;
|
|
current_A?: number | null;
|
|
optimizerVoltage_V?: number | null;
|
|
power_W?: number | null;
|
|
voltage_V?: number | null;
|
|
total_last_telemetry_energy_WH?: number | null;
|
|
[property: string]: unknown;
|
|
}>;
|
|
};
|
|
```
|
|
|
|
```ts
|
|
const information = await portal.getOptimizerInformation([
|
|
"OPTIMIZER_SERIAL_1",
|
|
"OPTIMIZER_SERIAL_2",
|
|
]);
|
|
```
|
|
|
|
### Optimizer energy graph
|
|
|
|
```http
|
|
GET /services/layout/energy-graph/site/{siteId}/optimizers
|
|
?chart-time-unit=hours
|
|
&start-date=yyyy-MM-dd
|
|
&end-date=yyyy-MM-dd
|
|
&optimizer-serials=SERIAL_1,SERIAL_2
|
|
```
|
|
|
|
Observed response:
|
|
|
|
```ts
|
|
type SolarEdgeOptimizerEnergy = {
|
|
totalEnergy: number;
|
|
energyBars: Array<{
|
|
measurementTime: string;
|
|
energy: number | null;
|
|
}>;
|
|
};
|
|
```
|
|
|
|
```ts
|
|
const energy = await portal.getOptimizerEnergy({
|
|
startDate: "2026-08-09",
|
|
endDate: "2026-08-09",
|
|
optimizerSerials: ["OPTIMIZER_SERIAL_1"],
|
|
chartTimeUnit: "hours",
|
|
});
|
|
```
|
|
|
|
## Limitations and operational safety
|
|
|
|
- The implementation currently supports username/password login without an
|
|
additional MFA challenge. It throws `AUTH_CHALLENGE_REQUIRED` if SolarEdge
|
|
asks for an OTP, authenticator, passkey, or another step.
|
|
- Credentials remain in process memory because they are needed for a fresh
|
|
login after session expiry. Do not log client options or error request bodies.
|
|
- Use conservative polling and caching. These endpoints have no published rate
|
|
limits or stability guarantees.
|
|
- Use this only for an account and site you are authorized to access. Review the
|
|
SolarEdge Monitoring Portal terms before production use.
|