Plugin API v2#

Kompletne API dostępne wewnątrz pluginu.

Tworzenie instancji#

local plugin = Plugin:new("my-plugin", {
    name = "My Plugin",
    version = "1.0.0",
    description = "Opis pluginu"
})

Lifecycle#

plugin:onInit(function(config)
    -- Wywoływana po załadowaniu z obiektem config
end)

plugin:onCleanup(function()
    -- Wywoływana przed wyładowaniem
    -- Timery, eventy, MQTT auto-czyszczone przez sandbox
end)

HTTP#

httpRequest#

plugin:httpRequest({
    url = "https://api.example.com/data",
    method = "GET",                    -- GET|POST|PUT|DELETE
    headers = {["X-Key"] = "value"},
    body = "raw body",                 -- lub:
    json = {key = "value"},            -- auto JSON.encode
    form = {field = "value"},          -- form-urlencoded
    timeout = 10000,                   -- ms
    parseJson = "success",             -- "success"|"always"|"never"
    log = {redact = true},             -- ukryj sekrety w logach
}, function(resp)
    -- resp.status   — HTTP status code
    -- resp.body     — raw body string
    -- resp.headers  — response headers
    -- resp.json     — parsed JSON (jeśli parseJson)
    -- resp.err      — error string lub nil
    -- resp.elapsed  — czas odpowiedzi ms
    -- resp.ok       — true jeśli 2xx
end)

URL builder#

local url = plugin:url("https://api.example.com/data", {
    apiKey = "secret",
    city = "Warsaw",
    format = "json"
})
-- → https://api.example.com/data?apiKey=secret&city=Warsaw&format=json

Parametry sortowane alfabetycznie, wartości URL-encoded.

Auth helpers#

plugin:basicAuth("user", "pass")    -- → "Basic dXNlcjpwYXNz"
plugin:bearerAuth("token")          -- → "Bearer token"

Poller#

Zarządzana pętla HTTP z retry i backoff:

local poller = plugin:poller("fetch", {
    interval = 60000,             -- ms między tickami
    immediate = true,             -- start od razu
    initialDelay = 1000,          -- opóźnienie pierwszego ticku
    timeout = 30000,              -- timeout ticku
    retry = {
        maxAttempts = 3,
        backoff = 2000,           -- ms między retry
    },

    onTick = function(done)
        plugin:httpRequest({
            url = plugin:url(baseUrl, params),
            parseJson = "success"
        }, function(resp)
            if resp.err then
                done(resp.err)     -- trigger retry
                return
            end
            -- przetwarzanie danych...
            done()                 -- sukces
        end)
    end,

    onError = function(err, stats)
        plugin:log("error", "Failed: " .. err)
    end
})

poller:start()
poller:stop()
poller:poll()           -- wymuś natychmiast
poller:stats()          -- statystyki (ticks, errors, lastTick)

Registry (obiekty pluginu)#

Obiekty zapisywane w plugins.{namespace}.{shortId}.{path}:

-- Utwórz lub nadpisz
plugin:upsertObject("current", {
    ready = false,
    value = 0,
    lastUpdate = 0
})

-- Aktualizuj (shallow merge, wymaga istnienia)
plugin:updateObject("current", {
    ready = true,
    value = 22.5,
    lastUpdate = os.time()
})

-- Odczyt (własne lub pełna ścieżka)
local obj = plugin:getObject("current")
local other = plugin:getObject("plugins.vclu.weather.current")

updateObject waliduje nazwy pól i sugeruje poprawki przy literówkach.


Events#

-- Emisja
plugin:emit("weather:changed", {temp = 22.5}, {
    throttle = 60000           -- max 1 event na 60s
})

-- Emisja z trailing edge
plugin:emit("position:updated", data, {
    throttle = 1000,
    trailing = true            -- emituj ostatnią wartość po oknie
})

-- Coalesce (zbieranie)
plugin:emit("batch:item", {item = "a"}, {
    throttle = 5000,
    coalesce = true,
    key = "item"               -- wynik: {items = ["a", "b", "c"]}
})

