Files
xteink-apps/apps/HomeAssistant/main.lua
T

185 lines
6.0 KiB
Lua

-- DESCRIPTION: Home Assistant status cards
local CONFIG_PATH = "/.apps/HomeAssistant/config.txt"
local config = { entities = {} }
local cards = {}
local screen = nil
local connecting = false
local connected = false
local needsDraw = true
local function trim(value)
return value:match("^%s*(.-)%s*$")
end
local function loadConfig()
local content = fs.readFile(CONFIG_PATH)
if not content then return "Missing " .. CONFIG_PATH end
local lineNumber = 0
for rawLine in (content:gsub("\r", "") .. "\n"):gmatch("(.-)\n") do
lineNumber = lineNumber + 1
local line = trim(rawLine)
if line ~= "" and line:sub(1, 1) ~= "#" then
local key, value = line:match("^([%w_]+)%s*=%s*(.-)%s*$")
if not key or value == "" then return "Invalid config line " .. lineNumber end
if key == "url" then
config.url = value:gsub("/+$", "")
elseif key == "token" then
config.token = value
elseif key == "entity" then
if not value:match("^[%w_]+%.[%w_]+$") then return "Invalid entity on line " .. lineNumber end
config.entities[#config.entities + 1] = value
else
return "Unknown config key: " .. key
end
end
end
if not config.url then return "Config is missing url" end
if not config.token then return "Config is missing token" end
if #config.entities == 0 then return "Config has no entities" end
if #config.entities > 12 then return "Config supports up to 12 entities" end
end
local function jsonString(value)
local escapes = { ["\\"] = "\\\\", ["\""] = "\\\"", ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" }
return '"' .. value:gsub('[\\"\n\r\t]', escapes) .. '"'
end
local function templateRequest()
local lines = {}
for _, entity in ipairs(config.entities) do
lines[#lines + 1] = "{{ (state_attr('" .. entity .. "','friendly_name') or '" .. entity .. "') | replace('|','/') }}|" ..
entity .. "|{{ states('" .. entity .. "') | replace('|','/') }}|{{ (state_attr('" .. entity ..
"','unit_of_measurement') or '') | replace('|','/') }}"
end
return '{"template":' .. jsonString(table.concat(lines, "\n")) .. "}"
end
local function titleCase(value)
return value:gsub("_", " "):gsub("(%a)([%w']*)", function(first, rest)
return first:upper() .. rest
end)
end
local function parseCards(body)
local result = {}
for line in (body .. "\n"):gmatch("(.-)\n") do
local title, entity, value, unit = line:match("^([^|]*)|([^|]*)|([^|]*)|(.*)$")
if title then
result[#result + 1] = {
title = title,
entity = entity,
value = titleCase(value),
detail = unit
}
end
end
return result
end
local function fetchCards()
screen = "Loading Home Assistant..."
needsDraw = true
local body, status = net.post(config.url .. "/api/template", templateRequest(), {
Authorization = "Bearer " .. config.token,
["Content-Type"] = "application/json"
})
if body and status == 200 then
cards = parseCards(body)
if #cards == #config.entities then
screen = nil
else
screen = "Invalid Home Assistant response"
end
else
screen = "Home Assistant error " .. tostring(status)
end
needsDraw = true
end
local function drawCardText(font, x, y, width, text, style)
local textWidth = gui.getTextWidth(font, text, style)
gui.drawText(font, x + math.floor((width - textWidth) / 2), y, text, COLOR_BLACK, style)
end
local function drawCard(card, x, y, width, height)
gui.drawRoundedRect(x, y, width, height, 2, 10, COLOR_BLACK)
gui.drawText(FONT_UI_10, x + 12, y + 10, card.title, COLOR_BLACK, STYLE_BOLD)
drawCardText(FONT_NOTOSANS_16, x, y + math.floor(height / 2) - 8, width, card.value, STYLE_BOLD)
if card.detail ~= "" then
drawCardText(FONT_SMALL, x, y + height - 28, width, card.detail, STYLE_REGULAR)
end
end
local function render()
local width, height = gui.width(), gui.height()
gui.clear()
gui.drawText(FONT_NOTOSANS_16, 20, 18, "Home Assistant", COLOR_BLACK, STYLE_BOLD)
gui.drawLine(20, 58, width - 20, 58, 2, COLOR_BLACK)
if screen then
gui.drawCenteredText(FONT_UI_12, math.floor(height / 2), screen, COLOR_BLACK, STYLE_REGULAR)
else
local columns = width >= 700 and 4 or 2
local rows = math.ceil(#cards / columns)
local gap = 10
local margin = 16
local top = 72
local bottom = 58
local cardWidth = math.floor((width - margin * 2 - gap * (columns - 1)) / columns)
local cardHeight = math.floor((height - top - bottom - gap * (rows - 1)) / rows)
for index, card in ipairs(cards) do
local column = (index - 1) % columns
local row = math.floor((index - 1) / columns)
drawCard(card, margin + column * (cardWidth + gap), top + row * (cardHeight + gap), cardWidth, cardHeight)
end
end
gui.drawButtonHints("<<", connected and "Refresh" or "", "", "")
gui.refresh(REFRESH_HALF)
needsDraw = false
end
function init()
local configError = loadConfig()
if configError then
screen = configError
return
end
screen = "Connecting to Wi-Fi..."
connecting = true
net.wifiConnect()
end
function draw()
if input.wasPressed("back") then
if connecting or connected then net.wifiDisconnect() end
sys.exit()
return
end
if connecting then
local wifi = net.wifiStatus()
if wifi == "connected" then
connecting = false
connected = true
fetchCards()
elseif wifi == "failed" then
connecting = false
screen = "Wi-Fi connection failed"
needsDraw = true
end
elseif connected and input.wasPressed("confirm") then
fetchCards()
end
if needsDraw then render() end
end