Skip to content

Instantly share code, notes, and snippets.

@SplinterII
Last active May 13, 2023 06:56
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save SplinterII/dbd6621ce479194571fde919dcb172d1 to your computer and use it in GitHub Desktop.
Save SplinterII/dbd6621ce479194571fde919dcb172d1 to your computer and use it in GitHub Desktop.
Goodwe SEMS portal Solar panel data to Node-RED

** THIS NODE IS NO LONGER MAINTAINED **

UPDATE 1 nov 2021: Mind the power station ID!

The Goodwe PV solar panel inverters come with a webportal which show all it's performance. This Node-RED flow outputs a (small) part of the data available from the Goodwe API. It's using the version 2 API.

Outputs from this flow include:

  • fysical address of the converter,
  • name of the station,
  • total power generated and
  • power generated today.

Much more data is readily available.

In the fuction nodes the flow needs these inputs:

  • The loginname to the portal,
  • The corresponding password (both set in the "Get ID token"-function node).
  • The power station ID, as found in the URL when logged into the portal.

The power station ID is set in the "Get station output"-function node. To find the ID for your installation, login to the normal SEMS webportal. Once logged in, the last part of the URL in your webbrowser is the ID. Copy this part into the script for the variable "psid".

Among other sources, this helped most: DiedB/Homey-SolarPanels#28 (comment)

The first HTTP API call returns a session login token ID. This token is used in the second call, which returns all data. It seems the word "token" is used differently throughout the API, which can be confusing. The token is session-bound and expires in ? time. Replies from the "Request station" call can be delayed upto 10-ish seconds.

