Git Product home page Git Product logo

lxp-packet's Introduction

LXP Communications Library

This is a Ruby gem to parse and generate data packets from/to a LuxPower inverter. This is tested with my ACS3600 model but may work with others.

Unfortunately LuxPower refuse to release API documentation for this inverter, so this is all reverse engineered. Use with care.

That said, I've managed to work out quite a bit of it. You can parse "input" packets (via ReadInput) from the inverter (which are the data sent in 3 packets at 2 minute intervals, concerning energy flows and states), request settings (via ReadHold) for a specific register, and then modify and write those settings back (via WriteSingle).

There's a WriteMulti too but I've not used that yet. It may be used for setting the time, but I'm not interested in that so didn't bother working it out.

When you send the inverter a read/write packet, it sends a reply with the same register you sent it, and in the case of write packets, the updated value. This can be used to check the write has taken effect.

Note that the replies appear to be sent to all connected clients. If you updating a setting then the reply containing the new updated value is sent to all connected clients (including Lux in China, as per the default inverter configuration). They probably just ignore it as being unexpected; but this is partly the reason there's a read_reply method below which ignores packets until we get the one we expect.

See the docs in doc/ for a list of registers and my notes on how the packets are constructed. Please remember there may be errors so test carefully before letting any of this loose on your own kit. To repeat, this is all detective work by myself without the assistance of any official docs.

Also note that lib/lxp/packet/registers.rb only contains registers I've used, not all of them. I'll happily accept PRs adding new ones.

This library uses semantic versioning, and accordingly I currently make no guarantees about preserving backwards compatibility. Lock your gem version if this might affect you!

Install

Standard Ruby fare in your Gemfile:

gem 'lxp-packet'

Then require the base library when you want it:

require 'lxp/packet'

Setup

You obviously need the WiFi datalogger module in your inverter to use this. Mine came with it as standard.

The datalogger by default sends information about itself to LuxPower (see Connection 1 below) every 2 minutes. It connects to Lux at the IP shown below. This is how their portal gets information about you, and how they can send your inverter commands over the open channel.

It can optionally be configured with a second network endpoint; I set this to TCP Server with a port of 4346, which means you can connect to the inverter on that port and get the same information sent to you. You can also send it commands. So the "Network Setting" page of my inverter looks like this:

LXP ACS Network Settings

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.

Inverter Fundamentals

The inverter has two basic sets of information.

There are 114 registers (0-113), which are also referred to as "holdings". See 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.

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.

Examples

The inverter requires that your datalog serial and inverter serial are in the packets you send to it for it to respond.

These are set like so; this will not be repeated in all subsequent examples.

pkt = LXP::Packet::ReadHold.new
pkt.datalog_serial = 'AB12345678'
pkt.inverter_serial = '1234567890'

There's also a commonly-used loop that I'll refer to. This reads input from the socket until it gets a decoded packet which matches the packet register we've previously sent to the inverter, ignoring heartbeats and other data.

This is extremely rudimentary but you get the idea. It should really have a timeout so it doesn't block forever.

def read_reply(sock, pkt)
  loop do
    input = sock.recvfrom(2000)[0]
    # puts "IN: #{input.unpack('C*')}"
    r = LXP::Packet::Parser.parse(input)
    return r if r.is_a?(pkt.class) && r.register == pkt.register
  end
end

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).

Reading Holding Registers

This is the simplest use-case; read the value of a holding register from the inverter.

pkt = LXP::Packet::ReadHold.new
pkt.datalog_serial = 'AB12345678'
pkt.inverter_serial = '1234567890'
pkt.register = LXP::Packet::Registers::DISCHG_CUT_OFF_SOC_EOD

# pkt.bytes returns an array of integers if you want to inspect what we'll send

# pack the integers into a binary packet to send to a socket
out = pkt.to_bin

# assuming your inverter is at 192.168.0.30
sock = TCPSocket.new('192.168.0.30', 4346)
sock.write(out)

r = read_reply(sock, pkt)
puts "Received: #{r.value}" # should be discharge cut-off value

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.

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:

# get registers 0 through 22 inclusive:
pkt.register = 0
pkt.value = 23

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.

# assuming pkt is a parsed packet with multiple registers/values:
pkt[0] # => 35462 # value of register 0
pkt.to_h # { 0 => 35462, 1 => 1, ... }

# assuming pkt is a parsed packet with only register 21:
pkt[21] # => value of register 21
pkt[22] # => nil
pkt.to_h # { 21 => 62292 }

Reading Input Registers

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.

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:

pkt = LXP::Packet::ReadInput1.new
pkt.datalog_serial = 'AB12345678'
pkt.inverter_serial = '1234567890'

# assuming your inverter is at 192.168.0.30
sock = TCPSocket.new('192.168.0.30', 4346)
sock.write(out)

r = read_reply(sock, pkt)
# r is a populated ReadInput1, which responds to #to_h:
r.to_h # => {:status=>16, :soc=>79, ... }

Writing

