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
# host: 'localhost',
# serverPath: 'api/websocket',
# protocol: 'ws',
# retryTimeout: 5000,
# timeout: 5000,
# retryCount: -1,
# port: 8123,
# host: 'localhost'
# serverPath: 'api/websocket'
# protocol: 'ws'
# retryTimeout: 5000
# timeout: 5000
# retryCount: -1
# port: 8123
# 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

View File

@ -1,10 +1,10 @@
import Hass from '../src/homeassistant'
import Hass from '../src/homeassistant.js'
import readYaml from 'load-yaml-file'
;
(async () => {
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)
const hass = new Hass(opts)
hass.on('connection', msg => console.log(`connection: ${msg}`))

View File

@ -1,18 +1,18 @@
{
"name": "@uci/ha",
"version": "0.0.3",
"version": "0.0.7",
"description": "websocket api access to home assistant",
"main": "./src/homeassistant.js",
"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",
"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"
},
"author": "",
"license": "ISC",
"dependencies": {
"@uci-utils/logger": "^0.0.14",
"@uci-utils/logger": "^0.0.18",
"await-to-js": "^2.1.1",
"better-try-catch": "^0.6.2",
"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
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
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

View File

@ -1,4 +1,4 @@
import createSocket from './createSocket'
import createSocket from './createSocket.js'
import to from 'await-to-js'
import btc from 'better-try-catch'
import isPlainObject from 'is-plain-object'
@ -29,28 +29,34 @@ class HomeAssistant extends EventEmitter {
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) {
log.debug({msg:'error in connection, unable to establish socket', error:err})
throw 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
await this._listen()
// if (this.opts.monitor == null || this.opts.ppmonitor)
this._monitorConnection(this.opts.ppmonitor)
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() {
@ -60,8 +66,6 @@ class HomeAssistant extends EventEmitter {
process.exit(0)
}
nextId () { return ++this.cmdId }
async send (cmd,options={}) {
@ -79,52 +83,58 @@ class HomeAssistant extends EventEmitter {
let timeout = setTimeout( ()=>{
reject({error:'failed to get a response in 5 seconds', packet:packet})
},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)
resolve(packet) })
resolve(res)
})
}.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})
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)
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)
}
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.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})
}
else {
log.debug(res)
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
@ -145,41 +155,68 @@ class HomeAssistant extends EventEmitter {
return res
}
async callService(service,data) {
async callService(domain,service,data) {
let packet = {
type: 'call_service',
domain: service.split('.')[0],
service:service.split('.')[1],
domain: domain,
service: service,
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) {
return await this.callService('variable.set_variable', {variable:variable, value:value})
async setVariable(eid,value) {
return await this.callService('variable','set_variable', {variable:eid, 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 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] = await this.getEntities(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 fo removing listener
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(`wl-${name}`, changed)
this.emit(`${this.watchListEventPrefix}-${name}`, changed)
}
}
@ -199,17 +236,21 @@ class HomeAssistant extends EventEmitter {
let ping = null
async function queuePing() {
let id = this.nextId()
log.debug(`sending ping id ${id}, setting pong timeout`)
// 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)
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
this.connect()
} else {
if (res.type !== 'pong' ) {
log.debug({msg:'something major wrong, message was not a pong for this id', response:res})
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)
@ -218,8 +259,8 @@ class HomeAssistant extends EventEmitter {
}
if (enabled) {
log.debug('enabling ping pong monitor')
ping = queuePing.call(this)
log.debug('ping pong monitor enabled')
}
else {
if (ping) clearTimeout(ping)