lxp-packet 0.3.0 → 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +2 -0
- data/CHANGELOG.md +41 -1
- data/README.md +56 -4
- data/doc/LXP_PACKET.txt +8 -13
- data/doc/LXP_REGISTERS.txt +14 -4
- data/lib/lxp/packet/base.rb +19 -14
- data/lib/lxp/packet/device_functions.rb +6 -0
- data/lib/lxp/packet/heartbeat.rb +8 -4
- data/lib/lxp/packet/read_hold.rb +29 -11
- data/lib/lxp/packet/read_input.rb +9 -0
- data/lib/lxp/packet/read_input1.rb +68 -28
- data/lib/lxp/packet/read_input2.rb +40 -12
- data/lib/lxp/packet/read_input3.rb +23 -5
- data/lib/lxp/packet/registers.rb +3 -0
- data/lib/lxp/packet/write_single.rb +1 -5
- data/lib/lxp/utils.rb +3 -5
- data/lib/lxp/version.rb +1 -1
- data/lxp-packet.gemspec +1 -0
- data/spec/parser_spec.rb +103 -0
- data/spec/read_hold_spec.rb +34 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/write_single_spec.rb +33 -0
- metadata +27 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '096a5bb502e1d685696a0a14e483d59ddc8669bc8bda9c785dba152180d7ee9e'
|
4
|
+
data.tar.gz: 6448359ba7d17210b71eaf6173943d71d28ec37f243e544899efee05b8341ae0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3f483b86c1e92673f5c6d0f48da17db156e458af250ca7fe598fc5fec18f27c1a2c8dd8fb795192685b05497b385063a65b0db2f3d21131ca93c48fb313ac870
|
7
|
+
data.tar.gz: e3f9c800854a950d28ffb7e86c92a3bbaa60e53892a274489ccb9f774ae54818d177b373c4d48950f23787a7a85ca9555c161c575a6a2139705d3ad47f602235
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
CHANGED
@@ -5,7 +5,47 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
## [
|
8
|
+
## [0.7.1] - 2021-04-28
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
- `AC_CHARGE_SOC_LIMIT` register constant
|
13
|
+
|
14
|
+
## [0.7.0] - 2020-05-08
|
15
|
+
|
16
|
+
### Added
|
17
|
+
|
18
|
+
- support for requesting input registers on demand
|
19
|
+
- support for sending heartbeats
|
20
|
+
|
21
|
+
## [0.6.0] - 2020-04-10
|
22
|
+
|
23
|
+
### Added
|
24
|
+
|
25
|
+
- support `v_pv_*` keys in `ReadInput1`
|
26
|
+
- support `bat_count` key in `ReadInput3`
|
27
|
+
|
28
|
+
## [0.5.0] - 2020-04-09
|
29
|
+
|
30
|
+
### Added
|
31
|
+
|
32
|
+
- support 3 PV strings in `ReadInput1` and `ReadInput2`
|
33
|
+
- support 3 phase power in `ReadInput1`
|
34
|
+
- support `t_bat` key in `ReadInput2#to_h` (suspect this is Lead Acid only as mine is 0)
|
35
|
+
- support `uptime` key in `ReadInput2#to_h`
|
36
|
+
|
37
|
+
### Fixed
|
38
|
+
|
39
|
+
- parsing of temperatures above 255C in ReadInput2
|
40
|
+
|
41
|
+
|
42
|
+
## [0.4.0] - 2020-04-07
|
43
|
+
|
44
|
+
### Added
|
45
|
+
|
46
|
+
- parsing of ReadHold packets with multiple registers/values
|
47
|
+
- beginnings of a test suite!
|
48
|
+
|
9
49
|
|
10
50
|
## [0.3.0] - 2020-04-02
|
11
51
|
|
data/README.md
CHANGED
@@ -44,6 +44,14 @@ It can optionally be configured with a second network endpoint; I set this to TC
|
|
44
44
|
|
45
45
|
Alternatively you can probably just change the first setting if you don't care about the official Lux portal or mobile app being updated, though I found it useful to verify I was setting the right values at first. This would also prevent LuxPower sending you firmware updates (for better or for worse), not that I've had any so far.
|
46
46
|
|
47
|
+
## Inverter Fundamentals
|
48
|
+
|
49
|
+
The inverter has two basic sets of information.
|
50
|
+
|
51
|
+
There are 114 registers (0-113), which are also referred to as "holdings". See [doc/LXP_REGISTERS.txt](doc/LXP_REGISTERS.txt) for a list of them. Most of these you can write, and they affect inverter operation. Some pieces of information span several registers, for example the serial number is in registers 2 through 8.
|
52
|
+
|
53
|
+
Additionally there are input registers. These are transient information which the inverter broadcasts to any connected client every 2 minutes. These are sent as sets of 3 packets. `ReadInput1` / `ReadInput2` / `ReadInput3` are used to parse these.
|
54
|
+
|
47
55
|
## Examples
|
48
56
|
|
49
57
|
The inverter requires that your datalog serial and inverter serial are in the packets you send to it for it to respond.
|
@@ -73,9 +81,9 @@ end
|
|
73
81
|
|
74
82
|
This is necessary because occasionally the inverter will send us state data and heartbeats, as well as replies for other clients (see above) which we need to either process (if you're interested in them) or ignore (which is easier, and done here).
|
75
83
|
|
76
|
-
### Reading
|
84
|
+
### Reading Holding Registers
|
77
85
|
|
78
|
-
This is the simplest use-case; read the value of
|
86
|
+
This is the simplest use-case; read the value of a holding register from the inverter.
|
79
87
|
|
80
88
|
```ruby
|
81
89
|
pkt = LXP::Packet::ReadHold.new
|
@@ -96,6 +104,50 @@ r = read_reply(sock, pkt)
|
|
96
104
|
puts "Received: #{r.value}" # should be discharge cut-off value
|
97
105
|
```
|
98
106
|
|
107
|
+
Usually, `ReadHold` instances contain the details of just one register. However, it is possible they can contain multiple. Pressing "Read" on the LuxPower Web Portal provokes the inverter into sending out 5 packets that each contain multiple registers, for example.
|
108
|
+
|
109
|
+
To do this yourself, set `#value` in a `ReadHold` you're going to send to the inverter. This tells it how many registers you want in the reply, and they'll start from the number set in `#register`:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
# get registers 0 through 22 inclusive:
|
113
|
+
pkt.register = 0
|
114
|
+
pkt.value = 23
|
115
|
+
```
|
116
|
+
|
117
|
+
To access these in the reply, you can use subscript notation to get a register directly, or call `#to_h` to get a hash of registers/values. For convenience this also works with single register packets, though obviously only one subscript will ever return data, and `to_h` will only have one key.
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
# assuming pkt is a parsed packet with multiple registers/values:
|
121
|
+
pkt[0] # => 35462 # value of register 0
|
122
|
+
pkt.to_h # { 0 => 35462, 1 => 1, ... }
|
123
|
+
|
124
|
+
# assuming pkt is a parsed packet with only register 21:
|
125
|
+
pkt[21] # => value of register 21
|
126
|
+
pkt[22] # => nil
|
127
|
+
pkt.to_h # { 21 => 62292 }
|
128
|
+
```
|
129
|
+
|
130
|
+
### Reading Input Registers
|
131
|
+
|
132
|
+
This is similar to reading holdings. The inverter should send these packets every 2 minutes anyway, but if you want them on demand, you can create a `ReadInput1` (or 2, or 3) and send it.
|
133
|
+
|
134
|
+
The response packet contains a bunch of data, the simplest way to get at these is to call `to_h` on the packet, which returns a Hash of data:
|
135
|
+
|
136
|
+
```ruby
|
137
|
+
pkt = LXP::Packet::ReadInput1.new
|
138
|
+
pkt.datalog_serial = 'AB12345678'
|
139
|
+
pkt.inverter_serial = '1234567890'
|
140
|
+
|
141
|
+
# assuming your inverter is at 192.168.0.30
|
142
|
+
sock = TCPSocket.new('192.168.0.30', 4346)
|
143
|
+
sock.write(out)
|
144
|
+
|
145
|
+
r = read_reply(sock, pkt)
|
146
|
+
# r is a populated ReadInput1, which responds to #to_h:
|
147
|
+
r.to_h # => {:status=>16, :soc=>79, ... }
|
148
|
+
```
|
149
|
+
|
150
|
+
|
99
151
|
### Writing
|
100
152
|
|
101
153
|
Updating a value on the inverter.
|
@@ -119,7 +171,7 @@ r = read_reply(sock, pkt)
|
|
119
171
|
puts "Received: #{r.value}" # should be new discharge cut-off value, 20
|
120
172
|
```
|
121
173
|
|
122
|
-
### Updating a
|
174
|
+
### Updating a bitwise register
|
123
175
|
|
124
176
|
The Lux has two registers that contain multiple settings. In `doc/LXP_REGISTERS.txt` you can see them at 21 and 110. They have two bytes.
|
125
177
|
|
@@ -127,7 +179,7 @@ This library combines them into a 16bit word, so that the constants in `LXP::Pac
|
|
127
179
|
|
128
180
|
First you need to read the previous value, update it with a new bit, then write it back. This is really just a combination of the two above examples.
|
129
181
|
|
130
|
-
This enables AC charge. You need to OR the bit with the previous value so as not to change other settings
|
182
|
+
This example enables AC charge. You need to OR the bit with the previous value so as not to change other settings stored in register 21.
|
131
183
|
|
132
184
|
It could be improved not to bother doing the write if it was already enabled.
|
133
185
|
|
data/doc/LXP_PACKET.txt
CHANGED
@@ -28,15 +28,13 @@ Then there's a header:
|
|
28
28
|
194=data to translate
|
29
29
|
195=read param 196=write param
|
30
30
|
8 10 datalog serial number
|
31
|
-
18 2 data frame length
|
31
|
+
18 2 data frame length
|
32
32
|
|
33
|
-
Mostly the packets I've been dealing with have TCP_FUNCTION 194. I ignore
|
34
|
-
heartbeats, and I've not seen 195 or 196 I don't think.
|
35
33
|
|
36
|
-
For 194, bytes 18/19 are the data frame length which should match
|
37
|
-
of bytes that follows. It's sometimes easier to think of the
|
38
|
-
its own structure so here are byte offsets in both the dataframe
|
39
|
-
packet:
|
34
|
+
For TCP_FUNCTION 194, bytes 18/19 are the data frame length which should match
|
35
|
+
the number of bytes that follows. It's sometimes easier to think of the
|
36
|
+
dataframe as its own structure so here are byte offsets in both the dataframe
|
37
|
+
and the entire packet:
|
40
38
|
|
41
39
|
BYTE BYTES VALUE MEANING
|
42
40
|
0/20 1 address? 0 when writing to inverter, 1 when reading?
|
@@ -56,9 +54,6 @@ between R_HOLD and R_INPUT yet. W_SINGLE writes a single register; not used
|
|
56
54
|
W_MULTI.
|
57
55
|
|
58
56
|
To get the inverter to populate the response value for an R_HOLD, the value
|
59
|
-
you send
|
60
|
-
|
61
|
-
|
62
|
-
this seems to be for the regular stats the inverter sends out every 2 minutes;
|
63
|
-
seems to send 3 packets, the first one with address 0, then 40, then 80.
|
64
|
-
Not really done much with this yet.
|
57
|
+
you send needs to be above 0. This tells the inverter how many registers you
|
58
|
+
want to read. Normally this is 1, if its higher than you'll get seqeuential
|
59
|
+
registers
|
data/doc/LXP_REGISTERS.txt
CHANGED
@@ -8,10 +8,20 @@ testing in case I got anything wrong!
|
|
8
8
|
Registers 21 and 110 are bitmasks; normally you fetch the previous
|
9
9
|
settings, apply your changes, and set the new value back.
|
10
10
|
|
11
|
-
0 MODEL
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
0-1 MODEL
|
12
|
+
contains (not sure how to decode these yet):
|
13
|
+
lithiumType
|
14
|
+
powerRating
|
15
|
+
leadAcidType
|
16
|
+
ruleMask
|
17
|
+
batteryType
|
18
|
+
meterBrand
|
19
|
+
measurement
|
20
|
+
rule
|
21
|
+
2-8 SERIAL_NUM
|
22
|
+
7 FW_CODE ?
|
23
|
+
9-10 ?
|
24
|
+
12-14 TIME
|
15
25
|
15 COM_ADDR
|
16
26
|
16 LANGUAGE
|
17
27
|
20 PV_INPUT_MODE
|
data/lib/lxp/packet/base.rb
CHANGED
@@ -16,9 +16,9 @@ class LXP
|
|
16
16
|
attr_accessor :chksum
|
17
17
|
|
18
18
|
def initialize
|
19
|
-
@header
|
20
|
-
@data
|
21
|
-
@chksum
|
19
|
+
@header ||= [0] * 20
|
20
|
+
@data ||= [0] * 16
|
21
|
+
@chksum ||= [0, 0]
|
22
22
|
|
23
23
|
# prefix
|
24
24
|
@header[0] = 161
|
@@ -26,7 +26,6 @@ class LXP
|
|
26
26
|
|
27
27
|
self.protocol = 1
|
28
28
|
|
29
|
-
# length after first 6 bytes maybe?
|
30
29
|
self.packet_length = 32
|
31
30
|
|
32
31
|
@header[6] = 1 # unsure, always seems to be 1
|
@@ -89,6 +88,10 @@ class LXP
|
|
89
88
|
@header[7] = tcp_function & 0xff
|
90
89
|
end
|
91
90
|
|
91
|
+
def datalog_serial
|
92
|
+
@header[8, 10].pack('C*')
|
93
|
+
end
|
94
|
+
|
92
95
|
# Passed as a string
|
93
96
|
def datalog_serial=(datalog_serial)
|
94
97
|
@header[8, 10] = datalog_serial.bytes
|
@@ -111,6 +114,10 @@ class LXP
|
|
111
114
|
@data[1] = device_function
|
112
115
|
end
|
113
116
|
|
117
|
+
def inverter_serial
|
118
|
+
@data[2, 10].pack('C*')
|
119
|
+
end
|
120
|
+
|
114
121
|
# Passed as a string
|
115
122
|
def inverter_serial=(inverter_serial)
|
116
123
|
@data[2, 10] = inverter_serial.bytes
|
@@ -125,12 +132,6 @@ class LXP
|
|
125
132
|
@data[13] = (register >> 8) & 0xff
|
126
133
|
end
|
127
134
|
|
128
|
-
def value_length_byte?
|
129
|
-
@value_length_byte ||=
|
130
|
-
protocol == 2 &&
|
131
|
-
device_function != DeviceFunctions::WRITE_SINGLE
|
132
|
-
end
|
133
|
-
|
134
135
|
def value_length
|
135
136
|
if value_length_byte?
|
136
137
|
@data[14]
|
@@ -142,13 +143,11 @@ class LXP
|
|
142
143
|
# protocol 1 has value at 14 and 15
|
143
144
|
# protocol 2 has length at 14, then that many bytes of values
|
144
145
|
#
|
145
|
-
|
146
|
-
#
|
147
|
-
def value
|
146
|
+
def values
|
148
147
|
if value_length_byte?
|
149
148
|
@data[15, value_length]
|
150
149
|
else
|
151
|
-
@data[14] | @data[15] << 8
|
150
|
+
@data[14, 2] # | @data[15] << 8
|
152
151
|
end
|
153
152
|
end
|
154
153
|
|
@@ -171,6 +170,12 @@ class LXP
|
|
171
170
|
|
172
171
|
private
|
173
172
|
|
173
|
+
def value_length_byte?
|
174
|
+
@value_length_byte ||=
|
175
|
+
protocol == 2 &&
|
176
|
+
device_function != DeviceFunctions::WRITE_SINGLE
|
177
|
+
end
|
178
|
+
|
174
179
|
def crc16_modbus(arr)
|
175
180
|
arr.length.times.inject(0xffff) do |r, n|
|
176
181
|
r ^= arr[n]
|
data/lib/lxp/packet/heartbeat.rb
CHANGED
@@ -15,16 +15,20 @@ class LXP
|
|
15
15
|
#
|
16
16
|
# They have no data and no checksum, so there's really not a lot here.
|
17
17
|
#
|
18
|
-
# Ideally these could be instantiated via .parse, but they only seem to
|
19
|
-
# have one byte for the length, and .parse expects two. There's nothing
|
20
|
-
# to parse anyway so never mind.
|
21
|
-
#
|
22
18
|
class Heartbeat < Base
|
23
19
|
def initialize
|
20
|
+
@header = [0] * 19
|
21
|
+
|
24
22
|
super
|
25
23
|
|
24
|
+
self.packet_length = 13
|
25
|
+
self.protocol = 2
|
26
26
|
self.tcp_function = TcpFunctions::HEARTBEAT
|
27
27
|
end
|
28
|
+
|
29
|
+
def bytes
|
30
|
+
header
|
31
|
+
end
|
28
32
|
end
|
29
33
|
end
|
30
34
|
end
|
data/lib/lxp/packet/read_hold.rb
CHANGED
@@ -12,24 +12,42 @@ class LXP
|
|
12
12
|
self.device_function = DeviceFunctions::READ_HOLD
|
13
13
|
|
14
14
|
self.data_length = 18
|
15
|
-
|
16
|
-
#
|
17
|
-
#
|
15
|
+
|
16
|
+
# in ReadHold packets, the value is the number of registers
|
17
|
+
# we want to read. Default to 1.
|
18
18
|
self.value = 1
|
19
19
|
end
|
20
20
|
|
21
|
-
#
|
21
|
+
# Return the first value in a ReadHold packet. This is normally used
|
22
|
+
# when the packet only has one value.
|
23
|
+
#
|
24
|
+
# #values returns an Array. This converts it to an int.
|
22
25
|
#
|
23
|
-
|
26
|
+
def value(offset = 0)
|
27
|
+
Utils.int(values[offset, 2])
|
28
|
+
end
|
29
|
+
|
30
|
+
# Subscript notation is used when the ReadHold packet has multiple
|
31
|
+
# registers in it. This is indicated by value_length > 2.
|
24
32
|
#
|
25
|
-
#
|
26
|
-
#
|
33
|
+
# In this case, #register tells us the first register in the values, then
|
34
|
+
# each 2 bytes are subsequent registers.
|
27
35
|
#
|
28
|
-
def
|
29
|
-
|
36
|
+
def [](reg_num)
|
37
|
+
offset = (reg_num - register) * 2
|
38
|
+
|
39
|
+
return if offset.negative? || offset > data_length
|
40
|
+
|
41
|
+
value(offset)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return a Hash of all register->values in the ReadHold packet.
|
45
|
+
def to_h
|
46
|
+
r = register
|
30
47
|
|
31
|
-
|
32
|
-
|
48
|
+
values.each_slice(2).each_with_index.map do |v, idx|
|
49
|
+
[r + idx, Utils.int(v)]
|
50
|
+
end.to_h
|
33
51
|
end
|
34
52
|
end
|
35
53
|
end
|
@@ -6,6 +6,15 @@ class LXP
|
|
6
6
|
class Packet
|
7
7
|
# Input packets are a stream of values related to energy flows (inputs?)
|
8
8
|
class ReadInput < Base
|
9
|
+
def initialize
|
10
|
+
super
|
11
|
+
|
12
|
+
self.tcp_function = TcpFunctions::TRANSLATED_DATA
|
13
|
+
self.device_function = DeviceFunctions::READ_INPUT
|
14
|
+
|
15
|
+
self.data_length = 18
|
16
|
+
end
|
17
|
+
|
9
18
|
def self.parse(ascii)
|
10
19
|
i = super
|
11
20
|
|
@@ -5,47 +5,87 @@ require_relative 'read_input'
|
|
5
5
|
class LXP
|
6
6
|
class Packet
|
7
7
|
class ReadInput1 < ReadInput
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
self.register = 0
|
12
|
+
self.value = 40
|
13
|
+
end
|
14
|
+
|
8
15
|
# Decode the data and return a hash of values in this input packet
|
9
16
|
def to_h
|
10
17
|
{
|
11
|
-
|
18
|
+
# 0 = static 1
|
19
|
+
# 1 = R_INPUT
|
20
|
+
# 2..12 = serial
|
21
|
+
# 13/14 = length
|
22
|
+
|
23
|
+
status: Utils.int(@data[15, 2]),
|
24
|
+
|
25
|
+
v_pv_1: Utils.int(@data[17, 2]) / 10.0, # V
|
26
|
+
v_pv_2: Utils.int(@data[19, 2]) / 10.0, # V
|
27
|
+
v_pv_3: Utils.int(@data[21, 2]) / 10.0, # V
|
12
28
|
|
13
|
-
v_bat: Utils.int(@data[23, 2]
|
29
|
+
v_bat: Utils.int(@data[23, 2]) / 10.0, # V
|
14
30
|
soc: @data[25], # %
|
31
|
+
# 26 used for anything?
|
15
32
|
|
16
|
-
#
|
33
|
+
# 27 always been 0 so far
|
34
|
+
# 28 I've seen anything from 0 to 53, changes occasionally but
|
35
|
+
# not spotted a pattern yet.
|
36
|
+
_unknown_i1_27: @data[27],
|
37
|
+
_unknown_i1_28: @data[28],
|
17
38
|
|
18
|
-
|
19
|
-
|
20
|
-
p_discharge: Utils.int(@data[37, 2], :lsb), # W
|
21
|
-
v_acr: Utils.int(@data[39, 2], :lsb) / 10.0, # V
|
22
|
-
f_ac: Utils.int(@data[45, 2], :lsb) / 100.0, # Hz
|
39
|
+
# this might be useless if 27 and 28 are independent
|
40
|
+
_unknown_i1_27_28: Utils.int(@data[27, 2]),
|
23
41
|
|
24
|
-
|
25
|
-
|
42
|
+
p_pv: Utils.int(@data[29, 2]) +
|
43
|
+
Utils.int(@data[31, 2]) +
|
44
|
+
Utils.int(@data[33, 2]), # W
|
45
|
+
p_pv_1: Utils.int(@data[29, 2]), # W
|
46
|
+
p_pv_2: Utils.int(@data[31, 2]), # W
|
47
|
+
p_pv_3: Utils.int(@data[33, 2]), # W
|
48
|
+
p_charge: Utils.int(@data[35, 2]), # W
|
49
|
+
p_discharge: Utils.int(@data[37, 2]), # W
|
50
|
+
v_ac_r: Utils.int(@data[39, 2]) / 10.0, # V
|
51
|
+
v_ac_s: Utils.int(@data[41, 2]) / 10.0, # V
|
52
|
+
v_ac_t: Utils.int(@data[43, 2]) / 10.0, # V
|
53
|
+
f_ac: Utils.int(@data[45, 2]) / 100.0, # Hz
|
26
54
|
|
27
|
-
#
|
55
|
+
p_inv: Utils.int(@data[47, 2]), # W
|
56
|
+
p_rec: Utils.int(@data[49, 2]), # W
|
28
57
|
|
29
|
-
|
30
|
-
|
58
|
+
# this seems to track with charge/discharge but at lower values.
|
59
|
+
# no idea what this means.
|
60
|
+
_unknown_i1_51_52: Utils.int(@data[51, 2]), # W ?
|
61
|
+
|
62
|
+
pf: Utils.int(@data[53, 2]) / 1000.0, # Hz
|
63
|
+
|
64
|
+
v_eps_r: Utils.int(@data[55, 2]) / 10.0, # V
|
65
|
+
v_eps_s: Utils.int(@data[57, 2]) / 10.0, # V
|
66
|
+
v_eps_t: Utils.int(@data[59, 2]) / 10.0, # V
|
67
|
+
f_eps: Utils.int(@data[61, 2]) / 100.0, # Hz
|
31
68
|
|
32
69
|
# peps and seps in 63..66?
|
33
70
|
|
34
|
-
p_to_grid: Utils.int(@data[67, 2]
|
35
|
-
p_to_user: Utils.int(@data[69, 2]
|
36
|
-
|
37
|
-
e_pv_day: Utils.int(@data[71, 2]
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
71
|
+
p_to_grid: Utils.int(@data[67, 2]), # W
|
72
|
+
p_to_user: Utils.int(@data[69, 2]), # W
|
73
|
+
|
74
|
+
e_pv_day: (Utils.int(@data[71, 2]) +
|
75
|
+
Utils.int(@data[73, 2]) + Utils.int(@data[75, 2])) / 10.0, # kWh
|
76
|
+
e_pv_1_day: Utils.int(@data[71, 2]) / 10.0, # kWh
|
77
|
+
e_pv_2_day: Utils.int(@data[73, 2]) / 10.0, # kWh
|
78
|
+
e_pv_3_day: Utils.int(@data[75, 2]) / 10.0, # kWh
|
79
|
+
e_inv_day: Utils.int(@data[77, 2]) / 10.0, # kWh
|
80
|
+
e_rec_day: Utils.int(@data[79, 2]) / 10.0, # kWh
|
81
|
+
e_chg_day: Utils.int(@data[81, 2]) / 10.0, # kWh
|
82
|
+
e_dischg_day: Utils.int(@data[83, 2]) / 10.0, # kWh
|
83
|
+
e_eps_day: Utils.int(@data[85, 2]) / 10.0, # kWh
|
84
|
+
e_to_grid_day: Utils.int(@data[87, 2]) / 10.0, # kWh
|
85
|
+
e_to_user_day: Utils.int(@data[89, 2]) / 10.0, # kWh
|
86
|
+
|
87
|
+
v_bus_1: Utils.int(@data[91, 2]) / 10.0, # V
|
88
|
+
v_bus_2: Utils.int(@data[93, 2]) / 10.0 # V
|
49
89
|
}
|
50
90
|
end
|
51
91
|
end
|
@@ -5,21 +5,49 @@ require_relative 'read_input'
|
|
5
5
|
class LXP
|
6
6
|
class Packet
|
7
7
|
class ReadInput2 < ReadInput
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
self.register = 40
|
12
|
+
self.value = 40
|
13
|
+
end
|
14
|
+
|
8
15
|
# Decode the data and return a hash of values in this input packet
|
9
16
|
def to_h
|
10
17
|
{
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
# 0 = static 1
|
19
|
+
# 1 = R_INPUT
|
20
|
+
# 2..12 = serial
|
21
|
+
# 13/14 = length
|
22
|
+
|
23
|
+
e_pv_all: (Utils.int(@data[15, 4]) +
|
24
|
+
Utils.int(@data[19, 4]) +
|
25
|
+
Utils.int(@data[23, 4])) / 10.0, # kWh
|
26
|
+
e_pv_1_all: Utils.int(@data[15, 4]) / 10.0, # kWh
|
27
|
+
e_pv_2_all: Utils.int(@data[19, 4]) / 10.0, # kWh
|
28
|
+
e_pv_3_all: Utils.int(@data[23, 4]) / 10.0, # kWh
|
29
|
+
e_inv_all: Utils.int(@data[27, 4]) / 10.0, # kWh
|
30
|
+
e_rec_all: Utils.int(@data[31, 4]) / 10.0, # kWh
|
31
|
+
e_chg_all: Utils.int(@data[35, 4]) / 10.0, # kWh
|
32
|
+
e_dischg_all: Utils.int(@data[39, 4]) / 10.0, # kWh
|
33
|
+
e_eps_all: Utils.int(@data[43, 4]) / 10.0, # kWh
|
34
|
+
e_to_grid_all: Utils.int(@data[47, 4]) / 10.0, # kWh
|
35
|
+
e_to_user_all: Utils.int(@data[51, 4]) / 10.0, # kWh
|
36
|
+
|
37
|
+
# 55 .. 62?
|
38
|
+
# fault code? 4 bytes?
|
39
|
+
# warning code? 4 bytes?
|
40
|
+
|
41
|
+
t_inner: Utils.int(@data[63, 2]),
|
42
|
+
t_rad_1: Utils.int(@data[65, 2]),
|
43
|
+
t_rad_2: Utils.int(@data[67, 2]),
|
44
|
+
t_bat: Utils.int(@data[69, 2]),
|
45
|
+
|
46
|
+
# 71..72 ?
|
47
|
+
|
48
|
+
# this actually seems to be cumulative runtime.
|
49
|
+
# not found an uptime since reboot yet.
|
50
|
+
uptime: Utils.int(@data[73, 4]) # seconds
|
23
51
|
}
|
24
52
|
end
|
25
53
|
end
|
@@ -5,14 +5,30 @@ require_relative 'read_input'
|
|
5
5
|
class LXP
|
6
6
|
class Packet
|
7
7
|
class ReadInput3 < ReadInput
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
|
11
|
+
self.register = 80
|
12
|
+
self.value = 40
|
13
|
+
end
|
14
|
+
|
8
15
|
# Decode the data and return a hash of values in this input packet
|
9
16
|
def to_h
|
10
17
|
{
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
18
|
+
# 0 = static 1
|
19
|
+
# 1 = R_INPUT
|
20
|
+
# 2..12 = serial
|
21
|
+
# 13/14 = length
|
15
22
|
|
23
|
+
# 15..16? .. (observed: 10)
|
24
|
+
|
25
|
+
max_chg_curr: Utils.int(@data[17, 2]) / 100.0, # A
|
26
|
+
max_dischg_curr: Utils.int(@data[19, 2]) / 100.0, # A
|
27
|
+
charge_volt_ref: Utils.int(@data[21, 2]) / 10.0, # V
|
28
|
+
dischg_cut_volt: Utils.int(@data[23, 2]) / 10.0, # V
|
29
|
+
|
30
|
+
# are these actually 2 bytes as well?
|
31
|
+
# never seen data in them so its hard to tell.
|
16
32
|
bat_status_0: @data[25],
|
17
33
|
bat_status_1: @data[27],
|
18
34
|
bat_status_2: @data[29],
|
@@ -23,7 +39,9 @@ class LXP
|
|
23
39
|
bat_status_7: @data[39],
|
24
40
|
bat_status_8: @data[41],
|
25
41
|
bat_status_9: @data[43],
|
26
|
-
bat_status_inv: @data[45]
|
42
|
+
bat_status_inv: @data[45],
|
43
|
+
|
44
|
+
bat_count: Utils.int(@data[47, 2])
|
27
45
|
}
|
28
46
|
end
|
29
47
|
end
|
data/lib/lxp/packet/registers.rb
CHANGED
@@ -27,14 +27,10 @@ class LXP
|
|
27
27
|
#
|
28
28
|
# Raise if not, as that is not expected?
|
29
29
|
#
|
30
|
-
# Base#value will return an int for protocol 1, or an Array
|
31
|
-
# for protocol 2. If we can, convert that Array to an int.
|
32
|
-
#
|
33
30
|
def value
|
34
31
|
raise 'value_length not 2?' unless value_length == 2
|
35
32
|
|
36
|
-
|
37
|
-
r.is_a?(Array) ? Utils.int(r, 2) : r
|
33
|
+
Utils.int(values[0, 2])
|
38
34
|
end
|
39
35
|
end
|
40
36
|
end
|
data/lib/lxp/utils.rb
CHANGED
@@ -4,16 +4,14 @@ class LXP
|
|
4
4
|
module Utils
|
5
5
|
module_function
|
6
6
|
|
7
|
-
def int(bytes
|
8
|
-
bytes = bytes.reverse if order == :msb
|
9
|
-
|
7
|
+
def int(bytes)
|
10
8
|
bytes.each_with_index.map do |b, idx|
|
11
9
|
b << (idx * 8)
|
12
10
|
end.inject(:|)
|
13
11
|
end
|
14
12
|
|
15
|
-
def int_complement(bytes
|
16
|
-
r = int(bytes
|
13
|
+
def int_complement(bytes)
|
14
|
+
r = int(bytes)
|
17
15
|
r -= 0x10000 if r & 0x8000 == 0x8000
|
18
16
|
r
|
19
17
|
end
|
data/lib/lxp/version.rb
CHANGED
data/lxp-packet.gemspec
CHANGED
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe LXP::Packet::Parser do
|
4
|
+
let(:parser) { LXP::Packet::Parser.new(input.pack('C*')) }
|
5
|
+
let(:packet) { parser.parse }
|
6
|
+
|
7
|
+
subject { packet }
|
8
|
+
|
9
|
+
context 'ReadHold multi data (registers 0-22)' do
|
10
|
+
let(:input) { [161, 26, 2, 0, 77, 0, 1, 194, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 63, 0, 1, 3, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 0, 0, 46, 134, 138, 1, 0, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 66, 65, 65, 65, 1, 10, 10, 1, 0, 0, 20, 4, 7, 16, 14, 49, 1, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 84, 243, 176, 4, 40, 224] }
|
11
|
+
|
12
|
+
it { is_expected.to be_a LXP::Packet::ReadHold }
|
13
|
+
|
14
|
+
it 'has the correct attributes' do
|
15
|
+
expect(packet).to have_attributes register: 0,
|
16
|
+
values: [134, 138, 1, 0, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 66, 65, 65, 65, 1, 10, 10, 1, 0, 0, 20, 4, 7, 16, 14, 49, 1, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 84, 243, 176, 4],
|
17
|
+
to_h: { 0 => 35_462, 1 => 1, 2 => 22_616, 3 => 22_616, 4 => 22_616, 5 => 22_616, 6 => 22_616, 7 => 16_706, 8 => 16_705, 9 => 2561, 10 => 266, 11 => 0, 12 => 1044, 13 => 4103, 14 => 12_558, 15 => 1, 16 => 1, 17 => 0, 18 => 0, 19 => 4, 20 => 0, 21 => 62_292, 22 => 1200 },
|
18
|
+
protocol: 2,
|
19
|
+
packet_length: 77, data_length: 63,
|
20
|
+
tcp_function: LXP::Packet::TcpFunctions::TRANSLATED_DATA,
|
21
|
+
device_function: LXP::Packet::DeviceFunctions::READ_HOLD,
|
22
|
+
inverter_serial: 'XXXXXXXXXX',
|
23
|
+
datalog_serial: 'cccccccccc',
|
24
|
+
bytes: input
|
25
|
+
expect(packet[22]).to eq 1200
|
26
|
+
|
27
|
+
# test out of bounds subscripts return nil
|
28
|
+
expect(packet[23]).to be_nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'ReadHold multi data (registers 80-113)' do
|
33
|
+
let(:input) { [161, 26, 2, 0, 99, 0, 1, 194, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 85, 0, 1, 3, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 80, 0, 68, 0, 0, 0, 0, 100, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 50, 0, 111, 9, 252, 8, 20, 0, 5, 0, 0, 0, 0, 0, 0, 0, 48, 2, 144, 1, 66, 0, 66, 0, 0, 0, 0, 0, 10, 0, 56, 255, 38, 2, 0, 0, 144, 1, 2, 0, 0, 0, 0, 0, 0, 0, 193, 242] }
|
34
|
+
|
35
|
+
it { is_expected.to be_a LXP::Packet::ReadHold }
|
36
|
+
|
37
|
+
it 'has the correct attributes' do
|
38
|
+
expect(packet).to have_attributes register: 80,
|
39
|
+
values: [0, 0, 0, 0, 100, 0, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 0, 50, 0, 111, 9, 252, 8, 20, 0, 5, 0, 0, 0, 0, 0, 0, 0, 48, 2, 144, 1, 66, 0, 66, 0, 0, 0, 0, 0, 10, 0, 56, 255, 38, 2, 0, 0, 144, 1, 2, 0, 0, 0, 0, 0, 0, 0],
|
40
|
+
to_h: { 80 => 0, 81 => 0, 82 => 100, 83 => 20, 84 => 0, 85 => 0, 86 => 0, 87 => 0, 88 => 0, 89 => 0, 90 => 230, 91 => 50, 92 => 2415, 93 => 2300, 94 => 20, 95 => 5, 96 => 0, 97 => 0, 98 => 0, 99 => 560, 100 => 400, 101 => 66, 102 => 66, 103 => 0, 104 => 0, 105 => 10, 106 => 65_336, 107 => 550, 108 => 0, 109 => 400, 110 => 2, 111 => 0, 112 => 0, 113 => 0 },
|
41
|
+
protocol: 2,
|
42
|
+
packet_length: 99, data_length: 85,
|
43
|
+
tcp_function: LXP::Packet::TcpFunctions::TRANSLATED_DATA,
|
44
|
+
device_function: LXP::Packet::DeviceFunctions::READ_HOLD,
|
45
|
+
inverter_serial: 'XXXXXXXXXX',
|
46
|
+
datalog_serial: 'cccccccccc',
|
47
|
+
bytes: input
|
48
|
+
expect(packet[80]).to eq 0
|
49
|
+
expect(packet[94]).to eq 20
|
50
|
+
expect(packet[113]).to eq 0
|
51
|
+
|
52
|
+
# test out of bounds subscripts return nil
|
53
|
+
expect(packet[114]).to be_nil
|
54
|
+
expect(packet[79]).to be_nil
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'ReadInput1 data' do
|
59
|
+
let(:input) { [161, 26, 2, 0, 111, 0, 1, 194, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 97, 0, 1, 4, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 0, 0, 80, 16, 0, 0, 0, 0, 0, 0, 0, 241, 1, 95, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 121, 2, 172, 9, 1, 0, 0, 0, 146, 19, 57, 2, 0, 0, 227, 0, 232, 3, 172, 9, 0, 11, 80, 112, 146, 19, 0, 0, 0, 0, 0, 0, 0, 0, 221, 0, 0, 0, 0, 0, 42, 0, 73, 0, 86, 0, 52, 0, 0, 0, 36, 0, 2, 0, 172, 14, 181, 11, 127, 131] }
|
60
|
+
|
61
|
+
it { is_expected.to be_a LXP::Packet::ReadInput1 }
|
62
|
+
|
63
|
+
it 'has the correct attributes' do
|
64
|
+
expect(packet).to have_attributes register: 0,
|
65
|
+
values: [16, 0, 0, 0, 0, 0, 0, 0, 241, 1, 95, 0, 0, 47, 0, 0, 0, 0, 0, 0, 0, 0, 121, 2, 172, 9, 1, 0, 0, 0, 146, 19, 57, 2, 0, 0, 227, 0, 232, 3, 172, 9, 0, 11, 80, 112, 146, 19, 0, 0, 0, 0, 0, 0, 0, 0, 221, 0, 0, 0, 0, 0, 42, 0, 73, 0, 86, 0, 52, 0, 0, 0, 36, 0, 2, 0, 172, 14, 181, 11],
|
66
|
+
protocol: 2,
|
67
|
+
packet_length: 111, data_length: 97,
|
68
|
+
tcp_function: LXP::Packet::TcpFunctions::TRANSLATED_DATA,
|
69
|
+
device_function: LXP::Packet::DeviceFunctions::READ_INPUT,
|
70
|
+
inverter_serial: 'XXXXXXXXXX',
|
71
|
+
datalog_serial: 'cccccccccc',
|
72
|
+
bytes: input
|
73
|
+
end
|
74
|
+
|
75
|
+
describe '#to_h' do
|
76
|
+
subject { packet.to_h }
|
77
|
+
it { is_expected.to eq _unknown_i1_27: 0, _unknown_i1_28: 47, _unknown_i1_27_28: 12_032, _unknown_i1_51_52: 227, status: 16, v_pv_1: 0.0, v_pv_2: 0.0, v_pv_3: 0.0, v_bat: 49.7, soc: 95, p_pv: 0, p_pv_1: 0, p_pv_2: 0, p_pv_3: 0, p_charge: 0, p_discharge: 633, v_ac_r: 247.6, v_ac_s: 0.1, v_ac_t: 0.0, f_ac: 50.1, p_inv: 569, p_rec: 0, pf: 1.0, v_eps_r: 247.6, v_eps_s: 281.6, v_eps_t: 2875.2, f_eps: 50.1, p_to_grid: 0, p_to_user: 0, e_pv_day: 22.1, e_pv_1_day: 22.1, e_pv_2_day: 0.0, e_pv_3_day: 0.0, e_inv_day: 4.2, e_rec_day: 7.3, e_chg_day: 8.6, e_dischg_day: 5.2, e_eps_day: 0.0, e_to_grid_day: 3.6, e_to_user_day: 0.2, v_bus_1: 375.6, v_bus_2: 299.7 }
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'ReadInput2 data' do
|
82
|
+
let(:input) { [161, 26, 2, 0, 111, 0, 1, 194, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 97, 0, 1, 4, 88, 88, 88, 88, 88, 88, 88, 88, 88, 88, 40, 0, 80, 83, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 179, 38, 0, 0, 43, 45, 0, 0, 105, 51, 0, 0, 44, 47, 0, 0, 0, 0, 0, 0, 95, 5, 0, 0, 63, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 36, 0, 37, 0, 0, 0, 0, 0, 35, 136, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 188, 199] }
|
83
|
+
|
84
|
+
it { is_expected.to be_a LXP::Packet::ReadInput2 }
|
85
|
+
|
86
|
+
it 'has the correct attributes' do
|
87
|
+
expect(packet).to have_attributes register: 40,
|
88
|
+
values: [83, 69, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 179, 38, 0, 0, 43, 45, 0, 0, 105, 51, 0, 0, 44, 47, 0, 0, 0, 0, 0, 0, 95, 5, 0, 0, 63, 46, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 0, 36, 0, 37, 0, 0, 0, 0, 0, 35, 136, 224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
89
|
+
protocol: 2,
|
90
|
+
packet_length: 111, data_length: 97,
|
91
|
+
tcp_function: LXP::Packet::TcpFunctions::TRANSLATED_DATA,
|
92
|
+
device_function: LXP::Packet::DeviceFunctions::READ_INPUT,
|
93
|
+
inverter_serial: 'XXXXXXXXXX',
|
94
|
+
datalog_serial: 'cccccccccc',
|
95
|
+
bytes: input
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#to_h' do
|
99
|
+
subject { packet.to_h }
|
100
|
+
it { is_expected.to eq e_chg_all: 1316.1, e_dischg_all: 1207.6, e_eps_all: 0.0, e_inv_all: 990.7, e_pv_all: 1774.7, e_pv_1_all: 1774.7, e_pv_2_all: 0.0, e_pv_3_all: 0.0, e_rec_all: 1156.3, e_to_grid_all: 137.5, e_to_user_all: 1183.9, t_bat: 0, t_inner: 48, t_rad_1: 36, t_rad_2: 37, uptime: 14_714_915 }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe LXP::Packet::ReadHold do
|
4
|
+
let(:packet) { LXP::Packet::ReadHold.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
packet.inverter_serial = '1234567890'
|
8
|
+
packet.datalog_serial = '0987654321'
|
9
|
+
packet.register = 21
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'has the correct attributes' do
|
13
|
+
expect(packet).to have_attributes register: 21, values: [1, 0], value: 1,
|
14
|
+
to_h: { 21 => 1 },
|
15
|
+
protocol: 1,
|
16
|
+
packet_length: 32, data_length: 18,
|
17
|
+
tcp_function: LXP::Packet::TcpFunctions::TRANSLATED_DATA,
|
18
|
+
device_function: LXP::Packet::DeviceFunctions::READ_HOLD,
|
19
|
+
inverter_serial: '1234567890',
|
20
|
+
datalog_serial: '0987654321',
|
21
|
+
bytes: [161, 26, 1, 0, 32, 0, 1, 194, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 18, 0, 0, 3, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 21, 0, 1, 0, 241, 72]
|
22
|
+
expect(packet[21]).to eq 1
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'creates the correct binary' do
|
26
|
+
expect(packet.to_bin).to eq [161, 26, 1, 0, 32, 0, 1, 194, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 18, 0, 0, 3, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 21, 0, 1, 0, 241, 72].pack('C*')
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'works with protocol 2' do
|
30
|
+
packet.protocol = 2
|
31
|
+
expect(packet).to have_attributes protocol: 2,
|
32
|
+
bytes: [161, 26, 2, 0, 32, 0, 1, 194, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 18, 0, 0, 3, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 21, 0, 1, 0, 241, 72]
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe LXP::Packet::WriteSingle do
|
4
|
+
let(:packet) { LXP::Packet::WriteSingle.new }
|
5
|
+
|
6
|
+
before do
|
7
|
+
packet.inverter_serial = '1234567890'
|
8
|
+
packet.datalog_serial = '0987654321'
|
9
|
+
packet.register = 21
|
10
|
+
packet.value = 8000
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'has the correct attributes' do
|
14
|
+
expect(packet).to have_attributes register: 21, values: [64, 31], value: 8000,
|
15
|
+
protocol: 1,
|
16
|
+
packet_length: 32, data_length: 18,
|
17
|
+
tcp_function: LXP::Packet::TcpFunctions::TRANSLATED_DATA,
|
18
|
+
device_function: LXP::Packet::DeviceFunctions::WRITE_SINGLE,
|
19
|
+
inverter_serial: '1234567890',
|
20
|
+
datalog_serial: '0987654321',
|
21
|
+
bytes: [161, 26, 1, 0, 32, 0, 1, 194, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 18, 0, 0, 6, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 21, 0, 64, 31, 69, 211]
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'creates the correct binary' do
|
25
|
+
expect(packet.to_bin).to eq [161, 26, 1, 0, 32, 0, 1, 194, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 18, 0, 0, 6, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 21, 0, 64, 31, 69, 211].pack('C*')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'works with protocol 2' do
|
29
|
+
packet.protocol = 2
|
30
|
+
expect(packet).to have_attributes protocol: 2,
|
31
|
+
bytes: [161, 26, 2, 0, 32, 0, 1, 194, 48, 57, 56, 55, 54, 53, 52, 51, 50, 49, 18, 0, 0, 6, 49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 21, 0, 64, 31, 69, 211]
|
32
|
+
end
|
33
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lxp-packet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Elsworth
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -24,7 +24,21 @@ dependencies:
|
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '2.0'
|
27
|
-
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description:
|
28
42
|
email:
|
29
43
|
- chris@cae.me.uk
|
30
44
|
executables: []
|
@@ -32,6 +46,8 @@ extensions: []
|
|
32
46
|
extra_rdoc_files: []
|
33
47
|
files:
|
34
48
|
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- ".rubocop.yml"
|
35
51
|
- CHANGELOG.md
|
36
52
|
- Gemfile
|
37
53
|
- LICENSE.txt
|
@@ -55,13 +71,17 @@ files:
|
|
55
71
|
- lib/lxp/utils.rb
|
56
72
|
- lib/lxp/version.rb
|
57
73
|
- lxp-packet.gemspec
|
74
|
+
- spec/parser_spec.rb
|
75
|
+
- spec/read_hold_spec.rb
|
76
|
+
- spec/spec_helper.rb
|
77
|
+
- spec/write_single_spec.rb
|
58
78
|
homepage: https://github.com/celsworth/lxp-packet
|
59
79
|
licenses:
|
60
80
|
- MIT
|
61
81
|
metadata:
|
62
82
|
homepage_uri: https://github.com/celsworth/lxp-packet
|
63
83
|
source_code_uri: https://github.com/celsworth/lxp-packet
|
64
|
-
post_install_message:
|
84
|
+
post_install_message:
|
65
85
|
rdoc_options: []
|
66
86
|
require_paths:
|
67
87
|
- lib
|
@@ -76,9 +96,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
76
96
|
- !ruby/object:Gem::Version
|
77
97
|
version: '0'
|
78
98
|
requirements: []
|
79
|
-
|
80
|
-
|
81
|
-
signing_key:
|
99
|
+
rubygems_version: 3.1.4
|
100
|
+
signing_key:
|
82
101
|
specification_version: 4
|
83
102
|
summary: Library to generate and parse LuxPower inverter packets
|
84
103
|
test_files: []
|