axpert_rs232 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: feaf14c95a0bb048162f2de460083f56e1564c9a
4
+ data.tar.gz: 124bfdfe33531983e742cff13ce5883ca8163d9f
5
+ SHA512:
6
+ metadata.gz: 4f69247e57e69cd8bee76208639a0fbd55f94743d85b9282fe7f1a300bbc289e85a6c2ae4859d3e55ab5d2417922a88712bd85465d88ff99b85af690bb53e0a7
7
+ data.tar.gz: cd36a586e15a3f5421abf9260b63bcd5c7d35c0499876bf6b3adcf57212d635792d766d2f8bd1cefc281ff07f6e93465eaac4d6595f19822135302f09e3ed5b7
@@ -0,0 +1,543 @@
1
+ ##
2
+ # A list of known commands
3
+ #
4
+ # Commands implemented as constants
5
+ # Each command includes the actual command to be executed and a parser for the results of the command
6
+ #
7
+ # Commands source: http://forums.aeva.asn.au/uploads/293/HS_MS_MSX_RS232_Protocol_20140822_after_current_upgrade.pdf
8
+ #
9
+ # @author: Johan van der Vyver
10
+ module AxpertCommands
11
+ require 'voltronic_device_operation'
12
+ OP = ::VoltronicDeviceOperation # :nodoc
13
+
14
+ ##
15
+ # Device protocol ID
16
+ #
17
+ # Returns:
18
+ # # An Integer specifying the protocol ID
19
+ # 30 # Example
20
+ PROTOCOL_ID = OP.new(command: 'QPI', parser: lambda { |r| Integer(r.data[3..-1]) })
21
+
22
+ ##
23
+ # Device serial number
24
+ #
25
+ # Returns:
26
+ # # A String specifying the device serial number
27
+ # "XXXXXXXXXXXXXX" # Example
28
+ SERIAL_NUMBER = OP.new(command: 'QID', parser: lambda { |r| r.data[1..-1] })
29
+
30
+ ##
31
+ # Main CPU Firmware version
32
+ #
33
+ # Returns:
34
+ # # A String representing the CPU Firmware as a hexidecimal number
35
+ # "124004.10" # Example
36
+ #
37
+ MAIN_CPU_FIRMWARE = OP.new(command: 'QVFW', parser: lambda do |r|
38
+ r.data[8..-1].split('.').map { |s| s.chars.map { |c| Integer(c, 16).to_s }.flatten.join }.join('.').upcase
39
+ end)
40
+
41
+ ##
42
+ # Other CPU Firmware version
43
+ #
44
+ # Returns:
45
+ # # A String representing the CPU Firmware as a hexidecimal number
46
+ # "124004.10" # Example
47
+ OTHER_CPU_FIRMWARE = OP.new(command: 'QVFW2', parser: lambda do |r|
48
+ r.data[8..-1].split('.').map { |s| s.chars.map { |c| Integer(c, 16).to_s }.flatten.join }.join('.').upcase
49
+ end)
50
+
51
+ ##
52
+ # Device rating information
53
+ #
54
+ # Returns:
55
+ # # A Hash containing device rating information
56
+ # { grid_voltage: 230.5, grid_current: 11.2, .. }
57
+ DEVICE_RATING = OP.new(command: 'QPIRI', parser: lambda do |r|
58
+ r = r.data[1..-1].split(' ')
59
+ { grid_voltage: Float(r[0]),
60
+ grid_current: Float(r[1]),
61
+ output_voltage: Float(r[2]),
62
+ output_frequency: Float(r[3]),
63
+ output_current: Float(r[4]),
64
+ output_va: Integer(r[5]),
65
+ output_watts: Integer(r[6]),
66
+ battery_voltage: Float(r[7]),
67
+ battery_recharge_voltage: Float(r[8]),
68
+ battery_under_voltage: Float(r[9]),
69
+ battery_bulk_charge_voltage: Float(r[10]),
70
+ battery_float_charge_voltage: Float(r[11]),
71
+ battery_type: ::AxpertConstants::BATTERY_TYPE[r[12]],
72
+ maximum_ac_charge_current: Integer(r[13]),
73
+ maximum_charge_current: Integer(r[14]),
74
+ input_voltage_sensitivity: CONS::INPUT_VOLTAGE_SENSITIVITY[r[15]],
75
+ output_source_priority: CONS::OUTPUT_SOURCE_PRIORITY[r[16]],
76
+ charger_source_priority: CONS::CHARGER_SOURCE_PRIORITY[r[17]],
77
+ maximum_parallel_units: Integer(r[18]),
78
+ device_type: CONS::DEVICE_TYPE[r[19]],
79
+ device_topology: CONS::DEVICE_TOPOLOGY[r[20]],
80
+ output_mode: CONS::OUTPUT_MODE[r[21]],
81
+ battery_redischarge_voltage: Float(r[22]),
82
+ pv_parallel_ok_mode: CONS::PV_PARALLEL_OK_MODE[r[23]],
83
+ pv_power_balance_mode: CONS::PV_POWER_BALANCE_MODE[r[24]] }
84
+ end)
85
+
86
+
87
+ ##
88
+ # Device flags
89
+ #
90
+ # Returns:
91
+ # # A Hash containing device flag status as booleans
92
+ # {enable_buzzer: true/false, nable_bypass_to_utility_on_overload: true/false, ... }
93
+ DEVICE_FLAGS = OP.new(command: 'QFLAG', parser: lambda do |r|
94
+ r = r.data[1..-1]
95
+ lookup = Hash.new { |_, k| raise "The device did not return the status of the flag '#{k}'" }
96
+ r.scan(/[E][a-z]*/).first.to_s[1..-1].to_s.chars.each { |e| lookup[e.upcase] = true }
97
+ r.scan(/[D][a-z]*/).first.to_s[1..-1].to_s.chars.each { |d| lookup[d.upcase] = false }
98
+ { enable_buzzer: lookup['A'],
99
+ enable_bypass_to_utility_on_overload: lookup['B'],
100
+ enable_power_saving: lookup['J'],
101
+ enable_lcd_timeout_escape_to_default_page: lookup['K'],
102
+ enable_overload_restart: lookup['U'],
103
+ enable_over_temperature_restart: lookup['V'],
104
+ enable_lcd_backlight: lookup['X'],
105
+ enable_primary_source_interrupt_alarm: lookup['Y'],
106
+ enable_fault_code_recording: lookup['Z'] }
107
+ end)
108
+
109
+ ##
110
+ # The current device status
111
+ #
112
+ # Returns:
113
+ # # A Hash containing device status information
114
+ # { grid_voltage: 230.5, grid_current: 11.2, .. }
115
+ DEVICE_STATUS = OP.new(command: 'QPIGS', parser: lambda do |r|
116
+ r = r.data[1..-1].split(' ')
117
+ status = r[16].chars.map { |c| Boolean(c) }
118
+ { grid_voltage: Float(r[0]),
119
+ grid_frequency: Float(r[1]),
120
+ output_voltage: Float(r[2]),
121
+ output_frequency: Float(r[3]),
122
+ output_va: Integer(r[4]),
123
+ output_watts: Integer(r[5]),
124
+ output_load_percent: Integer(r[6]),
125
+ dc_bus_voltage: Integer(r[7]),
126
+ battery_voltage: Float(r[8]),
127
+ battery_charge_current: Float(r[9]),
128
+ battery_capacity_remaining: Integer(r[10]),
129
+ inverter_heatsink_celsius_temperature: Integer(r[11]),
130
+ pv_battery_input_current: Integer(r[12]),
131
+ pv_input_voltage: Float(r[13]),
132
+ solar_charge_controller_battery_voltage: Float(r[14]),
133
+ battery_discharge_current: Integer(r[15]),
134
+ add_sbu_priority_version: status[0],
135
+ configuration_changed: status[1],
136
+ solar_charge_controller_firmware_changed: status[2],
137
+ load_on: status[3],
138
+ battery_voltage_stable: status[4],
139
+ charger_enabled: status[5],
140
+ charging_from_solar_charge_controller: status[6],
141
+ charging_from_utility: status[7] }
142
+ end)
143
+
144
+ # Device mode
145
+ #
146
+ # Returns:
147
+ # # A symbol denoting the device mode
148
+ # See AxpertConstants::DEVICE_MODE for constants
149
+ DEVICE_MODE = OP.new(command: 'QMOD', parser: lambda { |r| CONS::DEVICE_MODE[r.data[1..-1].upcase] })
150
+
151
+ ##
152
+ # Device warning status messages
153
+ #
154
+ # Returns:
155
+ # # An Array of Hashes
156
+ # # Each Hash has the format { description: String description, level: :none/:fault/:warning }
157
+ # # The level is only :none if a reserved error was returned which may signify an update
158
+ # # of the parser is needed
159
+ # [{ description: 'Bus voltage is too high', level: :fault}, ..]
160
+ DEVICE_WARNING_STATUS = OP.new(command: 'QPIWS', parser: lambda do |r|
161
+ r = r.data[1..-1].chars.map { |s| Boolean(s) }
162
+ parse = lambda { |desc, lvl = :none| { description: desc, level: lvl } }
163
+ errors = []
164
+ errors << parse.yield('Reserved') if r[0]
165
+ errors << parse.yield('Inverter fault', :fault) if r[1]
166
+ errors << parse.yield('Bus voltage is too high', :fault) if r[2]
167
+ errors << parse.yield('Bus voltage is too low', :fault) if r[3]
168
+ errors << parse.yield('Bus soft start failed ', :fault) if r[4]
169
+ errors << parse.yield('LINE_FAIL', :warning) if r[5]
170
+ errors << parse.yield('Output short circuited', :warning) if r[6]
171
+ errors << parse.yield('Inverter voltage too low', :fault) if r[7]
172
+ errors << parse.yield('Output voltage is too high', :fault) if r[8]
173
+ errors << parse.yield('Over temperature', (r[1] ? :fault : :warning)) if r[9]
174
+ errors << parse.yield('Fan is locked', (r[1] ? :fault : :warning)) if r[10]
175
+ errors << parse.yield('Battery voltage is too high', (r[1] ? :fault : :warning)) if r[11]
176
+ errors << parse.yield('Battery voltage is too low', 'Fault') if r[12]
177
+ errors << parse.yield('Reserved') if r[13]
178
+ errors << parse.yield('Battery under shutdown', :warning) if r[14]
179
+ errors << parse.yield('Reserved') if r[15]
180
+ errors << parse.yield('Overload', (r[1] ? :fault : :warning)) if r[16]
181
+ errors << parse.yield('EEPROM fault', :warning) if r[17]
182
+ errors << parse.yield('Inverter over current', :fault) if r[18]
183
+ errors << parse.yield('Inverter soft start failed', :fault) if r[19]
184
+ errors << parse.yield('Self test fail', :fault) if r[20]
185
+ errors << parse.yield('Over voltage on DC output of inverter', :fault) if r[21]
186
+ errors << parse.yield('Battery connection is open', :fault) if r[22]
187
+ errors << parse.yield('Current sensor failed ', :fault) if r[23]
188
+ errors << parse.yield('Battery short', :fault) if r[24]
189
+ errors << parse.yield('Power limit', :warning) if r[25]
190
+ errors << parse.yield('PV voltage high', :warning) if r[26]
191
+ errors << parse.yield('MPPT overload fault', :warning) if r[27]
192
+ errors << parse.yield('MPPT overload warning', :warning) if r[28]
193
+ errors << parse.yield('Battery too low to charge', :warning) if r[29]
194
+ errors << parse.yield('Reserved') if r[30]
195
+ errors << parse.yield('Reserved') if r[31]
196
+ errors
197
+ end)
198
+
199
+ ##
200
+ # The device default settings
201
+ #
202
+ # Returns:
203
+ # # A Hash containing device defaults
204
+ # { grid_voltage: 230.5, grid_current: 11.2, .. }
205
+ DEFAULT_SETTINGS = OP.new(command: 'QDI', parser: lambda do |r|
206
+ r = r.data[1..-1].split(' ')
207
+ { output_voltage: Float(r[0]),
208
+ output_frequency: Float(r[1]),
209
+ maximum_ac_charge_current: Integer(r[2]),
210
+ battery_under_voltage: Float(r[3]),
211
+ battery_float_charge_voltage: Float(r[4]),
212
+ battery_bulk_charge_voltage: Float(r[5]),
213
+ battery_recharge_voltage: Float(r[6]),
214
+ maximum_charge_current: Integer(r[7]),
215
+ input_voltage_sensitivity: CONS::INPUT_VOLTAGE_SENSITIVITY[r[8]],
216
+ output_source_priority: CONS::OUTPUT_SOURCE_PRIORITY[r[9]],
217
+ charger_source_priority: CONS::CHARGER_SOURCE_PRIORITY[r[10]],
218
+ battery_type: ::AxpertConstants::BATTERY_TYPE[r[11]],
219
+ enable_buzzer: !Boolean(r[12]),
220
+ enable_power_saving: Boolean(r[13]),
221
+ enable_overload_restart: Boolean(r[14]),
222
+ enable_over_temperature_restart: Boolean(r[15]),
223
+ enable_lcd_backlight: Boolean(r[16]),
224
+ enable_primary_source_interrupt_alarm: Boolean(r[17]),
225
+ enable_fault_code_recording: Boolean(r[18]),
226
+ enable_bypass_to_utility_on_overload: Boolean(r[19]),
227
+ enable_lcd_timeout_escape_to_default_page: Boolean(r[20]),
228
+ output_mode: CONS::OUTPUT_MODE[r[21]],
229
+ battery_redischarge_voltage: Float(r[22]),
230
+ pv_parallel_ok_mode: CONS::PV_PARALLEL_OK_MODE[r[23]],
231
+ pv_power_balance_mode: CONS::PV_POWER_BALANCE_MODE[r[24]] }
232
+ end)
233
+
234
+ ##
235
+ # All the possible input values for the charge current setting
236
+ #
237
+ # Returns:
238
+ # [10, 20, 30] # => Array of Integers, example given
239
+ ACCEPTED_CHARGE_CURRENT_VALUES = OP.new(command: 'QMCHGCR', parser: lambda do |r|
240
+ r.data[1..-1].split(' ').map { |s| Integer(s) }
241
+ end)
242
+
243
+ ##
244
+ # All the possible input values for the utility charge current setting
245
+ #
246
+ # Returns:
247
+ # [10, 20, 30] # => Array of Integers, example given
248
+ ACCEPTED_UTILITY_CHARGE_CURRENT_VALUES = OP.new(command: 'QMUCHGCR', parser: lambda do |r|
249
+ r.data[1..-1].split(' ').map { |s| Integer(s) }
250
+ end)
251
+
252
+ ##
253
+ # Has device undergone DSP bootstrap
254
+ #
255
+ # Returns:
256
+ # # A Boolean indicating if the device has undergone DSP bootstrap
257
+ # true # Example
258
+ DSP_BOOTSTRAP_STATUS = OP.new(command: 'QBOOT', parser: lambda { |r| Boolean(r.data[1..-1]) })
259
+
260
+ ##
261
+ # Device parallel output mode status
262
+ #
263
+ # Returns:
264
+ # # A symbol denoting the current device parallel output mode
265
+ # See AxpertConstants::OUTPUT_MODE for constants
266
+ OUTPUT_MODE = OP.new(command: 'QOPM', parser: lambda { |r| CONS::OUTPUT_MODE[r.data[2..-1]] })
267
+
268
+ ##
269
+ # Parallel device information
270
+ #
271
+ # Input: (OPTIONAL, default = 0)
272
+ # # Default of 0 seems to work for single unit mode
273
+ # 2 # => parallel_machine_number (Parallel mode only)
274
+ #
275
+ # Returns:
276
+ # # A Hash containing parallel device status information
277
+ # { grid_voltage: 230.5, grid_current: 11.2, .. }
278
+ PARALLEL_DEVICE_STATUS = OP.new(command: lambda { |i = 0| "QPGS#{Integer(i)}" }, parser: lambda do |r|
279
+ r = r.data[1..-1].split(' ')
280
+ status = r[19].chars
281
+ { parallel_number_exists: Boolean(r[0]),
282
+ serial_number: r[1],
283
+ device_mode: CONS::DEVICE_MODE[r[2]],
284
+ fault_code: CONS::FAULT_CODE[r[3]],
285
+ grid_voltage: Float(r[4]),
286
+ grid_frequency: Float(r[5]),
287
+ output_voltage: Float(r[6]),
288
+ output_frequency: Float(r[7]),
289
+ output_va: Integer(r[8]),
290
+ output_watts: Integer(r[9]),
291
+ load_percentage: Integer(r[10]),
292
+ battery_voltage: Float(r[11]),
293
+ battery_charge_current: Integer(r[12]),
294
+ battery_capacity: Integer(r[13]),
295
+ pv_input_voltage: Float(r[14]),
296
+ total_charge_current: Integer(r[15]),
297
+ total_output_va: Integer(r[16]),
298
+ total_output_watt: Integer(r[17]),
299
+ total_load_percentage: Integer(r[18]),
300
+ solar_charge_controller_enabled: Boolean(status[0]),
301
+ charging_from_utility: Boolean(status[1]),
302
+ charging_from_solar_charge_controller: Boolean(status[2]),
303
+ battery_status: [:normal, :under, :open][Integer("#{status[3]}#{status[4]}")],
304
+ line_status_ok: !Boolean(status[5]),
305
+ load_on: Boolean(status[6]),
306
+ configuration_changed: Boolean(status[7]),
307
+ output_mode: CONS::OUTPUT_MODE[r[20]],
308
+ charger_source_priority: CONS::CHARGER_SOURCE_PRIORITY[r[21]],
309
+ maximum_charge_current: Integer(r[22]),
310
+ device_maximum_charge_current: Integer(r[23]),
311
+ pv_input_current: Integer(r[24]),
312
+ battery_discharge_current: Integer(r[25]) }
313
+ end)
314
+
315
+ ##
316
+ # Reset device to factory defaults
317
+ #
318
+ # Returns:
319
+ # # A Boolean indicating if the device has reset to defaults succesfully
320
+ # true # Example
321
+ RESET_TO_DEFAULT = OP.new(command: 'PF', error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
322
+
323
+ ##
324
+ # Set device output frequency
325
+ #
326
+ # Input:
327
+ # 50 # => Any integer is accepted, consult manual for valid values
328
+ #
329
+ # Returns:
330
+ # # A Boolean indicating if the frequency was set succesfully
331
+ # true # Example
332
+ SET_OUTPUT_FREQUENCY = OP.new(command: lambda { |input| "F#{Integer(input).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
333
+
334
+ ##
335
+ # Set device output source priority
336
+ #
337
+ # Input:
338
+ # # A symbol donating the output source priority setting
339
+ # See AxpertConstants::OUTPUT_SOURCE_PRIORITY
340
+ #
341
+ # Returns:
342
+ # # A Boolean indicating if the output priority was set succesfully
343
+ # true # Example
344
+ SET_OUTPUT_SOURCE_PRIORITY = OP.new(command: lambda { |i| "POP#{CONS::OUTPUT_SOURCE_PRIORITY.key(i).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
345
+
346
+ ##
347
+ # Set battery re-charge voltage
348
+ #
349
+ # Input:
350
+ # 24.3 # => Any float is accepted, consult manual for valid values
351
+ #
352
+ # Returns:
353
+ # # A Boolean indicating if the recharge voltage was successfully set
354
+ # true # Example
355
+ SET_BATTERY_RECHARGE_VOLTAGE = OP.new(command: lambda { |input| "PBCV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
356
+
357
+ ##
358
+ # Set battery re-discharge voltage
359
+ #
360
+ # Input:
361
+ # 24.3 # => Any float is accepted, consult manual for valid values
362
+ #
363
+ # Returns:
364
+ # # A Boolean indicating if the re-discharge voltage was successfully set
365
+ # true # Example
366
+ SET_BATTERY_REDISCHARGE_VOLTAGE = OP.new(command: lambda { |input| "PBDV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
367
+
368
+ ##
369
+ # Set device charger priority
370
+ #
371
+ # Input:
372
+ # # (REQUIRED) Parameter 0 => A symbol donating the charger source priority
373
+ # See AxpertConstants::CHARGER_SOURCE_PRIORITY
374
+ #
375
+ # # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
376
+ # 5 # => Do not pass in this value unless the device is running in parallel mode
377
+ #
378
+ # Returns:
379
+ # # A Boolean indicating if the device charger priority was set successfully
380
+ # true # Example
381
+ SET_CHARGER_SOURCE_PRIORITY = OP.new(command: lambda do |mode, parallel_machine_number = nil|
382
+ if parallel_machine_number.nil?
383
+ "PPCP#{Integer(parallel_machine_number).to_s}#{CONS::CHARGER_SOURCE_PRIORITY.key(mode).to_s.rjust(2, '0')}"
384
+ else
385
+ "PCP#{CONS::CHARGER_SOURCE_PRIORITY.key(mode).to_s.rjust(2, '0')}"
386
+ end
387
+ end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
388
+
389
+ ##
390
+ # Set device input voltage sensitivity
391
+ #
392
+ # Input:
393
+ # # A symbol denoting the input voltage sensitivity
394
+ # See AxpertConstants::INPUT_VOLTAGE_SENSITIVITY
395
+ #
396
+ # Returns:
397
+ # # A Boolean indicating if the device input voltage sensitivity was set successfully
398
+ # true # Example
399
+ SET_INPUT_VOLTAGE_SENSITIVITY = OP.new(command: lambda { |i| "PGR#{CONS::INPUT_VOLTAGE_SENSITIVITY.key(i).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
400
+
401
+ ##
402
+ # Set battery type
403
+ #
404
+ # Input:
405
+ # # A symbol denoting the battery type
406
+ # See AxpertConstants::BATTERY_TYPE
407
+ #
408
+ # Returns:
409
+ # # A Boolean indicating if the device battery type was set successfully
410
+ # true # Example
411
+ SET_BATTERY_TYPE = OP.new(command: lambda { |i| "PBT#{CONS::BATTERY_TYPE.key(i).to_s.rjust(2, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
412
+
413
+ ##
414
+ # Set device battery cut-off voltage
415
+ #
416
+ # Input:
417
+ # 22.1 # => Any float is accepted, consult manual for valid values
418
+ #
419
+ # Returns:
420
+ # # A Boolean indicating if the battery cut-off voltage was successfully set
421
+ # true # Example
422
+ SET_BATTERY_CUTOFF_VOLTAGE = OP.new(command: lambda { |input| "PSDV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
423
+
424
+ ##
425
+ # Set device battery constant charging voltage
426
+ #
427
+ # Input:
428
+ # 28.3 # => Any float is accepted, consult manual for valid values
429
+ #
430
+ # Returns:
431
+ # # A Boolean indicating if the battery constant charging voltage was successfully set
432
+ # true # Example
433
+ SET_BATTERY_CONSTANT_CHARGING_VOLTAGE = OP.new(command: lambda { |input| "PCVV#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
434
+
435
+ ##
436
+ # Set device battery float charging voltage
437
+ #
438
+ # Input:
439
+ # 26.5 # => Any float is accepted, consult manual for valid values
440
+ #
441
+ # Returns:
442
+ # # A Boolean indicating if the battery float charging was successfully set
443
+ # true # Example
444
+ SET_BATTERY_FLOAT_CHARGING_VOLTAGE = OP.new(command: lambda { |input| "PBFT#{Float(input).to_s.rjust(4, '0')}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
445
+
446
+ ##
447
+ # Set parallel PV OK condition
448
+ #
449
+ # Input:
450
+ # # A input symbol donating the PV Parallel OK condition mode setting
451
+ # See AxpertConstants::PV_PARALLEL_OK_MODE
452
+ #
453
+ # Returns:
454
+ # # A Boolean indicating if the parallel PV OK condition was set successfully
455
+ # true # Example
456
+ SET_PV_PARALLEL_OK_MODE = OP.new(command: lambda { |i| "PPVOKC#{CONS::PV_PARALLEL_OK_MODE.key(i).to_s}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
457
+
458
+ ##
459
+ # Set PV power balance mode
460
+ #
461
+ # Input:
462
+ # # A input symbol donating the PV Power balance mode
463
+ # See AxpertConstants::PV_POWER_BALANCE_MODE
464
+ #
465
+ # Returns:
466
+ # # A Boolean indicating if the PV power balance mode was set successfully
467
+ # true # Example
468
+ SET_PV_POWER_BALANCE_MODE = OP.new(command: lambda { |i| "PSPB#{CONS::PV_POWER_BALANCE_MODE.key(i).to_s}" }, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
469
+
470
+ ##
471
+ # Set the maximum charging current
472
+ #
473
+ # Input:
474
+ # # (REQUIRED) Parameter 0 => Charge current
475
+ # 30 # => Any integer is accepted, consult manual for valid values
476
+ #
477
+ # # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
478
+ # 5 # => Do not pass in this value unless the device is running in parallel mode
479
+ #
480
+ # Returns:
481
+ # # A Boolean indicating if the maximum charging current was set successfully
482
+ # true # Example
483
+ SET_MAXIMUM_CHARGING_CURRENT = OP.new(command: lambda do |current, parallel_machine_number = 0|
484
+ current = Integer(current)
485
+ if (current >= 100)
486
+ "MNCHGC#{Integer(parallel_machine_number).to_s}#{current.to_s.rjust(3, '0')}"
487
+ else
488
+ "MCHGC#{Integer(parallel_machine_number).to_s}#{current.to_s.rjust(2, '0')}"
489
+ end
490
+ end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
491
+
492
+ #
493
+ # Set the maximum charging current for utility
494
+ #
495
+ # Input:
496
+ # # (REQUIRED) Parameter 0 => Charge current
497
+ # 30 # => Any integer is accepted, consult manual for valid values
498
+ #
499
+ # # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
500
+ # 5 # => Do not pass in this value unless the device is running in parallel mode
501
+ #
502
+ # Returns:
503
+ # # A Boolean indicating if the maximum charging current for utility was set successfully
504
+ # true # Example
505
+ SET_MAXIMUM_UTILITY_CHARGING_CURRENT = OP.new(command: lambda do |current, parallel_machine_number = 0|
506
+ "MUCHGC#{Integer(parallel_machine_number).to_s}#{Integer(current).to_s.rjust(2, '0')}"
507
+ end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
508
+
509
+ ##
510
+ # Set the parallel output mode (or single mode)
511
+ #
512
+ # Input:
513
+ # # (REQUIRED) Parameter 0 => A input symbol donating output mode
514
+ # See AxpertConstants::OUTPUT_MODE
515
+ #
516
+ # # (OPTIONAL) Parameter 1 => parallel_machine_number (Parallel mode only)
517
+ # 5 # => Do not pass in this value unless the device is running in parallel mode
518
+ #
519
+ # Returns:
520
+ # # A Boolean indicating if the parallel output mode was set successfully
521
+ # true # Example
522
+ SET_OUTPUT_MODE = OP.new(command: lambda do |mode, parallel_machine_number = 0|
523
+ "POPM#{CONS::OUTPUT_MODE.key(mode)}#{Integer(parallel_machine_number).to_s}"
524
+ end, error_on_nak: false, parser: lambda { |r| (r.data == '(ACK') })
525
+
526
+ require 'axpert_constants'
527
+ CONS = ::AxpertConstants # :nodoc:
528
+ send(:remove_const, :OP)
529
+ end
530
+
531
+ # This is hacky but it is really a must for accurate parsing
532
+ module ::Kernel # :nodoc:
533
+ def Boolean(input) # :nodoc:
534
+ return true if input.equal?(TrueClass)
535
+ return false if input.equal?(FalseClass)
536
+ parse = input.to_s.chomp.downcase
537
+ return true if ('true' == parse) || ('y' == parse) || ('t' == parse) || (1 == (Integer(parse) rescue nil))
538
+ return false if ('false' == parse) || ('n' == parse) || ('f' == parse) || (0 == (Integer(parse) rescue nil))
539
+ raise
540
+ rescue StandardError, ScriptError
541
+ raise ::ArgumentError.new("invalid value for Boolean(): \"#{input}\"")
542
+ end
543
+ end
@@ -0,0 +1,151 @@
1
+ ##
2
+ # A list of constants used by the Axpert devices with a convenient parser
3
+ #
4
+ # @author: Johan van der Vyver
5
+ module AxpertConstants
6
+ def self.lookup_hash(type, values) # :nodoc:
7
+ type = type.to_s.strip.freeze
8
+ values.values.each(&:freeze)
9
+ lookup = Hash.new do |_, k|
10
+ if lookup.has_key?(k.to_s)
11
+ lookup[k.to_s]
12
+ elsif (k.is_a?(Numeric) && (k.to_i == k) && (lookup.values.count > k))
13
+ lookup.values[k]
14
+ elsif lookup.values.include?(k)
15
+ k
16
+ else
17
+ raise ::AxpertConstants::UnknownConstant.new("Unknown #{type} '#{k}'")
18
+ end
19
+ end
20
+ lookup.instance_eval <<-RUBY_CODE
21
+ def key(input) # :nodoc:
22
+ parse = super(self[input]) rescue nil
23
+ parse = (super(input.to_s.to_sym) rescue nil) if parse.nil?
24
+ parse = (super(input) rescue nil) if parse.nil?
25
+ return parse unless parse.nil?
26
+ raise ::AxpertConstants::UnknownConstant.new("Could not find the #{type} constant '\#{input}' in \#{self.values}")
27
+ end
28
+ RUBY_CODE
29
+ lookup.merge!(values).freeze
30
+ end
31
+
32
+ ##
33
+ # The type of batteries connected to the device
34
+ # Used to determine the float, bulk charging, re-charge and re-discharge voltage ranges
35
+ #
36
+ # :agm # => Absorbent Glass Mat (AGM)
37
+ # :flooded # => Flooded Cell
38
+ # :user # => User defined
39
+ BATTERY_TYPE = lookup_hash('battery type', {'0' => :agm, '1' => :flooded, '2' => :user})
40
+
41
+ ##
42
+ # The current device mode
43
+ #
44
+ # :power # => Power on
45
+ # :standby # => Standby
46
+ # :line # => Line
47
+ # :battery # => Battery
48
+ # :fault # => Fault
49
+ DEVICE_MODE = lookup_hash('device mode', {'P' => :power, 'S' => :standby, 'L' => :line, 'B' => :battery, 'F' => :fault})
50
+
51
+ ##
52
+ # The input voltage is monitored to determine if it is within an acceptable range
53
+ # The sensitivity determines when the unit switches output mode to/from Utility
54
+ #
55
+ # :appliance # => Appliance mode sensitivity (see manual for ranges)
56
+ # :ups # => UPS mode sensitivity (see manual for ranges)
57
+ INPUT_VOLTAGE_SENSITIVITY = lookup_hash('input voltage sensitivity', {'0' => :appliance, '1' => :ups})
58
+
59
+ ##
60
+ # Device output priority
61
+ #
62
+ # :utility # => Prefer utility as first output power source
63
+ # :solar # => Prefer solar as first output power source
64
+ # :sbu # => Prefer solar first, then battery and utility last as output power source
65
+ OUTPUT_SOURCE_PRIORITY = lookup_hash('output source priority', {'0' => :utility, '1' => :solar, '2' => :sbu})
66
+
67
+ ##
68
+ # Battery charger source priority
69
+ #
70
+ # :utility_first # => Charge batteries from utility first
71
+ # :solar:first # => Charge batteries from solar first,
72
+ # :solar_and_utility # => Charge batteries from solar & utility
73
+ # :solar_only # => Charge batteries from solar only
74
+ CHARGER_SOURCE_PRIORITY = lookup_hash('charger source priority', {'0' => :utility_first, '1' => :solar_first, '2' => :solar_and_utility, '3' => :solar_only})
75
+
76
+ ##
77
+ # The type of device
78
+ #
79
+ # :grid_tie # => Grid tie device
80
+ # :off_grid # => Off-Grid device
81
+ # :hybrid # =>Hybrid device
82
+ DEVICE_TYPE = lookup_hash('device type', {'00' => :grid_tie, '01' => :off_grid, '10' => :hybrid})
83
+
84
+ ##
85
+ # The internal device topology
86
+ # NOTE: All models make use of a transformer in Inverter mode
87
+ #
88
+ # :transformerless # => The device output does not pass through an isolation transformer
89
+ # :transformer # => The device output does pass through an isolation transformer
90
+ DEVICE_TOPOLOGY = lookup_hash('device topology', {'0' => :transformerless, '1' => :transformer})
91
+
92
+ ##
93
+ # The current output mode of the device
94
+ #
95
+ # :single # => The device is running in single mode, single phase output
96
+ # :parallel # => The device is running in parallel mode (allow multiple units in parallel), single phase output
97
+ # :phase1 # => The device is running in parallel mode, 3 phase output, set to phase 1 of 3
98
+ # :phase2 # => The device is running in parallel mode, 3 phase output, set to phase 2 of 3
99
+ # :phase3 # => The device is running in parallel mode, 3 phase output, set to phase 3 of 3
100
+ OUTPUT_MODE = lookup_hash('output mode', {'0' => :single, '1' => :parallel, '2' => :phase1, '3' => :phase2, '4' => :phase3})
101
+
102
+ ##
103
+ # Only applicable to units running in Parallel!
104
+ # The required mode for the PV to report OK on the inverter in parallel mode
105
+ #
106
+ # :one # => Only one unit needs to report PV is OK
107
+ # :all # => All units must report that PV is OK
108
+ PV_PARALLEL_OK_MODE = lookup_hash('PV parallel mode', {'0' => :one, '1' => :all})
109
+
110
+ ##
111
+ # The PV power balance mode setting
112
+ #
113
+ # :charge # => PV output is limited to battery charge current
114
+ # :charge_and_load # => PV output will attempt to use enough power for charging the battery and enough to supply the connected load
115
+ PV_POWER_BALANCE_MODE = lookup_hash('PV power balance mode', {'0' => :charge, '1' => :charge_and_load})
116
+
117
+ ##
118
+ # Possible fault codes returned by the device
119
+ FAULT_CODE = lookup_hash('fault code',
120
+ { '00' => 'No faults',
121
+ '01' => 'Fan is locked',
122
+ '02' => 'Over temperature',
123
+ '03' => 'Battery voltage is too high',
124
+ '04' => 'Battery voltage is too low',
125
+ '05' => 'Output short circuited/Over temperature',
126
+ '06' => 'Output voltage is too high',
127
+ '07' => 'Overload time out',
128
+ '08' => 'Bus voltage is too high',
129
+ '09' => 'Bus soft start failed',
130
+ '11' => 'Main relay failed',
131
+ '51' => 'Inverter over current',
132
+ '52' => 'Bus soft start failed',
133
+ '53' => 'Inverter soft start failed',
134
+ '54' => 'Self-test failed',
135
+ '55' => 'Inverter over voltage on DC output',
136
+ '56' => 'Battery connection is open',
137
+ '57' => 'Current sensor failed',
138
+ '58' => 'Output voltage is too low',
139
+ '60' => 'Inverter negative power',
140
+ '71' => 'Parallel version different',
141
+ '71' => 'Output circuit failed',
142
+ '80' => 'CAN communication failed',
143
+ '81' => 'Parallel host line lost',
144
+ '82' => 'Parallel synchronized signal lost',
145
+ '83' => 'Parallel battery voltage is detected as different',
146
+ '84' => 'Parallel line voltage or frequency is detected as different',
147
+ '85' => 'Parallel line input current unbalanced',
148
+ '86' => 'Parallel output setting is different' })
149
+
150
+ class UnknownConstant < RuntimeError; end # :nodoc:
151
+ end
@@ -0,0 +1,2 @@
1
+ require 'axpert_commands'
2
+
@@ -0,0 +1,113 @@
1
+ ##
2
+ # A convenient immutable object to encapsulate the logic
3
+ # of a Voltronic Device operation consisting of:
4
+ # Command, parameter validation and result parser
5
+ #
6
+ # @author: Johan van der Vyver
7
+ class VoltronicDeviceOperation
8
+ require 'voltronic_rs232'
9
+ require 'time'
10
+
11
+ def initialize(input, &blk)
12
+ input = {}.merge(input) rescue (raise ::ArgumentError.new("Expected an input hash"))
13
+
14
+ @command = begin
15
+ as_lambda(input.fetch(:command))
16
+ rescue ::StandardError => err
17
+ err = "#{err.class.name.to_s} thrown; #{err.message.to_s}"
18
+ raise ::ArgumentError.new("Expected :command to be a String with a device command or Proc (#{err})")
19
+ end
20
+
21
+ @error_on_nak = (true == input.fetch(:error_on_nak, true))
22
+
23
+ @parser = begin
24
+ as_lambda(input.fetch(:parser))
25
+ rescue ::StandardError => err
26
+ err = "#{err.class.name.to_s} thrown; #{err.message.to_s}"
27
+ raise ::ArgumentError.new("Expected :parser to be a Proc or Lambda (#{err})")
28
+ end
29
+
30
+ @read_timeout = Integer(input.fetch(:serial_read_timeout_seconds, 2))
31
+ @write_timeout = Integer(input.fetch(:serial_write_timeout_seconds, 2))
32
+
33
+ @termination_character = begin
34
+ parse = input.fetch(:serial_termination_character, "\r").to_s
35
+ raise ::ArgumentError.new("Expected :serial_termination_character to be a single character") unless (parse.length == 1)
36
+ parse.freeze
37
+ end
38
+
39
+ freeze
40
+ end
41
+
42
+ ##
43
+ # Issue a command to the device and parse the output result
44
+ def issue_command(serial, *args)
45
+ serial.read_timeout = -1 # Prevent locking on serial
46
+
47
+ serial.write(command(*args).bytes)
48
+ result = begin
49
+ parse = ''
50
+ read_timeout = ::Time.now.to_i + @read_timeout # 2 seconds
51
+ while(true)
52
+ ch = serial.getc # Retrieve a single character from Serial port
53
+ if ch.nil?
54
+ sleep 0.1 # 100ms pause before polling again
55
+ next
56
+ end
57
+ parse += ch
58
+ break if (@termination_character == ch)
59
+ raise ::IOError.new("IO read timeout reached, giving up") if (Time.now.to_i > read_timeout)
60
+ end
61
+ parse
62
+ end
63
+
64
+ parse_result(result[0..-4])
65
+ end
66
+
67
+ ##
68
+ # Create an VoltronicRS232 object containing a command
69
+ # and optional parameter to execute on the device
70
+ def command(*args)
71
+ RS232_PROTO.new(@command.yield(*args))
72
+ end
73
+
74
+ ##
75
+ # Parse the command output returned from the Voltronic device
76
+ def parse_result(result)
77
+ result = RS232_PROTO.new(result)
78
+ if (@error_on_nak && ('(NAK' == result.data.upcase))
79
+ raise NAKReceivedError.new("Received NAK from device, this usually means an error occured, an invalid value was supplied or the command is not supported")
80
+ end
81
+ @parser.yield(result)
82
+ rescue ::StandardError, ::ScriptError => err
83
+ raise err if err.is_a?(NAKReceivedError)
84
+ err = "#{err.class.name.to_s} thrown; #{err.message.to_s}"
85
+ raise RS232ParseError.new("Could not parse the result, the output format may have changed (#{err})")
86
+ end
87
+
88
+ def to_s # :nodoc:
89
+ "#{self.class.name.to_s.split('::').last}"
90
+ end
91
+
92
+ def inspect # :nodoc:
93
+ self.to_s
94
+ end
95
+
96
+ private
97
+
98
+ def as_lambda(input)
99
+ if !input.is_a?(Proc)
100
+ input = begin
101
+ input_string = input.to_s.chomp.freeze
102
+ lambda { input_string.to_s.chomp.freeze }
103
+ end
104
+ end
105
+ result = Object.new
106
+ result.define_singleton_method(:_, &input)
107
+ result.method(:_).to_proc.freeze
108
+ end
109
+
110
+ RS232_PROTO = ::VoltronicRS232 # :nodoc
111
+ class NAKReceivedError < ::RuntimeError; end # :nodoc:
112
+ class RS232ParseError < RuntimeError; end # :nodoc:
113
+ end
@@ -0,0 +1,75 @@
1
+ ##
2
+ # Simple immutable object representing the Voltronic RS232 protocol
3
+ #
4
+ # @author: Johan van der Vyver
5
+ class VoltronicRS232
6
+ ##
7
+ # The human readable command to be sent to the device
8
+ attr_reader :data
9
+
10
+ ##
11
+ # The calculated CRC for the data
12
+ attr_reader :crc
13
+
14
+ ##
15
+ # The encoded data that will be transmitted over the wire
16
+ # Format: <DATA><CRC><CR>
17
+ attr_reader :bytes
18
+
19
+ def initialize(data) #:nodoc:
20
+ @data = data.to_s.chomp.dup.freeze
21
+ if (@data != @data.encode(::Encoding.find('ASCII'), {invalid: :replace, undef: :replace, replace: ''}))
22
+ raise ArgumentError.new("Input data can only be ASCII")
23
+ end
24
+ @crc = calculate_crc(data.bytes.to_a).map { |b| b.chr }.join.freeze
25
+ @bytes = "#{@data}#{@crc}\r".freeze
26
+ self.freeze
27
+ end
28
+
29
+ def to_s #:nodoc:
30
+ "#{self.class.name.to_s.split('::').last}(data: '#{data}')"
31
+ end
32
+
33
+ def inspect #:nodoc:
34
+ to_s
35
+ end
36
+
37
+ def ==(other) #:nodoc:
38
+ return true if self.equal?(other)
39
+ return false if other.nil?
40
+ return false unless other.respond_to?(:bytes)
41
+ (self.bytes == other.bytes)
42
+ end
43
+
44
+ private
45
+
46
+ # CRC calculation source: http://forums.aeva.asn.au/pip4048ms-inverter_topic4332_post53760.html#53760
47
+ def calculate_crc(pin) #:nodoc:
48
+ crc, da = 0, 0
49
+ for index in 0..(pin.length-1)
50
+ da = byte(byte(crc >> 8) >> 4)
51
+ crc = short(short(crc << 4) ^ CRC_TABLE[byte(da ^ byte(pin[index] >> 4))])
52
+ da = byte(byte(crc >> 8) >> 4)
53
+ crc = short(short(crc << 4) ^ CRC_TABLE[byte(da ^ byte(pin[index] & 0x0f))])
54
+ end
55
+
56
+ crc_low, crc_high = byte(crc & 0x00FF), byte(crc >> 8)
57
+ crc_low = short(crc_low + 1) if CRC_MOD.include?(crc_low)
58
+ crc_high = short(crc_high + 1) if CRC_MOD.include?(crc_high)
59
+ crc = short(short(crc_high << 8) | crc_low)
60
+ [byte(short(crc >> 8) & 0xff), byte(crc & 0xff)]
61
+ end
62
+
63
+ def byte(input) #:nodoc:
64
+ (input & 255)
65
+ end
66
+
67
+ def short(input) #:nodoc:
68
+ (input & 65535)
69
+ end
70
+
71
+ CRC_TABLE = [0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
72
+ 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef].freeze #:nodoc:
73
+
74
+ CRC_MOD = [0x28, 0x0d, 0x0a].freeze #:nodoc:
75
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: axpert_rs232
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Johan van der Vyver
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-08-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Simplify communicating with a Voltronics Axpert range inverter
14
+ email: johan.vdvyver@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - lib/axpert_commands.rb
20
+ - lib/axpert_constants.rb
21
+ - lib/voltronic_device_operation.rb
22
+ - lib/voltronic_rs232.rb
23
+ - lib/axpert_rs232.rb
24
+ homepage: http://rubygems.org/gems/axpert_rs232
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - '>='
35
+ - !ruby/object:Gem::Version
36
+ version: '0'
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 2.0.14
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Simplify communicating with a Voltronics Axpert range inverter
48
+ test_files: []