├── .npmignore ├── .gitignore ├── .prettierrc.json ├── package.json ├── LICENSE.md ├── CHANGELOG.md ├── yarn.lock ├── README.md ├── tsconfig.json └── src └── index.ts /.npmignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { "singleQuote": true } 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homeassistant-ws", 3 | "version": "0.2.5", 4 | "license": "MIT", 5 | "description": "Client for Homeassistant's websocket API", 6 | "keywords": [ 7 | "homeassistant", 8 | "websocket", 9 | "hass", 10 | "ha", 11 | "smart", 12 | "home", 13 | "api", 14 | "hassio" 15 | ], 16 | "homepage": "https://github.com/filp/homeassistant-ws", 17 | "main": "build/index.js", 18 | "types": "build/index.d.ts", 19 | "bugs": { 20 | "url": "https://github.com/filp/homeassistant-ws/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/filp/homeassistant-ws.git" 25 | }, 26 | "scripts": { 27 | "build": "yarn prepare", 28 | "prepare": "tsc" 29 | }, 30 | "devDependencies": { 31 | "@types/events": "^3.0.0", 32 | "@types/node": "^15.6.0", 33 | "prettier": "^2.3.0", 34 | "typescript": "^4.2.4" 35 | }, 36 | "dependencies": { 37 | "isomorphic-ws": "^4.0.1", 38 | "ws": "^8.0.0" 39 | }, 40 | "optionalDependencies": { 41 | "buffer": "^5.6.0", 42 | "events": "^3.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Filipe Dobreira 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. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 0.2.5 4 | 5 | Minor improvements 6 | 7 | - Exposed `command` method, which was previously only available internally 8 | 9 | This is a thin wrapper that allows you to directly call arbitrary HomeAssistant WebSocket API commands. For example, to retrieve a list of persistent notifications: 10 | 11 | ```ts 12 | async function getPersistentNotifications() { 13 | const notifications = await hass.command('persistent_notification/get'); 14 | 15 | if (notifications.length > 0) { 16 | notifications.forEach((notif) => { 17 | console.log(notif.title, notif.message); 18 | }); 19 | } 20 | } 21 | ``` 22 | 23 | This was previously only possible by directly acessing the websocket client, and managing the command result-phase manually (including incrementing the internal message ID correctly). 24 | 25 | - Fixed an issue with an outdated value in `package.json` 26 | - Updated some of the type annotations for clarity 27 | 28 | # 0.2.0 29 | 30 | Typescript rewrite 31 | 32 | - Rewrote codebase in Typescript 33 | - Migrated to use Yarn instead of npm 34 | - Simplified event subscription flow for most common case: homeassistant-ws automatically subscribes to all events, and 35 | handles delegation internally. 36 | 37 | ## 0.1.1 38 | 39 | Quality of life improvements: 40 | 41 | - Add initial type information 42 | - Fix issue where WebSocket errors were wrapped in a new `Error` before a `reject()` 43 | - Update README 44 | 45 | ## 0.1.0 46 | 47 | First release 48 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/events@^3.0.0": 6 | version "3.0.0" 7 | resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" 8 | integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== 9 | 10 | "@types/node@^15.6.0": 11 | version "15.6.0" 12 | resolved "https://registry.yarnpkg.com/@types/node/-/node-15.6.0.tgz#f0ddca5a61e52627c9dcb771a6039d44694597bc" 13 | integrity sha512-gCYSfQpy+LYhOFTKAeE8BkyGqaxmlFxe+n4DKM6DR0wzw/HISUE/hAmkC/KT8Sw5PCJblqg062b3z9gucv3k0A== 14 | 15 | base64-js@^1.3.1: 16 | version "1.5.1" 17 | resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" 18 | integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== 19 | 20 | buffer@^5.6.0: 21 | version "5.7.1" 22 | resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" 23 | integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== 24 | dependencies: 25 | base64-js "^1.3.1" 26 | ieee754 "^1.1.13" 27 | 28 | events@^3.1.0: 29 | version "3.3.0" 30 | resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" 31 | integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== 32 | 33 | ieee754@^1.1.13: 34 | version "1.2.1" 35 | resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" 36 | integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== 37 | 38 | isomorphic-ws@^4.0.1: 39 | version "4.0.1" 40 | resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" 41 | integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== 42 | 43 | prettier@^2.3.0: 44 | version "2.3.0" 45 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18" 46 | integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w== 47 | 48 | typescript@^4.2.4: 49 | version "4.2.4" 50 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.2.4.tgz#8610b59747de028fda898a8aef0e103f156d0961" 51 | integrity sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg== 52 | 53 | ws@^8.0.0: 54 | version "8.0.0" 55 | resolved "https://registry.yarnpkg.com/ws/-/ws-8.0.0.tgz#550605d13dfc1437c9ec1396975709c6d7ffc57d" 56 | integrity sha512-6AcSIXpBlS0QvCVKk+3cWnWElLsA6SzC0lkQ43ciEglgXJXiCWK3/CGFEJ+Ybgp006CMibamAsqOlxE9s4AvYA== 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # homeassistant-ws 2 | 3 | [![npm](https://img.shields.io/npm/v/homeassistant-ws?color=%23ff11dd&style=flat-square)](https://www.npmjs.com/package/homeassistant-ws) 4 | [![GitHub](https://img.shields.io/github/license/filp/homeassistant-ws?style=flat-square)](https://github.com/filp/homeassistant-ws/blob/master/LICENSE.md) 5 | 6 | Minimalist client library for [Homeassistant's Websocket API](https://developers.home-assistant.io/docs/external_api_websocket). Works in node, and also in the browser. 7 | 8 | --- 9 | 10 | ## Installation: 11 | 12 | Using `npm`: 13 | 14 | ```shell 15 | $ npm i --save homeassistant-ws 16 | ``` 17 | 18 | Import it in your project: 19 | 20 | ```js 21 | import hass from 'homeassistant-ws'; 22 | 23 | async function main() { 24 | // Assuming hass running in `localhost`, under the default `8321` port: 25 | const client = await hass({ 26 | token: 'my-secret-token', 27 | }); 28 | } 29 | ``` 30 | 31 | Tokens are available from your profile page under the Homeassistant UI. For documentation on the authentication API, see [the official HA documentation](https://developers.home-assistant.io/docs/auth_api/). 32 | 33 | ## Configuration options 34 | 35 | The following properties (shown with their defaults) can be passed to the constructor. All are **optional**. 36 | 37 | ```js 38 | hass({ 39 | protocol: 'ws', 40 | host: 'localhost', 41 | port: 8123, 42 | path: '/api/websocket', 43 | 44 | // Must be set if HA expects authentication: 45 | token: null, 46 | 47 | // Used to serialize outgoing messages: 48 | messageSerializer: (outgoingMessage) => JSON.stringify(outgoingMessage), 49 | 50 | // Used to parse incoming messages. Receives the entire Websocket message object: 51 | messageParser: (incomingMessage) => JSON.parse(incomingMessage.data), 52 | 53 | // Should return a WebSocket instance 54 | ws: (opts) => { 55 | return new WebSocket( 56 | `${opts.protocol}://${opts.host}:${opts.port}${opts.path}` 57 | ); 58 | }, 59 | }); 60 | ``` 61 | 62 | ## Example 63 | 64 | The following example includes all available methods. For more details on available Homeassistant event types, states, etc. see the [official Websocket API](https://developers.home-assistant.io/docs/external_api_websocket) 65 | 66 | ```js 67 | import hass from 'hass'; 68 | 69 | async function main() { 70 | // Establishes a connection, and authenticates if necessary: 71 | const client = await hass({ token: 'my-token' }); 72 | 73 | // Get a list of all available states, panels or services: 74 | await client.getStates(); 75 | await client.getServices(); 76 | await client.getPanels(); 77 | 78 | // Get hass configuration: 79 | await client.getConfig(); 80 | 81 | // Get a Buffer containing the current thumbnail for the given media player 82 | await client.getMediaPlayerThumbnail('media_player.my_player'); 83 | // { content_type: 'image/jpeg', content: Buffer<...>} 84 | 85 | // Get a Buffer containing a thumbnail for the given camera 86 | await client.getCameraThumbnail('camera.front_yard'); 87 | // { content_type: 'image/jpeg', content: Buffer<...>} 88 | 89 | // Call a service, by its domain and name. The third argument is optional. 90 | await client.callService('lights', 'turn_on', { 91 | entity_id: 'light.my_light', 92 | }); 93 | 94 | // Listen for all HASS events - the 'message' event is a homeassistant-ws event triggered for 95 | // all messages received through the websocket connection with HASS: 96 | // 97 | // See https://developers.home-assistant.io/docs/api/websocket/ for details on HASS events: 98 | client.on('message', (rawMessageData) => { 99 | console.log(rawMessageData); 100 | }); 101 | 102 | // Listen only for state changes: 103 | client.on('state_changed', (stateChangedEvent) => { 104 | console.log(stateChangedEvent.data.new_state.state); 105 | }); 106 | } 107 | ``` 108 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./build/" /* Redirect output structure to the directory. */, 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 48 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true /* Skip type checking of declaration files. */, 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'events'; 2 | import WebSocket from 'isomorphic-ws'; 3 | 4 | export type HassWsOptions = { 5 | protocol: 'ws' | 'wss'; 6 | host: string; 7 | port: number; 8 | path: string; 9 | token: string; 10 | messageSerializer: (outgoingMessage: any) => string; 11 | messageParser: (incomingMessage: MessageEvent) => any; 12 | ws: (opts: HassWsOptions) => WebSocket; 13 | }; 14 | 15 | type HassClient = { 16 | seq: number; 17 | options: HassWsOptions; 18 | resultMap: { [resultId: number]: any }; 19 | emitter: EventEmitter; 20 | ws: WebSocket; 21 | }; 22 | 23 | type HassCommandArgs = { 24 | type: string; 25 | [additionalArg: string]: any; 26 | }; 27 | 28 | export type EventListener = (...args: any[]) => void; 29 | export type EventType = string | symbol; 30 | 31 | export type HassApi = { 32 | rawClient: HassClient; 33 | getStates: () => Promise; 34 | getServices: () => Promise; 35 | getPanels: () => Promise; 36 | getConfig: () => Promise<{}>; 37 | 38 | getMediaPlayerThumbnail: (entityId: string) => Promise<{}>; 39 | getCameraThumbnail: (entityId: string) => Promise<{}>; 40 | 41 | /** 42 | * Allows calling arbitrary Hass WS commands, outside of the ones officially 43 | * supported by homeassistant-ws. 44 | * 45 | * Returns a promise that resolves once a result is received. 46 | * 47 | * Refer to the HomeAssistant WebSocket API documentation for details on 48 | * available commands. 49 | */ 50 | command: ( 51 | commandType: string, 52 | additionalArgs?: Record 53 | ) => Promise; 54 | 55 | /** 56 | * Bind a listener on the internal event emitter used by homeassistant-ws. Can be 57 | * used to set your own custom event handlers for HomeAssistant WebSocket API events. 58 | */ 59 | on: (eventType: EventType, cb: EventListener) => void; 60 | 61 | callService: ( 62 | domain: string, 63 | service: string, 64 | extraArgs?: any, 65 | options?: { 66 | returnResponse?: boolean; 67 | } 68 | ) => Promise; 69 | }; 70 | 71 | const defaultOptions: Partial = { 72 | protocol: 'ws', 73 | host: 'localhost', 74 | port: 8123, 75 | path: '/api/websocket', 76 | 77 | messageSerializer: (outgoingMessage: any) => JSON.stringify(outgoingMessage), 78 | messageParser: (incomingMessage: { data: string }) => 79 | JSON.parse(incomingMessage.data), 80 | 81 | // A method that returns a websocket instance. Can be overriden to use a custom behavior: 82 | ws: (opts: HassWsOptions) => { 83 | return new WebSocket( 84 | `${opts.protocol}://${opts.host}:${opts.port}${opts.path}` 85 | ); 86 | }, 87 | }; 88 | 89 | const command = async ( 90 | commandArgs: HassCommandArgs, 91 | client: HassClient 92 | ): Promise => { 93 | return new Promise((resolve, reject) => { 94 | const id = client.seq; 95 | 96 | client.resultMap[id] = (resultMessage: any) => { 97 | if (resultMessage.success) resolve(resultMessage.result); 98 | else reject(new Error(resultMessage.error.message)); 99 | 100 | // We won't need this callback again once we use it: 101 | delete client.resultMap[id]; 102 | }; 103 | 104 | client.ws.send( 105 | client.options.messageSerializer({ 106 | ...commandArgs, 107 | id, 108 | }) 109 | ); 110 | 111 | // Increment the shared message id sequence: 112 | client.seq++; 113 | }); 114 | }; 115 | 116 | const binaryResultTransform = (result: any) => { 117 | return { 118 | content_type: result.content_type, 119 | content: Buffer.from(result.content, 'base64'), 120 | }; 121 | }; 122 | 123 | const messageHandler = (client: HassClient) => { 124 | return (wsMessage: MessageEvent) => { 125 | const message = client.options.messageParser(wsMessage); 126 | 127 | // Emit an event for any message under a main 'message' listener: 128 | client.emitter.emit('message', message); 129 | 130 | // Emit an event for any message of any type: 131 | if (message.type) client.emitter.emit(message.type, message); 132 | 133 | // Emit an event for event-type messages: 134 | if (message.type === 'event' && message.event.event_type) { 135 | client.emitter.emit(message.event.event_type, message.event); 136 | } 137 | 138 | // If this is a result message, match it with the results map on the client 139 | // and call the matching function: 140 | if (message.id && message.type === 'result') { 141 | if (typeof client.resultMap[message.id] !== 'undefined') { 142 | client.resultMap[message.id](message); 143 | } 144 | } 145 | }; 146 | }; 147 | 148 | const clientObject = (client: HassClient): HassApi => { 149 | return { 150 | rawClient: client, 151 | 152 | command: async ( 153 | commandType: string, 154 | additionalArgs: Record = {} 155 | ) => { 156 | return command( 157 | { 158 | type: commandType, 159 | ...additionalArgs, 160 | }, 161 | client 162 | ); 163 | }, 164 | 165 | getStates: async () => command({ type: 'get_states' }, client), 166 | getServices: async () => command({ type: 'get_services' }, client), 167 | getPanels: async () => command({ type: 'get_panels' }, client), 168 | getConfig: async () => command({ type: 'get_config' }, client), 169 | 170 | on: (eventId: EventType, cb: EventListener): void => { 171 | client.emitter.on(eventId, cb); 172 | }, 173 | 174 | async callService( 175 | domain, 176 | service, 177 | additionalArgs = {}, 178 | options = { 179 | returnResponse: false, 180 | } 181 | ) { 182 | return command( 183 | { 184 | type: 'call_service', 185 | domain, 186 | service, 187 | service_data: additionalArgs, 188 | return_response: options?.returnResponse, 189 | }, 190 | client 191 | ); 192 | }, 193 | 194 | async getMediaPlayerThumbnail(entityId) { 195 | return command( 196 | { 197 | type: 'media_player_thumbnail', 198 | entity_id: entityId, 199 | }, 200 | client 201 | ).then(binaryResultTransform); 202 | }, 203 | 204 | async getCameraThumbnail(entityId) { 205 | return command( 206 | { 207 | type: 'camera_thumbnail', 208 | entity_id: entityId, 209 | }, 210 | client 211 | ).then(binaryResultTransform); 212 | }, 213 | }; 214 | }; 215 | 216 | const connectAndAuthorize = async ( 217 | client: HassClient, 218 | resolveWith: HassApi 219 | ): Promise => { 220 | return new Promise((resolve, reject) => { 221 | client.ws.onmessage = messageHandler(client); 222 | 223 | client.ws.onerror = (err: Error) => { 224 | // Unlikely for a listener to exist at this stage, but just in case: 225 | client.emitter.emit('ws_error', err); 226 | reject(err); 227 | }; 228 | 229 | // Pass-through onclose events to the client: 230 | client.ws.onclose = (event: CloseEvent) => 231 | client.emitter.emit('ws_close', event); 232 | 233 | client.emitter.on('auth_ok', () => { 234 | // Immediately subscribe to all events, and return the client handle: 235 | command({ type: 'subscribe_events' }, client) 236 | .then(() => resolve(resolveWith)) 237 | .catch((err) => reject(err)); 238 | }); 239 | 240 | client.emitter.on('auth_invalid', (msg: { message: string }) => 241 | reject(new Error(msg.message)) 242 | ); 243 | client.emitter.on('auth_required', () => { 244 | // If auth is required, immediately reject the promise if no token was provided: 245 | if (!client.options.token) { 246 | reject( 247 | new Error( 248 | 'Homeassistant requires authentication, but token not provided in options' 249 | ) 250 | ); 251 | } 252 | 253 | client.ws.send( 254 | client.options.messageSerializer({ 255 | type: 'auth', 256 | access_token: client.options.token, 257 | }) 258 | ); 259 | }); 260 | }); 261 | }; 262 | 263 | export default async function createClient( 264 | callerOptions: Partial = {} 265 | ): Promise { 266 | const options = { 267 | ...defaultOptions, 268 | ...callerOptions, 269 | } as HassWsOptions; 270 | 271 | const client: HassClient = { 272 | seq: 1, 273 | options, 274 | resultMap: {}, 275 | emitter: new (EventEmitter as any)(), 276 | ws: options.ws(options), 277 | }; 278 | 279 | return connectAndAuthorize(client, clientObject(client)); 280 | } 281 | --------------------------------------------------------------------------------