home-assistant / home-assistant-js-websocket

:aerial_tramway: JavaScript websocket client for Home Assistant

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

using long lived token and access via nodejs only ws client

dkebler opened this issue · comments

Trying to gain access to the api directly from node (not browser). It's not clear to me if module supports long lived access token (Is that what authCode is??) and access via nodejs?

Anyway I can't get authenticated which seems to be related to a bad url than the token given the error. Module is written in ts so hard for me to track down exactly where it's going wrong. Does this module only support the native WS client (for the browser?) maybe that's the issue? I don't see any dependency for https://www.npmjs.com/package/ws which would be the nodejs ws client.

Here's the code. You can see I've tried several versions of the url, one via my reverse proxy, on via lookup on server and other using ip. All fail the same. ( I run latest node 12.7 using esm module to support es6 code)

import to from 'await-to-js'
import {
  getAuth,
  createConnection,
  subscribeEntities,
  ERR_HASS_HOST_REQUIRED,
  ERR_INVALID_AUTH,
  ERR_INVALID_HTTPS_TO_HTTP
} from 'home-assistant-js-websocket'

let opts = {
  // hassUrl: 'http://ha.<my domain>',
  // hassUrl: 'http://nas.<my domain>.net:8123',
  hassUrl: '10.0.0.3:8123',
  authCode: 'my long lived token here',
}

;
(async () => {
  console.log(
    ERR_HASS_HOST_REQUIRED,
    ERR_INVALID_AUTH,
    ERR_INVALID_HTTPS_TO_HTTP
  )
  let conn = await connect(opts)
  subscribeEntities(conn, ent => console.log('event on entities', ent))

})().catch(err => {
  console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
  process.kill(process.pid, 'SIGTERM')
})

async function connect(opts) {
  console.log(opts)
  let [erra,auth] = await to(getAuth(opts))
  if (erra) console.log('authorization error\n',erra)
  let [errc,connection] = await to(createConnection( {auth}))
  if (errc) console.log('connection error',errc)
  return connection
}

Here is the error

authorization error
 ReferenceError: location is not defined
    at i (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/home-assistant-js-websocket/dist/haws.umd.js:1)
    at e.getAuth (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/home-assistant-js-websocket/dist/haws.umd.js:1)
    at connect (/mnt/AllData/hacking/active-dev-repos/HA/ha/index.js:35:30)
    at /mnt/AllData/hacking/active-dev-repos/HA/ha/index.js:25:20
    at Object.<anonymous> (/mnt/AllData/hacking/active-dev-repos/HA/ha/index.js:28:3)
    at Generator.next (<anonymous>)
connection error 4
FATAL: UNABLE TO START SYSTEM!
 TypeError: Cannot read property '_ent' of undefined
    at d (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/home-assistant-js-websocket/dist/haws.umd.js:1:5621)
    at S (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/home-assistant-js-websocket/dist/haws.umd.js:1:8276)
    at e.subscribeEntities (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/home-assistant-js-websocket/dist/haws.umd.js:1:11609)
    at /mnt/AllData/hacking/active-dev-repos/HA/ha/index.js:26:3
    at processTicksAndRejections (internal/process/task_queues.js:85:5)
    at process.runNextTicks [as _tickCallback] (internal/process/task_queues.js:62:3)
    at /mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/esm/esm.js:1:34535
    at /mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/esm/esm.js:1:34176
    at process.<anonymous> (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/esm/esm.js:1:34506)
    at Function.<anonymous> (/mnt/AllData/hacking/active-dev-repos/HA/ha/node_modules/esm/esm.js:1:296856)
Terminated

BTW I've been using this REST api module and have no problem with accessing the HA api https://github.com/goyney/homeassistant_node so my lltoken is fine.

looks like I didn't scroll down far enough in the readme. :-).
https://github.com/home-assistant/home-assistant-js-websocket#using-this-with-long-lived-access-tokens

Maybe u might move that verbage up into the first auth/connect sections. Didn't occur to me to look at the very bottom.

I'll reopen this if I still can't get it to work.

Oh well guess I do need some assistance.

I created my own auth instance for the long lived token per the readme but then I see I can't just pass that to the the nodejs websocket connect.

    // Functions to handle authentication with Home Assistant
    // Implement yourself :)

