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 methodsmaster
parent
224fb07f10
commit
27c1964cd8
|
@ -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
|
||||
|
|
|
@ -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}`))
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue