HTTP#
vCLU oferuje trzy moduly HTTP dostepne z poziomu Lua:
| Modul | Rola | Uzycie |
|---|---|---|
| HttpClient | Klient HTTP (sync/async) | Wywolywanie zewnetrznych API |
| HTTPServer | Wbudowany serwer HTTP | Odbieranie requestow, webhooки, REST API |
| HttpListener | Listener kompatybilny z OM | Obiekt z get/set/execute, zdarzenie ON_REQUEST |
HttpClient - klient HTTP#
Tworzenie#
-- Domyslna instancja (globalna)
local resp = HttpClient:GET("https://httpbin.org/get")
-- Nowa instancja z wlasna konfiguracja
local api = HttpClient:new("myapi")
api:setBaseUrl("https://api.example.com")
:setTimeout(5000)
:setBearerToken("my-token")Metody synchroniczne#
Blokuja do otrzymania odpowiedzi. Zwracaja obiekt response:
local resp = HttpClient:GET("/status")
-- resp = {status=200, body="...", headers={...}, ok=true}
local resp = HttpClient:POST("/data", '{"key":"value"}')
local resp = HttpClient:PUT("/data/1", body)
local resp = HttpClient:DELETE("/data/1")
local resp = HttpClient:PATCH("/data/1", body)
local resp = HttpClient:HEAD("/health")Ogolna metoda z custom headerami:
local resp = HttpClient:request("POST", "/endpoint", body, {
["Content-Type"] = "application/xml",
["X-Custom"] = "value"
})
if resp.ok then
log.info("Status: %d", resp.status)
log.info("Body: %s", resp.body)
endObiekt response#
Kazda metoda HTTP zwraca tabele:
| Pole | Typ | Opis |
|---|---|---|
status | number | Kod HTTP (200, 404, 500…) |
body | string | Tresc odpowiedzi |
headers | table | Naglowki odpowiedzi |
ok | boolean | true gdy status 200-299 |
Metody asynchroniczne#
Nie blokuja petli glownej - callback wykonywany jest po otrzymaniu odpowiedzi:
HttpClient:asyncGET("/slow-endpoint", function(response, error)
if error then
log.error("Blad: %s", error)
return
end
log.info("Odpowiedz: %s", response.body)
end)
HttpClient:asyncPOST("/webhook", payload, function(response, error)
if response.ok then
log.info("Webhook wyslany")
end
end)
-- Ogolna z headerami
HttpClient:asyncRequest("PUT", "/data", body, headers, callback)Async requesty sa przetwarzane w goroutine Go. RunLoop co 10ms sprawdza gotowe odpowiedzi i wywoluje callbacki w watku Lua.
Konfiguracja#
Chainable API#
local api = HttpClient:new()
api:setBaseUrl("https://api.example.com")
:setTimeout(10000)
:setHeader("X-Api-Key", "abc123")
:setBearerToken("token123")Dostepne metody konfiguracji:
| Metoda | Opis |
|---|---|
setBaseUrl(url) | Bazowy URL dla sciezek relatywnych |
setTimeout(ms) | Timeout w milisekundach |
setHeader(name, value) | Dodaj domyslny header |
removeHeader(name) | Usun domyslny header |
setContentType(type) | Domyslny Content-Type |
setAuthorization(value) | Header Authorization |
setBearerToken(token) | Bearer token (skrot) |
setBasicAuth(user, pass) | Basic Auth (automatyczny base64) |
setVerifySSL(bool) | Weryfikacja SSL (domyslnie true) |
setFollowRedirects(bool) | Podazanie za redirectami (domyslnie true) |
Styl Grenton (get/set)#
api:set(HttpClient.Properties.BaseUrl, "https://api.example.com")
api:set(HttpClient.Properties.Timeout, 5000)
api:set(HttpClient.Properties.VerifySSL, false)
local url = api:get(HttpClient.Properties.BaseUrl)| Property ID | Nazwa | Domyslnie |
|---|---|---|
| 0 | BaseUrl | "" |
| 1 | Timeout | 30000 |
| 2 | ContentType | "application/json" |
| 3 | Authorization | nil |
| 4 | FollowRedirects | true |
| 5 | VerifySSL | true |
JSON helpers#
-- POST z automatyczna serializacja tabeli do JSON
HttpClient:postJSON("/api/data", {name = "test", value = 42})
-- PUT JSON
HttpClient:putJSON("/api/data/1", {value = 43})
-- Parsowanie odpowiedzi JSON
local resp = HttpClient:GET("/api/data")
local data = HttpClient:parseJSON(resp)
-- data = {name = "test", value = 42}Jezeli body w POST/PUT jest tabela Lua, automatycznie jest serializowane do JSON:
-- Te dwa wywolania sa rownowazne:
HttpClient:POST("/api", {key = "value"})
HttpClient:POST("/api", '{"key":"value"}')Kody statusu (stale)#
if resp.status == HttpClient.Status.OK then ...
if resp.status == HttpClient.Status.NotFound then ...| Stala | Wartosc |
|---|---|
HttpClient.Status.OK | 200 |
HttpClient.Status.Created | 201 |
HttpClient.Status.NoContent | 204 |
HttpClient.Status.BadRequest | 400 |
HttpClient.Status.Unauthorized | 401 |
HttpClient.Status.Forbidden | 403 |
HttpClient.Status.NotFound | 404 |
HttpClient.Status.ServerError | 500 |
HTTPServer - wbudowany serwer HTTP#
HTTPServer pozwala uruchomic serwer HTTP na dowolnym porcie, rejestrujac endpointy obslugiwane z poziomu Lua.
Uruchamianie#
-- Serwer bez autoryzacji
HTTPServer:start(8081)
-- Serwer z globalna autoryzacja Basic Auth
HTTPServer:start(8081, {
auth = { username = "admin", password = "secret123" }
})Port musi byc wolny. Jesli port jest zajety, `start()` zwroci `false, "server already running"`. Domyslny port to `8081`.
Rejestracja endpointow#
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.id | string | ID requestu (wewnetrzne) |
req.path | string | Sciezka URL |
req.method | string | Metoda HTTP (GET, POST…) |
req.headers | table | Naglowki requestu |
req.body | string | Tresc requestu |
req.query | table | Parametry query string |
req.remoteAddr | string | Adres klienta |
Handler zwraca tabele odpowiedzi:
| Pole | Typ | Domyslnie | Opis |
|---|---|---|---|
status | number | 200 | Kod HTTP |
body | string | "" | Tresc odpowiedzi |
headers | table | {} | Naglowki 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("user_data", data)
return {status = 201, body = "Created"}
end
end)Endpointy publiczne#
Endpointy oznaczone jako public omijaja globalna autoryzacje:
-- Serwer z autoryzacja
HTTPServer:start(8081, {auth = {username = "admin", password = "secret"}})
-- Ten endpoint wymaga Basic Auth
HTTPServer:on("/api/admin", "POST", function(req)
return {status = 200, body = "admin only"}
end)
-- Ten jest publiczny - bez autoryzacji
HTTPServer:on("/health", "GET", function(req)
return {status = 200, body = "OK"}
end, {public = true})
-- Ten ma wlasne dane logowania (nadpisuja globalne)
HTTPServer:on("/api/special", "GET", function(req)
return {status = 200, body = "special"}
end, {auth = {username = "special_user", password = "other_pass"}})Zarzadzanie serwerem#
-- Sprawdz czy dziala
if HTTPServer:isRunning() then
log.info("Serwer HTTP aktywny")
end
-- Lista zarejestrowanych endpointow
local paths = HTTPServer:endpoints()
for _, path in ipairs(paths) do
log.info("Endpoint: " .. path)
end
-- Usun endpoint
HTTPServer:off("/api/old")
-- Zatrzymaj serwer
HTTPServer:stop()Limity#
| Parametr | Wartosc |
|---|---|
| Max rozmiar body requestu | 1 MB |
| Timeout na odpowiedz handlera | 30 sekund |
| Kolejka requestow | 100 |
Gdy kolejka jest pelna, serwer zwraca 503 Service Unavailable.
HttpListener - kompatybilny z OM#
HttpListener to modul kompatybilny z Object Manager - obsluguje requesty HTTP przez zdarzenia, w stylu typowego obiektu Grenton (get/set/execute/add_event).
Tworzenie#
local listener = HttpListener:new("webhook1", nil, EventBus:getShared())
listener:set(HttpListener.FEATURE_PATH, "/api/webhook")Po ustawieniu FEATURE_PATH, listener automatycznie rejestruje sie w HTTPServer.
HTTPServer musi byc uruchomiony (`HTTPServer:start(port)`) zanim HttpListener bedzie mogl obslugiwac requesty.
Obsluga requestow#
listener:add_event(HttpListener.EVENT_ON_REQUEST, function()
-- Odczytaj dane requestu
local method = listener:get(HttpListener.FEATURE_METHOD)
local body = listener:get(HttpListener.FEATURE_REQUEST_BODY)
local query = listener:get(HttpListener.FEATURE_QUERY_STRING_PARAMS)
-- Ustaw odpowiedz
listener:set(HttpListener.FEATURE_STATUS_CODE, 200)
listener:set(HttpListener.FEATURE_RESPONSE_TYPE, HttpListener.TYPE_JSON)
listener:set(HttpListener.FEATURE_RESPONSE_BODY, {result = "ok"})
-- Wyslij
listener:execute(HttpListener.METHOD_SEND_RESPONSE)
end)Cechy (Features)#
| Stala | Wartosc | R/W | Opis |
|---|---|---|---|
FEATURE_PATH | 0 | R/W | Sciezka URL (ustawienie rejestruje listener) |
FEATURE_METHOD | 1 | R | Metoda HTTP (GET, POST…) |
FEATURE_QUERY_STRING_PARAMS | 2 | R | Parametry query string (tabela) |
FEATURE_REQUEST_TYPE | 3 | R | Typ requestu (0-5) |
FEATURE_REQUEST_BODY | 5 | R | Tresc requestu |
FEATURE_RESPONSE_TYPE | 6 | R/W | Typ odpowiedzi (0-4) |
FEATURE_RESPONSE_BODY | 8 | R/W | Tresc odpowiedzi |
FEATURE_STATUS_CODE | 9 | R/W | Kod HTTP odpowiedzi |
Typy request/response#
| Stala | Wartosc | Content-Type |
|---|---|---|
TYPE_NONE | 0 | (brak) |
TYPE_TEXT | 1 | text/plain |
TYPE_JSON | 2 | application/json |
TYPE_XML | 3 | text/xml |
TYPE_FORM_DATA | 4 | application/x-www-form-urlencoded |
TYPE_OTHER | 5 | (inne) |
Typ requestu jest wykrywany automatycznie z naglowka Content-Type. JSON i FormData sa automatycznie parsowane do tabel Lua.
Metody (execute)#
| Stala | Wartosc | Opis |
|---|---|---|
METHOD_SEND_RESPONSE | 0 | Wyslij odpowiedz |
METHOD_CLEAR | 1 | Wyczysc dane odpowiedzi |
Zdarzenia#
| Stala | Wartosc | Opis |
|---|---|---|
EVENT_ON_REQUEST | 0 | Otrzymano request HTTP |
Przyklady#
Wysylanie danych do zewnetrznego API#
local api = HttpClient:new()
api:setBaseUrl("https://api.example.com")
:setBearerToken("my-api-token")
:setTimeout(10000)
-- Wyslij temperature
local function reportTemperature(temp)
local resp = api:postJSON("/sensors/temperature", {
device = "vclu-1",
value = temp,
timestamp = os.time()
})
if not resp.ok then
log.error("Blad wysylania: HTTP %d", resp.status)
end
end
-- Co 5 minut
every(300000, function()
local temp = _:get("CLU.ANA1"):get(0)
reportTemperature(temp)
end)Webhook - wysylanie powiadomien#
local notify = HttpClient:new()
notify:setBaseUrl("https://hooks.slack.com")
local function sendSlack(message)
notify:asyncPOST("/services/xxx/yyy/zzz", JSON.encode({
text = message
}), function(resp, err)
if err then log.error("Slack error: %s", err) end
end)
end
-- Powiadomienie przy alarmie
_:get("CLU.DIN1"):add_event(DIN.EVENT_ON_CLICK, function()
sendSlack("Alarm! Czujnik ruchu wykryl ruch.")
end)Integracja z Tasmota (HTTP)#
local tasmota = HttpClient:new()
tasmota:setBaseUrl("http://192.168.1.50")
:setTimeout(3000)
-- Wlacz urzadzenie Tasmota
local function tasmotaOn()
tasmota:GET("/cm?cmnd=Power%20On")
end
-- Wylacz
local function tasmotaOff()
tasmota:GET("/cm?cmnd=Power%20Off")
end
-- Stan
local function tasmotaStatus()
local resp = tasmota:GET("/cm?cmnd=Status%200")
if resp.ok then
local data = tasmota:parseJSON(resp)
return data
end
return nil
end
-- Wlacz Tasmota na przycisk
local btn = GPIO_DIN:new("BTN1", 27)
btn:add_event(DIN.EVENT_ON_CLICK, function()
tasmotaOn()
end)Odpytywanie pogody z cache#
local weather = HttpClient:new()
weather:setBaseUrl("https://api.openweathermap.org/data/2.5")
:setTimeout(10000)
local function getWeather(city)
-- Sprawdz cache
local cached = kv:get("weather_" .. city)
if cached then return cached end
-- Pobierz z API
local resp = weather:GET("/weather?q=" .. city .. "&appid=YOUR_KEY&units=metric")
if resp.ok then
local data = weather:parseJSON(resp)
if data then
local result = {
temp = data.main and data.main.temp,
humidity = data.main and data.main.humidity,
description = data.weather and data.weather[1] and data.weather[1].description
}
-- Cache na 15 minut
kv:set("weather_" .. city, result, false, 900)
return result
end
end
return nil
end
-- Odpytuj co 15 minut
every(900000, function()
local w = getWeather("Warsaw")
if w then
log.info("Pogoda: %s, temp: %.1f°C", w.description, w.temp)
end
end)Serwer REST API#
HTTPServer:start(8081)
-- Baza danych w KV store
local function getItems()
return kv:get("items") or {}
end
local function saveItems(items)
kv:set("items", items)
end
-- GET /api/items - lista
HTTPServer:on("/api/items", "GET", function(req)
return {
status = 200,
body = JSON.encode(getItems()),
headers = {["Content-Type"] = "application/json"}
}
end)
-- POST /api/items - dodaj
HTTPServer:on("/api/items", "POST", function(req)
local item = JSON.decode(req.body)
if not item or not item.name then
return {status = 400, body = "Missing 'name'"}
end
local items = getItems()
item.id = #items + 1
table.insert(items, item)
saveItems(items)
return {
status = 201,
body = JSON.encode(item),
headers = {["Content-Type"] = "application/json"}
}
end)
-- GET /api/items?id=1 - szczegoly
HTTPServer:on("/api/item", "GET", function(req)
local id = tonumber(req.query.id)
if not id then
return {status = 400, body = "Missing 'id'"}
end
local items = getItems()
for _, item in ipairs(items) do
if item.id == id then
return {
status = 200,
body = JSON.encode(item),
headers = {["Content-Type"] = "application/json"}
}
end
end
return {status = 404, body = "Not found"}
end)Webhook receiver (np. z Tasmota, Shelly, IFTTT)#
HTTPServer:start(8081)
-- Odbierz webhook z Tasmota (Rule → HTTP)
HTTPServer:on("/webhook/tasmota", "POST", function(req)
local data = JSON.decode(req.body)
if not data then
return {status = 400, body = "Invalid JSON"}
end
log.info("Tasmota webhook: %s", req.body)
-- Obsluz zdarzenie
if data.POWER == "ON" then
_:get("CLU.DOU5048"):execute(DOUT.METHOD_SWITCH_ON)
elseif data.POWER == "OFF" then
_:get("CLU.DOU5048"):execute(DOUT.METHOD_SWITCH_OFF)
end
return {status = 200, body = "OK"}
end, {public = true}) -- publiczny - Tasmota nie wysyla Basic Auth
-- Odbierz zdarzenie z Shelly
HTTPServer:on("/webhook/shelly", "POST,GET", function(req)
local action = req.query.action or "unknown"
log.info("Shelly: action=%s", action)
if action == "on" then
runScene("wlacz_swiatla")
elseif action == "off" then
runScene("wylacz_swiatla")
end
return {status = 200, body = "OK"}
end, {public = true})Sterowanie vCLU przez HTTP#
HTTPServer:start(8081, {auth = {username = "admin", password = "secret"}})
-- Wykonaj scene przez HTTP
HTTPServer:on("/api/scene", "POST", function(req)
local name = req.query.name
if not name then
return {status = 400, body = "Missing 'name' query param"}
end
local scenes = getAllScenes()
for _, s in ipairs(scenes) do
if s == name then
runScene(name)
return {status = 200, body = "Scene '" .. name .. "' executed"}
end
end
return {status = 404, body = "Scene not found"}
end)
-- Przelacz relay
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 = "Object not found"}
end
local action = data.action or "toggle"
if action == "on" then
obj:execute(DOUT.METHOD_SWITCH_ON)
elseif action == "off" then
obj:execute(DOUT.METHOD_SWITCH_OFF)
else
obj:execute(DOUT.METHOD_SWITCH)
end
return {
status = 200,
body = JSON.encode({id = data.id, value = obj:get(DOUT.FEATURE_VALUE)})
}
end)
-- Stan systemu - publiczny
HTTPServer:on("/api/status", "GET", function(req)
return {
status = 200,
body = JSON.encode({
running = true,
uptime = os.time(),
scenes = getAllScenes(),
server = HTTPServer:isRunning()
}),
headers = {["Content-Type"] = "application/json"}
}
end, {public = true})HttpListener - odbieranie webhookow (styl OM)#
HTTPServer:start(8081)
-- Listener na /api/webhook
local wh = HttpListener:new("webhook1", nil, EventBus:getShared())
wh:set(HttpListener.FEATURE_PATH, "/api/webhook")
wh:add_event(HttpListener.EVENT_ON_REQUEST, function()
local method = wh:get(HttpListener.FEATURE_METHOD)
local body = wh:get(HttpListener.FEATURE_REQUEST_BODY)
local query = wh:get(HttpListener.FEATURE_QUERY_STRING_PARAMS)
log.info("Webhook: %s, body=%s", method, tostring(body))
-- JSON body jest automatycznie parsowany do tabeli
if type(body) == "table" and body.command then
if body.command == "lights_on" then
runScene("wlacz_swiatla")
end
end
-- Odpowiedz JSON
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)Przekierowanie requestow (proxy)#
-- Proxy: odbierz request na vCLU, przekaz do zewnetrznego API
HTTPServer:start(8081)
local backend = HttpClient:new()
backend:setBaseUrl("https://api.external.com")
:setBearerToken("token123")
HTTPServer:on("/proxy", "GET,POST", function(req)
local resp
if req.method == "GET" then
resp = backend:GET(req.query.path or "/")
else
resp = backend:POST(req.query.path or "/", req.body)
end
return {
status = resp.status,
body = resp.body,
headers = {["Content-Type"] = "application/json"}
}
end)Backend Go#
Requesty HttpClient sa delegowane do HTTPManager w Go:
| Lua | Go bridge | Opis |
|---|---|---|
HttpClient:request(...) | __go_http_request | Sync - blokuje Lua do odpowiedzi |
HttpClient:asyncRequest(...) | __go_http_async_request | Async - nie blokuje |
| (polling) | __go_http_poll_async | Sprawdza gotowe async odpowiedzi |
| (result) | __go_http_get_async_response | Pobiera wynik async |
Async flow:
asyncRequest()wysyla request i zwracarequestId- Go wykonuje request w goroutine
- Lua co 100ms polluje
__go_http_poll_async()- sprawdza gotowe odpowiedzi - Gdy odpowiedz gotowa - wykonuje callback Lua
HTTPServer uzywa osobnego zestawu funkcji Go:
| Lua | Go bridge | Opis |
|---|---|---|
HTTPServer:start(port) | __go_http_server_start | Uruchom serwer |
HTTPServer:stop() | __go_http_server_stop | Zatrzymaj serwer |
HTTPServer:isRunning() | __go_http_server_is_running | Czy dziala |
HTTPServer:on(...) | __go_http_server_add_endpoint | Zarejestruj endpoint |
HTTPServer:off(...) | __go_http_server_remove_endpoint | Usun endpoint |
| (polling) | __go_http_server_poll | Pobierz request z kolejki |
| (respond) | __go_http_server_respond | Wyslij odpowiedz |
Serwer HTTP dziala w osobnej goroutine. Requesty trafiaja do kolejki (rozmiar 100), skad RunLoop pobiera je i przekazuje do handlerow Lua. Odpowiedz wraca kanalem do goroutine, ktora odsyla ja klientowi HTTP.