185 lines
6.0 KiB
Lua
185 lines
6.0 KiB
Lua
-- DESCRIPTION: Home Assistant status cards
|
|
|
|
local CONFIG_PATH = "/plugins/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
|