From 27c1964cd82e46845c177a9848adcf09a98d178e Mon Sep 17 00:00:00 2001 From: David Kebler Date: Sat, 30 May 2020 18:13:37 -0700 Subject: [PATCH] 0.0.7 refactor ping/disconnect/connect to work better and emit connection event catch errors on service call rework switch method add setNumber, setSelect methods --- examples/opts.yaml | 18 +++--- examples/ping-pong.js | 4 +- package.json | 8 +-- readme.md | 4 +- src/homeassistant.js | 135 +++++++++++++++++++++++++++--------------- 5 files changed, 106 insertions(+), 63 deletions(-) diff --git a/examples/opts.yaml b/examples/opts.yaml index 849b70e..38b3667 100644 --- a/examples/opts.yaml +++ b/examples/opts.yaml @@ -1,11 +1,13 @@ -access_token: 'your long lived token generated in HA' # REQUIRED! +#access_token: 'your long lived token generated in HA' # REQUIRED! +access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiI0ZDVmZWJmMjAxNTU0YzkzODUyMDMwOTFmNDVjNjExMSIsImlhdCI6MTU4ODY5NTEwNSwiZXhwIjoxOTA0MDU1MTA1fQ.ycuiRDZ4Qawwbz7YlwGEyp1GaJyaVZGmrQzm9g2zIY4 +host: 'hassio.kebler.net' # default options below -# host: 'localhost', -# serverPath: 'api/websocket', -# protocol: 'ws', -# retryTimeout: 5000, -# timeout: 5000, -# retryCount: -1, -# port: 8123, +# host: 'localhost' +# serverPath: 'api/websocket' +# protocol: 'ws' +# retryTimeout: 5000 +# timeout: 5000 +# retryCount: -1 +# port: 8123 # ppmonitor: true # by default the server is ping/pong monitored for active connection # url: ' ' # you can opt to provide the url (less server path) instead of have it created by above opts diff --git a/examples/ping-pong.js b/examples/ping-pong.js index 47b1f82..04408c7 100644 --- a/examples/ping-pong.js +++ b/examples/ping-pong.js @@ -1,10 +1,10 @@ -import Hass from '../src/homeassistant' +import Hass from '../src/homeassistant.js' import readYaml from 'load-yaml-file' ; (async () => { let opts = await readYaml(process.env.HA_OPTS_PATH || './examples/opts.yaml') - opts.ppmonitor = false // turn of automated ping/pong connection monitor for this example - on by default + opts.ppmonitor = false // turn off automated ping/pong connection monitor for this example - on by default console.log(opts) const hass = new Hass(opts) hass.on('connection', msg => console.log(`connection: ${msg}`)) diff --git a/package.json b/package.json index ed57c2d..8e121d9 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "@uci/ha", - "version": "0.0.3", + "version": "0.0.7", "description": "websocket api access to home assistant", "main": "./src/homeassistant.js", "scripts": { - "pp": "node -r esm ./examples/ping-pong.js", + "pp": "node ./examples/ping-pong.js", "ppd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/ping-pong.js", - "ws": "node -r esm ./examples/watch-set.js", + "ws": "nodemon -r esm ./examples/watch-set.js", "wsd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/watch-set.js" }, "author": "", "license": "ISC", "dependencies": { - "@uci-utils/logger": "^0.0.14", + "@uci-utils/logger": "^0.0.18", "await-to-js": "^2.1.1", "better-try-catch": "^0.6.2", "delay": "^4.3.0", diff --git a/readme.md b/readme.md index 46edcd8..c94e599 100644 --- a/readme.md +++ b/readme.md @@ -15,11 +15,11 @@ This module allows one to communicate with a Home Assistant instance via their w ## What's it good for -Allows the a nodejs coder to easily replace most of the limited yaml script and automation with. +Easily replace most of the limited HA yaml script and automation with nodejs code. ## Prerequisites -You'll of course need nodejs running and either npm or yarn. UCI tries to keep node as current as possible during development so use the latest version 10.x for best results or at +You'll of course need nodejs running and either npm or yarn. UCI tries to keep node as current as possible during development so use the latest version 12.x for best results or at ## OS and Node Support diff --git a/src/homeassistant.js b/src/homeassistant.js index f76a29c..13164a3 100644 --- a/src/homeassistant.js +++ b/src/homeassistant.js @@ -1,4 +1,4 @@ -import createSocket from './createSocket' +import createSocket from './createSocket.js' import to from 'await-to-js' import btc from 'better-try-catch' import isPlainObject from 'is-plain-object' @@ -29,28 +29,34 @@ class HomeAssistant extends EventEmitter { this.cmdId = 1 this.eventBusId = null this._watchLists = {} + this.watchListEventPrefix = opts.watchListEventPrefix || 'wl' } async connect () { this.opts.retriesLeft = this.opts.retryCount let [err,socket] = await to(createSocket(this.url,this.opts)) if (err) { - log.debug({msg:'error in connection, unable to establish socket', error:err}) - throw err + const error ={msg:'error in connection, unable to establish socket', error:err} + log.debug(error) + this.emit('error',error) + return error } this.socket = socket - await this._listen() - // if (this.opts.monitor == null || this.opts.ppmonitor) - this._monitorConnection(this.opts.ppmonitor) + this.emit('connection','connected') log.info('Successfuly connected to Home Assistant') + await this._listen() + // if (this.opts.monitor == null || this.opts.ppmonitor) + this._monitorConnection(this.opts.ppmonitor) + + return 'success' } // end connect - async disconnect() { this.socket.removeAllListeners('message') // cancels _listen this.socket = {} + this.emit('connection','disconnected') } async exit() { @@ -60,8 +66,6 @@ class HomeAssistant extends EventEmitter { process.exit(0) } - - nextId () { return ++this.cmdId } async send (cmd,options={}) { @@ -79,52 +83,58 @@ class HomeAssistant extends EventEmitter { let timeout = setTimeout( ()=>{ reject({error:'failed to get a response in 5 seconds', packet:packet}) },5000) - this.on(packet.id, (packet) => { + this.on(packet.id, (res) => { + log.debug({msg:'reply packet from send', id:packet.id, response:res}) clearTimeout(timeout) - resolve(packet) }) + resolve(res) + }) }.bind(this)) } - async _listen() { this.socket.on('message', (ev) => { - // log.debug('incoming message packet from server', ev.data) let [err, packet] = btc(JSON.parse)(ev.data) if (err) { - this.emit('error',{msg:'failed json parse of event data', event:ev, error:err}) + this.emit('error',{msg:'failed json parse of event data', event:ev.data, error:err}) } else { + log.debug('incoming message packet from server', packet.id, packet.type) + // event + if (packet.type === 'event') { + // this.emit(packet.id,packet.event) + this.emit('event', packet) + if (packet.event.event_type === 'state_changed') { + this.emit('state_changed',packet.event.data) + this.emit(packet.event.data.entity_id,packet.event.data.new_state) + } + return + } // result if (packet.type === 'result') { if (!packet.success) { this.emit('error',{msg:'failed result', packet:packet}) this.emit(packet.id,{error:packet.error}) } else this.emit(packet.id,{id:packet.id, result:packet.result || packet.success}) + return } // pong - if (packet.type === 'pong') { this.emit('pong', packet) - this.emit(packet.id, 'pong') - } - // event - if (packet.type === 'event') { - this.emit(packet.id,packet.event) - this.emit('event', packet) - if (packet.event.event_type === 'state_changed') { - this.emit('state_changed',packet.event.data) - this.emit(packet.event.data.entity_id,packet.event.data.new_state) - } + if (packet.type === 'pong') { + this.emit('pong', packet) // so anyone can listen to pong + this.emit(packet.id, 'pong') // for send command } + } }) // subscribe to complete event bus let [err,res] = await to(this.send('subscribe_events')) log.debug('return from subscribe events', res) if (err || res.error) { - log.debug({msg:'subscription to event bus failed!', level:'fatal', error:err || res.error}) + log.error({msg:'subscription to event bus failed!', level:'fatal', error:err || res.error}) this.emit('error', {msg:'subscription to event bus failed!', level:'fatal', error:err || res.error}) } else { - log.debug(res) + log.info({msg:'connection to Home Assitant ready for communication'}) this.eventBusId = res.id + this.emit('connection','ready') } // resubscribe to any specific events that are in stored on disk or in memeory } // end listen @@ -145,41 +155,68 @@ class HomeAssistant extends EventEmitter { return res } - async callService(service,data) { + async callService(domain,service,data) { let packet = { type: 'call_service', - domain: service.split('.')[0], - service:service.split('.')[1], + domain: domain, + service: service, service_data: data } - return await this.send(packet) + const [err, res] = await to(this.send(packet)) + if (err) { + const error ={msg:'service call failed', level:'error', error:err} + log.error(error) + this.emit('error', error) + return 'error' + } + return res } - async setVariable(variable,value) { - return await this.callService('variable.set_variable', {variable:variable, value:value}) + async setVariable(eid,value) { + return await this.callService('variable','set_variable', {variable:eid, value:value}) } - async switch(eid, mode) { - mode.toLowerCase() - let service = (mode === 'on' || mode === 'off') ? `switch.turn_${mode}` : 'switch.toggle' - if (eid.split('.')[0] !== 'switch') eid = 'switch.'+eid - return await this.callService(service, {entity_id:eid}) + async switch(eid, mode='toggle') { // works for input_boolean too + if (typeof mode ==='boolean') mode = mode ?'on':'off' + else mode = mode.toLowerCase() + const domain = eid.split('.').shift() + const service = (mode === 'on' || mode === 'off') ? `turn_${mode}` : 'toggle' + return await this.callService(domain,service,{entity_id:eid}) } + async setNumber(eid,value) { + const domain = eid.split('.').shift() + const service = 'set_value' + return await this.callService(domain, service, {entity_id:eid, value:value}) + } + + async setSelect(eid,value) { + const domain = eid.split('.').shift() + const service = Array.isArray(value) ? 'set_options':'select_option' + const key = Array.isArray(value) ? 'options' : 'option' + return await this.callService(domain,service, {entity_id:eid, [key]:value}) + } + + // async setSelectOptions(eid,options) { + // const service = eid.split('.').shift()+'.set_options' + // // console.log('setting service-',service, {entity_id:eid, options:options}) + // return await this.callService(service, {entity_id:eid, options:options}) + // } + async makeWatchList (ents,name='default') { if (typeof entity === 'string') ents = [ents] - this._watchLists[name] = await this.getEntities(ents) + this._watchLists[name] = ents // await this.getEntities(ents) let list = this._watchLists[name] ents.forEach(ent => { this.on(ent, handleUpdate) }) - this._handleUpdate = handleUpdate // need to save pointer fo removing listener + this._handleUpdate = handleUpdate // need to save pointer for removing listener function handleUpdate (changed) { list[changed.entity_id] = changed // update entity in watch list // log.debug(changed.state,list[changed.entity_id]) - this.emit(`wl-${name}`, changed) + this.emit(`${this.watchListEventPrefix}-${name}`, changed) } } @@ -199,17 +236,21 @@ class HomeAssistant extends EventEmitter { let ping = null async function queuePing() { - let id = this.nextId() - log.debug(`sending ping id ${id}, setting pong timeout`) + // let id = this.nextId() + log.debug(`sending ping id ${this.cmdId+1}, setting pong timeout`) let [err, res] = await to(this.send('ping')) if (err) { clearTimeout(ping) - log.debug('no pong received in 5 seconds, notifiy and attempt new connection') + const msg ='no pong received in 5 seconds, notifiy and attempt new connection' + log.debug({msg:msg}) + this.emit('error',{msg:msg}) await this.disconnect() // removes socket and clears message listener this.connect() } else { - if (res.type !== 'pong' ) { - log.debug({msg:'something major wrong, message was not a pong for this id', response:res}) + if (res !== 'pong' ) { + const error = {msg:'something major wrong, message was not a pong for this id', response:res} + log.error(error) + this.emit('error',error) } else { log.debug('pong received, waiting 5 secs before sending next ping') setTimeout( () => ping = queuePing.call(this),5000) @@ -218,8 +259,8 @@ class HomeAssistant extends EventEmitter { } if (enabled) { + log.debug('enabling ping pong monitor') ping = queuePing.call(this) - log.debug('ping pong monitor enabled') } else { if (ping) clearTimeout(ping)