HTTP#

vCLU oferuje trzy moduly HTTP dostepne z poziomu Lua:

ModulRolaUzycie
HttpClientKlient HTTP (sync/async)Wywolywanie zewnetrznych API
HTTPServerWbudowany serwer HTTPOdbieranie requestow, webhooки, REST API
HttpListenerListener kompatybilny z OMObiekt 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)
end

Obiekt response#

Kazda metoda HTTP zwraca tabele:

PoleTypOpis
statusnumberKod HTTP (200, 404, 500…)
bodystringTresc odpowiedzi
headerstableNaglowki odpowiedzi
okbooleantrue 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:

MetodaOpis
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 IDNazwaDomyslnie
0BaseUrl""
1Timeout30000
2ContentType"application/json"
3Authorizationnil
4FollowRedirectstrue
5VerifySSLtrue

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 ...
StalaWartosc
HttpClient.Status.OK200
HttpClient.Status.Created201
HttpClient.Status.NoContent204
HttpClient.Status.BadRequest400
HttpClient.Status.Unauthorized401
HttpClient.Status.Forbidden403
HttpClient.Status.NotFound404
HttpClient.Status.ServerError500

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:

PoleTypOpis
req.idstringID requestu (wewnetrzne)
req.pathstringSciezka URL
req.methodstringMetoda HTTP (GET, POST…)
req.headerstableNaglowki requestu
req.bodystringTresc requestu
req.querytableParametry query string
req.remoteAddrstringAdres klienta

Handler zwraca tabele odpowiedzi:

PoleTypDomyslnieOpis
statusnumber200Kod HTTP
bodystring""Tresc odpowiedzi
headerstable{}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#

ParametrWartosc
Max rozmiar body requestu1 MB
Timeout na odpowiedz handlera30 sekund
Kolejka requestow100

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)#

StalaWartoscR/WOpis
FEATURE_PATH0R/WSciezka URL (ustawienie rejestruje listener)
FEATURE_METHOD1RMetoda HTTP (GET, POST…)
FEATURE_QUERY_STRING_PARAMS2RParametry query string (tabela)
FEATURE_REQUEST_TYPE3RTyp requestu (0-5)
FEATURE_REQUEST_BODY5RTresc requestu
FEATURE_RESPONSE_TYPE6R/WTyp odpowiedzi (0-4)
FEATURE_RESPONSE_BODY8R/WTresc odpowiedzi
FEATURE_STATUS_CODE9R/WKod HTTP odpowiedzi

Typy request/response#

StalaWartoscContent-Type
TYPE_NONE0(brak)
TYPE_TEXT1text/plain
TYPE_JSON2application/json
TYPE_XML3text/xml
TYPE_FORM_DATA4application/x-www-form-urlencoded
TYPE_OTHER5(inne)

Typ requestu jest wykrywany automatycznie z naglowka Content-Type. JSON i FormData sa automatycznie parsowane do tabel Lua.

Metody (execute)#

StalaWartoscOpis
METHOD_SEND_RESPONSE0Wyslij odpowiedz
METHOD_CLEAR1Wyczysc dane odpowiedzi

Zdarzenia#

StalaWartoscOpis
EVENT_ON_REQUEST0Otrzymano 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:

LuaGo bridgeOpis
HttpClient:request(...)__go_http_requestSync - blokuje Lua do odpowiedzi
HttpClient:asyncRequest(...)__go_http_async_requestAsync - nie blokuje
(polling)__go_http_poll_asyncSprawdza gotowe async odpowiedzi
(result)__go_http_get_async_responsePobiera wynik async

Async flow:

  1. asyncRequest() wysyla request i zwraca requestId
  2. Go wykonuje request w goroutine
  3. Lua co 100ms polluje __go_http_poll_async() - sprawdza gotowe odpowiedzi
  4. Gdy odpowiedz gotowa - wykonuje callback Lua

HTTPServer uzywa osobnego zestawu funkcji Go:

LuaGo bridgeOpis
HTTPServer:start(port)__go_http_server_startUruchom serwer
HTTPServer:stop()__go_http_server_stopZatrzymaj serwer
HTTPServer:isRunning()__go_http_server_is_runningCzy dziala
HTTPServer:on(...)__go_http_server_add_endpointZarejestruj endpoint
HTTPServer:off(...)__go_http_server_remove_endpointUsun endpoint
(polling)__go_http_server_pollPobierz request z kolejki
(respond)__go_http_server_respondWyslij 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.