1

I am trying to connect with the AISWEI Solar API. I was sent a API user manual and a Token by email. When i log into my account online I also see an APP Key which consists of a 9 digit number and a alphanumeric string after that. My issue is that i have tried various HTTP request from python and ARC clients. I still seem to get no response back from their servers. I am trying to use the getPlantList conmmand.

API Command

Going by the API document, i first thought the request URL was a typo but if I type is as is, I get a 400 - bad request instead of a 404 - not found. So I assume the URL is correct.

Has anyone used this API or can assist me in fixing my code?

Below is my python code:

import requests

def get_plant_list(token, page=1, size=20):
    url = 'https://api.general.aisweicloud.com/planlist'
    params = {
        'token': token,
        'page': str(page),
        'size': str(size)
    }

    try:
        response = requests.get(url, params=params, verify=False)

        if response.status_code == 200:
            return response.json()
        else:
            print(f"Request failed with status code: {response.status_code}")
            return None

    except requests.exceptions.RequestException as e:
        print("An error occurred while making the request:", e)
        return None

token = 'XXXXXXXXXXXXX'

plant_list = get_plant_list(token)

if plant_list:
    print(plant_list)

Also I have share the API Manual here:

API Manual

Sorry I don't know how to upload PDFs here.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
kyleprr
  • 21
  • 5
  • You should get a `X-Ca-Error-Message` header back. What does that say? It's strange... you've got an outdated API Manual. Mine is V1.0 dates 2022-02-18. And in that one there is no `/getPlantList` anymore, only `/getplant` (but I target the European cloud eu-api-genergal.aisweicloud.com). – Rik Aug 11 '23 at 12:51
  • BTW. I didn't get a token so I can't call getplant. But you also need an AppKey and AppSecret to call the other endpoints (with special constructed signature). That should be in your account settings (but for me it wasn't and they had to add them). End result was that I saw that the day result of `/getPlantOutput` was only every 20 minutes while the site/app is every 10min. (I'll keep to the api of cloud.solplanet.net for now, where it is 10min.) – Rik Aug 11 '23 at 12:52
  • @Rik I do have an AppKey and AppSecret. But not sure where they are to be used. Can I ask what programming language did you use for your API usage and if you don't mind sharing a few code snippets to get it up and running. Even if it's just calling a simple command. – kyleprr Aug 17 '23 at 07:46
  • I programmed it in PHP. Yeah, you definitely need AppKey and AppSecret as well as ApiKey and (user)token. I'm already a step further and have it working. Because the consumer api only has 20 min interval I upgraded to the business api where the interval is 10 min. I have both api's working. I'll create an answer somewhat later today (it's early here) for both api's. (Although the answer won't be python you can certainly see how things are done.) – Rik Aug 18 '23 at 04:08

2 Answers2

1

Ok, getting data from the AISWEI Solar API...

