diff --git a/examples/ping-pong.js b/examples/ping-pong.js index 04408c7..aa212e1 100644 --- a/examples/ping-pong.js +++ b/examples/ping-pong.js @@ -4,14 +4,13 @@ import readYaml from 'load-yaml-file' ; (async () => { let opts = await readYaml(process.env.HA_OPTS_PATH || './examples/opts.yaml') - opts.ppmonitor = false // turn off 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}`)) + hass.once('connection', () => console.log('now try disconnecting/stopping home assistant')) await hass.connect() - console.log('sending a ping to server') - console.log('pong returned from server', await hass.send('ping')) - hass.exit() + })().catch(err => { console.error('FATAL: UNABLE TO START SYSTEM!\n',err) diff --git a/package.json b/package.json index 8e121d9..6983aa7 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,8 @@ "description": "websocket api access to home assistant", "main": "./src/homeassistant.js", "scripts": { - "pp": "node ./examples/ping-pong.js", - "ppd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/ping-pong.js", + "pp": "UCI_ENV=dev UCI_LOG_LEVEL=error nodemon -r esm ./examples/ping-pong.js", + "pp:debug": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/ping-pong.js", "ws": "nodemon -r esm ./examples/watch-set.js", "wsd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/watch-set.js" }, diff --git a/src/homeassistant.js b/src/homeassistant.js index 13164a3..edc008a 100644 --- a/src/homeassistant.js +++ b/src/homeassistant.js @@ -7,7 +7,7 @@ import { EventEmitter } from 'events' import logger from '@uci-utils/logger' let log = {} // declare module wide log to be set during construction -const defaultOpts = { +const DEFUALT_HASS_OPTS = { host: 'localhost', serverPath: 'api/websocket', protocol: 'ws', @@ -22,14 +22,18 @@ class HomeAssistant extends EventEmitter { constructor(opts) { super() log = logger({ name: 'HomeAssistant', id: this.id }) - this.opts = Object.assign(defaultOpts, opts) + this.opts = Object.assign(DEFUALT_HASS_OPTS, opts) log.debug({msg:'config to constructor',opts:opts}) this.url = (this.opts.url ? `${this.opts.url} + /${this.opts.serverPath}` : `${this.opts.protocol}://${this.opts.host}:${this.opts.port}`) + `/${this.opts.serverPath}` log.debug({msg:'url for websocket', url:this.url}) this.cmdId = 1 this.eventBusId = null this._watchLists = {} + this._silence = [] + this.connected=false + this.ready=false this.watchListEventPrefix = opts.watchListEventPrefix || 'wl' + this.on('error',msg=> log.error(msg)) } async connect () { @@ -44,16 +48,16 @@ class HomeAssistant extends EventEmitter { this.socket = socket this.emit('connection','connected') log.info('Successfuly connected to Home Assistant') + this.connected=true 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') @@ -68,27 +72,40 @@ class HomeAssistant extends EventEmitter { nextId () { return ++this.cmdId } - async send (cmd,options={}) { - return new Promise(function(resolve, reject) { + // if entity is added with call to silence + // useful for service calls where the response may result in unwanted state change events + isSilent(id) { return this._silence.includes(id) } + silence(id) { if (id) this._silence.push(id) } + hear(id) { if (id) this._silence = this._silence.filter(ent=> ent!==id) } + send (cmd,options={}) { + return new Promise( (resolve, reject) => { + // if (!this.connected) reject({error:'disconnected', packet:packet}) let packet = options if (isPlainObject(cmd)) { packet = cmd } else { packet.type = cmd } packet.id = packet.id || this.nextId() - log.debug('message to send', packet) + // console.log('message to send', packet) let [err, message] = btc(JSON.stringify)(packet) // try/catch or btc - if (err) reject ({error:'failed to parse message', packet:packet}) + if (err) { + const error = {msg:'failed to parse message', packet:packet} + this.emit('error',error) + reject(error) + } this.socket.send(message) let timeout = setTimeout( ()=>{ - reject({error:'failed to get a response in 5 seconds', packet:packet}) + let error = {msg:'failed to get a response in 5 seconds', packet:packet} + reject(error) + this.emit('error',error) },5000) this.on(packet.id, (res) => { - log.debug({msg:'reply packet from send', id:packet.id, response:res}) + log.debug({msg:'reply packet from send', packet:packet, response:res}) clearTimeout(timeout) resolve(res) }) - }.bind(this)) + + }) } async _listen() { @@ -104,7 +121,8 @@ class HomeAssistant extends EventEmitter { 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 (!this.isSilent(packet.event.data.entity_id)) this.emit(packet.event.data.entity_id,packet.event.data.new_state) + // else console.log(packet.event.data.entity_id, 'was silent, not emitting') } return } @@ -125,28 +143,38 @@ class HomeAssistant extends EventEmitter { } }) // 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.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}) + const res = await this.send('subscribe_events') + if (res.error) { + const error = {msg:'subscription to event bus failed!', level:'fatal', error:res.error} + this.emit('error', error) } else { log.info({msg:'connection to Home Assitant ready for communication'}) this.eventBusId = res.id this.emit('connection','ready') + this.ready=true } // resubscribe to any specific events that are in stored on disk or in memeory } // end listen async getEntities (ents='all',type='obj') { - if (ents =='array') {ents = 'all'; type = 'array'} - let res = (await this.send('get_states')).result + let single = false + if (typeof ents ==='string') { + if (ents ==='array' || ents ==='all') {ents = 'all'; type = 'array'} + else single=true + } + let [err, res ] = await to(this.send('get_states')) + if (err) { + const error = {msg:'unable to get entities', entities:ents, error:err} + this.emit('error',error) + return error + } if (ents !== 'all') { if (typeof list === 'string') ents = [ents] - res = res.filter( item => ents.indexOf(item.entity_id) > -1 ) + res = res.result.filter( item => ents.indexOf(item.entity_id) > -1 ) } + if (single) return res[0] || {} if (type == 'obj') { let obj = {} res.forEach(ent => obj[ent.entity_id]=ent) @@ -154,22 +182,35 @@ class HomeAssistant extends EventEmitter { } return res } - + // todo need to autobind async callService(domain,service,data) { - let packet = { - type: 'call_service', - domain: domain, - service: service, - service_data: data + // console.log(domain,service,data) + if (data.value !== {} || data.value==null) { + let packet = { + type: 'call_service', + domain: domain, + service: service, + service_data: data + } + this.silence(data.entity_id) + const [err, res] = await to(this.send(packet)) + this.hear(data.entity_id) + if (err) { + const error ={msg:'service call failed', level:'error', error:err} + this.emit('error', error) + return error + } + return res } - 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 + } + + // only changes state passively + async updateEntity(entity,state) { + // this.silence(entity) + // console.log('silence on',entity) + return await this.callService('python_script','state_change',{entity_id:entity, state:state}) + // console.log('silence off', entity) + // this.silence(entity) } async setVariable(eid,value) { @@ -190,6 +231,7 @@ class HomeAssistant extends EventEmitter { return await this.callService(domain, service, {entity_id:eid, value:value}) } + // if value is array then it's an array of options to set for select async setSelect(eid,value) { const domain = eid.split('.').shift() const service = Array.isArray(value) ? 'set_options':'select_option' @@ -197,12 +239,6 @@ class HomeAssistant extends EventEmitter { 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] = ents // await this.getEntities(ents) @@ -243,13 +279,12 @@ class HomeAssistant extends EventEmitter { clearTimeout(ping) const msg ='no pong received in 5 seconds, notifiy and attempt new connection' log.debug({msg:msg}) - this.emit('error',{msg:msg}) + // this.emit('error',{msg:msg}) await this.disconnect() // removes socket and clears message listener this.connect() } else { 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')