From 5cdc4e2ce2a60caad7fa121e9f0a1217791f06db Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 1 Mar 2026 08:59:15 +0000 Subject: [PATCH] Fix stale data after HA outage recovery Two bugs caused the module to show outdated data permanently after HA came back online: 1. refreshTimer was only checked at section level, but config sets it at the module config level. The setInterval never started, so there was no periodic re-render fallback when the WebSocket died. 2. _closeCircuit replayed queued templates but never reconnected the WebSocket. Without WS, no state_changed events fire, so the only render path was the (broken) refreshTimer. Also fixes a race condition in _healthCheck where breaker.state was briefly set to 'closed' before calling _openCircuit on failure. Now uses 'half-open' state instead. --- MMM-HomeAssistantDisplay.js | 26 ++++++++++++++++++++------ node_helper.js | 18 +++++++++++++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/MMM-HomeAssistantDisplay.js b/MMM-HomeAssistantDisplay.js index aed2b2b..ee2934e 100755 --- a/MMM-HomeAssistantDisplay.js +++ b/MMM-HomeAssistantDisplay.js @@ -52,15 +52,29 @@ Module.register("MMM-HomeAssistantDisplay", { entity: section.triggerEntities[entity] }); } - // Set up a timer to trigger re-rendering outside of any entity state update - if (section.refreshTimer) { - setInterval(()=> { - this.renderTemplates("timeout"); - this.updateDom(); - }, section.refreshTimer * 1000); + } + } + + // Refresh timer: check section-level first, then fall back to config-level. + // One interval per module instance — renderTemplates already hits all sections. + var refreshInterval = null; + if (this.config.sections) { + for (const sectioid in this.config.sections) { + if (this.config.sections[sectioid].refreshTimer) { + refreshInterval = this.config.sections[sectioid].refreshTimer; + break; } } } + if (!refreshInterval && this.config.refreshTimer) { + refreshInterval = this.config.refreshTimer; + } + if (refreshInterval) { + setInterval(() => { + this.renderTemplates("refreshTimer"); + this.updateDom(); + }, refreshInterval * 1000); + } this.renderTemplates("foo"); self.updateDom(self.config.animationSpeed); }, diff --git a/node_helper.js b/node_helper.js index dd81faf..da4fc0c 100644 --- a/node_helper.js +++ b/node_helper.js @@ -121,6 +121,7 @@ function _getBreaker(identifier) { function _openCircuit(identifier) { const breaker = this._getBreaker(identifier); if (breaker.state === 'open') return; // already open + // Accepts 'closed' (first failure) or 'half-open' (health check retry failed) breaker.state = 'open'; breaker.failCount++; @@ -150,6 +151,16 @@ function _closeCircuit(identifier) { breaker.retryTimer = null; } + // Reconnect WebSocket — it likely died during the outage and won't + // recover on its own because the frontend only sends RECONNECT_WS in + // response to HASSWS_DISCONNECTED, which the node_helper may never + // have emitted if the socket died silently. + const conn = this.connections[identifier]; + if (conn && conn.connectionConfig) { + this.logger.info(`Reconnecting WebSocket for ${identifier} after circuit recovery`); + this.backoffWSConnection(identifier, conn.connectionConfig); + } + // Replay queued template evaluations const queue = breaker.pendingQueue.splice(0); if (queue.length > 0) { @@ -189,8 +200,8 @@ async function _healthCheck(identifier) { this._closeCircuit(identifier); } catch (err) { this.logger.info(`Health check failed for ${identifier}: ${err.message}`); - // Re-arm with increased backoff - breaker.state = 'closed'; // briefly close so _openCircuit fires + // Re-arm with increased backoff — reset state so _openCircuit can fire + breaker.state = 'half-open'; this._openCircuit(identifier); } } @@ -277,7 +288,8 @@ async function connect(payload) { this.logger.info(`HomeAssistant connected for ${payload.identifier}`); this.connections[payload.identifier] = { hass, - entities: [] + entities: [], + connectionConfig, }; await this.backoffWSConnection(payload.identifier, connectionConfig)