There is a Pro (Business) version and a Consumer version of the API. The Consumer version only had an interval of 20 minutes (while the site at solplanet.net did have 10 minutes interval. You can upgrade to the Pro version via the App. The API's differ slightly.

Below is code for both Pro and Consumer versions of the API (done via the $pro variable). The getfromapi function will show that you need to sign your calls to the API. This is done by taking the header + url values and sign the value with the AppSecret. It's very important that the parameters in the url are in alphabetical order. Note: If something goes wrong, the API will throw back an error in the header X-Ca-Error-Message. So make sure to check the headers if it doesn't work.

First you need to get the ApiKey for your inverter. This should be under details at the site (different login-page for Consumer and Business). You can also find the AppKey and AppSecret there under your account (Account > Safety settings > API authorization code for Business and Account > Account and security > Safety settings for Consumer). If it's not there you can contact solplanet.net to activate it. For the Pro API you also need a token which you also can get via e-mail to solplanet.net (which have excellent service).

Following code is for PHP (python3 is below that). I run it via a cron-job every 5 minutes to retrieve data and push it to a mysql database for a local dashboard. Everything runs on a Raspberry Pi 3 B. It first retrieves the status of the inverter (last updated and status). The it retrieves the production values of today (or given date). Consumer at 20 minutes interval and Business at 10 minutes interval. And lastly it retrieves the month production (all days of a month) for the daily-table.

I hope you have some use for the code and can implement it in your own program. If you have any question, let me now.

Extra note: The time and ludt (last update time) is always in timezone for China for me with no timezone information (maybe because this was a zeversolar inverter). So I convert it here to my own timezone (with timezone information). Check for yourself if the time/ludt is returned correctly.

<?php
error_reporting(E_ALL ^ E_NOTICE);
date_default_timezone_set('Europe/Amsterdam');
$crlf = (php_sapi_name()==='cli' ? "\n" : "<br>\n"); // we want html if we are a browser

$host='https://eu-api-genergal.aisweicloud.com';

$ApiKey = 'xx'; // apikey for the inverter
$AppKey = 'xx'; // appkey for pro or consumer version
$AppSecret = 'xx';
$token = 'xx';  // only needed for pro
$pro = false; // is the appkey pro?

$con=false; // actually write to mysql database, false for testing

// $today and $month for calls to get inverter output / 2023-08-18 and 2023-08
// if we call via a browser we can pass today or month by parameters
// otherwise current day and month is taken
$today = isset($_GET['today']) ? $_GET['today'] : date("Y-m-d");
$month = isset($_GET['month']) ? $_GET['month'] : date('Y-m',strtotime("-1 days"));
if (isset($_GET['today'])) { $month=substr($today,0,7); }

if ($con) {
    include('database.php'); // file with $username, $password and $servername for mysql server
    $conn = new mysqli($servername, $username, $password, "p1");
    if ($conn->connect_error) { die("Connection failed: " . $conn->connect_error); }
}

// get data from api, pass url without host and without apikey/token
function getfromapi($url) {
    global $pro, $host, $token, $AppKey, $AppSecret, $ApiKey;

    $method = "GET";
    $accept = "application/json";
    $content_type = "application/json; charset=UTF-8";

    // add apikey and token
    $key = ($pro ? "apikey=$ApiKey" : "key=$ApiKey");  // pro uses apikey, otherwise key
    $url .= (parse_url($url, PHP_URL_QUERY) ? '&' : '?') . $key;  // add apikey
    if ($pro) $url = $url . "&token=$token"; // add token
    
    // disect and reshuffle url parameters in correct order, needed for signature
    $s1 = explode('?', $url);
    $s2 = explode('&', $s1[1]);
    sort($s2);
    $url = $s1[0].'?'.implode('&', $s2); // corrected url
    
    // headers
    $header = array();
    $header["User-Agent"] = 'app 1.0';
    $header["Content-Type"] = $content_type;
    $header["Accept"] = $accept;
    $header["X-Ca-Signature-Headers"] = "X-Ca-Key"; // we only pass extra ca-key in signature
    $header["X-Ca-Key"] = $AppKey;

    // sign
    $str_sign = $method . "\n";
    $str_sign .= $accept . "\n";
    $str_sign .= "\n";
    $str_sign .= $content_type . "\n";
    $str_sign .= "\n"; // we use no Date header
    $str_sign .= "X-Ca-Key:$AppKey" . "\n";
    $str_sign .= $url;
    $sign = base64_encode(hash_hmac('sha256', $str_sign, $AppSecret, true));
    $header['X-Ca-Signature'] = $sign;

    // push headers to an headerarray
    $headerArray = array();
    foreach ($header as $k => $v) { array_push($headerArray, $k . ": " . $v); }

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
    curl_setopt($ch, CURLOPT_URL, "$host$url");
    curl_setopt($ch, CURLINFO_HEADER_OUT, true);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headerArray);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    // curl_setopt($ch, CURLOPT_POST, 1);
    // curl_setopt($ch, CURLOPT_POSTFIELDS, $postdata);
    $data = curl_exec($ch);
    $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $header_len = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $header2 = substr($data, 0, $header_len);
    $data = substr($data, $header_len);
    $header1 = curl_getinfo($ch, CURLINFO_HEADER_OUT);
    curl_close($ch);

    $json = json_decode($data, true);
    $json['httpcode'] = $httpcode;
    if (!$pro) $json['status'] = '200'; // pro has no status, so use 200
    if ($httpcode != '200') {
        $json['status'] = $httpcode;
        $json['headers'] = $header2;
    }

    return $json; // return as array
}

