MickMake / GoSungrow

GoLang implementation to access the iSolarCloud API updated by SunGrow inverters.
https://mickmake.com/
GNU General Public License v2.0
148 stars 42 forks source link

Error 'er_invalid_appkey' / 'Request is not encrypted' #101

Open rob3r7 opened 7 months ago

rob3r7 commented 7 months ago

Since tonight I get with https://gateway.isolarcloud.eu in HA the following error:

ERROR: appkey is incorrect 'er_invalid_appkey

Checking with https://portaleu.isolarcloud.com/ I realized that the appkey could have changed to B0455FBE7AA0328DB57B59AA729F05D8 (at least I find this key when searching for the term appkey) .

When doing a direct request at /v1/userService/login at least I don't get any more an invalid_appkey error but now an Request is not encrypted error.

When looking at the source of https://portaleu.isolarcloud.com/#/dashboard there is the following function:

e.data.set("appkey", a.a.encryptHex(e.data.get("appkey"), h))

Did Sungrow changed the API access? How to deal with this change?

BTDrink commented 7 months ago

It seems that the entire payload is encrypted:

e.data = a.a.encryptHex(JSON.stringify(e.data), h)) with a new encryption key per request ex.: "webDK6Xl15mzc2RW"

I think they are using the standard CryptoJS CryptoJS.AES.encrypt("Message", "Secret Passphrase").ciphertext

as seen in their source: encryptHex: function(e, t) { return i(e, t).ciphertext.toString() }

jarg commented 7 months ago

Hi, same problem.

[09:36:41] INFO: Login to iSolarCloud using gateway https://gateway.isolarcloud.eu ... Error: appkey is incorrect 'er_invalid_appkey' Usage: GoSungrow api login [flags]

The key not change: 93D72E60331ABDCDC7B39ADC2D1F32B3

| --appkey          |            | GOSUNGROW_APPKEY          | SunGrow: api application key.  | 93D72E60331ABDCDC7B39ADC2D1F32B3 |
|                   |            |                           |                                | *                                |
| --host            |            | GOSUNGROW_HOST            | SunGrow: Provider API URL.     | https://gateway.isolarcloud.eu  

Regards

Bartleby1980 commented 7 months ago

I have the same Problem.

[13:45:06] INFO: Login to iSolarCloud using gateway https://gateway.isolarcloud.eu ... Error: appkey is incorrect 'er_invalid_appkey'

| --appkey | | GOSUNGROW_APPKEY | SunGrow: api application key. | 93D72E60331ABDCDC7B39ADC2D1F32B3

ERROR: appkey is incorrect 'er_invalid_appkey' s6-rc: info: service legacy-services: stopping s6-rc: info: service legacy-services successfully stopped s6-rc: info: service legacy-cont-init: stopping s6-rc: info: service legacy-cont-init successfully stopped s6-rc: info: service fix-attrs: stopping s6-rc: info: service fix-attrs successfully stopped s6-rc: info: service s6rc-oneshot-runner: stopping s6-rc: info: service s6rc-oneshot-runner successfully stopped

jangoetz commented 7 months ago

Hallo, same problem, with the same error messages...

monojk commented 7 months ago

Same problem

julianjwong commented 7 months ago

Same problem with the Australian server as well. I did upgrade my firmware and noticed I got a Session Expired error in the app which forced me to log in again

Techfluent-au commented 7 months ago

Same here, issue appears to start last week

https://augateway.isolarcloud.eu/

[3.0.7] - 2023-09-04

Tried

init: false Init: true hassio_role: default host_pid: true

Crisse771 commented 7 months ago

Hi,

I have the same Problem.

[20:55:36] INFO: Login to iSolarCloud using gateway https://gateway.isolarcloud.eu ... Error: appkey is incorrect 'er_invalid_appkey' Usage: GoSungrow api login [flags]

Examples: GoSungrow api login

Flags: Use "GoSungrow help flags" for more info.

Additional help topics:

