uci-ha/src/homeassistant.js

274 lines
8.7 KiB
JavaScript

import createSocket from './createSocket.js'
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 = {}
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) {
const error ={msg:'error in connection, unable to establish socket', error:err}
log.debug(error)
this.emit('error',error)
return error
}
this.socket = socket
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() {
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, (res) => {
log.debug({msg:'reply packet from send', id:packet.id, response:res})
clearTimeout(timeout)
resolve(res)
})
}.bind(this))
}
async _listen() {
this.socket.on('message', (ev) => {
let [err, packet] = btc(JSON.parse)(ev.data)
if (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) // 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.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.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
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(domain,service,data) {
let packet = {
type: 'call_service',
domain: domain,
service: service,
service_data: data
}
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(eid,value) {
return await this.callService('variable','set_variable', {variable:eid, value:value})
}
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] = ents // await this.getEntities(ents)
let list = this._watchLists[name]
ents.forEach(ent => {
this.on(ent, handleUpdate)
})
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(`${this.watchListEventPrefix}-${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 ${this.cmdId+1}, setting pong timeout`)
let [err, res] = await to(this.send('ping'))
if (err) {
clearTimeout(ping)
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 !== '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)
}
}
}
if (enabled) {
log.debug('enabling ping pong monitor')
ping = queuePing.call(this)
}
else {
if (ping) clearTimeout(ping)
log.debug('ping pong monitor disabled')
}
}
} // end class
export default HomeAssistant