mqttagent

MQTT Lua Agent
git clone https://git.instinctive.eu/mqttagent.git
Log | Files | Refs | README | LICENSE

commit 436eec498b0c1f69ca5aae995992faa7eaeaa819
parent edbee44ed701bc9eadf3de59c61353ee3b10c425
Author: Natasha Kerensikova <natgh@instinctive.eu>
Date:   Mon,  3 Feb 2025 19:23:32 +0000

Manual and examples
Diffstat:
MREADME.md | 249+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 249 insertions(+), 0 deletions(-)

diff --git a/README.md b/README.md @@ -18,3 +18,252 @@ It turns out that a Lua script is much easier for development (thanks to existing interpreters), for setup (thanks to a friendlier language), and for maintenance (thanks to basic logic not being scattered across a lot of small shell scripts). + +## Manual + +### Commands + +The provided commands provide varying levels of extra primitives bound +to the Lua environment. + +For now, the generated commands run `mqttagent.lua` in the current +directory and stay in the foreground. + +Once the script is initially run, the program runs until all connections +and all timers are finished (or the script calls `os.exit`). + +### MQTT client creation + +```lua +client = mqttclient.new(config) +``` + +It creates a new MQTT client, using a configuration table which +is deserializaed into [a `go-mqtt/mqtt.Config` +structure](https://pkg.go.dev/github.com/go-mqtt/mqtt#Config), +with the extra field `connection` used to make the `Dialer`. + +### MQTT reception callbacks + +The client has a table-like API to set or reset callbacks: + +```lua +-- Set a callback +client[topic_filer_string] = callback_function +-- Call or query an existing callback +client[topic_filter_string](self, mesasge, topic) +local f = client[topic_filter_string] +-- Reset a callback +client[topic_filter_string] = nil +``` + +The callbacks are called with four parameters: + + - the client object (`self`); + - the received message, as a string; + - the topic of the received message, as a string; + - the reception time, as a number in seconds (compatible with timers). + +Setting a new callback automatically subscribes to the filter, +and resetting the callback automaticall unsubscribes. + +Note that when subscribing to overlapping-but-different filters, messages +may be duplicated (i.e. when the overlap makes the broker send the message +twice, each callback will be called twice). +Since QoS is not supported yet, callbacks should always be ready to handle +multiple and missing messages anyway. + +### MQTT message sending + +The client has a function-like API to send messages: + +```lua +-- Send a ping to the server +client() +-- Send a message to the given topic +client(message, topic) +``` + +### Timers + +Timers are created using `timer.new` with a time and a callback: + +```lua +timer_obj = timer.new(t, callback) +``` + +Timers **are NOT** autoamtically repeated, the callback is called only +once, after `t`, and then the timer is destroyed unless explicitly +rescheduled: + +```lua +timer_obj:schedule(next_t) +``` + +To make repeating timers, explicitly call `self:schedule` within the +callback with the next time. + +When the time given in `timer.new` or `:schedule` is in the past, the +callback is called as soon as possible. + +The `:cancel` method is also available to de-schedule a timer without +destroying it. + +## Examples + +### Simplest client prints one message and leaves + +```lua +-- Create an anonymous client +local c = mqttclient.new({ connection = "127.0.0.1:1883" }) + +-- Subscribe to all topics under `test` +c["test/#"] = function(self, message, topic) + -- Print the received message + print(message) + -- Unsubscribe + c["test/#"] = nil +end +``` + +### Client with LWT, keepalive and timer + +```lua +-- Keep alive in seconds +local keepalive = 60 +-- Create the client +local c = mqttclient.new{ + connection = "127.0.0.1:1883", + user_name = "mqttagent", + password = "1234", + will = { + topic = "test/status/mqttagent", + message = "Offline", + }, + keep_alive = keepalive, +} + +-- Setup a ping timer for the keepalive +if keepalive > 0 then + -- Create a new timer, dropping the variable (`self` is enough) + timer.new(os.time() + keepalive, function(self, t) + -- Send a ping + c() + -- Schedule next keepalive + self:schedule(t + keepalive) + end) +end + +-- Print the next 10 messages send to `test` topic +local count = 10 +c["test"] = function(self, message, topic) + print(message) + count = count - 1 + if count <= 0 then + os.exit(0) + end +end + +-- Announce start +c("Online", "test/status/mqttagent") +``` + +### One-way MQTT Bridge + +```lua +-- Create the source client +local source = mqttclient.new{ + connection = "mqtt.example.com:1883", + user_name = "mqttagent", + password = "1234", +} +local dest = mqttclient.new{ "127.0.0.1:1883" } +source["#"] = function(self, message, topic) + dest(message, topic) +end +``` + +### RRD Update Using JSON Payload Form Tasmota Energy Sensor + +```lua +local json = require("json") +local c = mqttclient.new{ connection = "127.0.0.1:1883" } + +function millis(n) + if n then + return math.floor(n * 1000 + 0.5) + else + return "U" + end +end + +c["tele/+/SENSOR"] = function(self, message, topic) + -- Deduce file name from middle part of the topic + local name = string.sub(topic, 6, -8) + -- Sanity check + if string.find(name, "[^_%w%-]") then return end + -- Decode JSON payload + local obj, err = json.decode(message) + if err then + print(err) + return + end + if not obj.ENERGY then return end + + os.execute("rrdtool update \"" .. dbdir .. "/energy-sensor-" .. + name .. ".rrd\" N:" .. + millis(obj.ENERGY.Total) .. + ":" .. (obj.ENERGY.Period or "U") .. + ":" .. (obj.ENERGY.Power or "U") .. + ":" .. millis(obj.ENERGY.Factor) .. + ":" .. (obj.ENERGY.Voltage or "U") .. + ":" .. millis(obj.ENERGY.Current)) +end +``` + +### Backup Monitoring + +```lua +local c = mqttclient.new{ connection = "127.0.0.1:1883" } +-- List of topic suffix to check +local backups = { + ["dest1/src1"] = 0, + ["dest1/src2"] = 0, + ["dest2/src2"] = 0, + ["dest3/src1"] = 0, +} + +-- Check every day at 5:02:42 UTC that all backups have been done +function next_check() + local tbl = os.date("!*t") + tbl.hour = 5 + tbl.min = 2 + tbl.sec = 42 + return os.time(tbl) + 86400 +end + +timer.new(next_check(), function(self, t) + local msg = "" + -- Accumulate missing backup names in msg and reset the table + for k, v in paris(backups) do + if v == 0 then + msg = msg .. (#msg > 0 and ", " or "") .. k + end + backups[k] = 0 + end + -- Send an alert when a backup is missing + if #msg > 0 then + c("Missing: " .. msg, "alert/backup") + end + -- Check again tomorrow + self>schedule(t + 86400) +end) + +-- Mark backups as seen when notified +c["backup/#"] = function(self, _, topic, t) + local name = string.sub(topic, 8) + if backups[name] then + backups[name] = t + end +end +```