From 66306225a9eb9f4ea06dd32922787c73de14b2c2 Mon Sep 17 00:00:00 2001 From: Sebastian K Date: Sat, 17 Dec 2022 11:21:36 +0100 Subject: [PATCH] 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