ERROR: appkey is incorrect 'er_invalid_appkey' s6-rc: info: service legacy-services: stopping s6-rc: info: service legacy-services successfully stopped s6-rc: info: service legacy-cont-init: stopping s6-rc: info: service legacy-cont-init successfully stopped s6-rc: info: service fix-attrs: stopping s6-rc: info: service fix-attrs successfully stopped s6-rc: info: service s6rc-oneshot-runner: stopping s6-rc: info: service s6rc-oneshot-runner successfully stopped

It also says: | --token-expiry | | GOSUNGROW_TOKEN_EXPIRY | SunGrow: last login. | 2023-11-19T16:14:03

arekmic commented 7 months ago

same problem, the same error

rob3r7 commented 7 months ago

As @BTDrink mentioned the payload is encrypted. The session_key (ie. "webDK6Xl15mzc2RW") is generated on client side. It is then encrypted with public key (rsaEncryption (PKCS 1)) and send in the header:

var _ = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNA ...."

var h = e.randomKey;
return c.a.isUndefinedOrNull(h) || (e.headers["x-random-secret-key"] = v.a.sgEncrypt(h, _),
roabyd commented 7 months ago

Same issue, following for a fix

threepoints85 commented 7 months ago

Same Problem here

Razor094 commented 7 months ago

Same problme here - appkex 93D72E60331ABDCDC7B39ADC2D1F32B3 is used

[07:02:06] INFO: Login to iSolarCloud using gateway https://gateway.isolarcloud.eu ... Error: appkey is incorrect 'er_invalid_appkey' Usage: GoSungrow api login [flags]

Examples: GoSungrow api login

kilador commented 7 months ago

same here

lupus78 commented 7 months ago

Same here. I digged into the web interface's source code, and found they use an other key, which is called WebAppKey in the current repo here: https://github.com/MickMake/GoSungrow/blob/391253aaadd2cae9df32c95bec9e6b9bbf83f4d6/iSolarCloud/WebAppService/getMqttConfigInfoByAppkey/data.go#L17

If I use this appkey "B0455FBE7AA0328DB57B59AA729F05D8" than I get an other error... about encryption. They probably changed the encryption method.

./GoSungrow api login
Error: unknown error 'Request is not encrypted'
Usage:
  GoSungrow api login [flags]

Examples:
    GoSungrow api login  

Flags: Use "GoSungrow help flags" for more info.

Additional help topics:

ERROR: unknown error 'Request is not encrypted'
metawops commented 7 months ago

same problem here. Hoping for a fix ... 😳 @MickMake

rapptor7 commented 7 months ago

same problem, waiting for the correction.. thx

0SkillAllLuck commented 7 months ago

Body is encrypted using the randomKey and standard AES in ECB mode with PKCS7 padding. Decryption is done using the same key and parameters.

The x-random-secret-key is the randomKey encrypted using the secretKey (app and web seem to have different keys) with RSA and pkcs1. The secretKey seems to be related to x-access-key as changing one will break the encryption.

The randomKey is random generated by using a prefix (web or and) based on if you are using web or the android app. Guessing that ios would be ios but can't verify that atm.

Hope that helps bringing the project back to working.

Got the functions done in NodeJS but not in Go at this time:

import * as CryptoJS from "crypto-js";
import NodeRSA from "node-rsa";

export function randomKey() {
  return "and" + randomString(13);
}

export function encryptAES<T>(data: T, key: string): string {
  const d = CryptoJS.enc.Utf8.parse(JSON.stringify(data));
  const k = CryptoJS.enc.Utf8.parse(key);
  return CryptoJS.AES.encrypt(d, k, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
  })
    .ciphertext.toString()
    .toUpperCase();
}

export function decryptAES<T>(data: string, key: string): T {
  const d = CryptoJS.format.Hex.parse(data);
  const k = CryptoJS.enc.Utf8.parse(key);
  const dec = CryptoJS.AES.decrypt(d, k, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7,
  });
  return JSON.parse(CryptoJS.enc.Utf8.stringify(dec)) as T;
}

