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
master
David Kebler 2020-05-30 18:13:37 -07:00
parent 224fb07f10
commit 27c1964cd8
5 changed files with 106 additions and 63 deletions

View File

@ -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 # default options below
# host: 'localhost', # host: 'localhost'
# serverPath: 'api/websocket', # serverPath: 'api/websocket'
# protocol: 'ws', # protocol: 'ws'
# retryTimeout: 5000, # retryTimeout: 5000
# timeout: 5000, # timeout: 5000
# retryCount: -1, # retryCount: -1
# port: 8123, # port: 8123
# ppmonitor: true # by default the server is ping/pong monitored for active connection # 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 # url: ' ' # you can opt to provide the url (less server path) instead of have it created by above opts

View File

@ -1,10 +1,10 @@
import Hass from '../src/homeassistant' import Hass from '../src/homeassistant.js'
import readYaml from 'load-yaml-file' 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 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) 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}`))

View File

@ -1,18 +1,18 @@
{ {
"name": "@uci/ha", "name": "@uci/ha",
"version": "0.0.3", "version": "0.0.7",
"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 -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", "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" "wsd": "UCI_ENV=dev UCI_LOG_LEVEL=debug nodemon -r esm ./examples/watch-set.js"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@uci-utils/logger": "^0.0.14", "@uci-utils/logger": "^0.0.18",
"await-to-js": "^2.1.1", "await-to-js": "^2.1.1",
"better-try-catch": "^0.6.2", "better-try-catch": "^0.6.2",
"delay": "^4.3.0", "delay": "^4.3.0",

View File

@ -15,11 +15,11 @@ This module allows one to communicate with a Home Assistant instance via their w
## What's it good for ## 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 ## 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 ## OS and Node Support

View File

@ -1,4 +1,4 @@
import createSocket from './createSocket' import createSocket from './createSocket.js'
import to from 'await-to-js' import to from 'await-to-js'
import btc from 'better-try-catch' import btc from 'better-try-catch'
import isPlainObject from 'is-plain-object' import isPlainObject from 'is-plain-object'
@ -29,28 +29,34 @@ class HomeAssistant extends EventEmitter {
this.cmdId = 1 this.cmdId = 1
this.eventBusId = null this.eventBusId = null
this._watchLists = {} this._watchLists = {}
this.watchListEventPrefix = opts.watchListEventPrefix || 'wl'
} }
async connect () { async connect () {
this.opts.retriesLeft = this.opts.retryCount this.opts.retriesLeft = this.opts.retryCount
let [err,socket] = await to(createSocket(this.url,this.opts)) let [err,socket] = await to(createSocket(this.url,this.opts))
if (err) { if (err) {
log.debug({msg:'error in connection, unable to establish socket', error:err}) const error ={msg:'error in connection, unable to establish socket', error:err}
throw err log.debug(error)
this.emit('error',error)
return error
} }
this.socket = socket this.socket = socket
this.emit('connection','connected')
log.info('Successfuly connected to Home Assistant')
await this._listen() await this._listen()
// if (this.opts.monitor == null || this.opts.ppmonitor) // if (this.opts.monitor == null || this.opts.ppmonitor)
this._monitorConnection(this.opts.ppmonitor) this._monitorConnection(this.opts.ppmonitor)
log.info('Successfuly connected to Home Assistant')
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')
} }
async exit() { async exit() {
@ -60,8 +66,6 @@ class HomeAssistant extends EventEmitter {
process.exit(0) process.exit(0)
} }
nextId () { return ++this.cmdId } nextId () { return ++this.cmdId }
async send (cmd,options={}) { async send (cmd,options={}) {
@ -79,52 +83,58 @@ class HomeAssistant extends EventEmitter {
let timeout = setTimeout( ()=>{ let timeout = setTimeout( ()=>{
reject({error:'failed to get a response in 5 seconds', packet:packet}) reject({error:'failed to get a response in 5 seconds', packet:packet})
},5000) },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) clearTimeout(timeout)
resolve(packet) }) resolve(res)
})
}.bind(this)) }.bind(this))
} }
async _listen() { async _listen() {
this.socket.on('message', (ev) => { this.socket.on('message', (ev) => {
// log.debug('incoming message packet from server', ev.data)
let [err, packet] = btc(JSON.parse)(ev.data) let [err, packet] = btc(JSON.parse)(ev.data)
if (err) { 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 { } 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 // result
if (packet.type === 'result') { if (packet.type === 'result') {
if (!packet.success) { if (!packet.success) {
this.emit('error',{msg:'failed result', packet:packet}) this.emit('error',{msg:'failed result', packet:packet})
this.emit(packet.id,{error:packet.error}) this.emit(packet.id,{error:packet.error})
} else this.emit(packet.id,{id:packet.id, result:packet.result || packet.success}) } else this.emit(packet.id,{id:packet.id, result:packet.result || packet.success})
return
} }
// pong // pong
if (packet.type === 'pong') { this.emit('pong', packet) if (packet.type === 'pong') {
this.emit(packet.id, 'pong') this.emit('pong', packet) // so anyone can listen to pong
} this.emit(packet.id, 'pong') // for send command
// 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 // subscribe to complete event bus
let [err,res] = await to(this.send('subscribe_events')) let [err,res] = await to(this.send('subscribe_events'))
log.debug('return from subscribe events', res) log.debug('return from subscribe events', res)
if (err || res.error) { 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}) this.emit('error', {msg:'subscription to event bus failed!', level:'fatal', error:err || res.error})
} }
else { else {
log.debug(res) log.info({msg:'connection to Home Assitant ready for communication'})
this.eventBusId = res.id this.eventBusId = res.id
this.emit('connection','ready')
} }
// 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
@ -145,41 +155,68 @@ class HomeAssistant extends EventEmitter {
return res return res
} }
async callService(service,data) { async callService(domain,service,data) {
let packet = { let packet = {
type: 'call_service', type: 'call_service',
domain: service.split('.')[0], domain: domain,
service:service.split('.')[1], service: service,
service_data: data 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) { async setVariable(eid,value) {
return await this.callService('variable.set_variable', {variable:variable, value:value}) return await this.callService('variable','set_variable', {variable:eid, value:value})
} }
async switch(eid, mode) { async switch(eid, mode='toggle') { // works for input_boolean too
mode.toLowerCase() if (typeof mode ==='boolean') mode = mode ?'on':'off'
let service = (mode === 'on' || mode === 'off') ? `switch.turn_${mode}` : 'switch.toggle' else mode = mode.toLowerCase()
if (eid.split('.')[0] !== 'switch') eid = 'switch.'+eid const domain = eid.split('.').shift()
return await this.callService(service, {entity_id:eid}) 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') { async makeWatchList (ents,name='default') {
if (typeof entity === 'string') ents = [ents] 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] let list = this._watchLists[name]
ents.forEach(ent => { ents.forEach(ent => {
this.on(ent, handleUpdate) 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) { function handleUpdate (changed) {
list[changed.entity_id] = changed // update entity in watch list list[changed.entity_id] = changed // update entity in watch list
// log.debug(changed.state,list[changed.entity_id]) // 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 let ping = null
async function queuePing() { async function queuePing() {
let id = this.nextId() // let id = this.nextId()
log.debug(`sending ping id ${id}, setting pong timeout`) log.debug(`sending ping id ${this.cmdId+1}, setting pong timeout`)
let [err, res] = await to(this.send('ping')) let [err, res] = await to(this.send('ping'))
if (err) { if (err) {
clearTimeout(ping) 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 await this.disconnect() // removes socket and clears message listener
this.connect() this.connect()
} else { } else {
if (res.type !== 'pong' ) { if (res !== 'pong' ) {
log.debug({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)
} else { } else {
log.debug('pong received, waiting 5 secs before sending next ping') log.debug('pong received, waiting 5 secs before sending next ping')
setTimeout( () => ping = queuePing.call(this),5000) setTimeout( () => ping = queuePing.call(this),5000)
@ -218,8 +259,8 @@ class HomeAssistant extends EventEmitter {
} }
if (enabled) { if (enabled) {
log.debug('enabling ping pong monitor')
ping = queuePing.call(this) ping = queuePing.call(this)
log.debug('ping pong monitor enabled')
} }
else { else {
if (ping) clearTimeout(ping) if (ping) clearTimeout(ping)