fix(hassio): recover from CPU budget kills that froze the panel on (#205)
"Connecting"
This commit is contained in:
+143
-34
@@ -17,6 +17,10 @@ local PENDING_TIMEOUT_SECONDS = 8
|
||||
local view = "list" -- "list" | "browser"
|
||||
local allEntities = {}
|
||||
local browserLoading = false
|
||||
local browserLoadingSince = nil
|
||||
local browserFailed = false
|
||||
|
||||
local BROWSER_TIMEOUT_SECONDS = 20
|
||||
local searchText = ""
|
||||
local monitoredEntities = {} -- current monitored list (owned here, saved to file)
|
||||
local panelOpenCount = 0
|
||||
@@ -51,11 +55,13 @@ local function getCleanToken()
|
||||
return haToken:gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
local function haPost(path, body)
|
||||
local function haPost(path, body, onFailure)
|
||||
local url = getCleanUrl()
|
||||
if not url or url == "" then return end
|
||||
local cleanToken = getCleanToken()
|
||||
if cleanToken == "" then return end
|
||||
if not url or url == "" or cleanToken == "" then
|
||||
if onFailure then onFailure() end
|
||||
return
|
||||
end
|
||||
local encodedBody = noctalia.json.encode(body)
|
||||
|
||||
noctalia.http({
|
||||
@@ -66,11 +72,16 @@ local function haPost(path, body)
|
||||
"Content-Type: application/json",
|
||||
},
|
||||
body = encodedBody,
|
||||
}, function(response: HttpResponse)end)
|
||||
}, function(response: HttpResponse)
|
||||
local httpStatus = response and response.status or 0
|
||||
if httpStatus >= 200 and httpStatus < 300 then return end
|
||||
noctalia.log("Home Assistant rejected " .. path .. ": status " .. tostring(httpStatus))
|
||||
if onFailure then onFailure() end
|
||||
end)
|
||||
end
|
||||
|
||||
local function callService(domain, service, entityId)
|
||||
haPost("/api/services/" .. domain .. "/" .. service, { entity_id = entityId })
|
||||
local function callService(domain, service, entityId, onFailure)
|
||||
haPost("/api/services/" .. domain .. "/" .. service, { entity_id = entityId }, onFailure)
|
||||
end
|
||||
|
||||
local MANAGED_ENTITIES_FILE = "managed_entities.json"
|
||||
@@ -135,19 +146,34 @@ local function toggleBrowserPin(entityId)
|
||||
render()
|
||||
end
|
||||
|
||||
local function fetchAllEntities()
|
||||
if browserLoading then return end
|
||||
-- /api/states is megabytes on a large instance and decoding it overruns the host's
|
||||
-- per-callback CPU budget. The tab and newline outside the {{ }} are the record
|
||||
-- separators; the ones inside replace() are escapes for Jinja to interpret.
|
||||
local ENTITY_LIST_TEMPLATE =
|
||||
"{% for s in states %}{{ s.entity_id }}\t{{ s.name | replace('\\t',' ') | replace('\\n',' ') }}\n{% endfor %}"
|
||||
|
||||
-- HA iterates `states` in entity_id order, so this is normally just the linear check.
|
||||
local function sortEntitiesIfNeeded(list)
|
||||
for i = 2, #list do
|
||||
if list[i].entity_id < list[i - 1].entity_id then
|
||||
table.sort(list, function(a, b) return a.entity_id < b.entity_id end)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function finishBrowserLoad(list, failed)
|
||||
allEntities = list
|
||||
browserLoading = false
|
||||
browserLoadingSince = nil
|
||||
browserFailed = failed == true
|
||||
render()
|
||||
end
|
||||
|
||||
-- Continues an already-started load, so it deliberately doesn't re-check browserLoading.
|
||||
local function fetchAllEntitiesFromStates()
|
||||
local cleanToken = getCleanToken()
|
||||
local url = getCleanUrl()
|
||||
if not url or url == "" or cleanToken == "" then
|
||||
allEntities = {}
|
||||
browserLoading = false
|
||||
render()
|
||||
return
|
||||
end
|
||||
|
||||
browserLoading = true
|
||||
render()
|
||||
|
||||
noctalia.http({
|
||||
url = url .. "/api/states",
|
||||
@@ -155,23 +181,88 @@ local function fetchAllEntities()
|
||||
"Authorization: Bearer " .. cleanToken,
|
||||
}
|
||||
}, function(response: HttpResponse)
|
||||
browserLoading = false
|
||||
if response and response.body and response.body ~= "" then
|
||||
local states = noctalia.json.decode(response.body)
|
||||
if type(states) == "table" then
|
||||
allEntities = {}
|
||||
for _, s in ipairs(states) do
|
||||
local attrs = s.attributes or {}
|
||||
table.insert(allEntities, {
|
||||
entity_id = s.entity_id,
|
||||
friendly_name = attrs.friendly_name or s.entity_id,
|
||||
domain = s.entity_id:match("^([^.]+)"),
|
||||
})
|
||||
end
|
||||
table.sort(allEntities, function(a, b) return a.entity_id < b.entity_id end)
|
||||
end
|
||||
local httpStatus = response and response.status or 0
|
||||
local body = response and response.body or ""
|
||||
if httpStatus < 200 or httpStatus >= 300 or body == "" then
|
||||
noctalia.log("Could not list entities: /api/states returned " .. tostring(httpStatus))
|
||||
finishBrowserLoad({}, true)
|
||||
return
|
||||
end
|
||||
|
||||
local states = noctalia.json.decode(body)
|
||||
if type(states) ~= "table" then
|
||||
noctalia.log("Could not list entities: /api/states was not decodable")
|
||||
finishBrowserLoad({}, true)
|
||||
return
|
||||
end
|
||||
|
||||
local list = {}
|
||||
for _, s in ipairs(states) do
|
||||
local attrs = s.attributes or {}
|
||||
table.insert(list, {
|
||||
entity_id = s.entity_id,
|
||||
friendly_name = attrs.friendly_name or s.entity_id,
|
||||
domain = s.entity_id:match("^([^.]+)"),
|
||||
})
|
||||
end
|
||||
sortEntitiesIfNeeded(list)
|
||||
finishBrowserLoad(list)
|
||||
end)
|
||||
end
|
||||
|
||||
local function fetchAllEntities()
|
||||
if browserLoading then return end
|
||||
local cleanToken = getCleanToken()
|
||||
local url = getCleanUrl()
|
||||
if not url or url == "" or cleanToken == "" then
|
||||
allEntities = {}
|
||||
browserLoading = false
|
||||
browserLoadingSince = nil
|
||||
render()
|
||||
return
|
||||
end
|
||||
|
||||
browserLoading = true
|
||||
browserLoadingSince = os.clock()
|
||||
browserFailed = false
|
||||
render()
|
||||
|
||||
noctalia.http({
|
||||
url = url .. "/api/template",
|
||||
method = "POST",
|
||||
headers = {
|
||||
"Authorization: Bearer " .. cleanToken,
|
||||
"Content-Type: application/json",
|
||||
},
|
||||
body = noctalia.json.encode({ template = ENTITY_LIST_TEMPLATE }),
|
||||
}, function(response: HttpResponse)
|
||||
local httpStatus = response and response.status or 0
|
||||
local body = response and response.body or ""
|
||||
|
||||
if httpStatus < 200 or httpStatus >= 300 or body == "" then
|
||||
noctalia.log("Entity list template unavailable (status " .. tostring(httpStatus)
|
||||
.. "), falling back to /api/states")
|
||||
fetchAllEntitiesFromStates()
|
||||
return
|
||||
end
|
||||
|
||||
local list = {}
|
||||
for entityId, name in body:gmatch("([^\t\n]+)\t([^\n]*)") do
|
||||
table.insert(list, {
|
||||
entity_id = entityId,
|
||||
friendly_name = name ~= "" and name or entityId,
|
||||
domain = entityId:match("^([^.]+)"),
|
||||
})
|
||||
end
|
||||
|
||||
if #list == 0 then
|
||||
noctalia.log("Entity list template returned nothing usable, falling back to /api/states")
|
||||
fetchAllEntitiesFromStates()
|
||||
return
|
||||
end
|
||||
|
||||
sortEntitiesIfNeeded(list)
|
||||
finishBrowserLoad(list)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -229,7 +320,10 @@ local function actEntity(eid)
|
||||
local newState = entity.state == "on" and "off" or "on"
|
||||
pendingToggle[eid] = newState
|
||||
pendingSince[eid] = os.clock()
|
||||
callService(d, "toggle", eid)
|
||||
callService(d, "toggle", eid, function()
|
||||
clearPending(eid)
|
||||
render()
|
||||
end)
|
||||
render()
|
||||
end
|
||||
end
|
||||
@@ -309,6 +403,7 @@ function onBackToList()
|
||||
view = "list"
|
||||
allEntities = {}
|
||||
searchText = ""
|
||||
browserFailed = false
|
||||
render()
|
||||
end
|
||||
|
||||
@@ -495,6 +590,9 @@ local function buildBrowserBody()
|
||||
if browserLoading then
|
||||
return ui.label({ text = tr("loading_entities"), color = "on_surface_variant" })
|
||||
end
|
||||
if browserFailed then
|
||||
return ui.label({ text = tr("load_entities_failed"), color = "error" })
|
||||
end
|
||||
if #allEntities == 0 then
|
||||
return ui.label({ text = tr("no_entities_found"), color = "on_surface_variant" })
|
||||
end
|
||||
@@ -573,7 +671,18 @@ function onClose()
|
||||
end
|
||||
|
||||
function update()
|
||||
if reconcilePending(os.clock()) then
|
||||
local now = os.clock()
|
||||
|
||||
if browserLoading and browserLoadingSince and (now - browserLoadingSince) > BROWSER_TIMEOUT_SECONDS then
|
||||
browserLoading = false
|
||||
browserLoadingSince = nil
|
||||
browserFailed = true
|
||||
reconcilePending(now)
|
||||
render()
|
||||
return
|
||||
end
|
||||
|
||||
if reconcilePending(now) then
|
||||
render()
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user