Browse Source

Initial commit

main
susiederkin 1 month ago
commit
c0dc90ca79
  1. 4
      .gitignore
  2. 287
      SunGrowData.txt
  3. 13
      launch.json
  4. 92
      main.py
  5. 5
      plant_state.json
  6. 202
      sungrow_api.py
  7. 41
      sungrow_state.py

4
.gitignore

@ -0,0 +1,4 @@
__pycache__/
*.pyc
.env
.vscode/

287
SunGrowData.txt

@ -0,0 +1,287 @@
API Status: 200
API Body: {
"req_serial_num":"202604069c4343c38cc94dfb3a71c7fc",
"result_code":"1",
"result_msg":"success",
"result_data":{
"pageList":[
{
"total_energy":{
"unit":"MWh",
"value":"43.13"
},
"alarm_count":0,
"latitude":50.56129590941345,
"description":null,
"total_income_update_time":null,
"valid_flag":1,
"curr_power":{
"unit":"W",
"value":"0"
},
"ps_fault_status":3,
"co2_reduce_update_time":null,
"install_date":"2024-02-07 08:13:34",
"build_status":2,
"today_energy_update_time":"2026-04-06T05:01:52+08:00",
"year_income_update_time":null,
"total_energy_update_time":"2026-04-06T05:01:52+08:00",
"ps_type":5,
"longitude":9.72615326343812,
"total_capcity_update_time":"2024-03-19T15:51:21+08:00",
"equivalent_hour_update_time":"2026-04-06T05:01:52+08:00",
"ps_name":"PV_Siegel",
"co2_reduce_total":{
"unit":"kg",
"value":"42936"
},
"curr_power_update_time":"2026-04-06T05:01:52+08:00",
"today_income":{
"unit":"",
"value":"--"
},
"grid_connection_status":0,
"equivalent_hour":{
"unit":"Hour",
"value":"2.95"
},
"co2_reduce_total_update_time":null,
"ps_location":"Geisaer Weg 13, 36100 Petersberg, Deutschland",
"total_income":{
"unit":"EUR",
"value":"6901"
},
"total_capcity":{
"unit":"kWp",
"value":"21.93"
},
"share_type":"0",
"year_income":{
"unit":"EUR",
"value":"642.409"
},
"ps_current_time_zone":"GMT+2",
"today_income_update_time":null,
"ps_id":5425899,
"grid_connection_time":null,
"connect_type":2,
"today_energy":{
"unit":"kWh",
"value":"64.8"
},
"ps_status":1,
"co2_reduce":{
"unit":"kg",
"value":"0"
},
"fault_count":0
}
],
"rowCount":1
}
}
API Status: 200
API Body: {
"req_serial_num":"2026040649654a0cb04020d10860cc31",
"result_code":"1",
"result_msg":"success",
"result_data":{
"pageList":[
{
"type_name":"Batterie",
"ps_key":"5425899_43_2_1",
"firmware_version_info":{
"bat_version":"SBRBCU-S_22011.01.21"
},
"device_type":43,
"factory_name":"SUNGROW",
"uuid":3799231,
"grid_connection_date":"2024-02-07 16:42:54",
"device_name":"Battery (WR Master)",
"dev_fault_status":4,
"device_model_id":355481,
"communication_dev_sn":"B2341313692",
"device_model_code":"SBR160",
"chnnl_id":1,
"rel_time":"2024-02-07 16:42:54",
"device_sn":"S2309190060",
"dev_status":"1",
"rel_state":1,
"device_code":2,
"ps_id":5425899
},
{
"type_name":"Wechselrichter",
"ps_key":"5425899_1_1_2",
"firmware_version_info":{
"sdsp_version":"SUBCTL-S_04011.01.01",
"mdsp_version":"BERYL-S_03011.01.74",
"lcd_version":"BERYL-S_01011.01.39",
"afci_version":"AFD_06001.02.03"
},
"device_type":1,
"factory_name":"SUNGROW",
"uuid":3798939,
"grid_connection_date":"2024-02-07 15:17:44",
"device_name":"WR Slave",
"dev_fault_status":4,
"device_model_id":742,
"communication_dev_sn":"B2321768031",
"device_model_code":"SG10RT",
"chnnl_id":2,
"rel_time":"2024-02-07 15:17:44",
"device_sn":"A2322221161",
"dev_status":"1",
"rel_state":1,
"device_code":1,
"ps_id":5425899
},
{
"type_name":"Kommunikationsmodul",
"ps_key":"5425899_22_247_2",
"firmware_version_info":{
"m_version":"WINET-SV200.001.00.P038"
},
"device_type":22,
"factory_name":"SUNGROW",
"uuid":3798938,
"grid_connection_date":"2024-02-07 15:17:44",
"device_name":"Communication Module2",
"dev_fault_status":4,
"device_model_id":1361,
"communication_dev_sn":"B2321768031",
"device_model_code":"WiNet-S",
"chnnl_id":2,
"rel_time":"2024-02-07 15:17:44",
"device_sn":"B2321768031",
"dev_status":"1",
"rel_state":1,
"device_code":247,
"ps_id":5425899
},
{
"type_name":"Hybrid (speicherfähig)",
"ps_key":"5425899_14_1_1",
"firmware_version_info":{
"sdsp_version":"SUBCTL-S_04011.01.01",
"mdsp_version":"SAPPHIRE-H_03011.51.04",
"lcd_version":"SAPPHIRE-H_01011.51.05",
"bat_version":"SBRBCU-S_22011.01.21"
},
"device_type":14,
"factory_name":"SUNGROW",
"uuid":3798934,
"grid_connection_date":"2024-02-07 15:14:36",
"device_name":"WR Master",
"dev_fault_status":4,
"device_model_id":366316,
"communication_dev_sn":"B2341313692",
"device_model_code":"SH10RT-V112",
"chnnl_id":1,
"rel_time":"2024-02-07 15:14:36",
"device_sn":"A2341306461",
"dev_status":"1",
"rel_state":1,
"device_code":1,
"ps_id":5425899
},
{
"type_name":"Kommunikationsmodul",
"ps_key":"5425899_22_247_1",
"firmware_version_info":{
"m_version":"WINET-SV200.001.00.P038"
},
"device_type":22,
"factory_name":"SUNGROW",
"uuid":3798933,
"grid_connection_date":"2024-02-07 15:14:35",
"device_name":"Communication Module1",
"dev_fault_status":4,
"device_model_id":1361,
"communication_dev_sn":"B2341313692",
"device_model_code":"WiNet-S",
"chnnl_id":1,
"rel_time":"2024-02-07 15:14:35",
"device_sn":"B2341313692",
"dev_status":"1",
"rel_state":1,
"device_code":247,
"ps_id":5425899
}
],
"rowCount":5
}
}
API Status: 200
API Body: {
"req_serial_num":"20260406cbc64e2ca6dab1c63d4073e3",
"result_code":"1",
"result_msg":"success",
"result_data":{
"pageList":[
{
"type_name":"Anlage",
"ps_key":"5425899_11_0_0",
"firmware_version_info":{},
"device_type":11,
"factory_name":null,
"uuid":3798932,
"grid_connection_date":"2024-02-07 15:13:34",
"device_name":"PV_Siegel",
"dev_fault_status":4,
"device_model_id":null,
"communication_dev_sn":null,
"device_model_code":null,
"chnnl_id":0,
"rel_time":null,
"device_sn":null,
"dev_status":"1",
"rel_state":1,
"device_code":0,
"ps_id":5425899
}
],
"rowCount":1
}
}
API Status: 200
API Body: {
"req_serial_num":"202604064c60475f975eca70db983580",
"result_code":"1",
"result_msg":"success",
"result_data":{
"fail_ps_key_list":[],
"device_point_list":[
{
"device_point":{
"ps_key":"5425899_11_0_0",
"device_sn":null,
"dev_status":1,
"p83009":"33400.0",
"p83119":"18900.0",
"p83118":"39400.0",
"p83097":"5900.0",
"p83072":"25400.0",
"uuid":3798932,
"p83022":"64800.0",
"p83033":"0.0",
"p83252":"0.557",
"device_name":"PV_Siegel",
"dev_fault_status":4,
"ps_id":5425899,
"communication_dev_sn":null,
"device_time":"20260405234500"
}
}
]
}
}