Updating a value on the inverter.

pkt = LXP::Packet::WriteSingle.new
# set serials like above..

# set discharge cutoff to 20%
pkt.register = LXP::Packet::Registers::DISCHG_CUT_OFF_SOC_EOD
pkt.value = 20

# pack the integers into a binary packet to send to a socket
out = pkt.to_bin

# assuming your inverter is at 192.168.0.30
sock = TCPSocket.new('192.168.0.30', 4346)
sock.write(out)

r = read_reply(sock, pkt)
puts "Received: #{r.value}" # should be new discharge cut-off value, 20

Updating a bitwise register

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.

This library combines them into a 16bit word, so that the constants in LXP::Packet::RegisterBits can be applied directly to those values.

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.

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.

It could be improved not to bother doing the write if it was already enabled.

sock = TCPSocket.new('192.168.0.30', 4346)

pkt = LXP::Packet::ReadHold.new
# serials..

pkt.register = 21
sock.write(pkt.to_bin)

r = read_reply(sock, pkt)
# r.value is a 16bit integer

pkt = LXP::Packet::WriteSingle.new
# serials..

pkt.register = 21

# enable AC charge by ORing it with the previous value
pkt.value = r.value | LXP::Packet::RegisterBits::AC_CHARGE_ENABLE

# or maybe you want to disable AC charge
# pkt.value = r.value & ~LXP::Packet::RegisterBits::AC_CHARGE_ENABLE

sock.write(pkt.to_bin)

r = read_reply(sock, pkt)
# now r.value should be the updated 16bit integer

lxp-packet's People

Contributors

celsworth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

lxp-packet's Issues

Byte indices in read_input1.rb

The byte indices in read_input1.rb seem to be wrong.

Line 20, 21:

# 2..12 = serial
# 13/14 = length

The serial number is 10 not 11 bytes so it's 2 to 11 and the second length value is bytes 18 and 19 in the whole message which you call 12 and 13 (it's really confusing that you renumber the bytes).

Here is an example message

Index Index-6 Value Int16
    0          A1        Prefix
    1          1A        Prefix
    2           2        Protocol version
    3           0        Protocol version
    4          6F        Message length (low byte) 111
    5           0        Message length (high byte)
    6     0     1        Address 
    7     1    C2        TCP function
    8     2    42        SN
    9     3    **        SN
   10     4    **        SN
   11     5    **        SN
   12     6    **        SN
   13     7    **        SN
   14     8    **        SN
   15     9    **        SN
   16    10    **        SN
   17    11    30        SN

   18    12    61    97  Length 111 - 97 = 14 makes no sense
   19    13     0   256

   20    14     1  1025 Status?
   21    15     4 12804
   22    16    32 12850 PV1?
   23    17    32 13362
   24    18    34 12852 PV2?
   25    19    32 12338
   26    20    30 12848 PV3?
   27    21    32 13874
   28    22    36 12342 v_batt?
   29    23    30 13616
   30    24    35 12341 SOC?
   31    25    30 20528
   32    26    50    80
   33    27     0 20480
   34    28    50  6736
   35    29    1A    26
   36    30     0  4096
   37    31    10 10000
   38    32    27  4135
   39    33    10 10000
   40    34    27 16423
   41    35    40   576
   42    36     2 36866
   43    37    90   400
   44    38     1     1
   45    39     0     0
   46    40     0     0
   47    41     0     0
   48    42     0     0
   49    43     0     0
   50    44     0     0
   51    45     0     0
   52    46     0     0
   53    47     0     0
   54    48     0     0
   55    49     0     0
   56    50     0     0
   57    51     0     0
   58    52     0     0
   59    53     0     0
   60    54     0     0
   61    55     0     0
   62    56     0     0
   63    57     0     0
   64    58     0   768
   65    59     3     3
   66    60     0   768
   67    61     3     3
   68    62     0 48384
   69    63    BD   189
   70    64     0  2560
   71    65     A  1290
   72    66     5     5
   73    67     0     0
   74    68     0     0
   75    69     0     0
   76    70     0  2304
   77    71     9  3337
   78    72     D   525
   79    73     2  3330
   80    74     D 53773
   81    75    D2   210
   82    76     0 48640
   83    77    BE   190
   84    78     0     0
   85    79     0     0
   86    80     0 38400
   87    81    96   406
   88    82     1  5633
   89    83    16   534
   90    84     2     2
   91    85     0     0
   92    86     0     0
   93    87     0     0
   94    88     0     0
   95    89     0     0
   96    90     0     0
   97    91     0     0
   98    92     0     0
   99    93     0     0
  100    94     0     0
  101    95     0     0
  102    96     0     0
  103    97     0     0
  104    98     0 12800
  105    99    32 12850
  106   100    32 13362
  107   101    34 12852
  108   102    32 12338
  109   103    30 12848
  110   104    32 13874
  111   105    36 12342
  112   106    30 13616
  113   107    35 12341
  114   108    30 60464
  115   109    EC 10732
  116   110    29

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.