-
Notifications
You must be signed in to change notification settings - Fork 84
/
socket.ts
142 lines (121 loc) · 4.08 KB
/
socket.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/**
* Create a web socket connection with a Home Assistant instance.
*/
import {
ERR_INVALID_AUTH,
ERR_CANNOT_CONNECT,
ERR_HASS_HOST_REQUIRED,
} from "./errors.js";
import { Error } from "./types.js";
import type { ConnectionOptions } from "./connection.js";
import * as messages from "./messages.js";
import { atLeastHaVersion } from "./util.js";
const DEBUG = false;
export const MSG_TYPE_AUTH_REQUIRED = "auth_required";
export const MSG_TYPE_AUTH_INVALID = "auth_invalid";
export const MSG_TYPE_AUTH_OK = "auth_ok";
export interface HaWebSocket extends WebSocket {
haVersion: string;
}
export function createSocket(options: ConnectionOptions): Promise<HaWebSocket> {
if (!options.auth) {
throw ERR_HASS_HOST_REQUIRED;
}
const auth = options.auth;
// Start refreshing expired tokens even before the WS connection is open.
// We know that we will need auth anyway.
let authRefreshTask = auth.expired
? auth.refreshAccessToken().then(
() => {
authRefreshTask = undefined;
},
() => {
authRefreshTask = undefined;
},
)
: undefined;
// Convert from http:// -> ws://, https:// -> wss://
const url = auth.wsUrl;
if (DEBUG) {
console.log("[Auth phase] Initializing", url);
}
function connect(
triesLeft: number,
promResolve: (socket: HaWebSocket) => void,
promReject: (err: Error) => void,
) {
if (DEBUG) {
console.log("[Auth Phase] New connection", url);
}
const socket = new WebSocket(url) as HaWebSocket;
// If invalid auth, we will not try to reconnect.
let invalidAuth = false;
const closeMessage = () => {
// If we are in error handler make sure close handler doesn't also fire.
socket.removeEventListener("close", closeMessage);
if (invalidAuth) {
promReject(ERR_INVALID_AUTH);
return;
}
// Reject if we no longer have to retry
if (triesLeft === 0) {
// We never were connected and will not retry
promReject(ERR_CANNOT_CONNECT);
return;
}
const newTries = triesLeft === -1 ? -1 : triesLeft - 1;
// Try again in a second
setTimeout(() => connect(newTries, promResolve, promReject), 1000);
};
// Auth is mandatory, so we can send the auth message right away.
const handleOpen = async (event: MessageEventInit) => {
try {
if (auth.expired) {
await (authRefreshTask ? authRefreshTask : auth.refreshAccessToken());
}
socket.send(JSON.stringify(messages.auth(auth.accessToken)));
} catch (err) {
// Refresh token failed
invalidAuth = err === ERR_INVALID_AUTH;
socket.close();
}
};
const handleMessage = async (event: MessageEvent) => {
const message = JSON.parse(event.data);
if (DEBUG) {
console.log("[Auth phase] Received", message);
}
switch (message.type) {
case MSG_TYPE_AUTH_INVALID:
invalidAuth = true;
socket.close();
break;
case MSG_TYPE_AUTH_OK:
socket.removeEventListener("open", handleOpen);
socket.removeEventListener("message", handleMessage);
socket.removeEventListener("close", closeMessage);
socket.removeEventListener("error", closeMessage);
socket.haVersion = message.ha_version;
if (atLeastHaVersion(socket.haVersion, 2022, 9)) {
socket.send(JSON.stringify(messages.supportedFeatures()));
}
promResolve(socket);
break;
default:
if (DEBUG) {
// We already send response to this message when socket opens
if (message.type !== MSG_TYPE_AUTH_REQUIRED) {
console.warn("[Auth phase] Unhandled message", message);
}
}
}
};
socket.addEventListener("open", handleOpen);
socket.addEventListener("message", handleMessage);
socket.addEventListener("close", closeMessage);
socket.addEventListener("error", closeMessage);
}
return new Promise((resolve, reject) =>
connect(options.setupRetry, resolve, reject),
);
}