Initial commit
This commit is contained in:
commit
c0dc90ca79
7 files changed
+644
No files matched your search
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.env
|
||||
.vscode/
|
||||
+287
@@ -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
@@ -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"
|
||||
}
|
||||
+202
@@ -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
|
||||
Reference in new issue
Block a user