HTTP#

vCLU udostępnia trzy moduły HTTP:

ModułRola
HttpClientKlient HTTP - wywoływanie zewnętrznych API
HTTPServerSerwer HTTP - odbieranie requestów, webhooki
HttpListenerListener kompatybilny z OM

HttpClient#

Tworzenie i konfiguracja#

-- Domyślna instancja
local resp = HttpClient:GET("https://httpbin.org/get")

-- Nowa instancja z konfiguracją (chainable)
local api = HttpClient:new("myapi")
api:setBaseUrl("https://api.example.com/v1")
   :setTimeout(5000)
   :setBearerToken("my-token")

Dostępne metody konfiguracji (wszystkie chainable - zwracają self):

MetodaOpis
setBaseUrl(url)Bazowy URL dla ścieżek relatywnych
setTimeout(ms)Timeout w milisekundach (domyślnie 30000)
setHeader(name, value)Dodaj domyślny header
removeHeader(name)Usuń domyślny header
setContentType(type)Content-Type (domyślnie application/json)
setAuthorization(value)Header Authorization
setBearerToken(token)Bearer token
setBasicAuth(user, pass)Basic Auth (automatyczny base64)
setVerifySSL(bool)Weryfikacja SSL (domyślnie true)
setFollowRedirects(bool)Podążanie za redirectami (domyślnie true)

Metody HTTP#

local resp = api:GET("/users")
local resp = api:POST("/users", {name = "Jan"})
local resp = api:PUT("/users/1", {name = "Anna"})
local resp = api:DELETE("/users/1")
local resp = api:PATCH("/users/1", {status = "active"})
local resp = api:HEAD("/health")

Jeśli body jest tabelą Lua, automatycznie jest serializowane do JSON:

-- Te dwa wywołania są równoważne:
api:POST("/api", {key = "value"})
api:POST("/api", '{"key":"value"}')

Ogólna metoda z custom headerami:

local resp = api:request("POST", "/endpoint", body, {
    ["Content-Type"] = "application/xml",
    ["X-Custom"] = "value"
})

Obiekt response#

PoleTypOpis
statusnumberKod HTTP (200, 404, 500…)
bodystringTreść odpowiedzi
headerstableNagłówki odpowiedzi
okbooleantrue gdy status 200-299
local resp = api:GET("/users")
if resp.ok then
    local data = api:parseJSON(resp)
    log.info("Users: %s", inspect(data))
else
    log.error("HTTP %d", resp.status)
end

Metody asynchroniczne#

Nie blokują - callback wykonywany po otrzymaniu odpowiedzi:

api:asyncGET("/slow-endpoint", function(response, error)
    if error then
        log.error("Błąd: %s", error)
        return
    end
    log.info("Odpowiedź: %s", response.body)
end)

api:asyncPOST("/webhook", payload, function(response, error)
    if response.ok then log.info("Wysłano") end
end)

JSON helpers#

-- POST/PUT z automatyczną serializacją
api:postJSON("/data", {name = "test", value = 42})
api:putJSON("/data/1", {value = 43})

-- Parsowanie odpowiedzi
local resp = api:GET("/data")
local data = api:parseJSON(resp)

HTTPServer#

Serwer HTTP na dowolnym porcie, endpointy obsługiwane z Lua.

Uruchamianie#

-- Bez autoryzacji
HTTPServer:start(8081)

-- Z Basic Auth
HTTPServer:start(8081, {
    auth = { username = "admin", password = "secret123" }
})

Rejestracja endpointów#

HTTPServer:on("/api/status", "GET", function(req)
    return {
        status = 200,
        body = JSON.encode({status = "ok", uptime = os.time()}),
        headers = {["Content-Type"] = "application/json"}
    }
end)

Handler otrzymuje obiekt req:

PoleTypOpis
req.pathstringŚcieżka URL
req.methodstringMetoda HTTP
req.headerstableNagłówki requestu
req.bodystringTreść requestu
req.querytableParametry query string
req.remoteAddrstringAdres klienta

Handler zwraca tabelę odpowiedzi:

PoleDomyślnieOpis
status200Kod HTTP
body""Treść odpowiedzi
headers{}Nagłówki odpowiedzi

Wiele metod na jednym endpoincie#

