voltronic_power_interface 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c3a18820cbf50df7c487d5adfc987909141aabdd
4
+ data.tar.gz: 33b19ddfd04564be7ed903a8171c69aa09427c9b
5
+ SHA512:
6
+ metadata.gz: 7115f2515c3ff251dfc768da86c70ce24e1699124cbc375dba742e8faddff6893ed0a86f2ec634fe2c0718f511901931e5edd76ce830a745122aaf8281837a2e
7
+ data.tar.gz: e9f9e3dbb566bdd3abf5a82a97b324b11e412418f9326a812c242fe0aae10651e85be55cbbb9eda4dab82ec12b5f92774dfffaf573e49eb99ac1a2883970c6ed
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Johan van der Vyver
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ ## Voltronic Power Interface
2
+ A library to communicate with [Voltronic Power](http://www.voltronicpower.com/) inverter/PV products.
3
+
4
+ **NOTE: The author of this library has no association with the above mentioned company**
5
+
6
+ Some [public documentation](https://s3-eu-west-1.amazonaws.com/osor62gd45llv5fcg47yijafsz6dcrjn/Infini_RS232_Protocol.pdf) exists of supported commands and how to interpret the output.
7
+
8
+ Not all commands are supported by all devices
9
+
10
+ ## Use
11
+
12
+ ### SerialPort
13
+
14
+ **NOTE: Only Ruby Gem ```serialport``` is currently supported**
15
+
16
+ **Linux/Mac OS X/BSD**
17
+
18
+ Serial ports are typically found in /dev/tty*
19
+ These devices may require root privilege in which case it is recommended to add a udev rule with less restrictive permissions.
20
+
21
+ Example udev rule for Prolific devices:
22
+
23
+ `echo 'ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", MODE="0666", SYMLINK+="ttyProlific"' > /etc/udev/rules.d/37-prolific.rules`
24
+
25
+ require 'voltronic/protocol'
26
+ # => true
27
+
28
+ proto = Voltronic::Protocol.for_serialport('/dev/tty_usbserial')
29
+ # => Protocol(IO)
30
+
31
+ proto.execute 'QPI'
32
+ #=> '(PI30'
33
+
34
+ timeout = 0.5 # 500 milliseconds
35
+ proto.execute 'QPI', timeout
36
+ #=> '(PI30'
37
+
38
+ **Windows**
39
+
40
+ require 'voltronic/protocol'
41
+ # => true
42
+
43
+ proto = Voltronic::Protocol.for_serialport('COM1')
44
+ # => Protocol(IO)
45
+
46
+ proto.execute 'QPI'
47
+ #=> '(PI30'
48
+
49
+ timeout = 0.5 # 500 milliseconds
50
+ proto.execute 'QPI', timeout
51
+ #=> '(PI30'
52
+
53
+ ### USB
54
+ **The implementation currently only support Linux using HIDRaw**
55
+
56
+ To avoid running as root, execute the following to add a symlink with less restricted privileges
57
+
58
+ `echo 'ATTRS{idVendor}=="0665", ATTRS{idProduct}=="5161", SUBSYSTEMS=="usb", ACTION=="add", MODE="0666", GROUP="root", SYMLINK+="hidVoltronic"' > /etc/udev/rules.d/35-voltronic.rules`
59
+
60
+ require 'voltronic/protocol'
61
+ # => true
62
+
63
+ proto = Voltronic::Protocol.for_usb('/dev/hidVoltronic')
64
+ # => Protocol(IO)
65
+
66
+ proto.execute 'QPI'
67
+ #=> '(PI30'
68
+
69
+ timeout = 0.5 # 500 milliseconds
70
+ proto.execute 'QPI', timeout
71
+ #=> '(PI30'
@@ -0,0 +1,90 @@
1
+ module ::Voltronic
2
+ module Digest #:nodoc:
3
+ def self.parse(input) #:nodoc:
4
+ input = begin
5
+ input_bytes = input.to_s.bytes
6
+ end_index = input_bytes.length
7
+ while(0 != end_index)
8
+ if (@@termination_character == input_bytes[(end_index -= 1)])
9
+ break
10
+ end
11
+ end
12
+ input_bytes[0..end_index]
13
+ end
14
+
15
+ if (3 > input.length)
16
+ raise MalformedInputError.new 'Input must be at at least 3 bytes in size'
17
+ end
18
+
19
+ input_crc = input[-3..-2]
20
+ input = input[0..-4].map do |byte|
21
+ if (128 > byte)
22
+ byte.chr
23
+ else
24
+ raise MalformedInputError.new 'Input contained an unparsable character'
25
+ end
26
+ end.join
27
+
28
+ calculated_crc = encode(input).to_s.bytes[-3..-2]
29
+ if (calculated_crc == input_crc)
30
+ return input
31
+ end
32
+
33
+ raise DigestMismatchError.new ['Device digest [',
34
+ input_crc.first, ',', input_crc.last, '] != [',
35
+ calculated_crc.first, ',', calculated_crc.last,
36
+ '] calculated digest'].join
37
+ end
38
+
39
+ def self.encode(input) #:nodoc:
40
+ input = input.to_s
41
+ crc = 0
42
+ input.bytes.each do |byte|
43
+ crc = ((@@crc_table[(((crc >> 8) ^ byte) & 0xff)] ^ (crc << 8)) & 0xffff)
44
+ end
45
+
46
+ first_byte = ((crc & 0xff00) >> 8)
47
+ second_byte = (crc & 0xff)
48
+
49
+ if ((0x28 == first_byte) || (0x0d == first_byte) || (0x0a == first_byte))
50
+ first_byte += 1
51
+ end
52
+ if ((0x28 == second_byte) || (0x0d == second_byte) || (0x0a == second_byte))
53
+ second_byte += 1
54
+ end
55
+
56
+ [input, first_byte.chr, second_byte.chr, @@termination_character.chr].join
57
+ end
58
+
59
+ def self.eos?(input) #:nodoc:
60
+ input.to_s.bytes.any? { |byte| (@@termination_character == byte) }
61
+ end
62
+
63
+ @@termination_character ||= "\r".bytes.first #:nodoc:
64
+
65
+ @@crc_table ||= [ #:nodoc:
66
+ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c,
67
+ 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318,
68
+ 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4,
69
+ 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630,
70
+ 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4,
71
+ 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969,
72
+ 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf,
73
+ 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
74
+ 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13,
75
+ 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9,
76
+ 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046,
77
+ 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2,
78
+ 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2,
79
+ 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e,
80
+ 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e,
81
+ 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
82
+ 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1,
83
+ 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07,
84
+ 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9,
85
+ 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ].freeze
86
+
87
+ class DigestMismatchError < RuntimeError; end
88
+ class MalformedInputError < RuntimeError; end
89
+ end
90
+ end
@@ -0,0 +1,95 @@
1
+ module ::Voltronic
2
+ module Protocol
3
+ require 'voltronic/digest'
4
+
5
+ ##
6
+ # Open a connection to the device
7
+ # - Closes an existing connection, if one exists
8
+
9
+ ##
10
+ # Create a new Protocol object using an input IO object
11
+ def self.for_io(io)
12
+ require 'voltronic/protocols/io'
13
+
14
+ def self.for_io(io)
15
+ ::Voltronic::Protocol::IO.method(:new).call(io)
16
+ end
17
+
18
+ for_io(io)
19
+ end
20
+
21
+ ##
22
+ # Create a new Protocol object for Axpert device connected to a serial port
23
+ def self.for_serialport(port_or_dev, baud = 2400, data_bits = 8, stop_bits = 1, parity = :none)
24
+ begin
25
+ require 'serialport'
26
+ rescue Exception
27
+ raise LoadError.new 'RubyGem "serialport" required to make use of Axpert on a Serial Port'
28
+ end
29
+
30
+ def self.for_serialport(port_or_dev, baud = 2400, data_bits = 8, stop_bits = 1, parity = :none)
31
+ port_or_dev = port_or_dev.to_s.strip
32
+
33
+ baud = begin
34
+ parse = Integer(baud)
35
+ raise if (1 > parse)
36
+ parse
37
+ rescue
38
+ raise ArgumentError.new "Invalid baud #{baud}"
39
+ end
40
+
41
+ data_bits = begin
42
+ parse = Integer(data_bits)
43
+ raise unless ((5 == parse) || (6 == parse) || (7 == parse) || (8 == parse))
44
+ parse
45
+ rescue
46
+ raise ArgumentError.new "Invalid data bits #{data_bits}"
47
+ end
48
+
49
+ stop_bits = begin
50
+ parse = Integer(stop_bits)
51
+ raise unless ((1 == parse) || (2 == parse))
52
+ parse
53
+ rescue
54
+ raise ArgumentError.new "Invalid stop bits #{stop_bits}"
55
+ end
56
+
57
+ parity = begin
58
+ parse = parity.to_s.strip.upcase
59
+ raise unless (('NONE' == parse) || ('EVEN' == parse) || ('ODD' == parse) || ('MARK' == parse) || ('SPACE' == parse))
60
+ ::SerialPort.const_get(parse.to_sym)
61
+ rescue
62
+ raise ArgumentError.new "Invalid Parity #{parity}"
63
+ end
64
+
65
+ serial_io = ::SerialPort.new(port_or_dev, baud, data_bits, stop_bits, parity)
66
+ serial_io.read_timeout = -1
67
+ for_io(serial_io)
68
+ end
69
+
70
+ for_serialport(port_or_dev, baud, data_bits, stop_bits, parity)
71
+ end
72
+
73
+ ##
74
+ # Create a new Protocol object for Axpert device connected to USB
75
+ #
76
+ # Currently only supported on Linux kernels that include HIDRaw
77
+ def self.for_usb(dev)
78
+ if !(RUBY_PLATFORM =~ /linux/)
79
+ raise NotImplementedError.new 'USB is currently only supported in Linux'
80
+ end
81
+
82
+ def self.for_usb(dev)
83
+ for_io(::File.open(dev.to_s, (::IO::RDWR | ::IO::NONBLOCK)))
84
+ end
85
+
86
+ for_usb(dev)
87
+ end
88
+
89
+ ##
90
+ # Execute a query on the device
91
+ def execute(input, timeout = 1.0)
92
+ raise NotImplementedError.new 'Protocol implementation does not implement execute(..)'
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,145 @@
1
+ require 'voltronic/protocol'
2
+
3
+ ##
4
+ # Implementation of the Protocol for IO objects
5
+ class ::Voltronic::Protocol::IO #:nodoc:
6
+ require 'time'
7
+
8
+ MAXIMUM_RESPONSE_SIZE = 1024
9
+ READ_BLOCK_SIZE = 8 # USB device report size
10
+ READ_OPTIONS = { exception: false }.freeze
11
+ CLEAR_TIMEOUT = 2.0
12
+
13
+ def initialize(io) #:nodoc:
14
+ @io_array = [io].freeze
15
+
16
+ @io = begin # Verify input io quacks like an IO
17
+ begin
18
+ io.sync = true
19
+ rescue Exception
20
+ end
21
+
22
+ begin
23
+ io.binmode = true
24
+ rescue Exception
25
+ end
26
+
27
+ begin
28
+ io.read_nonblock(READ_BLOCK_SIZE, nil, READ_OPTIONS)
29
+ rescue Exception
30
+ raise ArgumentError.new [io.to_s, ' does not support read_nonblock'].join
31
+ end
32
+
33
+ if !io.respond_to?(:write)
34
+ raise ArgumentError.new [io.to_s, ' does not support write'].join
35
+ end
36
+
37
+ begin
38
+ ::IO.select(@io_array, nil, nil, 0)
39
+ rescue Exception
40
+ raise ArgumentError.new [io.to_s, ' does not support IO.select(', io.to_s, ')'].join
41
+ end
42
+
43
+ io
44
+ end
45
+
46
+ @support_flush ||= begin # Assert if flush works
47
+ @io.flush
48
+ true
49
+ rescue Exception
50
+ false
51
+ end
52
+
53
+ clear_read_buffer
54
+ end
55
+
56
+ def execute(input, timeout = 1.0) #:nodoc:
57
+ timeout = begin
58
+ parse = Float(timeout)
59
+ raise if (0 >= parse)
60
+ parse
61
+ rescue
62
+ raise ArgumentError.new ['Invalid timeout ', timeout.to_s].join
63
+ end
64
+
65
+ clear_read_buffer
66
+ write(input)
67
+ read(timeout)
68
+ end
69
+
70
+ def to_s #:nodoc:
71
+ 'Protocol(IO)'
72
+ end
73
+
74
+ def inspect #:nodoc:
75
+ self.to_s
76
+ end
77
+
78
+ def close
79
+ @io.close
80
+ true
81
+ rescue Exception
82
+ false
83
+ end
84
+
85
+ def closed?
86
+ @io.closed?
87
+ end
88
+
89
+ private
90
+
91
+ def clear_read_buffer #:nodoc:
92
+ loop_timeout = Time.now.to_f + CLEAR_TIMEOUT
93
+
94
+ while(true)
95
+ if ::IO.select(@io_array, nil, nil, 0).nil?
96
+ return true
97
+ end
98
+
99
+ @io.read_nonblock(READ_BLOCK_SIZE, nil, READ_OPTIONS)
100
+
101
+ if (Time.now.to_f > loop_timeout)
102
+ raise DeviceError.new 'Device is responding with binary data before a write has been issued'
103
+ end
104
+ end
105
+ end
106
+
107
+ def read(timeout) #:nodoc:
108
+ blocks = ''
109
+ loop_timeout = Time.now.to_f + timeout
110
+ while(true)
111
+ iteration_timeout = (loop_timeout - Time.now.to_f)
112
+ if (0 >= iteration_timeout)
113
+ raise TimeoutError.new ['Timeout of ', timeout.to_s, ' seconds reached'].join
114
+ end
115
+
116
+ next if ::IO.select(@io_array, nil, nil, iteration_timeout).nil?
117
+
118
+ block = @io.read_nonblock(READ_BLOCK_SIZE, nil, READ_OPTIONS)
119
+ next if block.nil?
120
+ blocks << block
121
+
122
+ if ::Voltronic::Digest.eos?(block)
123
+ return ::Voltronic::Digest.parse(blocks)
124
+ end
125
+
126
+ if (blocks.length > MAXIMUM_RESPONSE_SIZE)
127
+ raise BufferOverflowError.new ['Device response exceeds the maximum of ', MAXIMUM_RESPONSE_SIZE.to_s, ' bytes'].join
128
+ end
129
+ end
130
+ end
131
+
132
+ def write(input) #:nodoc:
133
+ number_of_bytes = @io.write(::Voltronic::Digest.encode(input))
134
+ if @support_flush
135
+ @io.flush
136
+ end
137
+ number_of_bytes
138
+ end
139
+
140
+ class TimeoutError < RuntimeError; end #:nodoc:
141
+ class BufferOverflowError < RuntimeError; end #:nodoc:
142
+ class DeviceError < RuntimeError; end #:nodoc:
143
+
144
+ private_class_method :new
145
+ end
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: voltronic_power_interface
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: 2017-07-09 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby library to communicate with Voltronic Power devices
14
+ email: code@johan.vdvyver.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files:
18
+ - LICENSE
19
+ files:
20
+ - LICENSE
21
+ - README.md
22
+ - lib/voltronic/digest.rb
23
+ - lib/voltronic/protocol.rb
24
+ - lib/voltronic/protocols/io.rb
25
+ homepage: https://github.com/jovandervyver/voltronic_power_interface
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options:
31
+ - "--quiet"
32
+ - "--line-numbers"
33
+ - "--inline-source"
34
+ - "--title"
35
+ - Voltronic Power Interface
36
+ - "--main"
37
+ - README.rdoc
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 1.9.3
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubyforge_project:
52
+ rubygems_version: 2.6.8
53
+ signing_key:
54
+ specification_version: 4
55
+ summary: Voltronic Power Interface
56
+ test_files: []