├── .gitignore ├── package.json ├── examples └── simple.js ├── LICENSE ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-homeassistant", 3 | "version": "1.6.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "ws": "^6.0.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | const Homeassistant = require('./../index') 2 | 3 | // Connect to home-assistant 4 | let ha = new Homeassistant({ 5 | host: '192.168.1.166' 6 | }) 7 | 8 | ha.connect() 9 | .then(() => { 10 | // subscribe to state changes 11 | ha.on('state:media_player.spotify', data => { 12 | console.log(data) 13 | }) 14 | 15 | // access current state 16 | console.log(ha.state('sun.sun')) 17 | 18 | // call a service 19 | return ha.call({ 20 | domain: 'light', 21 | service: 'turn_on' 22 | }) 23 | }) 24 | .catch(console.error) 25 | 26 | ha.on('connection', info => { 27 | console.log('connection changed', info) 28 | }) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Martin Wagner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js api for home-assistant [![](https://img.shields.io/npm/v/node-homeassistant.svg)](https://www.npmjs.com/package/node-homeassistant) 2 | 3 | A simple package to access & controll home-assistant from node.js using the websocket api. 4 | 5 | ## Installation 6 | 7 | ``` 8 | $ npm install node-homeassistant 9 | ``` 10 | 11 | ## Usage 12 | 13 | Create a new Homeassistant object: 14 | 15 | ```javascript 16 | const Homeassistant = require('node-homeassistant') 17 | 18 | let ha = new Homeassistant({ 19 | host: '192.168.1.166', 20 | protocol: 'ws', // "ws" (default) or "wss" for SSL 21 | retryTimeout: 1000, // in ms, default is 5000 22 | retryCount: 3, // default is 10, values < 0 mean unlimited 23 | password: 'http_api_password', // api_password is getting depricated by home assistant 24 | token: 'access_token' // for now both tokens and api_passwords are suported 25 | port: 8123 26 | }) 27 | 28 | ha.connect().then(() => { 29 | // do stuff 30 | }) 31 | ``` 32 | 33 | Access & subscribe to states: 34 | 35 | ```javascript 36 | console.log(ha.state('sun.sun')) 37 | 38 | 39 | ha.on('state:media_player.spotify', data => console.log) 40 | ``` 41 | 42 | Call services: 43 | 44 | ```javascript 45 | ha.call({ 46 | domain: 'light', 47 | service: 'turn_on' 48 | }) 49 | ``` 50 | 51 | You can subscribe to the 'connection' event to get information about the websocket connection. 52 | 53 | ```javascript 54 | ha.on('connection', info => { 55 | console.log('connection state is', info) 56 | }) 57 | ``` 58 | 59 | See the example folders for a working demo. 60 | 61 | # License 62 | 63 | MIT 64 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | const Websocket = require('ws') 3 | 4 | const defaultConfig = { 5 | host: 'localhost', 6 | protocol: 'ws', 7 | retryTimeout: 5000, 8 | timeout: 5000, 9 | retryCount: 10, 10 | port: 8123, 11 | password: '', 12 | token: '' 13 | } 14 | 15 | class Homeassistant extends EventEmitter { 16 | constructor(options) { 17 | super() 18 | 19 | this.config = Object.assign(defaultConfig, options) 20 | 21 | this.url = `${this.config.protocol}://${this.config.host}:${this.config.port}/api/websocket` 22 | this.retriesLeft = this.config.retryCount 23 | this.promises = {} 24 | this.states = [] 25 | this.id = 1 26 | } 27 | 28 | connect() { 29 | this.ws = new Websocket(this.url) 30 | 31 | this.ws.on('message', data => { 32 | data = JSON.parse(data) 33 | 34 | if (data.type == 'auth_ok') { 35 | this.emit('connection', 'authenticated') 36 | } 37 | 38 | if (data.type == 'auth_required') { 39 | if (!this.config.password && !this.config.token) throw new Error('Password required') 40 | 41 | if (this.config.token) 42 | return this.send({type: 'auth', access_token: this.config.token}, false) 43 | 44 | return this.send({type: 'auth', api_password: this.config.password}, false) 45 | } 46 | 47 | if (data.type == 'auth_invalid') { 48 | throw new Error('Invalid password') 49 | } 50 | 51 | let p = this.promises[data.id] 52 | 53 | if (!p) return false; 54 | 55 | if (p.timeout) { 56 | clearTimeout(p.timeout) 57 | } 58 | 59 | if (p.callback) { 60 | p.callback(data) 61 | } 62 | }) 63 | 64 | this.ws.on('open', () => { 65 | this.emit('connection', 'connected') 66 | if(this.retry) { 67 | clearTimeout(this.retry) 68 | this.retry = null 69 | } 70 | 71 | this.retriesLeft = this.config.retryCount 72 | }) 73 | 74 | this.ws.on('error', () => { 75 | this.emit('connection', 'connection_error') 76 | this.reconnect() 77 | }) 78 | 79 | this.ws.on('close', () => { 80 | this.emit('connection', 'connection_closed') 81 | this.reconnect() 82 | }) 83 | 84 | return new Promise((resolve, reject) => { 85 | this.on('connection', info => { 86 | if (info == 'authenticated') resolve(this) 87 | }) 88 | }).then(() => { 89 | return this.send({ 90 | type: 'get_states' 91 | }) 92 | }).then(states => { 93 | this.states = states.result 94 | 95 | return this.subscribe({ 96 | callback: this.updateState.bind(this) 97 | }) 98 | }) 99 | } 100 | 101 | reconnect() { 102 | if (this.retry) return true 103 | 104 | this.retry = setInterval(() => { 105 | if(this.retriesLeft === 0) { 106 | clearTimeout(this.retry) 107 | throw new Error('home-assistant connection closed') 108 | } 109 | 110 | if(this.retriesLeft > 0) this.retriesLeft-- 111 | 112 | try { 113 | this.emit('connection', 'reconnecting') 114 | this.connect() 115 | } catch (error) { } 116 | }, this.config.retryTimeout) 117 | } 118 | 119 | send(data, addId = true) { 120 | if (addId) { 121 | data.id = this.id 122 | this.id++ 123 | } 124 | 125 | return new Promise((resolve, reject) => { 126 | this.promises[data.id] = { 127 | timeout: setTimeout(() => { 128 | return reject(new Error('No response received from home-assistant')) 129 | }, this.config.timeout), 130 | callback: resolve 131 | } 132 | this.ws.send(JSON.stringify(data)) 133 | }) 134 | } 135 | 136 | call(options) { 137 | return this.send(Object.assign({type: 'call_service'}, options)) 138 | } 139 | 140 | subscribe(options) { 141 | if(!options.callback) throw new Error('Callback function is required') 142 | 143 | let data = { type: 'subscribe_events' } 144 | 145 | if(options.event) data.event_type = event 146 | 147 | return this.send(data) 148 | .then((data) => { 149 | if(!data.success) return Promise.reject(new Error(data)) 150 | 151 | this.promises[data.id].callback = options.callback 152 | return Promise.resolve(data) 153 | }) 154 | } 155 | 156 | unsubscribe(subscription) { 157 | return this.send({ 158 | type: 'unsubscribe_events', 159 | subscription 160 | }) 161 | } 162 | 163 | findEntity(id) { 164 | return this.states.findIndex(state => state.entity_id === id) 165 | } 166 | 167 | updateState(change) { 168 | let data = change.event.data 169 | if (change.event.event_type !== 'state_changed') return true 170 | 171 | let changeIndex = this.findEntity(data.entity_id) 172 | 173 | this.states[changeIndex] = data.new_state 174 | this.emit(`state:${data.entity_id}`, data) 175 | } 176 | 177 | state(entity) { 178 | return this.states[this.findEntity(entity)] 179 | } 180 | } 181 | 182 | module.exports = Homeassistant 183 | --------------------------------------------------------------------------------