Skip to content

Commit

Permalink
Added support for TLX hybrid inverters (#87)
Browse files Browse the repository at this point in the history
Co-authored-by: indykoning <[email protected]>
  • Loading branch information
johanzander and indykoning authored Nov 28, 2024
1 parent 75b6b73 commit ecb7784
Show file tree
Hide file tree
Showing 5 changed files with 732 additions and 83 deletions.
56 changes: 51 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ Any methods that may be useful.

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

`api.plant_settings(plant_id)` Get the current settings for the specified plant

`api.plant_detail(plant_id, timespan<1=day, 2=month>, date)` Get details of a specific plant.

`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.device_list(plant_id)` Get a list of devices in specified plant.
Expand All @@ -38,10 +42,26 @@ Any methods that may be useful.

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

`api.tlx_system_status(plant_id, tlx_id)` Get system status.

`api.tlx_energy_overview(plant_id, tlx_id)` Get energy overview of the system.

`api.tlx_energy_prod_cons(plant_id, tlx_id)` Get energy production and consumption for the system.

`api.tlx_data(tlx_id, date)` Get some basic data of a specific date for the tlx type inverter.

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

`api.tlx_params(tlx_id)` Get parameters for the tlx type inverter.

`api.tlx_get_all_settings(tlx_id)` Get all possible settings for the tlx type inverter.

`api.tlx_get_enabled_settings(tlx_id)` Get all enabled settings for the tlx type inverter.

`api.tlx_battery_info(serial_num)` Get battery info for tlx systems.

`api.tlx_battery_info_detailed(serial_num)` Get detailed battery info.

`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.

`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`).
Expand All @@ -58,8 +78,6 @@ Any methods that may be useful.

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

`api.get_plant_settings(plant_id)` Get the current settings for the specified plant

`api.is_plant_noah_system(plant_id)` Get the Information if noah devices are configured for the specified plant

`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.
Expand All @@ -68,6 +86,10 @@ Any methods that may be useful.

`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

`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.

`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.

`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

`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
Expand Down Expand Up @@ -137,7 +159,7 @@ The plant settings function(s) allow you to re-configure the settings for a spec
The function `update_plant_settings` allows you to provide a python dictionary of any/all of the above settings and change their value.

## Inverter Settings
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
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

The inverter settings function(s) allow you to change individual values on your inverter e.g. time, charging period etc.
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.
Expand Down Expand Up @@ -191,8 +213,32 @@ Known working settings & parameters are as follows (all parameter values are str
* `param15`: Schedule 3 - End time - Hour e.g. "02" (2am)
* `param16`: Schedule 3 - End time - Minute e.g. "00" (0 minutes)
* `param17`: Schedule 3 - Enabled/Disabled (0 = Disabled, 1 = Enabled)

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.
* **TLX inverter settings**
* function: `api.update_tlx_inverter_setting`
* type: `charge_power`
* param1: Charging power % (value between 0 and 100)
* type: `charge_stop_soc`
* param1: Charge Stop SOC
* type: `discharge_power`
* param1: Discharging power % (value between 0 and 100)
* type: `on_grid_discharge_stop_soc`
* param1: On-grid discharge Stop SOC
* type: `discharge_stop_soc`
* param1: Off-grid discharge Stop SOC
* type: `ac_charge`
* param1: Allow AC (grid) charging (0 = Disabled, 1 = Enabled)
* type: `pf_sys_year`
* param1: datetime in format: `YYYY-MM-DD HH:MM:SS`
* function: `api.update_tlx_inverter_time_segment`
* segment_id: The segment to update (1-9)
* batt_mode: Battery Mode for the segment: 0=Load First(Self-Consumption), 1=Battery First, 2=Grid First
* start_time: timedate object with start time of segment with format HH:MM
* end_time: timedate object with end time of segment with format HH:MM
* enabled: time segment enabled, boolean: True (Enabled), False (Disabled)

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.

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.

## Noah Settings
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
Expand Down
110 changes: 110 additions & 0 deletions examples/tlx_example.py
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)
127 changes: 127 additions & 0 deletions examples/tlx_example_dashboard.py
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}%')
Loading

0 comments on commit ecb7784

Please sign in to comment.