djansen1987 / SAJeSolar

SAJ eSolar Portal Sensors
GNU General Public License v3.0
21 stars 13 forks source link

IMPROVEMENT: Grid Voltage/Current/Frequency #27

Closed faanskit closed 1 year ago

faanskit commented 2 years ago

The WEB portal for eSolar does not provide the Grid information via its API. However, the APP utilized other API's from which additional information can be obtain.

The app does not use session cookies, but a token and userUid is required. To obtain the token, login with this endpoint and the password hashed with MD5.

        BASE_URL = "https://fopapp.saj-electric.com/sajAppApi/api"
        USER = "usename"
        PASSWORD= "password"
        response = requests.post(BASE_URL + "/oppersonal/login4C", data = {
            "appProjectName": "fop4lite",
            "appVersion": "1.5.0",
            "clientDate": today.strftime('%Y-%m-%d'),
            "lang": "en",
            "language": "en",
            "loginName": USER,
            "password": hashlib.md5(PASSWORD.encode()).hexdigest(),
            "platform": "iOS",
        }, timeout=5)

        _G["token"] = response.json().get("token")
        _G["userUid"] = response.json().get("bean").get("userId")

To obtain additional data, use this request. Take note that DeviceSN was already fetched with /getPlantDetailChart2 or /getPlantDetailInfo

        response = requests.post(BASE_URL + "/device/getDeviceRunInfoV2", data = {
            "appProjectName": "fop4lite",
            "appVersion": "1.5.0",
            "clientDate": today.strftime('%Y-%m-%d'),
            "deviceSN": _G["DeviceSN"],
            "lang": "en",
            "language": "en",
            "localDate": today.strftime('%Y-%m-%d'),
            "passKey": _G["userUid"],
            "platform": "iOS",
            "token": _G["token"],
        }, timeout=5)

This will produce the following output:

{
    "error_code": "0",
    "error_msg": "0",
    "data": {
        "pV1Volt": "573.1",
        "pV1Curr": "0.83",
        "pV2Volt": "582.6",
        "pV2Curr": "0.8",
        "pV3Volt": "N/A",
        "pV1StrCurr1": "N/A",
        "pV1StrCurr2": "N/A",
        "pV1StrCurr3": "N/A",
        "pV1StrCurr4": "N/A",
        "pV2StrCurr1": "N/A",
        "pV2StrCurr2": "N/A",
        "pV2StrCurr3": "N/A",
        "pV2StrCurr4": "N/A",
        "pV3StrCurr1": "N/A",
        "pV3StrCurr2": "N/A",
        "pV3StrCurr3": "N/A",
        "pV3StrCurr4": "N/A",
        "rGridVolt": "237.1",
        "rGridCurr": "1.54",
        "rGridFreq": "49.96",
        "sGridVolt": "238.4",
        "sGridCurr": "1.56",
        "sGridFreq": "49.96",
        "tGridVolt": "236.9",
        "tGridCurr": "1.56",
        "tGridFreq": "49.96",
        "runStatus": 1,
        "updateTime": "2022-06-19 09:20:00",
        "isAS1": 0,
        "pvList": [{
                "deviceType": 0,
                "pvVolt": "573.1",
                "pvCurr": "0.83",
                "strCurr": ["N/A", "N/A", "N/A", "N/A"],
                "pvPower": "N/A"
            }, {
                "deviceType": 0,
                "pvVolt": "582.6",
                "pvCurr": "0.8",
                "strCurr": ["N/A", "N/A", "N/A", "N/A"],
                "pvPower": "N/A"
            }, {
                "deviceType": 0,
                "pvVolt": "N/A",
                "pvCurr": "N/A",
                "strCurr": ["N/A", "N/A", "N/A", "N/A"],
                "pvPower": "N/A"
            }
        ]
    }
}

r/s/t GridVolt,GridCurr,Freq corresponds to the Grid output for the tree different phases.

The following endpoints used by the APP are fairly straight forward, but mostly redundant from the ones used by the portal and this integration:

POST https://fopapp.saj-electric.com/sajAppApi/api/oppersonal/login4C
POST https://fopapp.saj-electric.com/sajAppApi/api/opSettings/getData
POST https://fopapp.saj-electric.com/sajAppApi/api/bindingDeviceToken
POST https://fopapp.saj-electric.com/sajAppApi/api/GetNewVersionInfo
POST https://fopapp.saj-electric.com/sajAppApi/api/getInverters3
POST https://fopapp.saj-electric.com/sajAppApi/api/4.0/plant/getPlantAlarmNum
POST https://fopapp.saj-electric.com/sajAppApi/api/device/getDeviceBaseInfo
POST https://fopapp.saj-electric.com/sajAppApi/api/device/getDeviceRunInfoV2

The following endpoints used by the APP still needs to be figured out:

POST https://fopapp.saj-electric.com/sajAppApi/api/plant/plantPreviewListV5
POST https://fopapp.saj-electric.com/sajAppApi/api/plant/plantHomeDetail4Air
POST https://fopapp.saj-electric.com/sajAppApi/api/plant/getChartDeviceAndYearInfo
POST https://fopapp.saj-electric.com/sajAppApi/api/plant/getChartData

They appear to be using a SHA1 to create the signature, but I cannot reproduce it. Likely some salt is added by the app. I am clueless wrt. to reverse engineering apps, so I'm not sure I'll succeed. The app seems to be written in Kotlin, which makes it even harder to figure out (for me at least).

signParams:     appVersion,clientDate,lang,pageNo,pageSize,passKey,platform,timeStamp,token
signature:      B619F120519488AD00A0C760546038356ABD69D9

If my time allows, I'll add the code a provide it as a pull request. For now, I just wanted to offer my findings for anyone to pick up.

djansen1987 commented 2 years ago

HI Thanks for sharing! Great to know this is also an possibility, if i am correct the web does provide this info, but not in a structured api. For now there is no reason for me to implement this. The portal can be found on the inverter portal->realtime monitor

https://fop.saj-electric.com/saj/cloudMonitor/deviceInfo/findRawdataPageList image image

piio commented 2 years ago

Hi,

Such feature will be great!

djansen1987 commented 1 year ago

I will keep it open, feel free to create a pull request if you can workout the code. When things quiet down here i might have another look at it

RobbieDemaegdt commented 1 year ago

I'm currently developing a little bit on this integration. If I have some time over I will look into this.

RobbieDemaegdt commented 1 year ago

HI Thanks for sharing! Great to know this is also an possibility, if i am correct the web does provide this info, but not in a structured api. For now there is no reason for me to implement this. The portal can be found on the inverter portal->realtime monitor

https://fop.saj-electric.com/saj/cloudMonitor/deviceInfo/findRawdataPageList image image

I don't find that cloudmonitor link anymore, I think they removed it.

RobbieDemaegdt commented 1 year ago

@faanskit how did you get that api link from the APP? As I want to check those calls to implement them maybe in the code.

faanskit commented 1 year ago

@faanskit how did you get that api link from the APP? As I want to check those calls to implement them maybe in the code.

I intercepted the APP traffic from an (old) ipad using mitmproxy. Reason for using an old ipad was that it's possible to install your certificates to allow to sniff https traffic.

faanskit commented 1 year ago

eSolar_test.zip @robshot, I attached the code I used for trying this out. Add your own username and password.

Please take note that the parameter signature is NOT CORRECT. I still do not know how SAJ are calculating the signature. I 've tried many things, but I suspect they add some salt to the parameters listed under signParams Because of this, the code make use of both WEB based API's and APP based API's.

HTH/Marcus

RobbieDemaegdt commented 1 year ago

Ok that is how you captured the https traffic, gonna check if this also exist for android as I'm not owning an apple device.

Thx for the code, I will have a look at it.

djansen1987 commented 1 year ago

i am still able to find it using the web. It is hidden away for sure. (model: R5-3K-S1)

https://fop.saj-electric.com/saj/cloudMonitor/deviceInfo/toRealDataPage?devicesn=XXXXXXXXXXX

got there by going to: Plant->plantlist image

Clicking the plant name: (a new window opens) image

clicking on inverter: image

then clicking Realtim: image

RobbieDemaegdt commented 1 year ago

Ok thanks to show me where I can find that data.

I might look into it but it's currently very busy here.

faanskit commented 1 year ago

