diff --git a/README.md b/README.md index f6e1e3c..9cb07d4 100755 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Any methods that may be useful. `api.plant_energy_data(plant_id)` Get energy data for the specified plant. -`api.inverter_list(plant_id)` Get a list of inverters in specified plant. (May be deprecated in the future, since it gets all devices. Use `device_list` instead). +`api.inverter_list(plant_id)` Get a list of inverters in specified plant. (Maybe deprecated in the future, since it gets all devices. Use `device_list` instead). `api.device_list(plant_id)` Get a list of devices in specified plant. @@ -96,6 +96,35 @@ Any methods that may be useful. `api.update_noah_settings(serial_number, setting_type, parameters)` Applies the provided parameters (dictionary or array) for the specified setting on the specified noah device; see 'Noah settings' below for more information +#### Power/Energy chart data + +Methods returning power/energy metrics by time/day/month/year. Depending on your inverter, some endpoints might return invalid/inaccurate data. + +* `api.dashboard_data(plant_id, timespan, date)` + * power (pAC, pPV, (dis)charge) by time for single day + * energy (eAC, (dis)charge) by day/month/year +* `api.plant_power_chart(plant_id, timespan, date)` + * power (pAC) by time for single day + * energy (eAC) by day/month/year +* `api.plant_energy_chart(timespan, date)` + * energy (eAC) by day/month/year +* `api.plant_energy_chart_comparison(timespan, date)` + * energy (eAC) by month/quarter - compare multiple years +* `api.inverter_energy_chart(plant_id, inverter_id, date, timespan)` + * power (pAC, pPV1/2/3/4, vPV1/2/3/4, iPV1/2/3/4, vAC, iAC, fAC, Temp) by time for single day + * energy (eAC, ePV1/2/3/4) by day/month/year +* `api.tlx_data(tlx_id, date, tlx_data_type)` + * power (pAC, pPV) by time for single day + * panel voltage/current by time for single day +* `api.tlx_energy_chart(tlx_id, date, timespan)` + * energy (eAC) by day/month/year +* `api.tlx_energy_prod_cons(plant_id, tlx_id, timespan, date)` + * power (pAC, pPV, (dis)charge) by time for single day + * energy (eAC, (dis)charge) by day/month/year +* `api.mix_detail(mix_id, plant_id, timespan, date)` + * power (pAC, pPV, (dis)charge) by time for single day + * energy (eAC, (dis)charge) by day/month/year + ### Variables Some variables you may want to set. diff --git a/examples/neo_800_example.py b/examples/neo_800_example.py new file mode 100644 index 0000000..884d83d --- /dev/null +++ b/examples/neo_800_example.py @@ -0,0 +1,1776 @@ +import datetime +import getpass +import os +from time import sleep + +# Use system's certificate store +# if your system complains about SSL issues, run `pip install truststore` and uncomment following lines +# import truststore +# truststore.inject_into_ssl() + +import growattServer # noqa: E402 +from growattServer import ( # noqa: E402 + Timespan, + TlxDataTypeNeo, + LanguageCode, +) + +""" +This script will show all data available in the ShinePhone Android App (version 2025-01-31) for a NEO 800M-X +Network calls have been sniffed using BlueStack and HTTP-Toolkit +""" + + +def miniplot( + key_value_dict: dict, + rows_to_plot: int = 6, + max_width: int = 80, + prefix: str = "| ", +): + """hacky function to plot an ascii chart""" + if not key_value_dict: + print(f"| ↑") + print(f"| │ no data to plot") + print(f"| │") + print(f"| └───────────────────→") + return + + key_value = {k: float(v) for k, v in key_value_dict.items()} + x_min, x_max = min(key_value.keys()), max(key_value.keys()) + y_max = max(key_value.values()) + tuples = sorted(key_value.items()) + step = (len(key_value) * 3) // max_width + 1 + print(f"{prefix} ↑") + for row in range(rows_to_plot, 0, -1): + row_string = f"{prefix} │ " + range_start = (y_max / rows_to_plot) * (row - 1) + range_end = (y_max / rows_to_plot) * row + for x, y in tuples[::step]: + if (y <= 0) or (y < range_start): + row_string += " " + elif y >= range_end - ((range_end - range_start) / 2): + row_string += " | " + else: + row_string += " . " + print(row_string) + print(f"{prefix} └" + "─" * (len(key_value) // step) * 3 + "──→") + print(f"{prefix} {x_min} " + " " * (len(key_value) // step) + "..." + " " * (len(key_value) // step) + f"{x_max}") + return + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> User data input - query for username and password +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" + +# Prompt user for username (if not in environment variables) +USER_NAME = os.environ.get("GROWATT_USER") or input("Enter username:") + +# Prompt user to input password (if not in environment variables) +PASSWORD = os.environ.get("GROWATT_PASS") or getpass.getpass("Enter password:") + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> Login to Growatt server API +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" + +api = growattServer.GrowattApi( + add_random_user_id=True, + agent_identifier=os.environ.get("GROWATT_USERAGENT"), # use custom user agent if defined +) + +login_response = api.login_v2(USER_NAME, PASSWORD) + +# remember plant id as we will use it in following requests +PLANT_ID = (login_response.get("data") or [{}])[0].get("plantId") + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Dashboard +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# get weather data +weather_dict = api.weather(language="de") + +# get dashboard energy values +dashboard_energy_dict = api.plant_energy_data_v2(language_code=LanguageCode.de) + +# get chart data +# get chart data - tab "Power|Energy" +dashboard_energy_chart = api.plant_energy_chart( + timespan=Timespan.month, # daily values + # timespan=Timespan.year, # monthly values + # timespan=Timespan.total, # yearly values + date=datetime.date.today(), +) +# get chart data - tab "Power comparison" +dashboard_energy_chart_comparison = api.plant_energy_chart_comparison( + timespan=Timespan.year, # monthly values + # timespan=Timespan.total, # quarterly values + date=datetime.date.today(), +) + +sunshine_time = datetime.datetime.strptime(weather_dict["basic"]["ss"], "%H:%M") - datetime.datetime.strptime( + weather_dict["basic"]["sr"], "%H:%M" +) +online_status = ", ".join( + [f"{k.replace('Num', '').capitalize()}: {v}" for k, v in dashboard_energy_dict["statusMap"].items() if v > 0] +) + +# prepare data for miniplot +plot_energy_chart = dashboard_energy_chart["chartData"] +plot_dashboard_energy_chart_comparison = {} +for k, v in dashboard_energy_chart_comparison["chartData"].items(): + plot_dashboard_energy_chart_comparison[f"{k}_0"] = v[0] + plot_dashboard_energy_chart_comparison[f"{k}_1"] = v[1] + +print('\nTab "Dashboard":') +print( + f""" ++=================================================================================+ +| Dashboard | ++=================================================================================+ +| [{weather_dict["now"]["cond_code"]}] {weather_dict["now"]["cond_txt"]}, {weather_dict["now"]["tmp"]}°C ++---------------------------------------------------------------------------------+ +| Daily power generation: {dashboard_energy_dict['todayValue']} {dashboard_energy_dict['todayUnit']} +| Month: {dashboard_energy_dict['monthValue']} kWh +| Total: {dashboard_energy_dict['totalValue']} kWh +| Current Power: {dashboard_energy_dict['powerValue']} W ++---------------------------------------------------------------------------------+ +| Daily Revenue: {dashboard_energy_dict['todayProfitStr']} +| Monthly Revenue: {dashboard_energy_dict['monthProfitStr']} +| Total: {dashboard_energy_dict['totalProfitStr']} ++---------------------------------------------------------------------------------+ +| PV capacity {dashboard_energy_dict['nominalPowerValue']:0.0f} Wp +| Power station status: {online_status} +| Alarm: {dashboard_energy_dict['alarmValue']} ++---------------------------------------------------------------------------------+ +| Power|Energy +| [DAY][MONTH][YEAR] [2025] +""".strip() +) +miniplot(plot_energy_chart) +print( + f""" ++---------------------------------------------------------------------------------+ +| Power comparison +| [MONTH][QUARTER] [2025] +""".strip() +) +miniplot(plot_dashboard_energy_chart_comparison) +print( + f""" ++---------------------------------------------------------------------------------+ +| CO2 reduced: {dashboard_energy_dict["formulaCo2Vlue"]} kg +| Standard coal saved: {dashboard_energy_dict["formulaCoalValue"]} kg +| Deforestation reduced: {dashboard_energy_dict["treeValue"]} ++---------------------------------------------------------------------------------+ +| [{weather_dict["now"]["cond_code"]}] +| {weather_dict["city"]} +| {weather_dict["now"]["cond_txt"]}, {weather_dict["now"]["tmp"]}°C +| Wind direction: {weather_dict["now"]["wind_dir"]} +| Wind speed: {weather_dict["now"]["wind_spd"]} km/h +| Sunrise: {weather_dict["basic"]["sr"]} +| Sunset: {weather_dict["basic"]["ss"]} +| Length of sunshine: {sunshine_time.total_seconds() // 60 // 60:0.0f}h{sunshine_time.total_seconds() // 60 % 60:02.0f}min ++=================================================================================+ +| (*Dashboard*) ( Plant ) ( Service ) ( Me ) | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# get basic plant energy data +plant_energy_dict = api.plant_energy_data(plant_id=PLANT_ID, language_code=LanguageCode.de) + +# get inverter list data +plant_info_dict = api.plant_info(plant_id=PLANT_ID, language="de") +inv_dict = plant_info_dict["invList"][0] + +# get Power|Energy chart +plant_power_chart_dict = api.plant_power_chart( + plant_id=PLANT_ID, + timespan=Timespan.day, + # timespan=Timespan.month, + # timespan=Timespan.year, + # timespan=Timespan.total, + date=datetime.date.today(), +) + +device_status_mapper = { + "-1": "Disconnected", + "0": "Standby", + "1": "Online", + "2": "Off Grid", + "3": "Fault", + "4": "Maintenance", + "5": "Checking", +} + +print('\nTab "Plant":') +print( + f""" ++=================================================================================+ +| {plant_energy_dict['plantBean']['plantName']} ++=================================================================================+ +| (#) Panel view (#) Panel data +| Address: {plant_energy_dict['plantBean']['plantAddress']} +| PV capacity: {plant_energy_dict['plantBean']['nominalPower']} Wp +| Installation date: {plant_energy_dict['plantBean']['createDateText']} ++---------------------------------------------------------------------------------+ +| [{plant_energy_dict['weatherMap']['cond_code']}] {plant_energy_dict['weatherMap']['cond_txt']}, {float(plant_energy_dict['weatherMap']['tmp']):0.1f}°C +| Today: {float(plant_energy_dict['todayValue']):5.1f} kWh +| Generation this month: {float(plant_energy_dict['monthValue']):5.1f} kWh +| Total power generation: {float(plant_energy_dict['totalValue']):5.1f} kWh ++---------------------------------------------------------------------------------+ +| Current Power: {plant_energy_dict['powerValue']} W ++---------------------------------------------------------------------------------+ +| Power|Energy +| [Hour] [DAY] [MONTH] [YEAR] +| (<) {datetime.date.today()} (>) +""".strip() +) +miniplot(plant_power_chart_dict) +print( + f""" ++---------------------------------------------------------------------------------+ +| My device list > +| {inv_dict['deviceAilas'] or inv_dict['deviceSn']} {device_status_mapper.get(inv_dict['deviceStatus'], "Unknown")} +| Power: {float(inv_dict['power']):4.0f} W Today: {inv_dict['eToday']} kWh +| Data logger: {inv_dict['datalogSn']} ++---------------------------------------------------------------------------------+ +| CO2 reduced: {plant_energy_dict['formulaCo2Vlue']} kg +| Coal saved: {plant_energy_dict['formulaCoalValue']} kg +| Deforestation reduced: {plant_energy_dict['treeValue']} ++=================================================================================+ +| ( Dashboard ) (*Plant*) ( Service ) ( Me ) | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> Plant list (button in upper left) +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +plant_list_dicts = api.plant_list_two(language_code=LanguageCode.de, page_size=5) + +cnt_all = len(plant_list_dicts) +cnt_online = len([x for x in plant_list_dicts if x["status"] == 0]) +cnt_offline = len([x for x in plant_list_dicts if x["status"] == 1]) +cnt_fault = cnt_all - cnt_online - cnt_offline + +print('\nTab "Plant" -> Plant list:') +print( + f""" ++=================================================================================+ +| Plant list | ++=================================================================================+ +| [#] (🔍Search ______________________ x) | ++---------------------------------------------------------------------------------+ +| All ({cnt_all}) Online({cnt_online}) Offline({cnt_offline}) Fault({cnt_fault}) +| Plant name Current Power↕ PV capacity↕ Daily Power Gen.↕ Total Power Gen.↕ +""".strip() +) +for plant_dict in plant_list_dicts: + print( + f""" ++---------------------------------------------------------------------------------+ +| ___ {plant_dict['plantName']} ___ +| Image: [{plant_dict['imgPath']}] +| Address: {plant_dict['plantAddress']} +| Current Power: {(plant_dict['currentPac']):0.0f} W +| Installation date: {plant_dict['createDateText']} +| PV capacity: {plant_dict['nominalPower']} Wp +| Daily Power Generation: {plant_dict['eToday']:0.2f} kWh +""".strip() + ) +print( + f""" ++=================================================================================+ +| ( Dashboard ) (*Plant*) ( Service ) ( Me ) | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> => Datalogger list shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" + +# get datalogger list data +# # data already retrieved before +# plant_info_dict = api.plant_info(plant_id=PLANT_ID, language="de") +datalogger_list = plant_info_dict["datalogList"] + +print('\nTab "Plant" -> My device list => Datalogger:') +print( + f""" ++=================================================================================+ +| My device list | ++---------------------------------------------------------------------------------+ +| ( Search _______________________________ 🔍 ) +| [*Datalogger*] [ Inverter ] +""".strip() +) +for datalogger_dict in datalogger_list: + print( + f""" ++---------------------------------------------------------------------------------+ +| {datalogger_dict['alias'] or datalogger_dict['datalog_sn']} +| SN: {datalogger_dict['datalog_sn']} +| Device type: {datalogger_dict['device_type']} +""".strip() + ) + for item_name, item_value in zip(datalogger_dict["keys"], datalogger_dict["values"]): + print(f"| {(item_name + ':'):21} {item_value}") +print( + f""" +| | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> -> Tab "Inverter" +> => Inverter list shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" + +# get inverter list data +# # data already retrieved before +# plant_info_dict = api.plant_info(plant_id=PLANT_ID, language="de") +inverter_list = plant_info_dict["invList"] + +print('\nTab "Plant" -> My device list => Inverter:') +print( + f""" ++=================================================================================+ +| My device list | ++---------------------------------------------------------------------------------+ +| ( Search _______________________________ 🔍 ) +| [ Datalogger ] [*Inverter*] +""".strip() +) +for inverter_dict in inverter_list: + print( + f""" ++---------------------------------------------------------------------------------+ +| {inverter_dict['deviceAilas'] or inverter_dict['deviceSn']} +| Connection Status: {device_status_mapper.get(inverter_dict['deviceStatus'], "Unknown")} +| Power: {inverter_dict['power']} W +| Today: {inverter_dict['eToday']} kWh +| Data Logger: {inverter_dict['datalogSn']} +| Device type: {inverter_dict['deviceType'].upper()} +| SN: {inverter_dict['deviceSn']} +""".strip() + ) +print( + f""" +| | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> -> Tab "Inverter" +> -> select your (NEO/TLX) inverter +> => Inverter details shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# as "NEO 800M-X" is a TLX inverter, we get TLX data +INVERTER_SN = inverter_list[0]["deviceSn"] + +# get inverter detail data +tlx_info_dict = api.tlx_params(tlx_id=INVERTER_SN) +tlx_info_bean = tlx_info_dict["newBean"] + +# get chart data for "Real time data" chart +# "Hour" (5min values) chart support different types of data +hour_pac = api.tlx_data( + tlx_id=INVERTER_SN, + date=datetime.date.today(), + tlx_data_type=TlxDataTypeNeo.power_ac, +)["invPacData"] +hour_ppv = api.tlx_data( + tlx_id=INVERTER_SN, + date=datetime.date.today(), + tlx_data_type=TlxDataTypeNeo.power_pv, +)["invPacData"] +hour_upv1 = api.tlx_data( + tlx_id=INVERTER_SN, + date=datetime.date.today(), + tlx_data_type=TlxDataTypeNeo.voltage_pv1, +)["invPacData"] +hour_ipv1 = api.tlx_data( + tlx_id=INVERTER_SN, + date=datetime.date.today(), + tlx_data_type=TlxDataTypeNeo.current_pv1, +)["invPacData"] +hour_upv2 = api.tlx_data( + tlx_id=INVERTER_SN, + date=datetime.date.today(), + tlx_data_type=TlxDataTypeNeo.voltage_pv2, +)["invPacData"] +hour_ipv2 = api.tlx_data( + tlx_id=INVERTER_SN, + date=datetime.date.today(), + tlx_data_type=TlxDataTypeNeo.current_pv2, +)["invPacData"] +# Month/Year/Total chart uses different endpoint +month_pac = api.tlx_energy_chart(tlx_id=INVERTER_SN, timespan=Timespan.month, date=datetime.date.today()) +year_pac = api.tlx_energy_chart(tlx_id=INVERTER_SN, timespan=Timespan.year, date=datetime.date.today()) +total_pac = api.tlx_energy_chart(tlx_id=INVERTER_SN, timespan=Timespan.total, date=datetime.date.today()) + +print('\nTab "Plant" -> My device list -> select NEO inverter:') +print( + f""" ++=================================================================================+ +| {tlx_info_bean['alias'] or tlx_info_bean['serialNum']} ++---------------------------------------------------------------------------------+ +| SN: {tlx_info_bean['serialNum']} +| Model: {tlx_info_dict['inverterType']} All parameters> ++---------------------------------------------------------------------------------+ +| {device_status_mapper.get(str(tlx_info_bean['status']), "Unknown")} +| Current Power: {tlx_info_bean['power']:0.1f} W +| Rated power: {tlx_info_bean['pmax']:0.0f} W +| Daily Power Generation: {tlx_info_bean['eToday']:0.1f} kWh +| Total Power Generation: {tlx_info_bean['eTotal']:0.1f} kWh ++---------------------------------------------------------------------------------+ +| Real time data +| [*Hour*] [ DAY ] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today()} (>) [Pac] +""".strip() +) +miniplot(hour_pac, rows_to_plot=5, max_width=80) +print( + f""" ++---------------------------------------------------------------------------------+ +| [*Hour*] [ DAY ] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today()} (>) [PV Power] +""".strip() +) +miniplot(hour_ppv, rows_to_plot=3) +print( + f""" ++---------------------------------------------------------------------------------+ +| [*Hour*] [ DAY ] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today()} (>) [PV1 Voltage] +""".strip() +) +miniplot(hour_upv1, rows_to_plot=3) +print( + f""" ++---------------------------------------------------------------------------------+ +| [*Hour*] [ DAY ] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today()} (>) [PV1 Current] +""".strip() +) +miniplot(hour_ipv1, rows_to_plot=3) +print( + f""" ++---------------------------------------------------------------------------------+ +| [*Hour*] [ DAY ] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today()} (>) [PV2 Voltage] +""".strip() +) +miniplot(hour_upv2, rows_to_plot=3) +print( + f""" ++---------------------------------------------------------------------------------+ +| [*Hour*] [ DAY ] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today()} (>) [PV2 Current] +""".strip() +) +miniplot(hour_ipv2, rows_to_plot=3) +print( + f""" ++---------------------------------------------------------------------------------+ +| [ Hour ] [*DAY*] [ MONTH ] [ YEAR ] +| (<) {datetime.date.today().strftime("%Y-%m")} (>) +""".strip() +) +miniplot(month_pac, rows_to_plot=4) +print( + f""" ++---------------------------------------------------------------------------------+ +| [ Hour ] [ DAY ] [*MONTH*] [ YEAR ] +| (<) {datetime.date.today().strftime("%Y")} (>) +""".strip() +) +miniplot(year_pac, rows_to_plot=4) +print( + f""" ++---------------------------------------------------------------------------------+ +| [ Hour ] [ DAY ] [ MONTH ] [*YEAR*] +| +""".strip() +) +miniplot(total_pac, rows_to_plot=4) +print( + f""" ++=================================================================================+ +| ( Events ) ( Control ) ( Edit ) | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> -> Tab "Inverter" +> -> select your (NEO/TLX) inverter +> -> click "All parameters>" +> => Inverter details shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" + +# get inverter detail data +# # already retrieved before +# tlx_info_dict = api.tlx_params(tlx_id=INVERTER_SN) +# tlx_info_bean = tlx_info_dict["newBean"] + +# get "Important" Data (current Volt/Current/Power) +tlx_detail_info_vaw = api.device_detail(device_id=INVERTER_SN, device_type="tlx") + +# get "Other" Data (frequency, temperature, etc.) +# GET https://server-api.growatt.com/newTlxApi.do?op=getTlxDetailData&id={INVERTER_SN} +tlx_detail_dict = api.tlx_detail(tlx_id=INVERTER_SN) + +print('\nTab "Plant" -> My device list -> select NEO inverter -> All parameters:') +print( + f""" ++=================================================================================+ +| Info | ++---------------------------------------------------------------------------------+ +| Basic Parameters | +| SN: {tlx_info_bean['serialNum']} +| Model: {tlx_info_dict['inverterType']} +| Firmware Version: {tlx_info_bean['fwVersion']}/{tlx_info_bean['innerVersion']} +| COM software version: {tlx_info_bean['communicationVersion']} +| Port: {tlx_info_bean['dataLogSn']} +| Rated power: {tlx_info_bean['pmax']:0.0f} W +| Model: {tlx_info_bean['modelText']} ++---------------------------------------------------------------------------------+ +| Important Data | +""".strip() +) +for info_line in tlx_detail_info_vaw["parameterGreat"]: + print("| " + " ".join([f"{token:15}" for token in info_line])) +print( + f""" ++---------------------------------------------------------------------------------+ +| Other Data | +| Fac: {tlx_detail_dict['data']['fac']} Hz +| Pac: {tlx_detail_dict['data']['pac']} W +| Ppv: {tlx_detail_dict['data']['ppv']} W +| Ppv1: {tlx_detail_dict['data']['ppv1']} W +| Ppv2: {tlx_detail_dict['data']['ppv2']} W +| Vpv1: {tlx_detail_dict['data']['vpv1']} V +| Ipv1: {tlx_detail_dict['data']['ipv1']} A +| Vpv2: {tlx_detail_dict['data']['vpv2']} V +| Ipv2: {tlx_detail_dict['data']['ipv2']} A +| Vac1: {tlx_detail_dict['data']['vac1']} V +| Iac1: {tlx_detail_dict['data']['iac1']} A +| Pac1: {tlx_detail_dict['data']['pac1']} W +| VacRs: {tlx_detail_dict['data']['vacRs']} V +| EacToday: {tlx_detail_dict['data']['eacToday']} kWh +| EacTotal: {tlx_detail_dict['data']['eacTotal']} kWh +| Epv1Today: {tlx_detail_dict['data']['epv1Today']} kWh +| Epv2Today: {tlx_detail_dict['data']['epv2Today']} kWh +| Temp1: {tlx_detail_dict['data']['temp1']} ℃ +| Temp2: {tlx_detail_dict['data']['temp2']} ℃ +| Temp3: {tlx_detail_dict['data']['temp3']} ℃ +| Temp4: {tlx_detail_dict['data']['temp4']} ℃ +| Temp5: {tlx_detail_dict['data']['temp5']} ℃ +| PBusVoltage: {tlx_detail_dict['data']['pBusVoltage']} V +| NBusVoltage: {tlx_detail_dict['data']['nBusVoltage']} V +| OpFullwatt: {tlx_detail_dict['data']['opFullwatt']} W +| InvDelayTime: {tlx_detail_dict['data']['invDelayTime']} S +| DciR: {tlx_detail_dict['data']['dciR']} mA +| DciS: {tlx_detail_dict['data']['dciS']} mA +| DciT: {tlx_detail_dict['data']['dciT']} mA +| SysFaultWord: {tlx_detail_dict['data']['sysFaultWord']} +| SysFaultWord1: {tlx_detail_dict['data']['sysFaultWord1']} +| SysFaultWord2: {tlx_detail_dict['data']['sysFaultWord2']} +| SysFaultWord3: {tlx_detail_dict['data']['sysFaultWord3']} +| SysFaultWord4: {tlx_detail_dict['data']['sysFaultWord4']} +| SysFaultWord5: {tlx_detail_dict['data']['sysFaultWord5']} +| SysFaultWord6: {tlx_detail_dict['data']['sysFaultWord6']} +| SysFaultWord7: {tlx_detail_dict['data']['sysFaultWord7']} +| FaultType: {tlx_detail_dict['data']['faultType']} +| WarnCode: {tlx_detail_dict['data']['warnCode']} +| RealOPPercent: {tlx_detail_dict['data']['realOPPercent']} +| DeratingMode: {tlx_detail_dict['data']['deratingMode']} +| DryContactStatus: {tlx_detail_dict['data']['dryContactStatus']} +| LoadPercent: {tlx_detail_dict['data']['loadPercent']} +| uwSysWorkMode: {tlx_detail_dict['data']['uwSysWorkMode']} +| Gfci: {tlx_detail_dict['data']['gfci']} mA +| Iso: {tlx_detail_dict['data']['iso']} KΩ +| EtoUserToday: {tlx_detail_dict['data']['etoUserToday']} kWh +| EtoUserTotal: {tlx_detail_dict['data']['etoUserTotal']} kWh +| EtoGridToday: {tlx_detail_dict['data']['etoGridToday']} kWh +| EtoGridTotal: {tlx_detail_dict['data']['etoGridTotal']} kWh +| ElocalLoadToday: {tlx_detail_dict['data']['elocalLoadToday']} kWh +| ElocalLoadTotal: {tlx_detail_dict['data']['elocalLoadTotal']} kWh +| Epv1Total: {tlx_detail_dict['data']['epv1Total']} kWh +| Epv2Total: {tlx_detail_dict['data']['epv2Total']} kWh +| EpvTotal: {tlx_detail_dict['data']['epvTotal']} kWh +| BsystemWorkMode: {tlx_detail_dict['data']['bsystemWorkMode']} +| BgridType: {tlx_detail_dict['data']['bgridType']} ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> -> Tab "Inverter" +> -> select your (NEO/TLX) inverter +> -> click "Events" +> => Event log (Alarms) shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# get event log data +event_log_dict = api.device_event_logs( + device_id=INVERTER_SN, + device_type="tlx", + language_code=LanguageCode.de, +) +events = event_log_dict.get("eventList", []) + +print('\nTab "Plant" -> My device list -> select NEO inverter -> Events:') +print( + f""" ++=================================================================================+ +| Warning list | ++---------------------------------------------------------------------------------+ +""".strip() +) +for event in events: + print( + f""" +| ({event['occurTime']}) +| SN: {event['deviceAlias'] or event['deviceSerialNum']} +| Plant name: {event_log_dict['plantName']} +| Description: {event['eventCode']} {event['description']} +| Instruction: {event['solution']} ++---------------------------------------------------------------------------------+ +""".strip() + ) +print( + f""" +| No more data +| ++=================================================================================+ +| (*Events*) ( Control ) ( Edit ) | ++=================================================================================+ +""".lstrip() +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> -> Tab "Inverter" +> -> select your (NEO/TLX) inverter +> -> click "Edit" +> => Inverter alias shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# get inverter detail data +# # already retrieved before +# tlx_info_dict = api.tlx_params(tlx_id=INVERTER_SN) + +print('\nTab "Plant" -> My device list -> select NEO inverter -> Edit:') +print( + f""" ++=================================================================================+ +| Edit [ Save ] | ++---------------------------------------------------------------------------------+ +| Alias: {tlx_info_bean['alias'] or tlx_info_bean['serialNum']} ++---------------------------------------------------------------------------------+ +| [Delete device] ++=================================================================================+ +| ( Events ) ( Control ) (*Edit*) | ++=================================================================================+ +""".lstrip() +) + +# change alias and save +# enable if you know what you are doing +# print(f""") +# +=================================================================================+ +# | Edit [*Save*] | +# +=================================================================================+ +# | | +# | +-------------------------------------------------------------------+ | +# """.strip()) +# new_alias = f"{tlx_info_bean['serialNum']}" +# if api.update_device_alias(device_id=INVERTER_SN, device_type="tlx", new_alias=new_alias): +# print(f"| | Device alias changed to '{new_alias}'") +# else: +# print(f"| | Failed to save alias '{new_alias}'") +# print(f""" +# | +-------------------------------------------------------------------+ | +# | | +# +=================================================================================+ +# | ( Events ) ( Control ) (*Edit*) | +# +=================================================================================+ +# """.lstrip()) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> Panel data +> -> Daily performance curve +> => Chart is shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# get chart data for "Daily performance curve" +inverter_chart_data = api.inverter_energy_chart( + plant_id=PLANT_ID, + inverter_id=INVERTER_SN, + date=datetime.date.today(), + timespan=Timespan.day, + filter_type="all", +) + +print('\nTab "Plant" -> Panel data -> Daily performance curve:') +print( + f""" ++=================================================================================+ +| Panel data | ++---------------------------------------------------------------------------------+ +| [*Daily performance curve*] [ Production ] | +| [ Filter "All" ▼ ] (<) {datetime.date.today()} (>) ++---------------------------------------------------------------------------------+ +| AC(W) +""".strip() +) +miniplot( + {k: v for k, v in zip(inverter_chart_data["time"], inverter_chart_data["pac"])}, + rows_to_plot=4, +) +print( + f""" ++---------------------------------------------------------------------------------+ +| PV1 (W) +""".strip() +) +miniplot( + {k: v for k, v in zip(inverter_chart_data["time"], inverter_chart_data["ppv1"])}, + rows_to_plot=4, +) +print( + f""" ++---------------------------------------------------------------------------------+ +| T (°C) +""".strip() +) +miniplot( + {k: v for k, v in zip(inverter_chart_data["time"], inverter_chart_data["temp1"])}, + rows_to_plot=4, +) +print( + f""" ++---------------------------------------------------------------------------------+ +| ☑ PV1 (W) ☑ PV2 (W) ☑ PV3 (W) ☑ PV4 (W) ☑ PV1 (V) ☑ PV2 (V) | +| ☑ PV3 (V) ☑ PV4 (V) ☑ PV1 (I) ☑ PV2 (I) ☑ PV3 (I) ☑ PV4 (I) | +| ☑ AC (W) ☑ AC (F) ☑ AC (V) ☑ AC (I) ☑ T (°C) ☑ AP (W) | ++=================================================================================+ +""".lstrip() +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> Panel data +> -> Production +> => Chart is shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# get chart data for "Production" +inverter_chart_data_month = api.inverter_energy_chart( + plant_id=PLANT_ID, + inverter_id=INVERTER_SN, + date=datetime.date.today(), + timespan=Timespan.month, + filter_type="pv1", +) +inverter_chart_data_year = api.inverter_energy_chart( + plant_id=PLANT_ID, + inverter_id=INVERTER_SN, + date=datetime.date.today(), + timespan=Timespan.year, + filter_type="pv1", +) +inverter_chart_data_total = api.inverter_energy_chart( + plant_id=PLANT_ID, + inverter_id=INVERTER_SN, + date=datetime.date.today(), + timespan=Timespan.total, + filter_type="pv1", +) + +print('\nTab "Plant" -> Panel data -> Production:') +print( + f""" ++=================================================================================+ +| Panel data | ++---------------------------------------------------------------------------------+ +| [ Daily performance curve ] [*Production*] | ++---------------------------------------------------------------------------------+ +| [ Filter "PV1" ▼ ] (<) {datetime.datetime.today().strftime("%Y-%m")} (>) +| [*DAY*] [ MONTH ] [ YEAR ] +""".strip() +) +miniplot( + {k: v for k, v in zip(inverter_chart_data_month["time"], inverter_chart_data_month["epv1Energy"])}, + rows_to_plot=4, +) +print( + f""" ++---------------------------------------------------------------------------------+ +| [ Filter "PV1" ▼ ] (<) {datetime.datetime.today().strftime("%Y")} (>) +| [ DAY ] [*MONTH*] [ YEAR ] +""".strip() +) +miniplot( + { + k: v + for k, v in zip( + [int(t) for t in inverter_chart_data_year["time"]], + inverter_chart_data_year["epv1Energy"], + ) + }, + rows_to_plot=4, +) +print( + f""" ++---------------------------------------------------------------------------------+ +| [ Filter "PV1" ▼ ] {(datetime.datetime.today().year - 5):04g}-{datetime.datetime.today().year:04g} +| [ DAY ] [ MONTH ] [*YEAR*] +""".strip() +) +miniplot( + { + k: v + for k, v in zip( + [int(t) for t in inverter_chart_data_total["time"]], + inverter_chart_data_total["epv1Energy"], + ) + }, + rows_to_plot=4, +) +print( + f""" ++---------------------------------------------------------------------------------+ +| ☑ PV1 (Energy) ☐ PV2 (Energy) ☐ PV3 (Energy) ☐ PV4 (Energy) ☐ AC (Energy) | ++=================================================================================+ +""".lstrip() +) + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> Panel view +> -> Panel power +> => Small solar-panel pictures shown in a grid +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +panel_view_chart_data = api.inverter_panel_energy_chart( + plant_id=PLANT_ID, + inverter_id=INVERTER_SN, + date=datetime.date.today(), + timespan=Timespan.day, +) + +# small plots for each panel +plot_boxes = {} +for ts, box_data in panel_view_chart_data["box"].items(): + for box in box_data: + box_name = box["name"] + plot_boxes[box_name] = plot_boxes.get(box_name, {}) + plot_boxes[box_name][ts] = float(box["power"]) + +# big plot for cumulated data +plot_big = { + k: float(v) for k, v in zip(panel_view_chart_data["chart"]["time"], panel_view_chart_data["chart"]["power"]) +} + +print('\nTab "Plant" -> Panel view (Panel power):') +print( + f""" ++=================================================================================+ +| Panel view | ++---------------------------------------------------------------------------------+ +| [*Panel power*] [ Panel production ] | +| [ Filter "All" ▼ ] (<) {datetime.date.today()} (>) ++---------------------------------------------------------------------------------+ +| | +""".strip() +) +for idx, (panel_name, plt_data) in enumerate(plot_boxes.items()): + print(f"| +-------------------------------------------------------------------------+ |") + print(f"| | {panel_name} (mean: {sum(plt_data.values()) / len(plt_data):0.1f} W)") + miniplot(plt_data, rows_to_plot=3, max_width=70, prefix="| | ") + print(f"| +-------------------------------------------------------------------------+ |") + print(f"| |") + +print(f"+---------------------------------------------------------------------------------+") +miniplot(plot_big) +print( + f""" ++=================================================================================+ +""".lstrip() +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> Panel view +> -> Panel producution +> => Small solar-panel pictures shown in a grid +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +plot_date = datetime.date.today() - datetime.timedelta(days=31) +panel_view_chart_data = api.inverter_panel_energy_chart( + plant_id=PLANT_ID, + inverter_id=INVERTER_SN, + date=plot_date, + timespan=Timespan.month, +) + +# small plots for each panel +plot_boxes = {} +for ts, box_data in panel_view_chart_data["box"].items(): + for box in box_data: + box_name = box["name"] + plot_boxes[box_name] = plot_boxes.get(box_name, {}) + plot_boxes[box_name][int(ts)] = float(box["energy"]) + +# big plot for cumulated data +plot_big = { + int(k): float(v) for k, v in zip(panel_view_chart_data["chart"]["time"], panel_view_chart_data["chart"]["energy"]) +} + +print('\nTab "Plant" -> Panel view (Panel production):') +print( + f""" ++=================================================================================+ +| Panel view | ++---------------------------------------------------------------------------------+ +| [ Panel power ] [*Panel production*] | +| [ Filter "All" ▼ ] (<) {plot_date.strftime("%Y-%m")} (>) ++---------------------------------------------------------------------------------+ +| | +""".strip() +) +for idx, (panel_name, plt_data) in enumerate(plot_boxes.items()): + print(f"| +-------------------------------------------------------------------------+ |") + print(f"| | {panel_name} (sum: {sum(plt_data.values())} kWh)") + miniplot(plt_data, rows_to_plot=3, max_width=70, prefix="| | ") + print(f"| +-------------------------------------------------------------------------+ |") + print(f"| |") + +print(f"+---------------------------------------------------------------------------------+") +miniplot(plot_big) +print( + f""" ++=================================================================================+ +""".lstrip() +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> APP +> -> Plant +> -> My device list +> -> Tab "Inverter" +> -> select your (NEO/TLX) inverter +> -> click "Control" +> => Inverter settings shown +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# # already retrieved before +# tlx_info_dict = api.tlx_params(tlx_id=INVERTER_SN) +# tlx_info_bean = tlx_info_dict["newBean"] +inverter_device_type_int = tlx_info_bean["deviceType"] + +# get all settings +tlx_settings_all_dict = api.tlx_all_settings(tlx_id=INVERTER_SN) + +# enabled settings and PW +tlx_enabled_settings = api.tlx_enabled_settings( + tlx_id=INVERTER_SN, + language_code=LanguageCode.de, + device_type_id=inverter_device_type_int, +) +settings_pw = tlx_enabled_settings["settings_password"] + + +def try_type_convert(val_, type_): + try: + if type_ is bool: + return val_ == "1" # as bool('0') would evaluate to True + return type_(val_) + except (ValueError, TypeError, IndexError): + return val_ + + +def get_inv_setting(tcpset: str = None, register: int = None, tlx_keys: list = None, dtypes: list = None): + # try to get from tlx settings + if tlx_keys: + results = {0: "CACHED"} + collected = True + for idx_, param_name in enumerate(tlx_keys): + if param_name not in tlx_settings_all_dict: + collected = False + break # not found in cached data + results[idx_ + 1] = try_type_convert(tlx_settings_all_dict[param_name], dtypes[idx_]) + if collected: + return results + # get from tcpSet using setting name or register + elif (tcpset is not None) or (register is not None): + inv_result = api.read_inverter_setting( + inverter_id=INVERTER_SN, + device_type="tlx", + setting_name=tcpset if tcpset else None, + register_address=register if not tcpset else None, + ) + if inv_result["success"] is False: + return {i: f"ERROR {inv_result['error_message']}" for i in range(10)} + retval = {0: "SUCCESS"} + idx_ = 0 + for key_ in sorted(inv_result.keys()): + if key_.startswith("param"): + val_ = try_type_convert(inv_result[key_], dtypes[idx_]) + idx_ += 1 + retval[idx_] = val_ + return retval + else: + raise AttributeError + + +country_data = get_inv_setting(register=90, dtypes=[int]) +active_power_data = get_inv_setting( + tlx_keys=["activeRate", "pvPfCmdMemoryState"], + tcpset="pv_active_p_rate", + dtypes=[int, bool], +) +reactive_power = get_inv_setting(tcpset="pv_reactive_p_rate", dtypes=[int, int, bool]) +reactive_power_mode, range_hint = { + 0: ("PF fixed", "(value can only be '1')"), + 1: ("Set power factor", "(-1 ~ 1)"), + 2: ("Default PF Curve", "(value field not existing for this setting)"), + 3: ("Customize PF Curve", "special settings apply"), + 4: ("Conductive reactive power ratio (%)", "(0 ~ 100)"), + 5: ("Inductive reactive power ratio (%)", "(0 ~ 100)"), + 6: ("QV mode", "(value field not existing for this setting)"), + 7: ("Set reactive power percentage", "(-100 ~ +100)"), +}.get(reactive_power[2], (f"{reactive_power[2]}", "")) +if reactive_power[2] == 0: + # PF fixed: value can only be '1' + reactive_power[1] = 1 +# "Customize PF Curve" has special settings +reactive_power_custom = get_inv_setting( + tlx_keys=[ + "tlx_pflinep1_lp", + "tlx_pflinep1_pf", + "tlx_pflinep2_lp", + "tlx_pflinep2_pf", + "tlx_pflinep3_lp", + "tlx_pflinep3_pf", + "tlx_pflinep4_lp", + "tlx_pflinep4_pf", + ], + tcpset="tlx_custom_pf_curve", + dtypes=[int, float, int, float, int, float, int, float], +) +inverter_time = get_inv_setting( + tcpset="pf_sys_year", + tlx_keys=["pf_sys_year"], + dtypes=[ + # returns param1='2024-2-4 11:38:4' # (without leading zeroes) + lambda x: datetime.datetime.strptime(x, "%Y-%m-%d %H:%M:%S").isoformat(sep=" ") + ], +) +over_voltage = get_inv_setting(tcpset="pv_grid_voltage_high", tlx_keys=["pv_grid_voltage_high"], dtypes=[float]) +under_voltage = get_inv_setting(tcpset="pv_grid_voltage_low", tlx_keys=["pv_grid_voltage_low"], dtypes=[float]) +over_frequency = get_inv_setting(tcpset="pv_grid_frequency_high", tlx_keys=["pv_grid_frequency_high"], dtypes=[float]) +under_frequency = get_inv_setting(tcpset="pv_grid_frequency_low", tlx_keys=["pv_grid_frequency_low"], dtypes=[float]) +export_limitation = get_inv_setting(tcpset="backflow_setting", dtypes=[int, float, bool]) +export_limitation_selection = {0: "Disable", 1: "Enable meter"}.get(export_limitation[1], f"{export_limitation[1]}") +failsafe_power_limit = get_inv_setting( + tcpset="tlx_backflow_default_power", + tlx_keys=["tlx_backflow_default_power"], + dtypes=[float], +) +power_sensor = get_inv_setting(tcpset="tlx_limit_device", tlx_keys=["tlx_limit_device"], dtypes=[int]) +power_sensor_setting = { + 0: "not connected to power collection", + 1: "Meter", + 2: "CT", +}.get(power_sensor[1], f"{power_sensor[1]}") + +operating_mode = get_inv_setting(tcpset="system_work_mode", tlx_keys=["system_work_mode"], dtypes=[int]) +operating_mode_setting = { + 0: "Default", + 1: "System retrofit", + 2: "Multi-parallel", + 3: "System retrofit-simplification", +}.get(operating_mode[1], f"unknown value '{operating_mode[1]}'") + +print('\nTab "Plant" -> My device list -> select NEO inverter -> Control:') +print( + f""" ++=================================================================================+ +| {tlx_info_bean['serialNum']} ++---------------------------------------------------------------------------------+ +| Country & Regulation [Read] +| [ {(country_data[1] or "Not set - Click to select"):^30} ] +| ( Done ) ++---------------------------------------------------------------------------------+ +| Set active power [Read] +| [ {active_power_data[1]:^30} ] % (0 ~ 100) +| Whether to remember ({active_power_data[2]}) +| ( Done ) ++---------------------------------------------------------------------------------+ +| Set reactive power [Read] +| [ {reactive_power_mode:^28s} ▼ ] +""".strip() +) +if reactive_power[2] == 3: + print( + f""" +| | +| +--- PopUp -----------------------------------------------------------------+ | +| | Point 1 Power percentage [ {reactive_power_custom[1]:^5} ] (0 ~ 100) +| | Power factor point [ {reactive_power_custom[2]:^5} ] (-1 ~ -0.8 | 0.8 ~ 1) +| | Point 2 Power percentage [ {reactive_power_custom[3]:^5} ] (0 ~ 100) +| | Power factor point [ {reactive_power_custom[4]:^5} ] (-1 ~ -0.8 | 0.8 ~ 1) +| | Point 3 Power percentage [ {reactive_power_custom[5]:^5} ] (0 ~ 100) +| | Power factor point [ {reactive_power_custom[6]:^5} ] (-1 ~ -0.8 | 0.8 ~ 1) +| | Point 4 Power percentage [ {reactive_power_custom[7]:^5} ] (0 ~ 100) +| | Power factor point [ {reactive_power_custom[8]:^5} ] (-1 ~ -0.8 | 0.8 ~ 1) +| | ( Yes ) +| +---------------------------------------------------------------------------+ | +| | +""".strip() + ) +elif reactive_power[1] is not None: + print(f"| [ {reactive_power[1]:^30} ] {range_hint}") +print( + f""" +| Whether to remember ({reactive_power[3]}) +| ( Done ) ++---------------------------------------------------------------------------------+ +| Set inverter time [Read] +| [ {inverter_time[1]:^30} ] +| ( Done ) ++---------------------------------------------------------------------------------+ +| Over voltage / High Grid Voltage Limit [Read] +| [ {over_voltage[1]:^30} ] +| ( Yes ) ++---------------------------------------------------------------------------------+ +| Under voltage / Low Grid Voltage Limit [Read] +| [ {under_voltage[1]:^30} ] +| ( Yes ) ++---------------------------------------------------------------------------------+ +| Overfrequency / High Grid Frequency Limit [Read] +| [ {over_frequency[1]:^30} ] +| ( Yes ) ++---------------------------------------------------------------------------------+ +| Underfrequency / High Grid Frequency Limit [Read] +| [ {under_frequency[1]:^30} ] +| ( Done ) ++---------------------------------------------------------------------------------+ +| Export Limitation [Read] +| [ {export_limitation_selection:^28} ▼ ] +""".strip() +) +if export_limitation[1] != 0: + print(f"| [ {export_limitation[2]:^30} ] Power (W)") +print( + f""" +| ( Done ) ++---------------------------------------------------------------------------------+ +| Failsafe power limit [Read] +| [ {failsafe_power_limit[1]:^30} ] +| ( Yes ) ++---------------------------------------------------------------------------------+ +| Power Sensor [Read] +| [ {power_sensor_setting:^28} ▼ ] +| ( Done ) ++---------------------------------------------------------------------------------+ +| Reset [Read] +| Make sure the inverter is in a waiting or standby state. +| ( Done ) ++---------------------------------------------------------------------------------+ +| Operating mode [Read] +| [ {operating_mode_setting:^28} ▼ ] +| ( Done ) ++=================================================================================+ +| ( Events ) (*Control*) ( Edit ) | ++=================================================================================+ +""".lstrip() +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> Web Frontend (offers additional settings not available in App) +> -> Dashboard +> -> My Photovoltaic Devices +> -> select your (NEO/TLX) inverter +> -> click "Setting" -> "Advanced Setting" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +regulation_frequency_low = get_inv_setting( + tcpset="l_1_freq", tlx_keys=["l_1_freq_1", "l_1_freq_2"], dtypes=[float, float] +) +regulation_frequency_high = get_inv_setting( + tcpset="h_1_freq", tlx_keys=["h_1_freq_1", "h_1_freq_2"], dtypes=[float, float] +) +regulation_voltage_low = get_inv_setting( + tcpset="l_1_volt", tlx_keys=["l_1_volt_1", "l_1_volt_2"], dtypes=[float, float] +) +regulation_voltage_high = get_inv_setting( + tcpset="h_1_volt", tlx_keys=["h_1_volt_1", "h_1_volt_2"], dtypes=[float, float] +) +loading_rate = get_inv_setting(tcpset="loading_rate", tlx_keys=["loading_rate"], dtypes=[float]) +restart_loading_rate = get_inv_setting(tcpset="restart_loading_rate", tlx_keys=["restart_loading_rate"], dtypes=[float]) +overfrequency_start_point = get_inv_setting(tcpset="over_fre_drop_point", dtypes=[float]) +overfrequency_loadreduction_gradient = get_inv_setting(tcpset="over_fre_lored_slope", dtypes=[float]) +overfrequency_load_reduction_delay_time = get_inv_setting(tcpset="over_fre_lored_delaytime", dtypes=[float]) +qv_volt_high_out = get_inv_setting(tlx_keys=["qv_h1"], tcpset="qv_h1", dtypes=[float]) +qv_volt_high_in = get_inv_setting(tlx_keys=["qv_h2"], tcpset="qv_h2", dtypes=[float]) +qv_volt_low_out = get_inv_setting(tlx_keys=["qv_l1"], tcpset="qv_l1", dtypes=[float]) +qv_volt_low_in = get_inv_setting(tlx_keys=["qv_l2"], tcpset="qv_l2", dtypes=[float]) +qv_delay = get_inv_setting(tlx_keys=["delay_time"], tcpset="delay_time", dtypes=[float]) +qv_percent = get_inv_setting(tlx_keys=["q_percent_max"], tcpset="q_percent_max", dtypes=[float]) +inverter_on_off = get_inv_setting(tlx_keys=["tlx_on_off"], tcpset="tlx_on_off", dtypes=[int]) +inverter_on_off_str = { + 1: "On", + 0: "Off", +}.get(inverter_on_off[1], f"Unknown status {inverter_on_off[1]}") + +print("\nAdditional settings from Web frontend") +print( + f""" ++=================================================================================+ +| Additional settings available in Web frontend | ++---------------------------------------------------------------------------------+ +| ▼ Regulation parameter setting +| Low Frequency Setting +| AC Frequency Low 1 [ {regulation_frequency_low[1]:^8} ] +| AC Frequency Low 2 [ {regulation_frequency_low[2]:^8} ] +| High Frequency Setting +| AC Frequency High 1 [ {regulation_frequency_high[1]:^8} ] +| AC Frequency High 2 [ {regulation_frequency_high[2]:^8} ] +| Low Voltage Setting +| AC Voltage Low 1 [ {regulation_voltage_low[1]:^8} ] +| AC Voltage Low 2 [ {regulation_voltage_low[2]:^8} ] +| High Voltage Setting +| AC Voltage High 1 [ {regulation_voltage_high[1]:^8} ] +| AC Voltage High 2 [ {regulation_voltage_high[2]:^8} ] +| Loading rate [ {loading_rate[1]:^8} ] % ( 0.0 ~ 6000.0 ) +| Restart loading rate [ {restart_loading_rate[1]:^8} ] % ( 0.0 ~ 6000.0 ) +| Over-freq. start point [ {overfrequency_start_point[1]:^8} ] Hz +| Over-freq. load red. gradient [ {overfrequency_loadreduction_gradient[1]:^8} ]% +| Over-freq. load red. delay [ {overfrequency_load_reduction_delay_time[1]:^8} ] ms ++---------------------------------------------------------------------------------+ +| ▼ Q (V) setting +| Cut out high Voltage [ {qv_volt_high_out[1]:^8} ] V +| Cut into high Voltage [ {qv_volt_high_in[1]:^8} ] V +| Cut out low Voltage [ {qv_volt_low_out[1]:^8} ] V +| Cut into low Voltage [ {qv_volt_low_in[1]:^8} ] V +| Delay time [ {qv_delay[1]:^8} ] ms +| Reactive power percentage [ {qv_percent[1]:^8} ] % ++---------------------------------------------------------------------------------+ +| Set Inverter On/Off [ {inverter_on_off_str:^8} ] +| Register [ ________ ] Value [ ________ ] -> see next screen "Register dump" ++---------------------------------------------------------------------------------+ +| Please enter password ({settings_pw}) (Yes) (Advanced Settings) (Cancel) | ++=================================================================================+ +""" +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> Web Frontend (register dump not available in App) +> -> Dashboard +> -> My Photovoltaic Devices +> -> select your (NEO/TLX) inverter +> -> click "Setting" -> "Advanced Setting" +> -> Start Address (...) / End Address (...) -> "Advanced Read) +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" +# dump inverter registers +# fmt: off +register_ranges = [ # skipping "Reserved" registers + (0, 124), (125, 179), (180, 188), (201, 203), (209, 223), (229, 229), (230, 242), (304, 345), (532, 554), (600, 612), + (660, 660), (1000, 1038), (1044, 1044), (1047, 1048), (1060, 1062), (1070, 1071), (1080, 1092), (1100, 1121), + (1125, 1204), (1244, 1249), (3000, 3059), (3070, 3071), (3079, 3082), (3085, 3114), (3125, 3238), +] +# fmt: on + +min_reg = register_ranges[0][0] +max_reg = register_ranges[-1][1] +print(f"\nreading registers {min_reg} ~ {max_reg}") +register_result = {} +register_read_error = None +for register_start, register_end in register_ranges: + chunk_size = register_end - register_start + 1 + query_start = register_start + while query_start < register_end: + print(f"\r {((query_start / max_reg) * 100):0.1f}% ...", end="") + query_end = min(query_start + chunk_size, register_end) + registers_dict = api.read_inverter_registers( + inverter_id=INVERTER_SN, + device_type="tlx", + register_address_start=query_start, + register_address_end=query_end, + ) + if registers_dict["success"] is True: + register_result.update(registers_dict["register"]) + query_start = query_end + sleep(0.1) + elif registers_dict["error_message"].startswith("500"): + new_chunk_size = max(chunk_size // 2, 10) + if new_chunk_size == chunk_size: + print( + f"\rrequest timed out - inverter is either offline or booting from standby - Waiting 30 seconds..." + ) + sleep(30) + else: + print(f"\rrequest timed out - reduced chunk size to {chunk_size}") + chunk_size = new_chunk_size + sleep(0.3) + else: + register_read_error = f"failed to read registers: '{registers_dict['error_message']}'" + break +print(f"\r finished reading registers {min_reg} ~ {max_reg}\n") + +# fmt: off +register_names = { + 0: "OnOff", 1: "SaftyFuncEn", 2: "PF CMD memory state", 3: "Active P Rate (Power limit)", 4: "Reactive P Rate", 5: "Power factor", 6: "Pmax HighByte (Wp)", 7: "Pmax LowByte (Wp)", 8: "Vnormal", 9: "Fw version H", + 10: "Fw version M", 11: "Fw version L", 12: "Fw version2 H", 13: "Fw version2 M", 14: "Fw version2 L", 15: "LCD language", 16: "CountrySelected", 17: "Vpv start", 18: "Time start", 19: "RestartDelay Time", + 20: "wPowerStart Slope", 21: "wPowerRestartSlopeEE", 22: "wSelectBaudrate", 23: "Serial NO", 24: "Serial NO", 25: "Serial NO", 26: "Serial NO", 27: "Serial NO", 28: "Module H", 29: "Module L", + 30: "Com Address", 31: "FlashStart", 32: "Reset User Info", 33: "Reset to factory", 34: "Manufacturer Info 8", 35: "Manufacturer Info 7", 36: "Manufacturer Info 6", 37: "Manufacturer Info 5", 38: "Manufacturer Info 4", 39: "Manufacturer Info3", + 40: "Manufacturer Info 2", 41: "Manufacturer Info 1", 42: "bfailsafeEn", 43: "DTC", 44: "TP", 45: "Sys Year", 46: "Sys Month", 47: "Sys Day", 48: "Sys Hour", 49: "Sys Min", + 50: "Sys Sec", 51: "Sys Weekly", 52: "Vac low", 53: "Vac high", 54: "Fac low", 55: "Fac high", 56: "Vac low 2", 57: "Vac high 2", 58: "Fac low 2", 59: "Fac high 2", + 60: "Vac low 3", 61: "Vac high 3", 62: "Fac low 3", 63: "Fac high 3", 64: "Vac low C", 65: "Vac high C", 66: "Fac low C", 67: "Fac high C", 68: "Vac time", 69: "Vac time", + 70: "Vac time", 71: "Vac time", 72: "Fac time", 73: "Fac time", 74: "Fac time", 75: "Fac time", 76: "Vac time", 77: "Vac time", 78: "Fac time", 79: "Fac time", + 80: "U10min", 81: "PV Voltage High Fault", 82: "FW Build No. 5", 83: "FW Build No. 4", 84: "FW Build No. 3", 85: "FW Build No. 2", 86: "FW Build No. 1", 87: "FW Build No. 0", 88: "ModbusVersion", 89: "PFModel", + 90: "GPRS IP Flag", 91: "FreqDerateStart", 92: "FLrate", 93: "V1S", 94: "V2S", 95: "V1L", 96: "V2L", 97: "QlockInpower", 98: "QlockOutpower", 99: "LIGridV", + 100: "LOGridV", 101: "PFAdj1", 102: "PFAdj2", 103: "PFAdj3", 104: "PFAdj4", 105: "PFAdj5", 106: "PFAdj6", 107: "QVRPDelayTimeEE", 108: "OverFDeratDelayTimeEE", 109: "QpercentMax", + 110: "PFLineP1_LP", 111: "PFLineP1_PF", 112: "PFLineP2_LP", 113: "PFLineP2_PF", 114: "PFLineP3_LP", 115: "PFLineP3_PF", 116: "PFLineP4_LP", 117: "PFLineP4_PF", 118: "Inverter Model SxxBxx", 119: "Inverter Model DxxTxx", + 120: "Inverter Model PxxUxx", 121: "Inverter Model Mxxxx (Wp/100)", 122: "ExportLimit_En/dis", 123: "ExportLimitPowerRate", 124: "TrakerModel", 125: "INV Type-1", 126: "INV Type-2", 127: "INV Type-3", 128: "INV Type-4", 129: "INV Type-5", + 130: "INV Type-6", 131: "INV Type-7", 132: "INV Type-8", 133: "BLVersion1", 134: "BLVersion2", 135: "BLVersion3", 136: "BLVersion4", 137: "Reactive P ValueH", 138: "Reactive P ValueL", 139: "ReactiveOut putPriorityEnable", + 140: "Reactive P Value(Ratio)", 141: "SvgFunction Enable", 142: "uwUnderFU ploadPoint", 143: "uwOFDerate RecoverPoint", 144: "uwOFDerate RecoverDelayTime", 145: "ZeroCurrent Enable", 146: "uwZeroCurrentStaticlowVolt", 147: "uwZeroCurrentStaticHighVolt", 148: "uwHVoltDerateHighPoint", 149: "uwHVoltDerateLowPoint", + 150: "uwQVPowerStableTime", 151: "uwUnderFUploadStopPoint", 152: "fUnderFreqPoint", 153: "fUnderFreqEndPoint", 154: "fOverFreqPoint", 155: "fOverFreqEndPoint", 156: "fUnderVoltPoint", 157: "fUnderVoltEndPoint", 158: "fOverVoltPoint", 159: "fOverVoltEndPoint", + 160: "uwNominalGridVolt", 161: "uwGridWattDelay", 162: "uwReconnectStartSlope", 163: "uwLFRTEE", 164: "uwLFRTTimeEE", 165: "uwLFRT2EE", 166: "uwLFRTTime2EE", 167: "uwHFRTEE", 168: "uwHFRTTim eEE", 169: "uwHFRT2EE", + 170: "uwHFRTTim e2EE", 171: "uwHVRTEE", 172: "uwHVRTTim eEE", 173: "uwHVRT2EE", 174: "uwHVRTTim e2EE", 175: "uwUnderFUploadDelayTime", 176: "uwUnderFUploadRateEE", 177: "uwGridRestart_H_Freq", 178: "OverFDeratResponseTime", 179: "UnderFUploadResponseTime", + 180: "MeterLink", 181: "OPT Number", 182: "OPT ConfigOK Flag", 183: "PvStrScan", 184: "BDCLinkNum", 185: "PackNum", 186: "Reserved", 187: "VPP function enable status", 188: "dataLog Connect Server status", + 200: "Reserved", 201: "PID Working Model", 202: "PID On/Off Ctrl", 203: "PID Option", 209: "New Serial", + 210: "New Serial", 211: "New Serial", 212: "New Serial", 213: "New Serial", 214: "New Serial", 215: "New Serial", 216: "New Serial", 217: "New Serial", 218: "New Serial", 219: "New Serial", + 220: "New Serial", 221: "New Serial", 222: "New Serial", 223: "New Serial", 229: "EnergyAdjust", + 230: "IslandDisable", 231: "FanCheck", 232: "EnableNLine", 233: "wCheckHardware", 234: "wCheckHardware2", 235: "ubNToGNDDetect", 236: "NonStdVacEnable", 237: "uwEnableSpecSet", 238: "Fast MPPT enable", + 240: "Check Step", 241: "INV-Lng", 242: "INV-Lat", + 304: "uwAntiBackflowFailPowerLimitEE", 305: "Qloadspeed", 306: "bParallelAntiBackflowEnable", 307: "uwAntiBackflowFailureResponseTime", 308: "uwParallelAntiBackflowPowerLimitEE", 309: "bISOCheckCmd", + 310: "bGPRSStatus", 311: "uwQmax_Inductive", 312: "uwQmax_Capactive", 313: "uwReactivePowerAdjustFailureResponseTime", 314: "bSuperAntiBackflowEnable", 315: "uwReactivePowerStableTime", 316: "uwQpStableTime", 317: "uwPuDerateTime", 318: "uwQVModelQ2Point", 319: "uwQVModelQ3Point", + 320: "bVrefModelEnable", 321: "uwVrefModelFilterTime", 322: "uwUserQPM odeP1Krate", 323: "uwUserQPModeP2Krate", 324: "uwUserQPModeP3Krate", 325: "uwUserQPModeQ1Krate", 326: "uwUserQPModeQ2Krate", 327: "uwUserQPModeQ3Krate", 328: "uwAcVoltHighDeratPowerLimit", 329: "BackflowSingleCtrl", + 330: "bAntiBackflowProtectMode", 331: "uwUnderFUploadZeroPowerPoint", 332: "FreqDerateZeroPowerPoint", 333: "bFreqDeratingStopModeEnable", 334: "bFreqIncreasingEnable", 335: "uwFreqIncreasingRecoverTime", 336: "uwFreqIncreasingEndLowPoint", 337: "bFreqIncreasingStopModeEnable", 338: "uwUserQpChrP1Krate", 339: "uwUserQpChrP2Krate", + 340: "uwUserQpChrP3Krate", 341: "wUserQpChrQ1Krate", 342: "wUserQpChrQ2Krate", 343: "wUserQpChrQ3Krate", 344: "uwFreqDeratingRecoverLowPoint", 345: "uwFreqIncreasingRecoverHighPoint", + 532: "TurnOffUnloadSpeed", 533: "LimitDevice", 534: "PowerSetOnDCSourceMode", 535: "OUFreqGrade1En", 536: "Country Set", 538: "InterlockEnable", 539: "OvTemperDeratePoint", + 540: "SafetySetPassword", 541: "AFCIOnoff", 542: "AfciSelfCheck", 543: "AfciReset", 544: "AFCIValue1", 545: "AFCIValue2", 546: "AFCIValue3", 547: "OverThresholdValueMaxCnt", 548: "AFCIScanTypeEnable", 549: "PowerVoltStopModeEn", + 550: "VoltWattRecoverTime", 551: "HVoltDerateStopPower", 552: "QVTimeExponent", 553: "Volt-Watt Watt1", 554: "Volt-Watt Watt2", + 600: "Volt-Var Var1", 601: "Volt-Var Var2", 602: "Volt-Var Var3", 603: "Volt-Var Var4", 605: "OPModEnergize", 608: "OneKeySetBDCMode", 609: "PowerOutputEnable", + 610: "DealDebugParaFlag", 612: "bAcCoupleEn", + 660: "ReloadCmd", + 1000: "Float charge current limit", 1001: "PF CMD memory state", 1002: "VbatStartForDischarge", 1003: "VbatlowWarnClr", 1004: "Vbatstopfordischarge", 1005: "Vbat stop for charge", 1006: "Vbat start for discharge", 1007: "Vbat constant charge", 1008: "EESysInfo.SysSetEn", 1009: "Battemp lower limit d", + 1010: "Bat temp upper limit d", 1011: "Bat temp lower limit c", 1012: "Bat temp upper limit c", 1013: "uwUnderFreDischargeDelyTime", 1014: "BatMdlSerialNum", 1015: "BatMdlParallNum", 1016: "DRMS_EN", 1017: "Bat First Start Time 4", 1018: "Bat First Stop Time 4", 1019: "Bat First on/off Switch 4", + 1020: "Bat First Start Time 5", 1021: "Bat First Stop Time 5", 1022: "Bat First on/off Switch 5", 1023: "Bat First Start Time 6", 1024: "Bat First Stop Time 6", 1025: "Bat First on/off Switch 6", 1026: "Grid First Start Time 4", 1027: "Grid First Stop Time 4", 1028: "Grid First Stop Switch 4", 1029: "Grid First Start Time 5", + 1030: "Grid First Stop Time 5", 1031: "Grid First Stop Switch 5", 1032: "Grid First Start Time 6", 1033: "Grid First Stop Time 6", 1034: "Grid First Stop Switch 6", 1035: "Bat First Start Time 4", 1037: "bCTMode", 1038: "CTAdjust", + 1044: "Priority", 1047: "AgingTestStepCmd", 1048: "BatteryType", + 1060: "BuckUpsFunEn", 1061: "BuckUPSVoltSet", 1062: "UPSFreqSet", + 1070: "GridFirstDischargePowerRate", 1071: "Grid First Stop SOC", + 1080: "Grid First Start Time 1", 1081: "Grid First Stop Time 1", 1082: "Grid First Stop Switch 1", 1083: "Grid First Start Time 2", 1084: "Grid First Stop Time 2", 1085: "Grid First Stop Switch 2", 1086: "Grid First Start Time 3", 1087: "Grid First Stop Time 3", 1088: "Grid First Stop Switch 3", + 1090: "Bat First Power Rate", 1091: "wBat First stop SOC", 1092: "AC charge Switch", + 1100: "Bat First Start Time 1", 1101: "Bat First Stop Time 1", 1102: "Bat First on/off Switch 1", 1103: "Bat First Start Time 2", 1104: "Bat First Stop Time 2", 1105: "Bat Firston/off Switch 2", 1106: "Bat First Start Time 3", 1107: "Bat First Stop Time 3", 1108: "Bat First on/off Switch 3", + 1110: "Load First Start Time 1", 1111: "Load First Stop Time 1", 1112: "Load First Switch 1", 1113: "Load First Start Time2", 1114: "Load First Stop Time 2", 1115: "Load First Switch 2", 1116: "Load First Start Time 3", 1117: "Load First Stop Time 3", 1118: "Load First Switch 3", 1119: "NewEPowerCalcFlag", + 1120: "BackUpEn", 1121: "SGIPEn", 1125: "BatSerialNO. 8 (pack 1)", 1126: "BatSerialNO. 7 (pack 1)", 1127: "BatSerialNO. 6 (pack 1)", 1128: "BatSerialNO. 5 (pack 1)", 1129: "BatSerialNO. 4 (pack 1)", + 1130: "BatSerialNO. 3 (pack 1)", 1131: "BatSerialNO. 2 (pack 1)", 1132: "BatSerialNO. 1 (pack 1)", 1133: "BatSerialNO. 8 (pack 2)", 1134: "BatSerialNO. 7 (pack 2)", 1135: "BatSerialNO. 6 (pack 2)", 1136: "BatSerialNO. 5 (pack 2)", 1137: "BatSerialNO. 4 (pack 2)", 1138: "BatSerialNO. 3 (pack 2)", 1139: "BatSerialNO. 2 (pack 2)", + 1140: "BatSerialNO. 1 (pack 2)", 1141: "BatSerialNO. 8 (pack 3)", 1142: "BatSerialNO. 7 (pack 3)", 1143: "BatSerialNO. 6 (pack 3)", 1144: "BatSerialNO. 5 (pack 3)", 1145: "BatSerialNO. 4 (pack 3)", 1146: "BatSerialNO. 3 (pack 3)", 1147: "BatSerialNO. 2 (pack 3)", 1148: "BatSerialNO. 1 (pack 3)", 1149: "BatSerialNO. 8 (pack 4)", + 1150: "BatSerialNO. 7 (pack 4)", 1151: "BatSerialNO. 6 (pack 4)", 1152: "BatSerialNO. 5 (pack 4)", 1153: "BatSerialNO. 4 (pack 4)", 1154: "BatSerialNO. 3 (pack 4)", 1155: "BatSerialNO. 2 (pack 4)", 1156: "BatSerialNO. 1 (pack 4)", 1157: "BatSerialNO. 8 (pack 5)", 1158: "BatSerialNO. 7 (pack 5)", 1159: "BatSerialNO. 6 (pack 5)", + 1160: "BatSerialNO. 5 (pack 5)", 1161: "BatSerialNO. 4 (pack 5)", 1162: "BatSerialNO. 3 (pack 5)", 1163: "BatSerialNO. 2 (pack 5)", 1164: "BatSerialNO. 1 (pack 5)", 1165: "BatSerialNO. 8 (pack 6)", 1166: "BatSerialNO. 7 (pack 6)", 1167: "BatSerialNO. 6 (pack 6)", 1168: "BatSerialNO. 5 (pack 6)", 1169: "BatSerialNO. 4 (pack 6)", + 1170: "BatSerialNO. 3 (pack 6)", 1171: "BatSerialNO. 2 (pack 6)", 1172: "BatSerialNO. 1 (pack 6)", 1173: "BatSerialNO. 8 (pack 7)", 1174: "BatSerialNO. 7 (pack 7)", 1175: "BatSerialNO. 6 (pack 7)", 1176: "BatSerialNO. 5 (pack 7)", 1177: "BatSerialNO. 4 (pack 7)", 1178: "BatSerialNO. 3 (pack 7)", 1179: "BatSerialNO. 2 (pack 7)", + 1180: "BatSerialNO. 1 (pack 7)", 1181: "BatSerialNO. 8 (pack 8)", 1182: "BatSerialNO. 7 (pack 8)", 1183: "BatSerialNO. 6 (pack 8)", 1184: "BatSerialNO. 5 (pack 8)", 1185: "BatSerialNO. 4 (pack 8)", 1186: "BatSerialNO. 3 (pack 8)", 1187: "BatSerialNO. 2 (pack 8)", 1188: "BatSerialNO. 1 (pack 8)", 1189: "BatSerialNO. 8 (pack 9)", + 1190: "BatSerialNO. 7 (pack 9)", 1191: "BatSerialNO. 6 (pack 9)", 1192: "BatSerialNO. 5 (pack 9)", 1193: "BatSerialNO. 4 (pack 9)", 1194: "BatSerialNO. 3 (pack 9)", 1195: "BatSerialNO. 2 (pack 9)", 1196: "BatSerialNO. 1 (pack 9)", 1197: "BatSerialNO. 8 (pack 10)", 1198: "BatSerialNO. 7 (pack 10)", 1199: "BatSerialNO. 6 (pack 10)", + 1200: "BatSerialNO. 5 (pack 10)", 1201: "BatSerialNO. 4 (pack 10)", 1202: "BatSerialNO. 3 (pack 10)", 1203: "BatSerialNO. 2 (pack 10)", 1204: "BatSerialNO. 1 (pack 10)", + 1244: "Com version NameH", 1245: "Com version NameL", 1246: "Com version No", 1247: "Com version NameH", 1248: "Com version NameL", 1249: "Com version No", + 3000: "ExportLimitFailedPowerRate", 3001: "New Serial", 3002: "New Serial", 3003: "New Serial", 3004: "New Serial", 3005: "New Serial", 3006: "New Serial", 3007: "New Serial", 3008: "New Serial", 3009: "New Serial NO", + 3010: "New Serial NO", 3011: "New Serial NO", 3012: "New Serial NO", 3013: "New Serial NO", 3014: "New Serial NO", 3015: "New Serial NO", 3016: "DryContactFuncEn", 3017: "DryContactOnRate", 3018: "bWorkMode", 3019: "DryContactOffRate", + 3020: "BoxCtrlInvOrder", 3021: "ExterCommOffGridEn", 3022: "uwBdcStopWorkOfBusVolt", 3023: "bGridType", 3024: "Float charge current limit", 3025: "VbatWarning", 3026: "VbatlowWarnClr", 3027: "Vbat stop for discharge", 3028: "Vbat stop for charge", 3029: "Vbat start for discharge", + 3030: "Vbat constant charge", 3031: "Battemp lower limit d", 3032: "Bat temp upper limit d", 3033: "Bat temp lower limit c", 3034: "Bat temp upper limit c", 3035: "uwUnderFreDischargeDelyTime", 3036: "GridFirstDischargePowerRate", 3037: "GridFirstStopSOC", 3038: "Time 1(xh) start", 3039: "Time 1(xh) end", + 3040: "Time 2(xh) start", 3041: "Time 2(xh) end", 3042: "Time 3(xh) start", 3043: "Time 3(xh) end", 3044: "Time 4(xh) start", 3045: "Time 4(xh) end", 3047: "Bat First Power Rate", 3048: "wBat First stop SOC", 3049: "AcChargeEnable", + 3050: "Time 5(xh) start", 3051: "Time 5(xh) end", 3052: "Time 6(xh) start", 3053: "Time 6(xh) end", 3054: "Time 7(xh) start", 3055: "Time 7(xh) end", 3056: "Time 8(xh) start", 3057: "Time 8(xh) end", 3058: "Time 9(xh) start", 3059: "Time 9(xh) end", + 3070: "BatteryType", 3071: "BatMdlSeria/ParalNum", 3079: "UpsFunEn", + 3080: "UPSVoltSet", 3081: "UPSFreqSet", 3082: "bLoadFirstStopSocSet", 3085: "Com Address", 3086: "BaudRate", 3087: "Serial NO. 1", 3088: "Serial NO. 2", 3089: "Serial NO. 3", + 3090: "Serial NO. 4", 3091: "Serial No. 5", 3092: "Serial No.6", 3093: "Serial No. 7", 3094: "Serial No. 8", 3095: "BdcResetCmd", 3096: "ARKM3 Code", 3097: "ARKM3 Code 2", 3098: "DTC", 3099: "FW Code", + 3100: "FW Code", 3101: "Processor1 FW Vision", 3102: "BusVoltRef", 3103: "ARKM3Ver", 3104: "BMS_MCUVersion", 3105: "BMS_FW", 3106: "BMS_Info", 3107: "BMSCommType", 3108: "Module 4", 3109: "Module 3", + 3110: "Module 2", 3111: "Module 1", 3112: "Reserved", 3113: "unProtocolVer", 3114: "uwCertificationVer", + 3125: "Time Month1", 3126: "Time Month2", 3127: "Time Month3", 3128: "Time Month4", 3129: "Time 1 (us) start", + 3130: "Time 1 (us) end", 3131: "Time 2 (us) start", 3132: "Time 2 (us) end", 3133: "Time 3 (us) start", 3134: "Time 3 (us) start", 3135: "Time 4 (us) end", 3136: "Time 4 (us) start", 3137: "Time 5 (us) start", 3138: "Time 5 (us) end", 3139: "Time 6 (us) start", + 3140: "Time 6 (us) start", 3141: "Time 7 (us) end", 3142: "Time 7 (us) start", 3143: "Time 8 (us) start", 3144: "Time 8 (us) end", 3145: "Time 9 (us) start", 3146: "Time 9 (us) start", 3147: "Time 10 (us) end", 3148: "Time 10 (us) start", 3149: "Time 11 (us) start", + 3150: "Time 11 (us) end", 3151: "Time 12 (us) start", 3152: "Time 12 (us) start", 3153: "Time 13 (us) end", 3154: "Time 13 (us) start", 3155: "Time 14 (us) start", 3156: "Time 14 (us) end", 3157: "Time 15 (us) start", 3158: "Time 15 (us) start", 3159: "Time 16 (us) end", + 3160: "Time 16 (us) start", 3161: "Time 17 (us) start", 3162: "Time 17 (us) end", 3163: "Time 18 (us) start", 3164: "Time 18 (us) start", 3165: "Time 19 (us) end", 3166: "Time 19 (us) start", 3167: "Time 20 (us) start", 3168: "Time 20 (us) end", 3169: "Time 21 (us) start", + 3170: "Time 21 (us) start", 3171: "Time 22 (us) end", 3172: "Time 22 (us) start", 3173: "Time 23 (us) start", 3174: "Time 23 (us) end", 3175: "Time 24 (us) start", 3176: "Time 24 (us) start", 3177: "Time 25 (us) end", 3178: "Time 25 (us) start", 3179: "Time 26 (us) start", + 3180: "Time 26 (us) end", 3181: "Time 27 (us) start", 3182: "Time 27 (us) start", 3183: "Time 28 (us) end", 3184: "Time 28 (us) start", 3185: "Time 29 (us) start", 3186: "Time 29 (us) end", 3187: "Time 30 (us) start", 3188: "Time 30 (us) start", 3189: "Time 31 (us) end", + 3190: "Time 31 (us) start", 3191: "Time 32 (us) start", 3192: "Time 32 (us) end", 3193: "Time 33 (us) start", 3194: "Time 33 (us) start", 3195: "Time 34 (us) end", 3196: "Time 34 (us) start", 3197: "Time 35 (us) start", 3198: "Time 35 (us) end", 3199: "Time 36 (us) start", + 3200: "Time 36 (us) start", 3201: "SpecialDay1", 3202: "SpecialDay1_Time1 start", 3203: "SpecialDay1_Time1 end", 3204: "SpecialDay1_Time2 start", 3205: "SpecialDay1_Time2 end", 3206: "SpecialDay1_Time3 start", 3207: "SpecialDay1_Time3 end", 3208: "SpecialDay1_Time4 start", 3209: "SpecialDay1_Time4 end", + 3210: "SpecialDay1_Time5 start", 3211: "SpecialDay1_Time5 end", 3212: "SpecialDay1_Time6 start", 3213: "SpecialDay1_Time6 end", 3214: "SpecialDay1_Time7 start", 3215: "SpecialDay1_Time7 end", 3216: "SpecialDay1_Time8 start", 3217: "SpecialDay1_Time8 end", 3218: "SpecialDay1_Time9 start", 3219: "SpecialDay1_Time9 end", + 3220: "SpecialDay2", 3221: "SpecialDay2_Time1 start", 3222: "SpecialDay2_Time1 end", 3223: "SpecialDay2_Time2 start", 3224: "SpecialDay2_Time2 end", 3225: "SpecialDay2_Time3 start", 3226: "SpecialDay2_Time3 end", 3227: "SpecialDay2_Time4 start", 3228: "SpecialDay2_Time4 end", 3229: "SpecialDay2_Time5 start", + 3230: "SpecialDay2_Time5 end", 3231: "SpecialDay2_Time6 start", 3232: "SpecialDay2_Time6 end", 3233: "SpecialDay2_Time7 start", 3234: "SpecialDay2_Time7 end", 3235: "SpecialDay2_Time8 start", 3236: "SpecialDay2_Time8 end", 3237: "SpecialDay2_Time9 start", 3238: "SpecialDay2_Time9 end", +} +# fmt: on + +print("\nWeb settings -> Read register") +print( + f""" ++=================================================================================+ +| Register dump | ++---------------------------------------------------------------------------------+ +| | +""".strip() +) + +if register_read_error: + print(f"| {register_read_error}") +else: + # NEW + print("| Register | Hex | Decimal | ASCII | Register name |") + print("+---------------------------------------------------------------------------------+") + # print register values in int, hex and ascii + for num_, str_ in register_result.items(): + int_ = int(str_) + hex_ = f"{int_:04x}".upper() + try: + if num_ == 0: # OnOff + ascii_ = ["Off", "On", "BDC Off", "BDC On"][int_] + elif num_ == 3: # Power limit + ascii_ = f"{int_}%" + elif num_ in [6, 7]: # Watt peak + int_h = int(register_result.get(6, 0)) + int_l = int(register_result.get(7, 0)) + dword_ = f"{int_h:04x}{int_l:04x}".upper() + ascii_ = f"{int(dword_, 16) // 10}Wp" + elif num_ == 118: + ascii_ = f"S{hex_[:2]}B{hex_[2:]}" + elif num_ == 119: + ascii_ = f"D{hex_[:2]}T{hex_[2:]}" + elif num_ == 120: + ascii_ = f"P{hex_[:2]}U{hex_[2:]}" + elif num_ == 121: + ascii_ = f"M{hex_}" + else: + ascii_ = bytes.fromhex(hex_).decode("ascii") + ascii_ = "".join(c if c.isprintable() else "·" for c in ascii_) + except ValueError: + ascii_ = "··" + print(f"| {num_:04d} | {hex_:4s} | {int_:05d} |{ascii_:^10s}| {register_names.get(num_, ''):38s} | ") + +print( + f""" ++=================================================================================+ +""".lstrip() +) + + +""" +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +> Change inverter settings +> ! These are just examples ! +> ! Do not execute the code unless you are 100% sure what you are doing ! +> ! You might brick your inverter ! +>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> +""" + + +def update_setting(name, params: dict): + # read and log value before overwriting + if (name == "set_any_reg") and ("param1" in params): + before: dict = api.read_inverter_setting( + inverter_id=INVERTER_SN, + device_type="tlx", + register_address=params["param1"], + ) + print( + f"Changing inverter register '{params['param1']}'\n from {before.get('param1') or before.get('error_message')}\n to {params.get('param2')}\nGOOD LUCK" + ) + else: + before: dict = api.read_inverter_setting( + inverter_id=INVERTER_SN, + device_type="tlx", + setting_name=name, + ) + print(f"Changing inverter setting '{name}'\n from {before}\n to {params}\nGOOD LUCK") + upd_result = api.update_tlx_inverter_setting(serial_number=INVERTER_SN, setting_type=name, parameter=params) + print(f"Update returned {upd_result}") + return upd_result + + +# ######################################################################### +# # If you know what you are doing, uncomment according to your needs + +# # Set active power to 100% +# result = update_setting( +# "pv_active_p_rate", { +# "param1": 100, # 100 % +# "param2": 0, # "Whether_to_remember": No=0, Yes=1 +# "param3": None}) +# # Set reactive power to "PF fixed" +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": 1, # fixed at 1 +# "param2": 0, # "PF fixed" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "Set power factor" with factor 1 +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": 1, # factor 1 +# "param2": 1, # "Set power factor" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "Default PF Curve" +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": None, +# "param2": 2, # "Default PF Curve" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "Inductive reactive power ratio" with 100% +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": 100, # 100% +# "param2": 5, # "Inductive reactive power ratio" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "Conductive reactive power ratio" with 100% +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": 100, # 100% +# "param2": 4, # "Conductive reactive power ratio" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "QV mode" +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": None, +# "param2": 6, # "QV mode" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "Set reactive power percentage" to 0% +# result = update_setting( +# "pv_reactive_p_rate", { +# "param1": 0, # 0% +# "param2": 7, # "Set reactive power percentage" +# "param3": 0}) # "Whether_to_remember": No=0, Yes=1 +# # Set reactive power to "Customize PF Curve" +# result = update_setting( +# "tlx_custom_pf_curve", { +# "param1": 255, # Point 1: Power percentage = 255 +# "param2": 1.0, # Point 1: Power factor point = 1.0 +# "param3": 255, # Point 2: Power percentage = 255 +# "param4": 1.0, # Point 2: Power factor point = 1.0 +# "param5": 255, # Point 3: Power percentage = 255 +# "param6": 1.0, # Point 3: Power factor point = 1.0 +# "param7": 255, # Point 4: Power percentage = 255 +# "param8": 1.0}) # Point 4: Power factor point = 1.0 +# # Set "Inverter time" to current local time +# result = update_setting( +# "pf_sys_year", { +# "param1": datetime.datetime.now().isoformat(sep=" ")}) +# # Set "Over voltage / High Grid Voltage Limit" to 253.1 +# result = update_setting( +# "pv_grid_voltage_high", { +# "param1": 253.1}) +# # Set "Under voltage / Low Grid Voltage Limit" to 195.5 +# result = update_setting( +# "pv_grid_voltage_low", { +# "param1": 195.5}) +# # Set "Overfrequency / High Grid Frequency Limit" to 50.1 +# result = update_setting( +# "pv_grid_frequency_high", { +# "param1": 50.1}) +# # Set "Underfrequency / High Grid Frequency Limit" to 47.65 +# result = update_setting( +# "pv_grid_frequency_low", { +# "param1": 47.65}) +# # Set "Export Limitation" to "Disabled" +# result = update_setting( +# "backflow_setting", { +# "param1": 0, # Disabled +# "param2": 0, # W Power (0 for Disabled) +# "param3": 1}) # unknown - recorded from App +# # Set "Export Limitation" to "Enable meter" with 200W +# result = update_setting( +# "backflow_setting", { +# "param1": 1, # Meter +# "param2": 200, # 200W Power +# "param3": 1}) # unknown - recorded from App +# # Set "Failsafe power limit" to 0 +# result = update_setting( +# "backflow_setting", { +# "param1": 0.0, # 0 +# "param2": None, +# "param3": None}) +# # Set "Power Sensor" to "do not connected to power collection" (sic!) +# result = update_setting( +# "tlx_limit_device", { +# "param1": 0, # 0="no device connected", 2="Meter", 3="CT" +# "param2": None, +# "param3": None}) +# # Set "Operating mode" to "Default" +# result = update_setting( +# "system_work_mode", { +# "param1": 0, # 0="Default", 1="System retrofit", 2="Multi-parallel", 3="System retrofit-simplification" +# "param2": None, +# "param3": None}) +# # Perform Factory rest +# result = update_setting( +# "tlx_reset_to_factory", { +# "param1": None, +# "param2": None, +# "param3": None}) +# # Set "Regulation parameter setting" -> "Low Frequency Setting" +# result = update_setting( +# "l_1_freq", { +# "param1": 47.5, # AC Frequency Low 1 +# "param2": 47.5}) # AC Frequency Low 2 +# # Set "Regulation parameter setting" -> "High Frequency Setting" +# result = update_setting( +# "h_1_freq", { +# "param1": 51.5, # AC Frequency High 1 +# "param2": 51.5}) # AC Frequency High 2 +# # Set "Regulation parameter setting" -> "Low Voltage Setting" +# result = update_setting( +# "l_1_freq", { +# "param1": 47.5, # AC Voltage Low 1 +# "param2": 47.5}) # AC Voltage Low 2 +# # Set "Regulation parameter setting" -> "High Voltage Setting" +# result = update_setting( +# "h_1_freq", { +# "param1": 51.5, # AC Voltage High 1 +# "param2": 51.5}) # AC Voltage High 2 +# # Set "Regulation parameter setting" -> "Loading rate" +# result = update_setting( +# "loading_rate", { +# "param1": 9.0}) # 9 % +# # Set "Regulation parameter setting" -> "Restart loading rate" +# result = update_setting( +# "restart_loading_rate", { +# "param1": 9.0}) # 9 % +# # Set "Regulation parameter setting" -> Over-frequency start point(f)" +# result = update_setting( +# "over_fre_drop_point", { +# "param1": 50.2}) # 50.2 Hz +# # Set "Regulation parameter setting" -> "Over-frequency load reduction Gradient(f)" +# result = update_setting( +# "over_fre_lored_slope", { +# "param1": 40.0}) # 40 % +# # Set "Regulation parameter setting" -> "Over-frequency load reduction delay time" +# result = update_setting( +# "over_fre_lored_delaytime", { +# "param1": 0.0}) # 0 ms +# # Set "Q (V)" -> "Cut out high Voltage" +# result = update_setting( +# "qv_h1", { +# "param1": 236.9}) # 236.9 V +# # Set "Q (V)" -> "Cut into high Voltage" +# result = update_setting( +# "qv_h1", { +# "param1": 246.1}) # 246.1 V +# # Set "Q (V)" -> "Cut out low Voltage" +# result = update_setting( +# "qv_h1", { +# "param1": 223.1}) # 223.1 V +# # Set "Q (V)" -> "Cut into low Voltage" +# result = update_setting( +# "qv_h1", { +# "param1": 213.9}) # 213.9 V +# # Set "Q (V)" -> "Delay time" +# result = update_setting( +# "qv_h1", { +# "param1": 10000.0}) # 10000 ms +# # Set "Q (V)" -> "Reactive power percentage" +# result = update_setting( +# "qv_h1", { +# "param1": 43.0}) # 43 % +# # Turn inverter on / off +# result = update_setting( +# "tlx_on_off", { +# "param1": '0001'}) # '0001'=On, '0000'=Off +# # Turn inverter on / off +# result = update_setting( +# "tlx_on_off", { +# "param1": '0001'}) # '0001'=On, '0000'=Off +# +# # Set any register to the desired value +# result = update_setting( +# "set_any_reg", { +# "param1": '0', # register to modify +# "param2": '1'}) # value to set +# +# +# # # Set inverter output power to 600 Wp +# # # first we need to enter standby mode to unlock the setting +# # result = update_setting( +# # "set_any_reg", { +# # "param1": '0', # register 0 = On/Off +# # "param2": '0'}) # 0 = Off +# # # now change to 800Wp +# # result = update_setting( +# # "set_any_reg", { +# # "param1": '121', # register 121 = Inverter Model ...Mxxxx (xxxx = max power) +# # "param2": '6'}) # 6 = 600Wp (Watts/100) +# # # return to On(line) mode +# # result = update_setting( +# # "set_any_reg", { +# # "param1": '0', # register 0 = On/Off +# # "param2": '1'}) # 1 = On + +print("DONE") diff --git a/examples/simple.py b/examples/simple.py index eaba3df..fceeb4d 100755 --- a/examples/simple.py +++ b/examples/simple.py @@ -1,6 +1,10 @@ import growattServer api = growattServer.GrowattApi() -login_response = api.login(, ) -#Get a list of growatt plants. + +username = input("Enter username:") +password = input("Enter password:") +login_response = api.login(username, password) + +# Get a list of growatt plants. print(api.plant_list(login_response['user']['id'])) diff --git a/growattServer/__init__.py b/growattServer/__init__.py index cc53527..ebb196b 100755 --- a/growattServer/__init__.py +++ b/growattServer/__init__.py @@ -1,342 +1,1480 @@ import datetime from enum import IntEnum +from typing import Optional, Dict, Any, Union, Literal, List + import requests from random import randint import warnings import hashlib +from requests import HTTPError + name = "growattServer" BATT_MODE_LOAD_FIRST = 0 BATT_MODE_BATTERY_FIRST = 1 BATT_MODE_GRID_FIRST = 2 -def hash_password(password): + +def hash_password(password: str) -> str: """ Normal MD5, except add c if a byte of the digest is less than 10. + + Args: + password (str): + api password in clear text + + Returns: + str: + MD5-hashed password """ - password_md5 = hashlib.md5(password.encode('utf-8')).hexdigest() + password_md5 = hashlib.md5(password.encode("utf-8")).hexdigest() for i in range(0, len(password_md5), 2): - if password_md5[i] == '0': - password_md5 = password_md5[0:i] + 'c' + password_md5[i + 1:] + if password_md5[i] == "0": + password_md5 = password_md5[0:i] + "c" + password_md5[i + 1 :] + return password_md5 + class Timespan(IntEnum): + """ + Many endpoints require a Timespan to be defined. + Use this enum to provide the data in a convenient way. + + Some endpoints may only support a subset of the Timespan values. + """ + hour = 0 day = 1 month = 2 + year = 3 + total = 4 + + +class TlxDataTypeNeo(IntEnum): + """ + Enum for the type of data to get from the TLX inverter (tlx_data). + + Following data types have been recorded for a "NEO 800M-X". + Other inverters might use different values/mappings. + In that case, define a new data type enum similar to this one + and extend the Union[] in the tlx_data() method's parameter list. + """ + + power_ac = 6 + power_pv = 1 + voltage_pv1 = 2 + current_pv1 = 3 + voltage_pv2 = 4 + current_pv2 = 5 + # other values will return PV power + + +class LanguageCode(IntEnum): + """ + Enum for the language code required by some endpoints + + Values have been reverse-engineered. Feel free to add more if you know more values. + """ + + # Chinese + cn = 0 + # English + en = 1 + uk = 1 + us = 1 + # German + de = 19 + gm = 19 + # Add more if you know any... + class GrowattApi: - server_url = 'https://openapi.growatt.com/' - agent_identifier = "Dalvik/2.1.0 (Linux; U; Android 12; https://github.com/indykoning/PyPi_GrowattServer)" + server_url: str = "https://openapi.growatt.com/" + agent_identifier: str = "Dalvik/2.1.0 (Linux; U; Android 12; https://github.com/indykoning/PyPi_GrowattServer)" + + session: requests.Session - def __init__(self, add_random_user_id=False, agent_identifier=None): - if (agent_identifier != None): - self.agent_identifier = agent_identifier + def __init__(self, add_random_user_id: bool = False, agent_identifier: Optional[str] = None): + if agent_identifier is not None: + self.agent_identifier = agent_identifier - #If a random user id is required, generate a 5 digit number and add it to the user agent - if (add_random_user_id): - random_number = ''.join(["{}".format(randint(0,9)) for num in range(0,5)]) - self.agent_identifier += " - " + random_number + # If a random user id is required, generate a 5-digit number and add it to the user agent + if add_random_user_id: + self.agent_identifier += f" - {randint(0, 99999):05d}" self.session = requests.Session() - self.session.hooks = { - 'response': lambda response, *args, **kwargs: response.raise_for_status() - } + self.session.hooks = {"response": lambda response, *args, **kwargs: response.raise_for_status()} - headers = {'User-Agent': self.agent_identifier} + headers = {"User-Agent": self.agent_identifier} self.session.headers.update(headers) - def __get_date_string(self, timespan=None, date=None): + @staticmethod + def __get_date_string( + timespan: Optional[Timespan] = None, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + ) -> str: + """ + create a date string matching to the Timespan supplied. + e.g. Timespan.month will create "YYYY-MM" while Timespan.day will create "YYYY-MM-DD" + + By default, this will method return current date in day format + + Args: + timespan (Optional[Timespan]) = None: + Timespan definition to use. defaults to Timespan.day if unset + date (Optional[Union[datetime.datetime, datetime.date]]) = datetime.date.today(): + date to format. defaults to today's date. + + Returns: + str: + string of today's date in "YYYY-MM-DD" / "YYYY-MM" / "YYYY" format + """ + if timespan is not None: assert timespan in Timespan if date is None: date = datetime.datetime.now() - date_str="" - if timespan == Timespan.month: - date_str = date.strftime('%Y-%m') + if timespan == Timespan.total: + date_str = date.strftime("%Y") + elif timespan == Timespan.year: + date_str = date.strftime("%Y") + elif timespan == Timespan.month: + date_str = date.strftime("%Y-%m") else: - date_str = date.strftime('%Y-%m-%d') + date_str = date.strftime("%Y-%m-%d") return date_str - def get_url(self, page): + def get_url(self, page: str) -> str: """ Simple helper function to get the page URL. + + Args: + page (str): + Api endpoint name + + Returns: + str: + Api endpoint prefixed with server base url """ - return self.server_url + page - def login(self, username, password, is_password_hashed=False): + return f"{self.server_url.rstrip('/')}/{page}" + + def login(self, username: str, password: str, is_password_hashed: bool = False) -> Dict[str, Any]: """ - Log the user in. + Authenticate user. - Returns - 'data' -- A List containing Objects containing the folowing - 'plantName' -- Friendly name of the plant - 'plantId' -- The ID of the plant - 'service' - 'quality' - 'isOpenSmartFamily' - 'totalData' -- An Object - 'success' -- True or False - 'msg' - 'app_code' - 'user' -- An Object containing a lot of user information - 'uid' - 'userLanguage' - 'inverterGroup' -- A List - 'timeZone' -- A Number - 'lat' - 'lng' - 'dataAcqList' -- A List - 'type' - 'accountName' -- The username - 'password' -- The password hash of the user - 'isValiPhone' - 'kind' - 'mailNotice' -- True or False - 'id' - 'lasLoginIp' - 'lastLoginTime' - 'userDeviceType' - 'phoneNum' - 'approved' -- True or False - 'area' -- Continent of the user - 'smsNotice' -- True or False - 'isAgent' - 'token' - 'nickName' - 'parentUserId' - 'customerCode' - 'country' - 'isPhoneNumReg' - 'createDate' - 'rightlevel' - 'appType' - 'serverUrl' - 'roleId' - 'enabled' -- True or False - 'agentCode' - 'inverterList' -- A list - 'email' - 'company' - 'activeName' - 'codeIndex' - 'appAlias' - 'isBigCustomer' - 'noticeType' + Args: + username (str): + your username + password (str): + your password - cleartext or hashed + is_password_hashed (bool) = False: + set to True if passing a hashed password + + Returns: + Dict[str, Any] + e.g. + { + 'app_code': '1', + 'data': [{'plantId': '{PLANT_ID}', 'plantName': 'My plant name'}], + 'deviceCount': '1', + 'isCheckUserAuth': True, + 'isEicUserAddSmartDevice': True, + 'isOpenDeviceList': 1, + 'isOpenDeviceParams': 0, + 'isOpenSmartFamily': 0, + 'isViewDeviceInfo': False, + 'msg': '', + 'quality': '0', + 'service': '1', + 'success': True, + 'totalData': {}, + 'user': { + 'accountName': '{USER_NAME}', + 'accountNameOss': '{USER_NAME}', + 'activeName': '', + 'agentCode': '{INSTALLER_CODE}', + 'appAlias': '', + 'appType': 'c', + 'approved': False, + 'area': 'Europe', + 'codeIndex': 1, + 'company': '', + 'counrty': 'Germany', + 'cpowerAuth': 'ey...UM', + 'cpowerToken': '01234567890abcdef000000000000000', + 'createDate': '2024-11-30 17:00:00', + 'customerCode': '', + 'dataAcqList': [], + 'distributorEnable': '1', + 'email': '{USER_EMAIL}', + 'enabled': True, + 'id': '{USER_ID}', + 'installerEnable': '1', + 'inverterGroup': [], + 'inverterList': [], + 'isAgent': 0, + 'isBigCustomer': 0, + 'isPhoneNumReg': 0, + 'isValiEmail': 0, + 'isValiPhone': 0, + 'kind': 0, + 'lastLoginIp': '12.3.4.56', + 'lastLoginTime': '2025-01-31 00:00:00', + 'lat': '', + 'lng': '', + 'mailNotice': True, + 'nickName': '', + 'noticeType': '', + 'parentUserId': 0, + 'password': '{PASSWORD_HASH}', + 'phoneNum': '030123456', + 'registerType': '0', + 'rightlevel': 1, + 'roleId': 0, + 'serverUrl': '', + 'smsNotice': False, + 'timeZone': 8, + 'token': '', + 'type': 2, + 'uid': '', + 'userDeviceType': -1, + 'userIconPath': '', + 'userLanguage': 'gm', + 'vipPoints': 10, + 'workerCode': '', + 'wxOpenid': '' + }, + 'userId': '{USER_ID}', + 'userLevel': 1 + } """ + if not is_password_hashed: password = hash_password(password) - response = self.session.post(self.get_url('newTwoLoginAPI.do'), data={ - 'userName': username, - 'password': password - }) - - data = response.json()['back'] - if data['success']: - data.update({ - 'userId': data['user']['id'], - 'userLevel': data['user']['rightlevel'] - }) + response = self.session.post( + self.get_url("newTwoLoginAPI.do"), data={"userName": username, "password": password} + ) + data = response.json()["back"] + + if data["success"]: + data.update({"userId": data["user"]["id"], "userLevel": data["user"]["rightlevel"]}) + return data - def plant_list(self, user_id): + def login_v2( + self, + username: str, + password: str, + is_password_hashed: bool = False, + language_code: Union[LanguageCode, int] = LanguageCode.en, + app_type: Optional[str] = "ShinePhone", + ipvcpc: Optional[str] = None, + phone_model: Optional[str] = None, + phone_sn: Optional[str] = None, + phone_type: Optional[str] = None, + shine_phone_version: Optional[str] = None, + system_version: Optional[int] = None, + ) -> Dict[str, Any]: """ - Get a list of plants connected to this account. + Authenticate user to API + + Most parameters are optional (except username and password) and are not needed to set in normal operation. + Thus they might come in handy for debugging. Args: - user_id (str): The ID of the user. + username (str): + your username + password (str): + your password - cleartext or hashed + is_password_hashed (bool) = False: + set to True if passing a hashed password + app_type (Optional[str]) = "ShinePhone" + e.g. "ShinePhone" + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + ipvcpc (Optional[str]) = None: + e.g. "ffffffff-000-0000-0000-000000000000" + phone_model (Optional[str]= = None: + e.g. "AB-00000" + phone_sn (Optional[str]) = None: + e.g. "ffffffff-0000-0000-00000000000000000" + phone_type (Optional[str]) = None: + e.g. "pad" + shine_phone_version (Optional[str]) = None: + e.g. "8.2.7.0" + system_version: (Optional[int]) = None: + e.g. 9 Returns: - list: A list of plants connected to the account. + Dict[str, Any] + e.g. + { + 'app_code': '1', + 'data': [{'plantId': '{PLANT_ID}', 'plantName': 'My plant name'}], + 'deviceCount': '1', + 'isCheckUserAuth': True, + 'isEicUserAddSmartDevice': True, + 'isOpenDeviceList': 1, + 'isOpenDeviceParams': 0, + 'isOpenSmartFamily': 0, + 'isViewDeviceInfo': False, + 'msg': '', + 'quality': '0', + 'service': '1', + 'success': True, + 'totalData': {}, + 'user': { + 'accountName': '{USER_NAME}', + 'accountNameOss': '{USER_NAME}', + 'activeName': '', + 'agentCode': '{INSTALLER_CODE}', + 'appAlias': '', + 'appType': 'c', + 'approved': False, + 'area': 'Europe', + 'codeIndex': 1, + 'company': '', + 'counrty': 'Germany', + 'cpowerAuth': 'ey...UM', + 'cpowerToken': '01234567890abcdef000000000000000', + 'createDate': '2024-11-30 17:00:00', + 'customerCode': '', + 'dataAcqList': [], + 'distributorEnable': '1', + 'email': '{USER_EMAIL}', + 'enabled': True, + 'id': '{USER_ID}', + 'installerEnable': '1', + 'inverterGroup': [], + 'inverterList': [], + 'isAgent': 0, + 'isBigCustomer': 0, + 'isPhoneNumReg': 0, + 'isValiEmail': 0, + 'isValiPhone': 0, + 'kind': 0, + 'lastLoginIp': '12.3.4.56', + 'lastLoginTime': '2025-01-31 00:00:00', + 'lat': '', + 'lng': '', + 'mailNotice': True, + 'nickName': '', + 'noticeType': '', + 'parentUserId': 0, + 'password': '{PASSWORD_HASH}', + 'phoneNum': '030123456', + 'registerType': '0', + 'rightlevel': 1, + 'roleId': 0, + 'serverUrl': '', + 'smsNotice': False, + 'timeZone': 8, + 'token': '', + 'type': 2, + 'uid': '', + 'userDeviceType': -1, + 'userIconPath': '', + 'userLanguage': 'gm', + 'vipPoints': 10, + 'workerCode': '', + 'wxOpenid': '' + }, + 'userId': '{USER_ID}', + 'userLevel': 1 + } + """ + + if not is_password_hashed: + password = hash_password(password) + + url = self.get_url("newTwoLoginAPI.do") + data = { + key: value + for key, value in { + "userName": username, + "password": password, + "newLogin": 1, + "loginTime": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + "timestamp": int(datetime.datetime.now().timestamp() * 1000), + "appType": app_type, + "ipvcpc": ipvcpc, + "language": int(language_code), + "phoneModel": phone_model, + "phoneSn": phone_sn, + "phoneType": phone_type, + "shinephoneVersion": shine_phone_version, + "systemVersion": system_version, + }.items() + if value is not None + } + + try: + response = self.session.post(url=url, data=data) + except HTTPError as e: + if e.response.status_code == 403: + # extend error message, to inform about user agent + e_args = list(e.args) # cannot edit tuple + e_args[0] += " (User agent seems to be blocked - see README)" + e.args = tuple(e_args) + raise e + + data = response.json().get("back", {}) - Raises: - Exception: If the request to the server fails. + # check login successful + if not data.get("success"): + error_msg = f"Login failed: Error {data.get('msg')} \"{data.get('error')}\"" + print(error_msg) + raise Exception(error_msg) + + # stay compatible with login() + data["userId"] = data.get("user", {}).get("id") + data["userLevel"] = data.get("user", {}).get("rightlevel") + + return data + + def plant_list(self, user_id: Union[str, int]) -> Dict[str, Any]: """ - response = self.session.get( - self.get_url('PlantListAPI.do'), - params={'userId': user_id}, - allow_redirects=False - ) + Get a list of plants connected to this account. - return response.json().get('back', []) + Args: + user_id (Union[str,int]): + The ID of the user. - def plant_detail(self, plant_id, timespan, date=None): + Returns: + Dict[str, Any]: + "data": A list of plants connected to the account. + "totalData": Accumulated energy data for all plants connected to the account. + e.g. + { + 'success': True, + 'data': [ + { + 'currentPower': '0 W', + 'isHaveStorage': 'false', + 'plantId': '{PLANT_ID}', + 'plantMoneyText': '1.2 ', + 'plantName': '{PLANT_NAME}', + 'todayEnergy': '0 kWh', + 'totalEnergy': '12.3 kWh' + } + ], + 'totalData': { + 'CO2Sum': '12.34 T', + 'currentPowerSum': '0 W', + 'eTotalMoneyText': '1.2 ', + 'isHaveStorage': 'false', + 'todayEnergySum': '0 kWh', + 'totalEnergySum': '12.3 kWh' + } + } + """ + + response = self.session.get(self.get_url("PlantListAPI.do"), params={"userId": user_id}, allow_redirects=False) + + return response.json().get("back", {}) + + def plant_detail( + self, + plant_id: Union[str, int], + timespan: Literal[Timespan.day, Timespan.month, Timespan.year, Timespan.total] = Timespan.day, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + ) -> Dict[str, Any]: """ Get plant details for specified timespan. Args: - plant_id (str): The ID of the plant. - timespan (Timespan): The ENUM value conforming to the time window you want e.g. hours from today, days, or months. - date (datetime, optional): The date you are interested in. Defaults to datetime.datetime.now(). + plant_id (Union[str, int]): + The ID of the plant. + timespan (Timespan) = Timespan.day: + The ENUM value conforming to the time window you want + * day: return daily values for one month + * month: return monthly values for one year + * year: return yearly values for six years + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + The date you are interested in. Returns: - dict: A dictionary containing the plant details. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the plant energy metrics for specified timespan. + e.g. + { + 'success': True, + 'data': { + '01': '0.5', + '02': '0', + #... + '31': '0.0', + }, + 'plantData': { + 'currentEnergy': '1 kWh', + 'plantId': '{PLANT_ID}', + 'plantMoneyText': '1.2 ', + 'plantName': '{PLANT_NAME}' + }, + } """ - date_str = self.__get_date_string(timespan, date) - response = self.session.get(self.get_url('PlantDetailAPI.do'), params={ - 'plantId': plant_id, - 'type': timespan.value, - 'date': date_str - }) + date_str = self.__get_date_string(timespan, date) - return response.json().get('back', {}) + response = self.session.get( + self.get_url("PlantDetailAPI.do"), params={"plantId": plant_id, "type": timespan.value, "date": date_str} + ) - def plant_list_two(self): + return response.json().get("back", {}) + + def plant_list_two( + self, + language_code: Union[LanguageCode, int] = LanguageCode.en, + page_size: int = 20, + page_num: int = 1, + nominal_power: Optional[str] = None, + plant_name: Optional[str] = None, + plant_status: Optional[str] = None, + ) -> List[Dict[str, Any]]: """ Get a list of all plants with detailed information. + Args: + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + page_size (int) = 20: + Number of items per page + page_num (int) = 1: + Page number + nominal_power (Optional[str]) = None: + query by plant peak power + plant_name (Optional[str]) = None: + query by plant name + plant_status (Optional[str]) = None: + query by plant status + Returns: - list: A list of plants with detailed information. + List[Dict[str, Any]]: + A list of plants with detailed information. + e.g. + [ + { + "alarmValue": 0, + "alias": "{PLANT_NAME}", + "children": [], + "city": "{CITY}", + "companyName": "", + "country": "", + "createDate": { + "year": 124, # 2024 is returned as 124 (sic!) + "month": 12, + "day": 1, # weekday + "date": 27, + "hours": 0, + "minutes": 0, + "seconds": 0, + "timezoneOffset": -480, + "time": 1700000000000, + }, + "createDateText": "2024-12-01", + "createDateTextA": "", + "currentPac": 0, + "currentPacStr": "0kW", + "currentPacTxt": "0", + "dataLogList": [], + "defaultPlant": False, + "designCompany": "", + "deviceCount": 1, + "emonthCo2Text": "0", + "emonthCoalText": "0", + "emonthMoneyText": "0", + "emonthSo2Text": "0", + "energyMonth": 0, + "energyYear": 0, + "envTemp": 0, + "eToday": 0, + "etodayCo2Text": "0", + "etodayCoalText": "0", + "etodayMoney": 0, + "etodayMoneyText": "0", + "etodaySo2Text": "0", + "eTotal": 12.345678, + "etotalCo2Text": "27", + "etotalCoalText": "10.8", + "etotalFormulaTreeText": "1.49", + "etotalMoney": 8.13, + "etotalMoneyText": "8.1", + "etotalSo2Text": "0.8", + "eventMessBeanList": [], + "EYearMoneyText": "0", + "fixedPowerPrice": 0, + "flatPeriodPrice": 0, + "formulaCo2": 0, + "formulaCoal": 0, + "formulaMoney": 0.30, + "formulaMoneyStr": "", + "formulaMoneyUnitId": "EUR", + "formulaSo2": 0, + "formulaTree": 0, + "gridCompany": "", + "gridLfdi": "", + "gridPort": "", + "gridServerUrl": "", + "hasDeviceOnLine": 0, + "hasStorage": 0, + "id": {PLANT_ID}, + "imgPath": "css/img/plant.gif", + "installMapName": "", + "irradiance": 0, + "isShare": None, + "latitude_d": "", + "latitude_f": "", + "latitude_m": "", + "latitudeText": "null°null?null?", + "level": 1, + "locationImgName": "", + "logoImgName": "", + "longitude_d": "", + "longitude_f": "", + "longitude_m": "", + "longitudeText": "null°null?null?", + "map_areaId": 0, + "map_cityId": 0, + "map_countryId": 0, + "map_provinceId": 0, + "mapCity": "", + "mapLat": "", + "mapLng": "", + "moneyUnitText": "€", + "nominalPower": 800, + "nominalPowerStr": "0.8kWp", + "onLineEnvCount": 0, + "pairViewUserAccount": "", + "panelTemp": 0, + "paramBean": None, + "parentID": "", + "peakPeriodPrice": 0, + "phoneNum": "", + "plant_lat": "", + "plant_lng": "", + "plantAddress": "{STREET}", + "plantFromBean": None, + "plantImgName": "", + "plantName": "{PLANT_NAME}" + "plantNmi": "", + "plantType": 0, + "prMonth": "", + "protocolId": "", + "prToday": "", + "remark": "", + "status": 0, + "storage_BattoryPercentage": 0, + "storage_eChargeToday": 0, + "storage_eDisChargeToday": 0, + "storage_TodayToGrid": 0, + "storage_TodayToUser": 0, + "storage_TotalToGrid": 0, + "storage_TotalToUser": 0, + "tempType": 0, + "timezone": 1, + "timezoneText": "GMT+1", + "timezoneValue": "+1:00", + "treeID": "PLANT_{PLANT_ID}", + "treeName": "{PLANT_NAME}", + "unitMap": None, + "userAccount": "{USER_NAME}", + "userBean": None, + "valleyPeriodPrice": 0, + "windAngle": 0, + "windSpeed": 0, + } + ] """ + response = self.session.post( - self.get_url('newTwoPlantAPI.do'), - params={'op': 'getAllPlantListTwo'}, + self.get_url("newTwoPlantAPI.do"), + params={"op": "getAllPlantListTwo"}, data={ - 'language': '1', - 'nominalPower': '', - 'order': '1', - 'pageSize': '15', - 'plantName': '', - 'plantStatus': '', - 'toPageNum': '1' - } + k: v + for k, v in { + "language": int(language_code), + "pageSize": page_size, + "toPageNum": page_num, + "nominalPower": nominal_power, + "plantName": plant_name, + "plantStatus": plant_status, + "order": 1, + }.items() + if v is not None + }, ) - return response.json().get('PlantList', []) + return response.json().get("PlantList", []) - def inverter_data(self, inverter_id, date=None): + def inverter_data( + self, inverter_id: str, date: Optional[Union[datetime.datetime, datetime.date]] = None + ) -> Dict[str, Any]: """ Get inverter data for specified date or today. Args: - inverter_id (str): The ID of the inverter. - date (datetime, optional): The date you are interested in. Defaults to datetime.datetime.now(). + inverter_id (str): + The ID of the inverter. + date (Optional[Union[datetime.datetime, datetime.date]]) = datetime.date.today(): + date to format. defaults to today's date. Returns: - dict: A dictionary containing the inverter data. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the inverter data. + Might not work for all inverter types. + e.g. NEO inverter returns {'msg': '501', 'success': False} """ date_str = self.__get_date_string(date=date) - response = self.session.get(self.get_url('newInverterAPI.do'), params={ - 'op': 'getInverterData', - 'id': inverter_id, - 'type': 1, - 'date': date_str - }) + response = self.session.get( + self.get_url("newInverterAPI.do"), + params={"op": "getInverterData", "id": inverter_id, "type": 1, "date": date_str}, + ) return response.json() - def inverter_detail(self, inverter_id): + def inverter_detail( + self, + inverter_id: str, + ) -> Dict[str, Any]: """ Get detailed data from PV inverter. + If you're missing a specific attribute, consider trying inverter_detail_two() Args: - inverter_id (str): The ID of the inverter. + inverter_id (str): + The ID of the inverter. Returns: - dict: A dictionary containing the inverter details. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the inverter details. + e.g. + { + 'again': False, + 'bigDevice': False, + 'currentString1': 0, + 'currentString2': 0, + 'currentString3': 0, + 'currentString4': 0, + 'currentString5': 0, + 'currentString6': 0, + 'currentString7': 0, + 'currentString8': 0, + 'dwStringWarningValue1': 0, + 'eRacToday': 0, + 'eRacTotal': 0, + 'epv1Today': 0, + 'epv1Total': 0, + 'epv2Today': 0, + 'epv2Total': 0, + 'epvTotal': 0, + 'fac': 0, + 'faultType': 0, + 'iPidPvape': 0, + 'iPidPvbpe': 0, + 'iacr': 0, + 'iacs': 0, + 'iact': 0, + 'id': 0, + 'inverterBean': None, + 'inverterId': '', + 'ipmTemperature': 0, + 'ipv1': 0, + 'ipv2': 0, + 'ipv3': 0, + 'nBusVoltage': 0, + 'opFullwatt': 0, + 'pBusVoltage': 0, + 'pac': 0, + 'pacr': 0, + 'pacs': 0, + 'pact': 0, + 'pf': 0, + 'pidStatus': 0, + 'powerToday': 0, + 'powerTotal': 0, + 'ppv': 0, + 'ppv1': 0, + 'ppv2': 0, + 'ppv3': 0, + 'rac': 0, + 'realOPPercent': 0, + 'status': 0, + 'statusText': 'Waiting', + 'strFault': 0, + 'temperature': 0, + 'time': '2025-01-02 03:04:05', + 'timeCalendar': None, + 'timeTotal': 0, + 'timeTotalText': '0', + 'vPidPvape': 0, + 'vPidPvbpe': 0, + 'vString1': 0, + 'vString2': 0, + 'vString3': 0, + 'vString4': 0, + 'vString5': 0, + 'vString6': 0, + 'vString7': 0, + 'vString8': 0, + 'vacr': 0, + 'vacs': 0, + 'vact': 0, + 'vpv1': 0, + 'vpv2': 0, + 'vpv3': 0, + 'wPIDFaultValue': 0, + 'wStringStatusValue': 0, + 'warnCode': 0, + 'warningValue1': 0, + 'warningValue2': 0 + } """ - response = self.session.get(self.get_url('newInverterAPI.do'), params={ - 'op': 'getInverterDetailData', - 'inverterId': inverter_id - }) + + response = self.session.get( + self.get_url("newInverterAPI.do"), params={"op": "getInverterDetailData", "inverterId": inverter_id} + ) return response.json() - def inverter_detail_two(self, inverter_id): + def inverter_detail_two( + self, + inverter_id: str, + ) -> Dict[str, Any]: """ Get detailed data from PV inverter (alternative endpoint). + Returns more attributes than inverter_detail() Args: - inverter_id (str): The ID of the inverter. + inverter_id (str): + The ID of the inverter. Returns: + Dict[str, Any]: dict: A dictionary containing the inverter details. - - Raises: - Exception: If the request to the server fails. + e.g. + { + 'data': { + 'e_rac_today': 0.0, + 'e_rac_total': 0.0, + 'e_today': 0.0, + 'e_total': 0.0, + 'fac': 0.0, + 'iacr': 0.0, + 'iacs': 0.0, + 'iact': 0.0, + 'ipv1': 0.0, + 'ipv2': 0.0, + 'ipv3': 0.0, + 'istring1': 0.0, + 'istring2': 0.0, + 'istring3': 0.0, + 'istring4': 0.0, + 'istring5': 0.0, + 'istring6': 0.0, + 'istring7': 0.0, + 'istring8': 0.0, + 'pac': 0.0, + 'pacr': 0.0, + 'pacs': 0.0, + 'pact': 0.0, + 'pidwarning': 0, + 'ppv': 0.0, + 'ppv1': 0.0, + 'ppv2': 0.0, + 'ppv3': 0.0, + 'rac': 0.0, + 'strbreak': 0, + 'strfault': 0, + 'strwarning': 0, + 't_total': 0.0, + 'vacr': 0.0, + 'vacs': 0.0, + 'vact': 0.0, + 'vpv1': 0.0, + 'vpv2': 0.0, + 'vpv3': 0.0, + 'vstring1': 0.0, + 'vstring2': 0.0, + 'vstring3': 0.0, + 'vstring4': 0.0, + 'vstring5': 0.0, + 'vstring6': 0.0, + 'vstring7': 0.0, + 'vstring8': 0.0 + }, + 'parameterName': 'Fac(Hz),Pac(W),E_Today(kWh),E_Total(kWh),Vpv1(V),Ipv1(A),' + 'Ppv1(W),Vpv2(V),Ipv2(A),Ppv2(W),Vpv3(V),Ipv3(A),Ppv3(W),' + 'Ppv(W),VacR(...V),Istring5(A),Vstring6(V),Istring6(A),' + 'Vstring7(V),Istring7(A),Vstring8(V),Istring8(A),StrFault,' + 'StrWarning,StrBreak,PIDWarning' + } """ - response = self.session.get(self.get_url('newInverterAPI.do'), params={ - 'op': 'getInverterDetailData_two', - 'inverterId': inverter_id - }) + + response = self.session.get( + self.get_url("newInverterAPI.do"), params={"op": "getInverterDetailData_two", "inverterId": inverter_id} + ) return response.json() - def tlx_system_status(self, plant_id, tlx_id): + def inverter_energy_chart( + self, + plant_id: Union[str, int], + inverter_id: str, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + timespan: Literal[Timespan.day, Timespan.month, Timespan.year, Timespan.total] = Timespan.day, + filter_type: Literal["all", "ac", "pv1", "pv2", "pv3", "pv4"] = "all", + ) -> Dict[str, Any]: """ - Get status of the system + Get energy chart data. + + Values can be seen in the Web frontend at "Module" -> "Module data" + -> "Daily energy curve" + -> "Electricity" + + Values can be seen in the App at "Plant" -> "Panel Data" + -> "Daily performance curve" + -> "Production" Args: - plant_id (str): The ID of the plant. - tlx_id (str): The ID of the TLX inverter. + plant_id (Union[str, int]): + The ID of the plant. + inverter_id (str): + The ID of the inverter. + timespan (Timespan) = Timespan.day: + The ENUM value conforming to the time window you want e.g. hours from today, days, or months. + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + The date you are interested in. + filter_type (Literal["all", "ac", "pv1", "pv2", "pv3", "pv4"]) = "all": + Restrict output to + * "all": all data + * "ac": AC (W/F/V/I), T (°C), AP (W) + * "pv{1..4}": PV{1..4} (W/V/I) Returns: - dict: A dictionary containing system status. + Dict[str, Any]: + A dictionary containing the energy chart data. + If timespan == Timespan.day: + return daily energy curve (e.g. PV1/2/3/4 (W/V/A), AC(W/V/A/Hz), Temp (°C)) + Else: + return energy history (e.g. PV1/2/3/4 (kWh), AC(kWH)) + + Example response: + * Timespan.day + { + 'time': ['08:35', ..., '15:40'], + 'ppv1': ['14.8', ..., '1.9'], + 'vpv1': ['29.0', ..., '29.7'], + 'ipv1': ['0.5', ..., '0'], + 'ppv2': ['0', ..., '0'], + 'vpv2': ['10.0', ..., '9.9'], + 'ipv2': ['0', '..., '0'], + 'ppv3': ['0', ..., '0'], + 'vpv3': ['0', ..., '0'], + 'ipv3': ['0', ..., '0'], + 'ppv4': ['0', ..., '0'], + 'vpv4': ['0', ..., '0'] + 'ipv4': ['0', ..., '0'], + 'pac': ['10.4', ..., '7.0'], + 'vac1': ['231.1', ..., '233.0'], + 'iac1': ['0.2', ..., '0.2'], + 'fac': ['50.0', ..., '50.0'], + 'temp1': ['5.7', ..., '9.6'], + } + * Timespan.month + { + 'time': ['1', ..., '31'], + 'eacChargeEnergy': ['0', ..., '0'], + 'epv1Energy': ['0.4', ..., '12.0'], + 'epv2Energy': ['0.3', ..., '12.3'], + 'epv3Energy': ['0', ..., '0'], + 'epv4Energy': ['0', ..., '0'] + } + * Timespan.year + { + 'time': ['1', ..., '12'], + '...': see Timespan.month + } + * Timespan.total + { + 'time': ['2020', ..., '2025'], + '...': see Timespan.month + } + """ + + # dataType: + # 0: Daily performance curve (supports YYYY-MM-DD) + # 1: Production (supports YYYY-MM / YYYY) + if timespan == Timespan.day: # 5min performance + data_type = 0 + else: # daily/monthly/yearly production + data_type = 1 + + # in contrast to other endpoints, this one always expects a full date string + date = date or datetime.date.today() + date_str = date.strftime("%Y-%m-%d") + + # in contrast to other endpoints, this uses a different logic mapping timespan to int + # 0: Daily performance curve - day + # 1: Production (daily values) - month + # 2: Production (monthly values) - year + # 3: Production (yearly values) - total + date_type = { + Timespan.total: 3, + Timespan.year: 2, + Timespan.month: 1, + Timespan.day: 0, + }[timespan] + + # filter for desired data + data_level = { + "all": 0, + "ac": 1, + "pv1": 2, + "pv2": 3, + "pv3": 4, + "pv4": 5, + }.get(filter_type, 0) + + url = self.get_url("componentsApi/getData") + data = { + "plantId": plant_id, + "sn": inverter_id, + # dataType: + # 0: Daily energy curve (supports YYYY-MM-DD) + # 1: Electricity (supports YYYY-MM / YYYY) + "dataType": data_type, + # dateStr + # YYYY-MM-DD (always) + "dateStr": date_str, + # dateType: + # 0: day (5min values) + # 1: month (daily values) + # 2: year (monthly values) + # 3: total (yearly values) + "dateType": date_type, + # dataLevel: + # 0: All + # 1: Output + # 2: PV1 + # 3: PV2 + # 4: PV3 + # 5: PV4 + "dataLevel": data_level, + } + + response = self.session.post(url=url, data=data) + + return response.json().get("obj") or {} + + def inverter_panel_energy_chart( + self, + plant_id: str, + inverter_id: Optional[str] = None, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + timespan: Literal[Timespan.day, Timespan.month, Timespan.year, Timespan.total] = Timespan.day, + ) -> Dict[str, Any]: + """ + Get energy chart data. + * for each panel separately (see "box" in response) + * total for all panels + + Values can be seen in the Web frontend at "Module" -> "Module view" + -> "Panel power (W)" + -> "Module power (W)" + + Values can be seen in the App at "Plant" -> "Panel View" + -> "Panel power" + -> "Panel production" + + Args: + plant_id (str): + The ID of the plant. + inverter_id (Optional[str]) = None: + set to filter for a specific inverter + timespan (Timespan): + The ENUM value conforming to the time window you want e.g. hours from today, days, or months. + date (Union[datetime.datetime, datetime.date]) = Timespan.day: + The date you are interested in. + + Returns: + Dict[str, Any]: + A dictionary containing the energy chart data. + If timespan == Timespan.day: + return daily energy curve (e.g. PV1/2/3/4 (W/V/A), AC(W/V/A/Hz), Temp (°C)) + Else: + return energy history (e.g. PV1/2/3/4 (kWh), AC(kWH)) + + Example response: + * Timespan.day + { + 'box': { + '09:25': [ + { + 'current': '0', + 'datalogSn': '{DATALOGGER_ID}', + 'energy': '0', + 'id': '{INVERTER_ID}-PV1', + 'name': '{INVERTER_ID_LAST_3_DIGITS}-PV1', + 'power': '1.2', + 'voltage': '0', + 'x': 0, + 'y': 0 + }, + { + 'current': '0', + 'datalogSn': '{DATALOGGER_ID}', + 'energy': '0', + 'id': '{INVERTER_ID}-PV2', + 'name': '{INVERTER_ID_LAST_3_DIGITS}-PV2', + 'power': '0.0', + 'voltage': '0', + 'x': 1, + 'y': 0 + } + ], + '09:30': [ + {'current': '0', ...}, + {'current': '0', ...} + ], + ... + }, + 'chart': { + 'power': ['1.2', '3.4', '5.6', ...], + 'time': ['09:25', '09:30', '09:35', ...] + }, + 'checkNeoNum': False + } + * Timespan.month + { + 'box': { + '1': [ + { + 'current': '0', + 'datalogSn': '{DATALOGGER_ID}', + 'energy': '1.3', + 'id': '{INVERTER_ID}-PV1', + 'name': '{INVERTER_ID_LAST_3_DIGITS}-PV1', + 'power': '0', + 'voltage': '0', + 'x': 0, + 'y': 0 + }, + {'current': '0', + 'datalogSn': '{DATALOGGER_ID}', + 'energy': '0', + 'id': '{INVERTER_ID}-PV2', + 'name': '{INVERTER_ID_LAST_3_DIGITS}-PV2', + 'power': '0', + 'voltage': '0', + 'x': 1, + 'y': 0 + } + ], + '10': [ + { + 'current': '0', + # ... + }, + { + 'current': '0', + # ... + } + ], + # ... + }, + 'chart': { + 'energy': ['1.2', '0.1', '1.1', ...], + 'time': ['1', '2', '3', ..., '31'] + }, + 'checkNeoNum': False + } + * Timespan.year + same as month but with + 'time': ['1', '2', '3', ..., '12'] + * Timespan.total + same as month but with + 'time': ['2020', '2021', '2022', '2023', '2024', '2025'] + """ + + # dataType: + # 0: panel power (today power) + # 1: panel production (historical energy) + if timespan == Timespan.day: # 5min performance + data_type = 0 + else: # daily/monthly/yearly production + data_type = 1 + + # in contrast to other endpoints, this one always expects a full date string + date = date or datetime.date.today() + date_str = date.strftime("%Y-%m-%d") + + # in contrast to other endpoints, this uses a different logic mapping timespan to int + # and yes, also different from inverter_energy_chart + # 0: Daily performance curve - day + # 1: Production (daily values) - month + # 2: Production (monthly values) - year + # 3: Production (yearly values) - total + date_type = { + Timespan.total: 2, # historical energy (yearly values) + Timespan.year: 1, # historical energy (monthly values) + Timespan.month: 0, # historical energy (daily values) + Timespan.day: 0, # panel power (today power) + }[timespan] + + url = self.get_url("componentsApi/getViewData") + data = { + "plantId": plant_id, + # snList: + # inverter id + # or 'all' + "snList": inverter_id or "all", + # dataType: + # 0: 5min power (panel power) + # 1: daily/monthly/yearly energy (panel production) + "dataType": data_type, + # dateStr + # YYYY-MM-DD (always) + "dateStr": date_str, + # dateType: + # 0: day (5min values) + # 1: month (daily values) + # 2: year (monthly values) + # 3: total (yearly values) + "dateType": date_type, + } + + response = self.session.post(url=url, data=data) - Raises: - Exception: If the request to the server fails. + return response.json().get("obj") or {} + + def tlx_system_status(self, plant_id: Union[str, int], tlx_id: str) -> Dict[str, Any]: + """ + Get status of the system + + Args: + plant_id (Union[str, int]): + The ID of the plant. + tlx_id (str): + The ID of the TLX inverter. + + Returns: + Dict[str, Any]: + A dictionary containing system status. + e.g. + { + 'bMerterConnectFlag': '0', + 'bdcStatus': '0', + 'bmsBatteryEnergy': '0kWh', + 'chargePower': '0.02', + 'chargePower1': '0', + 'chargePower2': '0', + 'dType': '5', + 'deviceType': '2', + 'fAc': '50', + 'invStatus': '-10', + 'isMasterOne': '0', + 'lost': 'tlx.status.checking', + 'operatingMode': '0', + 'pLocalLoad': '0', + 'pPv1': '0.02', + 'pPv2': '0', + 'pPv3': '0', + 'pPv4': '0', + 'pac': '0.01', + 'pactogrid': '0', + 'pactouser': '0', + 'pdisCharge': '0', + 'pdisCharge1': '0', + 'pdisCharge2': '0', + 'pex': '0', + 'pmax': '0.8', + 'ppv': '0.02', + 'prePto': '-1', + 'priorityChoose': '0', + 'SOC': '0', + 'soc1': '0', + 'SOC2': '0', + 'soc2': '0', + 'socType': '1', + 'status': '1', + 'tbModuleNum': '0', + 'tips': '2', + 'unit': 'kW', + 'upsFac': '0', + 'upsVac1': '0', + 'uwSysWorkMode': '1', + 'vAc1': '232.1', + 'vBat': '0', + 'vPv1': '29.5', + 'vPv2': '10', + 'vPv3': '0', + 'vPv4': '0', + 'vac1': '232.1', + 'wBatteryType': '0' + } """ + response = self.session.post( - self.get_url("newTlxApi.do"), - params={"op": "getSystemStatus_KW"}, - data={"plantId": plant_id, - "id": tlx_id} + self.get_url("newTlxApi.do"), params={"op": "getSystemStatus_KW"}, data={"plantId": plant_id, "id": tlx_id} ) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def tlx_energy_overview(self, plant_id, tlx_id): + def tlx_energy_overview(self, plant_id: Union[str, int], tlx_id: str) -> Dict[str, Any]: """ Get energy overview Args: - plant_id (str): The ID of the plant. - tlx_id (str): The ID of the TLX inverter. + plant_id (Union[str, int]): + The ID of the plant. + tlx_id (str): + The ID of the TLX inverter. Returns: - dict: A dictionary containing energy data. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing energy data. + e.g. + { + 'echargetoday': '0', + 'echargetotal': '0', + 'edischargeToday': '0', + 'edischargeTotal': '0', + 'elocalLoadToday': '0', + 'elocalLoadTotal': '0', + 'epvToday': '0', + 'epvTotal': '12.3', + 'etoGridToday': '0', + 'etogridTotal': '0', + 'isMasterOne': '0' + } """ + response = self.session.post( - self.get_url("newTlxApi.do"), - params={"op": "getEnergyOverview"}, - data={"plantId": plant_id, - "id": tlx_id} + self.get_url("newTlxApi.do"), params={"op": "getEnergyOverview"}, data={"plantId": plant_id, "id": tlx_id} ) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def tlx_energy_prod_cons(self, plant_id, tlx_id, timespan=Timespan.hour, date=None): + def tlx_energy_prod_cons( + self, + plant_id: Union[str, int], + tlx_id: str, + timespan: Literal[Timespan.hour, Timespan.day, Timespan.month, Timespan.year] = Timespan.hour, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + language_code: Union[LanguageCode, int] = LanguageCode.en, + ): """ Get energy production and consumption (KW) + Note: output format for "hour" differs from other timespans + Args: - tlx_id (str): The ID of the TLX inverter. - timespan (Timespan): The ENUM value conforming to the time window you want e.g. hours from today, days, or months. - date (datetime): The date you are interested in. + plant_id (Union[str, int]): + The ID of the plant. + tlx_id (str): + The ID of the TLX inverter. + timespan (Timespan): + The ENUM value conforming to the time window you want + * hour => one day, 5min values (power) + * day => one month, daily values (energy) + * month => one year, monthly values (energy) + * year => six years, yearly values (energy) + date (Optional[Union[datetime.datetime, datetime.date]]) = datetime.date.today(): + The date you are interested in. + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode Returns: dict: A dictionary containing energy data. - - Raises: - Exception: If the request to the server fails. + e.g. (Timespan.hour) + { + 'chartData': { + '00:00': {'chargePower': 0, 'outP': 0, 'pacToGrid': 0, 'pacToUser': 0, 'pex': 0, 'ppv': 0, 'pself': 0, 'sysOut': 0, 'userLoad': 0}, + '00:05': {'chargePower': 0, 'outP': 0, 'pacToGrid': 0, 'pacToUser': 0, 'pex': 0, 'ppv': 0, 'pself': 0, 'sysOut': 0, 'userLoad': 0}, + ... + '23:55': {'chargePower': 0, 'outP': 0, 'pacToGrid': 0, 'pacToUser': 0, 'pex': 0, 'ppv': 0, 'pself': 0, 'sysOut': 0, 'userLoad': 0}, + }, + 'eAcCharge': '0', + 'eCharge': '0', + 'eChargeToday': '0', + 'eChargeToday1': '0', + 'eChargeToday2': '0', + 'echarge1': '0', + 'echargeToat': '0', + 'elocalLoad': '0', + 'etouser': '0', + 'isMasterOne': '0', + 'keyNames': ['Photovoltaic Output', 'Load Consumption', 'Imported From Grid', 'From Battery'], + 'newBean': { + 'dryContactStatus': 0, + 'exportLimit': 0, + 'exportLimitPower2': '0kW', + 'pac': '0kW' + }, + 'photovoltaic': '0', + 'ratio1': '0%', + 'ratio2': '0%', + 'ratio3': '0%', + 'ratio4': '0%', + 'ratio5': '0%', + 'ratio6': '0%', + 'unit': 'kWh', + 'unit2': 'kW' + } + e.g. (Timespan.day) + { + 'chartData': { + 'date': '2020-01-02', + 'acCharge': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'charge': [0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'eac': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'echarge': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'epv3': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'pacToGrid': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'pex': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'pself': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'sysOut': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + for other attributes, see "hour" example + } + e.g. (Timespan.month) + { + 'chartData': { + 'date': '2025-02', + 'acCharge': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'charge': [19.8, 0.5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'eac': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'echarge': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'epv3': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'pacToGrid': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'pex': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'pself': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'sysOut': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + for other attributes, see "hour" example + } + e.g. (Timespan.year) + { + 'chartData': { + 'date': '2025', + 'acCharge': [0, 0, 0, 0, 0, 0], + 'charge': [0, 0, 0, 0, 12.3, 45.6], + 'eac': [0, 0, 0, 0, 0, 0], + 'echarge': [0, 0, 0, 0, 0, 0], + 'epv3': [0, 0, 0, 0, 0, 0], + 'pacToGrid': [0, 0, 0, 0, 0, 0], + 'pex': [0, 0, 0, 0, 0, 0], + 'pself': [0, 0, 0, 0, 0, 0], + 'sysOut': [0, 0, 0, 0, 0, 0] + }, + for other attributes, see "hour" example + } """ date_str = self.__get_date_string(timespan, date) @@ -344,830 +1482,2685 @@ def tlx_energy_prod_cons(self, plant_id, tlx_id, timespan=Timespan.hour, date=No response = self.session.post( self.get_url("newTlxApi.do"), params={"op": "getEnergyProdAndCons_KW"}, - data={'date': date_str, + data={ "plantId": plant_id, - "language": "1", - "id": tlx_id, - "type": timespan.value} + "id": tlx_id, + "type": timespan.value, + "date": date_str, + "language": int(language_code), + }, ) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def tlx_data(self, tlx_id, date=None): + def tlx_data( + self, + tlx_id: str, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + tlx_data_type: Union[TlxDataTypeNeo, int] = 1, + ) -> Dict[str, Any]: """ Get TLX inverter data for specified date or today. Args: - tlx_id (str): The ID of the TLX inverter. - date (datetime, optional): The date you are interested in. Defaults to datetime.datetime.now(). + tlx_id (str): + The ID of the TLX inverter. + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + The date for which to retrieve the chart data. + tlx_data_type (Union[TlxDataTypeNeo, int]) = 1: + The type of data to get from the TLX inverter. + see TlxDataTypeNeo Returns: - dict: A dictionary containing the TLX inverter data. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing chart data and current energy. + e.g. + { + 'date': '2025-01-31', + 'dryContactStatus': 0, + 'eToday': '0', + 'eTotal': '12.1', + 'exportLimit': 0, + 'exportLimitPower': '0', + 'exportLimitPower2': '0', + 'invPacData': { + '2025-01-31 08:50': 0, + '2025-01-31 09:20': 7.3, + ... + '2025-01-31 23:25': 0 + }, + 'nominalPower': '800', + 'power': '0' + } """ + date_str = self.__get_date_string(date=date) - response = self.session.get(self.get_url('newTlxApi.do'), params={ - 'op': 'getTlxData', - 'id': tlx_id, - 'type': 1, - 'date': date_str - }) + response = self.session.get( + self.get_url("newTlxApi.do"), + params={"op": "getTlxData", "id": tlx_id, "type": int(tlx_data_type), "date": date_str}, + ) return response.json() - def tlx_detail(self, tlx_id): + def tlx_energy_chart( + self, + tlx_id: str, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + timespan: Literal[Timespan.month, Timespan.year, Timespan.total] = Timespan.month, + ) -> Dict[str, Optional[float]]: """ - Get detailed data from TLX inverter. + Get monthly/yearly/total AC-Power values as shown in App->Inverter->"Real time data" chart. + + These values differ from the ones shown in tlx_energy_prod_cons(). + This is not an API issue but differing values can also be seen in the App and Web frontends. + + Info: API might not return all days/months/years as days without data are not returned. Args: - tlx_id (str): The ID of the TLX inverter. + tlx_id (str): + The ID of the TLX inverter. + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + The date for which to retrieve the chart data. + timespan (Timespan): + The ENUM value conforming to the time window you want + * Timespan.month: month overview providing daily values + * Timespan.year: year overview providing monthly values + * Timespan.total: total overview providing yearly values Returns: - dict: A dictionary containing the detailed TLX inverter data. + Dict[str, Optional[float]]: + A dictionary containing the energy data. + e.g. + {'2024': 9.1, '2025': 18.7} + """ - Raises: - Exception: If the request to the server fails. + date_str = self.__get_date_string(timespan=timespan, date=date) + + # API uses different actions for different timespans + # Parameters as recorded from App + if timespan == Timespan.total: + params = {"op": "getTotalPac", "id": tlx_id} + elif timespan == Timespan.year: + params = {"op": "getYearPac", "id": tlx_id, "date": date_str} + elif timespan == Timespan.month: + params = {"op": "getMonthPac", "id": tlx_id, "date": date_str} + else: + raise ValueError(f"Unsupported timespan: {timespan}") + + response = self.session.get(self.get_url("newTlxApi.do"), params=params) + + return response.json() + + def tlx_detail(self, tlx_id: str) -> Dict[str, Any]: + """ + Get detailed data from TLX inverter. + + Args: + tlx_id (str): + The ID of the TLX inverter. + + Returns: + Dict[str, Any]: + A dictionary containing the detailed TLX inverter data. + e.g. + { + 'data': { + 'address': 0, 'again': False, 'alias': None, + 'bMerterConnectFlag': 0, 'batSn': None, 'batteryNo': 0, + 'batterySN': None, 'bdc1ChargePower': 0, 'bdc1ChargeTotal': 0, + 'bdc1DischargePower': 0, 'bdc1DischargeTotal': 0, + 'bdc1FaultType': 0, 'bdc1Ibat': 0, 'bdc1Ibb': 0, 'bdc1Illc': 0, + 'bdc1Mode': 0, 'bdc1Soc': 0, 'bdc1Status': 0, 'bdc1Temp1': 0, + 'bdc1Temp2': 0, 'bdc1Vbat': 0, 'bdc1Vbus1': 0, 'bdc1Vbus2': 0, + 'bdc1WarnCode': 0, 'bdc2ChargePower': 0, 'bdc2ChargeTotal': 0, + 'bdc2DischargePower': 0, 'bdc2DischargeTotal': 0, 'bdc2FaultType': 0, + 'bdc2Ibat': 0, 'bdc2Ibb': 0, 'bdc2Illc': 0, 'bdc2Mode': 0, + 'bdc2Soc': 0, 'bdc2Status': 0, 'bdc2Temp1': 0, 'bdc2Temp2': 0, + 'bdc2Vbat': 0, 'bdc2Vbus1': 0, 'bdc2Vbus2': 0, 'bdc2WarnCode': 0, + 'bdcBusRef': 0, 'bdcDerateReason': 0, 'bdcFaultSubCode': 0, + 'bdcStatus': 0, 'bdcVbus2Neg': 0, 'bdcWarnSubCode': 0, 'bgridType': 0, + 'bmsCommunicationType': 0, 'bmsCvVolt': 0, 'bmsError2': 0, + 'bmsError3': 0, 'bmsError4': 0, 'bmsFaultType': 0, 'bmsFwVersion': '', + 'bmsIbat': 0, 'bmsIcycle': 0, 'bmsInfo': 0, 'bmsIosStatus': 0, + 'bmsMaxCurr': 0, 'bmsMcuVersion': '', 'bmsPackInfo': 0, 'bmsSoc': 0, + 'bmsSoh': 0, 'bmsStatus': 0, 'bmsTemp1Bat': 0, 'bmsUsingCap': 0, + 'bmsVbat': 0, 'bmsVdelta': 0, 'bmsWarn2': 0, 'bmsWarnCode': 0, + 'bsystemWorkMode': 0, + 'calendar': None, + 'dataLogSn': None, 'day': None, 'dcVoltage': 0, 'dciR': 0, 'dciS': 0, + 'dciT': 0, 'debug1': None, 'debug2': None, 'deratingMode': 0, + 'dryContactStatus': 0, + 'eacChargeToday': 0, 'eacChargeTotal': 0, 'eacToday': 0, 'eacTotal': 0, + 'echargeToday': 0, 'echargeTotal': 0, 'edischargeToday': 0, + 'edischargeTotal': 0, 'eex1Today': 0, 'eex1Total': 0, 'eex2Today': 0, + 'eex2Total': 0, 'elocalLoadToday': 0, 'elocalLoadTotal': 0, 'epsFac': 0, + 'epsIac1': 0, 'epsIac2': 0, 'epsIac3': 0, 'epsPac': 0, 'epsPac1': 0, + 'epsPac2': 0, 'epsPac3': 0, 'epsPf': 0, 'epsVac1': 0, 'epsVac2': 0, + 'epsVac3': 0, 'epv1Today': 0, 'epv1Total': 0, 'epv2Today': 0, + 'epv2Total': 0, 'epv3Today': 0, 'epv3Total': 0, 'epv4Today': 0, + 'epv4Total': 0, 'epvTotal': 0, 'errorText': 'Unknown', + 'eselfToday': 0, 'eselfTotal': 0, 'esystemToday': 0, 'esystemTotal': 0, + 'etoGridToday': 0, 'etoGridTotal': 0, 'etoUserToday': 0, + 'etoUserTotal': 0, + 'fac': 0, 'faultType': 0, 'faultType1': 0, + 'gfci': 0, + 'iac1': 0, 'iac2': 0, 'iac3': 0, 'iacr': 0, 'invDelayTime': 0, + 'ipv1': 0, 'ipv2': 0, 'ipv3': 0, 'ipv4': 0, 'isAgain': False, + 'iso': 0, 'loadPercent': 0, 'lost': True, + 'mtncMode': 0, 'mtncRqst': 0, + 'nBusVoltage': 0, 'newWarnCode': 0, 'newWarnSubCode': 0, + 'opFullwatt': 0, 'operatingMode': 0, + 'pBusVoltage': 0, 'pac': 0, 'pac1': 0, 'pac2': 0, 'pac3': 0, + 'pacToGridTotal': 0, 'pacToLocalLoad': 0, 'pacToUserTotal': 0, + 'pacr': 0, 'pex1': 0, 'pex2': 0, 'pf': 0, 'ppv': 0, 'ppv1': 0, + 'ppv2': 0, 'ppv3': 0, 'ppv4': 0, 'pself': 0, 'psystem': 0, + 'realOPPercent': 0, + 'serialNum': None, 'soc1': 0, 'soc2': 0, 'status': 0, + 'statusText': 'Standby', 'sysFaultWord': 0, 'sysFaultWord1': 0, + 'sysFaultWord2': 0, 'sysFaultWord3': 0, 'sysFaultWord4': 0, + 'sysFaultWord5': 0, 'sysFaultWord6': 0, 'sysFaultWord7': 0, + 'tMtncStrt': None, 'tWinEnd': None, 'tWinStart': None, + 'temp1': 0, 'temp2': 0, 'temp3': 0, 'temp4': 0, 'temp5': 0, + 'time': '', 'timeTotal': 0, 'tlxBean': None, + 'totalWorkingTime': 0, + 'uwSysWorkMode': 0, + 'vac1': 0, 'vac2': 0, 'vac3': 0, 'vacRs': 0, 'vacSt': 0, + 'vacTr': 0, 'vacr': 0, 'vacrs': 0, 'vpv1': 0, 'vpv2': 0, + 'vpv3': 0, 'vpv4': 0, + 'warnCode': 0, 'warnCode1': 0, 'warnText': 'Unknown', + 'winMode': 0, 'winOffGridSOC': 0, 'winOnGridSOC': 0, + 'winRequest': 0, 'withTime': False + }, + 'parameterName': 'Fac(Hz),Pac(W),Ppv(W),Ppv1(W),Ppv2(W),Vpv1(V),Ipv1(A),Vpv2(V),Ipv2(A),Vac1(V),Iac1(A),Pac1(W),VacRs(V),EacToday(kWh),EacTotal + ...GridTotal(kWh),ElocalLoadToday(kWh),ElocalLoadTotal(kWh),Epv1Total(kWh),Epv2Total(kWh),EpvTotal(kWh),BsystemWorkMode,BgridType' + } """ - response = self.session.get(self.get_url('newTlxApi.do'), params={ - 'op': 'getTlxDetailData', - 'id': tlx_id - }) + + response = self.session.get(self.get_url("newTlxApi.do"), params={"op": "getTlxDetailData", "id": tlx_id}) return response.json() - def tlx_params(self, tlx_id): + def tlx_params(self, tlx_id: str) -> Dict[str, Any]: """ Get parameters for TLX inverter. Args: - tlx_id (str): The ID of the TLX inverter. + tlx_id (str): + The ID of the TLX inverter. Returns: - dict: A dictionary containing the TLX inverter parameters. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the TLX inverter parameters. + e.g. + { + 'inverterType': 'NEO 800M-X', + 'region': '', + 'newBean': { + 'addr': 1, + 'alias': '', + 'bagingTestStep': 0, + 'batParallelNum': 0, + 'batSeriesNum': 0, + 'batSysEnergy': 0, + 'batTempLowerLimitC': 0, + 'batTempLowerLimitD': 0, + 'batTempUpperLimitC': 0, + 'batTempUpperLimitD': 0, + 'batteryType': 0, + 'bctAdjust': 0, + 'bctMode': 0, + 'bcuVersion': '', + 'bdc1Model': '0', + 'bdc1Sn': '', + 'bdc1Version': '\x00\x00\x00\x00-0', + 'bdcAuthversion': 0, + 'bdcMode': 0, + 'bmsCommunicationType': 0, + 'bmsSoftwareVersion': '', + 'children': [], + 'comAddress': 1, + 'communicationVersion': 'GJAA-0003', + 'countrySelected': 1, + 'dataLogSn': '{DATALOGGER_SN}', + 'deviceType': 5, + 'dtc': 5203, + 'eToday': 0, + 'eTotal': 12.3456789, + 'energyDayMap': {}, + 'energyMonth': 0, + 'energyMonthText': '0', + 'fwVersion': 'GJ1.0', + 'groupId': -1, + 'hwVersion': '', + 'id': 0, + 'imgPath': './css/img/status_gray.gif', + 'innerVersion': 'GJAA03xx', + 'lastUpdateTime': { + 'year': 125, # 2015 is 125 (sic!) + 'month': 0, + 'date': 30, + 'day': 4, # weekday + 'hours': 17, + 'minutes': 11, + 'seconds': 23, + 'time': 1738228283000, + 'timezoneOffset': -480, + }, + 'lastUpdateTimeText': '2025-01-02 03:04:05', + 'level': 4, + 'liBatteryFwVersion': '', + 'liBatteryManufacturers': '', + 'location': '', + 'lost': True, + 'manufacturer': ' PV Inverter ', + 'modbusVersion': 307, + 'model': 123456789012345678, + 'modelText': 'S00B00D00T00P0FU00M0000', + 'monitorVersion': '', + 'mppt': 0, + 'optimezerList': [], + 'pCharge': 0, + 'pDischarge': 0, + 'parentID': 'LIST_{DATALOGGER_SN}_22', + 'plantId': 0, + 'plantname': '', + 'pmax': 800, + 'portName': 'port_name', + 'power': 0, + 'powerMax': '', + 'powerMaxText': '', + 'powerMaxTime': '', + 'priorityChoose': 0, + 'record': None, + 'restartTime': 65, + 'safetyVersion': 0, + 'serialNum': '{INVERTER_SN}', + 'startTime': 65, + 'status': -1, + 'statusText': 'tlx.status.lost', + 'strNum': 0, + 'sysTime': '', + 'tcpServerIp': '12.123.123.12', + 'timezone': 8, + 'tlxSetbean': None, + 'trakerModel': 0, + 'treeID': 'ST_{INVERTER_SN}', + 'treeName': '', + 'updating': False, + 'userName': '', + 'vbatStartForDischarge': 0, + 'vbatStopForCharge': 0, + 'vbatStopForDischarge': 0, + 'vbatWarnClr': 0, + 'vbatWarning': 0, + 'vnormal': 280, + 'vppOpen': 0, + 'wselectBaudrate': 0 + } + } """ - response = self.session.get(self.get_url('newTlxApi.do'), params={ - 'op': 'getTlxParams', - 'id': tlx_id - }) + + response = self.session.get(self.get_url("newTlxApi.do"), params={"op": "getTlxParams", "id": tlx_id}) return response.json() - def tlx_all_settings(self, tlx_id): + def tlx_all_settings(self, tlx_id: str, kind: int = 0) -> Dict[str, Any]: """ Get all possible settings from TLX inverter. Args: - tlx_id (str): The ID of the TLX inverter. + tlx_id (str): + The ID of the TLX inverter. + kind (int) = 0: + Unknown parameter seen in App's HTTP traffic Returns: - dict: A dictionary containing all possible settings for the TLX inverter. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: A dictionary containing all possible settings for the TLX inverter. + e.g. + { + 'acChargeEnable': '0', 'ac_charge': '0', 'activeRate': '100', + 'afci_enabled': '-1', 'afci_reset': '-1', 'afci_self_check': '-1', + 'afci_thresholdd': '-1', 'afci_thresholdh': '-1', + 'afci_thresholdl': '-1', + 'brazilRule': '0', + 'chargePowerCommand': '0', 'charge_power': '0', 'charge_stop_soc': '0', + 'compatibleFlag': '0', 'delay_time': '10000.0', + 'demand_manage_enable': '0', 'disChargePowerCommand': '0', + 'discharge_power': '0', 'discharge_stop_soc': '0', 'dtc': '5203', + 'exportLimit': '0', 'exportLimitPowerRateStr': '0.0', + 'fail_safe_curr': '0', 'fft_threshold_count': '-1', + 'forcedStopSwitch1': '0', 'forcedStopSwitch2': '0', + 'forcedStopSwitch3': '0', 'forcedStopSwitch4': '0', + 'forcedStopSwitch5': '0', 'forcedStopSwitch6': '0', + 'forcedStopSwitch7': '0', 'forcedStopSwitch8': '0', + 'forcedStopSwitch9': '0', 'forcedTimeStart1': '0:0', + 'forcedTimeStart2': '0:0', 'forcedTimeStart3': '0:0', + 'forcedTimeStart4': '0:0', 'forcedTimeStart5': '0:0', + 'forcedTimeStart6': '0:0', 'forcedTimeStart7': '0:0', + 'forcedTimeStart8': '0:0', 'forcedTimeStart9': '0:0', + 'forcedTimeStop1': '0:0', 'forcedTimeStop2': '0:0', + 'forcedTimeStop3': '0:0', 'forcedTimeStop4': '0:0', + 'forcedTimeStop5': '0:0', 'forcedTimeStop6': '0:0', + 'forcedTimeStop7': '0:0', 'forcedTimeStop8': '0:0', + 'forcedTimeStop9': '0:0', + 'gen_charge_enable': '0', 'gen_ctrl': '0', 'gen_rated_power': '0', + 'grid_type': '0', + 'h_1_freq_1': '51.5', 'h_1_freq_2': '51.5', 'h_1_volt_1': '287.5', + 'h_1_volt_2': '287.5', + 'isLGBattery': '0', + 'l_1_freq': '47.5', 'l_1_freq_1': '47.5', 'l_1_freq_2': '47.5', + 'l_1_volt_1': '184.0', 'l_1_volt_2': '103.5', 'l_2_freq': '47.5', + 'loading_rate': '9.0', + 'max_allow_curr': '0', 'modbusVersion': '307', + 'normalPower': '800', + 'off_net_box': '0', 'onGridMode': '-1', 'onGridStatus': '-1', + 'on_grid_discharge_stop_soc': '0', + 'peak_shaving_enable': '0', 'pf_sys_year': '2025-01-02 03:04:05', + 'power_down_enable': '0', 'pre_pto': '0', 'priorityChoose': '0', + 'prot_enable': '0', 'pvPfCmdMemoryState': '0', + 'pv_grid_frequency_high': '50.1', 'pv_grid_frequency_low': '47.65', + 'pv_grid_voltage_high': '253.0', 'pv_grid_voltage_low': '195.5', + 'q_percent_max': '43.0', 'qv_h1': '236.9', 'qv_h2': '246.1', + 'qv_l1': '223.1', 'qv_l2': '213.9', + 'restart_loading_rate': '9.0', + 'safetyCorrespondNumList': [], 'safetyNum': '0', + 'safety_correspond_num': '0', 'season1MonthTime': '0_0_0', + 'season1Time1': '0_0_0_0_0_0_0', 'season1Time2': '0_0_0_0_0_0_0', + 'season1Time3': '0_0_0_0_0_0_0', 'season1Time4': '0_0_0_0_0_0_0', + 'season1Time5': '0_0_0_0_0_0_0', 'season1Time6': '0_0_0_0_0_0_0', + 'season1Time7': '0_0_0_0_0_0_0', 'season1Time8': '0_0_0_0_0_0_0', + 'season1Time9': '0_0_0_0_0_0_0', 'season2MonthTime': '0_0_0', + 'season2Time1': '0_0_0_0_0_0_0', 'season2Time2': '0_0_0_0_0_0_0', + 'season2Time3': '0_0_0_0_0_0_0', 'season2Time4': '0_0_0_0_0_0_0', + 'season2Time5': '0_0_0_0_0_0_0', 'season2Time6': '0_0_0_0_0_0_0', + 'season2Time7': '0_0_0_0_0_0_0', 'season2Time8': '0_0_0_0_0_0_0', + 'season2Time9': '0_0_0_0_0_0_0', 'season3MonthTime': '0_0_0', + 'season3Time1': '0_0_0_0_0_0_0', 'season3Time2': '0_0_0_0_0_0_0', + 'season3Time3': '0_0_0_0_0_0_0', 'season3Time4': '0_0_0_0_0_0_0', + 'season3Time5': '0_0_0_0_0_0_0', 'season3Time6': '0_0_0_0_0_0_0', + 'season3Time7': '0_0_0_0_0_0_0', 'season3Time8': '0_0_0_0_0_0_0', + 'season3Time9': '0_0_0_0_0_0_0', 'season4MonthTime': '0_0_0', + 'season4Time1': '0_0_0_0_0_0_0', 'season4Time2': '0_0_0_0_0_0_0', + 'season4Time3': '0_0_0_0_0_0_0', 'season4Time4': '0_0_0_0_0_0_0', + 'season4Time5': '0_0_0_0_0_0_0', 'season4Time6': '0_0_0_0_0_0_0', + 'season4Time7': '0_0_0_0_0_0_0', 'season4Time8': '0_0_0_0_0_0_0', + 'season4Time9': '0_0_0_0_0_0_0', 'seasonYearTime': '0_0_0', + 'showPeakShaving': '0', 'special1MonthTime': '0_0_0', + 'special1Time1': '0_0_0_0_0_0', 'special1Time2': '0_0_0_0_0_0', + 'special1Time3': '0_0_0_0_0_0', 'special1Time4': '0_0_0_0_0_0', + 'special1Time5': '0_0_0_0_0_0', 'special1Time6': '0_0_0_0_0_0', + 'special1Time7': '0_0_0_0_0_0', 'special1Time8': '0_0_0_0_0_0', + 'special1Time9': '0_0_0_0_0_0', 'special2MonthTime': '0_0_0', + 'special2Time1': '0_0_0_0_0_0', 'special2Time2': '0_0_0_0_0_0', + 'special2Time3': '0_0_0_0_0_0', 'special2Time4': '0_0_0_0_0_0', + 'special2Time5': '0_0_0_0_0_0', 'special2Time6': '0_0_0_0_0_0', + 'special2Time7': '0_0_0_0_0_0', 'special2Time8': '0_0_0_0_0_0', + 'special2Time9': '0_0_0_0_0_0', 'sysMtncAvail': '0', + 'system_work_mode': '0', + 'time1Mode': '0', 'time2Mode': '0', 'time3Mode': '0', 'time4Mode': '0', + 'time5Mode': '0', 'time6Mode': '0', 'time7Mode': '0', 'time8Mode': '0', + 'time9Mode': '0', 'time_segment1': '0_0:0_0:0_0', + 'time_segment2': '0_0:0_0:0_0', 'time_segment3': '0_0:0_0:0_0', + 'time_segment4': '0_0:0_0:0_0', 'time_segment5': '0_0:0_0:0_0', + 'time_segment6': '0_0:0_0:0_0', 'time_segment7': '0_0:0_0:0_0', + 'time_segment8': '0_0:0_0:0_0', 'time_segment9': '0_0:0_0:0_0', + 'tlx_ac_discharge_frequency': '0', 'tlx_ac_discharge_voltage': '0', + 'tlx_backflow_default_power': '0.0', 'tlx_cc_current': '0.0', + 'tlx_cv_voltage': '0.0', 'tlx_dry_contact_enable': '0', + 'tlx_dry_contact_off_power': '0.0', 'tlx_dry_contact_power': '0.0', + 'tlx_exter_comm_Off_GridEn': '0', 'tlx_lcd_Language': '0', + 'tlx_limit_device': '0', 'tlx_off_grid_enable': '0', 'tlx_on_off': '1', + 'tlx_one_key_set_bdc_mode': '0', 'tlx_pf': '0.0', + 'tlx_pflinep1_lp': '255', 'tlx_pflinep1_pf': '1.0', + 'tlx_pflinep2_lp': '255', 'tlx_pflinep2_pf': '1.0', + 'tlx_pflinep3_lp': '255', 'tlx_pflinep3_pf': '1.0', + 'tlx_pflinep4_lp': '255', 'tlx_pflinep4_pf': '1.0', + 'ub_ac_charging_stop_soc': '0', 'ub_peak_shaving_backup_soc': '0', + 'usBatteryType': '0', 'uw_ac_charging_max_power_limit': '0.0', + 'uw_demand_mgt_downstrm_power_limit': '0.0', + 'uw_demand_mgt_revse_power_limit': '0.0', + 'wchargeSOCLowLimit': '0', 'wdisChargeSOCLowLimit': '0', + 'winModeEndTime': '', 'winModeFlag': '0', + 'winModeOffGridDischargeStopSOC': '10', + 'winModeOnGridDischargeStopSOC': '10', 'winModeStartTime': '', + 'winterOrMaintain': '0', + 'yearSettingFlag': '0', 'year_time1': '0_0_0_0_0_0_0', 'year_time2': '', + 'year_time3': '', 'year_time4': '', 'year_time5': '', 'year_time6': '', + 'year_time7': '', 'year_time8': '', 'year_time9': '' + } """ - response = self.session.post(self.get_url('newTlxApi.do'), params={ - 'op': 'getTlxSetData' - }, data={ - 'serialNum': tlx_id - }) - return response.json().get('obj', {}).get('tlxSetBean') + response = self.session.post( + self.get_url("newTlxApi.do"), + params={"op": "getTlxSetData"}, + data={ + "serialNum": tlx_id, + "kind": kind, + }, + ) + + return response.json().get("obj", {}).get("tlxSetBean") - def tlx_enabled_settings(self, tlx_id): + def tlx_enabled_settings( + self, + tlx_id: str, + language_code: Union[LanguageCode, int] = LanguageCode.en, + device_type_id: int = 5, # see docstring + ) -> Dict[str, Any]: """ Get "Enabled settings" from TLX inverter. - + Also retrieves the password required to change settings. + Args: - tlx_id (str): The ID of the TLX inverter. - + tlx_id (str): + The ID of the TLX inverter. + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + device_type_id (int) = 5: + device type id as returned by e.g. + - device_list()[0]["type2"] + - plant_info()["invList"][0]["type2"] + - tlx_params()["newBean"]["deviceType"] + - tlx_system_status()["dType"] + e.g. 5 = TLX inverter (NEO) + Returns: - dict: A dictionary containing the enabled settings. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the enabled settings. + e.g. + { + 'haveAfci': False, + 'settings_password': 'growatt********', + 'switch': True, + 'enable': { + 'ac_charge': '1', + 'backflow_setting': '1', + 'charge_power': '1', 'charge_stop_soc': '1', + 'discharge_power': '1', 'discharge_stop_soc': '1', + 'pf_sys_year': '1','pv_active_p_rate': '1', + 'pv_grid_frequency_high': '1', 'pv_grid_frequency_low': '1', + 'pv_grid_voltage_high': '1', 'pv_grid_voltage_low': '1', + 'pv_reactive_p_rate': '1', + 'time_segment1': '1', 'time_segment2': '1', 'time_segment3': '1', + 'time_segment4': '1', 'time_segment5': '1', 'time_segment6': '1', + 'time_segment7': '1', 'time_segment8': '1', 'time_segment9': '1', + 'tlx_ac_discharge_frequency': '1', + 'tlx_ac_discharge_time_period': '1', 'tlx_ac_discharge_voltage': '1', + 'tlx_backflow_default_power': '1', 'tlx_cc_current': '1', + 'tlx_custom_pf_curve': '1', 'tlx_cv_voltage': '1', + 'tlx_dry_contact_enable': '1', 'tlx_dry_contact_off_power': '1', + 'tlx_dry_contact_power': '1', 'tlx_exter_comm_Off_GridEn': '1', + 'tlx_lcd_Language': '1', 'tlx_limit_device': '1', + 'tlx_off_grid_enable': '1', 'tlx_on_off': '0', + 'tlx_optimezer_set_param_multi': '1', 'tlx_reset_to_factory': '1' + }, + } """ - string_time = datetime.datetime.now().strftime('%Y-%m-%d') - response = self.session.post( - self.get_url('newLoginAPI.do'), - params={'op': 'getSetPass'}, - data={'deviceSn': tlx_id, 'stringTime': string_time, 'type': '5'} - ) - return response.json().get('obj', {}) + string_time = datetime.datetime.now().strftime("%Y-%m-%d") + + url = self.get_url("newLoginAPI.do") + params = {"op": "getSetPass"} + data = { + "deviceSn": tlx_id, + "type": device_type_id, + "stringTime": string_time, + "language": int(language_code), + } + + response = self.session.post(url=url, params=params, data=data) + + response_json = response.json() + + if response_json.get("result") == 1: # extract settings pw + settings_pw = response_json.get("msg") + else: + settings_pw = datetime.date.today().strftime("growatt%Y%m%d") + + data = response_json.get("obj") or {} + data["settings_password"] = settings_pw # include settings pw - def tlx_battery_info(self, serial_num): + return data + + def tlx_battery_info( + self, + serial_num: str, + language_code: Union[LanguageCode, int] = LanguageCode.en, + ) -> Dict[str, Any]: """ Get battery information. - + Args: - serial_num (str): The serial number of the battery. - + serial_num (str): + The serial number of the battery. + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + Returns: - dict: A dictionary containing the battery information. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the battery information. """ response = self.session.post( - self.get_url('newTlxApi.do'), - params={'op': 'getBatInfo'}, - data={'lan': 1, 'serialNum': serial_num} + self.get_url("newTlxApi.do"), + params={"op": "getBatInfo"}, + data={"lan": int(language_code), "serialNum": serial_num}, ) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def tlx_battery_info_detailed(self, plant_id, serial_num): + def tlx_battery_info_detailed( + self, + plant_id: Union[str, int], + serial_num: str, + language_code: Union[LanguageCode, int] = LanguageCode.en, + ) -> Dict[str, Any]: """ Get detailed battery information. - + Args: - plant_id (str): The ID of the plant. - serial_num (str): The serial number of the battery. - + plant_id (Union[str, int]): + The ID of the plant. + serial_num (str): + The serial number of the battery. + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + Returns: - dict: A dictionary containing the detailed battery information. - - Raises: - Exception: If the request to the server fails. + Dict[str, Any]: + A dictionary containing the detailed battery information. + e.g. + { + 'data': { + 'bmNum': '0', + 'chargeOrDisPower': '0', + 'dischargeTotal': '0', + 'errorCode': '0', + 'errorSubCode': '0', + 'soc': '0', + 'status': '0', + 'systemWorkStatus': '0', + 'warnCode': '0', + 'warnSubCode': '0' + }, + 'parameterName': '' + } """ + response = self.session.post( - self.get_url('newTlxApi.do'), - params={'op': 'getBatDetailData'}, - data={'lan': 1, 'plantId': plant_id, 'id': serial_num} + self.get_url("newTlxApi.do"), + params={"op": "getBatDetailData"}, + data={ + "plantId": plant_id, + "id": serial_num, + "lan": int(language_code), + }, ) return response.json() - def mix_info(self, mix_id, plant_id = None): + def mix_info( + self, + mix_id: str, + plant_id: Optional[Union[int, str]] = None, + ) -> Dict[str, Any]: """ Returns high level values from Mix device - Keyword arguments: - mix_id -- The serial number (device_sn) of the inverter - plant_id -- The ID of the plant (the mobile app uses this but it does not appear to be necessary) (default None) + Args: + mix_id (str): + The serial number (device_sn) of the inverter + plant_id (Optional[Union[int, str]]) = None: + The ID of the plant + Note: the mobile app uses this but it does not appear to be necessary Returns: - 'acChargeEnergyToday' -- ??? 2.7 - 'acChargeEnergyTotal' -- ??? 25.3 - 'acChargePower' -- ??? 0 - 'capacity': '45' -- The current remaining capacity of the batteries (same as soc but without the % sign) - 'eBatChargeToday' -- Battery charged today in kWh - 'eBatChargeTotal' -- Battery charged total (all time) in kWh - 'eBatDisChargeToday' -- Battery discharged today in kWh - 'eBatDisChargeTotal' -- Battery discharged total (all time) in kWh - 'epvToday' -- Energy generated from PVs today in kWh - 'epvTotal' -- Energy generated from PVs total (all time) in kWh - 'isCharge'-- ??? 0 - Possible a 0/1 based on whether or not the battery is charging - 'pCharge1' -- ??? 0 - 'pDischarge1' -- Battery discharging rate in W - 'soc' -- Statement of charge including % symbol - 'upsPac1' -- ??? 0 - 'upsPac2' -- ??? 0 - 'upsPac3' -- ??? 0 - 'vbat' -- Battery Voltage - 'vbatdsp' -- ??? 51.8 - 'vpv1' -- Voltage PV1 - 'vpv2' -- Voltage PV2 - """ - request_params={ - 'op': 'getMixInfo', - 'mixId': mix_id - } + Dict[str, Any]: + battery energy data + e.g. (NEO inverter, no battery attached) + { + 'ibat': '0.0', + 'iGuid': '+0.0', + 'ipv': '0.0', + 'pbat': '0.0', + 'pGuid': '+0.0', + 'ppv': '0.0', + 'upsPac1': '0.0', + 'upsPac2': '0.0', + 'upsPac3': '0.0', + 'vbat': '0.0', + 'vGuid': '0.0', + 'vpv': '0.0' + } + e.g. (MIX) + { + 'acChargeEnergyToday': 2.7, + 'acChargeEnergyTotal': 25.3, + 'acChargePower': 0, + 'capacity': '45', # The current remaining capacity of the batteries (same as soc but without the % sign) + 'eBatChargeToday': 0, # Battery charged today in kWh + 'eBatChargeTotal': 0, # Battery charged total (all time) in kWh + 'eBatDisChargeToday': 0, # Battery discharged today in kWh + 'eBatDisChargeTotal': 0, # Battery discharged total (all time) in kWh + 'epvToday': 0, # Energy generated from PVs today in kWh + 'epvTotal': 0, # Energy generated from PVs total (all time) in kWh + 'isCharge': 0, # Possible a 0/1 based on whether the battery is charging + 'pCharge1': 0, + 'pDischarge1': 0, # Battery discharging rate in W + 'soc': '45%', # State of charge including % symbol + 'upsPac1': 0, + 'upsPac2': 0, + 'upsPac3': 0, + 'vbat': 0, # Battery Voltage + 'vbatdsp': 51.8, + 'vpv1': 0, # Voltage PV1 + 'vpv2': 0 # Voltage PV2 + } + """ + + params = {"op": "getMixInfo", "mixId": mix_id} - if (plant_id): - request_params['plantId'] = plant_id + if plant_id is not None: + params["plantId"] = plant_id - response = self.session.get(self.get_url('newMixApi.do'), params=request_params) + response = self.session.get(self.get_url("newMixApi.do"), params=params) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def mix_totals(self, mix_id, plant_id): + def mix_totals( + self, + mix_id: str, + plant_id: Union[int, str], + ) -> Dict[str, Any]: """ Returns "Totals" values from Mix device - Keyword arguments: - mix_id -- The serial number (device_sn) of the inverter - plant_id -- The ID of the plant + Args: + mix_id (str): + The serial number (device_sn) of the inverter + plant_id (Union[int, str]): + The ID of the plant Returns: - 'echargetoday' -- Battery charged today in kWh (same as eBatChargeToday from mix_info) - 'echargetotal' -- Battery charged total (all time) in kWh (same as eBatChargeTotal from mix_info) - 'edischarge1Today' -- Battery discharged today in kWh (same as eBatDisChargeToday from mix_info) - 'edischarge1Total' -- Battery discharged total (all time) in kWh (same as eBatDisChargeTotal from mix_info) - 'elocalLoadToday' -- Load consumption today in kWh - 'elocalLoadTotal' -- Load consumption total (all time) in kWh - 'epvToday' -- Energy generated from PVs today in kWh (same as epvToday from mix_info) - 'epvTotal' -- Energy generated from PVs total (all time) in kWh (same as epvTotal from mix_info) - 'etoGridToday' -- Energy exported to the grid today in kWh - 'etogridTotal' -- Energy exported to the grid total (all time) in kWh - 'photovoltaicRevenueToday' -- Revenue earned from PV today in 'unit' currency - 'photovoltaicRevenueTotal' -- Revenue earned from PV total (all time) in 'unit' currency - 'unit' -- Unit of currency for 'Revenue' - """ - response = self.session.post(self.get_url('newMixApi.do'), params={ - 'op': 'getEnergyOverview', - 'mixId': mix_id, - 'plantId': plant_id - }) - - return response.json().get('obj', {}) - - def mix_system_status(self, mix_id, plant_id): + Dict[str, Any]: + mix energy metrics + e.g. + { + 'echargetoday': 0, # Battery charged today in kWh (same as eBatChargeToday from mix_info) + 'echargetotal': 0, # Battery charged total (all time) in kWh (same as eBatChargeTotal from mix_info) + 'edischarge1Today': 0, # Battery discharged today in kWh (same as eBatDisChargeToday from mix_info) + 'edischarge1Total': 0, # Battery discharged total (all time) in kWh (same as eBatDisChargeTotal from mix_info) + 'elocalLoadToday': 0, # Load consumption today in kWh + 'elocalLoadTotal': 0, # Load consumption total (all time) in kWh + 'epvToday': 0, # Energy generated from PVs today in kWh (same as epvToday from mix_info) + 'epvTotal': 0, # Energy generated from PVs total (all time) in kWh (same as epvTotal from mix_info) + 'etoGridToday': 0, # Energy exported to the grid today in kWh + 'etogridTotal': 0, # Energy exported to the grid total (all time) in kWh + 'photovoltaicRevenueToday': 0, # Revenue earned from PV today in 'unit' currency + 'photovoltaicRevenueTotal': 0, # Revenue earned from PV total (all time) in 'unit' currency + 'unit': '', # Unit of currency for 'Revenue' + } + """ + + response = self.session.post( + self.get_url("newMixApi.do"), params={"op": "getEnergyOverview", "mixId": mix_id, "plantId": plant_id} + ) + + return response.json().get("obj", {}) + + def mix_system_status( + self, + mix_id: str, + plant_id: Union[int, str], + ) -> Dict[str, Any]: """ Returns current "Status" from Mix device - Keyword arguments: - mix_id -- The serial number (device_sn) of the inverter - plant_id -- The ID of the plant + Args: + mix_id (str): + The serial number (device_sn) of the inverter + plant_id (Union[int, str]): + The ID of the plant Returns: - 'SOC' -- Statement of charge (remaining battery %) - 'chargePower' -- Battery charging rate in kw - 'fAc' -- Frequency (Hz) - 'lost' -- System status e.g. 'mix.status.normal' - 'pLocalLoad' -- Load conumption in kW - 'pPv1' -- PV1 Wattage in W - 'pPv2' -- PV2 Wattage in W - 'pactogrid' -- Export to grid rate in kW - 'pactouser' -- Import from grid rate in kW - 'pdisCharge1' -- Discharging batteries rate in kW - 'pmax' -- ??? 6 ??? PV Maximum kW ?? - 'ppv' -- PV combined Wattage in kW - 'priorityChoose' -- Priority setting - 0=Local load - 'status' -- System statue - ENUM - Unknown values - 'unit' -- Unit of measurement e.g. 'kW' - 'upsFac' -- ??? 0 - 'upsVac1' -- ??? 0 - 'uwSysWorkMode' -- ??? 6 - 'vAc1' -- Grid voltage in V - 'vBat' -- Battery voltage in V - 'vPv1' -- PV1 voltage in V - 'vPv2' -- PV2 voltage in V - 'vac1' -- Grid voltage in V (same as vAc1) - 'wBatteryType' -- ??? 1 - """ - response = self.session.post(self.get_url('newMixApi.do'), params={ - 'op': 'getSystemStatus_KW', - 'mixId': mix_id, - 'plantId': plant_id - }) - - return response.json().get('obj', {}) - - def mix_detail(self, mix_id, plant_id, timespan=Timespan.hour, date=None): + Dict[str, Any]: + mix power metrics + e.g. + { + 'SOC': 0, # State of charge (remaining battery %) + 'chargePower': 0, # Battery charging rate in kw + 'fAc': 0, # Frequency (Hz) + 'lost': 'mix.status.normal', # System status + 'pLocalLoad': 0, # Load conumption in kW + 'pPv1': 0, # PV1 Wattage in W + 'pPv2': 0, # PV2 Wattage in W + 'pactogrid': 0, # Export to grid rate in kW + 'pactouser': 0, # Import from grid rate in kW + 'pdisCharge1': 0, # Discharging batteries rate in kW + 'pmax': 6, # PV Maximum kW? + 'ppv': 0, # PV combined Wattage in kW + 'priorityChoose': 0, # Priority setting - 0=Local load + 'status': 0, # System statue - ENUM - Unknown values + 'unit': 'kW', # Unit of measurement + 'upsFac': 0, + 'upsVac1': 0, + 'uwSysWorkMode': 6, + 'vAc1': 0, # Grid voltage in V + 'vBat': 0, # Battery voltage in V + 'vPv1': 0, # PV1 voltage in V + 'vPv2': 0, # PV2 voltage in V + 'vac1': 0, # Grid voltage in V (same as vAc1) + 'wBatteryType': 1, + } + """ + + response = self.session.post( + self.get_url("newMixApi.do"), params={"op": "getSystemStatus_KW", "mixId": mix_id, "plantId": plant_id} + ) + + return response.json().get("obj", {}) + + def mix_detail( + self, + mix_id: str, + plant_id: Union[int, str], + timespan: Timespan = Timespan.month, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + ) -> Dict[str, Any]: """ Get Mix details for specified timespan - Keyword arguments: - mix_id -- The serial number (device_sn) of the inverter - plant_id -- The ID of the plant - timespan -- The ENUM value conforming to the time window you want e.g. hours from today, days, or months (Default Timespan.hour) - date -- The date you are interested in (Default datetime.datetime.now()) + Note: It is possible to calculate the PV generation that went into charging the batteries by performing + the following calculation: + Solar to Battery = Solar Generation - Export to Grid - Load consumption from solar + epvToday (from mix_info) - eAcCharge - eChargeToday + + Args: + mix_id (str): + The serial number (device_sn) of the inverter + plant_id (Union[int, str]): + The ID of the plant + timespan (Timespan): + The ENUM value conforming to the time window you want + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + The date for which to retrieve the chart data. Returns: - A chartData object where each entry is for a specific 5 minute window e.g. 00:05 and 00:10 respectively (below) - 'chartData': { '00:05': { 'pacToGrid' -- Export rate to grid in kW - 'pacToUser' -- Import rate from grid in kW - 'pdischarge' -- Battery discharge in kW - 'ppv' -- Solar generation in kW - 'sysOut' -- Load consumption in kW - }, - '00:10': { 'pacToGrid': '0', - 'pacToUser': '0.93', - 'pdischarge': '0', - 'ppv': '0', - 'sysOut': '0.93'}, - ...... - } - 'eAcCharge' -- Exported to grid in kWh - 'eCharge' -- System production in kWh = Self-consumption + Exported to Grid - 'eChargeToday' -- Load consumption from solar in kWh - 'eChargeToday1' -- Self-consumption in kWh - 'eChargeToday2' -- Self-consumption in kWh (eChargeToday + echarge1) - 'echarge1' -- Load consumption from battery in kWh - 'echargeToat' -- Total battery discharged (all time) in kWh - 'elocalLoad' -- Load consumption in kW (battery + solar + imported) - 'etouser' -- Load consumption imported from grid in kWh - 'photovoltaic' -- Load consumption from solar in kWh (same as eChargeToday) - 'ratio1' -- % of system production that is self-consumed - 'ratio2' -- % of system production that is exported - 'ratio3' -- % of Load consumption that is "self consumption" - 'ratio4' -- % of Load consumption that is "imported from grid" - 'ratio5' -- % of Self consumption that is directly from Solar - 'ratio6' -- % of Self consumption that is from batteries - 'unit' -- Unit of measurement e.g kWh - 'unit2' -- Unit of measurement e.g kW - - - NOTE - It is possible to calculate the PV generation that went into charging the batteries by performing the following calculation: - Solar to Battery = Solar Generation - Export to Grid - Load consumption from solar - epvToday (from mix_info) - eAcCharge - eChargeToday + Dict[str, Any]: + A chartData object where each entry is for a specific 5 minute window e.g. 00:05 and 00:10 respectively (below) + e.g. + { + 'chartData': { + '00:05': { + 'pacToGrid': 0, # Export rate to grid in kW + 'pacToUser': 0, # Import rate from grid in kW + 'pdischarge': 0, # Battery discharge in kW + 'ppv': 0, # Solar generation in kW + 'sysOut': 0, # Load consumption in kW + }, + '00:10': { + 'pacToGrid': '0', + 'pacToUser': '0.93', + 'pdischarge': '0', + 'ppv': '0', + 'sysOut': '0.93', + }, + ... + }, + 'eAcCharge': 0, # Exported to grid in kWh + 'eCharge': 0, # System production in kWh = Self-consumption + Exported to Grid + 'eChargeToday': 0, # Load consumption from solar in kWh + 'eChargeToday1': 0, # Self-consumption in kWh + 'eChargeToday2': 0, # Self-consumption in kWh (eChargeToday + echarge1) + 'echarge1': 0, # Load consumption from battery in kWh + 'echargeToat': 0, # Total battery discharged (all time) in kWh + 'elocalLoad': 0, # Load consumption in kW (battery + solar + imported) + 'etouser': 0, # Load consumption imported from grid in kWh + 'photovoltaic': 0, # Load consumption from solar in kWh (same as eChargeToday) + 'ratio1': 0, # % of system production that is self-consumed + 'ratio2': 0, # % of system production that is exported + 'ratio3': 0, # % of Load consumption that is "self consumption" + 'ratio4': 0, # % of Load consumption that is "imported from grid" + 'ratio5': 0, # % of Self consumption that is directly from Solar + 'ratio6': 0, # % of Self consumption that is from batteries + 'unit': 'kWh', # Unit of measurement + 'unit2': 'kW', # Unit of measurement + } """ + date_str = self.__get_date_string(timespan, date) - response = self.session.post(self.get_url('newMixApi.do'), params={ - 'op': 'getEnergyProdAndCons_KW', - 'plantId': plant_id, - 'mixId': mix_id, - 'type': timespan.value, - 'date': date_str - }) + response = self.session.post( + self.get_url("newMixApi.do"), + params={ + "op": "getEnergyProdAndCons_KW", + "plantId": plant_id, + "mixId": mix_id, + "type": timespan.value, + "date": date_str, + }, + ) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def dashboard_data(self, plant_id, timespan=Timespan.hour, date=None): + def dashboard_data( + self, + plant_id: Union[str, int], + timespan: Timespan = Timespan.hour, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + ) -> Dict[str, Any]: """ Get 'dashboard' data for specified timespan - NOTE - All numerical values returned by this api call include units e.g. kWh or % - - Many of the 'total' values that are returned for a Mix system are inaccurate on the system this was tested against. - However, the statistics that are correct are not available on any other interface, plus these values may be accurate for - non-mix types of system. Where the values have been proven to be inaccurate they are commented below. - Keyword arguments: - plant_id -- The ID of the plant - timespan -- The ENUM value conforming to the time window you want e.g. hours from today, days, or months (Default Timespan.hour) - date -- The date you are interested in (Default datetime.datetime.now()) + Note: + * Does not return any data for a tlx system. Use plant_energy_data() instead. + * All numerical values returned by this api call include units e.g. kWh or % + * Many of the 'total' values that are returned for a Mix system are inaccurate on the system this was tested against. + However, the statistics that are correct are not available on any other interface, plus these values may be accurate for + non-mix types of system. Where the values have been proven to be inaccurate they are commented below. + + Args: + plant_id (Union[str, int]): + The ID of the plant. + timespan (Timespan) = Timespan.hour: + The ENUM value conforming to the time window you want e.g. hours from today, days, or months. + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + The date for which to retrieve the chart data. Returns: - A chartData object where each entry is for a specific 5 minute window e.g. 00:05 and 00:10 respectively (below) - NOTE: The keys are interpreted differently, the examples below describe what they are used for in a 'Mix' system - 'chartData': { '00:05': { 'pacToUser' -- Power from battery in kW - 'ppv' -- Solar generation in kW - 'sysOut' -- Load consumption in kW - 'userLoad' -- Export in kW - }, - '00:10': { 'pacToUser': '0', - 'ppv': '0', - 'sysOut': '0.7', - 'userLoad': '0'}, - ...... - } - 'chartDataUnit' -- Unit of measurement e.g. 'kW', - 'eAcCharge' -- Energy exported to the grid in kWh e.g. '20.5kWh' (not accurate for Mix systems) - 'eCharge' -- System production in kWh = Self-consumption + Exported to Grid e.g '23.1kWh' (not accurate for Mix systems - actually showing the total 'load consumption' - 'eChargeToday1' -- Self-consumption of PPV (possibly including excess diverted to batteries) in kWh e.g. '2.6kWh' (not accurate for Mix systems) - 'eChargeToday2' -- Total self-consumption (PPV consumption(eChargeToday2Echarge1) + Battery Consumption(echarge1)) e.g. '10.1kWh' (not accurate for Mix systems) - 'eChargeToday2Echarge1' -- Self-consumption of PPV only e.g. '0.8kWh' (not accurate for Mix systems) - 'echarge1' -- Self-consumption from Battery only e.g. '9.3kWh' - 'echargeToat' -- Not used on Dashboard view, likely to be total battery discharged e.g. '152.1kWh' - 'elocalLoad' -- Total load consumption (etouser + eChargeToday2) e.g. '20.3kWh', (not accurate for Mix systems) - 'etouser'-- Energy imported from grid today (includes both directly used by load and AC battery charging e.g. '10.2kWh' - 'keyNames' -- Keys to be used for the graph data e.g. ['Solar', 'Load Consumption', 'Export To Grid', 'From Battery'] - 'photovoltaic' -- Same as eChargeToday2Echarge1 e.g. '0.8kWh' - 'ratio1' -- % of 'Solar production' that is self-consumed e.g. '11.3%' (not accurate for Mix systems) - 'ratio2' -- % of 'Solar production' that is exported e.g. '88.7%' (not accurate for Mix systems) - 'ratio3' -- % of 'Load consumption' that is self consumption e.g. '49.8%' (not accurate for Mix systems) - 'ratio4' -- % of 'Load consumption' that is imported from the grid e.g '50.2%' (not accurate for Mix systems) - 'ratio5' -- % of Self consumption that is from batteries e.g. '92.1%' (not accurate for Mix systems) - 'ratio6' -- % of Self consumption that is directly from Solar e.g. '7.9%' (not accurate for Mix systems) - - NOTE: Does not return any data for a tlx system. Use plant_energy_data() instead. + Dict[str, Any] + A chartData object where each entry is for a specific 5 minute window e.g. 00:05 and 00:10 respectively. + Note: The keys are interpreted differently, the examples below describe what they are used for in a 'Mix' system + e.g. + { + 'chartData': { + '00:05': { + 'pacToUser': 0, # Power from battery in kW + 'ppv': '0', # Solar generation in kW + 'sysOut': '0', # Load consumption in kW + 'userLoad': '0', # Export in kW + }, + '00:10': { + 'pacToUser': '0', + 'ppv': '0', + 'sysOut': '0.7', + 'userLoad': '0', + }, + # ... + }, + 'chartDataUnit': 'kW', # Unit of measurement + 'eAcCharge': '20.5kWh', # Energy exported to the grid in kWh (not accurate for Mix systems) + 'eCharge': '23.1kWh', # System production in kWh = Self-consumption + Exported to Grid (not accurate for Mix systems - actually showing the total 'load consumption') + 'eChargeToday1': '2.6kWh', # Self-consumption of PPV (possibly including excess diverted to batteries) in kWh (not accurate for Mix systems) + 'eChargeToday2': '10.1kWh', # Total self-consumption (PPV consumption(eChargeToday2Echarge1) + Battery Consumption(echarge1)) (not accurate for Mix systems) + 'eChargeToday2Echarge1': '0.8kWh', # Self-consumption of PPV only (not accurate for Mix systems) + 'echarge1': '9.3kWh', # Self-consumption from Battery only + 'echargeToat': '152.1kWh', # Not used on Dashboard view, likely to be total battery discharged + 'elocalLoad': '20.3kWh', # Total load consumption (etouser + eChargeToday2) (not accurate for Mix systems) + 'etouser': '10.2kWh', # Energy imported from grid today (includes both directly used by load and AC battery charging) + 'keyNames': ['Solar', 'Load Consumption', 'Export To Grid', 'From Battery'], # Keys to be used for the graph data + 'photovoltaic': '0.8kWh', # Same as eChargeToday2Echarge1 + 'ratio1': '11.3%', # % of 'Solar production' that is self-consumed (not accurate for Mix systems) + 'ratio2': '88.7%', # % of 'Solar production' that is exported (not accurate for Mix systems) + 'ratio3': '49.8%', # % of 'Load consumption' that is self consumption (not accurate for Mix systems) + 'ratio4': '50.2%', # % of 'Load consumption' that is imported from the grid (not accurate for Mix systems) + 'ratio5': '92.1%', # % of Self consumption that is from batteries (not accurate for Mix systems) + 'ratio6': '7.9%', # % of Self consumption that is directly from Solar (not accurate for Mix systems) + } """ + date_str = self.__get_date_string(timespan, date) - response = self.session.post(self.get_url('newPlantAPI.do'), params={ - 'action': "getEnergyStorageData", - 'date': date_str, - 'type': timespan.value, - 'plantId': plant_id - }) + response = self.session.post( + self.get_url("newPlantAPI.do"), + params={"action": "getEnergyStorageData", "date": date_str, "type": timespan.value, "plantId": plant_id}, + ) return response.json() - def plant_settings(self, plant_id): + def plant_settings( + self, + plant_id: Union[str, int], + ) -> Dict[str, Any]: """ Returns a dictionary containing the settings for the specified plant - Keyword arguments: - plant_id -- The id of the plant you want the settings of + Args: + plant_id (Union[str, int]): + The ID of the plant. Returns: - A python dictionary containing the settings for the specified plant + Dict[str, Any] + settings for the specified plant + e.g. + { + 'EYearMoneyText': '0', + 'alarmValue': 0, + 'alias': '', + 'children': [], + 'city': '{CITY}', + 'companyName': '', + 'country': '{COUNTRY}', + 'createDate': { + 'year': 124, # 124 = 2024 + 'month': 1, + 'date': 1, # day + 'day': 5, # weekday + 'hours': 0, + 'minutes': 0, + 'seconds': 0, + 'time': 1732000000000, + 'timezoneOffset': -480, + }, + 'createDateText': '2024-01-01', + 'createDateTextA': '', + 'currentPac': 0, + 'currentPacStr': '', + 'currentPacTxt': '0', + 'dataLogList': [], + 'defaultPlant': False, + 'designCompany': '0', + 'deviceCount': 0, + 'eToday': 0, + 'eTotal': 0, + 'emonthCo2Text': '0', + 'emonthCoalText': '0', + 'emonthMoneyText': '0', + 'emonthSo2Text': '0', + 'energyMonth': 0, + 'energyYear': 0, + 'envTemp': 0, + 'etodayCo2Text': '0', + 'etodayCoalText': '0', + 'etodayMoney': 0, + 'etodayMoneyText': '0', + 'etodaySo2Text': '0', + 'etotalCo2Text': '0', + 'etotalCoalText': '0', + 'etotalFormulaTreeText': '0', + 'etotalMoney': 0, + 'etotalMoneyText': '0', + 'etotalSo2Text': '0', + 'eventMessBeanList': [], + 'fixedPowerPrice': 0.30, + 'flatPeriodPrice': 0.30, + 'formulaCo2': 0.40, + 'formulaCoal': 0.40, + 'formulaMoney': 0.30, + 'formulaMoneyStr': '0.3', + 'formulaMoneyUnitId': 'EUR', + 'formulaSo2': 0, + 'formulaTree': 0.055, + 'gridCompany': '', + 'gridLfdi': '', + 'gridPort': '', + 'gridServerUrl': '', + 'hasDeviceOnLine': 0, + 'hasStorage': 0, + 'id': {PLANT_ID}, + 'imgPath': 'css/img/plant.gif', + 'installMapName': '', + 'irradiance': 0, + 'isShare': False, + 'latitudeText': 'null°null′null″', + 'latitude_d': '', + 'latitude_f': '', + 'latitude_m': '', + 'level': 1, + 'locationImgName': '', + 'logoImgName': '', + 'longitudeText': 'null°null′null″', + 'longitude_d': '', + 'longitude_f': '', + 'longitude_m': '', + 'mapCity': '', + 'mapLat': '', + 'mapLng': '', + 'map_areaId': 0, + 'map_cityId': 0, + 'map_countryId': 0, + 'map_provinceId': 0, + 'moneyUnitText': '€', + 'nominalPower': 800, + 'nominalPowerStr': '0.8kWp', + 'onLineEnvCount': 0, + 'pairViewUserAccount': '', + 'panelTemp': 0, + 'paramBean': None, + 'parentID': '', + 'peakPeriodPrice': 0.30, + 'phoneNum': '', + 'plantAddress': '', + 'plantFromBean': None, + 'plantImgName': '', + 'plantName': '{PLANT_NAME}', + 'plantNmi': '', + 'plantType': 0, + 'plant_lat': '48.00000', + 'plant_lng': '9.00000', + 'prMonth': '', + 'prToday': '', + 'protocolId': '', + 'remark': '', + 'status': 0, + 'storage_BattoryPercentage': 0, + 'storage_TodayToGrid': 0, + 'storage_TodayToUser': 0, + 'storage_TotalToGrid': 0, + 'storage_TotalToUser': 0, + 'storage_eChargeToday': 0, + 'storage_eDisChargeToday': 0, + 'tempType': 0, + 'timezone': 1, + 'timezoneText': 'GMT+1', + 'timezoneValue': '+1:00', + 'treeID': 'PLANT_{PLANT_ID}', + 'treeName': '{PLANT_NAME}', + 'unitMap': { + 'AED': 'AED', ..., 'EUR': 'EUR', ..., 'ZMW': 'ZMW' + }, + 'userAccount': '{USER_NAME}', + 'userBean': None, + 'valleyPeriodPrice': 0.30, + 'windAngle': 0, + 'windSpeed': 0 + } """ - response = self.session.get(self.get_url('newPlantAPI.do'), params={ - 'op': 'getPlant', - 'plantId': plant_id - }) - + + response = self.session.get(self.get_url("newPlantAPI.do"), params={"op": "getPlant", "plantId": plant_id}) + return response.json() - def storage_detail(self, storage_id): + def storage_detail(self, storage_id: str) -> Dict[str, Any]: """ Get "All parameters" from battery storage. + + Args: + storage_id (str): + The ID of the storage. + + Returns: + Dict[str, Any]: + battery storage data + e.g. + { + 'batSn': '', + 'iGuid': '+0.0', + 'ibat': '0.0', + 'ipv': '0.0', + 'pGuid': '+0.0', + 'pbat': '0.0', + 'ppv': '0.0', + 'vGuid': '0.0', + 'vbat': '0.0', + 'vpv': '0.0' + } """ - response = self.session.get(self.get_url('newStorageAPI.do'), params={ - 'op': 'getStorageInfo_sacolar', - 'storageId': storage_id - }) + + response = self.session.get( + self.get_url("newStorageAPI.do"), params={"op": "getStorageInfo_sacolar", "storageId": storage_id} + ) return response.json() - def storage_params(self, storage_id): + def storage_params(self, storage_id: str) -> Dict[str, Any]: """ Get much more detail from battery storage. + + Args: + storage_id (str): + The ID of the storage. + + Returns: + Dict[str, Any]: + battery storage details """ - response = self.session.get(self.get_url('newStorageAPI.do'), params={ - 'op': 'getStorageParams_sacolar', - 'storageId': storage_id - }) + response = self.session.get( + self.get_url("newStorageAPI.do"), params={"op": "getStorageParams_sacolar", "storageId": storage_id} + ) return response.json() - def storage_energy_overview(self, plant_id, storage_id): + def storage_energy_overview(self, plant_id: Union[str, int], storage_id: str) -> Dict[str, Any]: """ Get some energy/generation overview data. + + Args: + plant_id (Union[str, int]): + The ID of the plant. + storage_id (str): + The ID of the storage. + + Returns: + Dict[str, Any]: + battery energy data """ - response = self.session.post(self.get_url('newStorageAPI.do?op=getEnergyOverviewData_sacolar'), params={ - 'plantId': plant_id, - 'storageSn': storage_id - }) + response = self.session.post( + self.get_url("newStorageAPI.do?op=getEnergyOverviewData_sacolar"), + params={"plantId": plant_id, "storageSn": storage_id}, + ) - return response.json().get('obj', {}) + return response.json().get("obj", {}) - def inverter_list(self, plant_id): + def inverter_list( + self, + plant_id: Union[str, int], + ) -> List[Dict[str, Any]]: """ Use device_list, it's more descriptive since the list contains more than inverters. + + Args: + plant_id (Union[str, int]): + The ID of the plant. + + Returns: + List[Dict[str, Any]]: + settings for the specified plant + e.g. + [ + { + 'bMerterConnectFlag': 0, + 'bdc1Soc': 0, + 'bdc2Soc': 0, + 'datalogSn': '{DATALOGGER_ID}', + 'deviceAilas': '{INVERTER_ID}', + 'deviceSn': '{INVERTER_ID}', + 'deviceStatus': '1', + 'deviceType': 'tlx', + 'eToday': '0', + 'eTodayStr': '0kWh', + 'energy': '29.3', + 'isParallel': 'false', + 'location': '', + 'lost': False, + 'power': '7.1', + 'powerStr': '0.01kW', + 'prePto': '-1', + 'type': 0, + 'type2': '5', + 'xe_ct': '0' + } + ] """ - warnings.warn("This function may be deprecated in the future because naming is not correct, use device_list instead", DeprecationWarning) + warnings.warn( + "This function may be deprecated in the future because naming is not correct, use device_list instead", + DeprecationWarning, + ) return self.device_list(plant_id) - def __get_all_devices(self, plant_id): + def _get_all_devices( + self, + plant_id: Union[str, int], + language_code: Union[LanguageCode, int] = LanguageCode.en, + ) -> List[Dict[str, Any]]: """ Get basic plant information with device list. + + Args: + plant_id (Union[str, int]): + The ID of the plant. + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + + Returns: + List[Dict[str, Any]] + list of devices assigned to plant + e.g. + [ + { + 'bMerterConnectFlag': 0, + 'bdc1Soc': 0, + 'bdc2Soc': 0, + 'datalogSn': '{DATALOGGER_ID}', + 'deviceAilas': '{INVERTER_ID}', + 'deviceSn': '{INVERTER_ID}', + 'deviceStatus': '1', + 'deviceType': 'tlx', + 'eToday': '0', + 'eTodayStr': '0kWh', + 'energy': '29.3', + 'isParallel': 'false', + 'location': '', + 'lost': False, + 'power': '7.1', + 'powerStr': '0.01kW', + 'prePto': '-1', + 'type': 0, + 'type2': '5', + 'xe_ct': '0' + } + ] """ - response = self.session.get(self.get_url('newTwoPlantAPI.do'), - params={'op': 'getAllDeviceList', - 'plantId': plant_id, - 'language': 1}) - return response.json().get('deviceList', {}) + response = self.session.get( + self.get_url("newTwoPlantAPI.do"), + params={"op": "getAllDeviceList", "plantId": plant_id, "language": int(language_code)}, + ) + + return response.json().get("deviceList", {}) - def device_list(self, plant_id): + def device_list( + self, + plant_id: Union[str, int], + language: str = "en", + ) -> List[Dict[str, Any]]: """ Get a list of all devices connected to plant. + + Args: + plant_id (Union[str, int]): + The ID of the plant. + language (str) = "en": + language to use for query, e.g. "en" + + Returns: + List[Dict[str, Any]] + list of devices assigned to plant + e.g. + [ + { + 'bMerterConnectFlag': 0, + 'bdc1Soc': 0, + 'bdc2Soc': 0, + 'datalogSn': '{DATALOGGER_ID}', + 'deviceAilas': '{INVERTER_ID}', + 'deviceSn': '{INVERTER_ID}', + 'deviceStatus': '1', + 'deviceType': 'tlx', + 'eToday': '0', + 'eTodayStr': '0kWh', + 'energy': '29.3', + 'isParallel': 'false', + 'location': '', + 'lost': False, + 'power': '7.1', + 'powerStr': '0.01kW', + 'prePto': '-1', + 'type': 0, + 'type2': '5', + 'xe_ct': '0' + } + ] """ - - device_list = self.plant_info(plant_id).get('deviceList', []) - + + device_list = self.plant_info( + plant_id=plant_id, + language=language, + ).get("deviceList", []) + if not device_list: # for tlx systems, the device_list in plant is empty, so use __get_all_devices() instead - device_list = self.__get_all_devices(plant_id) + device_list = self._get_all_devices( + plant_id=plant_id, + language_code=int( + getattr(LanguageCode, language, LanguageCode.en) + ), # look up language code, default to english + ) return device_list - def plant_info(self, plant_id): + def device_detail( + self, + device_id: str, + device_type: Literal["tlx", "mix", "storage"] = "tlx", + ) -> Dict[str, Any]: + """ + Get detailed data from TLX inverter. + Data is visible in App->Plant->Inverter->All parameters->Important data + + Args: + device_id (str): + The ID of the (TLX/MIX) inverter (device_id, tlx_id, mix_id). + device_type (Literal["tlx", "mix", "storage"]) = "tlx": + The type of device to get the data from. + + Returns: + Dict[str, Any]: + A dictionary containing the current Voltage/Current/Power values. + e.g. + { + 'parameterGreat': [ + ['', 'Voltage(V)', 'Current(A)', 'Power(W)'], + ['PV1', '0', '0', '0'], + ['PV2', '0', '0', '0'], + ['AC', '0', '0', '0'] + ] + } + """ + + response = self.session.post( + self.get_url("newTwoDeviceAPI.do"), + params={"op": "getDeviceDetailData"}, + data={"deviceType": f"{device_type.lower()}", "sn": f"{device_id}"}, + ) + + return response.json().get("obj", {}) + + def device_event_logs( + self, + device_id: str, + device_type: Literal["pcs", "bms", "jlinv", "min", "mic", "mod", "neo", "tlx"] = "tlx", + language_code: Union[LanguageCode, int] = LanguageCode.en, + page_num: int = 1, + ) -> Dict[str, Any]: + """ + Returns a dictionary containing events/alarms logged for specified device (e.g. inverter). + + Data is visible in App->Plant->Inverter->All parameters->Important data + + Args: + device_id (str): + The ID of the (TLX/MIX) inverter (device_id, tlx_id, mix_id). + device_type (Literal["pcs", "bms", "jlinv", "min", "mic", "mod", "neo", "tlx"]) = "tlx": + The type of device to get the data from. + Supported values (so far): + * "pcs" (PCS) + * "bms" (BMS - Battery Management System) + * "jlinv" (Solis Inverter) + * "min" (MIN Inverter) + * "mic" (MIC Inverter) + * "mod" (MOD Inverter) + * "neo" (NEO Inverter) + * "tlx" (MIN/MIC/MOD/NEO) + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + page_num (int) = 1: + Page number + + Returns: + Dict[str, Any]: + A dictionary containing the current Voltage/Current/Power values. + e.g. + { + 'city': '{PLANT_CITY}', + 'country': '{PLANT_COUNTRY}', + 'deviceType': 'MIN/MIC/MOD/NEO', + 'eventList': [ + { + 'description': 'No AC Connection', + 'deviceAlias': '', + 'deviceSerialNum': '{INVERTER_SN}', + 'event': 'ERROR_302', + 'eventCode': '302', + 'language': '19', + 'lcdid': '', + 'occurTime': '2024-01-02 03:04:05', + 'solution': '1.After shutdown,Check AC wiring. 2.If error message still exists,contact manufacturer.' + } + ], + 'pageSize': 1, + 'plantAddress': '{PLANT_ADDRESS}', + 'plantId': {PLANT_ID}, + 'plantName': '{PLANT_NAME}', + 'plant_lat': '40.123456', + 'plant_lng': '9.123456', + 'toPageNum': 1 + } + """ + + device_type_id = { + "pcs": 5, + "bms": 6, + "jlinv": 7, + "min": 8, + "mic": 8, + "mod": 8, + "neo": 8, + "tlx": 8, + }[device_type.lower()] + + url = self.get_url("newPlantAPI.do") + params = {"action": "getDeviceEvent"} + data = { + "deviceSn": device_id, + "deviceType": device_type_id, + "language": int(language_code), + "toPageNum": page_num, + } + response = self.session.post( + url=url, + params=params, + data=data, + ) + + # # device_type values have been reverse-engineered + # # by running this request with different device_type values + # # and checking response data's attribute "deviceType" + # # to check for allowed values, run this code: + # from time import sleep + # for device_type_id in range(20): + # data["deviceType"] = device_type_id + # response = self.session.post(url=url, params=params, data=data) + # print(f"{device_type_id:3} | {response.json().get('obj') or {}.get('deviceType') or None}") + # sleep(0.5) + # # => 5 | pcs + # # => 6 | bms + # # => 7 | jlinv + # # => 8 | MIN/MIC/MOD/NEO + + data = response.json().get("obj") or {} + + return data + + def plant_info( + self, + plant_id: Union[int, str], + language: str = "en", + ): """ Get basic plant information with device list. + + Args: + plant_id (Union[int, str]): + The ID of the plant + language (str) = "en": + e.g. en=English, de=German (None=Chinese) + + Returns: + Dict[str, Any] + e.g. + { + 'ammeterType': '0', + 'batList': [], + 'boostList': [], + 'chargerList': [], + 'Co2Reduction': '10.92', + 'invTodayPpv': '0', + 'isHaveOptimizer': 0, + 'isHavePumper': '0', + 'meterList': [], + 'nominalPower': 800, + 'nominal_Power': 800, + 'ogbList': [], + 'optimizerType': 0, + 'pidList': [], + 'plantMoneyText': '0.1/€', + 'sphList': [], + 'storageList': [], + 'storagePgrid': '0', + 'storagePuser': '0', + 'storageTodayPpv': '0', + 'todayDischarge': '0', + 'todayEnergy': '0.2', + 'totalEnergy': '27.3', + 'totalMoneyText': '8.19', + 'useEnergy': '0', + 'datalogList': [{ + 'alias': '{DATALOGGER_SN}', + 'client_url': '/10.10.0.100:12345', # internal IP + 'datalog_sn': '{DATALOGGER_SN}', # e.g. QMN0000000000000 + 'deviceTypeIndicate': '55', + 'device_type': 'ShineWeFi', + 'isGprs4G': '0', + 'keys': ['Signal', 'Connection Status', 'Last Update', 'Data Update Interval'], + 'lfdi': '0', + 'lost': True, + 'meter': {}, + 'simInfo': None, + 'type': '55', + 'unit_id': '', + 'update_interval': '5', + 'update_time': '2020-01-02 03:04:05', + 'values': ['Poor', 'Offline', '2020-01-02 03:04:05', '5'] + }], + 'deviceList': [], + 'invList': [{ + 'bMerterConnectFlag': 0, + 'bdc1Soc': 0, + 'bdc2Soc': 0, + 'datalogSn': '{DATALOGGER_SN}', + 'deviceAilas': '', + 'deviceSn': '{DEVICE_SN/INVERTER_ID/TLX_ID}', # e.g. BZP0000000 + 'deviceStatus': '-1', + 'deviceType': 'tlx', + 'eToday': '0.2', + 'eTodayStr': '0.2kWh', + 'energy': '20.3', + 'location': '', + 'lost': True, + 'plantId': {PLANT_ID}, + 'power': '0', + 'powerStr': '0kW', + 'prePto': '-1', + 'type': 0, + 'type2': '5' + }], + 'witList': [] + } """ - response = self.session.get(self.get_url('newTwoPlantAPI.do'), params={ - 'op': 'getAllDeviceListTwo', - 'plantId': plant_id, - 'pageNum': 1, - 'pageSize': 1 - }) + + response = self.session.get( + url=self.get_url("newTwoPlantAPI.do"), + params={ + "op": "getAllDeviceListTwo", + "plantId": plant_id, + "language": language, + "pageNum": 1, + "pageSize": 1, + }, + ) return response.json() - def plant_energy_data(self, plant_id): + def plant_energy_data( + self, + plant_id: Union[int, str], + language_code: Union[LanguageCode, int] = LanguageCode.en, + ) -> Dict[str, Any]: """ Get the energy data used in the 'Plant' tab in the phone + + Args: + plant_id (Union[int, str]): + The ID of the plant + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + + Returns: + Dict[str, Any] + e.g. + { + 'alarmValue': 0, + 'eventMessBeanList': [], + 'formulaCo2Str': '10.8kg', + 'formulaCo2Vlue': '10.8', + 'formulaCoalStr': '10.8kg', + 'formulaCoalValue': '10.8', + 'isHaveTigo': 0, + 'monthStr': '18kWh', + 'monthValue': '18.0', + 'nominalPowerStr': '0.8kWp', + 'nominalPowerValue': '800.0', + 'optimezerMap': {'isHaveOptimezer': 0}, + 'plantNumber': 1, + 'powerValue': '0.0', + 'powerValueStr': '0kW', + 'todayStr': '1.1kWh', + 'todayValue': '1.1', + 'totalStr': '12.1kWh', + 'totalValue': '12.1', + 'treeStr': '1', + 'treeValue': '1.5', + 'yearStr': '0kWh', + 'yearValue': '0.0', + 'weatherMap': { + 'cond_code': '100', + 'cond_txt': 'Sunny', + 'msg': '', + 'newTmp': '9.0°C', + 'success': True, + 'tmp': '9' + }, + 'plantBean': { + 'alarmValue': 0, + 'alias': None, + 'children': None, + 'city': 'Ulm', + 'companyName': '', + 'country': 'Germany', + 'createDate': 1730000000000, + 'createDateText': '2025-01-14', + 'createDateTextA': None, + 'currentPac': 0, + 'currentPacStr': None, + 'currentPacTxt': '0', + 'dataLogList': None, + 'defaultPlant': False, + 'designCompany': '0', + 'deviceCount': 0, + 'eToday': 0, + 'eTotal': 27.1, + 'emonthCo2Text': '0', + 'emonthCoalText': '0', + 'emonthMoneyText': '0', + 'emonthSo2Text': '0', + 'energyMonth': 0, + 'energyYear': 0, + 'envTemp': 0, + 'etodayCo2Text': '0', + 'etodayCoalText': '0', + 'etodayMoney': 0, + 'etodayMoneyText': '0', + 'etodaySo2Text': '0', + 'etotalCo2Text': '10.8', + 'etotalCoalText': '10.8', + 'etotalFormulaTreeText': '1.49', + 'etotalMoney': 0, + 'etotalMoneyText': '8.1', + 'etotalSo2Text': '0.8', + 'eventMessBeanList': None, + 'eyearMoneyText': '0', + 'fixedPowerPrice': 0.3, + 'flatPeriodPrice': 0.3, + 'formulaCo2': 0.4, + 'formulaCoal': 0.4, + 'formulaMoney': 0.3, + 'formulaMoneyStr': None, + 'formulaMoneyUnitId': 'EUR', + 'formulaSo2': 0, + 'formulaTree': 0.055, + 'gridCompany': None, + 'gridLfdi': None, + 'gridPort': None, + 'gridServerUrl': None, + 'hasDeviceOnLine': 0, + 'hasStorage': 0, + 'id': {PLANT_ID}, + 'imgPath': 'css/img/plant.gif', + 'installMapName': None, + 'irradiance': 0, + 'isShare': False, + 'latitudeText': 'null°null′null″', + 'latitude_d': None, + 'latitude_f': None, + 'latitude_m': None, + 'level': 1, + 'locationImgName': None, + 'logoImgName': '', + 'longitudeText': 'null°null′null″', + 'longitude_d': None, + 'longitude_f': None, + 'longitude_m': None, + 'mapCity': None, + 'mapLat': None, + 'mapLng': None, + 'map_areaId': 0, + 'map_cityId': 0, + 'map_countryId': 0, + 'map_provinceId': 0, + 'moneyUnitText': '€', + 'nominalPower': 800, + 'nominalPowerStr': '0.8kWp', + 'onLineEnvCount': 0, + 'pairViewUserAccount': None, + 'panelTemp': 0, + 'paramBean': None, + 'parentID': None, + 'peakPeriodPrice': 0.3, + 'phoneNum': None, + 'plantAddress': '{PLANT_ADDRESS}', + 'plantFromBean': None, + 'plantImgName': None, + 'plantName': '{PLANT_NAME}', + 'plantNmi': None, + 'plantType': 0, + 'plant_lat': '12.345678', + 'plant_lng': '9.101112', + 'prMonth': None, + 'prToday': None, + 'protocolId': None, + 'remark': None, + 'status': 0, + 'storage_BattoryPercentage': 0, + 'storage_TodayToGrid': 0, + 'storage_TodayToUser': 0, + 'storage_TotalToGrid': 0, + 'storage_TotalToUser': 0, + 'storage_eChargeToday': 0, + 'storage_eDisChargeToday': 0, + 'tempType': 0, + 'timezone': 1, + 'timezoneText': 'GMT+1', + 'timezoneValue': '+1:00', + 'treeID': 'PLANT_{PLANT_ID}', + 'treeName': '{PLANT_NAME}', + 'unitMap': None, + 'userAccount': '{USER_NAME}', + 'userBean': None, + 'valleyPeriodPrice': 0.3, + 'windAngle': 0, + 'windSpeed': 0 + } + } """ - response = self.session.post(self.get_url('newTwoPlantAPI.do'), - params={'op': 'getUserCenterEnertyDataByPlantid'}, - data={ 'language': 1, - 'plantId': plant_id}) + + response = self.session.post( + url=self.get_url("newTwoPlantAPI.do"), + params={"op": "getUserCenterEnertyDataByPlantid"}, + data={"language": int(language_code), "plantId": plant_id}, + ) return response.json() - - def is_plant_noah_system(self, plant_id): + + def plant_energy_data_v2( + self, + language_code: Union[LanguageCode, int] = LanguageCode.en, + ) -> Dict[str, Any]: + """ + Get the energy data used in the 'Dashboard' tab in the Android app + Note: yearValue is always 0.0 (at least for NEO inverter) + + like plant_energy_data but with today/month/total profit + + Args: + language_code (Union[LanguageCode, int]) = LanguageCode.en: + see enum LanguageCode + + Returns: + Dict[str, Any] + e.g. + { + "alarmValue": 0, + "deviceDetail": {}, + "deviceTypeMap": {"STORAGE": 0, "SPA": 0, "MIX": 0, "TLXH": 0}, + "eventMessBeanList": [], + "formulaCo2Str": "10.8kg", + "formulaCo2Vlue": "10.8", + "formulaCoalStr": "10.8kg", + "formulaCoalValue": "10.8", + "isHaveFormulaMoney": True, + "monthProfitStr": "€5.4" + "monthStr": "18kWh", + "monthValue": "18.0", + "nominalPowerStr": "0.8kWp", + "nominalPowerValue": 800, + "plantId": {PLANT_ID}, + "plantNumber": 1, + "plantStatus": 0, + "plantType": 1, + "powerValue": "0.0", + "powerValueStr": "0kW", + "statusMap": {"onlineNum": 0, "offline": 1, "faultNum": 0}, + "todayProfitStr": "€0.33", + "todayStr": "1.1kWh", + "todayUnit": "kWh", + "todayValue": "1.1", + "totalProfitStr": "€8.13", + "totalStr": "27.1kWh", + "totalValue": "27.1", + "treeStr": "1", + "treeValue": "1.5", + "yearStr": "0kWh", + "yearValue": "0.0", + } + + """ + + url = self.get_url("newPlantAPI.do") + params = {"action": "getUserCenterEnertyDataTwo"} + data = {"language": int(language_code)} + + response = self.session.post(url=url, params=params, data=data) + response_json = response.json() + + return response_json + + def plant_power_chart( + self, + plant_id: Union[int, str], + timespan: Literal[Timespan.day, Timespan.month, Timespan.year, Timespan.total] = Timespan.day, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + ): + """ + Retrieve data for Android app's "Power|Energy" chart available in "Plant" tab + + Args: + plant_id (Union[int, str]): + The ID of the plant + timespan (Timespan) = Timespan.day: + The ENUM value conforming to the time window you want + * day => one day, 5min values (power pAC) + * month => one month, daily values (energy eAC) + * year => one year, monthly values (energy eAC) + * total => six years, yearly values (energy eAC) + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + day/month/year for which to load the chart + + Returns: + Dict[str, Any]: A dictionary containing the chart data + e.g. (daily) + + """ + + # API uses different actions for different timespans + if timespan not in [Timespan.day, Timespan.month, Timespan.year, Timespan.total]: + raise ValueError(f"Unsupported timespan: {timespan}") + + date_str = self.__get_date_string(timespan=timespan, date=date) + + url = self.get_url("newPlantDetailAPI.do") + + # parameter "type" + # * type=1 returns power chart (5min values) for one day (App button "Hour") + # * type=2 returns energy chart (daily values) for one month (App button "DAY") + # * type=3 returns energy chart (monthly values) for one year (App button "MONTH") + # * type=4 returns energy chart (yearly values) for six years (App button "YEAR") + params = { + "plantId": plant_id, + "date": date_str, + "type": timespan.value, + } + + response = self.session.get(url=url, params=params) + response_json = response.json() + response_json = response_json.get("back", {}).get("data", {}) + + return response_json + + def plant_energy_chart( + self, + timespan: Literal[Timespan.month, Timespan.year, Timespan.total] = Timespan.month, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + id_: int = 0, + ): + """ + Retrieve data for Android app's "Power|Energy" chart available in "Dashboard" tab + + Args: + timespan (Timespan) = Timespan.month: + The ENUM value conforming to the time window you want + * month => one month, daily values + * year => one year, monthly values + * total => six years, yearly values + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + month/year for which to load the chart + id_ (int) = 0: + Unknown parameter (but required) + + Returns: + Dict[str, Any]: A dictionary containing the chart data + e.g. (daily) + { + "chartDataUnit": "kWh" + "chartData": { + "01": 1.2000000476837158, + "02": 0.10000000149011612, + ... + "12": 0.800000011920929 + } + """ + + # API uses different actions for different timespans + if timespan == Timespan.total: + action = "plantHistoryTotal" + elif timespan == Timespan.year: + action = "plantHistoryYear" + elif timespan == Timespan.month: + action = "plantHistoryMonth" + else: + raise ValueError(f"Unsupported timespan: {timespan}") + + date_str = self.__get_date_string(timespan=timespan, date=date) + + url = self.get_url("newTwoPlantAPI.do") + params = { + "action": action, + "date": date_str, + "id": id_, + } + + response = self.session.get(url=url, params=params) + response_json = response.json() + response_json = response_json.get("obj", {}) + + return response_json + + def plant_energy_chart_comparison( + self, + timespan: Literal[Timespan.year, Timespan.total] = Timespan.year, + date: Optional[Union[datetime.datetime, datetime.date]] = None, + id_: int = 0, + ): + """ + Retrieve data for Android app's "Power comparison" chart available in "Dashboard" tab + + comparison is available based on monthly or quarterly values + + Args: + timespan (Timespan) = Timespan.year: + The ENUM value conforming to the time window you want + * year => monthly values + * total => quarterly values + date (Union[datetime.datetime, datetime.date]) = datetime.date.today(): + year for which to load the comparison chart + id_ (int) = 0: + ??? + + Returns: + Dict[str, Any]: A dictionary containing the chart data + e.g. + { + "chartDataUnit": "kWh" + "chartData": { + "01": ["0", "18"], + "02": ["0", "0"], + ... + "12": ["9.1", "0"], + } + } + """ + + if timespan == Timespan.total: + month_or_quarter = 1 # quarterly values + elif timespan == Timespan.year: + month_or_quarter = 0 # monthly values + else: + raise ValueError(f"Unsupported timespan: {timespan}") + + date_str = self.__get_date_string(timespan=Timespan.year, date=date) + + url = self.get_url("newTwoPlantAPI.do") + params = { + "action": "dataComparison", + "date": date_str, + "type": month_or_quarter, + "id": id_, + } + + response = self.session.get(url=url, params=params) + response_json = response.json() + response_json = response_json.get("obj", {}) + + return response_json + + def is_plant_noah_system( + self, + plant_id: Union[str, int], + ) -> Dict[str, Any]: """ Returns a dictionary containing if noah devices are configured for the specified plant - Keyword arguments: - plant_id -- The id of the plant you want the noah devices of (str) + Args: + plant_id (Union[str, int]): + The ID of the plant you want the noah devices of + + Returns: + Dict[str, Any]: + e.g. + { + 'msg': '', + 'obj': { + 'deviceSn': '', # Serial number of the configured noah device + 'isPlantHaveNexa': False, + 'isPlantHaveNoah': False, # Are noah devices configured in the specified plant (True or False) + 'isPlantNoahSystem': False, # Is the specified plant a noah system (True or False) + 'plantId': '{PLANT_ID}', + 'plantName': '{PLANT_NAME}' + }, + 'result': 1 + } + """ + + response = self.session.post(self.get_url("noahDeviceApi/noah/isPlantNoahSystem"), data={"plantId": plant_id}) - Returns - 'msg' - 'result' -- True or False - 'obj' -- An Object containing if noah devices are configured - 'isPlantNoahSystem' -- Is the specified plant a noah system (True or False) - 'plantId' -- The ID of the plant - 'isPlantHaveNoah' -- Are noah devices configured in the specified plant (True or False) - 'deviceSn' -- Serial number of the configured noah device - 'plantName' -- Friendly name of the plant - """ - response = self.session.post(self.get_url('noahDeviceApi/noah/isPlantNoahSystem'), data={ - 'plantId': plant_id - }) return response.json() - - def noah_system_status(self, serial_number): + def noah_system_status(self, serial_number: str) -> Dict[str, Any]: """ Returns a dictionary containing the status for the specified Noah Device - Keyword arguments: - serial_number -- The Serial number of the noah device you want the status of (str) + Args: + serial_number (str): + The Serial number of the noah device you want the status of Returns - 'msg' - 'result' -- True or False - 'obj' -- An Object containing the noah device status - 'chargePower' -- Battery charging rate in watt e.g. '200Watt' - 'workMode' -- Workingmode of the battery (0 = Load First, 1 = Battery First) - 'soc' -- Statement of charge (remaining battery %) - 'associatedInvSn' -- ??? - 'batteryNum' -- Numbers of batterys - 'profitToday' -- Today generated profit through noah device - 'plantId' -- The ID of the plant - 'disChargePower' -- Battery discharging rate in watt e.g. '200Watt' - 'eacTotal' -- Total energy exported to the grid in kWh e.g. '20.5kWh' - 'eacToday' -- Today energy exported to the grid in kWh e.g. '20.5kWh' - 'pac' -- Export to grid rate in watt e.g. '200Watt' - 'ppv' -- Solar generation in watt e.g. '200Watt' - 'alias' -- Friendly name of the noah device - 'profitTotal' -- Total generated profit through noah device - 'moneyUnit' -- Unit of currency e.g. '€' - 'status' -- Is the noah device online (True or False) - """ - response = self.session.post(self.get_url('noahDeviceApi/noah/getSystemStatus'), data={ - 'deviceSn': serial_number - }) + Dict[str, Any] + NOAH device status + e.g. + { + 'msg': '', + 'result': 1, + 'obj': { + 'chargePower': '200Watt', # Battery charging rate in Watt + 'workMode': 0, # Workingmode of the battery (0 = Load First, 1 = Battery First) + 'soc': None, # State of charge (remaining battery %) + 'associatedInvSn': None, + 'batteryNum': None, # Numbers of batteries + 'profitToday': None, # Today generated profit through noah device + 'plantId': '{PLANT_ID}', # The ID of the plant + 'disChargePower': '200Watt', # Battery discharging rate in watt + 'eacTotal': '20.5kWh', # Total energy exported to the grid in kWh + 'eacToday': '20.5kWh', # Today energy exported to the grid in kWh + 'pac': '200Watt', # Export to grid rate in watt + 'ppv': '200Watt', # Solar generation in watt + 'alias': {NOAH_NAME}, # Friendly name of the noah device + 'profitTotal': 0.0, # Total generated profit through noah device + 'moneyUnit': '€', # Unit of currency + 'status': None, # Is the noah device online (True or False) + } + } + """ + + response = self.session.post( + self.get_url("noahDeviceApi/noah/getSystemStatus"), data={"deviceSn": serial_number} + ) + return response.json() - - def noah_info(self, serial_number): + def noah_info(self, serial_number: str) -> Dict[str, Any]: """ - Returns a dictionary containing the informations for the specified Noah Device + Returns a dictionary containing the information for the specified Noah Device - Keyword arguments: - serial_number -- The Serial number of the noah device you want the informations of (str) + Args: + serial_number (str): + The Serial number of the noah device you want the information of Returns - 'msg' - 'result' -- True or False - 'obj' -- An Object containing the noah device informations - 'neoList' -- A List containing Objects - 'unitList' -- A Object containing currency units e.g. "Euro": "euro", "DOLLAR": "dollar" - 'noah' -- A Object containing the folowing - 'time_segment' -- A List containing Objects with configured "Operation Mode" - NOTE: The keys are generated numerical, the values are generated with folowing syntax "[workingmode (0 = Load First, 1 = Battery First)]_[starttime]_[endtime]_[output power]" - 'time_segment': { - 'time_segment1': "0_0:0_8:0_150", ([Load First]_[00:00]_[08:00]_[150 watt]) - 'time_segment2': "1_8:0_18:0_0", ([Battery First]_[08:00]_[18:00]_[0 watt]) - .... - } - 'batSns' -- A List containing all battery Serial Numbers - 'associatedInvSn' -- ??? - 'plantId' -- The ID of the plant - 'chargingSocHighLimit' -- Configured "Battery Management" charging upper limit - 'chargingSocLowLimit' -- Configured "Battery Management" charging lower limit - 'defaultPower' -- Configured "System Default Output Power" - 'version' -- The Firmware Version of the noah device - 'deviceSn' -- The Serial number of the noah device - 'formulaMoney' -- Configured "Select Currency" energy cost per kWh e.g. '0.22' - 'alias' -- Friendly name of the noah device - 'model' -- Model Name of the noah device - 'plantName' -- Friendly name of the plant - 'tempType' -- ??? - 'moneyUnitText' -- Configured "Select Currency" (Value from the unitList) e.G. "euro" - 'plantList' -- A List containing Objects containing the folowing - 'plantId' -- The ID of the plant - 'plantImgName' -- Friendly name of the plant Image - 'plantName' -- Friendly name of the plant - """ - response = self.session.post(self.get_url('noahDeviceApi/noah/getNoahInfoBySn'), data={ - 'deviceSn': serial_number - }) - return response.json() + Dict[str, Any] + NOAH device info + Returns + Dict[str, Any]: + e.g. + { + 'msg': '', + 'result': 0, + 'obj': { + 'neoList': [], + 'unitList': {"Euro": "euro", "DOLLAR": "dollar"}, + 'noah': { + 'time_segment': { # Note: The keys are generated numerical, the values are generated with folowing syntax "[workingmode (0 = Load First, 1 = Battery First)]_[starttime]_[endtime]_[output power]" + 'time_segment1': "0_0:0_8:0_150", # ([Load First]_[00:00]_[08:00]_[150 watt]) + 'time_segment2': "1_8:0_18:0_0", # ([Battery First]_[08:00]_[18:00]_[0 watt]) + # ... + }, + 'batSns': [], # A list containing all battery Serial Numbers + 'associatedInvSn': None, + 'plantId': {PLANT_ID}, + 'chargingSocHighLimit': None, # Configured "Battery Management" charging upper limit + 'chargingSocLowLimit': None, # Configured "Battery Management" charging lower limit + 'defaultPower': None, # Configured "System Default Output Power" + 'version': None, # The Firmware Version of the noah device + 'deviceSn': '{NOAH_ID}', # The Serial number of the noah device + 'formulaMoney': '0.22', # Configured "Select Currency" energy cost per kWh + 'alias': '{NOAH_NAME}', # Friendly name of the noah device + 'model': None, # Model Name of the noah device + 'plantName': '{PLANT_NAME}', # Friendly name of the plant + 'tempType': None, + 'moneyUnitText': "euro", # Configured "Select Currency" (Value from the unitList) + }, + 'plantList': [ + { + 'plantId': {PLANT_ID}, # The ID of the plant + 'plantImgName': None, # plant Image + 'plantName': '{PLANT_NAME}', # name of the plant + } + ] + } + } + """ + response = self.session.post( + self.get_url("noahDeviceApi/noah/getNoahInfoBySn"), data={"deviceSn": serial_number} + ) + return response.json() - def update_plant_settings(self, plant_id, changed_settings, current_settings = None): + def update_plant_settings( + self, plant_id: str, changed_settings: Dict[str, Any], current_settings: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: """ Applies settings to the plant e.g. ID, Location, Timezone See README for all possible settings options - Keyword arguments: - plant_id -- The id of the plant you wish to update the settings for - changed_settings -- A python dictionary containing the settings to be changed and their value - current_settings -- A python dictionary containing the current settings of the plant (use the response from plant_settings), if None - fetched for you + Args: + plant_id (str): + The id of the plant you wish to update the settings for + changed_settings (Dict[str, Any]): + settings to be changed and their value + current_settings (Optional[Dict[str, Any]]) = None: + current settings of the plant (use the response from plant_settings) + will be fetched automatically if value is None Returns: - A response from the server stating whether the configuration was successful or not + Dict[str, Any]: + A response from the server stating whether the configuration was successful or not """ - #If no existing settings have been provided then get them from the growatt server - if current_settings == None: - current_settings = self.plant_settings(plant_id) - #These are the parameters that the form requires, without these an error is thrown. Pre-populate their values with the current values + # If no existing settings have been provided then get them from the growatt server + if current_settings is None: + current_settings = self.plant_settings(plant_id=plant_id) + + # These are the parameters that the form requires, without these an error is thrown. Pre-populate their values with the current values form_settings = { - 'plantCoal': (None, str(current_settings['formulaCoal'])), - 'plantSo2': (None, str(current_settings['formulaSo2'])), - 'accountName': (None, str(current_settings['userAccount'])), - 'plantID': (None, str(current_settings['id'])), - 'plantFirm': (None, '0'), #Hardcoded to 0 as I can't work out what value it should have - 'plantCountry': (None, str(current_settings['country'])), - 'plantType': (None, str(current_settings['plantType'])), - 'plantIncome': (None, str(current_settings['formulaMoneyStr'])), - 'plantAddress': (None, str(current_settings['plantAddress'])), - 'plantTimezone': (None, str(current_settings['timezone'])), - 'plantLng': (None, str(current_settings['plant_lng'])), - 'plantCity': (None, str(current_settings['city'])), - 'plantCo2': (None, str(current_settings['formulaCo2'])), - 'plantMoney': (None, str(current_settings['formulaMoneyUnitId'])), - 'plantPower': (None, str(current_settings['nominalPower'])), - 'plantLat': (None, str(current_settings['plant_lat'])), - 'plantDate': (None, str(current_settings['createDateText'])), - 'plantName': (None, str(current_settings['plantName'])), + "plantCoal": (None, str(current_settings["formulaCoal"])), + "plantSo2": (None, str(current_settings["formulaSo2"])), + "accountName": (None, str(current_settings["userAccount"])), + "plantID": (None, str(current_settings["id"])), + "plantFirm": (None, "0"), # Hardcoded to 0 as I can't work out what value it should have + "plantCountry": (None, str(current_settings["country"])), + "plantType": (None, str(current_settings["plantType"])), + "plantIncome": (None, str(current_settings["formulaMoneyStr"])), + "plantAddress": (None, str(current_settings["plantAddress"])), + "plantTimezone": (None, str(current_settings["timezone"])), + "plantLng": (None, str(current_settings["plant_lng"])), + "plantCity": (None, str(current_settings["city"])), + "plantCo2": (None, str(current_settings["formulaCo2"])), + "plantMoney": (None, str(current_settings["formulaMoneyUnitId"])), + "plantPower": (None, str(current_settings["nominalPower"])), + "plantLat": (None, str(current_settings["plant_lat"])), + "plantDate": (None, str(current_settings["createDateText"])), + "plantName": (None, str(current_settings["plantName"])), } - #Overwrite the current value of the setting with the new value + # Overwrite the current value of the setting with the new value for setting, value in changed_settings.items(): form_settings[setting] = (None, str(value)) - response = self.session.post(self.get_url('newTwoPlantAPI.do?op=updatePlant'), files = form_settings) + response = self.session.post(self.get_url("newTwoPlantAPI.do?op=updatePlant"), files=form_settings) return response.json() - def update_inverter_setting(self, serial_number, setting_type, - default_parameters, parameters): + def update_device_alias( + self, + device_id: str, + new_alias: str, + device_type: Literal["tlx"] = "tlx", + ) -> bool: + """ + Change device (inverter) name/alias + + Args: + device_id (str): + Serial number of the inverter + device_type (Literal["tlx"] = "tlx": + Type of the device (inverter) + ! until now, this setting has only be seen for TLX (NEO) inverter + new_alias (str): + New alias to be set + + Returns: + bool + True if change was successful + """ + + url = self.get_url("newTwoPlantAPI.do") + params = {"op": "updAllDevice"} + data = { + "deviceType": device_type, + "deviceSn": device_id, + "content": new_alias, + "updateType": 0, + } + + response = self.session.post(url=url, params=params, data=data) + + return response.json().get("success") or False + + def read_inverter_registers( + self, + inverter_id: str, + register_address_start: int, + register_address_end: Optional[int] = None, + device_type: Literal["pcs", "bms", "jlinv", "min", "mic", "mod", "neo", "tlx"] = "tlx", + ) -> Dict[str, Any]: + """ + Read inverter settings register + + seen in App -> Inverter -> Configure -> select any item -> press "Read" button + + Args: + inverter_id (str): + The ID of the (TLX) inverter. + register_address_start (int): + query registers from register_address_start to _end + register_address_end (Optional[int]) = None + query registers from register_address_start to _end + defaults to register_address_start if not set + device_type (Literal["pcs", "bms", "jlinv", "min", "mic", "mod", "neo", "tlx"]) = "tlx": + The type of device to get the data from. + Supported values (so far): + * "pcs" (PCS) + * "bms" (BMS - Battery Management System) + * "jlinv" (Solis Inverter) + * "min" (MIN Inverter) + * "mic" (MIC Inverter) + * "mod" (MOD Inverter) + * "neo" (NEO Inverter) + * "tlx" (MIN/MIC/MOD/NEO) + + Returns: + Dict[str, Any] + e.g. (success) + { + 'success': True, + 'error_message': '', + 'register': { + 0: '1', + 1: '413', + ... + 100: '2300' + } + } + e.g. (failure) + { + 'success': False, + 'error_message': '501 inverter offline', + 'register': {} + } + """ + + if register_address_end is None: + register_address_end = register_address_start + + device_type_id = { + "pcs": 5, + "bms": 6, + "jlinv": 7, + "min": 8, + "mic": 8, + "mod": 8, + "neo": 8, + "tlx": 8, + }[device_type.lower()] + + response = self.session.post( + url=self.get_url("newTcpsetAPI.do"), + params={"action": "readDeviceParam"}, + data={ + "serialNum": inverter_id, + "deviceTypeStr": device_type_id, + "startAddr": register_address_start, + "endAddr": register_address_end, + "paramId": "set_any_reg", + }, + ) + response_json = response.json() + + success = response_json.get("result") == 1 + error_message = response_json.get("msg") or "" + if error_message == "501": + # 501 is returned if inverter is offline + error_message += " inverter offline" + elif error_message == "500": + # 500 is returned in case of timeout + # for my NEO800MX, the maximum range is between 50 and 100 registers at once + error_message += " timeout or inverter offline" + if register_address_end - register_address_start > 1: + error_message += " - try querying less registers at once" + + # all registers are returned in 'param1' separated by "-" + # e.g. 'obj': {'param1': '1-413-0-'} + if success: + register_data = { + register_nr: register_value + for register_nr, register_value in zip( + range(register_address_start, register_address_end + 1), response_json["obj"]["param1"].split("-") + ) + } + else: + register_data = {} + + result_dict = { + "register": register_data, + "success": success, + "error_message": error_message, + } + + return result_dict + + def read_inverter_setting( + self, + inverter_id: str, + device_type: Literal["pcs", "bms", "jlinv", "min", "mic", "mod", "neo", "tlx"] = "tlx", + setting_name: Optional[str] = None, + register_address: Optional[int] = None, + ) -> Dict[str, Any]: + """ + Read single inverter setting + + seen in App -> Inverter -> Configure -> select any item -> press "Read" button + + You must specify + * either a setting name (e.g. "pv_active_p_rate") + * or a register address (e.g. 90 for "Country & Regulation") + + Args: + inverter_id (str): + The ID of the (TLX) inverter. + device_type (Literal["pcs", "bms", "jlinv", "min", "mic", "mod", "neo", "tlx"]) = "tlx": + The type of device to get the data from. + Supported values (so far): + * "pcs" (PCS) + * "bms" (BMS - Battery Management System) + * "jlinv" (Solis Inverter) + * "min" (MIN Inverter) + * "mic" (MIC Inverter) + * "mod" (MOD Inverter) + * "neo" (NEO Inverter) + * "tlx" (MIN/MIC/MOD/NEO) + setting_name (Optional[str]) = None + setting to query (e.g. "pv_active_p_rate") + register_address (Optional[int]) = None + register address to query (e.g. 90 for "Country & Regulation") + + Returns: + Dict[str, Any] + Info: JSON response from the server whether the configuration was successful + * result==0 means success, result==1 means failure + * msg "501" means inverter is offline + e.g. (success) + { + "success": True, + "error_message": "", + "param1": "0", + } + e.g. (failure) + { + "success": False, + "error_message": "501 inverter offline", + } + """ + + assert ( + setting_name is not None or register_address is not None + ), "You must specify either a setting name or a register address" + # ensure only one is set + assert ( + setting_name is None or register_address is None + ), "You must specify either a setting name or a register address - not both" + + if setting_name: + data = { + "paramId": setting_name, + "startAddr": -1, + "endAddr": -1, + } + else: + data = { + "startAddr": register_address, + "endAddr": register_address, + "paramId": "set_any_reg", + } + + device_type_id = { + "pcs": 5, + "bms": 6, + "jlinv": 7, + "min": 8, + "mic": 8, + "mod": 8, + "neo": 8, + "tlx": 8, + }[device_type.lower()] + data["deviceTypeStr"] = device_type_id + + # add inverter ID + data["serialNum"] = inverter_id + + response = self.session.post( + url=self.get_url("newTcpsetAPI.do"), params={"action": "readDeviceParam"}, data=data + ) + response_json = response.json() + + success = response_json.get("result") == 1 + error_message = response_json.get("msg") or "" + if error_message == "501": + # 501 is returned if inverter is offline + error_message += " inverter offline" + elif error_message == "500": + # 500 is returned if inverter did not respond + error_message += " timeout or inverter offline" + + result_dict = response_json.get("obj") or {} + result_dict["success"] = success + result_dict["error_message"] = error_message + + return result_dict + + def _update_inverter_setting( + self, + default_parameters: Dict[str, Any], + parameters: Union[Dict[str, Any], List[str]], + ) -> Dict[str, Any]: """ Applies settings for specified system based on serial number See README for known working settings - Arguments: - serial_number -- Serial number (device_sn) of the inverter (str) - setting_type -- Setting to be configured (str) - default_params -- Default set of parameters for the setting call (dict) - parameters -- Parameters to be sent to the system (dict or list of str) - (array which will be converted to a dictionary) + Args: + default_parameters (Dict[str, Any]): + Default set of parameters for the setting call (dict) + parameters (Union[Dict[str, Any], List[str]]): + Parameters to be sent to the system Returns: - JSON response from the server whether the configuration was successful + Dict[str, Any] + JSON response from the server whether the configuration was successful """ settings_parameters = parameters - - #If we've been passed an array then convert it into a dictionary + + # If we've been passed an array then convert it into a dictionary if isinstance(parameters, list): - settings_parameters = {} - for index, param in enumerate(parameters, start=1): - settings_parameters['param' + str(index)] = param - + settings_parameters = {f"param{idx}": param for idx, param in enumerate(parameters, start=1)} + settings_parameters = {**default_parameters, **settings_parameters} - response = self.session.post(self.get_url('newTcpsetAPI.do'), - params=settings_parameters) - + response = self.session.post(self.get_url("newTcpsetAPI.do"), params=settings_parameters) + return response.json() - def update_mix_inverter_setting(self, serial_number, setting_type, parameters): + def update_mix_inverter_setting( + self, + serial_number: str, + setting_type: str, + parameters: Union[Dict[str, Any], List[str]], + ) -> Dict[str, Any]: """ Alias for setting inverter parameters on a mix inverter See README for known working settings - Arguments: - serial_number -- Serial number (device_sn) of the inverter (str) - setting_type -- Setting to be configured (str) - parameters -- Parameters to be sent to the system (dict or list of str) - (array which will be converted to a dictionary) + Args: + serial_number (str): + The ID of the (MIX) inverter + setting_type (str): + Setting to be configured + parameters (Union[Dict[str, Any], List[str]]): + Parameters to be sent to the system Returns: - JSON response from the server whether the configuration was successful + Dict[str, Any]: + JSON response from the server whether the configuration was successful """ - default_parameters = { - 'op': 'mixSetApiNew', - 'serialNum': serial_number, - 'type': setting_type - } - return self.update_inverter_setting(serial_number, setting_type, - default_parameters, parameters) - def update_ac_inverter_setting(self, serial_number, setting_type, parameters): + default_parameters = {"op": "mixSetApiNew", "serialNum": serial_number, "type": setting_type} + return self._update_inverter_setting(default_parameters=default_parameters, parameters=parameters) + + def update_ac_inverter_setting( + self, + serial_number: str, + setting_type: str, + parameters: Union[Dict[str, Any], List[str]], + ) -> Dict[str, Any]: """ Alias for setting inverter parameters on an AC-coupled inverter See README for known working settings - Arguments: - serial_number -- Serial number (device_sn) of the inverter (str) - setting_type -- Setting to be configured (str) - parameters -- Parameters to be sent to the system (dict or list of str) - (array which will be converted to a dictionary) + Args: + serial_number (str): + The ID of the (SPA) inverter + setting_type (str): + Setting to be configured + parameters (Union[Dict[str, Any], List[str]]): + Parameters to be sent to the system Returns: - JSON response from the server whether the configuration was successful - """ - default_parameters = { - 'op': 'spaSetApi', - 'serialNum': serial_number, - 'type': setting_type - } - return self.update_inverter_setting(serial_number, setting_type, - default_parameters, parameters) + Dict[str, Any]: + JSON response from the server whether the configuration was successful - def update_tlx_inverter_time_segment(self, serial_number, segment_id, batt_mode, start_time, end_time, enabled): + """ + default_parameters = {"op": "spaSetApi", "serialNum": serial_number, "type": setting_type} + return self._update_inverter_setting(default_parameters=default_parameters, parameters=parameters) + + def update_tlx_inverter_time_segment( + self, + serial_number: str, + segment_id: int, + batt_mode: int, + start_time: datetime.datetime, + end_time: datetime.datetime, + enabled: bool, + ) -> Dict[str, Any]: """ Updates the time segment settings for a TLX hybrid inverter. - Arguments: - serial_number -- Serial number (device_sn) of the inverter (str) - segment_id -- ID of the time segment to be updated (int) - batt_mode -- Battery mode (int) - start_time -- Start time of the segment (datetime.time) - end_time -- End time of the segment (datetime.time) - enabled -- Whether the segment is enabled (bool) + Args: + serial_number (str): + The ID of the (TLX) inverter + segment_id (int): + ID of the time segment to be updated + batt_mode (int): + Battery mode + start_time (datetime.time): + Start time of the segment (datetime.time): + end_time (datetime.time): + End time of the segment + enabled (bool): + Whether the segment is enabled Returns: - JSON response from the server whether the configuration was successful + Dict[str, Any]: + JSON response from the server whether the configuration was successful """ - params = { - 'op': 'tlxSet' - } + + params = {"op": "tlxSet"} data = { - 'serialNum': serial_number, - 'type': f'time_segment{segment_id}', - 'param1': batt_mode, - 'param2': start_time.strftime('%H'), - 'param3': start_time.strftime('%M'), - 'param4': end_time.strftime('%H'), - 'param5': end_time.strftime('%M'), - 'param6': '1' if enabled else '0' + "serialNum": serial_number, + "type": f"time_segment{segment_id}", + "param1": batt_mode, + "param2": start_time.strftime("%H"), + "param3": start_time.strftime("%M"), + "param4": end_time.strftime("%H"), + "param5": end_time.strftime("%M"), + "param6": "1" if enabled else "0", } - - response = self.session.post(self.get_url('newTcpsetAPI.do'), params=params, data=data) + + response = self.session.post(self.get_url("newTcpsetAPI.do"), params=params, data=data) + result = response.json() - - if not result.get('success', False): + + if not result.get("success", False): raise Exception(f"Failed to update TLX inverter time segment: {result.get('msg', 'Unknown error')}") - + return result - def update_tlx_inverter_setting(self, serial_number, setting_type, parameter): + def update_tlx_inverter_setting( + self, + serial_number: str, + setting_type: str, + parameter: Union[ + List[Any], + Dict[str, Any], + Any, # bool, float, str,... + ], + ) -> Dict[str, Any]: """ Alias for setting parameters on a tlx hybrid inverter See README for known working settings - Arguments: - serial_number -- Serial number (device_sn) of the inverter (str) - setting_type -- Setting to be configured (str) - parameter -- Parameter(s) to be sent to the system (str, dict, list of str) - (array which will be converted to a dictionary) + Args: + serial_number (str): + The ID (serial number/device_sn/tlx_id) of the TLX inverter. + setting_type (str): + Name of setting to be configured (e.g. "pv_active_p_rate") + parameter (Union[str, List[Any], Dict[str, Any]]): + Parameter(s) to be sent to the system + * str/int will be converted to {"param1": parameter} + * list will be converted to {"param1": parameter[0], ...} + * dict will be passed as is. Format must be {"param1": "value1", "param^2": "value2", ...} Returns: - JSON response from the server whether the configuration was successful + Dict[str, Any] + JSON response from the server whether the configuration was successful """ - default_parameters = { - 'op': 'tlxSet', - 'serialNum': serial_number, - 'type': setting_type - } + default_parameters = {"op": "tlxSet", "serialNum": serial_number, "type": setting_type} - # If parameter is a single value, convert it to a dictionary + # If parameter is a single value or list of values, convert it to a dictionary if not isinstance(parameter, (dict, list)): - parameter = {'param1': parameter} + parameter = {"param1": parameter} elif isinstance(parameter, list): - parameter = {f'param{index+1}': param for index, param in enumerate(parameter)} + parameter = {f"param{index+1}": param for index, param in enumerate(parameter)} - return self.update_inverter_setting(serial_number, setting_type, - default_parameters, parameter) + return self._update_inverter_setting(default_parameters=default_parameters, parameters=parameter) - - def update_noah_settings(self, serial_number, setting_type, parameters): + def update_noah_settings( + self, serial_number: str, setting_type: str, parameters: Union[Dict[str, str], List[str]] + ) -> Dict[str, Any]: """ Applies settings for specified noah device based on serial number See README for known working settings - Arguments: - serial_number -- Serial number (device_sn) of the noah (str) - setting_type -- Setting to be configured (str) - parameters -- Parameters to be sent to the system (dict or list of str) - (array which will be converted to a dictionary) + Args: + serial_number (str): + Serial number (device_sn) of the noah + setting_type (str): + Setting to be configured + parameters (Union[Dict[str, str], List[str]]): + Parameters to be sent to the system Returns: - JSON response from the server whether the configuration was successful + Dict[str, Any]: + JSON response from the server whether the configuration was successful """ - default_parameters = { - 'serialNum': serial_number, - 'type': setting_type - } + + default_parameters = {"serialNum": serial_number, "type": setting_type} settings_parameters = parameters - - #If we've been passed an array then convert it into a dictionary + + # If we've been passed an array then convert it into a dictionary if isinstance(parameters, list): settings_parameters = {} for index, param in enumerate(parameters, start=1): - settings_parameters['param' + str(index)] = param - + settings_parameters["param" + str(index)] = param + settings_parameters = {**default_parameters, **settings_parameters} - response = self.session.post(self.get_url('noahDeviceApi/noah/set'), - data=settings_parameters) - + response = self.session.post(self.get_url("noahDeviceApi/noah/set"), data=settings_parameters) + return response.json() + def weather(self, language: str = "en"): + """ + Get weather data as shown in app dashboard + + Args: + language (str) = "en": + e.g. "en" / "de" / "cn" / ... + + Returns: + Dict[str, Any]: A dictionary containing the weather data + e.g. + { + 'city': '{CITY_NAME}', + 'week': 'Thursday' + 'radiant': '', + 'tempType': 0, + 'status': 'ok', + 'basic': { + 'admin_area': '{COUNTY_NAME}', + 'cnty': 'Germany', + 'location': '{CITY_NAME}', + 'parent_city': '{CITY_NAME}', + 'sr': '07:00', # sunrise time + 'ss': '17:30', # sunset time + 'toDay': '2025-01-30' + }, + 'now': { + 'cloud': '11', # cloudiness in percent + 'cond_code': '100', # weather condition code + 'cond_txt': 'Sunny', + 'fl': '7', # feels like temperature, °C + 'hum': '70', # Relative humidity in percent + 'newTmp': '9.0°C', + 'pcpn': '0.0', # Accumulated precipitation in the last hour, mm/h + 'pres': '1017', # Atmospheric pressure, hPa + 'tmp': '9', # Temperature, °C + 'wind_deg': '180', # Wind direction in azimuth degree, ° + 'wind_dir': 'S', # Wind direction str. e.g. "ESE" + 'wind_sc': '2', # Wind scale, Beaufort scale 0-12(-17) + 'wind_spd': '7' # Wind speed, km/h + }, + 'update': { + 'loc': '2025-01-30 22:06', + 'utc': '2025-01-30 14:06' + }, + } + """ + + url = self.get_url("newPlantAPI.do") + params = {"op": "getAPPWeattherTwo"} + data = {"language": language} + response = self.session.post(url=url, params=params, data=data) + response_json = response.json() + response_json = response_json.get("obj", {}) + response_json.update(response_json["data"]["HeWeather6"][0]) + response_json.pop("dataStr", None) + response_json.pop("data", None) + return response_json