Skip to content
Open
Show file tree
Hide file tree
Changes from 17 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
65 changes: 65 additions & 0 deletions docs/openapiv1/mix_sph_settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# MIX/SPH Inverter Settings

This is part of the [OpenAPI V1 doc](../openapiv1.md).

For MIX/SPH systems, the public V1 API provides a way to read and write inverter settings:

* **Device Settings**
* function: `api.device_settings`
* parameters:
* `device_sn`: The device serial number
* `device_type`: Use `DeviceType.MIX_SPH` for MIX/SPH inverters

* **Read Parameter**
* function: `api.read_parameter`
* parameters:
* `device_sn`: The device serial number
* `device_type`: Use `DeviceType.MIX_SPH` for MIX/SPH inverters
* `parameter_id`: Parameter ID to read (e.g., "ac_charge", "discharge_power")

* **Time Segments**
* function: `api.read_time_segments`
* parameters:
* `device_sn`: The device serial number
* `device_type`: Use `DeviceType.MIX_SPH` for MIX/SPH inverters
* `settings_data`: Optional settings data to avoid redundant API calls

Returns:
```python
[
{
'segment_id': int, # Segment number (1-9)
'batt_mode': int, # 0=Load First, 1=Battery First, 2=Grid First
'mode_name': str, # String representation of the mode
'start_time': str, # Start time in format "HH:MM"
'end_time': str, # End time in format "HH:MM"
'enabled': bool # Whether the segment is enabled
},
# ... (up to 9 segments)
]
```

## Common Usage Examples

### Reading Device Settings
```python
from growattServer import OpenApiV1, DeviceType

api = OpenApiV1(token="your_api_token")
settings = api.device_settings("DEVICE_SN", DeviceType.MIX_SPH)
```

### Reading Parameters
```python
# Read a specific parameter
value = api.read_parameter("DEVICE_SN", DeviceType.MIX_SPH, "ac_charge")
```

### Reading Time Segments
```python
# Option 1: Single call
segments = api.read_time_segments("DEVICE_SN", DeviceType.MIX_SPH)

# Option 2: Reuse settings data to avoid multiple API calls
settings = api.device_settings("DEVICE_SN", DeviceType.MIX_SPH)
segments = api.read_time_segments("DEVICE_SN", DeviceType.MIX_SPH, settings)
51 changes: 37 additions & 14 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,43 +28,65 @@
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:
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.device_details(
device_sn=inverter_sn,
device_type=device_type
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passing device_type?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let me check , but I believe I have 3 different ways to call the same methods , device_details() is the base method with sn and device type https://github.com/indykoning/PyPi_GrowattServer/pull/125/files#diff-12865f9986e5f28194397aae0efed2b7abc9137d18c0a09197682e6c7e65cd01R484-R511, the details() https://github.com/indykoning/PyPi_GrowattServer/pull/125/files#diff-12865f9986e5f28194397aae0efed2b7abc9137d18c0a09197682e6c7e65cd01R1225-R1227 where the sn and device type are set on the object, so not needed as params , I will roll back my changes in examples/min_example.py to use a backwards compatible min_detail(sn) which will just call device_details(sn, device_type=7 )

)
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.device_energy(
device_sn=inverter_sn,
device_type=device_type
)
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.device_energy_history(
device_sn=inverter_sn,
device_type=device_type
)
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.device_settings(
device_sn=inverter_sn,
device_type=device_type
)
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

Expand Down
54 changes: 38 additions & 16 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,23 +39,21 @@
# 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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great to add this to the library. But I have up to 4 individual PVs, so please use something more dynamic, taking into account the sum of up to 4 if present.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johanzander are they all of the same type or do you have a mix? that might be a interesting scenario

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a sum

Expand Down Expand Up @@ -92,4 +112,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
51 changes: 51 additions & 0 deletions examples/mix_v1_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from . import growattServer
import datetime
import json
import requests
import os

"""
# 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
api_token = os.environ.get("GROWATT_API_TOKEN") or input("Enter your Growatt API token: ")

# test token from official API docs https://www.showdoc.com.cn/262556420217021/1494053950115877
# api_token = "6eb6f069523055a339d71e5b1f6c88cc" # gitleaks:allow

try:
# Initialize the API with token instead of using login
api = growattServer.OpenApiV1(token=api_token)

# 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)

for device in devices:
# Works automatically for MIN, MIX, or any future device type!
device_type = device.device_type
device_sn = device.device_sn
print(f"Device: {device_type} SN: {device_sn}")
details = device.details()
energy = device.energy()
settings = device.settings()
history = device.energy_history(start_date=today)

print(f"Details: {details}")
print(f"Energy: {energy}")
print(f"Settings: {settings}")
print(f"History: {history}")

except growattServer.GrowattV1ApiError as e:
print(f"API Error: {e} (Code: {e.error_code}, Message: {e.error_msg})")
except growattServer.GrowattParameterError as e:
print(f"Parameter Error: {e}")
except requests.exceptions.RequestException as e:
print(f"Network Error: {e}")
except Exception as e:
print(f"Unexpected error: {e}")
Loading