HTTPServer:on("/api/data", "GET,POST", function(req)
    if req.method == "GET" then
        return {status = 200, body = JSON.encode(kv:getAll())}
    elseif req.method == "POST" then
        local data = JSON.decode(req.body)
        kv:set("data", data)
        return {status = 201, body = "Created"}
    end
end)

Endpointy publiczne#

-- Wymaga Basic Auth (domyślnie)
HTTPServer:on("/api/admin", "POST", handler)

-- Publiczny - bez autoryzacji
HTTPServer:on("/health", "GET", handler, {public = true})

-- Własne dane logowania
HTTPServer:on("/api/special", "GET", handler, {
    auth = {username = "special", password = "pass"}
})

Zarządzanie#

HTTPServer:isRunning()    -- czy działa
HTTPServer:endpoints()    -- lista endpointów
HTTPServer:off("/api/old") -- usuń endpoint
HTTPServer:stop()          -- zatrzymaj serwer

Przykłady#

Wysyłanie do zewnętrznego API#

local api = HttpClient:new()
api:setBaseUrl("https://api.example.com")
   :setBearerToken("my-api-token")

every(300000, function()  -- co 5 minut
    local temp = sensor:getValue()
    local resp = api:postJSON("/sensors/temperature", {
        device = "vclu-1",
        value = temp,
        timestamp = os.time()
    })
    if not resp.ok then
        log.error("Błąd: HTTP %d", resp.status)
    end
end)

Powiadomienie Slack#

local slack = HttpClient:new()
slack:setBaseUrl("https://hooks.slack.com")

local function sendSlack(message)
    slack:asyncPOST("/services/xxx/yyy/zzz",
        JSON.encode({text = message}),
        function(resp, err)
            if err then log.error("Slack: %s", err) end
        end)
end

-- Na przycisk
button:add_event(DIN.EVENT_ON_CLICK, function()
    sendSlack("Alarm! Czujnik ruchu.")
end)

Sterowanie Tasmota przez HTTP#

local tasmota = HttpClient:new()
tasmota:setBaseUrl("http://192.168.1.50")
       :setTimeout(3000)

local function tasmotaOn()  tasmota:GET("/cm?cmnd=Power%20On") end
local function tasmotaOff() tasmota:GET("/cm?cmnd=Power%20Off") end

button:add_event(DIN.EVENT_ON_CLICK, function()
    tasmotaOn()
end)

Webhook receiver (Tasmota, Shelly, IFTTT)#

HTTPServer:start(8081)

HTTPServer:on("/webhook/tasmota", "POST", function(req)
    local data = JSON.decode(req.body)
    if data and data.POWER == "ON" then
        lamp:switchOn()
    elseif data and data.POWER == "OFF" then
        lamp:switchOff()
    end
    return {status = 200, body = "OK"}
end, {public = true})

REST API na vCLU#

HTTPServer:start(8081, {auth = {username = "admin", password = "secret"}})

HTTPServer:on("/api/relay", "POST", function(req)
    local data = JSON.decode(req.body)
    if not data or not data.id then
        return {status = 400, body = "Missing 'id'"}
    end

    local obj = _:get(data.id)
    if not obj then
        return {status = 404, body = "Not found"}
    end

    local action = data.action or "toggle"
    if action == "on" then obj:switchOn()
    elseif action == "off" then obj:switchOff()
    else obj:toggle() end

    return {
        status = 200,
        body = JSON.encode({id = data.id, value = obj:getValue()})
    }
end)

-- Publiczny health check
HTTPServer:on("/health", "GET", function(req)
    return {status = 200, body = "OK"}
end, {public = true})

HttpListener (kompatybilność z OM)#

Obsługuje requesty HTTP w stylu obiektu OM (get/set/execute/add_event). Wymaga uruchomionego HTTPServer.

HTTPServer:start(8081)

local wh = HttpListener:new("webhook1", nil, EventBus:getShared())
wh:set(HttpListener.FEATURE_PATH, "/api/webhook")

wh:add_event(HttpListener.EVENT_ON_REQUEST, function()
    local body = wh:get(HttpListener.FEATURE_REQUEST_BODY)

    wh:set(HttpListener.FEATURE_STATUS_CODE, 200)
    wh:set(HttpListener.FEATURE_RESPONSE_TYPE, HttpListener.TYPE_JSON)
    wh:set(HttpListener.FEATURE_RESPONSE_BODY, {status = "received"})
    wh:execute(HttpListener.METHOD_SEND_RESPONSE)
end)