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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64f2d92693be97e362ae4d3485fa4772bc813a9271d0fd6aa90d60dad103fe31
4
- data.tar.gz: c55207ae772cbe564f7891d634547209d8e022e545711dbe5813138cd53de722
3
+ metadata.gz: '096a5bb502e1d685696a0a14e483d59ddc8669bc8bda9c785dba152180d7ee9e'
4
+ data.tar.gz: 6448359ba7d17210b71eaf6173943d71d28ec37f243e544899efee05b8341ae0
5
5
  SHA512:
6
- metadata.gz: 9ae5ae4c8df6d890bf75ec7280c598d2fb29c48977b7ffb256163b105566f73796b915c631388dea19c7b59aad2305720f7e0db7173bf0373d9bef0299d1b92f
7
- data.tar.gz: d9290a541c2489825f76a259b467779dfc5b20f44b156a1f645d98b3515e3ab6ff1764ba99e3aa8733af07db0cae3f68f4d31b7f56da48cc32cfabee349a5c89
6
+ metadata.gz: 3f483b86c1e92673f5c6d0f48da17db156e458af250ca7fe598fc5fec18f27c1a2c8dd8fb795192685b05497b385063a65b0db2f3d21131ca93c48fb313ac870
7
+ data.tar.gz: e3f9c800854a950d28ffb7e86c92a3bbaa60e53892a274489ccb9f774ae54818d177b373c4d48950f23787a7a85ca9555c161c575a6a2139705d3ad47f602235
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,2 @@
1
+ Metrics/LineLength:
2
+ Enabled: no
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
- ## [Unreleased]
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 something from the inverter.
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 multi-byte register
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 tored in register 21.
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 or heartbeat number?
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 the number
37
- of bytes that follows. It's sometimes easier to think of the dataframe as
38
- its own structure so here are byte offsets in both the dataframe and the entire
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 seems to need to be 1.
60
-
61
- Register and value are also referred to as start address and point number;
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
@@ -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
- 2 SERIAL_NUM
13
- 7 FW_CODE
14
- 12 TIME
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
@@ -16,9 +16,9 @@ class LXP
16
16
  attr_accessor :chksum
17
17
 
18
18
  def initialize
19
- @header = [0] * 20
20
- @data = [0] * 16
21
- @chksum = [0, 0]
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
- # So this can return an int or an array.
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]
@@ -7,6 +7,12 @@ class LXP
7
7
  READ_INPUT = 4
8
8
  WRITE_SINGLE = 6
9
9
  WRITE_MULTI = 16
10
+
11
+ # not handled yet
12
+ READ_HOLD_ERROR = 131
13
+ READ_INPUT_ERROR = 132
14
+ WRITE_SINGLE_ERROR = 134
15
+ WRITE_MULTI_ERROR = 144
10
16
  end
11
17
  end
12
18
  end
@@ -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
@@ -12,24 +12,42 @@ class LXP
12
12
  self.device_function = DeviceFunctions::READ_HOLD
13
13
 
14
14
  self.data_length = 18
15
- # start by assuming this packet will be sent to the inverter.
16
- # we need to put a 1 in the value to get the inverter to
17
- # populate the reply.
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
- # ReadHold packets should always (I think) have two byte values.
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
- # Raise if not, as that is not expected?
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
- # Base#value will return an int for protocol 1, or an Array
26
- # for protocol 2. If we can, convert that Array to an int.
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 value
29
- raise 'value_length not 2?' unless value_length == 2
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
- r = super
32
- r.is_a?(Array) ? Utils.int(r, 2) : r
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
- status: @data[15],
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], :lsb) / 10.0, # V
29
+ v_bat: Utils.int(@data[23, 2]) / 10.0, # V
14
30
  soc: @data[25], # %
31
+ # 26 used for anything?
15
32
 
16
- # 26..28 ?
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
- p_pv: Utils.int(@data[29, 2], :lsb), # W
19
- p_charge: Utils.int(@data[35, 2], :lsb), # W
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
- p_inv: Utils.int(@data[47, 2], :lsb), # W
25
- p_rec: Utils.int(@data[49, 2], :lsb), # W
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
- # 51..54 ?
55
+ p_inv: Utils.int(@data[47, 2]), # W
56
+ p_rec: Utils.int(@data[49, 2]), # W
28
57
 
