commit
c0dc90ca79
7 changed files with 644 additions and 0 deletions
@ -0,0 +1,4 @@ |
|||
__pycache__/ |
|||
*.pyc |
|||
.env |
|||
.vscode/ |
|||
@ -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" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
] |
|||
} |
|||
@ -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() |
|||
@ -0,0 +1,5 @@ |
|||
{ |
|||
"ps_id": 5425899, |
|||
"device_name": "PV_Siegel", |
|||
"ps_key": "5425899_11_0_0" |
|||
} |
|||
@ -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)) |
|||
@ -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…
Reference in new issue