fix(hassio): Resolve perpetual "connecting" and add unreachable state (#78)
* fix(service_sse): Fixed perpetual connecting from hung states request. * fix(service_sse): Distinguish unreachable server from disconnected/auth states. * chore(plugin): Bumped version to 2.0.3
This commit is contained in:
@@ -39,6 +39,8 @@ local function getStatusText()
|
|||||||
return noctalia.tr("widget.status_connecting")
|
return noctalia.tr("widget.status_connecting")
|
||||||
elseif status == "disconnected" then
|
elseif status == "disconnected" then
|
||||||
return noctalia.tr("widget.status_disconnected")
|
return noctalia.tr("widget.status_disconnected")
|
||||||
|
elseif status == "unreachable" then
|
||||||
|
return noctalia.tr("widget.status_unreachable")
|
||||||
elseif status == "auth_failed" then
|
elseif status == "auth_failed" then
|
||||||
return noctalia.tr("widget.status_auth_failed")
|
return noctalia.tr("widget.status_auth_failed")
|
||||||
else
|
else
|
||||||
@@ -519,6 +521,9 @@ local function buildListBody(ids)
|
|||||||
if status == "disconnected" then
|
if status == "disconnected" then
|
||||||
return ui.label({ text = tr("disconnected_reconnecting"), color = "error" })
|
return ui.label({ text = tr("disconnected_reconnecting"), color = "error" })
|
||||||
end
|
end
|
||||||
|
if status == "unreachable" then
|
||||||
|
return ui.label({ text = tr("unreachable"), color = "error" })
|
||||||
|
end
|
||||||
if #ids == 0 then
|
if #ids == 0 then
|
||||||
return ui.label({
|
return ui.label({
|
||||||
text = status == "connected"
|
text = status == "connected"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
id = "pozzoo/hassio"
|
id = "pozzoo/hassio"
|
||||||
name = "Home Assistant"
|
name = "Home Assistant"
|
||||||
version = "2.0.2"
|
version = "2.0.3"
|
||||||
plugin_api = 4
|
plugin_api = 4
|
||||||
author = "Pozzoo"
|
author = "Pozzoo"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+48
-33
@@ -35,9 +35,10 @@ local streamHandle = nil
|
|||||||
local MAX_RECONNECT_RETRIES = 10
|
local MAX_RECONNECT_RETRIES = 10
|
||||||
local CONNECTED_POLL_INTERVAL_MS = 30000
|
local CONNECTED_POLL_INTERVAL_MS = 30000
|
||||||
local RECONNECT_POLL_INTERVAL_MS = 1000
|
local RECONNECT_POLL_INTERVAL_MS = 1000
|
||||||
|
local BACKOFF_POLL_INTERVAL_MS = 60000
|
||||||
local retryCount = 0
|
local retryCount = 0
|
||||||
local maxRetriesNotified = false
|
local maxRetriesNotified = false
|
||||||
local suspended = false
|
local backoff = false
|
||||||
local connectionRequestInFlight = false
|
local connectionRequestInFlight = false
|
||||||
local connectingSince = nil
|
local connectingSince = nil
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ end
|
|||||||
local function syncUpdateInterval()
|
local function syncUpdateInterval()
|
||||||
if status == "connected" and sseActive then
|
if status == "connected" and sseActive then
|
||||||
noctalia.setUpdateInterval(CONNECTED_POLL_INTERVAL_MS)
|
noctalia.setUpdateInterval(CONNECTED_POLL_INTERVAL_MS)
|
||||||
|
elseif backoff then
|
||||||
|
noctalia.setUpdateInterval(BACKOFF_POLL_INTERVAL_MS)
|
||||||
else
|
else
|
||||||
noctalia.setUpdateInterval(RECONNECT_POLL_INTERVAL_MS)
|
noctalia.setUpdateInterval(RECONNECT_POLL_INTERVAL_MS)
|
||||||
end
|
end
|
||||||
@@ -70,14 +73,19 @@ local function haRequest(endpoint, method, body, callback)
|
|||||||
jsonBody = noctalia.json.encode(body)
|
jsonBody = noctalia.json.encode(body)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local headers = {
|
||||||
|
"Authorization: Bearer " .. cleanToken,
|
||||||
|
}
|
||||||
|
-- Only advertise a JSON body when we actually send one.
|
||||||
|
if jsonBody then
|
||||||
|
table.insert(headers, "Content-Type: application/json")
|
||||||
|
end
|
||||||
|
|
||||||
local launched = noctalia.http({
|
local launched = noctalia.http({
|
||||||
url = url,
|
url = url,
|
||||||
method = method,
|
method = method,
|
||||||
body = jsonBody,
|
body = jsonBody,
|
||||||
headers = {
|
headers = headers,
|
||||||
"Authorization: Bearer " .. cleanToken,
|
|
||||||
"Content-Type: application/json",
|
|
||||||
}
|
|
||||||
}, function(response: HttpResponse)
|
}, function(response: HttpResponse)
|
||||||
if not response then
|
if not response then
|
||||||
callback({ ok = false, status = 0, body = "No response" })
|
callback({ ok = false, status = 0, body = "No response" })
|
||||||
@@ -213,13 +221,32 @@ local function processSSELine(line)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function fetchInitialStates()
|
local function fetchInitialStates()
|
||||||
if not isConfigured() or suspended or connectionRequestInFlight then return end
|
if not isConfigured() or connectionRequestInFlight then return end
|
||||||
connectionRequestInFlight = true
|
connectionRequestInFlight = true
|
||||||
connectingSince = os.clock()
|
connectingSince = os.clock()
|
||||||
local launched = haRequest("/api/states", "GET", nil, function(res)
|
local launched = haRequest("/api/states", "GET", nil, function(res)
|
||||||
connectionRequestInFlight = false
|
connectionRequestInFlight = false
|
||||||
connectingSince = nil
|
connectingSince = nil
|
||||||
|
|
||||||
|
-- status 0 = no HTTP response at all (couldn't connect / DNS / timeout): the
|
||||||
|
-- server is unreachable, which is a distinct case from an auth or HTTP error.
|
||||||
|
if res.status == 0 then
|
||||||
|
status = "unreachable"
|
||||||
|
noctalia.log("Cannot reach Home Assistant (connection failed)")
|
||||||
|
noctalia.state.set("connection_status", status)
|
||||||
|
syncUpdateInterval()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if res.status == 401 or (res.body and (res.body:match("^401") or res.body:match("Unauthorized"))) then
|
||||||
|
status = "auth_failed"
|
||||||
|
noctalia.log("Authentication failed - check your token")
|
||||||
|
noctalia.notifyError(noctalia.tr("notifications.auth_failed"))
|
||||||
|
noctalia.state.set("connection_status", status)
|
||||||
|
syncUpdateInterval()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if not res.body or res.body == "" then
|
if not res.body or res.body == "" then
|
||||||
noctalia.log("Empty response body from /api/states")
|
noctalia.log("Empty response body from /api/states")
|
||||||
status = "disconnected"
|
status = "disconnected"
|
||||||
@@ -228,16 +255,7 @@ local function fetchInitialStates()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if not res.ok or res.status == 401 or res.body:match("^401") or res.body:match("Unauthorized") then
|
if not res.ok or res.status >= 400 then
|
||||||
status = "auth_failed"
|
|
||||||
noctalia.log("Authentication failed - check your token")
|
|
||||||
noctalia.notifyError(noctalia.tr("notifications.auth_failed"))
|
|
||||||
noctalia.state.set("connection_status", status)
|
|
||||||
syncUpdateInterval()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if not res.ok or (res.status and res.status >= 400) then
|
|
||||||
status = "disconnected"
|
status = "disconnected"
|
||||||
noctalia.log("HTTP request failed: status " .. (res.status or "unknown"))
|
noctalia.log("HTTP request failed: status " .. (res.status or "unknown"))
|
||||||
noctalia.state.set("connection_status", status)
|
noctalia.state.set("connection_status", status)
|
||||||
@@ -258,7 +276,7 @@ local function fetchInitialStates()
|
|||||||
noctalia.state.set("connection_status", status)
|
noctalia.state.set("connection_status", status)
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
maxRetriesNotified = false
|
maxRetriesNotified = false
|
||||||
suspended = false
|
backoff = false
|
||||||
|
|
||||||
entityStates = {}
|
entityStates = {}
|
||||||
local count = 0
|
local count = 0
|
||||||
@@ -291,14 +309,13 @@ local function fetchInitialStates()
|
|||||||
if not launched then
|
if not launched then
|
||||||
connectionRequestInFlight = false
|
connectionRequestInFlight = false
|
||||||
connectingSince = nil
|
connectingSince = nil
|
||||||
status = "disconnected"
|
status = "unreachable"
|
||||||
noctalia.state.set("connection_status", status)
|
noctalia.state.set("connection_status", status)
|
||||||
syncUpdateInterval()
|
syncUpdateInterval()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function startSSEStream()
|
function startSSEStream()
|
||||||
if suspended then return false end
|
|
||||||
if sseActive then return true end
|
if sseActive then return true end
|
||||||
|
|
||||||
local cleanToken = getCleanToken()
|
local cleanToken = getCleanToken()
|
||||||
@@ -327,8 +344,6 @@ function startSSEStream()
|
|||||||
sseActive = false
|
sseActive = false
|
||||||
currentEvent = {}
|
currentEvent = {}
|
||||||
|
|
||||||
if suspended then return end
|
|
||||||
|
|
||||||
if result and result.ok and result.status == 401 then
|
if result and result.ok and result.status == 401 then
|
||||||
status = "auth_failed"
|
status = "auth_failed"
|
||||||
noctalia.log("Authentication failed - check your token")
|
noctalia.log("Authentication failed - check your token")
|
||||||
@@ -382,7 +397,7 @@ noctalia.state.watch("panel_open_signal", function(value)
|
|||||||
if value then
|
if value then
|
||||||
retryCount = 0
|
retryCount = 0
|
||||||
maxRetriesNotified = false
|
maxRetriesNotified = false
|
||||||
suspended = false
|
backoff = false
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -396,10 +411,9 @@ function update()
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if suspended then
|
-- noctalia.http exposes no transport timeout, so a hung request would otherwise
|
||||||
return
|
-- pin status on "connecting" indefinitely. Treat 15s without a callback as a
|
||||||
end
|
-- failed attempt and fall through to the reconnect path below.
|
||||||
|
|
||||||
if status == "connecting" and connectingSince and (os.clock() - connectingSince) > 15 then
|
if status == "connecting" and connectingSince and (os.clock() - connectingSince) > 15 then
|
||||||
connectionRequestInFlight = false
|
connectionRequestInFlight = false
|
||||||
connectingSince = nil
|
connectingSince = nil
|
||||||
@@ -408,18 +422,19 @@ function update()
|
|||||||
syncUpdateInterval()
|
syncUpdateInterval()
|
||||||
end
|
end
|
||||||
|
|
||||||
if status == "disconnected" then
|
if status == "disconnected" or status == "unreachable" then
|
||||||
retryCount = retryCount + 1
|
retryCount = retryCount + 1
|
||||||
if retryCount >= MAX_RECONNECT_RETRIES then
|
if retryCount >= MAX_RECONNECT_RETRIES and not backoff then
|
||||||
|
-- After the initial burst of fast retries, fall back to a
|
||||||
|
-- slow poll and keep trying, so the service reconnects on its own once HA is
|
||||||
|
-- reachable again (previously it suspended and only revived on panel open).
|
||||||
|
backoff = true
|
||||||
if not maxRetriesNotified then
|
if not maxRetriesNotified then
|
||||||
maxRetriesNotified = true
|
maxRetriesNotified = true
|
||||||
noctalia.notifyError(noctalia.tr("notifications.reconnect_failed", { count = MAX_RECONNECT_RETRIES }))
|
noctalia.notifyError(noctalia.tr("notifications.reconnect_failed", { count = MAX_RECONNECT_RETRIES }))
|
||||||
end
|
end
|
||||||
suspended = true
|
syncUpdateInterval()
|
||||||
status = "disconnected"
|
end
|
||||||
noctalia.state.set("connection_status", status)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
status = "connecting"
|
status = "connecting"
|
||||||
noctalia.state.set("connection_status", status)
|
noctalia.state.set("connection_status", status)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"color_temp": "Color Temperature",
|
"color_temp": "Color Temperature",
|
||||||
"connecting": "Connecting…",
|
"connecting": "Connecting…",
|
||||||
"disconnected_reconnecting": "Disconnected. Reconnecting…",
|
"disconnected_reconnecting": "Disconnected. Reconnecting…",
|
||||||
|
"unreachable": "Cannot reach Home Assistant. Check the URL and that the server is running.",
|
||||||
"hue": "Hue",
|
"hue": "Hue",
|
||||||
"loading_entities": "Loading entities…",
|
"loading_entities": "Loading entities…",
|
||||||
"manage_entities_title": "Manage Entities",
|
"manage_entities_title": "Manage Entities",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"status_connected": "Connected",
|
"status_connected": "Connected",
|
||||||
"status_connecting": "Connecting",
|
"status_connecting": "Connecting",
|
||||||
"status_disconnected": "Disconnected",
|
"status_disconnected": "Disconnected",
|
||||||
|
"status_unreachable": "Unreachable",
|
||||||
"status_unconfigured": "Not Configured",
|
"status_unconfigured": "Not Configured",
|
||||||
"tooltip": "Home Assistant: {status}",
|
"tooltip": "Home Assistant: {status}",
|
||||||
"tooltip_entities": "Home Assistant: {status} ({count} entities)"
|
"tooltip_entities": "Home Assistant: {status} ({count} entities)"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"color_temp": "Temperatura de Cor",
|
"color_temp": "Temperatura de Cor",
|
||||||
"connecting": "Conectando…",
|
"connecting": "Conectando…",
|
||||||
"disconnected_reconnecting": "Desconectado. Reconectando…",
|
"disconnected_reconnecting": "Desconectado. Reconectando…",
|
||||||
|
"unreachable": "Não foi possível acessar o Home Assistant. Verifique a URL e se o servidor está em execução.",
|
||||||
"hue": "Matiz",
|
"hue": "Matiz",
|
||||||
"loading_entities": "Carregando entidades…",
|
"loading_entities": "Carregando entidades…",
|
||||||
"manage_entities_title": "Gerenciar Entidades",
|
"manage_entities_title": "Gerenciar Entidades",
|
||||||
@@ -64,6 +65,7 @@
|
|||||||
"status_connected": "Conectado",
|
"status_connected": "Conectado",
|
||||||
"status_connecting": "Conectando",
|
"status_connecting": "Conectando",
|
||||||
"status_disconnected": "Desconectado",
|
"status_disconnected": "Desconectado",
|
||||||
|
"status_unreachable": "Inacessível",
|
||||||
"status_unconfigured": "Não Configurado",
|
"status_unconfigured": "Não Configurado",
|
||||||
"tooltip": "Home Assistant: {status}",
|
"tooltip": "Home Assistant: {status}",
|
||||||
"tooltip_entities": "Home Assistant: {status} ({count} entidades)"
|
"tooltip_entities": "Home Assistant: {status} ({count} entidades)"
|
||||||
|
|||||||
+3
-1
@@ -34,6 +34,8 @@ local function getStatusText()
|
|||||||
return tr("status_connecting")
|
return tr("status_connecting")
|
||||||
elseif status == "disconnected" then
|
elseif status == "disconnected" then
|
||||||
return tr("status_disconnected")
|
return tr("status_disconnected")
|
||||||
|
elseif status == "unreachable" then
|
||||||
|
return tr("status_unreachable")
|
||||||
elseif status == "auth_failed" then
|
elseif status == "auth_failed" then
|
||||||
return tr("status_auth_failed")
|
return tr("status_auth_failed")
|
||||||
else
|
else
|
||||||
@@ -46,7 +48,7 @@ local function getStatusColor()
|
|||||||
return "primary"
|
return "primary"
|
||||||
elseif status == "connecting" then
|
elseif status == "connecting" then
|
||||||
return "on_error"
|
return "on_error"
|
||||||
elseif status == "disconnected" or status == "auth_failed" then
|
elseif status == "disconnected" or status == "unreachable" or status == "auth_failed" then
|
||||||
return "error"
|
return "error"
|
||||||
else
|
else
|
||||||
return "on_surface_variant"
|
return "on_surface_variant"
|
||||||
|
|||||||
Reference in New Issue
Block a user