// ===============================================================
// reading inverter state
// ===============================================================
$url = "/pro/getDeviceListPro";
if (!$pro) $url = "/devicelist";
$json = getfromapi($url);
if ($json['status']=='200') {

    if ($pro) {
        $status=$json['data'][0]['inverters'][0]['istate'];
        $update=$json['data'][0]['inverters'][0]['ludt'];
        $current=$json['time'];
    } else {
        $status=$json['data']['list'][0]['inverters'][0]['istate'];
        $update=$json['data']['list'][0]['inverters'][0]['ludt'];
        $dt = new DateTime('now', new DateTimeZone('Asia/Shanghai'));
        $current = $dt->format('Y-m-d H:i:s'); // no current time in json
    }

    // time and ludt are in China/Guizhou time, so add the timezone
    $dt = new DateTime($current, new DateTimeZone('Asia/Shanghai'));
    $current = $dt->format('Y-m-d H:i:sP'); // no current time in json
    $dt = new DateTime($update, new DateTimeZone('Asia/Shanghai'));
    $update = $dt->format('Y-m-d H:i:sP'); 

    // and convert to our own timezone
    $current = date('Y-m-d H:i:sP', strtotime($current));
    $update = date('Y-m-d H:i:sP', strtotime($update));

    $stat = 'warning';
    if ($status == '0') $stat = 'offline';
    if ($status == '1') $stat = 'normal';
    if ($status == '2') $stat = 'warning';
    if ($status == '3') $stat = 'error';
    $json['state'] = $stat;
    $json['last'] = $update;

    echo "Current time   = " . $current . $crlf;
    echo "Last update    = " . $update . $crlf;
    echo "Inverter state = " . $stat . $crlf;

} else {
    echo "Error reading state: " . $json['status'] . $crlf . $json['headers'];
}

// ===============================================================
// readings from today
// ===============================================================
$url = "/pro/getPlantOutputPro?period=bydays&date=$today";
if (!$pro) $url = "/getPlantOutput?period=bydays&date=$today";
$json = getfromapi($url);
if ($json['status']=='200') {

    // process
    if ($pro) {
        $dt=$today;
        $unit = $json['data']['dataunit'];
        $x = $json['data']['result'];
    } else {
        $dt=$today;
        $unit = $json['dataunit'];
        $x = $json['data'];
    }

    foreach ($x as $key => $val) {
        $tm=$val['time'];
        $pw=$val['value'];
        if ($unit=='W') $pw=$pw/1000;
        $sql = "REPLACE INTO solar (tijd, power) VALUES ('$dt $tm', $pw);";
        if ($con) {
            if (!$conn->query($sql)) {
                echo "No data for inserted !!!!!!!!!!!!!<br>".$conn->error;
            }
        } else {
            //echo(".");
            echo($sql.$crlf);
        }
    }
    if (!$con) echo($crlf);
    echo "Daily output processed" . $crlf;

} else {
    echo "Error reading daily: " . $json['status'] . $crlf . $json['headers'];
}