export function encryptRSA(publicKey: string, value: string): string {
  const key = new NodeRSA();
  key.setOptions({ encryptionScheme: "pkcs1" });
  key.importKey(publicKey, "pkcs8-public-pem");
  return key.encrypt(value, "base64");
}
0SkillAllLuck commented 7 months ago

The key values for the app are:

const ACCESS_KEY = "kme8xdq4fp88wps563qd5d57vw6jxrf4"
const APP_KEY = "3A51762ED80A39AD3DF3DB3CE6767884"
const SECRET_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlcwFJfpjOsy5U6KBpDEC9ZU_sgjD4AQ_Io0MuuGmQq8wdeLoozOdXRlkyZ2GovikSa6IXMkJ25NeChWGwBDTsnXuvZ3JIFqiTNt5eMtb42u2iHumWtv7fsjj17FFknOIIVzUMPBJ3eIb2"
0SkillAllLuck commented 7 months ago

Additionally there is the x-limit-obj header that needs to be send on all non-login requests. It is constructed by encrypting the user_id with the secretKey using the same RSA method used for the x-random-secret-key header

triamazikamno commented 7 months ago

It looks like there was an app key that allowed non-encrypted requests, but it's gone now. Some kind of integration got discontinued on their end perhaps? This is some terrible practice from Sungrow... I guess they want to charge money for integration with their api

nielstiben commented 7 months ago

Keys found for the web interface:

X_ACCESS_KEY = "9grzgbmxdsp3arfmmgq347xjbza4ysps"
SECRET_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkecphb6vgsBx4LJknKKes-eyj7-RKQ3fikF5B67EObZ3t4moFZyMGuuJPiadYdaxvRqtxyblIlVM7omAasROtKRhtgKwwRxo2a6878qBhTgUVlsqugpI_7ZC9RmO2Rpmr8WzDeAapGANfHN5bVr7G7GYGwIrjvyxMrAVit_oM4wIDAQAB'
APP_KEY = 'B0455FBE7AA0328DB57B59AA729F05D8'
loucksg commented 7 months ago

is there an easy patch path here? Changing definitely sees the change in error message, happy to test, as I can replicate this, just crawling through their .js tome

jhillau commented 7 months ago

Same issue. Following for fix. Please!

Thibaut1976 commented 7 months ago

Same error here, pls explain way around

sikelo83 commented 7 months ago

Hi, Same issue It would be very generous to fix this issue. Sorry my Go knowhow is too bad to do it on my own. Thank you

stefanknegt commented 7 months ago

Same issue here

rob3r7 commented 7 months ago

Here we go. A first minimal MVP. The api_key_param is updated with each request before encryption.

import json
import random
import string
from base64 import b64decode, b64encode, urlsafe_b64decode
from datetime import datetime

import requests
from Crypto.Cipher import AES, PKCS1_v1_5
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad

def encrypt_hex(data_str, key):
    cipher = AES.new(key.encode("UTF-8"), AES.MODE_ECB)
    date_byte = cipher.encrypt(pad(data_str.encode("UTF-8"), 16))
    return date_byte.hex()

def decrypt_hex(data_hex_str, key):
    cipher = AES.new(key.encode("UTF-8"), AES.MODE_ECB)
    text = unpad(cipher.decrypt(bytes.fromhex(data_hex_str)), 16).decode("UTF-8")
    return json.loads(text)

public_key = RSA.import_key(
    urlsafe_b64decode(
        "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkecphb6vgsBx4LJknKKes-eyj7-RKQ3fikF5B67EObZ3t4moFZyMGuuJPiadYdaxvRqtxyblIlVM7omAasROtKRhtgKwwRxo2a6878qBhTgUVlsqugpI_7ZC9RmO2Rpmr8WzDeAapGANfHN5bVr7G7GYGwIrjvyxMrAVit_oM4wIDAQAB"
    )
)
cipher = PKCS1_v1_5.new(public_key)

