Add support for requesting input data on demand

Additional docs added in doc/MQ.md
This commit is contained in:
Chris Elsworth 2020-05-08 20:37:02 +01:00
parent cd00265522
commit a8380d24db
4 changed files with 111 additions and 40 deletions

View File

@ -6,7 +6,7 @@ ruby '>= 2.3.0'
gem 'zeitwerk'
gem 'inifile'
gem 'lxp-packet', '~> 0.6.0'
gem 'lxp-packet', '~> 0.7.0'
gem 'roda'

View File

@ -12,7 +12,7 @@ You can use mqtts for secure connections and add username/password; see the URL
## Getting Inverter Status
As mentioned in the README, the inverter sends power data when it feels like it. This is every 2 minutes. There's no way to change this or ask for it on demand.
As mentioned in the README, the inverter sends power data (also called inputs) when it feels like it. This is every 2 minutes.
It's sent by the inverter as 3 packets in sequence, around a second apart. When we receive each one, it is published under the keys `octolux/inputs/1`, `octolux/inputs/2`, and `octolux/inputs/3`. Each one always has the same set of data, and it should look something like this:
@ -33,6 +33,16 @@ Documenting all these is beyond the scope of this document, but broadly speaking
* `t_` are temperatures in celsius; internally, and both radiators on the back of the inverter
* not really worked out what all the `bat_status_` are yet as they're usually mostly 0
You can also request this information to be sent immediately with `octolux/cmd/read_input`, with a payload of 1, 2 or 3, depending on which set of inputs you want:
```
$ mosquitto_pub -t octolux/cmd/read_input -m 1
```
This will prompt a further MQ message of `octolux/inputs/1` (as above), and additionally `octolux/result/read_input` will be sent with `OK` when it's complete.
## Controlling the Inverter
`server.rb` will subscribe to a few topics that can be used for inverter control.
@ -83,3 +93,11 @@ This says the inverter has told us that register 65 now contains the value 50. I
So, clearly for now this requires your client code to know that register 65 is what will change in response to `discharge_pct`. For this reason, the `result` topics are probably more useful for now.
However, you could use these topics to record or graph every time a register changes, regardless of *how* it was changed, since these will be published even if MQ wasn't used to action the change (eg, via LuxPower's portal or app).
Finally, if you know the register you want and want it on-demand, you can send `octolux/cmd/read_hold` with an integer message:
```
$ mosquitto_pub -t octolux/cmd/read_hold -m 65
```
This will result in `octolux/hold/65` being sent (as above) and additionally `octolux/result/read_hold` will be sent (with `OK`) once complete.

View File

@ -18,6 +18,24 @@ class LuxController
@socket = nil
end
def read_input(num)
LOGGER.debug "read_input(#{num})"
type = case num
when 1 then LXP::Packet::ReadInput1
when 2 then LXP::Packet::ReadInput2
when 3 then LXP::Packet::ReadInput3
end
pkt = packet(type: type)
socket.write(pkt)
read_reply(pkt)
end
def read_hold(register)
read_register(register)
end
def charge(enable)
LOGGER.debug "charge(#{enable})"
update_register(21, LXP::Packet::RegisterBits::AC_CHARGE_ENABLE, enable)
@ -77,10 +95,7 @@ class LuxController
LOGGER.debug "read_register(#{register})"
pkt = packet(type: LXP::Packet::ReadHold, register: register)
socket.write(pkt)
unless (r = socket.read_reply(pkt))
LOGGER.fatal 'invalid/no reply from inverter'
raise SocketError
end
r = read_reply(pkt)
LOGGER.debug "read_register(#{register}) => #{r.value}"
@ -94,10 +109,7 @@ class LuxController
pkt.value = val
socket.write(pkt)
unless (r = socket.read_reply(pkt))
LOGGER.fatal 'invalid/no reply from inverter'
raise SocketError
end
r = read_reply(pkt)
LOGGER.debug "set_register(#{register}) => #{r.value}"
@ -108,11 +120,20 @@ class LuxController
@socket ||= LuxSocket.new(host: @host, port: @port)
end
def packet(type:, register:)
def packet(type:, register: nil)
type.new.tap do |pkt|
pkt.register = register
pkt.register = register if register
pkt.datalog_serial = @datalog
pkt.inverter_serial = @serial
end
end
def read_reply(pkt)
unless (r = socket.read_reply(pkt))
LOGGER.fatal 'invalid/no reply from inverter'
raise SocketError
end
r
end
end

