0.0.8 fix issue when emitting 'error' by adding a listener (node throws error if not)
restore all the aync err,res handling. now when connection is lost works finemaster
parent
27c1964cd8
commit
a59aac5c0e
|
@ -4,14 +4,13 @@ import readYaml from 'load-yaml-file'
|
||||||
;
|
;
|
||||||
(async () => {
|
(async () => {
|
||||||
let opts = await readYaml(process.env.HA_OPTS_PATH || './examples/opts.yaml')
|
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)
|
console.log(opts)
|
||||||
const hass = new Hass(opts)
|
const hass = new Hass(opts)
|
||||||
hass.on('connection', msg => console.log(`connection: ${msg}`))
|
hass.on('connection', msg => console.log(`connection: ${msg}`))
|
||||||
|
hass.once('connection', () => console.log('now try disconnecting/stopping home assistant'))
|
||||||
await hass.connect()
|
await hass.connect()
|
||||||
console.log('sending a ping to server')
|
|
||||||
console.log('pong returned from server', await hass.send('ping'))
|
|
||||||
hass.exit()
|
|
||||||
|
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
"description": "websocket api access to home assistant",
|
"description": "websocket api access to home assistant",
|
||||||
"main": "./src/homeassistant.js",
|
"main": "./src/homeassistant.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"pp": "node ./examples/ping-pong.js",
|
"pp": "UCI_ENV=dev UCI_LOG_LEVEL=error nodemon -r esm ./examples/ping-pong.js",
|
||||||
"ppd": "UCI_ENV=dev UCI_LOG_LEVEL=debug 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",
|
"ws": "nodemon -r esm ./examples/watch-set.js",
|
||||||
"wsd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/watch-set.js"
|
"wsd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/watch-set.js"
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { EventEmitter } from 'events'
|
||||||
import logger from '@uci-utils/logger'
|
import logger from '@uci-utils/logger'
|
||||||
let log = {} // declare module wide log to be set during construction
|
let log = {} // declare module wide log to be set during construction
|
||||||
|
|
||||||
const defaultOpts = {
|
const DEFUALT_HASS_OPTS = {
|
||||||
host: 'localhost',
|
host: 'localhost',
|
||||||
serverPath: 'api/websocket',
|
serverPath: 'api/websocket',
|
||||||
protocol: 'ws',
|
protocol: 'ws',
|
||||||
|
@ -22,14 +22,18 @@ class HomeAssistant extends EventEmitter {
|
||||||
constructor(opts) {
|
constructor(opts) {
|
||||||
super()
|
super()
|
||||||
log = logger({ name: 'HomeAssistant', id: this.id })
|
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})
|
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}`
|
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})
|
log.debug({msg:'url for websocket', url:this.url})
|
||||||
this.cmdId = 1
|
this.cmdId = 1
|
||||||
this.eventBusId = null
|
this.eventBusId = null
|
||||||
this._watchLists = {}
|
this._watchLists = {}
|
||||||
|
this._silence = []
|
||||||
|
this.connected=false
|
||||||
|
this.ready=false
|
||||||
this.watchListEventPrefix = opts.watchListEventPrefix || 'wl'
|
this.watchListEventPrefix = opts.watchListEventPrefix || 'wl'
|
||||||
|
this.on('error',msg=> log.error(msg))
|
||||||
}
|
}
|
||||||
|
|
||||||
async connect () {
|
async connect () {
|
||||||
|
@ -44,16 +48,16 @@ class HomeAssistant extends EventEmitter {
|
||||||
this.socket = socket
|
this.socket = socket
|
||||||
this.emit('connection','connected')
|
this.emit('connection','connected')
|
||||||
log.info('Successfuly connected to Home Assistant')
|
log.info('Successfuly connected to Home Assistant')
|
||||||
|
this.connected=true
|
||||||
await this._listen()
|
await this._listen()
|
||||||
// if (this.opts.monitor == null || this.opts.ppmonitor)
|
|
||||||
this._monitorConnection(this.opts.ppmonitor)
|
this._monitorConnection(this.opts.ppmonitor)
|
||||||
|
|
||||||
|
|
||||||
return 'success'
|
return 'success'
|
||||||
|
|
||||||
} // end connect
|
} // end connect
|
||||||
|
|
||||||
async disconnect() {
|
async disconnect() {
|
||||||
|
|
||||||
this.socket.removeAllListeners('message') // cancels _listen
|
this.socket.removeAllListeners('message') // cancels _listen
|
||||||
this.socket = {}
|
this.socket = {}
|
||||||
this.emit('connection','disconnected')
|
this.emit('connection','disconnected')
|
||||||
|
@ -68,27 +72,40 @@ class HomeAssistant extends EventEmitter {
|
||||||
|
|
||||||
nextId () { return ++this.cmdId }
|
nextId () { return ++this.cmdId }
|
||||||
|
|
||||||
async send (cmd,options={}) {
|
// if entity is added with call to silence
|
||||||
return new Promise(function(resolve, reject) {
|
// 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
|
let packet = options
|
||||||
if (isPlainObject(cmd)) { packet = cmd }
|
if (isPlainObject(cmd)) { packet = cmd }
|
||||||
else {
|
else {
|
||||||
packet.type = cmd
|
packet.type = cmd
|
||||||
}
|
}
|
||||||
packet.id = packet.id || this.nextId()
|
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
|
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)
|
this.socket.send(message)
|
||||||
let timeout = setTimeout( ()=>{
|
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)
|
},5000)
|
||||||
this.on(packet.id, (res) => {
|
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)
|
clearTimeout(timeout)
|
||||||
resolve(res)
|
resolve(res)
|
||||||
})
|
})
|
||||||
}.bind(this))
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async _listen() {
|
async _listen() {
|
||||||
|
@ -104,7 +121,8 @@ class HomeAssistant extends EventEmitter {
|
||||||
this.emit('event', packet)
|
this.emit('event', packet)
|
||||||
if (packet.event.event_type === 'state_changed') {
|
if (packet.event.event_type === 'state_changed') {
|
||||||
this.emit('state_changed',packet.event.data)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
@ -125,28 +143,38 @@ class HomeAssistant extends EventEmitter {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// subscribe to complete event bus
|
// subscribe to complete event bus
|
||||||
let [err,res] = await to(this.send('subscribe_events'))
|
const res = await this.send('subscribe_events')
|
||||||
log.debug('return from subscribe events', res)
|
if (res.error) {
|
||||||
if (err || res.error) {
|
const error = {msg:'subscription to event bus failed!', level:'fatal', error:res.error}
|
||||||
log.error({msg:'subscription to event bus failed!', level:'fatal', error:err || res.error})
|
this.emit('error', error)
|
||||||
this.emit('error', {msg:'subscription to event bus failed!', level:'fatal', error:err || res.error})
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.info({msg:'connection to Home Assitant ready for communication'})
|
log.info({msg:'connection to Home Assitant ready for communication'})
|
||||||
this.eventBusId = res.id
|
this.eventBusId = res.id
|
||||||
this.emit('connection','ready')
|
this.emit('connection','ready')
|
||||||
|
this.ready=true
|
||||||
}
|
}
|
||||||
// resubscribe to any specific events that are in stored on disk or in memeory
|
// resubscribe to any specific events that are in stored on disk or in memeory
|
||||||
} // end listen
|
} // end listen
|
||||||
|
|
||||||
|
|
||||||
async getEntities (ents='all',type='obj') {
|
async getEntities (ents='all',type='obj') {
|
||||||
if (ents =='array') {ents = 'all'; type = 'array'}
|
let single = false
|
||||||
let res = (await this.send('get_states')).result
|
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 (ents !== 'all') {
|
||||||
if (typeof list === 'string') ents = [ents]
|
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') {
|
if (type == 'obj') {
|
||||||
let obj = {}
|
let obj = {}
|
||||||
res.forEach(ent => obj[ent.entity_id]=ent)
|
res.forEach(ent => obj[ent.entity_id]=ent)
|
||||||
|
@ -154,23 +182,36 @@ class HomeAssistant extends EventEmitter {
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
// todo need to autobind
|
||||||
async callService(domain,service,data) {
|
async callService(domain,service,data) {
|
||||||
|
// console.log(domain,service,data)
|
||||||
|
if (data.value !== {} || data.value==null) {
|
||||||
let packet = {
|
let packet = {
|
||||||
type: 'call_service',
|
type: 'call_service',
|
||||||
domain: domain,
|
domain: domain,
|
||||||
service: service,
|
service: service,
|
||||||
service_data: data
|
service_data: data
|
||||||
}
|
}
|
||||||
|
this.silence(data.entity_id)
|
||||||
const [err, res] = await to(this.send(packet))
|
const [err, res] = await to(this.send(packet))
|
||||||
|
this.hear(data.entity_id)
|
||||||
if (err) {
|
if (err) {
|
||||||
const error ={msg:'service call failed', level:'error', error:err}
|
const error ={msg:'service call failed', level:'error', error:err}
|
||||||
log.error(error)
|
|
||||||
this.emit('error', error)
|
this.emit('error', error)
|
||||||
return 'error'
|
return error
|
||||||
}
|
}
|
||||||
return res
|
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) {
|
async setVariable(eid,value) {
|
||||||
return await this.callService('variable','set_variable', {variable:eid, value:value})
|
return await this.callService('variable','set_variable', {variable:eid, value:value})
|
||||||
|
@ -190,6 +231,7 @@ class HomeAssistant extends EventEmitter {
|
||||||
return await this.callService(domain, service, {entity_id:eid, value:value})
|
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) {
|
async setSelect(eid,value) {
|
||||||
const domain = eid.split('.').shift()
|
const domain = eid.split('.').shift()
|
||||||
const service = Array.isArray(value) ? 'set_options':'select_option'
|
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})
|
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') {
|
async makeWatchList (ents,name='default') {
|
||||||
if (typeof entity === 'string') ents = [ents]
|
if (typeof entity === 'string') ents = [ents]
|
||||||
this._watchLists[name] = ents // await this.getEntities(ents)
|
this._watchLists[name] = ents // await this.getEntities(ents)
|
||||||
|
@ -243,13 +279,12 @@ class HomeAssistant extends EventEmitter {
|
||||||
clearTimeout(ping)
|
clearTimeout(ping)
|
||||||
const msg ='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})
|
log.debug({msg:msg})
|
||||||
this.emit('error',{msg:msg})
|
// this.emit('error',{msg:msg})
|
||||||
await this.disconnect() // removes socket and clears message listener
|
await this.disconnect() // removes socket and clears message listener
|
||||||
this.connect()
|
this.connect()
|
||||||
} else {
|
} else {
|
||||||
if (res !== 'pong' ) {
|
if (res !== 'pong' ) {
|
||||||
const error = {msg:'something major wrong, message was not a pong for this id', response:res}
|
const error = {msg:'something major wrong, message was not a pong for this id', response:res}
|
||||||
log.error(error)
|
|
||||||
this.emit('error',error)
|
this.emit('error',error)
|
||||||
} else {
|
} else {
|
||||||
log.debug('pong received, waiting 5 secs before sending next ping')
|
log.debug('pong received, waiting 5 secs before sending next ping')
|
||||||
|
|
Loading…
Reference in New Issue