Skip to content

Commit 0005ed0

Browse files
committed
fix: new netatmo auth
1 parent 3b319c5 commit 0005ed0

File tree

4 files changed

+108
-32
lines changed

4 files changed

+108
-32
lines changed

README.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,19 @@ $ netatmo-weather-server --help
4444
### Parameters
4545

4646
You can pass the following params by environment variables :
47-
* **NWS_VERBOSE** Run with verbose mode
48-
* **NWS_PORT** Http server port
49-
* **NWS_LATITUDE** Latitude to search nearby
50-
* **NWS_LONGITUDE** Longitude to search nearby
51-
* **NWS_DISTANCE** Distance to search nearby (in KM)
52-
47+
* **NWS_VERBOSE** Run with verbose mode
48+
* **NWS_PORT** Http server port
49+
* **NWS_LATITUDE** Latitude to search nearby
50+
* **NWS_LONGITUDE** Longitude to search nearby
51+
* **NWS_DISTANCE** Distance to search nearby (in KM)
52+
* **NWS_NETATMO_TOKEN** Netatmo access token
53+
* **NWS_NETATMO_REFRESH_TOKEN** Netatmo refresh token (used to automatically refresh expired access token)
54+
* **NWS_NETATMO_CLIENT_ID** Netatmo client ID (from your [Netatmo Dev app](https://dev.netatmo.com/apps/))
55+
* **NWS_NETATMO_CLIENT_SECRET** Netatmo client secret (from your [Netatmo Dev app](https://dev.netatmo.com/apps/))
56+
57+
58+
For Netatmo variables, you need to create a [Netatmo Dev app](https://dev.netatmo.com/apps/).
59+
Then, generate a token by using Token Generator.
5360

5461
### How to use
5562
A REST Api is available to get weathers data.

src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,30 @@ const argv = yargs(process.argv)
3636
type: 'number',
3737
requiresArg: true,
3838
description: 'Distance to search nearby (in KM)'
39+
},
40+
netatmoToken: {
41+
alias: 't',
42+
type: 'string',
43+
requiresArg: true,
44+
description: 'Netatmo token'
45+
},
46+
netatmoRefreshToken: {
47+
alias: 'rt',
48+
type: 'string',
49+
requiresArg: true,
50+
description: 'Netatmo refresh token'
51+
},
52+
netatmoClientId: {
53+
alias: 'cid',
54+
type: 'string',
55+
requiresArg: true,
56+
description: 'Netatmo client ID'
57+
},
58+
netatmoClientSecret: {
59+
alias: 'cs',
60+
type: 'string',
61+
requiresArg: true,
62+
description: 'Netatmo client secret'
3963
}
4064
})
4165
.argv;

src/model/config.model.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ export class Config {
44
public latitude!: number;
55
public longitude!: number;
66
public distance!: number
7+
public netatmoToken!: string;
8+
public netatmoRefreshToken!: string;
9+
public netatmoClientId!: string;
10+
public netatmoClientSecret!: string;
711
}

src/service/netatmo-service.ts

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Axios } from 'axios-observable';
2-
import { Observable, of } from 'rxjs';
2+
import { AxiosError } from 'axios';
3+
import { Observable, of, throwError } from 'rxjs';
34
import { catchError, map, mergeMap, switchMap, tap, toArray } from 'rxjs/operators';
45
import { BoundsCoords } from '../model/bounds-coords.model';
56
import { Config } from '../model/config.model';
@@ -17,12 +18,14 @@ import { GeoService } from './geo-service';
1718
export class NetatmoService {
1819
private readonly config: Config;
1920
private readonly boundCoords: BoundsCoords;
20-
private token!: string;
21-
private refreshToken!: string;
21+
private accessToken: string;
22+
private refreshToken: string;
2223

2324
constructor(config: Config) {
2425
this.config = config;
2526
this.boundCoords = new GeoService().getBoundsCoords(this.config.latitude, this.config.longitude, this.config.distance);
27+
this.accessToken = this.config.netatmoToken;
28+
this.refreshToken = this.config.netatmoRefreshToken;
2629
}
2730

2831
/**
@@ -125,44 +128,69 @@ export class NetatmoService {
125128
}
126129

127130
/**
128-
* Find public access token in weather map home page
131+
* Check if we have a valid token
129132
* @returns
130133
*/
131134
private auth(): Observable<null> {
132-
if (this.token) {
135+
if (this.accessToken) {
133136
return of(null);
134137
}
135138

136-
return Axios.get(`https://weathermap.netatmo.com`)
137-
.pipe(
138-
tap((resp) => {
139-
if (this.config.verbose) {
140-
console.log(`Netatmo auth response`, resp);
141-
}
142-
}),
143-
map((resp) => {
144-
const matches = /accessToken: "?(.*)"/gm.exec(resp.data);
145-
if (matches && matches.length > 1) {
146-
this.token = matches[1].trim();
147-
}
148-
return null;
149-
})
150-
);
139+
// No token available, try to refresh
140+
return this.doRefreshToken();
141+
}
142+
143+
/**
144+
* Refresh the access token using the refresh token
145+
* @see https://dev.netatmo.com/apidocumentation/oauth
146+
* @returns
147+
*/
148+
private doRefreshToken(): Observable<null> {
149+
if (!this.refreshToken || !this.config.netatmoClientId || !this.config.netatmoClientSecret) {
150+
return throwError(() => new Error('Missing refresh token or client credentials for token refresh'));
151+
}
152+
153+
if (this.config.verbose) {
154+
console.log('Refreshing Netatmo access token...');
155+
}
156+
157+
const params = new URLSearchParams();
158+
params.append('grant_type', 'refresh_token');
159+
params.append('refresh_token', this.refreshToken);
160+
params.append('client_id', this.config.netatmoClientId);
161+
params.append('client_secret', this.config.netatmoClientSecret);
162+
163+
return Axios.post('https://api.netatmo.com/oauth2/token', params.toString(), {
164+
headers: {
165+
'Content-Type': 'application/x-www-form-urlencoded'
166+
}
167+
}).pipe(
168+
tap((resp) => {
169+
if (this.config.verbose) {
170+
console.log('Token refresh successful');
171+
}
172+
this.accessToken = resp.data.access_token;
173+
this.refreshToken = resp.data.refresh_token;
174+
}),
175+
map(() => null),
176+
catchError((err) => {
177+
console.error('Failed to refresh token', err.response?.data || err.message);
178+
return throwError(() => new Error('Failed to refresh Netatmo token'));
179+
})
180+
);
151181
}
152182

153183
/**
154184
* Load datas from Netatmo
185+
* @param isRetry - indicates if this is a retry after token refresh
155186
*/
156-
private getData(): Observable<WeatherStation[]> {
187+
private getData(isRetry = false): Observable<WeatherStation[]> {
157188
if (this.config.verbose) {
158189
console.log('Bounding coordinates to search nearby', this.boundCoords);
159190
}
160191

161192
return Axios.get(`https://app.netatmo.net/api/getpublicmeasures`, {
162-
headers: {
163-
"Authorization": `Bearer ${this.token}`,
164-
},
165-
data: {
193+
params: {
166194
'lat_ne': this.boundCoords.ne.lat,
167195
'lon_ne': this.boundCoords.ne.lng,
168196
'lat_sw': this.boundCoords.sw.lat,
@@ -172,6 +200,7 @@ export class NetatmoService {
172200
'zoom': 10,
173201
'date_end': 'last',
174202
'quality': 7,
203+
'access_token': this.accessToken,
175204
}
176205
})
177206
.pipe(
@@ -180,7 +209,19 @@ export class NetatmoService {
180209
console.log(`Weather API response`, resp.data);
181210
}
182211
}),
183-
map((resp) => resp.data.body)
212+
map((resp) => resp.data.body),
213+
catchError((err: AxiosError) => {
214+
// If 403 and not already a retry, refresh token and retry once
215+
if (err.response?.status === 403 && !isRetry) {
216+
if (this.config.verbose) {
217+
console.log('Received 403, attempting to refresh token and retry...');
218+
}
219+
return this.doRefreshToken().pipe(
220+
switchMap(() => this.getData(true))
221+
);
222+
}
223+
return throwError(() => err);
224+
})
184225
);
185226
}
186227

0 commit comments

Comments
 (0)