-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added support for TLX hybrid inverters (#87)
Co-authored-by: indykoning <[email protected]>
- Loading branch information
1 parent
75b6b73
commit ecb7784
Showing
5 changed files
with
732 additions
and
83 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import growattServer | ||
import datetime | ||
import getpass | ||
import json | ||
|
||
""" | ||
# Example script controlling a Growatt MID-30KTL3-XH + APX battery hybrid system by emulating the ShinePhone iOS app. | ||
# The same API calls are used by the ShinePhone Android app as well. Traffic intercepted using HTTP Toolkit. | ||
# | ||
# The plant / energy / device APIs seem to be generic for all Growatt systems, while the inverter and battery APIs use the TLX APIs. | ||
# | ||
# The available settings under the 'Control' tab in ShinePhone are created by combining the results from two function calls: | ||
# tlx_get_all_settings() seem to returns the sum of all settings for all systems while tlx_get_enabled_settings() tells | ||
# which of these settings are valid for the TLX system. | ||
# | ||
# Settings that takes a single parameter can be set using update_tlx_inverter_setting(). A helper function, update_tlx_inverter_time_segment() | ||
# is provided for updating time segments which take several parameters. The inverter is picky and time intervals can't be overlapping, | ||
# even if they are disabled. | ||
# | ||
# The set functions are commented out in the example, uncomment to test, and use at your own risk. Most likely all settings returned in | ||
# tlx_get_enabled_settings() can be set using update_tlx_inverter_setting(), but has not been tested. | ||
# | ||
""" | ||
|
||
# Prompt user for username | ||
username=input("Enter username:") | ||
|
||
# Prompt user to input password | ||
user_pass=getpass.getpass("Enter password:") | ||
|
||
user_agent = 'ShinePhone/8.1.17 (iPhone; iOS 15.6.1; Scale/2.00)' | ||
api = growattServer.GrowattApi(agent_identifier=user_agent) | ||
|
||
login_response = api.login(username, user_pass) | ||
user_id = login_response['user']['id'] | ||
print("Login successful, user_id:", user_id) | ||
|
||
# Plant info | ||
plant_list = api.plant_list_two() | ||
plant_id = plant_list[0]['id'] | ||
plant_info = api.plant_info(plant_id) | ||
print("Plant info:", json.dumps(plant_info, indent=4, sort_keys=True)) | ||
|
||
# Energy data (used in the 'Plant' Tab) | ||
energy_data = api.plant_energy_data(plant_id) | ||
print("Plant Energy data", json.dumps(energy_data, indent=4, sort_keys=True)) | ||
|
||
# Devices | ||
devices = api.device_list(plant_id) | ||
print("Devices:", json.dumps(devices, indent=4, sort_keys=True)) | ||
|
||
for device in devices: | ||
if device['deviceType'] == 'tlx': | ||
# Inverter info (used in inverter view) | ||
inverter_sn = device['deviceSn'] | ||
inverter_info = api.tlx_params(inverter_sn) | ||
print("Inverter info:", json.dumps(inverter_info, indent=4, sort_keys=True)) | ||
|
||
# PV production data | ||
data = api.tlx_data(inverter_sn, datetime.datetime.now()) | ||
print("PV production data:", json.dumps(data, indent=4, sort_keys=True)) | ||
|
||
# System settings | ||
all_settings = api.tlx_all_settings(inverter_sn) | ||
enabled_settings = api.tlx_enabled_settings(inverter_sn) | ||
# 'on_grid_discharge_stop_soc' is present in web UI, but for some reason not | ||
# returned in enabled settings so we enable it manually here instead | ||
enabled_settings['enable']['on_grid_discharge_stop_soc'] = '1' | ||
enabled_keys = enabled_settings['enable'].keys() | ||
available_settings = {k: v for k, v in all_settings.items() if k in enabled_keys} | ||
print("System settings:", json.dumps(available_settings, indent=4, sort_keys=True)) | ||
|
||
# System status | ||
data = api.tlx_system_status(plant_id, inverter_sn) | ||
print("System status:", json.dumps(data, indent=4, sort_keys=True)) | ||
|
||
# Energy overview | ||
data = api.tlx_energy_overview(plant_id, inverter_sn) | ||
print("Energy overview:", json.dumps(data, indent=4, sort_keys=True)) | ||
|
||
# Energy production & consumption | ||
data = api.tlx_energy_prod_cons(plant_id, inverter_sn) | ||
print("Energy production & consumption:", json.dumps(data, indent=4, sort_keys=True)) | ||
|
||
elif device['deviceType'] == 'bat': | ||
# Battery info | ||
batt_info = api.tlx_battery_info(device['deviceSn']) | ||
print("Battery info:", json.dumps(batt_info, indent=4, sort_keys=True)) | ||
batt_info_detailed = api.tlx_battery_info_detailed(plant_id, device['deviceSn']) | ||
print("Battery info: detailed", json.dumps(batt_info_detailed, indent=4, sort_keys=True)) | ||
|
||
|
||
# Examples of updating settings, uncomment to use | ||
|
||
# Set charging power to 95% | ||
#res = api.update_tlx_inverter_setting(inverter_sn, 'charge_power', 95) | ||
#print(res) | ||
|
||
# Turn on AC charging | ||
#res = api.update_tlx_inverter_setting(inverter_sn, 'ac_charge', 1) | ||
#print(res) | ||
|
||
# Enable Load First between 00:01 and 11:59 using time segment 1 | ||
#res = api.update_tlx_inverter_time_segment(serial_number = inverter_sn, | ||
# segment_id = 1, | ||
# batt_mode = growattServer.BATT_MODE_LOAD_FIRST, | ||
# start_time = datetime.time(00, 1), | ||
# end_time = datetime.time(11, 59), | ||
# enabled=True) | ||
#print(res) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
|
||
import growattServer | ||
import getpass | ||
|
||
# Example script fetching key power and today+total energy metrics from a Growatt MID-30KTL3-XH (TLX) + APX battery hybrid system | ||
# | ||
# There is a lot of overlap in what the various Growatt APIs returns. | ||
# tlx_detail() contains the bulk of the needed data, but some info is missing and is fetched from | ||
# tlx_system_status(), tlx_energy_overview() and tlx_battery_info_detailed() instead | ||
|
||
|
||
# Prompt user for username | ||
username=input("Enter username:") | ||
|
||
# Prompt user to input password | ||
user_pass=getpass.getpass("Enter password:") | ||
|
||
# Login, emulating the Growatt app | ||
user_agent = 'ShinePhone/8.1.17 (iPhone; iOS 15.6.1; Scale/2.00)' | ||
api = growattServer.GrowattApi(agent_identifier=user_agent) | ||
login_response = api.login(username, user_pass) | ||
if not login_response['success']: | ||
print(f"Failed to log in, msg: {login_response['msg']}, error: {login_response['error']}") | ||
exit() | ||
|
||
# Get plant(s) | ||
plant_list = api.plant_list_two() | ||
plant_id = plant_list[0]['id'] | ||
|
||
# Get devices in plant | ||
devices = api.device_list(plant_id) | ||
|
||
# Iterate over all devices. Here we are interested in data from 'tlx' inverters and 'bat' devices | ||
batteries_info = [] | ||
for device in devices: | ||
if device['deviceType'] == 'tlx': | ||
inverter_sn = device['deviceSn'] | ||
|
||
# Inverter detail, contains the bulk of energy and power values | ||
inverter_detail = api.tlx_detail(inverter_sn).get('data') | ||
|
||
# Energy overview is used to retrieve "epvToday" which is not present in tlx_detail() for some reason | ||
energy_overview = api.tlx_energy_overview(plant_id, inverter_sn) | ||
|
||
# System status, contains power values, not available in inverter_detail() | ||
system_status = api.tlx_system_status(plant_id, inverter_sn) | ||
|
||
if device['deviceType'] == 'bat': | ||
batt_info = api.tlx_battery_info(device['deviceSn']) | ||
if batt_info.get('lost'): | ||
# Disconnected batteries are listed with 'old' power/energy/SOC data | ||
# Therefore we check it it's 'lost' and skip it in that case. | ||
print("'Lost' battery found, skipping") | ||
continue | ||
|
||
# Battery info | ||
batt_info = api.tlx_battery_info_detailed(plant_id, device['deviceSn']).get('data') | ||
|
||
if float(batt_info['chargeOrDisPower']) > 0: | ||
bdcChargePower = float(batt_info['chargeOrDisPower']) | ||
bdcDischargePower = 0 | ||
else: | ||
bdcChargePower = 0 | ||
bdcDischargePower = float(batt_info['chargeOrDisPower']) | ||
bdcDischargePower = -bdcDischargePower | ||
|
||
battery_data = { | ||
'serialNum': device['deviceSn'], | ||
'bdcChargePower': bdcChargePower, | ||
'bdcDischargePower': bdcDischargePower, | ||
'dischargeTotal': batt_info['dischargeTotal'], | ||
'soc': batt_info['soc'] | ||
} | ||
batteries_info.append(battery_data) | ||
|
||
|
||
solar_production = f'{float(energy_overview["epvToday"]):.1f}/{float(energy_overview["epvTotal"]):.1f}' | ||
solar_production_pv1 = f'{float(inverter_detail["epv1Today"]):.1f}/{float(inverter_detail["epv1Total"]):.1f}' | ||
solar_production_pv2 = f'{float(inverter_detail["epv2Today"]):.1f}/{float(inverter_detail["epv2Total"]):.1f}' | ||
energy_output = f'{float(inverter_detail["eacToday"]):.1f}/{float(inverter_detail["eacTotal"]):.1f}' | ||
system_production = f'{float(inverter_detail["esystemToday"]):.1f}/{float(inverter_detail["esystemTotal"]):.1f}' | ||
battery_charged = f'{float(inverter_detail["echargeToday"]):.1f}/{float(inverter_detail["echargeTotal"]):.1f}' | ||
battery_grid_charge = f'{float(inverter_detail["eacChargeToday"]):.1f}/{float(inverter_detail["eacChargeTotal"]):.1f}' | ||
battery_discharged = f'{float(inverter_detail["edischargeToday"]):.1f}/{float(inverter_detail["edischargeTotal"]):.1f}' | ||
exported_to_grid = f'{float(inverter_detail["etoGridToday"]):.1f}/{float(inverter_detail["etoGridTotal"]):.1f}' | ||
imported_from_grid = f'{float(inverter_detail["etoUserToday"]):.1f}/{float(inverter_detail["etoUserTotal"]):.1f}' | ||
load_consumption = f'{float(inverter_detail["elocalLoadToday"]):.1f}/{float(inverter_detail["elocalLoadTotal"]):.1f}' | ||
self_consumption = f'{float(inverter_detail["eselfToday"]):.1f}/{float(inverter_detail["eselfTotal"]):.1f}' | ||
battery_charged = f'{float(inverter_detail["echargeToday"]):.1f}/{float(inverter_detail["echargeTotal"]):.1f}' | ||
|
||
print("\nGeneration overview Today/Total(kWh)") | ||
print(f'Solar production {solar_production:>22}') | ||
print(f' Solar production, PV1 {solar_production_pv1:>22}') | ||
print(f' Solar production, PV2 {solar_production_pv2:>22}') | ||
print(f'Energy Output {energy_output:>22}') | ||
print(f'System production {system_production:>22}') | ||
print(f'Self consumption {self_consumption:>22}') | ||
print(f'Load consumption {load_consumption:>22}') | ||
print(f'Battery Charged {battery_charged:>22}') | ||
print(f' Charged from grid {battery_grid_charge:>22}') | ||
print(f'Battery Discharged {battery_discharged:>22}') | ||
print(f'Import from grid {imported_from_grid:>22}') | ||
print(f'Export to grid {exported_to_grid:>22}') | ||
|
||
print("\nPower overview (Watts)") | ||
print(f'AC Power {float(inverter_detail["pac"]):>22.1f}') | ||
print(f'Self power {float(inverter_detail["pself"]):>22.1f}') | ||
print(f'Export power {float(inverter_detail["pacToGridTotal"]):>22.1f}') | ||
print(f'Import power {float(inverter_detail["pacToUserTotal"]):>22.1f}') | ||
print(f'Local load power {float(inverter_detail["pacToLocalLoad"]):>22.1f}') | ||
print(f'PV power {float(inverter_detail["psystem"]):>22.1f}') | ||
print(f'PV #1 power {float(inverter_detail["ppv1"]):>22.1f}') | ||
print(f'PV #2 power {float(inverter_detail["ppv2"]):>22.1f}') | ||
print(f'Battery charge power {float(system_status["chargePower"])*1000:>22.1f}') | ||
if len(batteries_info) > 0: | ||
print(f'Batt #1 charge power {float(batteries_info[0]["bdcChargePower"]):>22.1f}') | ||
if len(batteries_info) > 1: | ||
print(f'Batt #2 charge power {float(batteries_info[1]["bdcChargePower"]):>22.1f}') | ||
print(f'Battery discharge power {float(system_status["pdisCharge"])*1000:>18.1f}') | ||
if len(batteries_info) > 0: | ||
print(f'Batt #1 discharge power {float(batteries_info[0]["bdcDischargePower"]):>22.1f}') | ||
if len(batteries_info) > 1: | ||
print(f'Batt #2 discharge power {float(batteries_info[1]["bdcDischargePower"]):>22.1f}') | ||
if len(batteries_info) > 0: | ||
print(f'Batt #1 SOC {int(batteries_info[0]["soc"]):>21}%') | ||
if len(batteries_info) > 1: | ||
print(f'Batt #2 SOC {int(batteries_info[1]["soc"]):>21}%') |
Oops, something went wrong.