View File

@ -5,35 +5,13 @@ require 'mqtt/sub_handler'
class MQ
class << self
def run
sub.subscribe_to 'octolux/cmd/ac_charge' do |data|
LOGGER.info "MQ cmd/ac_charge => #{data}"
r = lux_controller.charge(bool(data))
lux_controller.close
sub.publish_to('octolux/result/ac_charge', r ? 'OK' : 'FAIL')
end
sub.subscribe_to 'octolux/cmd/read_hold', &method(:read_hold_cb)
sub.subscribe_to 'octolux/cmd/read_input', &method(:read_input_cb)
sub.subscribe_to 'octolux/cmd/forced_discharge' do |data|
LOGGER.info "MQ cmd/forced_discharge => #{data}"
r = lux_controller.discharge(bool(data))
lux_controller.close
sub.publish_to('octolux/result/forced_discharge', r ? 'OK' : 'FAIL')
end
sub.subscribe_to 'octolux/cmd/charge_pct' do |data|
LOGGER.info "MQ cmd/charge_pct => #{data}"
r = (lux_controller.charge_pct = data.to_i)
lux_controller.close
sub.publish_to('octolux/result/charge_pct',
r == data.to_i ? 'OK' : 'FAIL')
end
sub.subscribe_to 'octolux/cmd/discharge_pct' do |data|
LOGGER.info "MQ cmd/discharge_pct => #{data}"
r = (lux_controller.discharge_pct = data.to_i)
lux_controller.close
sub.publish_to('octolux/result/discharge_pct',
r == data.to_i ? 'OK' : 'FAIL')
end
sub.subscribe_to 'octolux/cmd/ac_charge', &method(:ac_charge_cb)
sub.subscribe_to 'octolux/cmd/forced_discharge', &method(:forced_discharge_cb)
sub.subscribe_to 'octolux/cmd/charge_pct', &method(:charge_pct_cb)
sub.subscribe_to 'octolux/cmd/discharge_pct', &method(:discharge_pct_cb)
Thread.stop # sleep forever
end
@ -44,6 +22,60 @@ class MQ
private
def read_hold_cb(data, *)
LOGGER.info "MQ cmd/read_hold => #{data}"
lux_controller.read_hold(data.to_i)
lux_controller.close
sub.publish_to('octolux/result/read_hold', 'OK')
rescue LuxController::SocketError
sub.publish_to('octolux/result/read_hold', 'FAIL')
end
def read_input_cb(data, *)
LOGGER.info "MQ cmd/read_input => #{data}"
lux_controller.read_input(data.to_i)
lux_controller.close
sub.publish_to('octolux/result/read_input', 'OK')
rescue LuxController::SocketError
sub.publish_to('octolux/result/read_input', 'FAIL')
end
def ac_charge_cb(data, *)
LOGGER.info "MQ cmd/ac_charge => #{data}"
r = lux_controller.charge(bool(data))
lux_controller.close
sub.publish_to('octolux/result/ac_charge', r ? 'OK' : 'FAIL')
rescue LuxController::SocketError
sub.publish_to('octolux/result/ac_charge', 'FAIL')
end
def forced_discharge_cb(data, *)
LOGGER.info "MQ cmd/forced_discharge => #{data}"
r = lux_controller.discharge(bool(data))
lux_controller.close
sub.publish_to('octolux/result/forced_discharge', r ? 'OK' : 'FAIL')
rescue LuxController::SocketError
sub.publish_to('octolux/result/forced_discharge', 'FAIL')
end
def charge_pct_cb(data, *)
LOGGER.info "MQ cmd/charge_pct => #{data}"
r = (lux_controller.charge_pct = data.to_i)
lux_controller.close
sub.publish_to('octolux/result/charge_pct', r == data.to_i ? 'OK' : 'FAIL')
rescue LuxController::SocketError
sub.publish_to('octolux/result/charge_pct', 'FAIL')
end
def discharge_pct_cb(data, *)
LOGGER.info "MQ cmd/discharge_pct => #{data}"
r = (lux_controller.discharge_pct = data.to_i)
lux_controller.close
sub.publish_to('octolux/result/discharge_pct', r == data.to_i ? 'OK' : 'FAIL')
rescue LuxController::SocketError
sub.publish_to('octolux/result/discharge_pct', 'FAIL')
end
def uri
CONFIG['mqtt']['uri']
end