Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 docs/shinephone.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ Any methods that may be useful.
| `api.update_ac_inverter_setting(serial_number, setting_type, parameters)` | serial_number: String, setting_type: String, parameters: Dict/Array | Apply the provided parameters for the specified setting on the specified AC-coupled inverter. see: [details](./shinephone/inverter_settings.md) |
| `api.update_noah_settings(serial_number, setting_type, parameters)` | serial_number: String, setting_type: String, parameters: Dict/Array | Apply the provided parameters for the specified setting on the specified Noah device. see: [details](./shinephone/noah_settings.md) |
| `api.update_classic_inverter_setting(default_parameters, parameters)` | default_parameters: Dict, parameters: Dict/Array | Applies settings for specified system based on serial number. This function is only going to work for classic inverters. |
| `api.classic_inverter_info(device_sn)` | device_sn: String | Get classic inverter information (including on/off status) by scraping the inverter settings page. Returns a dict with fields like `onOff`, `deviceModel`, `status`, `fwVersion`, `sn`, `alias`, etc. Note: this works by parsing HTML, not a JSON API. |

### Variables

Expand Down
46 changes: 42 additions & 4 deletions examples/settings_example_classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
This script demonstrates how to interface with the configuration settings of a plant and its classic inverters.
It uses the `update_classic_inverter_setting` function to apply settings to a classic inverter.
It also demonstrates how to get the current inverter status using `classic_inverter_info`.
"""
pp = pprint.PrettyPrinter(indent=4)

Expand All @@ -20,18 +21,55 @@

plant_list = api.plant_list(login_response["user"]["id"])

# Simple logic to just get the first inverter from the first plant
# Expand this using a for-loop to perform for more systems
plant = plant_list["data"][0] # This is an array - we just take the first - would need a for-loop for more systems
# Display available plants and let user choose
print("\n=== Available Plants ===")
plants = plant_list["data"]
for i, p in enumerate(plants, 1):
print(f"{i}. {p['plantName']} (ID: {p['plantId']})")

while True:
try:
plant_choice = int(input(f"\nSelect a plant (1-{len(plants)}): "))
if 1 <= plant_choice <= len(plants):
plant = plants[plant_choice - 1]
break
else:
print(f"Please enter a number between 1 and {len(plants)}")
except ValueError:
print("Please enter a valid number")

plant_id = plant["plantId"]
plant_name = plant["plantName"]
plant_info = api.plant_info(plant_id)

# Display available devices and let user choose
devices = api.device_list(plant_id)
device = devices[0] # This is an array - we just take the first - would need a for-loop for more systems
print(f"\n=== Available Devices for '{plant_name}' ===")
for i, d in enumerate(devices, 1):
print(f"{i}. {d.get('deviceName', 'N/A')} (SN: {d['deviceSn']}, Type: {d['deviceType']})")

while True:
try:
device_choice = int(input(f"\nSelect a device (1-{len(devices)}): "))
if 1 <= device_choice <= len(devices):
device = devices[device_choice - 1]
break
else:
print(f"Please enter a number between 1 and {len(devices)}")
except ValueError:
print("Please enter a valid number")

device_sn = device["deviceSn"]
device_type = device["deviceType"]

# Get inverter info (includes on/off status, firmware version, model, etc.)
print(f"\nGetting info for inverter: {device_sn}")
inverter_info = api.classic_inverter_info(device_sn)
pp.pprint(inverter_info)

on_off = inverter_info["onOff"]
print(f"Inverter on/off status: {'on' if on_off == '1' else 'off'}")

# Turn inverter on
print(f"Turning on inverter: {device_sn}")

Expand Down
150 changes: 146 additions & 4 deletions growattServer/base_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import datetime
import hashlib
import json
import re
import secrets
import warnings
from enum import IntEnum
Expand Down Expand Up @@ -1066,8 +1068,20 @@ def update_inverter_setting(self, serial_number, setting_type,
Args:
serial_number: Serial number of the inverter.
setting_type: Type of setting to configure.
default_parameters: Default parameter mapping for the request.
parameters: Parameters to send (dict or list).
default_parameters: Default parameter mapping for the request. This
should contain the required keys for the specific endpoint
(commonly keys like ``op``, ``serialNum`` and ``type``).
parameters: Parameters to send. May be either a ``dict`` mapping
parameter names to values, or a ``list`` of values. If a
``list`` is supplied it will be converted to a dictionary of
the form ``{"param1": value1, "param2": value2, ...}``.

Notes:
- The function merges ``default_parameters`` with the provided
``parameters`` and issues a POST request to ``newTcpsetAPI.do``.
- For Mix/AC/other inverter types the caller may wrap this helper
with specific defaults (see ``update_mix_inverter_setting`` and
``update_ac_inverter_setting``).

Returns:
dict: Server response JSON.
Expand Down Expand Up @@ -1234,13 +1248,141 @@ def update_noah_settings(self, serial_number, setting_type, parameters):

return response.json()

def classic_inverter_info(self, device_sn):
"""
Get classic inverter information by scraping the inverter settings page.