def encrypt_RSA(data_str):
    ciphertext = cipher.encrypt(data_str.encode("UTF-8"))
    return b64encode(ciphertext).decode("UTF-8")

def random_word(length):
    characters = string.ascii_lowercase + string.ascii_uppercase + string.digits
    random_word = "".join(random.choice(characters) for _ in range(length))
    return random_word

def get_data(url, data):
    random_key = "web" + random_word(13)
    data["api_key_param"] = {
        "timestamp": int(datetime.now().timestamp() * 1000),
        "nonce": random_word(32),
    }
    data["appkey"] = "B0455FBE7AA0328DB57B59AA729F05D8"
    data_str = json.dumps(data, separators=(",", ":"))
    data_hex = encrypt_hex(data_str, random_key)
    headers = {
        "content-type": "application/json;charset=UTF-8",
        "sys_code": "200",
        "x-access-key": "9grzgbmxdsp3arfmmgq347xjbza4ysps",
    }
    headers["x-random-secret-key"] = encrypt_RSA(random_key)

    response = requests.post(url, data=data_hex, headers=headers)
    return decrypt_hex(response.text, random_key)

token = get_data(
    "https://gateway.isolarcloud.eu/v1/userService/login",
    {
        "user_account": "XXXXX@XXXXX.ch",
        "user_password": "XXXXXXXX",
    },
)["result_data"]["token"]

get_data(
    "https://gateway.isolarcloud.eu/v1/commonService/queryMutiPointDataList",
    {
        "ps_key": "XXXXXXX_14_1_2",
        "points": "p13003",
        "start_time_stamp": "20231108000000",
        "end_time_stamp": "20231109000000",
        "token": token,
    },
)

@0SkillAllLuck: Grüsse nach Bern!

Pistro commented 7 months ago

Here is another working Python example of how to use query the new Sungrow API. Sadly, I forgot to refresh the page while working and didn't see the post of @rob3r7 ...

The scope of the code below is very similar to his contribution, with some extra additions:

You will see that the post method has a isFormData flag. The code associated with that flag being true is reverse engineered based on what I was able to make out of minified Sungrow encryption code. I do not know a call for which the flag should be set to true, so in that particular case there may be some issues...

import base64
import string
import random
from typing import Optional
import time
import json

import requests
from cryptography.hazmat.primitives import serialization, asymmetric, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# TODO: Getting these values directly from the files by the Sungrow API is better than hardcoding them...
LOGIN_RSA_PUBLIC_KEY: asymmetric.rsa.RSAPublicKey = serialization.load_pem_public_key(b"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJRGV7eyd9peLPOIqFg3oionWqpmrjVik2wyJzWqv8it3yAvo/o4OR40ybrZPHq526k6ngvqHOCNJvhrN7wXNUEIT+PXyLuwfWP04I4EDBS3Bn3LcTMAnGVoIka0f5O6lo3I0YtPWwnyhcQhrHWuTietGC0CNwueI11Juq8NV2nwIDAQAB\n-----END PUBLIC KEY-----")
APP_RSA_PUBLIC_KEY: asymmetric.rsa.RSAPublicKey   = serialization.load_pem_public_key(bytes("-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkecphb6vgsBx4LJknKKes-eyj7-RKQ3fikF5B67EObZ3t4moFZyMGuuJPiadYdaxvRqtxyblIlVM7omAasROtKRhtgKwwRxo2a6878qBhTgUVlsqugpI_7ZC9RmO2Rpmr8WzDeAapGANfHN5bVr7G7GYGwIrjvyxMrAVit_oM4wIDAQAB".replace("-", "+").replace("_", "/") + "\n-----END PUBLIC KEY-----",  'utf8'))
ACCESS_KEY = "9grzgbmxdsp3arfmmgq347xjbza4ysps"
APP_KEY = "B0455FBE7AA0328DB57B59AA729F05D8"

