233 lines
7.0 KiB
JavaScript
233 lines
7.0 KiB
JavaScript
import createSocket from './createSocket'
|
|
import to from 'await-to-js'
|
|
import btc from 'better-try-catch'
|
|
import isPlainObject from 'is-plain-object'
|
|
import { EventEmitter } from 'events'
|
|
|
|
import logger from '@uci-utils/logger'
|
|
let log = {} // declare module wide log to be set during construction
|
|
|
|
const defaultOpts = {
|
|
host: 'localhost',
|
|
serverPath: 'api/websocket',
|
|
protocol: 'ws',
|
|
retryTimeout: 5000,
|
|
timeout: 5000,
|
|
retryCount: -1,
|
|
port: 8123,
|
|
ppmonitor: true
|
|
}
|
|
|
|
class HomeAssistant extends EventEmitter {
|
|
constructor(opts) {
|
|
super()
|
|
log = logger({ name: 'HomeAssistant', id: this.id })
|
|
this.opts = Object.assign(defaultOpts, 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 = {}
|
|
}
|
|
|
|
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
|
|
}
|
|
this.socket = socket
|
|
await this._listen()
|
|
// if (this.opts.monitor == null || this.opts.ppmonitor)
|
|
this._monitorConnection(this.opts.ppmonitor)
|
|
log.info('Successfuly connected to Home Assistant')
|
|
return 'success'
|
|
|
|
} // end connect
|
|
|
|
|
|
async disconnect() {
|
|
this.socket.removeAllListeners('message') // cancels _listen
|
|
this.socket = {}
|
|
}
|
|
|
|
async exit() {
|
|
await this.disconnect()
|
|
// also do any other cleanup and saving
|
|
log.debug('exiting per request')
|
|
process.exit(0)
|
|
}
|
|
|
|
|
|
|
|
nextId () { return ++this.cmdId }
|
|
|
|
async send (cmd,options={}) {
|
|
return new Promise(function(resolve, reject) {
|
|
let packet = options
|
|
if (isPlainObject(cmd)) { packet = cmd }
|
|
else {
|
|
packet.type = cmd
|
|
}
|
|
packet.id = packet.id || this.nextId()
|
|
log.debug('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})
|
|
this.socket.send(message)
|
|
let timeout = setTimeout( ()=>{
|
|
reject({error:'failed to get a response in 5 seconds', packet:packet})
|
|
},5000)
|
|
this.on(packet.id, (packet) => {
|
|
clearTimeout(timeout)
|
|
resolve(packet) })
|
|
}.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})
|
|
} else {
|
|
// 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})
|
|
}
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
// 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})
|
|
this.emit('error', {msg:'subscription to event bus failed!', level:'fatal', error:err || res.error})
|
|
}
|
|
else {
|
|
log.debug(res)
|
|
this.eventBusId = res.id
|
|
}
|
|
// 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
|
|
if (ents !== 'all') {
|
|
if (typeof list === 'string') ents = [ents]
|
|
res = res.filter( item => ents.indexOf(item.entity_id) > -1 )
|
|
}
|
|
if (type == 'obj') {
|
|
let obj = {}
|
|
res.forEach(ent => obj[ent.entity_id]=ent)
|
|
return obj
|
|
}
|
|
return res
|
|
}
|
|
|
|
async callService(service,data) {
|
|
let packet = {
|
|
type: 'call_service',
|
|
domain: service.split('.')[0],
|
|
service:service.split('.')[1],
|
|
service_data: data
|
|
}
|
|
return await this.send(packet)
|
|
}
|
|
|
|
async setVariable(variable,value) {
|
|
return await this.callService('variable.set_variable', {variable:variable, 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 makeWatchList (ents,name='default') {
|
|
if (typeof entity === 'string') ents = [ents]
|
|
this._watchLists[name] = 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
|
|
|
|
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)
|
|
}
|
|
|
|
}
|
|
|
|
getWatchList(name) {
|
|
return this._watchLists[name]
|
|
}
|
|
|
|
removeWatchList (name) {
|
|
this.getWatchList(name).forEach(ent => this.removeListener(ent.entity_id,this._handleUpdate))
|
|
delete this._watchLists[name]
|
|
this.removeAllListeners(`wl-${name}`)
|
|
}
|
|
|
|
_monitorConnection(enabled=true) {
|
|
|
|
let ping = null
|
|
|
|
async function queuePing() {
|
|
let id = this.nextId()
|
|
log.debug(`sending ping id ${id}, 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')
|
|
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})
|
|
} else {
|
|
log.debug('pong received, waiting 5 secs before sending next ping')
|
|
setTimeout( () => ping = queuePing.call(this),5000)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (enabled) {
|
|
ping = queuePing.call(this)
|
|
log.debug('ping pong monitor enabled')
|
|
}
|
|
else {
|
|
if (ping) clearTimeout(ping)
|
|
log.debug('ping pong monitor disabled')
|
|
}
|
|
}
|
|
|
|
} // end class
|
|
|
|
export default HomeAssistant
|