MQTT#

vCLU obsługuje MQTT na dwa sposoby:

  1. Klient MQTT - vCLU łączy się z zewnętrznym brokerem (np. Mosquitto) i publikuje/odbiera wiadomości
  2. Wbudowany broker MQTT - vCLU sam jest brokerem, inne urządzenia (Tasmota, Shelly, ESPHome) łączą się do niego

Oba tryby mogą działać jednocześnie.

Architektura#

 Urządzenia zewnętrzne                    vCLU
 (Tasmota, Shelly...)                ┌──────────────────────┐
        │                            │                      │
        │ TCP :1883                  │  Wbudowany broker    │
        └───────────────────────────▶│  (mochi-mqtt)        │
                                     │       ▲              │
                                     │       │ localhost     │
                                     │  ┌────┴─────────┐    │
  Zewnętrzny broker                  │  │ Klient MQTT  │    │
  (Mosquitto, EMQX...)              │  │ (paho)       │    │
        ▲                            │  └──────────────┘    │
        │ TCP                        │                      │
        └────────────────────────────┤  Lua runtime         │
                                     └──────────────────────┘

Klient MQTT - vCLU jako klient#

Tworzenie i konfiguracja#

-- Utwórz nową instancję klienta
local mqtt = MQTT:new("moj_klient")

-- Konfiguracja brokera
mqtt:setHost("192.168.1.100")  -- adres brokera (automatycznie dodaje tcp://)
mqtt:setPort(1883)              -- port (domyślnie 1883)
mqtt:setClientId("vclu_salon")  -- unikalna nazwa klienta

-- Opcjonalna autoryzacja
mqtt:setCredentials("user", "haslo")

-- Opcjonalne ustawienia
mqtt:setKeepAlive(60)                  -- keep-alive w sekundach
mqtt:setAutoReconnect(true, 5000)      -- auto-reconnect co 5s
mqtt:setTopicPrefix("dom/vclu")        -- prefix dodawany do każdego topica
mqtt:setQoS(0)                         -- domyślny QoS (0, 1 lub 2)

Metody konfiguracyjne wspierają chaining:

local mqtt = MQTT:new("klient1")
mqtt:setHost("192.168.1.100")
    :setPort(1883)
    :setClientId("vclu_1")
    :setCredentials("user", "pass")

Połączenie#

if mqtt:connect() then
    print("Połączono z brokerem!")
else
    print("Nie udało się połączyć")
end

-- Sprawdź status
print(mqtt:isConnected())  -- true/false

Wysyłanie wiadomości (publish)#

-- Podstawowe wysłanie
mqtt:publish("salon/lampa/status", "ON")

-- Z parametrami QoS i retain
mqtt:publish("salon/temperatura", "22.5", 1, true)
--                                        QoS  retain

-- Tabela Lua jest automatycznie zamieniana na JSON
mqtt:publish("salon/czujnik", {
    temperature = 22.5,
    humidity = 45,
    battery = 98
})
-- Wysyła: {"temperature":22.5,"humidity":45,"battery":98}
Jeśli ustawiono `setTopicPrefix("dom/vclu")`, topic `"salon/lampa"` zostanie wysłany jako `"dom/vclu/salon/lampa"`.

Odbieranie wiadomości (subscribe)#

-- Subskrybuj topic z callbackiem
mqtt:subscribe("salon/przycisk/akcja", function(topic, payload, qos, retain)
    print("Odebrano: " .. topic .. " = " .. payload)

    if payload == "ON" then
        _:get("CLU.DOUT1"):execute(DOUT.METHOD_SWITCH_ON)
    elseif payload == "OFF" then
        _:get("CLU.DOUT1"):execute(DOUT.METHOD_SWITCH_OFF)
    end
end)

-- Wildcard "+" - jeden poziom
mqtt:subscribe("dom/+/temperatura", function(topic, payload)
    -- Dopasuje: dom/salon/temperatura, dom/kuchnia/temperatura
    print(topic .. " = " .. payload)
end)

-- Wildcard "#" - zero lub więcej poziomów (tylko na końcu)
mqtt:subscribe("dom/#", function(topic, payload)
    -- Dopasuje: dom/salon/lampa, dom/kuchnia/temp, dom/x/y/z
    print(topic .. " = " .. payload)
end)

-- Rezygnacja z subskrypcji
mqtt:unsubscribe("salon/przycisk/akcja")

Eventy#

mqtt:onConnect(function()
    print("Połączono!")
    -- Typowe: po połączeniu odnów subskrypcje
    mqtt:subscribe("dom/#", handler)
end)

mqtt:onDisconnect(function()
    print("Rozłączono!")
end)

-- Globalny handler - otrzymuje WSZYSTKIE wiadomości (niezależnie od subscribe)
mqtt:onMessage(function(topic, payload, qos, retain)
    log.debug("MQTT: " .. topic .. " = " .. payload)
end)

mqtt:onError(function(err)
    log.error("MQTT błąd: " .. tostring(err))
end)

Rozłączenie#

mqtt:disconnect()

Wbudowany broker MQTT - vCLU jako serwer#

Wbudowany broker pozwala urządzeniom IoT (Tasmota, Shelly, ESPHome) łączyć się bezpośrednio z vCLU bez zewnętrznego brokera.

Konfiguracja#

Broker konfiguruje się w panelu webowym (/mqtt) lub przez API:

curl -X POST http://vclu:3000/api/mqtt/broker/save \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "port": 1883,
    "wsPort": 0,
    "username": "mqtt",
    "password": "tajne"
  }'
