Skip to content

Commit

Permalink
Merge branch 'refs/heads/develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
skrollme committed Mar 6, 2023
2 parents ff782ed + a41aa9e commit 20b550b
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 25 deletions.
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 27 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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*
Expand All @@ -46,7 +54,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
Expand Down Expand Up @@ -80,15 +88,31 @@ If the whitelist contains at least one entry, all other ids will be excluded.

</pre>

### 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

Expand Down
39 changes: 36 additions & 3 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"type": "boolean",
"default": true,
"description": "Outputs log messages with loglevel set to info"
},
},
"auth": {
"title": "Netatmo credentials",
"type": "object",
Expand All @@ -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"
}
}
}
Expand Down
22 changes: 17 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
115 changes: 102 additions & 13 deletions lib/netatmo-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -89,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);
Expand All @@ -108,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;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "homebridge-eveatmo",
"version": "1.0.1",
"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": [
Expand Down

0 comments on commit 20b550b

Please sign in to comment.