Skip to content

Commit 75b6b73

Browse files
Add Support for Growatt NOAH Balcony Systems (#84)
Co-authored-by: Niklas Quenter <[email protected]>
1 parent e87eaf7 commit 75b6b73

File tree

3 files changed

+260
-1
lines changed

3 files changed

+260
-1
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,20 @@ Any methods that may be useful.
6060

6161
`api.get_plant_settings(plant_id)` Get the current settings for the specified plant
6262

63+
`api.is_plant_noah_system(plant_id)` Get the Information if noah devices are configured for the specified plant
64+
65+
`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.
66+
67+
`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
68+
6369
`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
6470

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

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

75+
`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
76+
6977
### Variables
7078

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

187195
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.
188196

197+
## Noah Settings
198+
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
199+
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.
200+
201+
Known working settings & parameters are as follows (all parameter values are strings):
202+
* **Change "System Default Output Power"**
203+
* function: `api.update_noah_settings`
204+
* setting type: `default_power`
205+
* params:
206+
* `param1`: System default output power in watt
207+
* **Change "Battery Management"**
208+
* function: `api.update_noah_settings`
209+
* setting type: `charging_soc`
210+
* params:
211+
* `param1`: Charge upper limit in %
212+
* `param2`: Charge lower limit in %
213+
* **Change "Operation Mode" Time Segment**
214+
* function: `api.update_noah_settings`
215+
* setting type: `time_segment` key from `api.noah_info(serial_number)`, for new `time_segment` count the ending number up
216+
* params:
217+
* `param1`: Workingmode (0 = Load First, 1 = Battery First)
218+
* `param2`: Start time - Hour e.g. "01" (1am)
219+
* `param3`: Start time - Minute e.g. "00" (0 minutes)
220+
* `param4`: End time - Hour e.g. "02" (2am)
221+
* `param5`: End time - Minute e.g. "00" (0 minutes)
222+
* `param6`: Output power in watt (For Workingmode "Battery First" always "0")
223+
* `param7`: Enabled/Disabled (0 = Disabled, 1 = Enabled)
224+
* **Change "Currency"**
225+
* function: `api.update_noah_settings`
226+
* setting type: `updatePlantMoney`
227+
* params:
228+
* `param1`: Plant Id
229+
* `param2`: Cost per kWh e.g. "0.22"
230+
* `param3`: Unit value from `api.noah_info(serial_number)` - `unitList`
231+
189232
## Settings Discovery
190233

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

examples/noah_example.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import growattServer
2+
import datetime
3+
import getpass
4+
import pprint
5+
6+
"""
7+
This is a very trivial script that logs into a user's account and prints out useful data for a "NOAH" system.
8+
This has been tested against my personal system (NOAH2000) which is a 2kW Balcony Storage system.
9+
10+
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
11+
specific library calls, just uncomment them and they will appear as part of the output.
12+
"""
13+
14+
pp = pprint.PrettyPrinter(indent=4)
15+
16+
"""
17+
A really hacky function to allow me to print out things with an indent in-front
18+
"""
19+
def indent_print(to_output, indent):
20+
indent_string = ""
21+
for x in range(indent):
22+
indent_string += " "
23+
print(indent_string + to_output)
24+
25+
#Prompt user for username
26+
username=input("Enter username:")
27+
28+
#Prompt user to input password
29+
user_pass=getpass.getpass("Enter password:")
30+
31+
api = growattServer.GrowattApi()
32+
login_response = api.login(username, user_pass)
33+
34+
plant_list = api.plant_list(login_response['user']['id'])
35+
#pp.pprint(plant_list)
36+
37+
print("***Totals for all plants***")
38+
pp.pprint(plant_list['totalData'])
39+
print("")
40+
41+
print("***List of plants***")
42+
for plant in plant_list['data']:
43+
indent_print("ID: %s, Name: %s"%(plant['plantId'], plant['plantName']), 2)
44+
print("")
45+
46+
for plant in plant_list['data']:
47+
plant_id = plant['plantId']
48+
plant_name = plant['plantName']
49+
plant_info=api.plant_info(plant_id)
50+
#pp.pprint(plant_info)
51+
print("***Info for Plant %s - %s***"%(plant_id, plant_name))
52+
#There are more values in plant_info, but these are some of the useful/interesting ones
53+
indent_print("CO2 Reducion: %s"%(plant_info['Co2Reduction']),2)
54+
indent_print("Nominal Power (w): %s"%(plant_info['nominal_Power']),2)
55+
indent_print("Solar Energy Today (kw): %s"%(plant_info['todayEnergy']),2)
56+
indent_print("Solar Energy Total (kw): %s"%(plant_info['totalEnergy']),2)
57+
print("")
58+
indent_print("Devices in plant:",2)
59+
for device in plant_info['deviceList']:
60+
device_sn = device['deviceSn']
61+
device_type = device['deviceType']
62+
indent_print("- Device - SN: %s, Type: %s"%(device_sn, device_type),4)
63+
64+
is_noah = api.is_plant_noah_system(plant['plantId'])
65+
if is_noah['result'] == 1 and (is_noah['obj']['isPlantNoahSystem'] or is_noah['obj']['isPlantHaveNoah']):
66+
device_sn = is_noah['obj']['deviceSn']
67+
indent_print("**NOAH - SN: %s**"%(device_sn),2)
68+
69+
noah_system = api.noah_system_status(is_noah['obj']['deviceSn'])
70+
pp.pprint(noah_system['obj'])
71+
print("")
72+
73+
noah_infos = api.noah_info(is_noah['obj']['deviceSn'])
74+
pp.pprint(noah_infos['obj']['noah'])
75+
print("")
76+
indent_print("Remaining battery (" + "%" + "): %s"%(noah_system['obj']['soc']),2)
77+
indent_print("Solar Power (w): %s"%(noah_system['obj']['ppv']),2)
78+
indent_print("Charge Power (w): %s"%(noah_system['obj']['chargePower']),2)
79+
indent_print("Discharge Power (w): %s"%(noah_system['obj']['disChargePower']),2)
80+
indent_print("Output Power (w): %s"%(noah_system['obj']['pac']),2)

growattServer/__init__.py

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -522,7 +522,7 @@ def plant_info(self, plant_id):
522522
Get basic plant information with device list.
523523
"""
524524
response = self.session.get(self.get_url('newTwoPlantAPI.do'), params={
525-
'op': 'getAllDeviceList',
525+
'op': 'getAllDeviceListTwo',
526526
'plantId': plant_id,
527527
'pageNum': 1,
528528
'pageSize': 1
@@ -547,6 +547,109 @@ def get_plant_settings(self, plant_id):
547547
})
548548
data = json.loads(response.content.decode('utf-8'))
549549
return data
550+
551+
def is_plant_noah_system(self, plant_id):
552+
"""
553+
Returns a dictionary containing if noah devices are configured for the specified plant
554+
555+
Keyword arguments:
556+
plant_id -- The id of the plant you want the noah devices of (str)
557+
558+
Returns
559+
'msg'
560+
'result' -- True or False
561+
'obj' -- An Object containing if noah devices are configured
562+
'isPlantNoahSystem' -- Is the specified plant a noah system (True or False)
563+
'plantId' -- The ID of the plant
564+
'isPlantHaveNoah' -- Are noah devices configured in the specified plant (True or False)
565+
'deviceSn' -- Serial number of the configured noah device
566+
'plantName' -- Friendly name of the plant
567+
"""
568+
response = self.session.post(self.get_url('noahDeviceApi/noah/isPlantNoahSystem'), data={
569+
'plantId': plant_id
570+
})
571+
data = json.loads(response.content.decode('utf-8'))
572+
return data
573+
574+
def noah_system_status(self, serial_number):
575+
"""
576+
Returns a dictionary containing the status for the specified Noah Device
577+
578+
Keyword arguments:
579+
serial_number -- The Serial number of the noah device you want the status of (str)
580+
581+
Returns
582+
'msg'
583+
'result' -- True or False
584+
'obj' -- An Object containing the noah device status
585+
'chargePower' -- Battery charging rate in watt e.g. '200Watt'
586+
'workMode' -- Workingmode of the battery (0 = Load First, 1 = Battery First)
587+
'soc' -- Statement of charge (remaining battery %)
588+
'associatedInvSn' -- ???
589+
'batteryNum' -- Numbers of batterys
590+
'profitToday' -- Today generated profit through noah device
591+
'plantId' -- The ID of the plant
592+
'disChargePower' -- Battery discharging rate in watt e.g. '200Watt'
593+
'eacTotal' -- Total energy exported to the grid in kWh e.g. '20.5kWh'
594+
'eacToday' -- Today energy exported to the grid in kWh e.g. '20.5kWh'
595+
'pac' -- Export to grid rate in watt e.g. '200Watt'
596+
'ppv' -- Solar generation in watt e.g. '200Watt'
597+
'alias' -- Friendly name of the noah device
598+
'profitTotal' -- Total generated profit through noah device
599+
'moneyUnit' -- Unit of currency e.g. '€'
600+
'status' -- Is the noah device online (True or False)
601+
"""
602+
response = self.session.post(self.get_url('noahDeviceApi/noah/getSystemStatus'), data={
603+
'deviceSn': serial_number
604+
})
605+
data = json.loads(response.content.decode('utf-8'))
606+
return data
607+
608+
def noah_info(self, serial_number):
609+
"""
610+
Returns a dictionary containing the informations for the specified Noah Device
611+
612+
Keyword arguments:
613+
serial_number -- The Serial number of the noah device you want the informations of (str)
614+
615+
Returns
616+
'msg'
617+
'result' -- True or False
618+
'obj' -- An Object containing the noah device informations
619+
'neoList' -- A List containing Objects
620+
'unitList' -- A Object containing currency units e.g. "Euro": "euro", "DOLLAR": "dollar"
621+
'noah' -- A Object containing the folowing
622+
'time_segment' -- A List containing Objects with configured "Operation Mode"
623+
NOTE: The keys are generated numerical, the values are generated with folowing syntax "[workingmode (0 = Load First, 1 = Battery First)]_[starttime]_[endtime]_[output power]"
624+
'time_segment': {
625+
'time_segment1': "0_0:0_8:0_150", ([Load First]_[00:00]_[08:00]_[150 watt])
626+
'time_segment2': "1_8:0_18:0_0", ([Battery First]_[08:00]_[18:00]_[0 watt])
627+
....
628+
}
629+
'batSns' -- A List containing all battery Serial Numbers
630+
'associatedInvSn' -- ???
631+
'plantId' -- The ID of the plant
632+
'chargingSocHighLimit' -- Configured "Battery Management" charging upper limit
633+
'chargingSocLowLimit' -- Configured "Battery Management" charging lower limit
634+
'defaultPower' -- Configured "System Default Output Power"
635+
'version' -- The Firmware Version of the noah device
636+
'deviceSn' -- The Serial number of the noah device
637+
'formulaMoney' -- Configured "Select Currency" energy cost per kWh e.g. '0.22'
638+
'alias' -- Friendly name of the noah device
639+
'model' -- Model Name of the noah device
640+
'plantName' -- Friendly name of the plant
641+
'tempType' -- ???
642+
'moneyUnitText' -- Configured "Select Currency" (Value from the unitList) e.G. "euro"
643+
'plantList' -- A List containing Objects containing the folowing
644+
'plantId' -- The ID of the plant
645+
'plantImgName' -- Friendly name of the plant Image
646+
'plantName' -- Friendly name of the plant
647+
"""
648+
response = self.session.post(self.get_url('noahDeviceApi/noah/getNoahInfoBySn'), data={
649+
'deviceSn': serial_number
650+
})
651+
data = json.loads(response.content.decode('utf-8'))
652+
return data
550653

551654
def update_plant_settings(self, plant_id, changed_settings, current_settings = None):
552655
"""
@@ -669,3 +772,36 @@ def update_ac_inverter_setting(self, serial_number, setting_type, parameters):
669772
}
670773
return self.update_inverter_setting(serial_number, setting_type,
671774
default_parameters, parameters)
775+
776+
def update_noah_settings(self, serial_number, setting_type, parameters):
777+
"""
778+
Applies settings for specified noah device based on serial number
779+
See README for known working settings
780+
781+
Arguments:
782+
serial_number -- Serial number (device_sn) of the noah (str)
783+
setting_type -- Setting to be configured (str)
784+
parameters -- Parameters to be sent to the system (dict or list of str)
785+
(array which will be converted to a dictionary)
786+
787+
Returns:
788+
JSON response from the server whether the configuration was successful
789+
"""
790+
default_parameters = {
791+
'serialNum': serial_number,
792+
'type': setting_type
793+
}
794+
settings_parameters = parameters
795+
796+
#If we've been passed an array then convert it into a dictionary
797+
if isinstance(parameters, list):
798+
settings_parameters = {}
799+
for index, param in enumerate(parameters, start=1):
800+
settings_parameters['param' + str(index)] = param
801+
802+
settings_parameters = {**default_parameters, **settings_parameters}
803+
804+
response = self.session.post(self.get_url('noahDeviceApi/noah/set'),
805+
data=settings_parameters)
806+
data = json.loads(response.content.decode('utf-8'))
807+
return data

0 commit comments

Comments
 (0)