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