// ===============================================================
// readings from month
// ===============================================================
$url = "/pro/getPlantOutputPro?period=bymonth&date=$month";
if (!$pro) $url = "/getPlantOutput?period=bymonth&date=$month";
$json = getfromapi($url);
if ($json['status']=='200') {

    // process
    if ($pro) {
        $unit = $json['data']['dataunit'];
        $x = $json['data']['result'];
    } else {
        $unit = $json['dataunit'];
        $x = $json['data'];
    }

    foreach ($x as $key => $val) {
        $tm=$val['time'];
        $pw=$val['value'];
        if ($unit=='W') $pw=$pw/1000;
        $sql = "REPLACE INTO solarmonth (tijd, power) VALUES ('$tm', $pw);";
        if ($con) {
            if (!$conn->query($sql)) {
                echo "No data for inserted !!!!!!!!!!!!!<br>".$conn->error;
            }
        } else {
            //echo(".");
            echo($sql.$crlf);
        }
    }
    if (!$con) echo($crlf);
    echo "Monthly output processed" . $crlf;
} else {
    echo "Error reading monthly: " . $json['status'] . $crlf . $json['headers'];
}

echo("Done" . $crlf);

Edit: Ok, it's been a while since I programmed in python so I called on the help of my friend chatGPT :D The following is (after some adjustments) working correctly for me (besides the database stuff but that falls outside of this question).

import requests
import json
import datetime
import pytz
import os
import time
import hmac
import hashlib
import base64
from datetime import datetime, timezone, timedelta

os.environ['TZ'] = 'Europe/Amsterdam'  # Setting the default timezone
time.tzset()
crlf = "\n"  # Line break

host = 'https://eu-api-genergal.aisweicloud.com'

ApiKey = 'xx'    # in the dashboard under details of the inverter/plant
AppKey = 'xx'    # under your account info, if not there, contact solplanet
AppSecret = 'xx' # same as AppKey
token = 'xx'     # not needed for consumer edition, otherwise contact solplanet
pro = False

con = False

today = datetime.today().strftime('%Y-%m-%d')
month = datetime.today().strftime('%Y-%m')

if con:
    # Include database connection setup here if needed
    pass

def getfromapi(url):
    global pro, host, token, AppKey, AppSecret, ApiKey

    method = "GET"
    accept = "application/json"
    content_type = "application/json; charset=UTF-8"

    key = f"apikey={ApiKey}" if pro else f"key={ApiKey}"
    url += ('&' if '?' in url else '?') + key
    if pro:
        url += f"&token={token}"

    s1 = url.split('?')
    s2 = sorted(s1[1].split('&'))
    url = s1[0] + '?' + '&'.join(s2)

    header = {
        "User-Agent": "app 1.0",
        "Content-Type": content_type,
        "Accept": accept,
        "X-Ca-Signature-Headers": "X-Ca-Key",
        "X-Ca-Key": AppKey
    }

    str_sign = f"{method}\n{accept}\n\n{content_type}\n\nX-Ca-Key:{AppKey}\n{url}"
    sign = base64.b64encode(hmac.new(AppSecret.encode('utf-8'), str_sign.encode('utf-8'), hashlib.sha256).digest())
    header['X-Ca-Signature'] = sign

    headerArray = [f"{k}: {v}" for k, v in header.items()]

    response = requests.get(f"{host}{url}", headers=header)
    
    httpcode = response.status_code
    header_len = len(response.headers)
    header2 = response.headers
    data = response.text

    try:
        json_data = json.loads(data)
    except:
        json_data = {}

    json_data['httpcode'] = httpcode
    if not pro:
        json_data['status'] = '200'
    if httpcode != 200:
        json_data['status'] = httpcode
        json_data['headers'] = header2

    return json_data

# ===============================================================
# reading inverter state
# ===============================================================
url = "/pro/getDeviceListPro" if pro else "/devicelist"
json1 = getfromapi(url)
if json1['status'] == '200':

    if pro:
        status = json1['data'][0]['inverters'][0]['istate']
        update = json1['data'][0]['inverters'][0]['ludt']
        current = json1['time']
    else:
        status = json1['data']['list'][0]['inverters'][0]['istate']
        update = json1['data']['list'][0]['inverters'][0]['ludt']
        current = datetime.now(timezone(timedelta(hours=8)))  # China/Guizhou time

    current = current.strftime('%Y-%m-%d %H:%M:%S %z')  # format with timezone
    update = datetime.strptime(update, '%Y-%m-%d %H:%M:%S').replace(tzinfo=timezone(timedelta(hours=8)))
    update = update.strftime('%Y-%m-%d %H:%M:%S %z')

    # Convert to your own timezone
    current = datetime.strptime(current, '%Y-%m-%d %H:%M:%S %z')
    current = current.astimezone(timezone.utc)
    current = current.strftime('%Y-%m-%d %H:%M:%S %z')
    
    status_map = {0: 'offline', 1: 'normal', 2: 'warning', 3: 'error'}
    stat = status_map.get(status, 'warning')

    json1['state'] = stat
    json1['last'] = update

    print("Current time   =", current)
    print("Last update    =", update)
    print("Inverter state =", stat)

    json1['current'] = current
    json1['status'] = stat
    json1['last'] = update
    html = json.dumps(json1)  # Assuming json is imported
    #with open('/home/pi/solstatus.json', 'w') as file:
    #    file.write(html)

else:
    print("Error reading state:", json1['status'], json1['headers'])

# ===============================================================
# readings from today
# ===============================================================
url = "/pro/getPlantOutputPro?period=bydays&date=" + today
if not pro:
    url = "/getPlantOutput?period=bydays&date=" + today
json1 = getfromapi(url)
if json1['status'] == '200':
    if pro:
        dt = today
        unit = json1['data']['dataunit']
        x = json1['data']['result']
    else:
        dt = today
        unit = json1['dataunit']
        x = json1['data']

    for val in x:
        tm = val['time']
        pw = val['value']
        if unit == 'W':
            pw /= 1000
        sql = f"REPLACE INTO solar (tijd, power) VALUES ('{dt} {tm}', {pw});"
        if con:
            if not conn.query(sql):
                print("No data for inserted !!!!!!!!!!!!!")
                print(conn.error)
        else:
            # print(".")
            print(sql)
    if not con:
        print("")
    print("Daily output processed")

    html = json.dumps(json1)  # Assuming json is imported
    #with open('/home/pi/solar1.json', 'w') as file:
    #    file.write(html)

else:
    print("Error reading daily:", json1['status'], json1['headers'])

# ===============================================================
# readings from month
# ===============================================================
url = "/pro/getPlantOutputPro?period=bymonth&date=" + month
if not pro:
    url = "/getPlantOutput?period=bymonth&date=" + month
json1 = getfromapi(url)
if json1['status'] == '200':
    
    if pro:
        unit = json1['data']['dataunit']
        x = json1['data']['result']
    else:
        unit = json1['dataunit']
        x = json1['data']

    for val in x:
        tm = val['time']
        pw = val['value']
        if unit == 'W':
            pw /= 1000
        sql = f"REPLACE INTO solarmonth (tijd, power) VALUES ('{tm}', {pw});"
        if con:
            if not conn.query(sql):
                print("No data for inserted !!!!!!!!!!!!!")
                print(conn.error)
        else:
            # print(".")
            print(sql)
    if not con:
        print("")
    print("Monthly output processed")

    html = json.dumps(json1)  # Assuming json is imported
    #with open('/home/pi/solar2.json', 'w') as file:
    #    file.write(html)

else:
    print("Error reading monthly:", json1['status'], json1['headers'])

print("Done")

Results for me in:

Current time = 2023-08-30 14:08:39 +0000
Last update = 2023-08-30 21:55:10 +0800
Inverter state = normal

