diff --git a/lib/base-node.js b/lib/base-node.js index 1d727f9708..4c7ab74257 100644 --- a/lib/base-node.js +++ b/lib/base-node.js @@ -182,6 +182,15 @@ class BaseNode { }; if (this.websocketClient) { + if (this.websocketClient.isHomeAssistantRunning) { + connectionStatus = { + shape: 'dot', + fill: 'green', + text: 'running', + }; + return connectionStatus; + } + switch (this.connectionState) { case this.websocketClient.CONNECTING: connectionStatus = { @@ -193,7 +202,7 @@ class BaseNode { case this.websocketClient.CONNECTED: connectionStatus = { shape: 'dot', - fill: 'green', + fill: 'yellow', text: 'node-red:common.status.connected', }; break; @@ -262,6 +271,13 @@ class BaseNode { return this.websocketClient && this.websocketClient.connectionState; } + get isHomeAssistantRunning() { + return ( + this.websocketClient.isConnected && + this.websocketClient.isHomeAssistantRunning + ); + } + getPrettyDate() { return new Date().toLocaleDateString('en-US', { month: 'short', diff --git a/lib/events-node.js b/lib/events-node.js index 98e65a2ef9..fd9595f48b 100644 --- a/lib/events-node.js +++ b/lib/events-node.js @@ -23,35 +23,19 @@ class EventsNode extends BaseNode { this.integrationErrorMessage = 'Node-RED custom integration needs to be installed in Home Assistant for this node to function correctly.'; - this.addEventClientListener( - 'ha_client:close', - this.onHaEventsClose.bind(this) + // Setup event listeners + const events = { + 'ha_client:close': this.onHaEventsClose, + 'ha_client:open': this.onHaEventsOpen, + 'ha_client:error': this.onHaEventsError, + 'ha_client:connecting': this.onHaEventsConnecting, + updateNodeStatus: this.onHaEventsUpdateStatus, + integration: this.onHaIntegration, + 'ha_client:running': this.onHaEventsRunning, + }; + Object.entries(events).forEach(([event, callback]) => + this.addEventClientListener(event, callback.bind(this)) ); - this.addEventClientListener( - 'ha_client:open', - this.onHaEventsOpen.bind(this) - ); - this.addEventClientListener( - 'ha_client:error', - this.onHaEventsError.bind(this) - ); - this.addEventClientListener( - 'ha_client:connecting', - this.onHaEventsConnecting.bind(this) - ); - this.addEventClientListener( - 'updateNodeStatus', - this.onHaEventsUpdateStatus.bind(this) - ); - this.addEventClientListener( - `ha_events:config_update`, - this.onHaConfigUpdate.bind(this) - ); - this.addEventClientListener( - `integration`, - this.onHaIntegration.bind(this) - ); - this.updateConnectionStatus(); } @@ -90,6 +74,10 @@ class EventsNode extends BaseNode { this.updateConnectionStatus(); } + onHaEventsRunning() { + this.updateConnectionStatus(); + } + onHaEventsError(err) { if (err.message) this.error(err.message); } diff --git a/lib/ha-websocket.js b/lib/ha-websocket.js index 17ce998a6a..9cebe62700 100644 --- a/lib/ha-websocket.js +++ b/lib/ha-websocket.js @@ -22,6 +22,7 @@ class HaWebsocket extends EventEmitter { this.subscribedEvents = new Set(); this.unsubCallback = {}; this.integrationVersion = 0; + this.isHomeAssistantRunning = false; this.setMaxListeners(0); } @@ -92,30 +93,53 @@ class HaWebsocket extends EventEmitter { this.emit('ha_client:connected'); // Client events - this.client.addEventListener('ready', this.onClientOpen.bind(this)); - this.client.addEventListener( - 'disconnected', - this.onClientClose.bind(this) - ); - this.client.addEventListener( - 'reconnect-error', - this.onClientError.bind(this) + const events = { + ready: this.onClientOpen, + disconnected: this.onClientClose, + 'reconnect-error': this.onClientError, + }; + Object.entries(events).forEach(([event, callback]) => + this.client.addEventListener(event, callback.bind(this)) ); + this.onStatesLoadedAndRunning(); // Home Assistant Events + homeassistant.subscribeConfig(this.client, (config) => + this.onClientConfigUpdate(config) + ); homeassistant.subscribeEntities(this.client, (ent) => this.onClientStates(ent) ); homeassistant.subscribeServices(this.client, (ent) => this.onClientServices(ent) ); - homeassistant.subscribeConfig(this.client, (config) => - this.onClientConfigUpdate(config) - ); return true; } + async getUser() { + return homeassistant.getUser(this.client); + } + + onHomeAssistantRunning() { + if (!this.isHomeAssistantRunning) { + this.isHomeAssistantRunning = true; + this.emit('ha_client:running'); + } + } + + onStatesLoadedAndRunning() { + const statesLoaded = new Promise((resolve, reject) => { + this.once('ha_client:states_loaded', resolve); + }); + const homeAssinstantRunning = new Promise((resolve, reject) => { + this.once('ha_client:running', resolve); + }); + Promise.all([statesLoaded, homeAssinstantRunning]).then(([states]) => { + this.emit('ha_client:initial_connection_ready', states); + }); + } + async subscribeEvents(events) { const currentEvents = new Set(Object.values(events)); @@ -206,6 +230,12 @@ class HaWebsocket extends EventEmitter { return; } + // Don't emit events if HA is not in a running state. Keep functioning + // the same as HA prior to vesrion 0.111.0. + if (!this.isHomeAssistantRunning) { + return; + } + if (msg) { const eventType = msg.event_type; const entityId = msg.data && msg.data.entity_id; @@ -254,8 +284,14 @@ class HaWebsocket extends EventEmitter { } async onClientConfigUpdate(config) { - this.integrationVersion = 0; - if (config.components.includes('nodered')) { + // Prior to HA 0.111.0 state didn't exist + if (config.state === undefined || config.state === 'RUNNING') { + this.onHomeAssistantRunning(); + } + if ( + config.components.includes('nodered') && + this.integrationVersion === 0 + ) { try { this.integrationVersion = await this.send({ type: 'nodered/version', @@ -267,12 +303,14 @@ class HaWebsocket extends EventEmitter { onClientOpen() { this.integrationVersion = 0; + this.isHomeAssistantRunning = false; this.connectionState = HaWebsocket.CONNECTED; this.emit('ha_client:open'); } onClientClose() { this.integrationVersion = 0; + this.isHomeAssistantRunning = false; this.connectionState = HaWebsocket.DISCONNECTED; this.emit('ha_client:close'); @@ -307,10 +345,6 @@ class HaWebsocket extends EventEmitter { } } - async getUser() { - return homeassistant.getUser(this.client); - } - async getStates(entityId, forceRefresh = false) { if (Object.keys(this.states).length === 0 || forceRefresh) { // TODO: handle forceRefresh and empty state object diff --git a/nodes/events-all/events-all.js b/nodes/events-all/events-all.js index b5120cd321..5a9cb5384f 100644 --- a/nodes/events-all/events-all.js +++ b/nodes/events-all/events-all.js @@ -92,6 +92,11 @@ module.exports = function (RED) { this.clientEvent('connecting'); } + onHaEventsRunning() { + super.onHaEventsRunning(); + this.clientEvent('running'); + } + onHaEventsError(err) { super.onHaEventsError(err); if (err) { diff --git a/nodes/events-state-changed/events-state-changed.js b/nodes/events-state-changed/events-state-changed.js index 59d16b0abd..7b193aa4e3 100644 --- a/nodes/events-state-changed/events-state-changed.js +++ b/nodes/events-state-changed/events-state-changed.js @@ -38,7 +38,7 @@ module.exports = function (RED) { this.onDeploy(); } else { this.addEventClientListener( - 'ha_client:states_loaded', + 'ha_client:initial_connection_ready', this.onStatesLoaded.bind(this) ); } diff --git a/nodes/poll-state/poll-state.js b/nodes/poll-state/poll-state.js index 47fdd1e694..e0174fb7a9 100644 --- a/nodes/poll-state/poll-state.js +++ b/nodes/poll-state/poll-state.js @@ -57,7 +57,7 @@ module.exports = function (RED) { if (this.nodeConfig.outputinitially) { this.addEventClientListener( - 'ha_client:states_loaded', + 'ha_client:initial_connection_ready', this.onTimer.bind(this) ); } @@ -72,7 +72,9 @@ module.exports = function (RED) { } async onTimer(triggered = false) { - if (!this.isConnected || this.isEnabled === false) return; + if (!this.isHomeAssistantRunning || this.isEnabled === false) { + return; + } const pollState = await this.nodeConfig.server.homeAssistant.getStates( this.nodeConfig.entity_id diff --git a/nodes/trigger-state/trigger-state.js b/nodes/trigger-state/trigger-state.js index b1328eebf9..797769b6d8 100644 --- a/nodes/trigger-state/trigger-state.js +++ b/nodes/trigger-state/trigger-state.js @@ -39,11 +39,11 @@ module.exports = function (RED) { if (this.nodeConfig.outputinitially) { // Here for when the node is deploy without the server config being deployed - if (this.isConnected) { + if (this.isHomeAssistantRunning) { this.onDeploy(); } else { this.addEventClientListener( - 'ha_client:states_loaded', + 'ha_client:initial_connection_ready', this.onStatesLoaded.bind(this) ); }