lxp-packet 0.6.0 → 0.7.0

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: 8be850807f4bf009125b77a32fc9e6572575e9b82a40039cd9898a55e0e27b9c
4
- data.tar.gz: efc7f72f2e855500f90ce223977909956c21edbe372e4c29dbe52017ee1599dc
3
+ metadata.gz: ddb052890f90bbf20bf9ff7fd399dcb1f2a7211612c53b7cc2e98a57cf5c69a1
4
+ data.tar.gz: 7234113e5decb4dad0581089b72277b138c7140af0105bd4231284342f187997
5
5
  SHA512:
6
- metadata.gz: 4d06b6bd60c3fff448a629e6732b9edb7a7f1b559d1c88c61420cf4b17e1c4f48e0489d0052a72442e841085295e7ac794630d97264f55194b3546d0b1fcba7a
7
- data.tar.gz: f9e98b57883625a567cf3d41162f3955b1bc9c311f816ce3fe40170155315e9f0f6bc6e7e2c2b9a2fcb36def46c4ea567f99c93f540803a7bc91a8de7b261e72
6
+ metadata.gz: 824355382e38cc49be2c1de0a9a7d875d8975bd4993156565d7dfa8b0cfa24c042deed6d6d4ab87d5a6c42b1e88f52ad7c61eaaa4c912a6c18f7a1ccb1eb0519
7
+ data.tar.gz: 11d143b9f00cce2cb821216e2f64d961e595d8b52451321cf2ce2073f8d4666f33c6e0d2c36cc21761c4e849a4ca0ff3dfd192ef0f08f15cc0beb1ce5a4c1e35
@@ -5,6 +5,13 @@ 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
+ ## [0.7.0] - 2020-05-08
9
+
10
+ ### Added
11
+
12
+ - support for requesting input registers on demand
13
+ - support for sending heartbeats
14
+
8
15
  ## [0.6.0] - 2020-04-10
9
16
 
10
17
  ### Added
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,9 +104,17 @@ r = read_reply(sock, pkt)
96
104
  puts "Received: #{r.value}" # should be discharge cut-off value
97
105
  ```
98
106
 
99
- 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. [TODO: work out how to request these packets ourselves]
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.
100
108
 
101
- To access these, 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.
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.
102
118
 
103
119
  ```ruby
104
120
  # assuming pkt is a parsed packet with multiple registers/values:
@@ -111,6 +127,26 @@ pkt[22] # => nil
111
127
  pkt.to_h # { 21 => 62292 }
112
128
  ```
113
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
+
114
150
 
115
151
  ### Writing
116
152
 
@@ -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,7 +8,16 @@ 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
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
12
21
  2-8 SERIAL_NUM
13
22
  7 FW_CODE ?
14
23
  9-10 ?
@@ -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
@@ -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
@@ -17,10 +17,18 @@ class LXP
17
17
  #
18
18
  class Heartbeat < Base
19
19
  def initialize
20
+ @header = [0] * 19
21
+
20
22
  super
21
23
 
24
+ self.packet_length = 13
25
+ self.protocol = 2
22
26
  self.tcp_function = TcpFunctions::HEARTBEAT
23
27
  end
28
+
29
+ def bytes
30
+ header
31
+ end
24
32
  end
25
33
  end
26
34
  end
@@ -12,9 +12,9 @@ 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
 
@@ -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,6 +5,13 @@ 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
  {
@@ -5,6 +5,13 @@ 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
  {
@@ -38,6 +45,8 @@ class LXP
38
45
 
39
46
  # 71..72 ?
40
47
 
48
+ # this actually seems to be cumulative runtime.
49
+ # not found an uptime since reboot yet.
41
50
  uptime: Utils.int(@data[73, 4]) # seconds
42
51
  }
43
52
  end
@@ -5,6 +5,13 @@ 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
  {
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class LXP
4
- VERSION = '0.6.0'
4
+ VERSION = '0.7.0'
5
5
  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.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chris Elsworth
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-10 00:00:00.000000000 Z
11
+ date: 2020-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -96,8 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
98
  requirements: []
99
- rubyforge_project:
100
- rubygems_version: 2.7.6.2
99
+ rubygems_version: 3.0.3
101
100
  signing_key:
102
101
  specification_version: 4
103
102
  summary: Library to generate and parse LuxPower inverter packets