voltronic_power_interface 1.0.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.
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: []