HTTP#
vCLU udostępnia trzy moduły HTTP:
| Moduł | Rola |
|---|---|
| HttpClient | Klient HTTP - wywoływanie zewnętrznych API |
| HTTPServer | Serwer HTTP - odbieranie requestów, webhooki |
| HttpListener | Listener 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):
| Metoda | Opis |
|---|---|
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#
| Pole | Typ | Opis |
|---|---|---|
status | number | Kod HTTP (200, 404, 500…) |
body | string | Treść odpowiedzi |
headers | table | Nagłówki odpowiedzi |
ok | boolean | true 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)
endMetody 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:
| Pole | Typ | Opis |
|---|---|---|
req.path | string | Ścieżka URL |
req.method | string | Metoda HTTP |
req.headers | table | Nagłówki requestu |
req.body | string | Treść requestu |
req.query | table | Parametry query string |
req.remoteAddr | string | Adres klienta |
Handler zwraca tabelę odpowiedzi:
| Pole | Domyślnie | Opis |
|---|---|---|
status | 200 | Kod 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 serwerPrzykł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)