13
launch.json

@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: main.py",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env"
}
]
}

92
main.py

@ -0,0 +1,92 @@
from sungrow_api import get_device_list, login, call_api, get_power_station_list, get_device_list, get_device_realtime_data, ensure_success, pretty_print
from sungrow_state import save_state
def main():
print("Logging in...")
token = login()
print("Token:", token[:12], "...")
# 1) Get plant list
plant_list = get_power_station_list(token, debug=True)
print("Plant List Response:")
pretty_print(plant_list)
# Validate before accessing
ensure_success(plant_list)
# 2) Extract ps_id
ps_id = plant_list["result_data"]["pageList"][0]["ps_id"]
print("Using ps_id:", ps_id)
# 3) Query device list
device_list = get_device_list(
token=token,
ps_id=ps_id,
cur_page=1,
size=20,
is_virtual_unit="0", # 0 physical devices, 1 virtual
#device_type_list=[1, 3, 11], # inverter, grid point, plant
#rel_state="1", # only claimed devices
is_get_firmware_version="1", # 1: include firmware info
debug=True
)
# 4) Query device list on plant level
device_list = get_device_list(
token=token,
ps_id=ps_id,
cur_page=1,
size=20,
is_virtual_unit="1", # 0 physical devices, 1 virtual
#device_type_list=[1, 3, 11], # inverter, grid point, plant
#rel_state="1", # only claimed devices
is_get_firmware_version="0", # 1: include firmware info
debug=True
)
# Extract relevant data from the first device in the list (assuming it's the plant-level virtual device)
device_info = device_list["result_data"]["pageList"][0]
ps_key = device_info["ps_key"]
device_name = device_info["device_name"]
# Save them persistently
save_state(ps_key=ps_key, device_name=device_name)
# 4) Query realtime data on plant level
realtime = get_device_realtime_data(
token=token,
point_id_list = [
"83022", # Daily Yield of Plant "64800.0" ok
"83033", # Plant Power "0.0"
#"83006", # Meter Daily Yield
#"83011", # Meter E-daily Consumption
#"83052", # Load Power
"83072", # Feed-in Energy Today "25400.0" ok
"83252", # Battery Level (SOC) "0.557" ok
#"83129", # Battery SOC
"83009", # Inverter Daily Yield "33400.0" ok, nur Inverter "WR Slave"
"83097", # Daily Direct Energy Consumption "5900.0" ??
"83118", # Daily Load Consumption "39400.0" ok
"83119" # Daily Feed-in Energy (PV) "18900.0" ??
],
device_type=11,
ps_key_list=["5425899_11_0_0"],
debug=True
)
print("Device List Response:")
pretty_print(device_list)
#print("Response:")
#print(data)
if __name__ == "__main__":
main()

