Closed faanskit closed 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
Hi,
Such feature will be great!
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
I'm currently developing a little bit on this integration. If I have some time over I will look into this.
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
![]()
I don't find that cloudmonitor link anymore, I think they removed it.
@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 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.
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
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.
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
Clicking the plant name: (a new window opens)
clicking on inverter:
then clicking Realtim:
Ok thanks to show me where I can find that data.
I might look into it but it's currently very busy here.
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)
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
Makes sense, this might be different for a sec or H1 installation as this would stay online. Thanks for all the feedback
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
anduserUid
is required. To obtain the token, login with this endpoint and the password hashed with MD5.To obtain additional data, use this request. Take note that
DeviceSN
was already fetched with/getPlantDetailChart2
or/getPlantDetailInfo
This will produce the following output:
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:
The following endpoints used by the APP still needs to be figured out:
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).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.