@@ -4,6 +4,8 @@ import type {
4
4
AirComfortDetailed ,
5
5
BoilerSystemInformation ,
6
6
Country ,
7
+ DeviceToken ,
8
+ DeviceVerification ,
7
9
EnergyIQConsumptionDetails ,
8
10
EnergyIQMeterReadings ,
9
11
EnergyIQOverview ,
@@ -28,106 +30,139 @@ import type {
28
30
RunningTimesSummaryOnly ,
29
31
State ,
30
32
StatePresence ,
33
+ Token ,
31
34
User ,
32
35
Weather ,
33
36
} from "./types" ;
34
37
35
38
import { Agent } from "https" ;
36
39
import axios , { Method } from "axios" ;
37
- import { AccessToken , ResourceOwnerPassword } from "simple-oauth2" ;
38
40
import { TadoError } from "./types" ;
39
41
40
- const EXPIRATION_WINDOW_IN_SECONDS = 300 ;
41
-
42
- const tado_auth_url = "https://auth.tado.com" ;
43
42
const tado_url = "https://my.tado.com" ;
44
- const tado_config = {
45
- client : {
46
- id : "tado-web-app" ,
47
- secret : "wZaRN7rpjn3FoNyF5IFuxg9uMzYJcvOoQ8QWiIqS3hfk6gLhVlG57j5YNoZL2Rtc" ,
48
- } ,
49
- auth : {
50
- tokenHost : tado_auth_url ,
51
- } ,
52
- } ;
53
-
54
- const client = new ResourceOwnerPassword ( tado_config ) ;
43
+ const client_id = "1bb50063-6b0c-4d11-bd99-387f4a91cc46" ;
44
+ const scope = "offline_access" ;
45
+ const grant_type = "urn:ietf:params:oauth:grant-type:device_code" ;
55
46
56
47
export class BaseTado {
57
48
#httpsAgent: Agent ;
58
- #accessToken?: AccessToken | undefined ;
59
- #username?: string ;
60
- #password?: string ;
49
+ #token?: Token | undefined ;
61
50
62
- constructor ( username ?: string , password ?: string ) {
63
- this . #username = username ;
64
- this . #password = password ;
51
+ constructor ( refreshToken ?: string ) {
65
52
this . #httpsAgent = new Agent ( { keepAlive : true } ) ;
66
- }
67
53
68
- async #login( ) : Promise < void > {
69
- if ( ! this . #username || ! this . #password) {
70
- throw new Error ( "Please login before using Tado!" ) ;
54
+ if ( refreshToken ) {
55
+ this . #token = {
56
+ access_token : "" ,
57
+ refresh_token : refreshToken ,
58
+ expiry : new Date ( 0 ) ,
59
+ } ;
71
60
}
61
+ }
72
62
73
- const tokenParams = {
74
- username : this . #username,
75
- password : this . #password,
76
- scope : "home.user" ,
77
- } ;
63
+ #parseDeviceToken( deviceToken : DeviceToken ) : Token {
64
+ const expiry = new Date ( ) ;
65
+ expiry . setSeconds ( expiry . getSeconds ( ) + deviceToken . expires_in - 5 ) ; // Minus 5 seconds grace
78
66
79
- this . #accessToken = await client . getToken ( tokenParams ) ;
67
+ return {
68
+ access_token : deviceToken . access_token ,
69
+ refresh_token : deviceToken . refresh_token ,
70
+ expiry,
71
+ } ;
80
72
}
81
73
82
- /**
83
- * Refreshes the access token if it has expired or is about to expire.
84
- *
85
- * The method checks if an access token is available. If not, it attempts to login to obtain one.
86
- * If the token is within the expiration window, it tries to refresh the token.
87
- * In case of a failure during the refresh, it sets the token to null and attempts to login again.
88
- *
89
- * @returns A promise that resolves when the token has been refreshed or re-obtained.
90
- * @throws {@link TadoError } if no access token is available after attempting to login.
91
- */
92
- async #refreshToken( ) : Promise < void > {
93
- if ( ! this . #accessToken) {
94
- await this . #login( ) ;
95
- }
74
+ async #checkDevice( device_code : string , timeout : number ) : Promise < Token > {
75
+ return new Promise ( ( resolve , reject ) => {
76
+ setTimeout ( ( ) => {
77
+ axios < DeviceToken > ( {
78
+ url : "https://login.tado.com/oauth2/token" ,
79
+ method : "POST" ,
80
+ params : {
81
+ client_id,
82
+ device_code : device_code ,
83
+ grant_type,
84
+ } ,
85
+ } )
86
+ . then ( ( resp ) => {
87
+ const deviceToken = resp . data ;
88
+ resolve ( this . #parseDeviceToken( deviceToken ) ) ;
89
+ } )
90
+ . catch ( reject ) ;
91
+ } , timeout ) ;
92
+ } ) ;
93
+ }
96
94
97
- if ( ! this . #accessToken) {
98
- throw new TadoError ( `No access token available, even after login in.` ) ;
99
- }
95
+ async #deviceAuth( ) : Promise < Token > {
96
+ const verify = await axios < DeviceVerification > ( {
97
+ url : "https://login.tado.com/oauth2/device_authorize" ,
98
+ method : "POST" ,
99
+ params : {
100
+ client_id,
101
+ scope,
102
+ } ,
103
+ } ) ;
100
104
101
- // If the start of the window has passed, refresh the token
102
- const shouldRefresh = this . #accessToken. expired ( EXPIRATION_WINDOW_IN_SECONDS ) ;
105
+ console . log ( "------------------------------------------------" ) ;
106
+ console . log ( "Device authentication required." ) ;
107
+ console . log ( "Please visit the following website in a browser." ) ;
108
+ console . log ( "" ) ;
109
+ console . log ( ` ${ verify . data . verification_uri_complete } ` ) ;
110
+ console . log ( "" ) ;
111
+ console . log (
112
+ `Checks will occur every ${ verify . data . interval } s up to a maximum of ${ verify . data . expires_in } s` ,
113
+ ) ;
114
+ console . log ( "------------------------------------------------" ) ;
103
115
104
- if ( shouldRefresh ) {
116
+ // Wait for user to click buttons
117
+ for ( let i = 0 ; i < verify . data . expires_in ; i += verify . data . interval ) {
105
118
try {
106
- this . #accessToken = await this . #accessToken. refresh ( ) ;
107
- } catch ( _error ) {
108
- this . #accessToken = undefined ;
109
- await this . #login( ) ;
119
+ console . log ( "calling" ) ;
120
+ const token = await this . #checkDevice(
121
+ verify . data . device_code ,
122
+ verify . data . interval * 1000 ,
123
+ ) ;
124
+ this . #token = token ;
125
+ return token ;
126
+ } catch {
127
+ // Keep trying, we'll throw later
110
128
}
111
129
}
112
- }
113
130
114
- get accessToken ( ) : AccessToken | undefined {
115
- return this . #accessToken;
131
+ throw new Error ( "Timeout waiting for user input" ) ;
116
132
}
117
133
118
- /**
119
- * Authenticates a user using the provided public client credentials, username and password.
120
- * For more information see
121
- * [https://support.tado.com/en/articles/8565472-how-do-i-update-my-rest-api-authentication-method-to-oauth-2](https://support.tado.com/en/articles/8565472-how-do-i-update-my-rest-api-authentication-method-to-oauth-2).
122
- *
123
- * @param username - The username of the user attempting to login.
124
- * @param password - The password of the user attempting to login.
125
- * @returns A promise that resolves when the login process is complete.
126
- */
127
- async login ( username : string , password : string ) : Promise < void > {
128
- this . #username = username ;
129
- this . #password = password ;
130
- await this . #login( ) ;
134
+ async getToken ( ) : Promise < Token > {
135
+ if ( ! this . #token) {
136
+ const token = await this . #deviceAuth( ) ;
137
+ this . #token = token ;
138
+ return token ;
139
+ }
140
+
141
+ const now = new Date ( ) ;
142
+
143
+ if ( this . #token. expiry < now ) {
144
+ try {
145
+ const resp = await axios < DeviceToken > ( {
146
+ url : "https://login.tado.com/oauth2/token" ,
147
+ method : "POST" ,
148
+ params : {
149
+ client_id,
150
+ grant_type : "refresh_token" ,
151
+ refresh_token : this . #token. refresh_token ,
152
+ } ,
153
+ } ) ;
154
+
155
+ const token = this . #parseDeviceToken( resp . data ) ;
156
+ this . #token = token ;
157
+ return token ;
158
+ } catch {
159
+ const token = await this . #deviceAuth( ) ;
160
+ this . #token = token ;
161
+ return token ;
162
+ }
163
+ } else {
164
+ return this . #token;
165
+ }
131
166
}
132
167
133
168
/**
@@ -141,7 +176,7 @@ export class BaseTado {
141
176
* @returns A promise that resolves to the response data.
142
177
*/
143
178
async apiCall < R , T = unknown > ( url : string , method : Method = "get" , data ?: T ) : Promise < R > {
144
- await this . #refreshToken ( ) ;
179
+ const token = await this . getToken ( ) ;
145
180
146
181
let callUrl = tado_url + url ;
147
182
if ( url . includes ( "https" ) ) {
@@ -152,16 +187,15 @@ export class BaseTado {
152
187
method : method ,
153
188
data : data ,
154
189
headers : {
155
- Authorization : " Bearer " + this . #accessToken ?. token . access_token ,
190
+ Authorization : ` Bearer ${ token . access_token } ` ,
156
191
} ,
157
192
httpsAgent : this . #httpsAgent,
158
193
} ;
159
194
if ( method !== "get" && method !== "GET" ) {
160
195
request . data = data ;
161
196
}
162
- const response = await axios ( request ) ;
163
-
164
- return response . data as R ;
197
+ const response = await axios < R > ( request ) ;
198
+ return response . data ;
165
199
}
166
200
167
201
/**
0 commit comments