home-assistant-js-websocket
Advanced tools
Comparing version 7.1.0 to 8.0.0
@@ -114,6 +114,5 @@ import { parseQuery } from "./util.js"; | ||
const formData = new FormData(); | ||
formData.append("action", "revoke"); | ||
formData.append("token", this.data.refresh_token); | ||
// There is no error checking, as revoke will always return 200 | ||
await fetch(`${this.data.hassUrl}/auth/token`, { | ||
await fetch(`${this.data.hassUrl}/auth/revoke`, { | ||
method: "POST", | ||
@@ -120,0 +119,0 @@ credentials: "same-origin", |
@@ -11,47 +11,52 @@ /** | ||
this._handleMessage = (event) => { | ||
const message = JSON.parse(event.data); | ||
if (DEBUG) { | ||
console.log("Received", message); | ||
let messageGroup = JSON.parse(event.data); | ||
if (!Array.isArray(messageGroup)) { | ||
messageGroup = [messageGroup]; | ||
} | ||
const info = this.commands.get(message.id); | ||
switch (message.type) { | ||
case "event": | ||
if (info) { | ||
info.callback(message.event); | ||
} | ||
else { | ||
console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`); | ||
this.sendMessagePromise(messages.unsubscribeEvents(message.id)); | ||
} | ||
break; | ||
case "result": | ||
// No info is fine. If just sendMessage is used, we did not store promise for result | ||
if (info) { | ||
if (message.success) { | ||
info.resolve(message.result); | ||
// Don't remove subscriptions. | ||
if (!("subscribe" in info)) { | ||
messageGroup.forEach((message) => { | ||
if (DEBUG) { | ||
console.log("Received", message); | ||
} | ||
const info = this.commands.get(message.id); | ||
switch (message.type) { | ||
case "event": | ||
if (info) { | ||
info.callback(message.event); | ||
} | ||
else { | ||
console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`); | ||
this.sendMessagePromise(messages.unsubscribeEvents(message.id)); | ||
} | ||
break; | ||
case "result": | ||
// No info is fine. If just sendMessage is used, we did not store promise for result | ||
if (info) { | ||
if (message.success) { | ||
info.resolve(message.result); | ||
// Don't remove subscriptions. | ||
if (!("subscribe" in info)) { | ||
this.commands.delete(message.id); | ||
} | ||
} | ||
else { | ||
info.reject(message.error); | ||
this.commands.delete(message.id); | ||
} | ||
} | ||
else { | ||
info.reject(message.error); | ||
break; | ||
case "pong": | ||
if (info) { | ||
info.resolve(); | ||
this.commands.delete(message.id); | ||
} | ||
} | ||
break; | ||
case "pong": | ||
if (info) { | ||
info.resolve(); | ||
this.commands.delete(message.id); | ||
} | ||
else { | ||
console.warn(`Received unknown pong response ${message.id}`); | ||
} | ||
break; | ||
default: | ||
if (DEBUG) { | ||
console.warn("Unhandled message", message); | ||
} | ||
} | ||
else { | ||
console.warn(`Received unknown pong response ${message.id}`); | ||
} | ||
break; | ||
default: | ||
if (DEBUG) { | ||
console.warn("Unhandled message", message); | ||
} | ||
} | ||
}); | ||
}; | ||
@@ -124,3 +129,3 @@ this._handleClose = async () => { | ||
// id if next command to send | ||
this.commandId = 1; | ||
this.commandId = 2; // socket may send 1 at the start to enable features | ||
// info about active subscriptions and commands in flight | ||
@@ -127,0 +132,0 @@ this.commands = new Map(); |
@@ -19,2 +19,9 @@ (function (global, factory) { | ||
} | ||
function supportedFeatures() { | ||
return { | ||
type: "supported_features", | ||
id: 1, | ||
features: { coalesce_messages: 1 }, | ||
}; | ||
} | ||
function states() { | ||
@@ -83,2 +90,52 @@ return { | ||
function parseQuery(queryString) { | ||
const query = {}; | ||
const items = queryString.split("&"); | ||
for (let i = 0; i < items.length; i++) { | ||
const item = items[i].split("="); | ||
const key = decodeURIComponent(item[0]); | ||
const value = item.length > 1 ? decodeURIComponent(item[1]) : undefined; | ||
query[key] = value; | ||
} | ||
return query; | ||
} | ||
// From: https://davidwalsh.name/javascript-debounce-function | ||
// Returns a function, that, as long as it continues to be invoked, will not | ||
// be triggered. The function will be called after it stops being called for | ||
// N milliseconds. If `immediate` is passed, trigger the function on the | ||
// leading edge, instead of the trailing. | ||
// eslint-disable-next-line: ban-types | ||
const debounce = (func, wait, immediate = false) => { | ||
let timeout; | ||
// @ts-ignore | ||
return function (...args) { | ||
// @ts-ignore | ||
const context = this; | ||
const later = () => { | ||
timeout = undefined; | ||
if (!immediate) { | ||
func.apply(context, args); | ||
} | ||
}; | ||
const callNow = immediate && !timeout; | ||
clearTimeout(timeout); | ||
timeout = setTimeout(later, wait); | ||
if (callNow) { | ||
func.apply(context, args); | ||
} | ||
}; | ||
}; | ||
const atLeastHaVersion = (version, major, minor, patch) => { | ||
const [haMajor, haMinor, haPatch] = version.split(".", 3); | ||
return (Number(haMajor) > major || | ||
(Number(haMajor) === major && | ||
(patch === undefined | ||
? Number(haMinor) >= minor | ||
: Number(haMinor) > minor)) || | ||
(patch !== undefined && | ||
Number(haMajor) === major && | ||
Number(haMinor) === minor && | ||
Number(haPatch) >= patch)); | ||
}; | ||
/** | ||
@@ -154,2 +211,5 @@ * Create a web socket connection with a Home Assistant instance. | ||
socket.haVersion = message.ha_version; | ||
if (atLeastHaVersion(socket.haVersion, 2022, 9)) { | ||
socket.send(JSON.stringify(supportedFeatures())); | ||
} | ||
promResolve(socket); | ||
@@ -174,40 +234,45 @@ break; | ||
this._handleMessage = (event) => { | ||
const message = JSON.parse(event.data); | ||
const info = this.commands.get(message.id); | ||
switch (message.type) { | ||
case "event": | ||
if (info) { | ||
info.callback(message.event); | ||
} | ||
else { | ||
console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`); | ||
this.sendMessagePromise(unsubscribeEvents(message.id)); | ||
} | ||
break; | ||
case "result": | ||
// No info is fine. If just sendMessage is used, we did not store promise for result | ||
if (info) { | ||
if (message.success) { | ||
info.resolve(message.result); | ||
// Don't remove subscriptions. | ||
if (!("subscribe" in info)) { | ||
let messageGroup = JSON.parse(event.data); | ||
if (!Array.isArray(messageGroup)) { | ||
messageGroup = [messageGroup]; | ||
} | ||
messageGroup.forEach((message) => { | ||
const info = this.commands.get(message.id); | ||
switch (message.type) { | ||
case "event": | ||
if (info) { | ||
info.callback(message.event); | ||
} | ||
else { | ||
console.warn(`Received event for unknown subscription ${message.id}. Unsubscribing.`); | ||
this.sendMessagePromise(unsubscribeEvents(message.id)); | ||
} | ||
break; | ||
case "result": | ||
// No info is fine. If just sendMessage is used, we did not store promise for result | ||
if (info) { | ||
if (message.success) { | ||
info.resolve(message.result); | ||
// Don't remove subscriptions. | ||
if (!("subscribe" in info)) { | ||
this.commands.delete(message.id); | ||
} | ||
} | ||
else { | ||
info.reject(message.error); | ||
this.commands.delete(message.id); | ||
} | ||
} | ||
else { | ||
info.reject(message.error); | ||
break; | ||
case "pong": | ||
if (info) { | ||
info.resolve(); | ||
this.commands.delete(message.id); | ||
} | ||
} | ||
break; | ||
case "pong": | ||
if (info) { | ||
info.resolve(); | ||
this.commands.delete(message.id); | ||
} | ||
else { | ||
console.warn(`Received unknown pong response ${message.id}`); | ||
} | ||
break; | ||
} | ||
else { | ||
console.warn(`Received unknown pong response ${message.id}`); | ||
} | ||
break; | ||
} | ||
}); | ||
}; | ||
@@ -277,3 +342,3 @@ this._handleClose = async () => { | ||
// id if next command to send | ||
this.commandId = 1; | ||
this.commandId = 2; // socket may send 1 at the start to enable features | ||
// info about active subscriptions and commands in flight | ||
@@ -477,52 +542,2 @@ this.commands = new Map(); | ||
function parseQuery(queryString) { | ||
const query = {}; | ||
const items = queryString.split("&"); | ||
for (let i = 0; i < items.length; i++) { | ||
const item = items[i].split("="); | ||
const key = decodeURIComponent(item[0]); | ||
const value = item.length > 1 ? decodeURIComponent(item[1]) : undefined; | ||
query[key] = value; | ||
} | ||
return query; | ||
} | ||
// From: https://davidwalsh.name/javascript-debounce-function | ||
// Returns a function, that, as long as it continues to be invoked, will not | ||
// be triggered. The function will be called after it stops being called for | ||
// N milliseconds. If `immediate` is passed, trigger the function on the | ||
// leading edge, instead of the trailing. | ||
// eslint-disable-next-line: ban-types | ||
const debounce = (func, wait, immediate = false) => { | ||
let timeout; | ||
// @ts-ignore | ||
return function (...args) { | ||
// @ts-ignore | ||
const context = this; | ||
const later = () => { | ||
timeout = undefined; | ||
if (!immediate) { | ||
func.apply(context, args); | ||
} | ||
}; | ||
const callNow = immediate && !timeout; | ||
clearTimeout(timeout); | ||
timeout = setTimeout(later, wait); | ||
if (callNow) { | ||
func.apply(context, args); | ||
} | ||
}; | ||
}; | ||
const atLeastHaVersion = (version, major, minor, patch) => { | ||
const [haMajor, haMinor, haPatch] = version.split(".", 3); | ||
return (Number(haMajor) > major || | ||
(Number(haMajor) === major && | ||
(patch === undefined | ||
? Number(haMinor) >= minor | ||
: Number(haMinor) > minor)) || | ||
(patch !== undefined && | ||
Number(haMajor) === major && | ||
Number(haMinor) === minor && | ||
Number(haPatch) >= patch)); | ||
}; | ||
const genClientId = () => `${location.protocol}//${location.host}/`; | ||
@@ -639,6 +654,5 @@ const genExpires = (expires_in) => { | ||
const formData = new FormData(); | ||
formData.append("action", "revoke"); | ||
formData.append("token", this.data.refresh_token); | ||
// There is no error checking, as revoke will always return 200 | ||
await fetch(`${this.data.hassUrl}/auth/token`, { | ||
await fetch(`${this.data.hassUrl}/auth/revoke`, { | ||
method: "POST", | ||
@@ -645,0 +659,0 @@ credentials: "same-origin", |
@@ -6,2 +6,9 @@ import { Error, HassServiceTarget } from "./types.js"; | ||
}; | ||
export declare function supportedFeatures(): { | ||
type: string; | ||
id: number; | ||
features: { | ||
coalesce_messages: number; | ||
}; | ||
}; | ||
export declare function states(): { | ||
@@ -8,0 +15,0 @@ type: string; |
@@ -7,2 +7,9 @@ export function auth(accessToken) { | ||
} | ||
export function supportedFeatures() { | ||
return { | ||
type: "supported_features", | ||
id: 1, | ||
features: { coalesce_messages: 1 }, | ||
}; | ||
} | ||
export function states() { | ||
@@ -9,0 +16,0 @@ return { |
@@ -6,2 +6,3 @@ /** | ||
import * as messages from "./messages.js"; | ||
import { atLeastHaVersion } from "./util.js"; | ||
const DEBUG = false; | ||
@@ -84,2 +85,5 @@ export const MSG_TYPE_AUTH_REQUIRED = "auth_required"; | ||
socket.haVersion = message.ha_version; | ||
if (atLeastHaVersion(socket.haVersion, 2022, 9)) { | ||
socket.send(JSON.stringify(messages.supportedFeatures())); | ||
} | ||
promResolve(socket); | ||
@@ -86,0 +90,0 @@ break; |
@@ -5,3 +5,3 @@ { | ||
"sideEffects": false, | ||
"version": "7.1.0", | ||
"version": "8.0.0", | ||
"description": "Home Assistant websocket client", | ||
@@ -40,3 +40,3 @@ "source": "lib/index.ts", | ||
"husky": "^4.2.5", | ||
"lint-staged": "^12.0.2", | ||
"lint-staged": "^13.0.0", | ||
"mocha": "^8.0.1", | ||
@@ -43,0 +43,0 @@ "prettier": "^2.0.5", |
@@ -446,20 +446,16 @@ # :aerial_tramway: JavaScript websocket client for Home Assistant | ||
NodeJS does not have a WebSocket client built-in, but there are some good ones on NPM. We recommend ws. You will need to create your own version of createSocket and pass that to the constructor. | ||
NodeJS does not have a WebSocket client built-in, but there are some good ones on NPM. We recommend ws. The easiest way to enable WebSocket is to polyfill it into the global namespace. | ||
Look at https://github.com/keesschollaart81/vscode-home-assistant/blob/master/src/language-service/src/home-assistant/socket.ts as an example using ws. | ||
If using TypeScript, you will need to add `"skipLibCheck": true` to your tsconfig.json to avoid typing errors. | ||
If using TypeScript, you will need to install `@types/ws` as well. | ||
```js | ||
const WebSocket = require("ws"); | ||
globalThis.WebSocket = require("ws"); | ||
``` | ||
createConnection({ | ||
createSocket() { | ||
// Open connection | ||
const ws = new WebSocket("ws://localhost:8123"); | ||
or in TypeScript: | ||
// TODO: Handle authentication with Home Assistant yourself :) | ||
return ws; | ||
}, | ||
}); | ||
```ts | ||
const wnd = globalThis; | ||
wnd.WebSocket = require("ws"); | ||
``` |
Sorry, the diff of this file is not supported yet
174579
3672
461