ParametrOpisDomyślnie
enabledWłącz brokerfalse
portPort TCP1883
wsPortPort WebSocket (0 = wyłączony)0
usernameNazwa użytkownika (pusty = bez autoryzacji)""
passwordHasło""

Konfiguracja zapisywana jest w .vclu.json:

{
  "mqttBroker": {
    "enabled": true,
    "port": 1883,
    "username": "mqtt",
    "password": "tajne"
  }
}

Łączenie z wbudowanym brokerem z Lua#

Aby odebrać wiadomości od urządzeń podłączonych do wbudowanego brokera, utwórz klienta MQTT wskazującego na localhost:

local mqtt = MQTT:new("lokalny")
mqtt:setHost("localhost")
mqtt:setPort(1883)
mqtt:setCredentials("mqtt", "tajne")  -- te same dane co w konfiguracji brokera
mqtt:connect()

Praktyczne przykłady#

Tasmota - sterowanie przekaźnikiem#

Tasmota publikuje stan na stat/<nazwa>/POWER i przyjmuje komendy na cmnd/<nazwa>/POWER.

local mqtt = MQTT:new("tasmota")
mqtt:setHost("localhost")  -- wbudowany broker
mqtt:setPort(1883)
mqtt:setCredentials("mqtt", "tajne")
mqtt:connect()

-- Odbierz stan z Tasmota
mqtt:subscribe("stat/gniazdko1/POWER", function(topic, payload)
    if payload == "ON" then
        log.info("Gniazdko włączone")
    else
        log.info("Gniazdko wyłączone")
    end
end)

-- Wyślij komendę do Tasmota
mqtt:publish("cmnd/gniazdko1/POWER", "TOGGLE")   -- przełącz
mqtt:publish("cmnd/gniazdko1/POWER", "ON")        -- włącz
mqtt:publish("cmnd/gniazdko1/POWER", "OFF")       -- wyłącz

Shelly - odczyt temperatury#

local mqtt = MQTT:new("shelly")
mqtt:setHost("localhost")
mqtt:setPort(1883)
mqtt:connect()

mqtt:subscribe("shellies/shelly-ht/sensor/temperature", function(topic, payload)
    local temp = tonumber(payload)
    log.info("Temperatura: " .. temp .. "°C")

    -- Reaguj na temperaturę
    if temp > 25 then
        _:get("CLU.DOUT2"):execute(DOUT.METHOD_SWITCH_ON)  -- włącz wentylator
    end
end)

Mostek między dwoma brokerami#

vCLU jako klient zewnętrznego brokera + wbudowany broker dla urządzeń lokalnych:

-- Klient do centralnego brokera w sieci (np. Mosquitto na NAS)
local central = MQTT:new("central")
central:setHost("192.168.1.50")
central:setPort(1883)
central:connect()

-- Klient do lokalnego wbudowanego brokera (urządzenia Tasmota/Shelly)
local local_mqtt = MQTT:new("local")
local_mqtt:setHost("localhost")
local_mqtt:setPort(1883)
local_mqtt:connect()

-- Lokalne urządzenia → centralny broker
-- Stany z Tasmota/Shelly trafiają do centralnego brokera z prefixem "vclu/"
local_mqtt:subscribe("stat/#", function(topic, payload)
    central:publish("vclu/" .. topic, payload)
end)

-- Centralny broker → lokalne urządzenia
-- Komendy wysłane na centralny broker trafiają do lokalnych urządzeń
central:subscribe("vclu/cmnd/#", function(topic, payload)
    -- Usuń prefix "vclu/" i przekaż do lokalnego brokera
    local localTopic = topic:gsub("^vclu/", "")
    local_mqtt:publish(localTopic, payload)
end)

Dzięki temu np. Node-RED podłączony do centralnego Mosquitto może sterować Tasmotą podłączoną do wbudowanego brokera vCLU:

Node-RED → Mosquitto → vCLU (central) → vCLU (local_mqtt) → Tasmota
           "vclu/cmnd/gniazdko1/POWER"    "cmnd/gniazdko1/POWER"

Przycisk Grenton → MQTT#

local mqtt = MQTT:new("eventy")
mqtt:setHost("localhost"):setPort(1883):connect()

-- Gdy przycisk DIN1 zostanie naciśnięty, wyślij na MQTT
_:get("CLU.DIN1"):add_event(DIN.EVENT_ON_SWITCH_ON, function()
    mqtt:publish("dom/salon/przycisk", "pressed", 0, false)
end)

MQTT → sterowanie sceną#

local mqtt = MQTT:new("sceny")
mqtt:setHost("localhost"):setPort(1883):connect()

mqtt:subscribe("dom/scena/uruchom", function(topic, payload)
    -- payload = nazwa sceny, np. "wieczor"
    runScene(payload)
end)

Panel webowy#

Strona /mqtt pozwala:

  • Włączyć/wyłączyć i skonfigurować wbudowany broker
  • Zobaczyć podłączonych klientów (urządzenia)
  • Zobaczyć instancje klienta MQTT z Lua
  • Wysłać testową wiadomość
  • Opublikować Home Assistant Discovery (patrz Home Assistant)

Jak działa pod spodem#

Lua: mqtt:publish("topic", "payload")
  ↓
Go: __go_mqtt_publish() → MQTTManager.Publish()
  ↓
Paho MQTT Client → broker (zewnętrzny lub wbudowany)
Broker odbiera wiadomość od urządzenia
  ↓
Paho Client → wiadomość trafia do kanału Go (bufor 1000)
  ↓
Lua: MQTT.pollMessages() (wywoływane co 100ms)
  ↓
__go_mqtt_poll() pobiera wiadomość z kanału
  ↓
Callback z mqtt:subscribe() jest wywołany