def encrypt_rsa(value: str, key: asymmetric.rsa.RSAPublicKey) -> str:
    # Encrypt the value
    encrypted = key.encrypt(
        value.encode(),
        asymmetric.padding.PKCS1v15(),
    )
    return base64.b64encode(encrypted).decode()

def encrypt_aes(data: str, key: str):
    key_bytes = key.encode('utf-8')
    data_bytes = data.encode('utf-8')

    # Ensure the key is 16 bytes (128 bits)
    if len(key_bytes) != 16:
        raise ValueError("Key must be 16 characters long")

    cipher = Cipher(algorithms.AES(key_bytes), modes.ECB(), backend=default_backend())
    encryptor = cipher.encryptor()
    padder = padding.PKCS7(algorithms.AES.block_size).padder()
    padded_data = padder.update(data_bytes) + padder.finalize()
    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
    return encrypted_data.hex()

def decrypt_aes(data: str, key: str):
    key_bytes = key.encode('utf-8')

    # Ensure the key is 16 bytes (128 bits)
    if len(key_bytes) != 16:
        raise ValueError("Key must be 16 characters long")

    encrypted_data = bytes.fromhex(data)
    cipher = Cipher(algorithms.AES(key_bytes), modes.ECB(), backend=default_backend())
    decryptor = cipher.decryptor()
    decrypted_padded_data = decryptor.update(encrypted_data) + decryptor.finalize()
    unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
    decrypted_data = unpadder.update(decrypted_padded_data) + unpadder.finalize()
    return decrypted_data.decode('utf-8')

def generate_random_word(length: int):
    char_pool = string.ascii_letters + string.digits
    random_word = ''.join(random.choice(char_pool) for _ in range(length))
    return random_word

class SungrowScraper:
    def __init__(self, username: str, password: str):
        self.baseUrl = "https://www.isolarcloud.com"
        # TODO: Set the gateway during the login procedure
        self.gatewayUrl = "https://gateway.isolarcloud.eu"
        self.username = username
        self.password = password
        self.session: "requests.Session" = requests.session()
        self.userToken: "str|None" = None

    def login(self):
        self.session = requests.session()
        resp = self.session.post(
            f"{self.baseUrl}/userLoginAction_login",
            data={
                "userAcct": self.username,
                "userPswd": encrypt_rsa(self.password, LOGIN_RSA_PUBLIC_KEY),
            },
            headers={
                "_isMd5": "1"
            },
            timeout=60,
        )
        self.userToken = resp.json()["user_token"]
        return self.userToken

    def post(self, relativeUrl: str, jsn: "Optional[dict]"=None, isFormData=False):
        userToken = self.userToken if self.userToken is not None else self.login()
        jsn = dict(jsn) if jsn is not None else {}
        nonce = generate_random_word(32)
        # TODO: Sungrow also adjusts for time difference between server and client
        # This is probably not a must though. The relevant call is:
        # https://gateway.isolarcloud.eu/v1/timestamp
        unixTimeMs = int(time.time() * 1000)
        jsn["api_key_param"] = {"timestamp": unixTimeMs, "nonce": nonce}
        randomKey = "web" + generate_random_word(13)
        userToken = self.userToken
        userId = userToken.split('_')[0]
        jsn["appkey"] = APP_KEY
        if "token" not in jsn:
            jsn["token"] = userToken
        jsn["sys_code"] = 200
        data: "dict|str"
        if isFormData:
            jsn["api_key_param"] = encrypt_aes(json.dumps(jsn["api_key_param"]), randomKey)
            jsn["appkey"] = encrypt_aes(jsn["appkey"], randomKey)
            jsn["token"] = encrypt_aes(jsn["token"], randomKey)
            data = jsn
        else:
            data = encrypt_aes(json.dumps(jsn, separators=(",", ":")), randomKey)
        resp = self.session.post(
            f"{self.gatewayUrl}{relativeUrl}",
            data=data,
            headers={
                "x-access-key": ACCESS_KEY,
                "x-random-secret-key": encrypt_rsa(randomKey, APP_RSA_PUBLIC_KEY),
                "x-limit-obj": encrypt_rsa(userId, APP_RSA_PUBLIC_KEY),
                "content-type": "application/json;charset=UTF-8"
            }
        )
        return decrypt_aes(resp.text, randomKey)

