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
|
# 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
|
||||||
|
|
|
@ -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}`))
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
await this._listen()
|
this.emit('connection','connected')
|
||||||
// if (this.opts.monitor == null || this.opts.ppmonitor)
|
|
||||||
this._monitorConnection(this.opts.ppmonitor)
|
|
||||||
log.info('Successfuly connected to Home Assistant')
|
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'
|
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)
|
||||||
|
|
Loading…
Reference in New Issue