pythoncloudsolar

AISWEI Solar API - Get Plant List


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.


Solution

  • 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