can you give some advice how to use that auth instance in this case to get connected

little painful but figured it out. At this point I just need to merge in the api calls binding the connection and I'll have a complete class for nodejs connection. With an option could use the native client websocket.

Maybe you'd like to go this way with your repo (single class). If not I'll probably publish this. At least for DRY this should be shared. Gotta believe lots of folks wanting access via node only using the long lived token.

https://gist.github.com/dkebler/fe872178e0de4a5874d6a6157b9f3537

homeassistant.js (es6 module syntax)

import to from 'await-to-js'
import ha from 'home-assistant-js-websocket'
import WebSocket from 'ws'
import { EventEmitter } from  'events'
import ndb from  'debug'
const debug = ndb('home-assistant:ws')

const MSG_TYPE_AUTH_REQUIRED = 'auth_required'
const MSG_TYPE_AUTH_INVALID = 'auth_invalid'
const MSG_TYPE_AUTH_OK = 'auth_ok'

class HomeAssistant extends EventEmitter {
  constructor(config) {
    super()
    this.config = config
    debug('config passed to websocket constructor', this.config)
    this.socket = {} // node websocket if access needed
    this.conn = {} // the connection object needed for api calls
    debug('constructor\n', this.config)
  }

  async connect () {

    let [errws,ws] = await to(createSocket(this.config))
    if (errws) {
      debug('socket create error', errws)
      throw errws
    }
    debug('websocket ready at', ws.url)

    let [err,conn] = await to(ha.createConnection({createSocket: () => {return ws}}))
    if (err) {
      debug('error in connection', err)
      throw err
    }
    this.conn = conn
    this.api = ha  // to be replaced with merged api calls
    debug('Connected to Home Assistant')
    return 'success'


    function createSocket (opts) {

      return new Promise((resolve, reject) => {

        if (!opts.hassUrl) {
          throw ha.ERR_HASS_HOST_REQUIRED
        }

        const auth = JSON.stringify({
          type: 'auth',
          access_token: opts.access_token
        })

        let url = `${opts.hassUrl}/${opts.wssPath || 'api/websocket'}`

        debug('[Auth Phase] Initializing', url)

        setTimeout(() => reject(new Error('Unable to Authorize in 5 seconds')),5000)

        let ws = new WebSocket(url,opts)

        ws.on('error', error.bind(ws))
        function error(err) {
          this.removeAllListeners('message',authorize)
          this.removeAllListeners('error', error)
          reject(err)
        }

        ws.on('message',authorize.bind(ws))
        function authorize(event) {
          const message = JSON.parse(event)
          debug('[Auth Phase] Message Received', message)

          switch (message.type) {
          case MSG_TYPE_AUTH_REQUIRED:
            try {
              debug('[Auth Phase] sending authorization\n',auth)
              this.send(auth)
            } catch (err) {
              debug('sending auth error')
              this.close()
              reject(new Error('unable to send authorization'))
            }
            break
          case MSG_TYPE_AUTH_OK:
            this.removeAllListeners('message',authorize)
            this.removeAllListeners('error', error)
            resolve(this)
            break
          case MSG_TYPE_AUTH_INVALID:
            this.close()
            reject(new Error(MSG_TYPE_AUTH_INVALID))
            break
          default:
            debug('[Auth Phase] Unhandled message', message)
            this.close()
            reject(new Error(`unhandled authorization error, ${message}`))
          }
        } // end authorize

      })
    } // end createSocket
  }

} // end class

export default HomeAssistant

usage looks like this

import to from 'await-to-js'
import Hass from './lib/homeassistant'

let opts = {
  hassUrl: '<ws/http/https>://<your domain or ip>:8123',
  access_token: 'your long lived token'
}

let hass = new Hass(opts)

;
(async () => {
  await hass.connect()
  // eventually do stuff now with hass like hass.subscribeEntities(handler)
  // but for now like this   
  hass.api.subscribeEntities(hass.conn, entities => console.log('New entities!', entities))

 })().catch(err => {
  console.error('FATAL: UNABLE TO START SYSTEM!\n',err)
  process.kill(process.pid, 'SIGTERM')
})

I don't have time to help you debug your program. However, have a look at these two Node projects that use this lib: