AISWEI Solar API - Get Plant List

570 views Asked by At

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.

2

There are 2 answers

19
Rik On BEST ANSWER

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

2
kyleprr On

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