Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c6703ba
Initial commit adding common methods for mix and min
GraemeDBlue Sep 24, 2025
6e487d6
Try tio simplify ,ethods and provide backwards compatible method names
GraemeDBlue Sep 24, 2025
8a07ba6
normalise method names
GraemeDBlue Sep 24, 2025
cf7d7b8
add Dashboard example for Mix V1
GraemeDBlue Sep 24, 2025
aa01035
Fixes for dashboard
GraemeDBlue Sep 24, 2025
1d1e8e2
Reworking
GraemeDBlue Sep 25, 2025
76b4548
added docs
GraemeDBlue Sep 29, 2025
52661fd
add backwards compatible methods for min_*
GraemeDBlue Sep 29, 2025
ce9afa5
uncomment code
GraemeDBlue Sep 29, 2025
21f9581
Fixing up example
GraemeDBlue Sep 29, 2025
fd98a51
fix example dashboard
GraemeDBlue Sep 29, 2025
7d4db09
debugging
GraemeDBlue Sep 30, 2025
837c78c
minor tweaks
GraemeDBlue Oct 2, 2025
f042b63
Code quality
GraemeDBlue Oct 2, 2025
0c03dfc
fixes
GraemeDBlue Oct 2, 2025
e4241e9
fix formatting and code quality
GraemeDBlue Oct 2, 2025
3f3790d
Merge pull request #1 from GraemeDBlue/cleanup
GraemeDBlue Oct 3, 2025
9baba05
revert changes in min_* example and add abckwards compatible wrapper …
GraemeDBlue Oct 10, 2025
f58d8b1
add missing renamed file for v1 example
GraemeDBlue Oct 10, 2025
48a8fc6
Merge pull request #2 from GraemeDBlue/cleanup
GraemeDBlue Oct 10, 2025
ae1f36e
Added all constants
GraemeDBlue Oct 13, 2025
4314600
Dynamically sum pev based on count of devices
GraemeDBlue Oct 13, 2025
8f75a8f
Remove white space
GraemeDBlue Oct 13, 2025
b32fd8d
Merge pull request #3 from GraemeDBlue/cleanup
GraemeDBlue Oct 13, 2025
c393caf
Fix max to 4 for sum
GraemeDBlue Oct 13, 2025
b90943c
Merge pull request #4 from GraemeDBlue/cleanup
GraemeDBlue Oct 13, 2025
5c75e6f
add tou
GraemeDBlue Oct 29, 2025
08668a3
New v1 example files
GraemeDBlue Oct 29, 2025
aae9343
Fix comments out code etc
GraemeDBlue Oct 29, 2025
0006892
Fix commented code
GraemeDBlue Oct 29, 2025
0125ebd
Merge pull request #5 from GraemeDBlue/read_tou
GraemeDBlue Oct 29, 2025
11e28ac
Add Write commands via interfaces for the input params
GraemeDBlue Oct 30, 2025
97b3e93
Add examples for setting charge time for SPH_MIX and MIN_TLX
GraemeDBlue Oct 30, 2025
b1429e8
add common methods
GraemeDBlue Oct 31, 2025
68dda25
fix device type check
GraemeDBlue Oct 31, 2025
1bba0a0
Merge pull request #7 from GraemeDBlue/write_values
GraemeDBlue Nov 3, 2025
33b84f8
Add additional params
GraemeDBlue Nov 6, 2025
4dc88d1
Merge pull request #8 from GraemeDBlue/write_values
GraemeDBlue Nov 6, 2025
a75df72
fixes spaces
GraemeDBlue Nov 6, 2025
68a6317
split tlx and sph read time segments logic
GraemeDBlue Nov 6, 2025
611e983
Add debug is .env variable if .env var is set
GraemeDBlue Nov 6, 2025
b1cd756
remove dead code
GraemeDBlue Nov 6, 2025
dc9648f
Merge pull request #9 from GraemeDBlue/write_values
GraemeDBlue Nov 6, 2025
7d183d3
Fix code quality
GraemeDBlue Nov 7, 2025
4ef89a5
Merge pull request #10 from GraemeDBlue/write_values
GraemeDBlue Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST
*.json