5
plant_state.json

@ -0,0 +1,5 @@
{
"ps_id": 5425899,
"device_name": "PV_Siegel",
"ps_key": "5425899_11_0_0"
}

202
sungrow_api.py

@ -0,0 +1,202 @@
import os
import time
import uuid
import requests
import json
from dotenv import load_dotenv
from sungrow_state import load_ps_id
BASE_URL = os.getenv("SUNGROW_BASE_URL")
USERNAME = os.getenv("SUNGROW_USERNAME")
PASSWORD = os.getenv("SUNGROW_PASSWORD")
APPKEY = os.getenv("SUNGROW_APPKEY")
KEY = os.getenv("SUNGROW_KEY")
def login():
url = f"{BASE_URL}/openapi/login"
headers = {
"Content-Type": "application/json;charset=UTF-8",
"sys_code": "901",
"x-access-key": KEY
}
body = {
"user_account": USERNAME,
"user_password": PASSWORD,
"appkey": APPKEY,
"lang": "_de_DE"
}
response = requests.post(url, json=body, headers=headers, timeout=10)
print("Login Status:", response.status_code)
print("Login Body:", response.text)
response.raise_for_status()
data = response.json()
if data.get("result_code") != "1":
raise Exception(f"Login failed: {data}")
return data["result_data"]["token"]
def call_api(endpoint: str, token: str, params: dict = None, debug: bool = False):
if params is None:
params = {}
# Auto-inject ps_id if requested
if "ps_id" in params and params["ps_id"] is None:
stored_ps_id = load_ps_id()
if stored_ps_id is None:
raise ValueError("ps_id was requested but no stored ps_id found.")
params["ps_id"] = stored_ps_id
url = f"{BASE_URL}{endpoint}"
headers = {
"Content-Type": "application/json;charset=UTF-8",
"sys_code": "901",
"x-access-key": KEY
}
base_body = {
"appkey": APPKEY,
"token": token,
"lang": "_de_DE",
"timestamp": str(int(time.time() * 1000)),
"nonce": uuid.uuid4().hex
}
body = {**base_body, **params}
# Debug: print full request
if debug:
import json
print("\n=== API REQUEST ===")
print("URL:", url)
print("Headers:", json.dumps(headers, indent=4, ensure_ascii=False))
print("Body:", json.dumps(body, indent=4, ensure_ascii=False))
print("===================\n")
response = requests.post(url, json=body, headers=headers, timeout=10)
# Debug: print full response
if debug:
print("=== API RESPONSE ===")
print("Status:", response.status_code)
print("Body:", response.text)
print("====================\n")
response.raise_for_status()
return response.json()
def get_power_station_list(token: str, cur_page: int = 1, size: int = 10, debug: bool = False):
"""
Wrapper for /openapi/getPowerStationList
"""
params = {
"curPage": cur_page,
"size": size
}
return call_api("/openapi/getPowerStationList", token, params, debug=debug)
def get_device_list(
token: str,
ps_id: int,
cur_page: int = 1,
size: int = 50,
is_virtual_unit: str | None = None,
device_type_list: list | None = None,
rel_state: str | None = None,
is_get_firmware_version: str | None = None,
debug: bool = False
):
"""
Wrapper for /openapi/getDeviceList
Parameters:
ps_id (int): Plant ID (required)
cur_page (int): Page number (required)
size (int): Page size (required)
is_virtual_unit (str): "1" = virtual, "0" = physical
device_type_list (list): e.g. [1, 3, 11]
rel_state (str): "0" = unclaimed, "1" = claimed
is_get_firmware_version (str): "0" = no, "1" = yes
"""
params = {
"ps_id": ps_id,
"curPage": cur_page,
"size": size
}
# Only include optional parameters if they are provided
if is_virtual_unit is not None:
params["is_virtual_unit"] = is_virtual_unit
if device_type_list is not None:
params["device_type_list"] = device_type_list
if rel_state is not None:
params["rel_state"] = rel_state
if is_get_firmware_version is not None:
params["is_get_firmware_version"] = is_get_firmware_version
return call_api("/openapi/getDeviceList", token, params, debug=debug)
def get_device_realtime_data(
token: str,
point_id_list: list,
device_type: int,
ps_key_list: list | None = None,
sn_list: list | None = None,
debug: bool = False
):
"""
Wrapper for /openapi/getDeviceRealTimeData
Required:
point_id_list (list[str])
device_type (int)
Optional (choose one):
ps_key_list (list[str])
sn_list (list[str])
"""
if ps_key_list is None and sn_list is None:
raise ValueError("You must provide either ps_key_list or sn_list.")
params = {
"point_id_list": point_id_list,
"device_type": device_type
}
if ps_key_list is not None:
params["ps_key_list"] = ps_key_list
if sn_list is not None:
params["sn_list"] = sn_list
return call_api("/openapi/getDeviceRealTimeData", token, params, debug=debug)
def ensure_success(response: dict):
if response.get("result_code") != "1":
raise Exception(
f"API error {response.get('result_code')}: {response.get('result_msg')}"
)
def pretty_print(data):
import json
print(json.dumps(data, indent=4, ensure_ascii=False))

41
sungrow_state.py

@ -0,0 +1,41 @@
import json
import os
STATE_FILE = "plant_state.json"
def save_state(ps_id=None, ps_key=None, device_name=None):
state = load_state() or {}
if device_name is not None:
state["device_name"] = device_name
if ps_id is not None:
state["ps_id"] = ps_id
if ps_key is not None:
state["ps_key"] = ps_key
with open(STATE_FILE, "w") as f:
json.dump(state, f, indent=4)
def load_state():
if not os.path.exists(STATE_FILE):
return None
with open(STATE_FILE) as f:
return json.load(f)
def load_ps_id():
state = load_state()
return state.get("ps_id") if state else None
def load_ps_key():
state = load_state()
return state.get("ps_key") if state else None
def load_device_name():
state = load_state()
return state.get("device_name") if state else None
Loading…
Cancel
Save