Dumping the code for this, made for a parallel integration (#45). Some rewrite to match this integration is obviously required, but here goes:

def web_get_device_page_list(session, plant_info):
    """
    Function to retrieve platUid from WEB Portal, requires web_authenticate
    """
    if session is None:
        raise ValueError("Missing session identifier trying to obain plants")

    headers = {
        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
    }

    try:
        chart_month = datetime.date.today().strftime("%Y-%m")
        url = f"{BASE_URL_WEB}/cloudMonitor/device/findDevicePageList"
        payload = f"officeId=1&pageNo=&pageSize=&orderName=1&orderType=2&plantuid=&deviceStatus=&localDate={chart_month}&localMonth={chart_month}"
        response = session.post(
            url=f"{BASE_URL_WEB}/cloudMonitor/device/findDevicePageList",
            data=payload,
            timeout=WEB_TIMEOUT,
        )
        response.raise_for_status()
        device_list = response.json()["list"]

        for device in device_list:
            url = f"{BASE_URL_WEB}/cloudMonitor/deviceInfo/findRawdataPageList"
            payload = f"deviceSn={device['devicesn']}&deviceType=0&timeStr={datetime.date.today().strftime('%Y-%m-%d')}"
            response = session.post(
                url, headers=headers, data=payload, timeout=WEB_TIMEOUT
            )
            response.raise_for_status()
            kit = response.json()
            if len(kit["list"]) > 0:
                device.update({"pv1_volt": kit["list"][0]["pV1Volt"]})
                device.update({"pv2_volt": kit["list"][0]["pV2Volt"]})
                device.update({"pv3_volt": kit["list"][0]["pV3Volt"]})

                device.update({"pv1_curr": kit["list"][0]["pV1Curr"]})
                device.update({"pv2_curr": kit["list"][0]["pV2Curr"]})
                device.update({"pv3_curr": kit["list"][0]["pV3Curr"]})

                device.update({"l1_volt": kit["list"][0]["rGridVolt"]})
                device.update({"l2_volt": kit["list"][0]["sGridVolt"]})
                device.update({"l3_volt": kit["list"][0]["tGridVolt"]})

                device.update({"l1_curr": kit["list"][0]["rGridCurr"]})
                device.update({"l2_curr": kit["list"][0]["sGridCurr"]})
                device.update({"l3_curr": kit["list"][0]["tGridCurr"]})

                device.update({"l1_freq": kit["list"][0]["rGridFreq"]})
                device.update({"l2_freq": kit["list"][0]["sGridFreq"]})
                device.update({"l3_freq": kit["list"][0]["tGridFreq"]})
            else:
                device.update({"pv1_volt": float(0)})
                device.update({"pv2_volt": float(0)})
                device.update({"pv3_volt": float(0)})

                device.update({"pv1_curr": float(0)})
                device.update({"pv2_curr": float(0)})
                device.update({"pv3_curr": float(0)})

                device.update({"l1_volt": float(0)})
                device.update({"l2_volt": float(0)})
                device.update({"l3_volt": float(0)})

                device.update({"l1_curr": float(0)})
                device.update({"l2_curr": float(0)})
                device.update({"l3_curr": float(0)})

                device.update({"l1_freq": float(0)})
                device.update({"l2_freq": float(0)})
                device.update({"l3_freq": float(0)})

        for plant in plant_info["plantList"]:
            kit = []
            for device in device_list:
                if device["devicesn"] in plant["plantDetail"]["snList"]:
                    kit.append(device)
            plant.update({"kitList": kit})

    except requests.exceptions.HTTPError as errh:
        raise requests.exceptions.HTTPError(errh)
    except requests.exceptions.ConnectionError as errc:
        raise requests.exceptions.ConnectionError(errc)
    except requests.exceptions.Timeout as errt:
        raise requests.exceptions.Timeout(errt)
    except requests.exceptions.RequestException as errr:
        raise requests.exceptions.RequestException(errr)
faanskit commented 1 year ago

Hey @djansen1987 and @robshot ,

I just want to raise a "warning" using the https://fop.saj-electric.com/saj/cloudMonitor/deviceInfo/findRawdataPageList API.

This API does not give access to the current data, it merely provides information about the last reported meter value. This API is obtaining historical values. As soon as the inverter stops to generate power, this API is "stuck" with the last reported value.

Therefore, from the point when the system went off-line, i.e. runningState == 3, these values should be set to either 0 or None.

In the APP, at this stage - the system reports N/A.

/Marcus

djansen1987 commented 1 year ago

Makes sense, this might be different for a sec or H1 installation as this would stay online. Thanks for all the feedback

djansen1987 commented 1 year ago

https://github.com/faanskit/ha-esolar