Skip to content

Commit ecb7784

Browse files
Added support for TLX hybrid inverters (#87)
Co-authored-by: indykoning <[email protected]>
1 parent 75b6b73 commit ecb7784

File tree

5 files changed

+732
-83
lines changed

5 files changed

+732
-83
lines changed

README.md

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,12 @@ Any methods that may be useful.
2828

2929
`api.plant_info(plant_id)` Get info for specified plant.
3030

31+
`api.plant_settings(plant_id)` Get the current settings for the specified plant
32+
3133
`api.plant_detail(plant_id, timespan<1=day, 2=month>, date)` Get details of a specific plant.
3234

35+
`api.plant_energy_data(plant_id)` Get energy data for the specified plant.
36+
3337
`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).
3438

3539
`api.device_list(plant_id)` Get a list of devices in specified plant.
@@ -38,10 +42,26 @@ Any methods that may be useful.
3842

3943
`api.inverter_detail(inverter_id)` Get detailed data on inverter.
4044

45+
`api.tlx_system_status(plant_id, tlx_id)` Get system status.
46+
47+
`api.tlx_energy_overview(plant_id, tlx_id)` Get energy overview of the system.
48+
49+
`api.tlx_energy_prod_cons(plant_id, tlx_id)` Get energy production and consumption for the system.
50+
4151
`api.tlx_data(tlx_id, date)` Get some basic data of a specific date for the tlx type inverter.
4252

4353
`api.tlx_detail(tlx_id)` Get detailed data on a tlx type inverter.
4454

55+
`api.tlx_params(tlx_id)` Get parameters for the tlx type inverter.
56+
57+
`api.tlx_get_all_settings(tlx_id)` Get all possible settings for the tlx type inverter.
58+
59+
`api.tlx_get_enabled_settings(tlx_id)` Get all enabled settings for the tlx type inverter.
60+
61+
`api.tlx_battery_info(serial_num)` Get battery info for tlx systems.
62+
63+
`api.tlx_battery_info_detailed(serial_num)` Get detailed battery info.
64+
4565
`api.mix_info(mix_id, plant_id=None)` Get high level information about the Mix system including daily and overall totals. NOTE: `plant_id` is an optional parameter, it does not appear to be used by the remote API, but is used by the mobile app these calls were reverse-engineered from.
4666

4767
`api.mix_totals(mix_id, plant_id)` Get daily and overall total information for the Mix system (duplicates some of the information from `mix_info`).
@@ -58,8 +78,6 @@ Any methods that may be useful.
5878

5979
`api.storage_energy_overview(plant_id, storage_id)` Get the information you see in the "Generation overview".
6080

61-
`api.get_plant_settings(plant_id)` Get the current settings for the specified plant
62-
6381
`api.is_plant_noah_system(plant_id)` Get the Information if noah devices are configured for the specified plant
6482

6583
`api.noah_system_status(serial_number)` Get the current status for the specified noah device e.g. workMode, soc, chargePower, disChargePower, current import/export etc.
@@ -68,6 +86,10 @@ Any methods that may be useful.
6886

6987
`api.update_plant_settings(plant_id, changed_settings, current_settings)` Update the settings for a plant to the values specified in the dictionary, if the `current_settings` are not provided it will look them up automatically using the `get_plant_settings` function - See 'Plant settings' below for more information
7088

89+
`api.update_tlx_inverter_setting(serial_number, setting_type, parameter)` Applies the provided parameter for the specified setting on the specified tlx inverter; see 'Inverter settings' below for more information.
90+
91+
`api.update_tlx_inverter_time_segment(serial_number, segment_id, batt_mode, start_time, end_time, enabled)` Updates one of the 9 time segments with the specified battery mode (load, battery, grid first); see 'Inverter settings' below for more information.
92+
7193
`api.update_mix_inverter_setting(serial_number, setting_type, parameters)` Applies the provided parameters (dictionary or array) for the specified setting on the specified mix inverter; see 'Inverter settings' below for more information
7294

7395
`api.update_ac_inverter_setting(serial_number, setting_type, parameters)` Applies the provided parameters (dictionary or array) for the specified setting on the specified AC-coupled inverter; see 'Inverter settings' below for more information
@@ -137,7 +159,7 @@ The plant settings function(s) allow you to re-configure the settings for a spec
137159
The function `update_plant_settings` allows you to provide a python dictionary of any/all of the above settings and change their value.
138160

139161
## Inverter Settings
140-
NOTE: The inverter settings function appears to only work with 'mix' systems based on the API call that it makes being specific to 'mix' inverters
162+
NOTE: The inverter settings function appears to only work with 'mix' and 'tlx' systems based on the API call that it makes being specific to those inverter types
141163

142164
The inverter settings function(s) allow you to change individual values on your inverter e.g. time, charging period etc.
143165
From what has been reverse engineered from the api, each setting has a `setting_type` and a set of `parameters` that are relevant to it.
@@ -191,8 +213,32 @@ Known working settings & parameters are as follows (all parameter values are str
191213
* `param15`: Schedule 3 - End time - Hour e.g. "02" (2am)
192214
* `param16`: Schedule 3 - End time - Minute e.g. "00" (0 minutes)
193215
* `param17`: Schedule 3 - Enabled/Disabled (0 = Disabled, 1 = Enabled)
194-
195-
The three functions `update_mix_inverter_setting`, `update_ac_inverter_setting`, and `update_inverter_setting` take either a dictionary or an array. If an array is passed it will automatically generate the `paramN` key based on array index since all params for settings seem to used the same numbering scheme.
216+
* **TLX inverter settings**
217+
* function: `api.update_tlx_inverter_setting`
218+
* type: `charge_power`
219+
* param1: Charging power % (value between 0 and 100)
220+
* type: `charge_stop_soc`
221+
* param1: Charge Stop SOC
222+
* type: `discharge_power`
223+
* param1: Discharging power % (value between 0 and 100)
224+
* type: `on_grid_discharge_stop_soc`
225+
* param1: On-grid discharge Stop SOC
226+
* type: `discharge_stop_soc`
227+
* param1: Off-grid discharge Stop SOC
228+
* type: `ac_charge`
229+
* param1: Allow AC (grid) charging (0 = Disabled, 1 = Enabled)
230+
* type: `pf_sys_year`
231+
* param1: datetime in format: `YYYY-MM-DD HH:MM:SS`
232+
* function: `api.update_tlx_inverter_time_segment`
233+
* segment_id: The segment to update (1-9)
234+
* batt_mode: Battery Mode for the segment: 0=Load First(Self-Consumption), 1=Battery First, 2=Grid First
235+
* start_time: timedate object with start time of segment with format HH:MM
236+
* end_time: timedate object with end time of segment with format HH:MM
237+
* enabled: time segment enabled, boolean: True (Enabled), False (Disabled)
238+
239+
The four functions `update_tlx_inverter_setting`, `update_mix_inverter_setting`, `update_ac_inverter_setting`, and `update_inverter_setting` take either a dictionary or an array. If an array is passed it will automatically generate the `paramN` key based on array index since all params for settings seem to used the same numbering scheme.
240+
241+
Only the settings described above have been tested with `update_tlx_inverter_setting` and they all take only one single parameter. It is very likely that the function works with all settings returned by `tlx_get_enabled_settings`, but this has not been tested. A helper function `update_tlx_inverter_time_segment` is provided for the settings that require more than one parameter.
196242

197243
## Noah Settings
198244
The noah settings function allow you to change individual values on your noah system e.g. system default output power, battery management, operation mode and currency

examples/tlx_example.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import growattServer
2+
import datetime
3+
import getpass
4+
import json
5+
6+
"""
7+
# Example script controlling a Growatt MID-30KTL3-XH + APX battery hybrid system by emulating the ShinePhone iOS app.
8+
# The same API calls are used by the ShinePhone Android app as well. Traffic intercepted using HTTP Toolkit.
9+
#
10+
# The plant / energy / device APIs seem to be generic for all Growatt systems, while the inverter and battery APIs use the TLX APIs.
11+
#
12+
# The available settings under the 'Control' tab in ShinePhone are created by combining the results from two function calls:
13+
# tlx_get_all_settings() seem to returns the sum of all settings for all systems while tlx_get_enabled_settings() tells
14+
# which of these settings are valid for the TLX system.
15+
#
16+
# Settings that takes a single parameter can be set using update_tlx_inverter_setting(). A helper function, update_tlx_inverter_time_segment()
17+
# is provided for updating time segments which take several parameters. The inverter is picky and time intervals can't be overlapping,
18+
# even if they are disabled.
19+
#
20+
# The set functions are commented out in the example, uncomment to test, and use at your own risk. Most likely all settings returned in
21+
# tlx_get_enabled_settings() can be set using update_tlx_inverter_setting(), but has not been tested.
22+
#
23+
"""
24+
25+
# Prompt user for username
26+
username=input("Enter username:")
27+
28+
# Prompt user to input password
29+
user_pass=getpass.getpass("Enter password:")
30+
31+
user_agent = 'ShinePhone/8.1.17 (iPhone; iOS 15.6.1; Scale/2.00)'
32+
api = growattServer.GrowattApi(agent_identifier=user_agent)
33+
34+
login_response = api.login(username, user_pass)
35+
user_id = login_response['user']['id']
36+
print("Login successful, user_id:", user_id)
37+
38+
# Plant info
39+
plant_list = api.plant_list_two()
40+
plant_id = plant_list[0]['id']
41+
plant_info = api.plant_info(plant_id)
42+
print("Plant info:", json.dumps(plant_info, indent=4, sort_keys=True))
43+
44+
# Energy data (used in the 'Plant' Tab)
45+
energy_data = api.plant_energy_data(plant_id)
46+
print("Plant Energy data", json.dumps(energy_data, indent=4, sort_keys=True))
47+
48+
# Devices
49+
devices = api.device_list(plant_id)
50+
print("Devices:", json.dumps(devices, indent=4, sort_keys=True))
51+
52+
for device in devices:
53+
if device['deviceType'] == 'tlx':
54+
# Inverter info (used in inverter view)
55+
inverter_sn = device['deviceSn']
56+
inverter_info = api.tlx_params(inverter_sn)
57+
print("Inverter info:", json.dumps(inverter_info, indent=4, sort_keys=True))
58+
59+
# PV production data
60+
data = api.tlx_data(inverter_sn, datetime.datetime.now())
61+
print("PV production data:", json.dumps(data, indent=4, sort_keys=True))
62+
63+
# System settings
64+
all_settings = api.tlx_all_settings(inverter_sn)
65+
enabled_settings = api.tlx_enabled_settings(inverter_sn)
66+
# 'on_grid_discharge_stop_soc' is present in web UI, but for some reason not
67+
# returned in enabled settings so we enable it manually here instead
68+
enabled_settings['enable']['on_grid_discharge_stop_soc'] = '1'
69+
enabled_keys = enabled_settings['enable'].keys()
70+
available_settings = {k: v for k, v in all_settings.items() if k in enabled_keys}
71+
print("System settings:", json.dumps(available_settings, indent=4, sort_keys=True))
72+
73+
# System status
74+
data = api.tlx_system_status(plant_id, inverter_sn)
75+
print("System status:", json.dumps(data, indent=4, sort_keys=True))
76+
77+
# Energy overview
78+
data = api.tlx_energy_overview(plant_id, inverter_sn)
79+
print("Energy overview:", json.dumps(data, indent=4, sort_keys=True))
80+
81+
# Energy production & consumption
82+
data = api.tlx_energy_prod_cons(plant_id, inverter_sn)
83+
print("Energy production & consumption:", json.dumps(data, indent=4, sort_keys=True))
84+
85+
elif device['deviceType'] == 'bat':
86+
# Battery info
87+
batt_info = api.tlx_battery_info(device['deviceSn'])
88+
print("Battery info:", json.dumps(batt_info, indent=4, sort_keys=True))
89+
batt_info_detailed = api.tlx_battery_info_detailed(plant_id, device['deviceSn'])
90+
print("Battery info: detailed", json.dumps(batt_info_detailed, indent=4, sort_keys=True))
91+
92+
93+
# Examples of updating settings, uncomment to use
94+
95+
# Set charging power to 95%
96+
#res = api.update_tlx_inverter_setting(inverter_sn, 'charge_power', 95)
97+
#print(res)
98+
99+
# Turn on AC charging
100+
#res = api.update_tlx_inverter_setting(inverter_sn, 'ac_charge', 1)
101+
#print(res)
102+
103+
# Enable Load First between 00:01 and 11:59 using time segment 1
104+
#res = api.update_tlx_inverter_time_segment(serial_number = inverter_sn,
105+
# segment_id = 1,
106+
# batt_mode = growattServer.BATT_MODE_LOAD_FIRST,
107+
# start_time = datetime.time(00, 1),
108+
# end_time = datetime.time(11, 59),
109+
# enabled=True)
110+
#print(res)

examples/tlx_example_dashboard.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
2+
import growattServer
3+
import getpass
4+
5+
# Example script fetching key power and today+total energy metrics from a Growatt MID-30KTL3-XH (TLX) + APX battery hybrid system
6+
#
7+
# There is a lot of overlap in what the various Growatt APIs returns.
8+
# tlx_detail() contains the bulk of the needed data, but some info is missing and is fetched from
9+
# tlx_system_status(), tlx_energy_overview() and tlx_battery_info_detailed() instead
10+
11+
12+
# Prompt user for username
13+
username=input("Enter username:")
14+
15+
# Prompt user to input password
16+
user_pass=getpass.getpass("Enter password:")
17+
18+
# Login, emulating the Growatt app
19+
user_agent = 'ShinePhone/8.1.17 (iPhone; iOS 15.6.1; Scale/2.00)'
20+
api = growattServer.GrowattApi(agent_identifier=user_agent)
21+
login_response = api.login(username, user_pass)
22+
if not login_response['success']:
23+
print(f"Failed to log in, msg: {login_response['msg']}, error: {login_response['error']}")
24+
exit()
25+
26+
# Get plant(s)
27+
plant_list = api.plant_list_two()
28+
plant_id = plant_list[0]['id']
29+
30+
# Get devices in plant
31+
devices = api.device_list(plant_id)
32+
33+
# Iterate over all devices. Here we are interested in data from 'tlx' inverters and 'bat' devices
34+
batteries_info = []
35+
for device in devices:
36+
if device['deviceType'] == 'tlx':
37+
inverter_sn = device['deviceSn']
38+
39+
# Inverter detail, contains the bulk of energy and power values
40+
inverter_detail = api.tlx_detail(inverter_sn).get('data')
41+
42+
# Energy overview is used to retrieve "epvToday" which is not present in tlx_detail() for some reason
43+
energy_overview = api.tlx_energy_overview(plant_id, inverter_sn)
44+
45+
# System status, contains power values, not available in inverter_detail()
46+
system_status = api.tlx_system_status(plant_id, inverter_sn)
47+
48+
if device['deviceType'] == 'bat':
49+
batt_info = api.tlx_battery_info(device['deviceSn'])
50+
if batt_info.get('lost'):
51+
# Disconnected batteries are listed with 'old' power/energy/SOC data
52+
# Therefore we check it it's 'lost' and skip it in that case.
53+
print("'Lost' battery found, skipping")
54+
continue
55+
56+
# Battery info
57+
batt_info = api.tlx_battery_info_detailed(plant_id, device['deviceSn']).get('data')
58+
59+
if float(batt_info['chargeOrDisPower']) > 0:
60+
bdcChargePower = float(batt_info['chargeOrDisPower'])
61+
bdcDischargePower = 0
62+
else:
63+
bdcChargePower = 0
64+
bdcDischargePower = float(batt_info['chargeOrDisPower'])
65+
bdcDischargePower = -bdcDischargePower
66+
67+
battery_data = {
68+
'serialNum': device['deviceSn'],
69+
'bdcChargePower': bdcChargePower,
70+
'bdcDischargePower': bdcDischargePower,
71+
'dischargeTotal': batt_info['dischargeTotal'],
72+
'soc': batt_info['soc']
73+
}
74+
batteries_info.append(battery_data)
75+
76+
77+
solar_production = f'{float(energy_overview["epvToday"]):.1f}/{float(energy_overview["epvTotal"]):.1f}'
78+
solar_production_pv1 = f'{float(inverter_detail["epv1Today"]):.1f}/{float(inverter_detail["epv1Total"]):.1f}'
79+
solar_production_pv2 = f'{float(inverter_detail["epv2Today"]):.1f}/{float(inverter_detail["epv2Total"]):.1f}'
80+
energy_output = f'{float(inverter_detail["eacToday"]):.1f}/{float(inverter_detail["eacTotal"]):.1f}'
81+
system_production = f'{float(inverter_detail["esystemToday"]):.1f}/{float(inverter_detail["esystemTotal"]):.1f}'
82+
battery_charged = f'{float(inverter_detail["echargeToday"]):.1f}/{float(inverter_detail["echargeTotal"]):.1f}'
83+
battery_grid_charge = f'{float(inverter_detail["eacChargeToday"]):.1f}/{float(inverter_detail["eacChargeTotal"]):.1f}'
84+
battery_discharged = f'{float(inverter_detail["edischargeToday"]):.1f}/{float(inverter_detail["edischargeTotal"]):.1f}'
85+
exported_to_grid = f'{float(inverter_detail["etoGridToday"]):.1f}/{float(inverter_detail["etoGridTotal"]):.1f}'
86+
imported_from_grid = f'{float(inverter_detail["etoUserToday"]):.1f}/{float(inverter_detail["etoUserTotal"]):.1f}'
87+
load_consumption = f'{float(inverter_detail["elocalLoadToday"]):.1f}/{float(inverter_detail["elocalLoadTotal"]):.1f}'
88+
self_consumption = f'{float(inverter_detail["eselfToday"]):.1f}/{float(inverter_detail["eselfTotal"]):.1f}'
89+
battery_charged = f'{float(inverter_detail["echargeToday"]):.1f}/{float(inverter_detail["echargeTotal"]):.1f}'
90+
91+
print("\nGeneration overview Today/Total(kWh)")
92+
print(f'Solar production {solar_production:>22}')
93+
print(f' Solar production, PV1 {solar_production_pv1:>22}')
94+
print(f' Solar production, PV2 {solar_production_pv2:>22}')
95+
print(f'Energy Output {energy_output:>22}')
96+
print(f'System production {system_production:>22}')
97+
print(f'Self consumption {self_consumption:>22}')
98+
print(f'Load consumption {load_consumption:>22}')
99+
print(f'Battery Charged {battery_charged:>22}')
100+
print(f' Charged from grid {battery_grid_charge:>22}')
101+
print(f'Battery Discharged {battery_discharged:>22}')
102+
print(f'Import from grid {imported_from_grid:>22}')
103+
print(f'Export to grid {exported_to_grid:>22}')
104+
105+
print("\nPower overview (Watts)")
106+
print(f'AC Power {float(inverter_detail["pac"]):>22.1f}')
107+
print(f'Self power {float(inverter_detail["pself"]):>22.1f}')
108+
print(f'Export power {float(inverter_detail["pacToGridTotal"]):>22.1f}')
109+
print(f'Import power {float(inverter_detail["pacToUserTotal"]):>22.1f}')
110+
print(f'Local load power {float(inverter_detail["pacToLocalLoad"]):>22.1f}')
111+
print(f'PV power {float(inverter_detail["psystem"]):>22.1f}')
112+
print(f'PV #1 power {float(inverter_detail["ppv1"]):>22.1f}')
113+
print(f'PV #2 power {float(inverter_detail["ppv2"]):>22.1f}')
114+
print(f'Battery charge power {float(system_status["chargePower"])*1000:>22.1f}')
115+
if len(batteries_info) > 0:
116+
print(f'Batt #1 charge power {float(batteries_info[0]["bdcChargePower"]):>22.1f}')
117+
if len(batteries_info) > 1:
118+
print(f'Batt #2 charge power {float(batteries_info[1]["bdcChargePower"]):>22.1f}')
119+
print(f'Battery discharge power {float(system_status["pdisCharge"])*1000:>18.1f}')
120+
if len(batteries_info) > 0:
121+
print(f'Batt #1 discharge power {float(batteries_info[0]["bdcDischargePower"]):>22.1f}')
122+
if len(batteries_info) > 1:
123+
print(f'Batt #2 discharge power {float(batteries_info[1]["bdcDischargePower"]):>22.1f}')
124+
if len(batteries_info) > 0:
125+
print(f'Batt #1 SOC {int(batteries_info[0]["soc"]):>21}%')
126+
if len(batteries_info) > 1:
127+
print(f'Batt #2 SOC {int(batteries_info[1]["soc"]):>21}%')

0 commit comments

Comments
 (0)