11import WebSocket from 'ws' ;
2- import { WsOutgoingMessage } from './types' ;
3- import { WS_URL } from './const' ;
2+ import { HelloWsOutgoingMessage , WsIncomingMessage , WsOutgoingMessage } from './types' ;
3+ import { MessageType , WS_URL } from './const' ;
44import { EventEmitter } from 'events' ;
55import { Logger } from 'homebridge' ;
66
7- const OPEN_TIMEOUT = 60 * 1000 ; // 1 minute
7+ const TIMEOUT = 60 * 1000 ; // 1 minute
88
99export class WsAPIClient extends EventEmitter {
1010
@@ -14,11 +14,41 @@ export class WsAPIClient extends EventEmitter {
1414
1515 constructor (
1616 private readonly log : Logger ,
17+ private readonly authorization : string ,
1718 ) {
1819 super ( ) ;
1920 }
2021
21- private open ( ) : Promise < WebSocket > {
22+ public async send < M extends WsOutgoingMessage > (
23+ message : Omit < M , 'message_id' > ,
24+ ) : Promise < M > {
25+
26+ const ws = await this . _getWebSocket ( ) ;
27+ return this . _send < M > ( message , ws ) ;
28+ }
29+
30+ private _getWebSocket ( ) : Promise < WebSocket > {
31+ return new Promise ( ( res , rej ) => {
32+ if (
33+ ! this . ws ||
34+ this . ws . readyState === WebSocket . CLOSING ||
35+ this . ws . readyState === WebSocket . CLOSED
36+ ) {
37+
38+ this . log . warn ( 'WS connection is not initialized or closed. Attempting to reopen' ) ;
39+
40+ this .
41+ _open ( )
42+ . then ( this . _handshake )
43+ . then ( ws => this . ws = ws )
44+ . catch ( err => rej ( err ) ) ;
45+ } else {
46+ return res ( this . ws ) ;
47+ }
48+ } ) ;
49+ }
50+
51+ private _open ( ) : Promise < WebSocket > {
2252 return new Promise ( ( res , rej ) => {
2353 this . log . debug ( 'Opening WS connection to ->' , WS_URL ) ;
2454 const ws = new WebSocket ( WS_URL ) ;
@@ -38,8 +68,8 @@ export class WsAPIClient extends EventEmitter {
3868 } ) ;
3969
4070 const openTimeout = setTimeout ( ( ) => {
41- rej ( new Error ( `WS connection was not ready in ${ OPEN_TIMEOUT } ms` ) ) ;
42- } , OPEN_TIMEOUT ) ;
71+ rej ( new Error ( `WS connection was not ready in ${ TIMEOUT } ms` ) ) ;
72+ } , TIMEOUT ) ;
4373
4474 ws . on ( 'open' , ( ) => {
4575 clearTimeout ( openTimeout ) ;
@@ -49,40 +79,29 @@ export class WsAPIClient extends EventEmitter {
4979 } ) ;
5080 }
5181
52- public async send < M extends WsOutgoingMessage > (
82+ private async _handshake ( ws : WebSocket ) : Promise < WebSocket > {
83+ return await this . _send < HelloWsOutgoingMessage > ( {
84+ type : MessageType . Hello ,
85+ version : '2.4.0' ,
86+ os : 'ios' ,
87+ source : 'climate' ,
88+ compatibility : 3 ,
89+ token : this . authorization ,
90+ } , ws ) . then ( ( ) => ws ) ;
91+ }
92+
93+ private async _send < M extends WsOutgoingMessage > (
5394 message : Omit < M , 'message_id' > ,
95+ ws : WebSocket ,
5496 ) : Promise < M > {
5597
56- const wsPromise : Promise < WebSocket > = new Promise ( ( res , rej ) => {
57- if (
58- ! this . ws ||
59- this . ws . readyState === WebSocket . CLOSING ||
60- this . ws . readyState === WebSocket . CLOSED
61- ) {
62-
63- this . log . warn ( 'WS connection is not initialized or closed. Attempting to reopen' ) ;
64-
65- this . open ( )
66- . then ( ( ws ) => {
67- this . ws = ws ;
68- res ( ws ) ;
69- } )
70- . catch ( err => rej ( err ) ) ;
71- } else {
72- return res ( this . ws ) ;
73- }
74- } ) ;
75-
76- const ws = await wsPromise ;
77-
78- const messageId = ++ this . lastMessageId ;
79- const fullMessage = {
80- ...message ,
81- message_id : messageId ,
82- } as M ;
83- this . log . debug ( 'Sending WS message -> ' , fullMessage ) ;
84-
85- return new Promise ( ( res , rej ) => {
98+ const sentMessage = await new Promise ( ( res , rej ) => {
99+ const messageId = ++ this . lastMessageId ;
100+ const fullMessage = {
101+ ...message ,
102+ message_id : messageId ,
103+ } as M ;
104+ this . log . debug ( 'Sending WS message -> ' , fullMessage ) ;
86105 ws . send (
87106 JSON . stringify ( fullMessage ) ,
88107 ( err ) => {
@@ -96,5 +115,39 @@ export class WsAPIClient extends EventEmitter {
96115 } ,
97116 ) ;
98117 } ) ;
118+
119+ return await this . _waitForResponse ( sentMessage as M ) ;
120+ }
121+
122+ private _waitForResponse < M extends WsOutgoingMessage > (
123+ outgoingMessage : M ,
124+ ) : Promise < M > {
125+ return new Promise ( ( res , rej ) => {
126+
127+ this . log . debug ( 'Waiting for message response -> ' , outgoingMessage ) ;
128+
129+ const timeout = setTimeout ( ( ) => {
130+ rej ( new Error ( `Didn't receive response in ${ TIMEOUT } !` ) ) ;
131+ } , TIMEOUT ) ;
132+
133+ const onMessage = ( incomingMessage : WsIncomingMessage ) => {
134+ if (
135+ incomingMessage . type === 'response' &&
136+ incomingMessage . message_id === outgoingMessage . message_id
137+ ) {
138+ this . log . debug ( 'Received response for message -> ' , outgoingMessage , incomingMessage ) ;
139+ this . off ( 'message' , onMessage ) ;
140+ clearTimeout ( timeout ) ;
141+ if ( incomingMessage . status === 200 ) {
142+ res ( outgoingMessage ) ;
143+ } else {
144+ rej ( incomingMessage ) ;
145+ }
146+ }
147+
148+ } ;
149+
150+ this . on ( 'message' , onMessage ) ;
151+ } ) ;
99152 }
100153}
0 commit comments