Rik
  • 1,982
  • 1
  • 17
  • 30
  • I converted the code to python. I now receive an X-Ca-Error message: { ....... "Server": "Kaede/3.5.3.842 (hz003fbrd)", "X-Ca-Error-Code": "A400IK", "X-Ca-Request-Id": "F63D7CEE-842D-45D0-92E7-FA8C77BFB3D1", "Access-Control-Allow-Origin": "*", "X-Ca-Error-Message": "Invalid AppKey", "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,HEAD,OPTIONS,PATCH", "Access-Control-Max-Age": "172800" } Correct me if i am wrong. The app key is the 9 digit number listed in my account settings? I will email support and also ask them to confirm this. – kyleprr Aug 28 '23 at 14:17
  • @kyleprr Yes, the AppKey is 9 digits. Are you sure you are targeting the consumer version of the api (when you login the consumer site)? Or if you login the pro site target the pro version of the api. They are not interchangeable. – Rik Aug 28 '23 at 18:23
  • I use this login page for the website: https://internation-cloud.solplanet.net/user/login – kyleprr Aug 29 '23 at 11:51
  • @kyleprr Not sure if that's the pro or consumer. I guess the consumer (in that case you should set pro to false and use the consumer api/urls). I only have https://pro-cloud.solplanet.net/ and https://cloud.solplanet.net/ as login. (you could try the pro urls if you used the consumer urls and got that error just to see which one works) – Rik Aug 29 '23 at 15:06
  • Hi Rik. Thanks for this code. Their manual is quite cryptic and so are their examples on GitHub (which are from the year 2016 BTW). One thing that I can't find on their website is the ApiKey for the inverter. Is it possible that this is the 7-digit number that I can see at the end of this address (plantId) https://cloud.solplanet.net/home/device?plantId=1234567? Is your ApiKey the same as plantId? I have the consumer version. Your code gives me X-Ca-Error-Message: Invalid Signature and I'm not sure if the wrong ApiKey is not the cause. – goodfellow Aug 30 '23 at 06:39
  • Rik, now I'm one step further. The API returns code -1 (the key was not found). I've tried the 7-digit number (plantId), plant name, inverter serial number, and all other numbers that I found on their website but still no go. I'm in contact with their service, but they also don't know. They will call the service in China and hopefully will get back to me with the proper ApiKey/inverter id. Thanks again for your example as it saved me a lot of time! – goodfellow Aug 30 '23 at 09:18
  • 1
    @goodfellow The ApiKey for the plant is under details for the inverter. Click Plant at the top and then click the plant to enter it. Next... at the right see the block with installation date. See these images https://i.imgur.com/ofTE5HL.png and https://i.imgur.com/Ux7MRj7.png – Rik Aug 30 '23 at 10:13
  • @goodfellow BTW. If you get an X-Ca-Error-Message: Invalid Signature then there seems to be something else wrong. If you give a wrong Api Key it should give you a json with status "status":10012. Not a signature error. – Rik Aug 30 '23 at 10:18
  • Rik, I sorted it out. To see the ApiKey in the web interface the user has to be the OWNER of the installation. Now everything is working fine (the signature problem was a silly mistake of mine). Thank you for the response. – goodfellow Aug 30 '23 at 12:53
  • @Rik my account is under the consumer but no matter how much i try and all the login API details i have, i still keep getting an Invalid AppKey error. These guys really need to update their API docs or have something better to test. I will sort out this issue with them. I will post the python version below if anyone wants it – kyleprr Aug 30 '23 at 13:11
  • @goodfellow are using the international version of the API? The same docs that i posted above? – kyleprr Aug 30 '23 at 13:12
  • I added a translation of the PHP code to python3 in my answer. The python3 code is also working correctly for me. – Rik Aug 30 '23 at 14:22
  • Now both the PHP and python3 code are identical (including today and month result extraction) and both work. – Rik Aug 30 '23 at 14:44
  • I have newer docs. The same as Rik. Please look at my comment under your second post. – goodfellow Aug 31 '23 at 00:08
  • @Rik, when I read samples every 20 minutes from my (consumer) API there are differences between samples displayed on the website. The trend is the same but values for given time stamps are always a bit different. Do you also notice this behavior in your pro/consumer version? – goodfellow Aug 31 '23 at 11:33
  • 1
    @goodfellow The interval of the consumer version is 20 minutes. That's why I asked to be switched to the business version. There the interval is 10 minutes and should be identical to the site (I didn't check if they were exactly the same but I would assume so). There is a manual to switch but then you also need an extra token and for that you need to contact solplanet. That was no problem. So you can contact them for it. – Rik Aug 31 '23 at 12:25
  • @Rik Thanks for all the help. Unfortunately i have to give up on this one or revisit some other time. Even with your python code and changing it to my correct details and endpoint i still receive an 'X-Ca-Error-Message': 'Invalid AppKey'. I don't think anyone here in Australia is using this API and the customer support is as good as not receiving an answer hahaha – kyleprr Aug 31 '23 at 14:04
  • @kyleprr Ok. When/if you revisit this you need to check if you are really using the consumer appkey with the consumer urls (or other/url). If so, they should check at solplanet why it doesn't work. I found the service quite good here in my region Netherlands/Europe. I normally got a reaction within one or two business days. PS. I also got some PHP code to login and extract data from the API which the website uses at `https://cloud.solplanet.net` (although I imagine that's not the official way ;) ). It does an automatic re-login every 6 hours (on detection of expired access_token). – Rik Aug 31 '23 at 16:25
