Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lua Scripting

Lua scripts are the data engine of Byonk screens. They fetch, process, and transform data before it’s rendered by the template. This guide covers all the APIs available to your scripts.

Script Structure

Every Lua script must return a table with data and refresh_rate:

-- Optional: Use params from config.yaml
local my_param = params.some_key or "default"

-- Your logic here
local result = do_something()

-- Required: Return data for template
return {
  data = {
    -- Passed to SVG template
  },
  refresh_rate = 300  -- Seconds until next refresh
}

Parameters

Device-specific parameters are available via the global params table:

# config.yaml
devices:
  "94:A9:90:8C:6D:18":
    screen: weather
    params:
      city: "Zurich"
      units: "metric"
-- In your script
local city = params.city        -- "Zurich"
local units = params.units      -- "metric"
local missing = params.other    -- nil (not defined)

-- Always provide defaults
local limit = params.limit or 10

HTTP Requests

http_get(url)

Fetches a URL and returns the response body as a string.

local response = http_get("https://api.example.com/data")

Error handling:

local ok, response = pcall(function()
  return http_get("https://api.example.com/data")
end)

if not ok then
  log_error("Request failed: " .. tostring(response))
  return {
    data = { error = "Failed to fetch data" },
    refresh_rate = 60
  }
end

URL encoding:

local city = "Zürich, Schweiz"
local encoded = city:gsub(" ", "%%20"):gsub(",", "%%2C")
local url = "https://api.example.com/city?name=" .. encoded

JSON

json_decode(string)

Parses a JSON string into a Lua table.

local response = http_get("https://api.example.com/data")
local data = json_decode(response)

-- Access fields
local name = data.name
local items = data.items
local first = data.items[1]  -- Lua arrays are 1-indexed!

json_encode(table)

Converts a Lua table to a JSON string.

local data = { name = "test", values = {1, 2, 3} }
local json_str = json_encode(data)
-- '{"name":"test","values":[1,2,3]}'

HTML Parsing

For scraping web pages, Byonk provides CSS selector-based HTML parsing.

html_parse(html)

Parses an HTML string and returns a document object.

local html = http_get("https://example.com")
local doc = html_parse(html)

doc:select(selector)

Queries elements using CSS selectors. Returns an elements collection.

local links = doc:select("a.nav-link")
local rows = doc:select("table.data tr")
local header = doc:select("h1")

doc:select_one(selector)

Returns only the first matching element (or nil).

local title = doc:select_one("title")
if title then
  log_info("Page title: " .. title:text())
end

elements:each(fn)

Iterates over matched elements.

local items = {}
doc:select("ul.list li"):each(function(el)
  table.insert(items, {
    text = el:text(),
    link = el:attr("href")
  })
end)

element:text()

Gets the inner text content.

local heading = doc:select_one("h1")
local text = heading:text()  -- "Welcome to Example"

element:attr(name)

Gets an attribute value.

local link = doc:select_one("a")
local href = link:attr("href")   -- "https://..."
local class = link:attr("class") -- "nav-link"

element:html()

Gets the inner HTML.

local div = doc:select_one("div.content")
local inner_html = div:html()

Example: Scraping a Table

local html = http_get("https://example.com/data")
local doc = html_parse(html)

local rows = {}
doc:select("table tbody tr"):each(function(row)
  local cells = {}
  row:select("td"):each(function(cell)
    table.insert(cells, cell:text())
  end)

  if #cells >= 2 then
    table.insert(rows, {
      name = cells[1],
      value = cells[2]
    })
  end
end)

return {
  data = { rows = rows },
  refresh_rate = 900
}

Time Functions

time_now()

Returns the current Unix timestamp (seconds since 1970).

local now = time_now()  -- e.g., 1703672400

time_format(timestamp, format)

Formats a timestamp into a string using strftime patterns.

local now = time_now()

time_format(now, "%H:%M")        -- "14:32"
time_format(now, "%H:%M:%S")     -- "14:32:05"
time_format(now, "%Y-%m-%d")     -- "2024-12-27"
time_format(now, "%A")           -- "Friday"
time_format(now, "%B %d, %Y")    -- "December 27, 2024"

Common format codes:

CodeDescriptionExample
%HHour (24h)14
%MMinute32
%SSecond05
%YYear2024
%mMonth12
%dDay27
%AWeekday nameFriday
%BMonth nameDecember
%aShort weekdayFri
%bShort monthDec

time_parse(string, format)

Parses a date string into a Unix timestamp.

local ts = time_parse("2024-12-27 14:30", "%Y-%m-%d %H:%M")

Logging

Write messages to the Byonk server logs.

log_info("Processing request for station: " .. station)
log_warn("API returned empty response")
log_error("Failed to parse JSON: " .. err)

Logs appear in the server output:

INFO script=true: Processing request for station: Olten
WARN script=true: API returned empty response
ERROR script=true: Failed to parse JSON: unexpected token

Complete Example: Transit API

Here’s a real-world example fetching transit data:

-- transit.lua - Fetch public transport departures

local station = params.station or "Olten"
local limit = params.limit or 8

log_info("Fetching departures for: " .. station)

-- URL encode the station name
local encoded = station:gsub(" ", "%%20"):gsub(",", "%%2C")
local url = "https://transport.opendata.ch/v1/stationboard"
      .. "?station=" .. encoded
      .. "&limit=" .. limit

-- Fetch with error handling
local ok, response = pcall(function()
  return http_get(url)
end)

if not ok then
  log_error("API request failed: " .. tostring(response))
  return {
    data = {
      station = station,
      error = "Failed to fetch departures",
      departures = {}
    },
    refresh_rate = 60
  }
end

-- Parse JSON
local json = json_decode(response)

-- Transform data for template
local departures = {}
local now = time_now()

for i, dep in ipairs(json.stationboard or {}) do
  local departure_time = dep.stop and dep.stop.departure or ""
  local hour, min = departure_time:match("T(%d+):(%d+)")

  table.insert(departures, {
    time = hour and (hour .. ":" .. min) or "??:??",
    line = (dep.category or "") .. (dep.number or ""),
    destination = dep.to or "Unknown",
    delay = dep.stop and dep.stop.delay or 0
  })
end

-- Calculate smart refresh rate
local refresh_rate = 300
if #departures > 0 and json.stationboard[1].stop then
  local first_dep = json.stationboard[1].stop.departureTimestamp
  if first_dep then
    local seconds_until = first_dep - now
    refresh_rate = math.max(30, math.min(seconds_until + 30, 900))
  end
end

log_info("Found " .. #departures .. " departures, refresh in " .. refresh_rate .. "s")

return {
  data = {
    station = json.station and json.station.name or station,
    departures = departures,
    updated_at = time_format(now, "%H:%M")
  },
  refresh_rate = refresh_rate
}

Tips & Best Practices

Always Handle Errors

local ok, result = pcall(function()
  return http_get(url)
end)

if not ok then
  return { data = { error = "..." }, refresh_rate = 60 }
end

Provide Default Values

local limit = params.limit or 10
local show_delays = params.show_delays or true

Log for Debugging

log_info("Params: " .. json_encode(params))
log_info("Fetched " .. #items .. " items")

Keep It Simple

Scripts run on every request. Avoid:

  • Complex computations
  • Multiple HTTP requests when one will do
  • Parsing more data than needed

Use Smart Refresh Rates

Don’t refresh more often than necessary:

-- Real-time data: 30-60 seconds
-- Regular updates: 300-900 seconds
-- Static content: 3600+ seconds

Next Steps