Skip to content

gnacho/soliscloud-scraper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SolisCloud Internal API Scraper

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.

Leer en castellano

The Problem

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-soc DOM element)

The Solution

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

Tested Setup

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.

How the Authentication Works

Overview

Every request to the SolisCloud internal API requires:

  1. A session cookie (token=token_<uuid>) obtained from the login endpoint
  2. An HMAC-SHA1 signature in the Authorization header
  3. A Content-MD5 header with the Base64-encoded MD5 hash of the request body

Step 1: Login

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.

Step 2: Sign Each Request

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 /api prefix stripped. Example: for POST /api/station/list, sign with /station/list

Critical: The signing path must NOT include /api. Some endpoints (like login2 and distributor/detail) are tolerant and accept signatures with or without the prefix. But station/list and station/detailMix return 403 Forbidden - wrong sign if you include /api in the signature.

Required Headers

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>

How the HMAC Secret Was Extracted

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 Base64

The key ID 2424 is a fixed constant embedded in the same function.

Verification

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);

API Endpoints Reference

/api/user/login2 (POST)

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"
}

/api/distributor/detail (POST)

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

/api/station/list (POST)

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

/api/station/detailMix (POST)

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

Other Endpoints (not used by scraper, but discovered)

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

Installation

Requirements

  • Node.js 18+ (tested with Node.js 22)
  • A SolisCloud account that can log in to https://www.soliscloud.com

Steps

git clone https://github.com/your-user/soliscloud-scraper.git
cd soliscloud-scraper
npm install
cp scraper.properties.example scraper.properties

Edit 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 = localpass

Run:

node scraper.js

You 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.tool

Running as a systemd Service

Create /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.target

Enable and start:

systemctl enable solis-scraper
systemctl start solis-scraper
journalctl -u solis-scraper -f

Running with Docker

FROM 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-scraper

Home Assistant Integration

REST Sensor

Add 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: 300

Template Sensors

template:
  - 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) }}"

Local API Endpoints

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

Performance Comparison

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

About the Official API

The official SolisCloud API exists and is documented (PDF), but requires manual activation by Solis support. To request access:

  1. Go to https://solis-service.solisinverters.com/en/support/tickets/new
  2. Select "API Access Request"
  3. Provide your SolisCloud email address
  4. 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 enabled
  • apiShowSwitch: 0/1 - Whether the API management menu is visible
  • userFunctions must include A0507

This scraper does not need the official API to be activated. It uses the same internal API the web frontend uses.

Disclaimer

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.

License

MIT

About

SolisCloud internal API scraper - no browser needed

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors