From 8d592c5de61c2f46943de43b6e80f20a3a97c92a Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Mon, 31 Oct 2022 17:34:27 +0100 Subject: [PATCH 1/6] changed config-description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 55e78d7..a3671e6 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ You can also configure this plugin via [ConfigUI-X's settings](https://github.co - **co2_alert_threshold (optional):** Sets the co2-level [ppm] at which the sensors switch to alert-state - **ttl: (optional)** Seconds between two Netatmo API polls. Lower is not neccessarily better! The weatherstation itself collects one value per 5minutes, so going below 300s makes no sense. Default value is *540* (=9min) - **auth:** Credentials for the Netatmo API (see below) -- **log_info_msg: (optional)** controls the logging of "fetching data" messages. Default value is _true_ +- **log_info_msg: (optional)** Outputs log messages with loglevel set to info. Default value is _true_ - **module_suffix: (optional)** If this is set, the Netatmo's devicename will not be prepended to the modulename. Instead this config-value will be appended - with a space - to the module name ### Control Accessories by device ID From 66306225a9eb9f4ea06dd32922787c73de14b2c2 Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Sat, 17 Dec 2022 11:21:36 +0100 Subject: [PATCH 2/6] readded password grant as an alternative --- README.md | 28 +++++++++++- index.js | 22 ++++++--- lib/netatmo-api.js | 109 ++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 141 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a3671e6..fe3469b 100644 --- a/README.md +++ b/README.md @@ -34,10 +34,18 @@ You can also configure this plugin via [ConfigUI-X's settings](https://github.co "client_id": "XXXXX Create at https://dev.netatmo.com/", "client_secret": "XXXXX Create at https://dev.netatmo.com/", "refresh_token": "a valid refresh token for the given client_id", + "grant_type": "refresh_token" + + ... or if you use password-grant ... + + "client_id": "XXXXX Create at https://dev.netatmo.com/", + "client_secret": "XXXXX Create at https://dev.netatmo.com/", + "username": "your netatmo account's mail-address", + "password": "your netatmo account's password", + "grant_type": "password" } } ], - ``` - **weatherstation** Enables support for Netatmo's WeatherStation. Default value is *true* @@ -80,15 +88,31 @@ If the whitelist contains at least one entry, all other ids will be excluded. -### Retrieve _client_id_, _client_secret_ and _refresh_token_ +## Netatmo API authentication +There are two methods to authenticate against the Netatmo API, but first 4 steps are always the same: 1. Register at http://dev.netatmo.com as a developer 2. After successful registration create your own app by using the menu entry "CREATE AN APP" 3. On the following page, enter a name for your app. Any name can be chosen. All other fields of the form (like _callback_url_, etc.) can be left blank. 4. After successfully submitting the form the overview page of your app should show _client_id_ and _client_secret_. + +### "refresh_token" grant +This one is **recommended** by Netatmo because it is more secure since you do not have to store your username and password in homebridge's config file. +The downside is, that it is a little bit less stable, especially when homebridge is not running constantly. +This is because the plugin always gets a short-lived token to fetch data for some time. When the token expires, the plugin has to fetch a new one from the API. + 5. Do an initial auth with the newly created app via the "Token generator" on your app's page https://dev.netatmo.com/apps/ to get a _refresh_token_ 6. Add the _client_id_, the _client_secret_ and the _refresh_token_ to the config's _auth_-section 7. The plugin will use the _refresh_token_ from the config to retrieve and refresh _auth_tokens_. It will also store newly retrieved tokens in a file (_netatmo-token.js_) in your homebridge config directory. If you delete the _netatmo-token.js_ file, you may have to regenerate a new _refresh_token_ like in step 5) if your initial _refresh_token_ (from the _config.json_) already has expired + +### "password" grant +This one is my preferred method, because in a single-user scenario and a most likely "at home and self-hosted"-setup it is totally fine for me. Netatmo deprecated this method but it is usable in cases where the user (here: homebridge) and the account (where the weatherstation is linked to) are the same. +Since this is the normal use-case for this homebridge-plugin I use this as long it is possible. + +5. Add the _client_id_, the _client_secret_, the _username_ (your account email) and the _password_ (your account password) to the config's _auth_-section + +### Retrieve _client_id_, _client_secret_ and _refresh_token_ + ## Siri Voice Commands diff --git a/index.js b/index.js index 7e18dda..fe3556f 100755 --- a/index.js +++ b/index.js @@ -30,13 +30,25 @@ class EveatmoPlatform { this.log.warn('CAUTION! USING FAKE NETATMO API: ' + config.mockapi); this.api = require("./lib/netatmo-api-mock")(config.mockapi); } else { - if (config.auth.username || config.auth.password) { - throw new Error("username / password auth is not supported anymore! Please see the readme and use a 'refresh_token' instead."); - } else if (!config.auth.refresh_token) { - throw new Error("Authenticate 'refresh_token' not set."); + this.config.auth.grant_type = typeof config.auth.grant_type !== 'undefined' ? config.auth.grant_type : 'refresh_token'; + + if (this.config.auth.grant_type == 'refresh_token') { + if (config.auth.username || config.auth.password) { + throw new Error("'username' and 'password' are not used in grant_type 'refresh_token'"); + } else if (!config.auth.refresh_token) { + throw new Error("'refresh_token' not set"); + } + this.log.info("Authenticating using 'refresh_token' grant"); + } else if (this.config.auth.grant_type == 'password') { + if (!config.auth.username || !config.auth.password) { + throw new Error("'username' and 'password' are mandatory when using grant_type 'password'"); + } + this.log.info("Authenticating using 'password' grant"); + } else { + throw new Error("Unsupported grant_type. Please use 'password' or 'refresh_token'"); } - this.api = new netatmo(config.auth, homebridge); + this.api = new netatmo(this.config.auth, homebridge); } this.api.on("error", function(error) { this.log.error('ERROR - Netatmo: ' + error); diff --git a/lib/netatmo-api.js b/lib/netatmo-api.js index 77348e9..7a7a0a9 100644 --- a/lib/netatmo-api.js +++ b/lib/netatmo-api.js @@ -21,20 +21,24 @@ var filename; var netatmo = function (args, homebridge) { EventEmitter.call(this); - client_id = args.client_id; - client_secret = args.client_secret; - filename = homebridge.user.storagePath() + '/netatmo-token.json'; + if (args.grant_type === 'refresh_token') { + client_id = args.client_id; + client_secret = args.client_secret; + filename = homebridge.user.storagePath() + '/netatmo-token.json'; + + if (fs.existsSync(filename)) { + let rawData = fs.readFileSync(filename); + let tokenData = JSON.parse(rawData); + access_token = tokenData.access_token; + refresh_token = tokenData.refresh_token; + } else { + refresh_token = args.refresh_token; + } - if (fs.existsSync(filename)) { - let rawData = fs.readFileSync(filename); - let tokenData = JSON.parse(rawData); - access_token = tokenData.access_token; - refresh_token = tokenData.refresh_token; + this.authenticate_refresh(); } else { - refresh_token = args.refresh_token; + this.authenticate(args, null); } - - this.authenticate_refresh(); }; util.inherits(netatmo, EventEmitter); @@ -68,6 +72,89 @@ netatmo.prototype.handleRequestError = function (err, response, body, message, c return error; }; +/** + * https://dev.netatmo.com/dev/resources/technical/guides/authentication + * @param args + * @param callback + * @returns {netatmo} + */ +netatmo.prototype.authenticate = function (args, callback) { + if (!args) { + this.emit("error", new Error("Authenticate 'args' not set.")); + return this; + } + + if (args.access_token) { + access_token = args.access_token; + return this; + } + + if (!args.client_id) { + this.emit("error", new Error("Authenticate 'client_id' not set.")); + return this; + } + + if (!args.client_secret) { + this.emit("error", new Error("Authenticate 'client_secret' not set.")); + return this; + } + + if (!args.username) { + this.emit("error", new Error("Authenticate 'username' not set.")); + return this; + } + + if (!args.password) { + this.emit("error", new Error("Authenticate 'password' not set.")); + return this; + } + + username = args.username; + password = args.password; + client_id = args.client_id; + client_secret = args.client_secret; + scope = args.scope || 'read_station read_thermostat write_thermostat read_camera write_camera access_camera read_presence access_presence read_smokedetector read_homecoach'; + + var form = { + client_id: client_id, + client_secret: client_secret, + username: username, + password: password, + scope: scope, + grant_type: 'password', + }; + + var url = util.format('%s/oauth2/token', BASE_URL); + + request({ + url: url, + method: "POST", + form: form, + }, function (err, response, body) { + if (err || response.statusCode != 200) { + return this.handleRequestError(err, response, body, "Authenticate error", true); + } + + body = JSON.parse(body); + + access_token = body.access_token; + + if (body.expires_in) { + setTimeout(this.authenticate_refresh.bind(this), body.expires_in * 1000, body.refresh_token); + } + + this.emit('authenticated'); + + if (callback) { + return callback(); + } + + return this; + }.bind(this)); + + return this; +}; + /** * https://dev.netatmo.com/dev/resources/technical/guides/authentication/refreshingatoken * @param refresh_token From 118a55d750bcac00bf23a7fe0fc13d7469ebaf72 Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Sat, 17 Dec 2022 11:24:17 +0100 Subject: [PATCH 3/6] bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 27bfff1..b44900e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-eveatmo", - "version": "1.0.1", + "version": "1.1.0-beta1", "description": "Homebridge plugin which adds a Netatmo weatherstation as HomeKit device and tries to act like Elgato Eve Room/Weather", "license": "ISC", "keywords": [ From c907dd710af9e5b589071423061bbe9dc22c50b4 Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Sat, 17 Dec 2022 18:19:19 +0100 Subject: [PATCH 4/6] try to refresh token earlier than necessary + retry on refresh-fail + updated config.schema.json --- config.schema.json | 39 ++++++++++++++++++++++++++++++++++++--- lib/netatmo-api.js | 6 ++++-- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/config.schema.json b/config.schema.json index 3dc428e..1eda9b2 100755 --- a/config.schema.json +++ b/config.schema.json @@ -55,7 +55,7 @@ "type": "boolean", "default": true, "description": "Outputs log messages with loglevel set to info" - }, + }, "auth": { "title": "Netatmo credentials", "type": "object", @@ -72,11 +72,44 @@ "required": true, "description": "Create this at https://dev.netatmo.com/" }, + "grant_type": { + "title": "grant_type", + "type": "string", + "required": true, + "default": "refresh_token", + "oneOf": [ + { + "title": "refresh_token", + "enum": [ + "refresh_token" + ] + }, + { + "title": "password", + "enum": [ + "password" + ] + } + ], + "description": "use either 'password' or 'refresh_token'. Please see https://github.com/skrollme/homebridge-eveatmo/blob/master/README.md for more information" + }, "refresh_token": { "title": "Refresh Token", "type": "string", - "required": true, - "description": "A valid Netatmo refreshToken, see https://dev.netatmo.com/apidocumentation/oauth for more information" + "required": false, + "description": "Necessary, if you use grant_type='refresh_token'. A valid Netatmo refreshToken. You can generate this on the page where you got the 'client_id' and 'client_secret' from" + }, + "username": { + "title": "Username", + "type": "string", + "required": false, + "description": "Necessary, if you use grant_type='password'. The email-address of your Netatmo account. Must be the same account as the developer app which the 'client_id' and 'client_secret' belong to" + }, + "password": { + "title": "Password", + "type": "string", + "required": false, + "description": "Necessary, if you use grant_type='password'. The password of your Netatmo account. Must be the same account as the developer app which the 'client_id' and 'client_secret' belong to" } } } diff --git a/lib/netatmo-api.js b/lib/netatmo-api.js index 7a7a0a9..6679a38 100644 --- a/lib/netatmo-api.js +++ b/lib/netatmo-api.js @@ -176,7 +176,9 @@ netatmo.prototype.authenticate_refresh = function () { form: form, }, function (err, response, body) { if (err || response.statusCode != 200) { - return this.handleRequestError(err, response, body, "Authenticate refresh error"); + this.handleRequestError(err, response, body, "Authenticate refresh error"); + setTimeout(this.authenticate_refresh.bind(this), 180 * 1000); // retry in 3min + return this } body = JSON.parse(body); @@ -195,7 +197,7 @@ netatmo.prototype.authenticate_refresh = function () { })); if (body.expires_in) { - setTimeout(this.authenticate_refresh.bind(this), body.expires_in * 1000); + setTimeout(this.authenticate_refresh.bind(this), (body.expires_in - 300) * 1000); // try refreshing the tokens 5min early } return this; From ec04af81fbbebb07518c32f3ec2389dd870091d9 Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Sat, 17 Dec 2022 18:34:20 +0100 Subject: [PATCH 5/6] bumped version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b44900e..a4fda35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-eveatmo", - "version": "1.1.0-beta1", + "version": "1.1.0-beta2", "description": "Homebridge plugin which adds a Netatmo weatherstation as HomeKit device and tries to act like Elgato Eve Room/Weather", "license": "ISC", "keywords": [ From 3d7bb52ce1b946a550e2acb0e2d2068bf692329b Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Mon, 6 Mar 2023 21:15:56 +0100 Subject: [PATCH 6/6] prepared version 1.1 --- HISTORY.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 03a15c0..97ed726 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ ## version history +### 1.1.0 +- allows new auth method (via refreshtoken) and old one (via password) + - added config-schema to configure this via UI +- refreshtokens are fetched 5min earlier to do not run into auth problems + - Added retry on failed refreshtoken-fetch + ### 1.0.1 - Thanks to a PR (https://github.com/skrollme/homebridge-eveatmo/pull/65) from @smhex: - Logging of "fetching weatherdata" is not configurable diff --git a/package.json b/package.json index a4fda35..5c22991 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-eveatmo", - "version": "1.1.0-beta2", + "version": "1.1.0", "description": "Homebridge plugin which adds a Netatmo weatherstation as HomeKit device and tries to act like Elgato Eve Room/Weather", "license": "ISC", "keywords": [