[
{
"id": "8d9517ae094a5e8f",
"type": "tab",
"label": "Query SEMS portal for Goodwe Inverter",
"disabled": false,
"info": ""
},
{
"id": "7666019830c94009",
"type": "debug",
"z": "8d9517ae094a5e8f",
"name": "Token Details",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 740,
"y": 200,
"wires": []
},
{
"id": "35b7837b9e219f92",
"type": "http request",
"z": "8d9517ae094a5e8f",
"name": "Request token",
"method": "POST",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"x": 480,
"y": 240,
"wires": [
[
"7666019830c94009",
"2f2802572b0a16fa"
]
]
},
{
"id": "20f12cc0ec2b10df",
"type": "function",
"z": "8d9517ae094a5e8f",
"name": "Get ID token from login",
"func": "// Credentials as used for login to the Semsportal at https://www.semsportal.com/\n\naccount = \"YOUR_SEMS_USERNAME\";\npwd = \"YOUR_SEMS_PASSWORD\";\n\n// ---------- no changes needed below ----------------------------\nloginPayload = { 'account': account, 'pwd': pwd };\n\n// It's a given, likely from the API documentation?\ntoken = '{\"client\": \"ios\", \"version\": \"v2.1.0\", \"language\": \"en\"}';\n\nglobal_url = 'https://www.semsportal.com/api/';\nheaders = { 'token': token };\n\nmsg.url = global_url + \"v2/Common/CrossLogin\";\nmsg.headers = headers;\nmsg.payload = loginPayload;\nreturn msg;\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 430,
"y": 180,
"wires": [
[
"35b7837b9e219f92"
]
]
},
{
"id": "6b960b40ef6d9b5c",
"type": "inject",
"z": "8d9517ae094a5e8f",
"name": "",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 220,
"y": 180,
"wires": [
[
"20f12cc0ec2b10df"
]
]
},
{
"id": "2f2802572b0a16fa",
"type": "function",
"z": "8d9517ae094a5e8f",
"name": "Get station output",
"func": "if(msg==\"no response from server\") { return; }\n\n// Power station ID can be found in the last part of the URL of the SEMSportlal, once logged in\n\n// Power station ID can be found in the last part of the URL of the SEMSportlal, once logged in\npsid = 'd0eac052-96....GET YOUR OWN CODE HERE....d0';\n\n\n// -------------- Generic below ---------------------------------\n\n// https://github.com/DiedB/Homey-SolarPanels/issues/28\n\n// Get the relevant response from the login call: \nvar uid = msg.payload.data.uid; // User ID\nvar id_token = msg.payload.data.token; // Time-bound token from login\nvar timestamp = msg.payload.data.timestamp;\n\n// Warning: The API uses 'token' with different meanings throughout the communication\ntoken = '{\"uid\": \"'+ uid + '\", \"timestamp\": '+ timestamp+ ', \"token\": \"'+ id_token + '\", \"client\": \"ios\", \"version\": \"v2.0.4\", \"language\": \"en\"}';\nheaders = { 'User-Agent': 'JH', 'Token': token }\n\nglobal_url = 'https://www.semsportal.com/api/'\nmsg.url = global_url + \"v2/PowerStation/GetMonitorDetailByPowerstationId\";\n\nmsg.headers = headers;\n\npayload = {'powerStationId': psid};\nmsg.payload = payload;\n\nreturn msg;\n\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 570,
"y": 300,
"wires": [
[
"c63f21c5f355ef7a",
"abc27ec229e67dd6"
]
]
},
{
"id": "ed7ace700ba8677a",
"type": "debug",
"z": "8d9517ae094a5e8f",
"name": "Data",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 890,
"y": 420,
"wires": []
},
{
"id": "c63f21c5f355ef7a",
"type": "http request",
"z": "8d9517ae094a5e8f",
"name": "Request station",
"method": "POST",
"ret": "obj",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"x": 620,
"y": 360,
"wires": [
[
"ebf19d5d3f71fc32",
"f6c3c5bea5812317",
"98187240f169989f"
]
]
},
{
"id": "ebf19d5d3f71fc32",
"type": "function",
"z": "8d9517ae094a5e8f",
"name": "Get values",
"func": "// Get values as shown in the webportal:\n\n// Stationname, top left as displayed in the dropdown (not sure how more stations handle....?)\nvar station_name = msg.payload.data.info.stationname;\n\n// Fysical address\nvar address = msg.payload.data.info.address;\n\n// Total power als shown on the webportal left below today's production\nvar total_power = msg.payload.data.kpi.total_power;\n\n// Today's power als in the circle middle down the webportal main page\nvar day_power = msg.payload.data.kpi.power;\n\nmsg = {};\n\nmsg.payload = {'station_name': station_name, 'address': address, 'total_power': total_power, 'day_power': day_power};\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 710,
"y": 420,
"wires": [
[
"ed7ace700ba8677a"
]
]
},
{
"id": "f6c3c5bea5812317",
"type": "debug",
"z": "8d9517ae094a5e8f",
"name": "Station Info",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 830,
"y": 360,
"wires": []
},
{
"id": "98187240f169989f",
"type": "change",
"z": "8d9517ae094a5e8f",
"name": "",
"rules": [
{
"t": "set",
"p": "payload",
"pt": "msg",
"to": "payload.data.inverter[0].d",
"tot": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 840,
"y": 300,
"wires": [
[
"a962b60aa198dfb0"
]
]
},
{
"id": "a962b60aa198dfb0",
"type": "debug",
"z": "8d9517ae094a5e8f",
"name": "Data",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 990,
"y": 300,
"wires": []
},
{
"id": "abc27ec229e67dd6",
"type": "debug",
"z": "8d9517ae094a5e8f",
"name": "Token Details",
"active": false,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 760,
"y": 240,
"wires": []
}
]
@craigcurtin-dev
Copy link

Hey Did a bit of an update for you as they have moved the portal to the new API and different structure. Here is the JSON below

[{"id":"e82b4b0.2a922b8","type":"tab","label":"Query SEMS portal for Goodwe Inverter","disabled":false,"info":""},{"id":"547c4204.8607bc","type":"debug","z":"e82b4b0.2a922b8","name":"Token Details","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":720,"y":240,"wires":[]},{"id":"7f1daadf.2b67bc","type":"http request","z":"e82b4b0.2a922b8","name":"Request token","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":480,"y":240,"wires":[["547c4204.8607bc","4865906.e8143f"]]},{"id":"6a0ae223.adb52c","type":"function","z":"e82b4b0.2a922b8","name":"Get ID token from login","func":"// Credentials as used for login to the Semsportal at https://www.semsportal.com/\naccount = \"YourSEMSLoginNamehere";\npwd = \"YourSEMSPasswordhere\";\n\n// ---------- no changes needed below ----------------------------\nloginPayload = { 'account': account, 'pwd': pwd };\n\n// It's a given, likely from the API documentation?\ntoken = '{\"client\": \"ios\", \"version\": \"v2.1.0\", \"language\": \"en\"}';\n\nglobal_url = 'https://www.semsportal.com/api/';\nheaders = { 'token': token };\n\nmsg.url = global_url + \"v2/Common/CrossLogin\";\nmsg.headers = headers;\nmsg.payload = loginPayload;\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":430,"y":180,"wires":[["7f1daadf.2b67bc"]]},{"id":"dbd14dd3.8b8fe8","type":"inject","z":"e82b4b0.2a922b8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":220,"y":180,"wires":[["6a0ae223.adb52c"]]},{"id":"4865906.e8143f","type":"function","z":"e82b4b0.2a922b8","name":"Get station output","func":"// Power station ID can be found in the last part of the URL of the SEMSportlal, once logged in\npsid = '3d5ae67f-4126-49cf-a75b-c9f389db6112';\n\n\n// -------------- Generic below ---------------------------------\n\n// https://github.com/DiedB/Homey-SolarPanels/issues/28\n\n// Get the relevant response from the login call: \nvar uid = msg.payload.data.uid; // User ID\nvar id_token = msg.payload.data.token; // Time-bound token from login\nvar timestamp = msg.payload.data.timestamp;\n\n// Warning: The API uses 'token' with different meanings throughout the communication\ntoken = '{\"uid\": \"'+ uid + '\", \"timestamp\": '+ timestamp+ ', \"token\": \"'+ id_token + '\", \"client\": \"ios\", \"version\": \"v2.0.4\", \"language\": \"en\"}';\nheaders = { 'User-Agent': 'JH', 'Token': token }\n\nglobal_url = 'https://www.semsportal.com/api/'\nmsg.url = global_url + \"v2/PowerStation/GetMonitorDetailByPowerstationId\";\n\nmsg.headers = headers;\n\npayload = {'powerStationId': psid};\nmsg.payload = payload;\n\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":300,"wires":[["391e2baf.eb6b34"]]},{"id":"efb25403.f9b5a","type":"debug","z":"e82b4b0.2a922b8","name":"Data","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":890,"y":420,"wires":[]},{"id":"391e2baf.eb6b34","type":"http request","z":"e82b4b0.2a922b8","name":"Request station","method":"POST","ret":"obj","paytoqs":"ignore","url":"","tls":"","persist":false,"proxy":"","authType":"","x":620,"y":360,"wires":[["fedcd784.eee18","734da56e.753b6c","8901911b.bafe8"]]},{"id":"fedcd784.eee18","type":"function","z":"e82b4b0.2a922b8","name":"Get values","func":"// Get values as shown in the webportal:\n\n// Stationname, top left as displayed in the dropdown (not sure how more stations handle....?)\nvar station_name = msg.payload.data.info.stationname;\n\n// Fysical address\nvar address = msg.payload.data.info.address;\n\n\n// Total power als shown on the webportal left below today's production\nvar total_power = msg.payload.data.kpi.total_power;\n\n// Today's power als in the circle middle down the webportal main page\nvar day_power = msg.payload.data.kpi.power;\n\nmsg = {};\n\nmsg.payload = {'station_name': station_name, 'address': address, 'total_power': total_power, 'day_power': day_power};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":710,"y":420,"wires":[["efb25403.f9b5a"]]},{"id":"734da56e.753b6c","type":"debug","z":"e82b4b0.2a922b8","name":"Station Info","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":830,"y":360,"wires":[]},{"id":"8901911b.bafe8","type":"change","z":"e82b4b0.2a922b8","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.data.inverter[0].d","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":540,"wires":[["ac1129ba.0412e8"]]},{"id":"ac1129ba.0412e8","type":"debug","z":"e82b4b0.2a922b8","name":"Data","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":910,"y":540,"wires":[]}]

@SplinterII
Copy link
Author

Thanks man! Made a new file for version 2, except for the copy-paste issues wrt "\n", all is working the same as expected!
Any clue what's new in v2?

Thanks again!

@craigcurtin-dev
Copy link

craigcurtin-dev commented Sep 2, 2021 via email

@Pianz
Copy link

Pianz commented Nov 1, 2021

Hello, i try to do the same thing but i get this message : "TypeError: Cannot read property 'info' of null" from the "Get Value" node ...
The debug node named "Station info" say me that "The authorization has expired, please log in again."

Can you help me please ?

@SplinterII
Copy link
Author

SplinterII commented Nov 1, 2021

Hi Pianz, if you copy the code from this page to a flow in NodeRED, what goes wrong?
https://flows.nodered.org/flow/dbd6621ce479194571fde919dcb172d1
De code above this page has issues with the "//" which do not copy-paste well to NodeRED. If you use the link, all should be fine (it is in my case anyway...). Goodluck!

@Pianz
Copy link

Pianz commented Nov 1, 2021

Thanks a lot for your fast response.
I have try with the link you provide but have the same error message. "The authorization has expired, please log in again."
For what I understand it's a login or authorization problem. Any idea ?

Screenshot 2021-11-01 at 15-39-55 Node-RED 192 168 5 12
.

@SplinterII
Copy link
Author

The flow works in 3 steps: first you 'login' to the portal (the "request token" node), which gives you a specific Token/ID, which is then used to retrieve the exact values. It seems the login part fails (a token is not returned from the Request token node). I pressume you checked your email and password, but maybe you do not have the appropriate access rights for the portal? You should be able to login to https://www.semsportal.com/home/login by using the exact same credentials. Can you?

@Pianz
Copy link

Pianz commented Nov 1, 2021

Yes, I understand the 3 steps.
I can login with my credentials.

The first step is working and I receive a token like : "4744e4bd4cc2f2a2694a53272bf8a2ff"
The second step isn't working correctly and send me the error message : "The authorization has expired, please log in again."
So the third step can't work because the second step return a "null". (TypeError: Cannot read property 'info' of null)

I really don't understand what's goin' on ...

Appreciate your help
Screenshot 2021-11-01 at 16-52-09 État de la centrale électrique - SEMS
.

@SplinterII
Copy link
Author

SplinterII commented Nov 1, 2021

No problem helping you, i like you're trying my flow. Unfortunately I'm not sure how to help you more specifally, it would require me to repeat your error, but I can't. Pressume you've seen this: https://github.com/JanJansen47/Goodwe-SEMS-simple. It's (if I remember correctly) the source of my script, maybe you can find help there?
Did you set the correct "psid" variable?
You have to update it in the "Get Station output"-function node. Set it with the ID from the last part of the url once you logged in to the SEMS web portal. It could explain the expired error you're getting...
Nice installation you have there by the way!

@SplinterII
Copy link
Author

SplinterII commented Nov 6, 2021 via email

@Pianz
Copy link

Pianz commented Nov 7, 2021

Thank you very much SplinterII !
Sorry for the late response, I'm working on this project in my spare time (weekends).
Yes, I have set the proper PSID in the "Get Station output"-function node.
No, I didn't succeed and I don't know which side to look for.
Could this be because I have several production facilities linked to my goodwe account ?

@SplinterII
Copy link
Author

SplinterII commented Nov 10, 2021 via email

@Robbie98
Copy link

Hi SplinterII, this is awesome and already looking to import more data, how would i go about doing this? things such as current power and voltages across phases?

Thanks

@SplinterII
Copy link
Author

Hi Robbie, there's a lot more data to be discovered, but frankly I haven't researched it more then what you see in the code. I'm even not sure where to find API documentation. Afraid I can't help you much further....

@Tinchen9b
Copy link

Hello,
Since few days I am receiving;

JSON parse error
TypeError: Cannot read property 'info' of undefined

anyone else as well?

@SplinterII
Copy link
Author

SplinterII commented Dec 2, 2022

Sorry, no clue, I'm not using the code anymore at all. But it does seem to me like a change in the API. Also at this point no clue where to find that....

Edit: ok, here we are: http://www.goodwe-power.com:82/swagger/ui/index and select version 3. My code was for version 2, which might be broken now in some way. No clue whether v3 is live since only a few days...

@Tinchen9b
Copy link

Tinchen9b commented Dec 4, 2022

Add

Change this line to :
global_url = 'http://www.goodwe-power.com:82/api/';
and in the node: "Request Token" the return to : "a parsed JSON object"
image

and it should work again.
.. at least mine is working again :)

Thank you for pointing me to the api documentation. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment