growatt 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,158 +1,160 @@
1
- require File.expand_path('api', __dir__)
2
- require File.expand_path('const', __dir__)
3
- require File.expand_path('error', __dir__)
4
-
5
- module Growatt
6
- # Wrapper for the Growatt REST API
7
- #
8
- # @see no API documentation, reverse engineered
9
- class Client < API
10
-
11
- def initialize(options = {})
12
- super(options)
13
- end
14
-
15
- # access data returned from login
16
- def login_info
17
- @login_data
18
- end
19
- def plant_list(user_id=nil)
20
- user_id = login_info['user']['id'] unless user_id
21
- _plant_list({'userId':user_id})
22
- end
23
- def plant_detail(plant_id,type=Timespan::DAY,date=Time.now)
24
- _plant_detail( {
25
- 'plantId': plant_id,
26
- 'type': type,
27
- 'date': timespan_date(type,date)
28
- })
29
- end
30
- def plant_info(plant_id)
31
- _plant_info({
32
- 'op': 'getAllDeviceList',
33
- 'plantId': plant_id,
34
- 'pageNum': 1,
35
- 'pageSize': 1
36
- })
37
- end
38
- def device_list(plant_id)
39
- plant_info(plant_id).deviceList
40
- end
41
-
42
- def inverter_list(plant_id)
43
- devices = device_list(plant_id)
44
- devices.select { |device| 'inverter'.eql? device.deviceType }
45
- end
46
-
47
- # get data for invertor control
48
- def inverter_control_data(inverter_id)
49
- _inverter_api({
50
- 'op': 'getMaxSetData',
51
- 'serialNum': inverter_id
52
- }).obj.maxSetBean
53
- end
54
-
55
- def update_inverter_setting(serial_number,command,param_id,parameters)
56
- command_parameters = {
57
- 'op': command,
58
- 'serialNum': serial_number,
59
- 'paramId': param_id
60
- }
61
- # repeated values to hash { param1: value1 }
62
-
63
- parameters = parameters.map.with_index { |value, index| ["param#{index + 1}", value] }.to_h if parameters.is_a? Array
64
- self.format = 'x-www-form-urlencoded'
65
- data = JSON.parse(post('newTcpsetAPI.do',command_parameters.merge(parameters)).body)
66
- self.format = :json
67
- data['success']
68
- end
69
-
70
- # turn invertor on of off
71
- def turn_inverter(serial_number,on=true)
72
- onoff = (on ? Inverter::ON : Inverter::OFF )
73
- update_inverter_setting(serial_number,'maxSetApi','max_cmd_on_off',[onoff])
74
- end
75
-
76
- # check if invertor is turned on
77
- def inverter_on?(serial_number)
78
- status = inverter_control_data(serial_number)
79
- Inverter::ON.eql? status.max_cmd_on_off
80
- end
81
-
82
- def export_limitation(serial_number,enable,value=nil)
83
- if Inverter::DISABLE.eql? enable
84
- params = [0]
85
- else
86
- raise ArgumentError, "exportlimitation enable should be Inverter::WATT or Inverter::PERCENTAGE" unless [Inverter::WATT,Inverter::PERCENTAGE].include? enable
87
- raise ArgumentError, "Value should be set for export limitation" unless value
88
- params = [1, value, enable]
89
- end
90
- update_inverter_setting(serial_number,'maxSetApi','backflow_setting',params)
91
- end
92
-
93
- # utility function to get date accordign timespan month/day
94
- def timespan_date(timespan=Timespan::DAY,date=Time.now)
95
- if Timespan::YEAR.eql? timespan
96
- date.strftime("%Y")
97
- elsif Timespan::MONTH.eql? timespan
98
- date.strftime("%Y-%m")
99
- elsif Timespan::DAY.eql? timespan
100
- date.strftime("%Y-%m-%d")
101
- end
102
- end
103
-
104
- #
105
- # functions below are copied from python example code but not sure if these work with MOD9000 inverters
106
- #
107
- def inverter_data(inverter_id,type=Timespan::DAY,date=Time.now)
108
- if Timespan::DAY.eql? type
109
- operator = 'getInverterData_max'
110
- elsif Timespan::MONTH.eql? type
111
- operator = 'getMaxMonthPac'
112
- elsif Timespan::YEAR.eql? type
113
- operator = 'getMaxYearPac'
114
- end
115
- _inverter_api({
116
- 'op': operator,
117
- 'id': inverter_id,
118
- 'type': 1,
119
- 'date': timespan_date(type,date)
120
- })
121
- end
122
- =begin
123
- def inverter_detail(inverter_id)
124
- _inverter_api({
125
- 'op': 'getInverterDetailData',
126
- 'inverterId': inverter_id
127
- })
128
- end
129
- def inverter_detail_two(inverter_id)
130
- _inverter_api({
131
- 'op': 'getInverterDetailData_two',
132
- 'inverterId': inverter_id
133
- })
134
- end
135
- =end
136
- def update_mix_inverter_setting(serial_number, setting_type, parameters)
137
- update_inverter_setting(serial_number,'mixSetApiNew',setting_type,parameters)
138
- end
139
- def update_ac_inverter_setting(serial_number, setting_type, parameters)
140
- update_inverter_setting(serial_number,'spaSetApi',setting_type,parameters)
141
- end
142
-
143
-
144
- private
145
- def self.api_endpoint(method,path)
146
- # all records
147
- self.send(:define_method, method) do |params = {}|
148
- # return data result
149
- get(path,params)
150
- end
151
- end
152
- api_endpoint :_plant_list, 'PlantListAPI.do'
153
- api_endpoint :_plant_detail, 'PlantDetailAPI.do'
154
- api_endpoint :_inverter_api, 'newInverterAPI.do'
155
- api_endpoint :_plant_info, 'newTwoPlantAPI.do'
156
-
157
- end
158
- end
1
+ require File.expand_path('api', __dir__)
2
+ require File.expand_path('const', __dir__)
3
+ require File.expand_path('error', __dir__)
4
+
5
+ module Growatt
6
+ # Wrapper for the Growatt REST API
7
+ #
8
+ # @see no API documentation, reverse engineered
9
+ class Client < API
10
+
11
+ def initialize(options = {})
12
+ super(options)
13
+ end
14
+
15
+ # access data returned from login
16
+ def login_info
17
+ @login_data
18
+ end
19
+ def plant_list(user_id=nil)
20
+ user_id = login_info['user']['id'] unless user_id
21
+ _plant_list({'userId':user_id})
22
+ end
23
+ def plant_detail(plant_id,type=Timespan::DAY,date=Time.now)
24
+ _plant_detail( {
25
+ 'plantId': plant_id,
26
+ 'type': type,
27
+ 'date': timespan_date(type,date)
28
+ })
29
+ end
30
+ def plant_info(plant_id)
31
+ _plant_info({
32
+ 'op': 'getAllDeviceList',
33
+ 'plantId': plant_id,
34
+ 'pageNum': 1,
35
+ 'pageSize': 1
36
+ })
37
+ end
38
+ def device_list(plant_id)
39
+ plant_info(plant_id).deviceList
40
+ end
41
+
42
+ def inverter_list(plant_id)
43
+ devices = device_list(plant_id)
44
+ devices.select { |device| 'inverter'.eql? device.deviceType }
45
+ end
46
+
47
+ # get data for invertor control
48
+ def inverter_control_data(inverter_id)
49
+ _inverter_api({
50
+ 'op': 'getMaxSetData',
51
+ 'serialNum': inverter_id
52
+ }).obj.maxSetBean
53
+ end
54
+
55
+ def update_inverter_setting(serial_number,command,param_id,parameters)
56
+ command_parameters = {
57
+ 'op': command,
58
+ 'serialNum': serial_number,
59
+ 'paramId': param_id
60
+ }
61
+ # repeated values to hash { param1: value1 }
62
+
63
+ parameters = parameters.map.with_index { |value, index| ["param#{index + 1}", value] }.to_h if parameters.is_a? Array
64
+ self.format = 'x-www-form-urlencoded'
65
+ data = JSON.parse(post('newTcpsetAPI.do',command_parameters.merge(parameters)).body)
66
+ self.format = :json
67
+ data['success']
68
+ end
69
+
70
+ # turn invertor on of off
71
+ def turn_inverter(serial_number,on=true)
72
+ onoff = (on ? Inverter::ON : Inverter::OFF )
73
+ update_inverter_setting(serial_number,'maxSetApi','max_cmd_on_off',[onoff])
74
+ end
75
+
76
+ # check if invertor is turned on
77
+ def inverter_on?(serial_number)
78
+ status = inverter_control_data(serial_number)
79
+ Inverter::ON.eql? status.max_cmd_on_off
80
+ end
81
+
82
+ def export_limit(serial_number,enable,value=nil)
83
+ if ExportLimit::DISABLE.eql? enable
84
+ params = [0]
85
+ else
86
+ raise ArgumentError, "exportlimitation enable should be ExportLimit::WATT or ExportLimit::PERCENTAGE" unless [ExportLimit::WATT,ExportLimit::PERCENTAGE].include? enable
87
+ raise ArgumentError, "Value should be set for export limitation" unless value
88
+ params = [1, value, enable]
89
+ end
90
+ update_inverter_setting(serial_number,'maxSetApi','backflow_setting',params)
91
+ end
92
+
93
+ # utility function to get date accordign timespan month/day
94
+ def timespan_date(timespan=Timespan::DAY,date=Time.now)
95
+ if Timespan::YEAR.eql? timespan
96
+ date.strftime("%Y")
97
+ elsif Timespan::MONTH.eql? timespan
98
+ date.strftime("%Y-%m")
99
+ elsif Timespan::DAY.eql? timespan
100
+ date.strftime("%Y-%m-%d")
101
+ end
102
+ end
103
+
104
+ #
105
+ # functions below are copied from python example code but not sure if these work with MOD9000 inverters
106
+ #
107
+ def inverter_data(inverter_id,type=Timespan::DAY,date=Time.now)
108
+ if Timespan::DAY.eql? type
109
+ operator = 'getInverterData_max'
110
+ elsif Timespan::MONTH.eql? type
111
+ operator = 'getMaxMonthPac'
112
+ elsif Timespan::YEAR.eql? type
113
+ operator = 'getMaxYearPac'
114
+ end
115
+ _inverter_api({
116
+ 'op': operator,
117
+ 'id': inverter_id,
118
+ 'type': 1,
119
+ 'date': timespan_date(type,date)
120
+ })
121
+ end
122
+ =begin
123
+ def inverter_detail(inverter_id)
124
+ _inverter_api({
125
+ 'op': 'getInverterDetailData',
126
+ 'inverterId': inverter_id
127
+ })
128
+ end
129
+ def inverter_detail_two(inverter_id)
130
+ _inverter_api({
131
+ 'op': 'getInverterDetailData_two',
132
+ 'inverterId': inverter_id
133
+ })
134
+ end
135
+ =end
136
+ def update_mix_inverter_setting(serial_number, setting_type, parameters)
137
+ update_inverter_setting(serial_number,'mixSetApiNew',setting_type,parameters)
138
+ end
139
+ def update_ac_inverter_setting(serial_number, setting_type, parameters)
140
+ update_inverter_setting(serial_number,'spaSetApi',setting_type,parameters)
141
+ end
142
+
143
+
144
+ private
145
+ def self.api_endpoint(method,path)
146
+ # all records
147
+ self.send(:define_method, method) do |params = {}|
148
+ # return data result
149
+ get(path,params) do |request|
150
+ request.headers['Accept'] = "application/#{format}"
151
+ end
152
+ end
153
+ end
154
+ api_endpoint :_plant_list, 'PlantListAPI.do'
155
+ api_endpoint :_plant_detail, 'PlantDetailAPI.do'
156
+ api_endpoint :_inverter_api, 'newInverterAPI.do'
157
+ api_endpoint :_plant_info, 'newTwoPlantAPI.do'
158
+
159
+ end
160
+ end
@@ -1,26 +1,26 @@
1
- require 'faraday'
2
- require 'faraday-cookie_jar'
3
-
4
- module Growatt
5
- # Create connection and use cookies for authentication tokens
6
- module Connection
7
- def connection
8
- raise ConfigurationError, "Option for endpoint is not defined" unless endpoint
9
-
10
- options = setup_options
11
- @connection ||= Faraday::Connection.new(options) do |connection|
12
- connection.use :cookie_jar
13
-
14
- connection.use Faraday::Response::RaiseError
15
- connection.adapter Faraday.default_adapter
16
- setup_authorization(connection)
17
- setup_headers(connection)
18
- connection.response :json, content_type: /\bjson$/
19
- connection.use Faraday::Request::UrlEncoded
20
-
21
- setup_logger_filtering(connection, logger) if logger
22
- end
23
- end
24
-
25
- end
26
- end
1
+ require 'faraday'
2
+ require 'faraday-cookie_jar'
3
+
4
+ module Growatt
5
+ # Create connection and use cookies for authentication tokens
6
+ module Connection
7
+ def connection
8
+ raise ConfigurationError, "Option for endpoint is not defined" unless endpoint
9
+
10
+ options = setup_options
11
+ @connection ||= Faraday::Connection.new(options) do |connection|
12
+ connection.use :cookie_jar
13
+
14
+ connection.use Faraday::Response::RaiseError
15
+ connection.adapter Faraday.default_adapter
16
+ setup_authorization(connection)
17
+ setup_headers(connection)
18
+ connection.response :json, content_type: /\bjson$/
19
+ connection.use Faraday::Request::UrlEncoded
20
+
21
+ setup_logger_filtering(connection, logger) if logger
22
+ end
23
+ end
24
+
25
+ end
26
+ end
data/lib/growatt/const.rb CHANGED
@@ -1,26 +1,29 @@
1
-
2
- module Growatt
3
- class Enum
4
- def self.enum(array)
5
- array.each do |c|
6
- const_set c,c
7
- end
8
- end
9
- end
10
-
11
- class Timespan
12
- HOUR = 0
13
- DAY = 1
14
- MONTH = 2
15
- YEAR = 3
16
- end
17
-
18
- class Inverter
19
- ON = "0101"
20
- OFF = "0000"
21
- DISABLE = -1
22
- WATT = 1
23
- PERCENTAGE = 0
24
- end
25
-
26
- end
1
+
2
+ module Growatt
3
+ class Enum
4
+ def self.enum(array)
5
+ array.each do |c|
6
+ const_set c,c
7
+ end
8
+ end
9
+ end
10
+
11
+ class Timespan
12
+ HOUR = 0
13
+ DAY = 1
14
+ MONTH = 2
15
+ YEAR = 3
16
+ end
17
+
18
+ class Inverter
19
+ ON = "0101"
20
+ OFF = "0000"
21
+ end
22
+
23
+ class ExportLimit
24
+ DISABLE = -1
25
+ WATT = 1
26
+ PERCENTAGE = 0
27
+ end
28
+
29
+ end
data/lib/growatt/error.rb CHANGED
@@ -1,12 +1,12 @@
1
- module Growatt
2
-
3
- # Generic error to be able to rescue all Hudu errors
4
- class GrowattError < StandardError; end
5
-
6
- # Configuration returns error
7
- class ConfigurationError < GrowattError; end
8
-
9
- # Issue authenticting
10
- class AuthenticationError < GrowattError; end
11
-
12
- end
1
+ module Growatt
2
+
3
+ # Generic error to be able to rescue all Hudu errors
4
+ class GrowattError < StandardError; end
5
+
6
+ # Configuration returns error
7
+ class ConfigurationError < GrowattError; end
8
+
9
+ # Issue authenticting
10
+ class AuthenticationError < GrowattError; end
11
+
12
+ end
@@ -1,26 +1,26 @@
1
- require 'uri'
2
- require 'json'
3
-
4
- module Growatt
5
-
6
- # Defines HTTP request methods
7
- module RequestPagination
8
-
9
- class DataPager < WrAPI::RequestPagination::DefaultPager
10
-
11
- def self.data(body)
12
- # data is at 'back'
13
- if body.is_a? Hash
14
- if body['back']
15
- body['back']
16
- else
17
- body
18
- end
19
- else
20
- # in some cases wrong contenttype is returned instead of app/json
21
- JSON.parse(body)
22
- end
23
- end
24
- end
25
- end
26
- end
1
+ require 'uri'
2
+ require 'json'
3
+
4
+ module Growatt
5
+
6
+ # Defines HTTP request methods
7
+ module RequestPagination
8
+
9
+ class DataPager < WrAPI::RequestPagination::DefaultPager
10
+
11
+ def self.data(body)
12
+ # data is at 'back'
13
+ if body.is_a? Hash
14
+ if body['back']
15
+ body['back']
16
+ else
17
+ body
18
+ end
19
+ else
20
+ # in some cases wrong contenttype is returned instead of app/json
21
+ JSON.parse(body)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,5 +1,5 @@
1
- # frozen_string_literal: true
2
-
3
- module Growatt
4
- VERSION = '0.1.3'
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Growatt
4
+ VERSION = '0.2.0'
5
+ end
data/lib/growatt.rb CHANGED
@@ -1,27 +1,27 @@
1
- require "wrapi"
2
- require File.expand_path('growatt/client', __dir__)
3
- require File.expand_path('growatt/version', __dir__)
4
- require File.expand_path('growatt/pagination', __dir__)
5
-
6
- module Growatt
7
- extend WrAPI::Configuration
8
- extend WrAPI::RespondTo
9
-
10
- DEFAULT_UA = "Ruby Growatt API client #{Growatt::VERSION}".freeze
11
- # https://openapi.growatt.com/ is an option but does not work with my account
12
- DEFAULT_ENDPOINT = 'https://server.growatt.com/'.freeze
13
- DEFAULT_PAGINATION = RequestPagination::DataPager
14
- #
15
- # @return [Growatt::Client]
16
- def self.client(options = {})
17
- Growatt::Client.new({ user_agent: DEFAULT_UA, endpoint: DEFAULT_ENDPOINT, pagination_class: DEFAULT_PAGINATION }.merge(options))
18
- end
19
-
20
- def self.reset
21
- super
22
- self.endpoint = nil
23
- self.user_agent = DEFAULT_UA
24
- self.endpoint = DEFAULT_ENDPOINT
25
- self.pagination_class = DEFAULT_PAGINATION
26
- end
27
- end
1
+ require "wrapi"
2
+ require File.expand_path('growatt/client', __dir__)
3
+ require File.expand_path('growatt/version', __dir__)
4
+ require File.expand_path('growatt/pagination', __dir__)
5
+
6
+ module Growatt
7
+ extend WrAPI::Configuration
8
+ extend WrAPI::RespondTo
9
+
10
+ DEFAULT_UA = "Ruby Growatt API client #{Growatt::VERSION}".freeze
11
+ # https://openapi.growatt.com/ is an option but does not work with my account
12
+ DEFAULT_ENDPOINT = 'https://server.growatt.com/'.freeze
13
+ DEFAULT_PAGINATION = RequestPagination::DataPager
14
+ #
15
+ # @return [Growatt::Client]
16
+ def self.client(options = {})
17
+ Growatt::Client.new({ user_agent: DEFAULT_UA, endpoint: DEFAULT_ENDPOINT, pagination_class: DEFAULT_PAGINATION }.merge(options))
18
+ end
19
+
20
+ def self.reset
21
+ super
22
+ self.endpoint = nil
23
+ self.user_agent = DEFAULT_UA
24
+ self.endpoint = DEFAULT_ENDPOINT
25
+ self.pagination_class = DEFAULT_PAGINATION
26
+ end
27
+ end
data/test/auth_test.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'dotenv'
2
+ require 'logger'
3
+ require 'test_helper'
4
+
5
+
6
+ describe 'authentication' do
7
+
8
+ it '#1 use wrong username/password' do
9
+ assert_raises Growatt::AuthenticationError do
10
+ client = Growatt.client( { username: "xxx"+Growatt.username, password: Growatt.password } )
11
+ client.login
12
+ flunk( 'AuthenticationError expected' )
13
+ end
14
+ end
15
+ it '#2 use correct username/password' do
16
+ client = Growatt.client( { username: Growatt.username, password:Growatt.password } )
17
+ client.login
18
+ end
19
+
20
+ end
@@ -0,0 +1,69 @@
1
+ require 'dotenv'
2
+ require 'logger'
3
+ require 'test_helper'
4
+
5
+ def p m, o
6
+ # puts "#{m}: #{o.inspect}"
7
+ end
8
+
9
+ describe 'client' do
10
+ before do
11
+ @client = Growatt.client
12
+ @client.login
13
+ end
14
+
15
+ it '#1 GET info' do
16
+
17
+ end
18
+
19
+ it "#2 plant/device list" do
20
+ plants = @client.plant_list
21
+
22
+ p "\n* plants", plants
23
+ plant_id = plants.data.first.plantId
24
+ assert plant_id, "plant_id should not be nil"
25
+
26
+ detail = @client.plant_detail(plant_id)
27
+ p "\n* plant detail", detail
28
+ assert value(detail.plantData.plantId).must_equal(plant_id), 'correct plantId/structure'
29
+
30
+ plant_info = @client.plant_info(plant_id)
31
+ p "\n* plant info:", plant_info
32
+
33
+ devices = @client.device_list(plant_id)
34
+ p "\n* devices:", plant_info.deviceList
35
+ inverter = devices.first
36
+ # get data
37
+ data = @client.inverter_data(inverter.deviceSn,Growatt::Timespan::DAY,Time.now)
38
+ assert data, "Get day data by hour"
39
+ data = @client.inverter_data(inverter.deviceSn,Growatt::Timespan::MONTH,Time.now)
40
+ assert data, "Get month data by day"
41
+ data = @client.inverter_data(inverter.deviceSn,Growatt::Timespan::YEAR,Time.now)
42
+ assert data, "Get year data by month"
43
+ puts "\n* Inverter data:"
44
+ puts data.to_json
45
+
46
+ # turn device on
47
+ result = @client.turn_inverter(inverter.deviceSn,true)
48
+ p "\n* turnon result:", result
49
+ is_on = @client.inverter_on?(inverter.deviceSn)
50
+
51
+ assert is_on, "Inverter should be on"
52
+ assert result, "inverter on should be success"
53
+ end
54
+ it "#3 export limitation parameters" do
55
+
56
+ assert_raises ArgumentError do
57
+ @client.export_limit('xxxx', 4)
58
+ flunk( 'ArgumentError expected, invalid limtation' )
59
+ end
60
+ assert_raises ArgumentError do
61
+ @client.export_limit('xxxx', Growatt::ExportLimit::WATT)
62
+ flunk( 'ArgumentError expected, no value given for WATTs' )
63
+ end
64
+ assert_raises ArgumentError do
65
+ @client.export_limit('xxxx',Growatt::ExportLimit::PERCENTAGE)
66
+ flunk( 'ArgumentError expected, no value given for PERCENTAGEs' )
67
+ end
68
+ end
69
+ end