s = SungrowScraper("MY_USERNAME", "MY_PASSWORD")
resp = s.post(
    "/v1/powerStationService/getPsListNova",
    jsn={
        "share_type_list": ["0", "1", "2"]
    }
)
print(resp)
threepoints85 commented 7 months ago

@Pistro @rob3r7 Thank you for the effort. But what do I have to do with these codes? Unfortunately I have no idea...

It would be great if someone could explain it to me step by step

KLucky13 commented 7 months ago

same problem since 3 days.... Still no fix?

KLucky13 commented 7 months ago

Here is another working Python example of how to use query the new Sungrow API. Sadly, I forgot to refresh the page while working and didn't see the post of @rob3r7 ...

The scope of the code below is very similar to his contribution, with some extra additions:

  • I have added a call to log in to the portal (this requires a different RSA key)
  • My x-limit-obj adds the RSA encrypted user id, which is required for some calls, as also mentioned by @0SkillAllLuck

You will see that the post method has a isFormData flag. The code associated with that flag being true is reverse engineered based on what I was able to make out of minified Sungrow encryption code. I do not know a call for which the flag should be set to true, so in that particular case there may be some issues...

import base64
import string
import random
from typing import Optional
import time
import json

import requests
from cryptography.hazmat.primitives import serialization, asymmetric, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# TODO: Getting these values directly from the files by the Sungrow API is better than hardcoding them...
LOGIN_RSA_PUBLIC_KEY: asymmetric.rsa.RSAPublicKey = serialization.load_pem_public_key(b"-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDJRGV7eyd9peLPOIqFg3oionWqpmrjVik2wyJzWqv8it3yAvo/o4OR40ybrZPHq526k6ngvqHOCNJvhrN7wXNUEIT+PXyLuwfWP04I4EDBS3Bn3LcTMAnGVoIka0f5O6lo3I0YtPWwnyhcQhrHWuTietGC0CNwueI11Juq8NV2nwIDAQAB\n-----END PUBLIC KEY-----")
APP_RSA_PUBLIC_KEY: asymmetric.rsa.RSAPublicKey   = serialization.load_pem_public_key(bytes("-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCkecphb6vgsBx4LJknKKes-eyj7-RKQ3fikF5B67EObZ3t4moFZyMGuuJPiadYdaxvRqtxyblIlVM7omAasROtKRhtgKwwRxo2a6878qBhTgUVlsqugpI_7ZC9RmO2Rpmr8WzDeAapGANfHN5bVr7G7GYGwIrjvyxMrAVit_oM4wIDAQAB".replace("-", "+").replace("_", "/") + "\n-----END PUBLIC KEY-----",  'utf8'))
ACCESS_KEY = "9grzgbmxdsp3arfmmgq347xjbza4ysps"
APP_KEY = "B0455FBE7AA0328DB57B59AA729F05D8"

def encrypt_rsa(value: str, key: asymmetric.rsa.RSAPublicKey) -> str:
    # Encrypt the value
    encrypted = key.encrypt(
        value.encode(),
        asymmetric.padding.PKCS1v15(),
    )
    return base64.b64encode(encrypted).decode()

def encrypt_aes(data: str, key: str):
    key_bytes = key.encode('utf-8')
    data_bytes = data.encode('utf-8')

    # Ensure the key is 16 bytes (128 bits)
    if len(key_bytes) != 16:
        raise ValueError("Key must be 16 characters long")

    cipher = Cipher(algorithms.AES(key_bytes), modes.ECB(), backend=default_backend())
    encryptor = cipher.encryptor()
    padder = padding.PKCS7(algorithms.AES.block_size).padder()
    padded_data = padder.update(data_bytes) + padder.finalize()
    encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
    return encrypted_data.hex()

