node-red-contrib-home-assistant-websocket
Version:
Node-RED integration with Home Assistant through websocket and REST API
2 lines (1 loc) • 10.8 kB
JavaScript
"use strict";var S=Object.create;var _=Object.defineProperty;var T=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var L=Object.getPrototypeOf,D=Object.prototype.hasOwnProperty;var w=(o,e)=>{for(var t in e)_(o,t,{get:e[t],enumerable:!0})},f=(o,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of A(e))!D.call(o,n)&&n!==t&&_(o,n,{get:()=>e[n],enumerable:!(i=T(e,n))||i.enumerable});return o};var H=(o,e,t)=>(t=o!=null?S(L(o)):{},f(e||!o||!o.__esModule?_(t,"default",{value:o,enumerable:!0}):t,o)),N=o=>f(_({},"__esModule",{value:!0}),o);var I={};w(I,{ClientEvent:()=>C,ClientState:()=>p,default:()=>m});module.exports=N(I);var E=H(require("debug")),a=require("home-assistant-js-websocket"),c=require("lodash"),r=require("../const"),g=require("../globals"),d=require("./"),l=require("./collections"),v=H(require("./createSocket")),y=require("./heartbeat");const u=(0,E.default)("home-assistant:ws");var p=(s=>(s[s.Connecting=0]="Connecting",s[s.Connected=1]="Connected",s[s.Disconnected=2]="Disconnected",s[s.Error=3]="Error",s[s.Running=4]="Running",s))(p||{}),C=(h=>(h.Close="ha_client:close",h.Connected="ha_client:connected",h.Connecting="ha_client:connecting",h.Error="ha_client:error",h.InitialConnectionReady="initial_connection_ready",h.Integration="integration",h.Open="ha_client:open",h.Ready="ha_client:ready",h.RegistriesLoaded="ha_client:registries_loaded",h.Running="ha_client:running",h.ServicesLoaded="ha_client:services_loaded",h.StatesLoaded="ha_client:states_loaded",h))(C||{});class m{constructor(e,t){this.#a=!1;this.#o=!1;this.#s=new Set;this.#r={};this.#h=!1;this.#d=!1;this.#l=!1;this.#g=!1;this.#u=!1;this.#v=!1;this.areas=[];this.devices=[];this.entities=new Map;this.floors=[];this.labels=[];this.connectionState=2;this.integrationVersion=r.NO_VERSION;this.isHomeAssistantRunning=!1;this.states={};this.services={};this.tags=null;this.#t=e,this.#n=t,this.#n.on("ha_client:connecting",this.onClientConnecting.bind(this)),this.onStatesLoadedAndRunning("initial_connection_ready")}#t;#n;#a;#o;#c;#s;#r;#h;#d;#l;#g;#u;#v;get isConnected(){return this.connectionState===1}get isStatesLoaded(){return this.#o}#e(e,t){return this.#n.emit(e,t)}async connect(){const e=`ws${this.#t.host.substring(4)}/api/websocket`,t={type:"auth",access_token:this.#t.access_token};this.client=await(0,a.createConnection)({createSocket:()=>(0,v.default)({auth:t,connectionDelay:this.#t.connectionDelay,eventBus:this.#n,rejectUnauthorizedCerts:this.#t.rejectUnauthorizedCerts,url:e})}).catch(i=>{this.connectionState=3,this.#e("ha_client:error");let n;switch(i){case a.ERR_CANNOT_CONNECT:n=g.RED._("home-assistant.error.cannot-connect");break;case a.ERR_INVALID_AUTH:n=g.RED._("home-assistant.error.invalid_auth");break;case a.ERR_CONNECTION_LOST:n=g.RED._("home-assistant.error.connection_lost");break;case a.ERR_HASS_HOST_REQUIRED:n=g.RED._("home-assistant.error.hass_host_required");break;case a.ERR_INVALID_HTTPS_TO_HTTP:n="ERR_INVALID_HTTPS_TO_HTTP";break}throw n?new Error(n):i}),await this.#H(),this.#f(),this.onClientOpen(),this.#e("ha_client:connected"),this.#E(),this.#y()}#f(){const[e,t,i]=r.HA_MIN_VERSION.split(".").map(Number);if(!(0,v.atLeastHaVersion)(this.client.haVersion,e,t,i))throw this.connectionState=3,this.client.close(),new Error(g.RED._("home-assistant.error.ha_version_not_supported",{version:this.client.haVersion,min_version:r.HA_MIN_VERSION}))}async#H(){if((await this.getUser()).is_admin===!1)throw this.connectionState=3,this.client.close(),new Error(g.RED._("home-assistant.error.user_not_admin"))}getUser(){return(0,a.getUser)(this.client)}#E(){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))}async#y(){await this.client.subscribeEvents(e=>this.#b(e),r.HA_EVENT_INTEGRATION),(0,a.subscribeConfig)(this.client,e=>this.onClientConfigUpdate(e)),(0,a.subscribeEntities)(this.client,this.onClientStates.bind(this)),(0,a.subscribeServices)(this.client,this.onClientServices.bind(this)),(0,l.subscribeAreaRegistry)(this.client,e=>{this.#h=!0,this.areas=e,this.#i(),this.#e(d.HaEvent.AreaRegistryUpdated,e)}),(0,l.subscribeDeviceRegistry)(this.client,e=>{this.#d=!0,this.devices=e,this.#i(),this.#e(d.HaEvent.DeviceRegistryUpdated,e)}),(0,l.subscribeEntityRegistry)(this.client,e=>{this.#l=!0,e.forEach(t=>this.entities.set(t.entity_id,t)),this.#i(),this.#e(d.HaEvent.EntityRegistryUpdated,e)}),(0,l.subscribeFloorRegistry)(this.client,e=>{this.#g=!0,this.floors=e,this.#i(),this.#e(d.HaEvent.FloorRegistryUpdated,e)}),(0,l.subscribeLabelRegistry)(this.client,e=>{this.#u=!0,this.labels=e,this.#i(),this.#e(d.HaEvent.LabelRegistryUpdated,e)})}#i(){!this.#v&&this.#h&&this.#d&&this.#l&&this.#g&&this.#u&&this.isStatesLoaded&&this.#a&&this.isHomeAssistantRunning&&(this.#e("ha_client:registries_loaded"),this.#v=!0)}get isAllRegistriesLoaded(){return this.#v}#p(){this.isHomeAssistantRunning||(this.isHomeAssistantRunning=!0,this.#e("ha_client:running"),this.integrationVersion===r.NO_VERSION&&this.createIntegrationEvent(r.INTEGRATION_NOT_LOADED),this.#i())}#b(e){const t=this.integrationVersion;switch(e?.data?.type){case r.INTEGRATION_LOADED:this.integrationVersion=e.data.version;break;case r.INTEGRATION_UNLOADED:this.integrationVersion=r.NO_VERSION;break;case r.INTEGRATION_NOT_LOADED:this.#e(r.INTEGRATION_EVENT,e.data.type);return}t!==this.integrationVersion&&this.#e(r.INTEGRATION_EVENT,e?.data?.type)}async subscribeEvents(e){const t=new Set(Object.values(e));if(t.has("__ALL__")){if(this.#s.has("__ALL__"))return;this.#s.forEach(s=>{s!=="__ALL__"&&(this.#r[s](),delete this.#r[s],this.#s.delete(s))}),this.#r.__ALL__=await this.client.subscribeEvents(s=>this.onClientEvents(s)),this.#s.add("__ALL__");return}t.add(d.HaEvent.StateChanged),t.add(d.HaEvent.TagScanned);const i=new Set([...t].filter(s=>!this.#s.has(s))),n=new Set([...this.#s].filter(s=>!t.has(s)));this.#s=new Set([...[...t].filter(s=>this.#s.has(s)),...i]),n.forEach(s=>{this.#r[s](),delete this.#r[s]});for(const s of i)this.#r[s]=await this.client.subscribeEvents(b=>this.onClientEvents(b),s)}subscribeMessage(e,t,i){return this.client.subscribeMessage(e,t,i)}onClientStates(e){!e||Object.keys(e).length===0||(this.states=e,this.#o||(this.#o=!0,this.#e("ha_client:states_loaded",this.states),this.#i()))}onClientServices(e){!e||Object.keys(e).length===0||(this.services=e,this.#e(r.HA_EVENT_SERVICES_UPDATED,this.services),this.#a||(this.#a=!0,this.#e("ha_client:services_loaded"),this.#i()))}onStatesLoadedAndRunning(e="ha_client:ready"){const t=new Promise(n=>{this.#n.once("ha_client:services_loaded",n)}),i=new Promise(n=>{this.#n.once("ha_client:running",n)});Promise.all([t,i]).then(([n])=>{this.#n.emit(e,n)})}onClientEvents(e){if(!e||!e.data)return;const t=e.event_type,i=e?.data?.entity_id,n={event_type:t,entity_id:i,event:e.data,origin:e.origin,time_fired:e.time_fired,context:e.context};if(t===d.HaEvent.StateChanged){const s=n?.event?.new_state;if(s&&i)this.states[i]=s;else{u(`Not processing ${d.HaEvent.StateChanged} event: ${JSON.stringify(n)}`);return}}t&&(this.#e(`${r.HA_EVENTS}:${t}`,n),i&&this.#e(`${r.HA_EVENTS}:${t}:${i}`,n)),this.#e(`${r.HA_EVENTS}:all`,n)}async onClientConfigUpdate(e){if(e.components.includes("nodered")&&this.integrationVersion===r.NO_VERSION)try{const t=await this.getIntegrationVersion();this.createIntegrationEvent(r.INTEGRATION_LOADED,t)}catch{}!this.tags&&e.components.includes("tag")&&await this.updateTagList(),(e.state===void 0||e.state==="RUNNING")&&this.#p()}async updateTagList(){try{this.tags=await this.send({type:"tag/list"})}catch(e){u(`Error fetching tag list: ${e}`)}return this.tags??[]}getIntegrationVersion(){return this.send({type:"nodered/version"})}createIntegrationEvent(e,t){this.#b({data:{type:e,version:t}})}onClientOpen(){this.onStatesLoadedAndRunning(),this.integrationVersion=r.NO_VERSION,this.isHomeAssistantRunning=!1,this.connectionState=1,this.#t.heartbeatInterval&&(this.#_(),this.#c=(0,y.startHeartbeat)(this.client,this.#t.heartbeatInterval,this.#t.host)),this.#e("ha_client:open")}onClientClose(){u("events connection closed, cleaning up connection"),this.resetClient()}onClientError(e){u("events connection error, cleaning up connection"),u(e),this.#e("ha_client:error",e),this.resetClient()}onClientConnecting(){this.connectionState=0}close(){this.#_(),this?.client?.close()}#_(){if(typeof this.#c=="function"){const e=this.#c;this.#c=void 0,e()}}resetClient(){this.#_(),this.integrationVersion=r.NO_VERSION,this.isHomeAssistantRunning=!1,this.#a=!1,this.#o=!1,this.#h=!1,this.#d=!1,this.#l=!1,this.#g=!1,this.#u=!1,this.connectionState=2,this.#e("ha_client:close")}getAreas(){return(0,c.cloneDeep)(this.areas)}getArea(e){return(0,c.cloneDeep)(this.areas.find(t=>t.area_id===e))}getDevices(){return(0,c.cloneDeep)(this.devices)}getDevice(e){return(0,c.cloneDeep)(this.devices.find(t=>t.id===e))}async getDeviceActions(e){return!this.isConnected||!e?[]:await this.send({type:"device_automation/action/list",device_id:e}).catch(i=>{throw i})}async getDeviceActionCapabilities(e){return!this.isConnected||!e?[]:(await this.send({type:"device_automation/action/capabilities",action:e}).catch(i=>{throw i})).extra_fields}async getDeviceTriggers(e){return!this.isConnected||!e?[]:await this.send({type:"device_automation/trigger/list",device_id:e}).catch(i=>{throw i})}async getDeviceTriggerCapabilities(e){return!this.isConnected||!e?[]:(await this.send({type:"device_automation/trigger/capabilities",trigger:e}).catch(i=>{throw i})).extra_fields}getEntities(){return(0,c.cloneDeep)(this.entities)}getEntity(e){return(0,c.cloneDeep)(this.entities.get(e))}getFloors(){return(0,c.cloneDeep)(this.floors)}getFloor(e){return(0,c.cloneDeep)(this.floors.find(t=>t.floor_id===e))}getLabels(){return(0,c.cloneDeep)(this.labels)}getLabel(e){return(0,c.cloneDeep)(this.labels.find(t=>t.label_id===e))}getStates(){return(0,c.cloneDeep)(this.states)}getState(e){return(0,c.cloneDeep)(this.states[e])}getServices(){return(0,c.cloneDeep)(this.services)}getTag(e){return(0,c.cloneDeep)(this.tags?.find(t=>t.id===e))}async getTranslations(e,t){return!this.isConnected||!e?[]:(await this.send({type:"frontend/get_translations",language:t,category:e})).resources}callService(e,t,i,n){const s=this.getServices(),b=(0,v.atLeastHaVersion)(this.client.haVersion,2023,12)?!!s[e]?.[t]?.response:void 0,R={type:"call_service",domain:e,service:t,service_data:i,target:n,return_response:b};return this.send(R)}send(e){if(!this.isConnected)throw new Error("Client is not connected");return u(`Send: ${JSON.stringify(e)}`),this.client?.sendMessagePromise(e)}}0&&(module.exports={ClientEvent,ClientState});