-- Subskrypcja
plugin:on("telegram:message", function(data)
    plugin:log("info", "Message: " .. data.text)
end)

Timery#

local id = plugin:setTimeout(5000, function()
    plugin:log("info", "Jednorazowy timer")
end)

local id = plugin:setInterval(10000, function()
    plugin:log("info", "Co 10 sekund")
end)

plugin:clearTimer(id)

Auto-czyszczone przy unload pluginu.


MQTT#

-- Publikacja
plugin:mqttPublish("sensors/temperature", "22.5", {
    retain = true,
    qos = 1
})

-- Subskrypcja
plugin:mqttSubscribe("commands/#", function(topic, payload)
    plugin:log("info", topic .. ": " .. payload)
end)

Sensor / Control (Expose API)#

-- Read-only sensor
plugin:sensor("temperature", function()
    return state.temperature
end)

-- Read-write control
plugin:control("fanSpeed",
    function() return state.fanSpeed end,        -- getter
    function(level)                               -- setter
        state.fanSpeed = level
        -- wysłanie komendy do urządzenia...
    end
)

-- Pobranie sensora/kontrolki
local sensor = plugin:get("temperature")

-- Powiadomienie expose o zmianie wartości
if sensor then sensor:notify() end

Sensory i kontrolki integrują się z expose():

expose(plugin:get("temperature"), "temperature", {
    name = "Temp zewnętrzna",
    unit = "°C"
})

expose(plugin:get("fanSpeed"), "fan", {
    name = "Wentylator",
    min = 0, max = 4
})

KV Store (persystencja)#

Dane przetrwają restart. Izolacja per-plugin.

-- Zapis
plugin:kvSet("last_offset", 12345)

-- Zapis z opcjami
plugin:kvSet("oauth_token", token, {
    secure = true,           -- ukryte w logach/UI
    ttl = 3600               -- wygasa po 1h
})

-- Odczyt
local offset = plugin:kvGet("last_offset", 0)   -- default = 0

-- Inne operacje
plugin:kvHas("key")              -- → boolean
plugin:kvDelete("key")
plugin:kvList("cache:")          -- → klucze z prefiksem
plugin:kvGetAll()                -- → {key → value}

Przechowywane w kv/{namespace}--{shortId}.json.


Type coercion#

Bezpieczna konwersja typów z config:

local interval = plugin:coerceNumber(config.interval, 3600)
local name = plugin:coerceString(config.name, "default")
local enabled = plugin:coerceBool(config.enabled, true)

Logging#

plugin:log("info", "Data fetched")
plugin:log("error", "Connection failed")
plugin:log("debug", "Raw response: " .. body)
plugin:log("warn", "Rate limit approaching")

-- Z redakcją sekretów
plugin:logSafe("info", "API call", {
    url = "https://api.example.com",
    apiKey = "sk-1234567890"         -- → "sk-12***"
}, true)

Redaktowane pola: apiKey, token, password, secret, auth, bearer.


Dostęp do innych pluginów#

-- Po full ID
local weather = Plugin.getPlugin("@vclu/weather")

-- Po short ID (jeśli unikalny)
local w = Plugin.getPlugin("weather")

if weather and weather:isReady() then
    local temp = weather:getTemperature()
end

-- Lista wszystkich pluginów
for _, p in ipairs(Plugin.list()) do
    plugin:log("info", p.name .. " v" .. p.version)
end

Expose do Home Assistant/HomeKit#

-- W onInit:
plugin:sensor("temperature", function() return state.temp end)
plugin:sensor("humidity", function() return state.humidity end)
plugin:control("fanSpeed",
    function() return state.fanSpeed end,
    function(v) state.fanSpeed = v end
)

-- W user.lua:
local salda = Plugin.getPlugin("@vclu/salda-recuperator")
if salda then
    expose(salda:get("supplyAir"), "temperature", {name = "Nawiew", unit = "°C"})
    expose(salda:get("humidity"), "humidity", {name = "Wilgotność"})
    expose(salda:get("fanSpeed"), "fan", {name = "Wentylator", min = 0, max = 4})
end