def decrypt_aes(data: str, key: str):
    key_bytes = key.encode('utf-8')

    # Ensure the key is 16 bytes (128 bits)
    if len(key_bytes) != 16:
        raise ValueError("Key must be 16 characters long")

    encrypted_data = bytes.fromhex(data)
    cipher = Cipher(algorithms.AES(key_bytes), modes.ECB(), backend=default_backend())
    decryptor = cipher.decryptor()
    decrypted_padded_data = decryptor.update(encrypted_data) + decryptor.finalize()
    unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
    decrypted_data = unpadder.update(decrypted_padded_data) + unpadder.finalize()
    return decrypted_data.decode('utf-8')

def generate_random_word(length: int):
    char_pool = string.ascii_letters + string.digits
    random_word = ''.join(random.choice(char_pool) for _ in range(length))
    return random_word

class SungrowScraper:
    def __init__(self, username: str, password: str):
        self.baseUrl = "https://www.isolarcloud.com"
        # TODO: Set the gateway during the login procedure
        self.gatewayUrl = "https://gateway.isolarcloud.eu"
        self.username = username
        self.password = password
        self.session: "requests.Session" = requests.session()
        self.userToken: "str|None" = None

    def login(self):
        self.session = requests.session()
        resp = self.session.post(
            f"{self.baseUrl}/userLoginAction_login",
            data={
                "userAcct": self.username,
                "userPswd": encrypt_rsa(self.password, LOGIN_RSA_PUBLIC_KEY),
            },
            headers={
                "_isMd5": "1"
            },
            timeout=60,
        )
        self.userToken = resp.json()["user_token"]
        return self.userToken

    def post(self, relativeUrl: str, jsn: "Optional[dict]"=None, isFormData=False):
        userToken = self.userToken if self.userToken is not None else self.login()
        jsn = dict(jsn) if jsn is not None else {}
        nonce = generate_random_word(32)
        # TODO: Sungrow also adjusts for time difference between server and client
        # This is probably not a must though. The relevant call is:
        # https://gateway.isolarcloud.eu/v1/timestamp
        unixTimeMs = int(time.time() * 1000)
        jsn["api_key_param"] = {"timestamp": unixTimeMs, "nonce": nonce}
        randomKey = "web" + generate_random_word(13)
        userToken = self.userToken
        userId = userToken.split('_')[0]
        jsn["appkey"] = APP_KEY
        if "token" not in jsn:
            jsn["token"] = userToken
        jsn["sys_code"] = 200
        data: "dict|str"
        if isFormData:
            jsn["api_key_param"] = encrypt_aes(json.dumps(jsn["api_key_param"]), randomKey)
            jsn["appkey"] = encrypt_aes(jsn["appkey"], randomKey)
            jsn["token"] = encrypt_aes(jsn["token"], randomKey)
            data = jsn
        else:
            data = encrypt_aes(json.dumps(jsn, separators=(",", ":")), randomKey)
        resp = self.session.post(
            f"{self.gatewayUrl}{relativeUrl}",
            data=data,
            headers={
                "x-access-key": ACCESS_KEY,
                "x-random-secret-key": encrypt_rsa(randomKey, APP_RSA_PUBLIC_KEY),
                "x-limit-obj": encrypt_rsa(userId, APP_RSA_PUBLIC_KEY),
                "content-type": "application/json;charset=UTF-8"
            }
        )
        return decrypt_aes(resp.text, randomKey)

s = SungrowScraper("MY_USERNAME", "MY_PASSWORD")
resp = s.post(
    "/v1/powerStationService/getPsListNova",
    jsn={
        "share_type_list": ["0", "1", "2"]
    }
)
print(resp)

Did you fix it? Is there any way you could explain what to do to get it to work again?

fhopley commented 7 months ago

Same issue, following for a fix.

plumpy80 commented 7 months ago