0

This is what I finally created based of @Riks PHP code. It seems to call the request and gets back results. Except that i cannot test it on my end since my AppKey is apparently invalid :(

Let me know if anyone try's it out and get's it working with this code.

import requests
import hashlib
import hmac
import base64
from datetime import datetime
import json

app_key = "123456789"
app_secret = "XXXXXX"

url = "https://api.general.aisweicloud.com/planlist"

request_body = {
    "FormParam1": "FormParamValue1",
    "FormParam2": "FormParamValue2"
}

headers = {
    "Host": "api.general.aisweicloud.com",
    "Date": datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT'),
    "User-Agent": "Apache-HttpClient/4.1.2 (java 1.6)",
    "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    "Accept": "application/json",
    "X-Ca-Request-Mode": "debug",
    "X-Ca-Version": "1",
    "X-Ca-Stage": "RELEASE",
    "X-Ca-Key": app_key,
    # "X-Ca-Timestamp": str(int(datetime.utcnow().timestamp() * 1000)),
    # "X-Ca-Nonce": "b931bc77-645a-4299-b24b-f3669be577ac"
}

headers_string = "\n".join([f"{key}:{headers[key]}" for key in sorted(headers)])

string_to_sign = f"GET\n\n\n{headers['Content-Type']}\n{headers['Date']}\n{headers_string}\n{url}"

signature = base64.b64encode(hmac.new(app_secret.encode(), string_to_sign.encode(), hashlib.sha256).digest()).decode()

headers["X-Ca-Signature"] = signature
# headers["X-Ca-Signature-Headers"] = "X-Ca-Request-Mode,X-Ca-Version,X-Ca-Stage,X-Ca-Key,X-Ca-Timestamp,X-Ca-Nonce"

response = requests.get(url, headers=headers, data=request_body, verify=False)

# Print the response
print(response.status_code)
print(response.headers)
print(response.text)
# print(json.dumps(response.json(), indent=4, sort_keys=True))
kyleprr
  • 21
  • 5
  • I couldn't get this code to work properly. I see there are a lot of headers which are not needed (and will even fail if there are too many). The url used in the signature should include the complete domain name but only the path. It was a while I did anything in python but I threw it into chatGPT and with some adjustments I got working code. I expanded my answer with the working python3 version. – Rik Aug 30 '23 at 14:21
  • I suspect that you have good AppKey but you are not using the ApiKey (inverter id) anywhere in your snippet which is mandatory. The API has to know which inverter you are asking for data in case you have more of them. Also, @Rik has a good point about these headers. I even ripped off the user-agent, content-type and accept leaving only the required ones. Rik has pointed out where to find the ApiKey in the comment under his answer. – goodfellow Aug 31 '23 at 00:17