The Growatt server does not provide a JSON API for classic inverter status,
so this method fetches the HTML settings page and extracts the inverter
data from an embedded JSON object in the JavaScript.

Args:
device_sn: The serial number of the inverter.

Returns:
dict: A dictionary containing the inverter information.
'innerVersion'
'timezone'
'isBig'
'voltageHighLimit' -- High voltage limit e.g. '263.0'
'wideVoltageEnable'
'reactiveRate'
'modelText'
'haveAfci'
'activeRate' -- Active power rate e.g. '100'
'lost'
'alias' -- Friendly name of the inverter
'datalogSn' -- Serial number of the datalogger
'sysTime' -- System time e.g. '2026-03-01 10:02:45'
'fwVersion' -- Firmware version e.g. 'AH1.0'
'model' -- Model number
'sn' -- Serial number of the inverter
'pvPfCmdMemoryState'
'onOff' -- Inverter on/off status ('0' = off, '1' = on)
'voltageLowLimit' -- Low voltage limit e.g. '186.0'
'plantId' -- The ID of the plant
'pfModel'
'workingFrequencyMin' -- Minimum working frequency e.g. '47.53'
'nominalPower' -- Nominal power in watts e.g. '3600'
'workingFrequencyMax' -- Maximum working frequency e.g. '51.5'
'pf' -- Power factor e.g. '1.0'
'location' -- Location string
'deviceModel' -- Device model name e.g. 'GROWATT 3000MTL-S'
'status' -- Inverter status code
'lastUpdateTime' -- Last data update time

Raises:
GrowattError: If the inverter data cannot be extracted from the response.

"""
response = self.session.get(
self.get_url("commonDeviceSetC/setInverter"),
params={"type": "server", "invSn": device_sn},
)

match = re.search(r"inv=JSON\.parse\('(\{.*?\})'\)", response.text)
if not match:
msg = f"Could not find inverter data in response for device {device_sn}"
raise GrowattError(msg)

try:
return json.loads(match.group(1))
except json.JSONDecodeError as err:
msg = f"Failed to parse inverter data JSON for device {device_sn}"
raise GrowattError(msg) from err

def update_classic_inverter_setting(self, default_parameters, parameters):
"""
Apply classic inverter settings.

Args:
default_parameters: Default parameters dict.
parameters: Parameters to send (dict or list).
default_parameters: Default parameters dict. For classic inverters
this commonly contains keys such as "action": "inverterSet"
and "serialNum": <device_sn> (see examples/settings_example_classic.py).
parameters: Parameters to send. Two common forms are accepted:
- dict: mapping of parameter names to values (e.g. {"param1": "..."}).
- list: positional values which will be converted to
{"param1": v1, "param2": v2, ...}.

Example:
The classic inverter settings example uses a parameter structure
like::

default_parameters = {
"action": "inverterSet",
"serialNum": device_sn,
}

# Example A: toggle PV on/off
parameters = {
"paramId": "pv_on_off",
"command_1": "0001", # 0001 to turn on, 0000 to turn off
"command_2": "",
}

# Example B: set active PV power percentage
parameters = {
"paramId": "pv_active_p_rate",
"command_1": "100", # percentage (0-100)
"command_2": "",
}

# Example C: set reactive PV power percentage
parameters = {
"paramId": "pv_reactive_p_rate",
"command_1": "100", # percentage (0-100)
"command_2": "over", # "over" for Inductive, "under" for Capacitive
}

# Example D: set time
parameters = {
"paramId": "pf_sys_year",
"command_1": "2026-01-01 20:00:00", # Time in "YYYY-MM-DD HH:MM:SS" format
"command_2": "",
}

# Example E: set Powerfactor (PF)
parameters = {
"paramId": "pv_power_factor",
"command_1": "1.0", # PF Value (-0.8 ~ -1/0.8 ~ 1)
"command_2": "",
}

# Example F: Set Grid Voltage High
parameters = {
"paramId": "pv_grid_voltage_high",
"command_1": "263.0", # Voltage in volts
"command_2": "",
}

# Example G: Set Grid Voltage Low
parameters = {
"paramId": "pv_grid_voltage_low",
"command_1": "186.0", # Voltage in volts
"command_2": "",
}

This method will POST the merged parameters to "tcpSet.do".

Returns:
dict: Server JSON response.
Expand Down
Loading