Same issue.... :(

borsa93 commented 7 months ago

same issue here, following for a fix. thanks in advance to whoever is working on it.

grf692 commented 7 months ago

Following for a fix :(

str2der commented 7 months ago

Same here. Following for a fix!

Private7Joker commented 7 months ago

Same here. Following for a fix!

plumpy80 commented 7 months ago

I'm using the ModbusTCP2MQTT now, with my Sungrow SG5.0RT inverter, and it's working fine.... If you can't wait the fix, try it...

https://github.com/MatterVN/ModbusTCP2MQTT

KLucky13 commented 7 months ago

I'm using the ModbusTCP2MQTT now, with my Sungrow SG5.0RT inverter, and it's working fine.... If you can't wait the fix, try it...

https://github.com/MatterVN/ModbusTCP2MQTT

I might be the issue here but I can't figure out how to properly set it up :-/

I created an issue for it, hopeing someone could help: https://github.com/MatterVN/ModbusTCP2MQTT/issues/68

DrLEEi commented 6 months ago

Unfortunately, I have the same problem when connecting to the https://gateway.isolarcloud.eu server. I'm waiting for the plugin to be fixed.

Thank you for your work!

AZKhalil commented 6 months ago

i have the same issue, is there any update or solution yet? @MickMake

DL4DP commented 6 months ago

Hello everyone, I haven't received any data for a few days.

A solution to the problem hasn't been found yet when I read through the posts.

It would be nice if a solution could be found. There is quite a lot of time involved in the integration.

Thanks!

monojk commented 6 months ago

@KLucky13, @Pistro: can your python scripts be changed such they have the same command and result interface as GoSungrow's queryMutiPointDataList, for querying the points? That would be great.

KLucky13 commented 6 months ago

@KLucky13, @Pistro: can your python scripts be changed such they have the same command and result interface as GoSungrow's queryMutiPointDataList, for querying the points? That would be great.

I have nothing to do with this integration I'm afraid, just a user like the rest :-)

Pistro commented 6 months ago

@KLucky13, @Pistro: can your python scripts be changed such they have the same command and result interface as GoSungrow's queryMutiPointDataList, for querying the points? That would be great.

I've been actively following this discussion because our company has developed an internal Python library to work with the Sungrow API. Like many of you, we encountered challenges when the Sungrow API was updated. To contribute to this community, I've shared our solution here, inspired by the useful tips from @0SkillAllLuck, which were instrumental in resolving our issues.

While I'm enthusiastic about this project, my current schedule prevents me from being directly involved in integrating our approach with the larger project, especially as my expertise in Go is quite limited. However, I'm more than willing to answer any questions or clarify any aspects of our Python code if you find it confusing or need more insight.

lupus78 commented 6 months ago

Looks like @MickMake has no time to deal with this project. My Go skill are none existent.

@Pistro what do you think, how much work would it be to transform your Python code into a proper HA component? My Python skills are okay, however I never made a ha component, only modified an existing one.

AZKhalil commented 6 months ago

I'm using the ModbusTCP2MQTT now, with my Sungrow SG5.0RT inverter, and it's working fine.... If you can't wait the fix, try it...

https://github.com/MatterVN/ModbusTCP2MQTT

for the time being I am using this addon with direct access to SH6.0RT, which works perfectly fine. I hope we will have a fixed for his asap.

KLucky13 commented 6 months ago

Unfortunatly, the addon above can only be used in combination with only 1 converter. I have 2, so I would to choose between one, which is not really interesting

grf692 commented 6 months ago

I'm using the ModbusTCP2MQTT now, with my Sungrow SG5.0RT inverter, and it's working fine.... If you can't wait the fix, try it... https://github.com/MatterVN/ModbusTCP2MQTT

for the time being I am using this addon with direct access to SH6.0RT, which works perfectly fine. I hope we will have a fixed for his asap.

I am also using it since yesterday with a SG8.0RT and it seems it's working (even though I dont have much sun at this time of the year where I am :D )