UNPKG

4.13 kBJavaScriptView Raw
1/**
2 * Create a web socket connection with a Home Assistant instance.
3 */
4import { ERR_INVALID_AUTH, ERR_CANNOT_CONNECT, ERR_HASS_HOST_REQUIRED } from "./errors.js";
5import * as messages from "./messages.js";
6const DEBUG = false;
7export const MSG_TYPE_AUTH_REQUIRED = "auth_required";
8export const MSG_TYPE_AUTH_INVALID = "auth_invalid";
9export const MSG_TYPE_AUTH_OK = "auth_ok";
10export function createSocket(options) {
11 if (!options.auth) {
12 throw ERR_HASS_HOST_REQUIRED;
13 }
14 const auth = options.auth;
15 // Start refreshing expired tokens even before the WS connection is open.
16 // We know that we will need auth anyway.
17 let authRefreshTask = auth.expired
18 ? auth.refreshAccessToken().then(() => {
19 authRefreshTask = undefined;
20 }, () => {
21 authRefreshTask = undefined;
22 })
23 : undefined;
24 // Convert from http:// -> ws://, https:// -> wss://
25 const url = auth.wsUrl;
26 if (DEBUG) {
27 console.log("[Auth phase] Initializing", url);
28 }
29 function connect(triesLeft, promResolve, promReject) {
30 if (DEBUG) {
31 console.log("[Auth Phase] New connection", url);
32 }
33 const socket = new WebSocket(url);
34 // If invalid auth, we will not try to reconnect.
35 let invalidAuth = false;
36 const closeMessage = () => {
37 // If we are in error handler make sure close handler doesn't also fire.
38 socket.removeEventListener("close", closeMessage);
39 if (invalidAuth) {
40 promReject(ERR_INVALID_AUTH);
41 return;
42 }
43 // Reject if we no longer have to retry
44 if (triesLeft === 0) {
45 // We never were connected and will not retry
46 promReject(ERR_CANNOT_CONNECT);
47 return;
48 }
49 const newTries = triesLeft === -1 ? -1 : triesLeft - 1;
50 // Try again in a second
51 setTimeout(() => connect(newTries, promResolve, promReject), 1000);
52 };
53 // Auth is mandatory, so we can send the auth message right away.
54 const handleOpen = async (event) => {
55 try {
56 if (auth.expired) {
57 await (authRefreshTask ? authRefreshTask : auth.refreshAccessToken());
58 }
59 socket.send(JSON.stringify(messages.auth(auth.accessToken)));
60 }
61 catch (err) {
62 // Refresh token failed
63 invalidAuth = err === ERR_INVALID_AUTH;
64 socket.close();
65 }
66 };
67 const handleMessage = async (event) => {
68 const message = JSON.parse(event.data);
69 if (DEBUG) {
70 console.log("[Auth phase] Received", message);
71 }
72 switch (message.type) {
73 case MSG_TYPE_AUTH_INVALID:
74 invalidAuth = true;
75 socket.close();
76 break;
77 case MSG_TYPE_AUTH_OK:
78 socket.removeEventListener("open", handleOpen);
79 socket.removeEventListener("message", handleMessage);
80 socket.removeEventListener("close", closeMessage);
81 socket.removeEventListener("error", closeMessage);
82 socket.haVersion = message.ha_version;
83 promResolve(socket);
84 break;
85 default:
86 if (DEBUG) {
87 // We already send response to this message when socket opens
88 if (message.type !== MSG_TYPE_AUTH_REQUIRED) {
89 console.warn("[Auth phase] Unhandled message", message);
90 }
91 }
92 }
93 };
94 socket.addEventListener("open", handleOpen);
95 socket.addEventListener("message", handleMessage);
96 socket.addEventListener("close", closeMessage);
97 socket.addEventListener("error", closeMessage);
98 }
99 return new Promise((resolve, reject) => connect(options.setupRetry, resolve, reject));
100}