A lightweight Node.js service that fetches real-time solar inverter and battery data from SolisCloud by calling the same internal API the web frontend uses. No browser, no Puppeteer, no Chromium, no official API activation required.
SolisCloud has an official monitoring API, but accessing it requires submitting a support ticket and waiting for Solis to manually activate it (setting apiSwitch and apiShowSwitch flags on your account). Many users wait weeks or months stuck on "Nonactivated".
Previous scraping approaches relied on Puppeteer + headless Chromium to navigate the web UI and extract data from the DOM. This:
- Consumed ~600 MB RAM (Chromium process)
- Took ~22 seconds per scrape cycle
- Generated ~9,000 HTTP requests per day (browser loading JS, CSS, images, fonts, Google Maps, Firebase, etc.)
- Broke every time Solis updated their frontend (July 2026: Vue 3 rewrite, new URL structure, removed
p.battery-socDOM element)
This scraper calls the SolisCloud internal API endpoints directly via HTTP, using the same HMAC-SHA1 authentication the web frontend uses. The auth mechanism was reverse-engineered from the frontend JavaScript.
- ~43 MB RAM (Node.js only)
- ~1 second per scrape cycle
- ~960 HTTP requests/day (4 API calls per cycle, every 6 minutes)
- No browser dependency - runs on any system with Node.js
- Includes battery data (SoC, power, voltage, direction) that the old DOM scraper couldn't reliably get
This was developed and tested with:
- Inverter: Solis S5-EH1P6K-L (also known as RHI-6K-48ES-5G), hybrid single-phase with battery
- Battery: Soluna 5.12 kWh (SN: 1031052248080003BAT01)
- WiFi Logger: Solis S2-WL-ST (firmware
00010154) - Station capacity: 4.4 kWp
- SolisCloud account type: End user (not installer)
It should work with any SolisCloud account that can log in to the web portal, regardless of inverter model or whether the official API is activated.
Every request to the SolisCloud internal API requires:
- A session cookie (
token=token_<uuid>) obtained from the login endpoint - An HMAC-SHA1 signature in the
Authorizationheader - A Content-MD5 header with the Base64-encoded MD5 hash of the request body
POST https://www.soliscloud.com/api/user/login2
The password is sent as its MD5 hex digest:
{
"userInfo": "your@email.com",
"passWord": "5dcf4da5467c8a61500283572408417a",
"yingZhenType": 1,
"localTime": 1783848004976,
"localTimeZone": 2,
"language": "2"
}The response sets a token=token_<uuid> cookie and an acw_tc cookie. Both must be sent with subsequent requests.
The Authorization header has the format:
Authorization: WEB 2424:<base64-encoded HMAC-SHA1 signature>
The HMAC-SHA1 is computed over this string:
POST\n<Content-MD5>\napplication/json\n<Date-UTC>\n<path-without-api-prefix>
With these components:
- HMAC key:
5704383536604a8bb94c83ebc059aa8c(hardcoded in the frontend JS) - Content-MD5:
Base64(MD5(request_body_string)) - Date-UTC: HTTP date format, e.g.
Sun, 12 Jul 2026 09:20:04 GMT - Path: The API path with the
/apiprefix stripped. Example: forPOST /api/station/list, sign with/station/list
Critical: The signing path must NOT include
/api. Some endpoints (likelogin2anddistributor/detail) are tolerant and accept signatures with or without the prefix. Butstation/listandstation/detailMixreturn403 Forbidden - wrong signif you include/apiin the signature.
| Header | Value |
|---|---|
Content-Type |
application/json;charset=UTF-8 |
Content-MD5 |
Base64-encoded MD5 of the request body |
Authorization |
WEB 2424:<HMAC-SHA1 signature> |
Time |
UTC date string |
Language |
2 (English) |
X-Cloud-Platform |
GLY |
Version |
5.3.001 |
Platform |
Web |
Cookie |
token=token_<uuid>; acw_tc=<value> |
The secret is embedded in the SolisCloud frontend JavaScript bundle (index-CqvhMJaa.js as of July 2026). It's stored as an array of three Base64-encoded strings that are concatenated at runtime:
// From the minified frontend JS:
const mK = ["NTcwNDM4MzUzNjYw", "NGE4YmI5NGM4M2Vi", "YzA1OWFhOGM="];
function hK() {
try {
return mK.map(e => atob(e)).join("");
} catch {
return "";
}
}Decoding each part:
atob("NTcwNDM4MzUzNjYw") = "5704383536604"
atob("NGE4YmI5NGM4M2Vi") = "4a8bb94c83eb"
atob("YzA1OWFhOGM=") = "c059aa8c"
Concatenated: 5704383536604a8bb94c83ebc059aa8c
The signing function in the frontend:
function yK({contentMd5Result: e, date: t, url: n, method: r}) {
const a = vK(); // returns the secret above
return "WEB 2424:" + EK(a, r.toUpperCase() + "\n" + e + "\napplication/json\n" + t + "\n" + n);
}
// EK() computes HMAC-SHA1 and returns Base64The key ID 2424 is a fixed constant embedded in the same function.
To verify the signing works, you can compute it manually:
const crypto = require("crypto");
const secret = "5704383536604a8bb94c83ebc059aa8c";
const body = JSON.stringify({localTime: Date.now(), localTimeZone: 2, language: "2"});
const contentMd5 = crypto.createHash("md5").update(body, "utf8").digest("base64");
const date = new Date().toUTCString();
const path = "/distributor/detail"; // WITHOUT /api
const signString = "POST\n" + contentMd5 + "\napplication/json\n" + date + "\n" + path;
const signature = crypto.createHmac("sha1", secret).update(signString, "utf8").digest("base64");
console.log("Authorization: WEB 2424:" + signature);Authenticates the user. Returns user profile and sets session cookies.
Request body:
{
"userInfo": "your@email.com",
"passWord": "<MD5 hex of password>",
"yingZhenType": 1,
"localTime": 1783848004976,
"localTimeZone": 2,
"language": "2"
}Returns aggregated account-level data.
Request body:
{
"localTime": 1783848005368,
"localTimeZone": 2,
"language": "2"
}Key response fields:
| Field | Description | Example |
|---|---|---|
power |
Current generation | 1.516 |
powerStr |
Unit | "kW" |
energyToday |
Energy generated today | 3.4 |
monthEnergy |
Energy this month | 243.3 |
yearEnergy |
Energy this year | 2.892 |
energyTotal |
Total energy all-time | 5.909 |
batteryChargeTodayEnergy |
Battery charged today | 2 |
batteryDischargeTodayEnergy |
Battery discharged today | 1 |
gridPurchasedTodayEnergy |
Grid import today | 6.27 |
gridSellTodayEnergy |
Grid export today | 0.11 |
homeLoadTodayEnergy |
Home consumption today | 7.46 |
incomeToday / incomeMonth / incomeTotal |
Income stats | 0.51 / 36.51 / 886.35 |
capacity |
Station capacity | 4.4 |
Returns the list of stations (plants). Used to discover the station ID needed for detailMix.
Request body:
{
"pageNo": 1,
"pageSize": 10,
"states": "0",
"stationType": "1",
"selectAreaValue": [],
"dataNumber": 0,
"localTime": 1783848005449,
"localTimeZone": 2,
"language": "2"
}Key response fields (in data.page.records[0]):
| Field | Description | Example |
|---|---|---|
id |
Station ID (needed for detailMix) | "1298491919450219817" |
stationName |
Station name | "Mandor 40" |
state |
1 = normal | 1 |
power |
Current power | 0.798 |
dayEnergy |
Today's energy | 3.4 |
dataTimestampStr |
Last data timestamp | "12/07/2026 11:04:31 (UTC+01:00)" |
inverterCount / inverterOnlineCount |
Inverter status | 1 / 1 |
Returns detailed real-time data for a specific station. This is the most valuable endpoint as it includes battery data not available elsewhere.
Request body:
{
"id": "<station_id from station/list>",
"localTime": 1783847743820,
"localTimeZone": 2,
"language": "2"
}Key response fields:
| Field | Description | Example |
|---|---|---|
batteryPercent |
Battery State of Charge (%) | 30 |
batteryPower |
Battery power in kW (negative = discharging) | -1.509 |
batteryPowerStr |
Unit | "kW" |
batteryDirection |
1 = charging, 2 = discharging | 1 |
storageBatteryVoltage |
Battery voltage | 51.5 |
inverterBatteryCapacity |
Total battery capacity | 5.12 |
inverterBatteryCapacityStr |
Unit | "kWh" |
batteryCount |
Number of batteries | 1 |
batteryChargeEnergy |
Today's charge energy | 2 |
batteryDischargeEnergy |
Today's discharge energy | 0 |
familyLoadPower |
Current house consumption | (available) |
gridPower |
Current grid power | (available) |
gridDirection |
Grid flow direction | (available) |
power |
Current solar power | 0.798 |
dayEnergyStorage |
Today's total yield | 3.4 |
| Endpoint | Description |
|---|---|
/api/user/find |
User profile (includes apiSwitch, apiShowSwitch flags) |
/api/chart/station/day/v2 |
Daily charts with batteryCapacitySocList (SoC history) |
/api/seng/chart |
Detailed time-series: realSoc, batteryPower, gridPower, familyLoadPower |
/api/seng/incomeChart |
Income charts |
/api/inverter/listV2 |
Inverter list |
/api/station/getDetailSample |
Station sample data |
/api/alarm/alarmReadAll |
Alarm status |
/api/weather/get7DayForecast |
Weather forecast for the station location |
/api/station/stationAllEnergy |
All energy statistics |
- Node.js 18+ (tested with Node.js 22)
- A SolisCloud account that can log in to
https://www.soliscloud.com
git clone https://github.com/your-user/soliscloud-scraper.git
cd soliscloud-scraper
npm install
cp scraper.properties.example scraper.propertiesEdit scraper.properties with your credentials:
# Your SolisCloud login credentials
solis.url = https://www.soliscloud.com
solis.username = your@email.com
solis.password = your_password
# Local API server config
service.port = 5561
service.username = localuser
service.password = localpassRun:
node scraper.jsYou should see:
Solis Cloud API scraper (no browser) listening on port 5561
Login OK - user: your@email.com
Discovered station ID: 1234567890123456789
Scrape OK - distributor: yes station: yes detailMix: yes batterySoc: 85 duration: 943ms
Test:
curl -u localuser:localpass http://localhost:5561/v2/data | python3 -m json.toolCreate /etc/systemd/system/solis-scraper.service:
[Unit]
Description=SolisCloud Scraper
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/path/to/soliscloud-scraper
ExecStart=/usr/bin/node scraper.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.targetEnable and start:
systemctl enable solis-scraper
systemctl start solis-scraper
journalctl -u solis-scraper -fFROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production
COPY scraper.js scraper.properties ./
EXPOSE 5561
CMD ["node", "scraper.js"]docker build -t soliscloud-scraper .
docker run -d --name solis-scraper -p 5561:5561 soliscloud-scraperAdd to your configuration.yaml:
sensor:
- platform: rest
name: Solis Scraper
unique_id: solis_scraper
resource: http://<scraper-host>:5561/v2/data
method: GET
username: localuser
password: localpass
authentication: basic
value_template: "{{ value_json.stationName }}"
json_attributes:
- batterySoc
- currentYield
- currentYieldUnit
- currentBatteryPower
- currentBatteryPowerUnit
- batteryDirection
- batteryVoltage
- batteryCapacity
- batteryCapacityUnit
- currentSolarPower
- currentSolarPowerUnit
- todayYield
- todayYieldUnit
- todaysCharging
- todaysChargingUnit
- todaysDischarging
- todaysDischargingUnit
- todayGridImport
- todayGridImportUnit
- todayGridExport
- todayGridExportUnit
- todayHouseConsumption
- todayHouseConsumptionUnit
- stationCapacity
- stationCapacityUnit
- batteryChargeTodayEnergy
- batteryDischargeTodayEnergy
- gridPurchasedTodayEnergy
- gridSellTodayEnergy
- homeLoadTodayEnergy
- monthEnergy
- monthEnergyStr
- yearEnergy
- yearEnergyStr
- totalEnergy
- totalEnergyStr
- incomeToday
- incomeTodayStr
- incomeMonth
- incomeMonthStr
- incomeTotal
- incomeTotalStr
- batteryChargeTotal
- batteryChargeTotalUnit
- batteryDischargeTotal
- batteryDischargeTotalUnit
- solarTotalEnergy
- solarTotalEnergyUnit
- inverterOnline
- inverterCount
- dataTimestamp
- error
- lastUpdate
scan_interval: 300template:
- sensor:
- name: "Solis Battery SoC"
unique_id: solis_battery_soc
unit_of_measurement: "%"
device_class: battery
state_class: measurement
state: "{{ state_attr('sensor.solis_scraper', 'batterySoc') | float(0) }}"
- name: "Solis Solar Power"
unique_id: solis_solar_power
unit_of_measurement: "kW"
device_class: power
state_class: measurement
state: "{{ state_attr('sensor.solis_scraper', 'currentYield') | float(0) }}"
- name: "Solis Energy Today"
unique_id: solis_energy_today
unit_of_measurement: "kWh"
device_class: energy
state_class: total
state: "{{ state_attr('sensor.solis_scraper', 'todayYield') | float(0) }}"
attributes:
last_reset: "{{ now().replace(hour=0, minute=0, second=0, microsecond=0).astimezone() }}"
- name: "Solis Grid Import Today"
unique_id: solis_grid_import_today
unit_of_measurement: "kWh"
device_class: energy
state_class: total
state: "{{ state_attr('sensor.solis_scraper', 'todayGridImport') | float(0) }}"
attributes:
last_reset: "{{ now().replace(hour=0, minute=0, second=0, microsecond=0).astimezone() }}"
- name: "Solis Grid Export Today"
unique_id: solis_grid_export_today
unit_of_measurement: "kWh"
device_class: energy
state_class: total
state: "{{ state_attr('sensor.solis_scraper', 'todayGridExport') | float(0) }}"
attributes:
last_reset: "{{ now().replace(hour=0, minute=0, second=0, microsecond=0).astimezone() }}"
- name: "Solis Battery Power"
unique_id: solis_battery_power
unit_of_measurement: "kW"
device_class: power
state_class: measurement
state: "{{ state_attr('sensor.solis_scraper', 'currentBatteryPower') | float(0) }}"All endpoints require Basic Auth.
| Endpoint | Description |
|---|---|
GET /v2/data |
Normalized data with all fields (recommended) |
GET /data |
Raw captured data (distributor + station + detailMix objects) |
GET /v1/data |
Same as /data |
GET /refresh |
Trigger an immediate refresh cycle |
| Puppeteer + Chromium | This (API Direct) | |
|---|---|---|
| RAM | ~600 MB | ~43 MB |
| Time per cycle | ~22 seconds | ~1 second |
| Requests/day | ~9,000 | ~960 |
| Dependencies | Node.js + Chromium | Node.js only |
| Battery SoC | DOM scraping (fragile) | API field batteryPercent |
| Battery power/voltage | Not available | Available |
| Portability | Needs Chromium | Runs anywhere |
The official SolisCloud API exists and is documented (PDF), but requires manual activation by Solis support. To request access:
- Go to https://solis-service.solisinverters.com/en/support/tickets/new
- Select "API Access Request"
- Provide your SolisCloud email address
- Wait for Solis to process the request
Once activated, the API management page appears at https://www.soliscloud.com/service/apiManagement where you can generate API keys.
The user profile flags that control this:
apiSwitch: 0/1- Whether the API is enabledapiShowSwitch: 0/1- Whether the API management menu is visibleuserFunctionsmust includeA0507
This scraper does not need the official API to be activated. It uses the same internal API the web frontend uses.
This project reverse-engineers the internal API of SolisCloud for personal, non-commercial monitoring use. The API endpoints, authentication mechanism, and HMAC secret are extracted from the publicly served frontend JavaScript and may change without notice. This is not affiliated with Ginlong/Solis in any way. Use at your own risk.
MIT