# Symlink
examples/growattServer
25 changes: 25 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# The contents of this file is based on https://github.com/home-assistant/core/blob/dev/pyproject.toml

target-version = "py313"

[lint]
select = [
"ALL",
]

ignore = [
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
"D203", # no-blank-line-before-class (incompatible with formatter)
"D212", # multi-line-summary-first-line (incompatible with formatter)
"COM812", # incompatible with formatter
"ISC001", # incompatible with formatter
]

[lint.flake8-pytest-style]
fixture-parentheses = false

[lint.pyupgrade]
keep-runtime-typing = true

[lint.mccabe]
max-complexity = 25
25 changes: 24 additions & 1 deletion docs/openapiv1/min_tlx_settings.md

Large diffs are not rendered by default.

124 changes: 124 additions & 0 deletions docs/openapiv1/mix_sph_settings.md

Large diffs are not rendered by default.

75 changes: 48 additions & 27 deletions examples/min_example.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import growattServer
from . import growattServer
import datetime
import json
import requests
import os

"""
# Example script controlling a MID/TLX Growatt (MID-30KTL3-XH + APX battery) system using the public growatt API
Expand All @@ -27,60 +28,80 @@
devices = api.device_list(plant_id)

for device in devices['devices']:
if device['type'] == 7: # (MIN/TLX)
print(device)
if device['device_type'] == growattServer.DeviceType.MIN_TLX.value:
inverter_sn = device['device_sn']
print(f"Processing inverter: {inverter_sn}")
device_type = device['device_type']
print(f"Processing {device_type.name} inverter: {inverter_sn}")

# Get device details
inverter_data = api.min_detail(inverter_sn)
inverter_data = api.min_detail(
device_sn=inverter_sn,
)
print("Saving inverter data to inverter_data.json")
with open('inverter_data.json', 'w') as f:
json.dump(inverter_data, f, indent=4, sort_keys=True)

# Get energy data
energy_data = api.min_energy(device_sn=inverter_sn)
energy_data = api.min_energy(
device_sn=inverter_sn,
)
print("Saving energy data to energy_data.json")
with open('energy_data.json', 'w') as f:
json.dump(energy_data, f, indent=4, sort_keys=True)

# Get energy history
energy_history_data = api.min_energy_history(inverter_sn)
energy_history_data = api.min_energy_history(
device_sn=inverter_sn,
)
print("Saving energy history data to energy_history.json")
with open('energy_history.json', 'w') as f:
json.dump(energy_history_data['datas'],
f, indent=4, sort_keys=True)
json.dump(energy_history_data.get('datas', []), f, indent=4, sort_keys=True)

# Get settings
settings_data = api.min_settings(device_sn=inverter_sn)
settings_data = api.min_settings(
device_sn=inverter_sn,
)
print("Saving settings data to settings_data.json")
with open('settings_data.json', 'w') as f:
json.dump(settings_data, f, indent=4, sort_keys=True)

# Read time segments
tou = api.min_read_time_segments(inverter_sn, settings_data)
print(json.dumps(tou, indent=4))
tou = api.read_time_segments(
device_sn=inverter_sn,
device_type=device_type,
settings_data=settings_data
)
print("Time-of-Use Segments:")
with open('tou_data.json', 'w') as f:
json.dump(tou, f, indent=4, sort_keys=True)

# Read discharge power
discharge_power = api.min_read_parameter(
inverter_sn, 'discharge_power')
print("Current discharge power:", discharge_power, "%")
discharge_power = api.common_read_parameter(
device_sn=inverter_sn,
device_type=device_type,
parameter_id='discharge_power'
)
print(f"Current discharge power: {discharge_power}%")

# Settings parameters - Uncomment to test

# Turn on AC charging
# api.min_write_parameter(inverter_sn, 'ac_charge', 1)
# print("AC charging enabled successfully")

# Enable Load First between 00:00 and 11:59 using time segment 1
# api.min_write_time_segment(
# device_sn=inverter_sn,
# segment_id=1,
# batt_mode=growattServer.BATT_MODE_BATTERY_FIRST,
# start_time=datetime.time(0, 0),
# end_time=datetime.time(00, 59),
# enabled=True
# )
# print("Time segment updated successfully")
# api.min_write_parameter(inverter_sn, 'ac_charge', 1)
# print("AC charging enabled successfully")
# # Enable Load First between 00:00 and 11:59 using time segment 1
# params = {
# 'segment_id': 1,
# 'start_time': datetime.time(0, 0),
# 'end_time': datetime.time(00, 59),
# 'enabled': True
# }
# api.min_write_time_segment(
# device_sn=inverter_sn,
# params=params
# )

# print("Time segment updated successfully")


except growattServer.GrowattV1ApiError as e:
Expand Down
89 changes: 58 additions & 31 deletions examples/min_example_dashboard.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,32 @@
import growattServer
from . import growattServer
import json
import requests
import datetime
import os


def safe_float(val, default=0.0):
try:
# If already a float, return as is
if isinstance(val, float):
return val
# If it's an int, convert to float
if isinstance(val, int):
return float(val)
# If it's a string, try to parse
if isinstance(val, str):
# Remove any commas, spaces, etc.
val = val.replace(',', '').strip()
return float(val)
# If it's a type that can be cast to float (e.g., numpy.float64)
return float(val)
except (TypeError, ValueError, KeyError, AttributeError):
return default


"""
Example script fetching key power and today+total energy metrics from a Growatt MID-30KTL3-XH (TLX) + APX battery hybrid system
using the V1 API with token-based authentication.
# Example script controlling a MIX/SPH Growatt (SPH3~6k TL BL UP + battery) system using the public growatt API
# You can obtain an API token from the Growatt API documentation or developer portal.
"""

# Get the API token from user input or environment variable
Expand All @@ -17,30 +39,33 @@
# Initialize the API with token
api = growattServer.OpenApiV1(token=api_token)

# Get plant list using V1 API
# Plant info
plants = api.plant_list()
print(f"Plants: Found {plants['count']} plants")
plant_id = plants['plants'][0]['plant_id']
today = datetime.date.today()
devices = api.get_devices(plant_id)

# Get devices in plant
devices = api.device_list(plant_id)

# Iterate over all devices
energy_data = None
for device in devices['devices']:
if device['type'] == 7: # (MIN/TLX)
inverter_sn = device['device_sn']

# Get energy data
energy_data = api.min_energy(device_sn=inverter_sn)
with open('energy_data.json', 'w') as f:
json.dump(energy_data, f, indent=4, sort_keys=True)
for device in devices:
# Works automatically for MIN, MIX, or any future device type!
energy_data = device.energy()
print(f"Energy: {energy_data}")

if energy_data is None:
raise Exception("No MIN_TLX device found to get energy data from.")

# energy data does not contain epvToday for some reason, so we need to calculate it
epv_today = energy_data["epv1Today"] + energy_data["epv2Today"]
# Dynamically calculate epvToday by summing all epvXToday fields
epv_today = sum(
safe_float(energy_data.get(f"epv{i}Today"), 0.0)
for i in range(1, 4) # Assuming a maximum of 4 devices
if f"epv{i}Today" in energy_data
)

solar_production = f'{float(epv_today):.1f}/{float(energy_data["epvTotal"]):.1f}'
solar_production_pv1 = f'{float(energy_data["epv1Today"]):.1f}/{float(energy_data["epv1Total"]):.1f}'
solar_production_pv2 = f'{float(energy_data["epv2Today"]):.1f}/{float(energy_data["epv2Total"]):.1f}'
solar_production = f'{safe_float(epv_today):.1f}/{safe_float(energy_data.get("epvTotal")):.1f}'
solar_production_pv1 = f'{safe_float(energy_data.get("epv1Today")):.1f}/{safe_float(energy_data.get("epv1Total")):.1f}'
solar_production_pv2 = f'{safe_float(energy_data.get("epv2Today")):.1f}/{safe_float(energy_data.get("epv2Total")):.1f}'
energy_output = f'{float(energy_data["eacToday"]):.1f}/{float(energy_data["eacTotal"]):.1f}'
system_production = f'{float(energy_data["esystemToday"]):.1f}/{float(energy_data["esystemTotal"]):.1f}'
battery_charged = f'{float(energy_data["echargeToday"]):.1f}/{float(energy_data["echargeTotal"]):.1f}'
Expand Down Expand Up @@ -68,22 +93,22 @@
print(f'Export to grid {exported_to_grid:>22}')

print("\nPower overview (Watts)")
print(f'AC Power {float(energy_data["pac"]):>22.1f}')
print(f'Self power {float(energy_data["pself"]):>22.1f}')
print(f'AC Power {float(energy_data["pac"]):>22.1f}')
print(f'Self power {float(energy_data["pself"]):>22.1f}')
print(
f'Export power {float(energy_data["pacToGridTotal"]):>22.1f}')
f'Export power {float(energy_data["pacToGridTotal"]):>22.1f}')
print(
f'Import power {float(energy_data["pacToUserTotal"]):>22.1f}')
f'Import power {float(energy_data["pacToUserTotal"]):>22.1f}')
print(
f'Local load power {float(energy_data["pacToLocalLoad"]):>22.1f}')
print(f'PV power {float(energy_data["ppv"]):>22.1f}')
print(f'PV #1 power {float(energy_data["ppv1"]):>22.1f}')
print(f'PV #2 power {float(energy_data["ppv2"]):>22.1f}')
f'Local load power {float(energy_data["pacToLocalLoad"]):>22.1f}')
print(f'PV power {float(energy_data["ppv"]):>22.1f}')
print(f'PV #1 power {float(energy_data["ppv1"]):>22.1f}')
print(f'PV #2 power {float(energy_data["ppv2"]):>22.1f}')
print(
f'Battery charge power {float(energy_data["bdc1ChargePower"]):>22.1f}')
f'Battery charge power {float(energy_data["bdc1ChargePower"]):>22.1f}')
print(
f'Battery discharge power {float(energy_data["bdc1DischargePower"]):>22.1f}')
print(f'Battery SOC {int(energy_data["bdc1Soc"]):>21}%')
f'Battery discharge power {float(energy_data["bdc1DischargePower"]):>22.1f}')
print(f'Battery SOC {int(energy_data["bdc1Soc"]):>21}%')

except growattServer.GrowattV1ApiError as e:
print(f"API Error: {e} (Code: {e.error_code}, Message: {e.error_msg})")
Expand All @@ -92,4 +117,6 @@
except requests.exceptions.RequestException as e:
print(f"Network Error: {e}")
except Exception as e:
import traceback
print(f"Unexpected error: {e}")
traceback.print_exc()
16 changes: 15 additions & 1 deletion examples/mix_example.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import growattServer
from . import growattServer
import datetime
import getpass
import pprint
import json

"""
This is a very trivial script that logs into a user's account and prints out useful data for a "Mix" system (Hybrid).
Expand Down Expand Up @@ -76,7 +77,16 @@ def indent_print(to_output, indent):
#These two API calls return lots of duplicated information, but each also holds unique information as well
mix_info = api.mix_info(device_sn, plant_id)
pp.pprint(mix_info)

print("Saving inverter data to old_inverter_data.json")
with open('old_inverter_data.json', 'w') as f:
json.dump(mix_info, f, indent=4, sort_keys=True)

mix_totals = api.mix_totals(device_sn, plant_id)
print("Saving energy data to old_energy_data.json")
with open('old_energy_data.json', 'w') as f:
json.dump(mix_totals, f, indent=4, sort_keys=True)

#pp.pprint(mix_totals)
indent_print("*TOTAL VALUES*", 4)
indent_print("==Today Totals==", 4)
Expand All @@ -94,6 +104,10 @@ def indent_print(to_output, indent):
print("")

mix_detail = api.mix_detail(device_sn, plant_id)

print("Saving energy data to old_detail_data.json")
with open('old_detail_data.json', 'w') as f:
json.dump(mix_detail, f, indent=4, sort_keys=True)
#pp.pprint(mix_detail)

#Some of the 'totals' values that are returned by this function do not align to what we would expect, however the graph data always seems to be accurate.
Expand Down
Loading