Skip to content

Commit

Permalink
Added functionality to retrieve and configure settings for Plant & Mi…
Browse files Browse the repository at this point in the history
…x Inverters
  • Loading branch information
muppet3000 authored Dec 3, 2021
1 parent 2406973 commit 3885e69
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,6 @@ share/python-wheels/
.installed.cfg
*.egg
MANIFEST

# Symlink
examples/growattServer
85 changes: 85 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ 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.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_mix_inverter_setting(serial_number, setting_type, parameters)` Applies the provided parameters (dictionary or array) for the specified setting on the specified inverter - See 'Inverter settings' below for more information - Appears to be limited to "mix" systems

### Variables

Some variables you may want to set.
Expand All @@ -67,3 +73,82 @@ Some variables you may want to set.
## Note

This is based on the endpoints used on the mobile app and could be changed without notice.

## Examples

The `examples` directory contains example usage for the library. You are required to have the library installed to use them `pip install growattServer`. However, if you are contributing to the library and want to use the latest version from the git repository, simply create a symlink to the growattServer directory inside the `examples` directory.

## Plant Settings

The plant settings function(s) allow you to re-configure the settings for a specified plant. The following settings are required (and are therefore pre-populated based on the existing values for these settings)
* `plantCoal` - The formula used to calculate equivalent coal usage
* `plantSo2` - The formula used to calculate So2 generation/saving
* `accountName` - The username that the system is assigned to
* `plantID` - The ID of the plant
* `plantFirm` - The 'firm' of the plant (unknown what this relates to - hardcoded to '0')
* `plantCountry` - The Country that the plant resides in
* `plantType` - The 'type' of plant (numerical value - mapped to an Enum)
* `plantIncome` - The formula used to calculate money per kwh
* `plantAddress` - The address of the plant
* `plantTimezone` - The timezone of the plant (relative to UTC)
* `plantLng` - The longitude of the plant's location
* `plantCity` - The city that the plant is located in
* `plantCo2` - The formula used to calculate Co2 saving/reduction
* `plantMoney` - The local currency e.g. gbp
* `plantPower` - The capacity/size of the plant in W e.g. 6400 (6.4kw)
* `plantLat` - The latitude of the plant's location
* `plantDate` - The date that the plant was installed
* `plantName` - The name of the plant

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

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.

Known working settings & parameters are as follows (all parameter values are strings):

* **Time/Date**
* type: `pf_sys_year`
* params:
* `param1`: datetime in format: `YYYY-MM-DD HH:MM:SS`
* **Hybrid inverter AC charge times**
* type: `mix_ac_charge_time_period`
* params:
* `param1`: Charging power % (value between 0 and 100)
* `param2`: Stop charging Statement of Charge % (value between 0 and 100)
* `param3`: Allow AC charging (0 = Disabled, 1 = Enabled)
* `param4`: Schedule 1 - Start time - Hour e.g. "01" (1am)
* `param5`: Schedule 1 - Start time - Minute e.g. "00" (0 minutes)
* `param6`: Schedule 1 - End time - Hour e.g. "02" (2am)
* `param7`: Schedule 1 - End time - Minute e.g. "00" (0 minutes)
* `param8`: Schedule 1 - Enabled/Disabled (0 = Disabled, 1 = Enabled)
* `param9`: Schedule 2 - Start time - Hour e.g. "01" (1am)
* `param10`: Schedule 2 - Start time - Minute e.g. "00" (0 minutes)
* `param11`: Schedule 2 - End time - Hour e.g. "02" (2am)
* `param12`: Schedule 2 - End time - Minute e.g. "00" (0 minutes)
* `param13`: Schedule 2 - Enabled/Disabled (0 = Disabled, 1 = Enabled)
* `param14`: Schedule 3 - Start time - Hour e.g. "01" (1am)
* `param15`: Schedule 3 - Start time - Minute e.g. "00" (0 minutes)
* `param16`: Schedule 3 - End time - Hour e.g. "02" (2am)
* `param17`: Schedule 3 - End time - Minute e.g. "00" (0 minutes)
* `param18`: Schedule 3 - Enabled/Disabled (0 = Disabled, 1 = Enabled)

Note the function `update_mix_inverter_setting` takes 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.

## Settings Discovery

The settings for the Plant and Inverter have been reverse engineered by using the ShinePhone Android App and the NetCapture SSL application together to inspect the API calls that are made by the application and the parameters that are provided with it.

## Disclaimer

The developers & maintainers of this library accept no responsibility for any damage, problems or issues that arise with your Growatt systems as a result of its use.

The library contains functions that allow you to modify the configuration of your plant & inverter which carries the ability to set values outside of normal operating parameters, therefore, settings should only be modified if you understand the consequences.

To the best of our knowledge only the `settings` functions perform modifications to your system and all other operations are read only. Regardless of the operation:

***The library is used entirely at your own risk.***

File renamed without changes.
87 changes: 87 additions & 0 deletions examples/settings_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import growattServer
import datetime
import getpass
import pprint

"""
This is a very trivial script to show how to interface with the configuration settings of a plant and it's inverters
This has been tested against my personal system (muppet3000) which is a hybrid (aka 'mix') inverter system.
Throughout the script there are points where 'pp.pprint' has been commented out. If you wish to see all the data that is returned from those
specific library calls, just uncomment them and they will appear as part of the output.
"""
pp = pprint.PrettyPrinter(indent=4)

#Prompt user for username
username=input("Enter username:")

#Prompt user to input password
user_pass=getpass.getpass("Enter password:")

api = growattServer.GrowattApi()
login_response = api.login(username, user_pass)

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 (see mix_example for more detail)
plant = plant_list['data'][0] #This is an array - we just take the first - would need a for-loop for more systems
plant_id = plant['plantId']
plant_name = plant['plantName']
plant_info=api.plant_info(plant_id)


device = plant_info['deviceList'][0] #This is an array - we just take the first - would need a for-loop for more systems
device_sn = device['deviceSn']
device_type = device['deviceType']


#Get plant settings - This is performed for us inside 'update_plant_settings' but you can get ALL of the settings using this
current_settings = api.get_plant_settings(plant_id)
#pp.pprint(current_settings)



#Change the timezone of the plant
plant_settings_changes = {
'plantTimezone': '0'
}
print("Changing the following plant setting(s):")
pp.pprint(plant_settings_changes)
response = api.update_plant_settings(plant_id, plant_settings_changes)
print(response)
print("")




#Set inverter time
now = datetime.datetime.now()
dt_string = now.strftime("%Y-%m-%d %H:%M:%S")
time_settings={
'param1': dt_string
}
print("Setting inverter time to: %s" %(dt_string))
response = api.update_mix_inverter_setting(device_sn, 'pf_sys_year', time_settings)
print(response)
print("")



#Set inverter schedule (Uses the 'array' method which assumes all parameters are named param1....paramN)
schedule_settings = ["100", #Charging power %
"100", #Stop charging SoC %
"1", #Allow AC charging (1 = Enabled)
"00", "40", #Schedule 1 - Start time
"04", "20", #Schedule 1 - End time
"1", #Schedule 1 - Enabled/Disabled (1 = Enabled)
"00", "00", #Schedule 2 - Start time
"00", "00", #Schedule 2 - End time
"0", #Schedule 2 - Enabled/Disabled (0 = Disabled)
"00", "00", #Schedule 3 - Start time
"00", "00", #Schedule 3 - End time
"0"] #Schedule 3 - Enabled/Disabled (0 = Disabled)
print("Setting the inverter charging schedule to:")
pp.pprint(schedule_settings)
response = api.update_mix_inverter_setting(device_sn, 'mix_ac_charge_time_period', schedule_settings)
print(response)
0 example.py → examples/simple.py
100644 → 100755
File renamed without changes.
95 changes: 95 additions & 0 deletions growattServer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,3 +515,98 @@ def plant_info(self, plant_id):

data = json.loads(response.content.decode('utf-8'))
return data

def get_plant_settings(self, plant_id):
"""
Returns a dictionary containing the settings for the specified plant
Keyword arguments:
plant_id -- The id of the plant you want the settings of
Returns:
A python dictionary containing the settings for the specified plant
"""
response = self.session.get(self.get_url('newPlantAPI.do'), params={
'op': 'getPlant',
'plantId': plant_id
})
data = json.loads(response.content.decode('utf-8'))
return data

def update_plant_settings(self, plant_id, changed_settings, current_settings = None):
"""
Applies settings to the plant e.g. ID, Location, Timezone
See README for all possible settings options
Keyword arguments:
plant_id -- The id of the plant you wish to update the settings for
changed_settings -- A python dictionary containing the settings to be changed and their value
current_settings -- A python dictionary containing the current settings of the plant (use the response from get_plant_settings), if None - fetched for you
Returns:
A response from the server stating whether the configuration was successful or not
"""
#If no existing settings have been provided then get them from the growatt server
if current_settings == None:
current_settings = self.get_plant_settings(plant_id)

#These are the parameters that the form requires, without these an error is thrown. Pre-populate their values with the current values
form_settings = {
'plantCoal': (None, str(current_settings['formulaCoal'])),
'plantSo2': (None, str(current_settings['formulaSo2'])),
'accountName': (None, str(current_settings['userAccount'])),
'plantID': (None, str(current_settings['id'])),
'plantFirm': (None, '0'), #Hardcoded to 0 as I can't work out what value it should have
'plantCountry': (None, str(current_settings['country'])),
'plantType': (None, str(current_settings['plantType'])),
'plantIncome': (None, str(current_settings['formulaMoneyStr'])),
'plantAddress': (None, str(current_settings['plantAddress'])),
'plantTimezone': (None, str(current_settings['timezone'])),
'plantLng': (None, str(current_settings['plant_lng'])),
'plantCity': (None, str(current_settings['city'])),
'plantCo2': (None, str(current_settings['formulaCo2'])),
'plantMoney': (None, str(current_settings['formulaMoneyUnitId'])),
'plantPower': (None, str(current_settings['nominalPower'])),
'plantLat': (None, str(current_settings['plant_lat'])),
'plantDate': (None, str(current_settings['createDateText'])),
'plantName': (None, str(current_settings['plantName'])),
}

#Overwrite the current value of the setting with the new value
for setting, value in changed_settings.items():
form_settings[setting] = (None, str(value))

response = self.session.post(self.get_url('newTwoPlantAPI.do?op=updatePlant'), files = form_settings)
data = json.loads(response.content.decode('utf-8'))
return data

def update_mix_inverter_setting(self, serial_number, setting_type, parameters):
"""
Applies settings for specified system based on serial number
See README for known working settings
Keyword arguments:
serial_number -- The serial number (device_sn) of the inverter
setting_type -- The setting to be configured (list of known working settings below)
parameters -- A python dictionary of the parameters to be sent to the system based on the chosen setting_type, OR a python array which will be converted to a params dictionary
Returns:
A response from the server stating whether the configuration was successful or not
"""
setting_parameters = parameters

#If we've been passed an array then convert it into a dictionary
if isinstance(parameters, list):
setting_parameters = {}
for index, param in enumerate(parameters, start=1):
setting_parameters['param' + str(index)] = param

default_params = {
'op': 'mixSetApiNew',
'serialNum': serial_number,
'type': setting_type
}
settings_params = {**default_params, **setting_parameters}
response = self.session.post(self.get_url('newTcpsetAPI.do'), params=settings_params)
data = json.loads(response.content.decode('utf-8'))
return data

0 comments on commit 3885e69

Please sign in to comment.