29
- v_eps: Utils.int(@data[55, 2], :lsb) / 10.0, # V
30
- f_eps: Utils.int(@data[61, 2], :lsb) / 100.0, # Hz
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], :lsb), # W
35
- p_to_user: Utils.int(@data[69, 2], :lsb), # W
36
-
37
- e_pv_day: Utils.int(@data[71, 2], :lsb) / 10.0, # kWh
38
- # 73..76 ?
39
- e_inv_day: Utils.int(@data[77, 2], :lsb) / 10.0, # kWh
40
- e_rec_day: Utils.int(@data[79, 2], :lsb) / 10.0, # kWh
41
- e_chg_day: Utils.int(@data[81, 2], :lsb) / 10.0, # kWh
42
- e_dischg_day: Utils.int(@data[83, 2], :lsb) / 10.0, # kWh
43
- e_eps_day: Utils.int(@data[85, 2], :lsb) / 10.0, # kWh
44
- e_to_grid_day: Utils.int(@data[87, 2], :lsb) / 10.0, # kWh
45
- e_to_user_day: Utils.int(@data[89, 2], :lsb) / 10.0, # kWh
46
-
47
- v_bus_1: Utils.int(@data[91, 2], :lsb) / 10.0, # V
48
- v_bus_2: Utils.int(@data[93, 2], :lsb) / 10.0 # V
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
- e_pv_all: Utils.int(@data[15, 4], :lsb) / 10.0, # kWh
12
- e_inv_all: Utils.int(@data[27, 4], :lsb) / 10.0, # kWh
13
- e_rec_all: Utils.int(@data[31, 4], :lsb) / 10.0, # kWh
14
- e_chg_all: Utils.int(@data[35, 4], :lsb) / 10.0, # kWh
15
- e_dischg_all: Utils.int(@data[39, 4], :lsb) / 10.0, # kWh
16
- e_eps_all: Utils.int(@data[43, 4], :lsb) / 10.0, # kWh
17
- e_to_grid_all: Utils.int(@data[47, 4], :lsb) / 10.0, # kWh
18
- e_to_user_all: Utils.int(@data[51, 4], :lsb) / 10.0, # kWh
19
-
20
- t_inner: Utils.int(@data[62, 2], :msb),
21
- t_rad_1: Utils.int(@data[64, 2], :msb),
22
- t_rad_2: Utils.int(@data[66, 2], :msb)
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
- max_chg_curr: Utils.int(@data[17, 2], :lsb) / 100.0, # A
12
- max_dischg_curr: Utils.int(@data[19, 2], :lsb) / 100.0, # A
13
- charge_volt_ref: Utils.int(@data[21, 2], :lsb) / 10.0, # V
14
- dischg_cut_volt: Utils.int(@data[23, 2], :lsb) / 10.0, # V
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
@@ -12,6 +12,9 @@ class LXP
12
12
  # Grid Charge Power Rate (%)
13
13
  AC_CHARGE_POWER_CMD = 66
14
14
 
15
+ # AC Charge SOC Limit (%)
16
+ AC_CHARGE_SOC_LIMIT = 67
17
+
15
18
  # Discharge cut-off SOC (%)
16
19
  DISCHG_CUT_OFF_SOC_EOD = 105
17
20
  end
@@ -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
- r = super
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, order = :lsb)
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, order = :lsb)
16
- r = int(bytes, order)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class LXP
4
- VERSION = '0.3.0'
4
+ VERSION = '0.7.1'
5
5
  end
data/lxp-packet.gemspec CHANGED
@@ -21,4 +21,5 @@ Gem::Specification.new do |s|
21
21
  s.require_paths = ['lib']
22
22
 
23
23
  s.add_development_dependency 'bundler', '~> 2.0'
24
+ s.add_development_dependency 'rspec'
24
25
  end
@@ -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
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lxp/packet'
@@ -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.3.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: 2020-04-02 00:00:00.000000000 Z
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
- description:
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
- rubyforge_project:
80
- rubygems_version: 2.7.6.2
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: []