Skip to content

Commit

Permalink
Add Support for Growatt NOAH Balcony Systems (#84)
Browse files Browse the repository at this point in the history
Co-authored-by: Niklas Quenter <[email protected]>
  • Loading branch information
Rappelkiste98 and nquenter authored Aug 9, 2024
1 parent e87eaf7 commit 75b6b73
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 1 deletion.
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,20 @@ Any methods that may be useful.

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

`api.noah_info(serial_number)` Get all information for the specified noah device e.g. configured Operation Modes, configured Battery Management charging upper & lower limit, configured System Default Output Power, Firmware Version

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

`api.update_noah_settings(serial_number, setting_type, parameters)` Applies the provided parameters (dictionary or array) for the specified setting on the specified noah device; see 'Noah settings' below for more information

### Variables

Some variables you may want to set.
Expand Down Expand Up @@ -186,6 +194,41 @@ Known working settings & parameters are as follows (all parameter values are str

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.

## 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
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):
* **Change "System Default Output Power"**
* function: `api.update_noah_settings`
* setting type: `default_power`
* params:
* `param1`: System default output power in watt
* **Change "Battery Management"**
* function: `api.update_noah_settings`
* setting type: `charging_soc`
* params:
* `param1`: Charge upper limit in %
* `param2`: Charge lower limit in %
* **Change "Operation Mode" Time Segment**
* function: `api.update_noah_settings`
* setting type: `time_segment` key from `api.noah_info(serial_number)`, for new `time_segment` count the ending number up
* params:
* `param1`: Workingmode (0 = Load First, 1 = Battery First)
* `param2`: Start time - Hour e.g. "01" (1am)
* `param3`: Start time - Minute e.g. "00" (0 minutes)
* `param4`: End time - Hour e.g. "02" (2am)
* `param5`: End time - Minute e.g. "00" (0 minutes)
* `param6`: Output power in watt (For Workingmode "Battery First" always "0")
* `param7`: Enabled/Disabled (0 = Disabled, 1 = Enabled)
* **Change "Currency"**
* function: `api.update_noah_settings`
* setting type: `updatePlantMoney`
* params:
* `param1`: Plant Id
* `param2`: Cost per kWh e.g. "0.22"
* `param3`: Unit value from `api.noah_info(serial_number)` - `unitList`

## 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.
Expand Down
80 changes: 80 additions & 0 deletions examples/noah_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import growattServer
import datetime
import getpass
import pprint

"""
This is a very trivial script that logs into a user's account and prints out useful data for a "NOAH" system.
This has been tested against my personal system (NOAH2000) which is a 2kW Balcony Storage 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)

"""
A really hacky function to allow me to print out things with an indent in-front
"""
def indent_print(to_output, indent):
indent_string = ""
for x in range(indent):
indent_string += " "
print(indent_string + to_output)

#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'])
#pp.pprint(plant_list)

print("***Totals for all plants***")
pp.pprint(plant_list['totalData'])
print("")

print("***List of plants***")
for plant in plant_list['data']:
indent_print("ID: %s, Name: %s"%(plant['plantId'], plant['plantName']), 2)
print("")

for plant in plant_list['data']:
plant_id = plant['plantId']
plant_name = plant['plantName']
plant_info=api.plant_info(plant_id)
#pp.pprint(plant_info)
print("***Info for Plant %s - %s***"%(plant_id, plant_name))
#There are more values in plant_info, but these are some of the useful/interesting ones
indent_print("CO2 Reducion: %s"%(plant_info['Co2Reduction']),2)
indent_print("Nominal Power (w): %s"%(plant_info['nominal_Power']),2)
indent_print("Solar Energy Today (kw): %s"%(plant_info['todayEnergy']),2)
indent_print("Solar Energy Total (kw): %s"%(plant_info['totalEnergy']),2)
print("")
indent_print("Devices in plant:",2)
for device in plant_info['deviceList']:
device_sn = device['deviceSn']
device_type = device['deviceType']
indent_print("- Device - SN: %s, Type: %s"%(device_sn, device_type),4)

is_noah = api.is_plant_noah_system(plant['plantId'])
if is_noah['result'] == 1 and (is_noah['obj']['isPlantNoahSystem'] or is_noah['obj']['isPlantHaveNoah']):
device_sn = is_noah['obj']['deviceSn']
indent_print("**NOAH - SN: %s**"%(device_sn),2)

noah_system = api.noah_system_status(is_noah['obj']['deviceSn'])
pp.pprint(noah_system['obj'])
print("")

noah_infos = api.noah_info(is_noah['obj']['deviceSn'])
pp.pprint(noah_infos['obj']['noah'])
print("")
indent_print("Remaining battery (" + "%" + "): %s"%(noah_system['obj']['soc']),2)
indent_print("Solar Power (w): %s"%(noah_system['obj']['ppv']),2)
indent_print("Charge Power (w): %s"%(noah_system['obj']['chargePower']),2)
indent_print("Discharge Power (w): %s"%(noah_system['obj']['disChargePower']),2)
indent_print("Output Power (w): %s"%(noah_system['obj']['pac']),2)
138 changes: 137 additions & 1 deletion growattServer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ def plant_info(self, plant_id):
Get basic plant information with device list.
"""
response = self.session.get(self.get_url('newTwoPlantAPI.do'), params={
'op': 'getAllDeviceList',
'op': 'getAllDeviceListTwo',
'plantId': plant_id,
'pageNum': 1,
'pageSize': 1
Expand All @@ -547,6 +547,109 @@ def get_plant_settings(self, plant_id):
})
data = json.loads(response.content.decode('utf-8'))
return data

def is_plant_noah_system(self, plant_id):
"""
Returns a dictionary containing if noah devices are configured for the specified plant
Keyword arguments:
plant_id -- The id of the plant you want the noah devices of (str)
Returns
'msg'
'result' -- True or False
'obj' -- An Object containing if noah devices are configured
'isPlantNoahSystem' -- Is the specified plant a noah system (True or False)
'plantId' -- The ID of the plant
'isPlantHaveNoah' -- Are noah devices configured in the specified plant (True or False)
'deviceSn' -- Serial number of the configured noah device
'plantName' -- Friendly name of the plant
"""
response = self.session.post(self.get_url('noahDeviceApi/noah/isPlantNoahSystem'), data={
'plantId': plant_id
})
data = json.loads(response.content.decode('utf-8'))
return data

def noah_system_status(self, serial_number):
"""
Returns a dictionary containing the status for the specified Noah Device
Keyword arguments:
serial_number -- The Serial number of the noah device you want the status of (str)
Returns
'msg'
'result' -- True or False
'obj' -- An Object containing the noah device status
'chargePower' -- Battery charging rate in watt e.g. '200Watt'
'workMode' -- Workingmode of the battery (0 = Load First, 1 = Battery First)
'soc' -- Statement of charge (remaining battery %)
'associatedInvSn' -- ???
'batteryNum' -- Numbers of batterys
'profitToday' -- Today generated profit through noah device
'plantId' -- The ID of the plant
'disChargePower' -- Battery discharging rate in watt e.g. '200Watt'
'eacTotal' -- Total energy exported to the grid in kWh e.g. '20.5kWh'
'eacToday' -- Today energy exported to the grid in kWh e.g. '20.5kWh'
'pac' -- Export to grid rate in watt e.g. '200Watt'
'ppv' -- Solar generation in watt e.g. '200Watt'
'alias' -- Friendly name of the noah device
'profitTotal' -- Total generated profit through noah device
'moneyUnit' -- Unit of currency e.g. '€'
'status' -- Is the noah device online (True or False)
"""
response = self.session.post(self.get_url('noahDeviceApi/noah/getSystemStatus'), data={
'deviceSn': serial_number
})
data = json.loads(response.content.decode('utf-8'))
return data

def noah_info(self, serial_number):
"""
Returns a dictionary containing the informations for the specified Noah Device
Keyword arguments:
serial_number -- The Serial number of the noah device you want the informations of (str)
Returns
'msg'
'result' -- True or False
'obj' -- An Object containing the noah device informations
'neoList' -- A List containing Objects
'unitList' -- A Object containing currency units e.g. "Euro": "euro", "DOLLAR": "dollar"
'noah' -- A Object containing the folowing
'time_segment' -- A List containing Objects with configured "Operation Mode"
NOTE: The keys are generated numerical, the values are generated with folowing syntax "[workingmode (0 = Load First, 1 = Battery First)]_[starttime]_[endtime]_[output power]"
'time_segment': {
'time_segment1': "0_0:0_8:0_150", ([Load First]_[00:00]_[08:00]_[150 watt])
'time_segment2': "1_8:0_18:0_0", ([Battery First]_[08:00]_[18:00]_[0 watt])
....
}
'batSns' -- A List containing all battery Serial Numbers
'associatedInvSn' -- ???
'plantId' -- The ID of the plant
'chargingSocHighLimit' -- Configured "Battery Management" charging upper limit
'chargingSocLowLimit' -- Configured "Battery Management" charging lower limit
'defaultPower' -- Configured "System Default Output Power"
'version' -- The Firmware Version of the noah device
'deviceSn' -- The Serial number of the noah device
'formulaMoney' -- Configured "Select Currency" energy cost per kWh e.g. '0.22'
'alias' -- Friendly name of the noah device
'model' -- Model Name of the noah device
'plantName' -- Friendly name of the plant
'tempType' -- ???
'moneyUnitText' -- Configured "Select Currency" (Value from the unitList) e.G. "euro"
'plantList' -- A List containing Objects containing the folowing
'plantId' -- The ID of the plant
'plantImgName' -- Friendly name of the plant Image
'plantName' -- Friendly name of the plant
"""
response = self.session.post(self.get_url('noahDeviceApi/noah/getNoahInfoBySn'), data={
'deviceSn': serial_number
})
data = json.loads(response.content.decode('utf-8'))
return data

def update_plant_settings(self, plant_id, changed_settings, current_settings = None):
"""
Expand Down Expand Up @@ -669,3 +772,36 @@ def update_ac_inverter_setting(self, serial_number, setting_type, parameters):
}
return self.update_inverter_setting(serial_number, setting_type,
default_parameters, parameters)

def update_noah_settings(self, serial_number, setting_type, parameters):
"""
Applies settings for specified noah device based on serial number
See README for known working settings
Arguments:
serial_number -- Serial number (device_sn) of the noah (str)
setting_type -- Setting to be configured (str)
parameters -- Parameters to be sent to the system (dict or list of str)
(array which will be converted to a dictionary)
Returns:
JSON response from the server whether the configuration was successful
"""
default_parameters = {
'serialNum': serial_number,
'type': setting_type
}
settings_parameters = parameters

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

settings_parameters = {**default_parameters, **settings_parameters}

response = self.session.post(self.get_url('noahDeviceApi/noah/set'),
data=settings_parameters)
data = json.loads(response.content.decode('utf-8'))
return data

0 comments on commit 75b6b73

Please sign in to comment.