Initial SolarEdge Optimizer Home Assistant App
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user