├── README.md ├── netatmo.js ├── package.json └── test.js /README.md: -------------------------------------------------------------------------------- 1 | node-netatmo 2 | ============ 3 | 4 | This is an NPM module to interface with the Netatmo Personal Weather Station API. (http://dev.netatmo.com) 5 | 6 | This module would not be possible without the work of Github user [mrose17](https://github.com/mrose17/). 7 | 8 | Install 9 | ------- 10 | 11 | npm install node-netatmo 12 | 13 | API 14 | --- 15 | 16 | Please consult the [netatmo](http://netatmo.com) private api [documentation](http://dev.netatmo.com/doc/). 17 | 18 | ### Load module 19 | 20 | var Netatmo = require('node-netatmo') 21 | , netatmo = new Netatmo.Netatmo() 22 | ; 23 | 24 | ### Login to cloud 25 | 26 | netatmo.on('error', function(err) { 27 | ... 28 | }).setConfig( '$CLIENT_ID' , '$CLIENT_SECRET' , '$USERNAME' , '$PASSWORD').getToken(function(err) { 29 | if (err) return console.log('getToken: ' + err.message); 30 | 31 | // good to go! 32 | }) 33 | 34 | ### Get user information 35 | 36 | netatmo.getUser(function(err, results) { 37 | if (err) return console.log('getUser: ' + err.message); 38 | if (results.status !== 'ok') { console.log('getUser not ok'); return console.log(results); } 39 | 40 | // inspect results.body 41 | } 42 | 43 | ### Get device information 44 | 45 | netatmo.getDevices(function(err, results) { 46 | if (err) return console.log('getDevices: ' + err.message); 47 | if (results.status !== 'ok') { console.log('getDevices not ok'); return console.log(results); } 48 | 49 | // inspect results.body 50 | } 51 | 52 | #### Get sensor measurements 53 | 54 | It would be best if you read the [documentation](http://dev.netatmo.com/doc/) to understand the supported values for params: 55 | 56 | var params = { device_id : '....' // if not present, uses the first device_id from the previous call to getDevices 57 | , module_id : null // optional 58 | , scale : '1day' // or: max, 30min, 3hours, 1week, 1month 59 | 60 | // at least one of these must be present 61 | , type : [ 'Temperature', 'CO2', 'Humidity', 'Pressure', 'Noise' ] 62 | 63 | // these are all optional 64 | , date_begin : utc-timestamp 65 | , date_end : utc-timestamp 66 | , limit : 1024 67 | , optimize : false 68 | }; 69 | 70 | netatmo.getMeasurement(params, function(err, results) { 71 | if (err) return console.log('getMeasurement: ' + err.message); 72 | if (results.status !== 'ok') { console.log('getMeasurement not ok'); return console.log(results); } 73 | 74 | // inspect results.body 75 | } 76 | 77 | Finally 78 | ------- 79 | 80 | Enjoy! 81 | -------------------------------------------------------------------------------- /netatmo.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013 Corey Menscher 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 6 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions 10 | of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 13 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | 18 | FYI: Netatmo is a registered trademark of Netatmo (SAS) 19 | 20 | */ 21 | 22 | //HTTP 23 | var https = require('https'); 24 | var querystring = require('querystring'); 25 | 26 | var events = require('events'); 27 | var util = require('util'); 28 | 29 | var DEFAULT_CONFIG = { 30 | auth_request: { 31 | grant_type: "password", 32 | client_id: '$CLIENT_ID', 33 | client_secret: '$CLIENT_SECRET', 34 | username: '$USERNAME', 35 | password: '$PASSWORD', 36 | scope: 'read_station' 37 | }, 38 | 39 | auth_refresh: { 40 | grant_type: "refresh_token", 41 | refresh_token: "", 42 | client_id: '$CLIENT_ID', 43 | client_secret: '$CLIENT_SECRET' 44 | }, 45 | 46 | auth_options: { 47 | hostname: "api.netatmo.net", 48 | headers: {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Content-Length": 0}, 49 | port: 443, 50 | path: "/oauth2/token", 51 | method: "POST" 52 | }, 53 | 54 | credentials: { 55 | "access_token": "", 56 | "expires_in": 0, 57 | "expire_in": 0, 58 | "scope": null, 59 | "refresh_token": "" 60 | }, 61 | 62 | api_options: { 63 | hostname: "api.netatmo.net", 64 | headers: {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"} 65 | }, 66 | 67 | tokenCheckInterval: 60 * 1000 68 | }; 69 | 70 | var DEFAULT_LOGGER = { error : function(msg, props) { console.log(msg); if (!!props) console.trace(props.exception); } 71 | , warning : function(msg, props) { console.log(msg); if (!!props) console.log(props); } 72 | , notice : function(msg, props) { console.log(msg); if (!!props) console.log(props); } 73 | , info : function(msg, props) { console.log(msg); if (!!props) console.log(props); } 74 | , debug : function(msg, props) { console.log(msg); if (!!props) console.log(props); } 75 | }; 76 | 77 | var Netatmo = function(info) { 78 | var op, param; 79 | 80 | this.devices = null; 81 | this.currentValue = 0; // holds the last value of whatever we decide to examine (i.e. Temp, Pressure, Humidity, Sound...) 82 | this.lastValue = 0; // holds the last value of whatever we decide to examine (i.e. Temp, Pressure, Humidity, Sound...) 83 | this.config = {}; 84 | for (op in DEFAULT_CONFIG) { 85 | if (!DEFAULT_CONFIG.hasOwnProperty(op)) continue; 86 | 87 | if ((typeof DEFAULT_CONFIG[op]) !== 'object') { 88 | this.config[op] = DEFAULT_CONFIG[op]; 89 | continue; 90 | } 91 | 92 | this.config[op] = {}; 93 | for (param in DEFAULT_CONFIG[op]) { 94 | if (DEFAULT_CONFIG[op].hasOwnProperty(param)) this.config[op][param] = DEFAULT_CONFIG[op][param]; 95 | } 96 | } 97 | this.logger = DEFAULT_LOGGER; 98 | 99 | if (!!info) this.initialize(info); 100 | }; 101 | util.inherits(Netatmo, events.EventEmitter); 102 | 103 | Netatmo.prototype.setConfig = function(clientID, clientSecret, username, password) { 104 | this.initialize({ $CLIENT_ID : clientID 105 | , $CLIENT_SECRET : clientSecret 106 | , $USERNAME : username 107 | , $PASSWORD : password 108 | }); 109 | return this; 110 | }; 111 | 112 | Netatmo.prototype.initialize = function(info) { 113 | var op, param, value; 114 | 115 | for (op in this.config) { 116 | if (!this.config.hasOwnProperty(op)) continue; 117 | 118 | for (param in this.config[op]) { 119 | if (!this.config[op].hasOwnProperty(param)) continue; 120 | 121 | value = this.config[op][param]; 122 | if (!!info[value]) this.config[op][param] = info[value]; 123 | } 124 | } 125 | }; 126 | 127 | Netatmo.prototype.getToken = function(callback) { 128 | var _this = this; 129 | 130 | _this.logger.info("Getting authorization token..."); 131 | 132 | if(arguments.length === 0) { 133 | callback = function(err) { _this.logger.error('getToken', { exception: err }); }; 134 | } 135 | 136 | var auth_data = querystring.stringify(_this.config.auth_request); 137 | 138 | //SET THE HEADERS!!! 139 | _this.config.auth_options.headers = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Content-Length": auth_data.length}; 140 | 141 | _this.tokenUpdated = new Date().getTime(); 142 | https.request(_this.config.auth_options, function(res) { 143 | var content = ''; 144 | 145 | res.setEncoding('utf8'); 146 | res.on('data', function(chunk) { 147 | content += chunk.toString(); 148 | }).on('end', function() { 149 | var cred_obj; 150 | 151 | try { 152 | cred_obj = JSON.parse(content); 153 | if ((!cred_obj.access_token) || (!cred_obj.refresh_token)) { 154 | return callback(new Error('getToken failed')); 155 | } 156 | } catch(ex) { return callback(ex); } 157 | 158 | _this.config.credentials.access_token = cred_obj.access_token; 159 | _this.config.credentials.expires_in = cred_obj.expires_in; 160 | if (_this.config.credentials.expires_in < 120) _this.config.credentials.expires_in = 180; 161 | _this.config.credentials.expire_in = cred_obj.expire_in; 162 | _this.config.credentials.scope = cred_obj.scope; 163 | _this.config.credentials.refresh_token = cred_obj.refresh_token; 164 | _this.config.tokenCheckInterval = (_this.config.credentials.expires_in - 120) * 1000; 165 | _this.logger.info("Successfully retrieved token, next check in " + (_this.config.tokenCheckInterval/1000) + ' secs'); 166 | 167 | setTimeout(function () { _this.refreshToken(_this); }, _this.config.tokenCheckInterval); 168 | callback(null); 169 | }); 170 | }).on('error', function(err) { 171 | callback(err); 172 | }).end(auth_data); 173 | 174 | return _this; 175 | }; 176 | 177 | Netatmo.prototype.refreshToken = function(_this) { 178 | _this.logger.info("Refreshing authorization token..."); 179 | 180 | // Set the refresh token based on current credentials 181 | _this.config.auth_refresh.refresh_token = _this.config.credentials.refresh_token; 182 | 183 | var auth_data = querystring.stringify(_this.config.auth_refresh); 184 | 185 | //SET THE HEADERS!!! 186 | _this.config.auth_options.headers = {"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", "Content-Length": auth_data.length}; 187 | 188 | _this.tokenUpdated = new Date().getTime(); 189 | https.request(_this.config.auth_options, function(res) { 190 | var content = ''; 191 | 192 | res.setEncoding('utf8'); 193 | res.on('data', function(chunk) { 194 | content += chunk.toString(); 195 | }).on('end', function() { 196 | var cred_obj; 197 | 198 | try { 199 | cred_obj = JSON.parse(content); 200 | if ((!cred_obj.access_token) || (!cred_obj.refresh_token)) { 201 | return _this.emit('error', new Error('refreshToken failed')); 202 | } 203 | } catch(ex) { return _this.emit('error', ex); } 204 | 205 | _this.config.credentials.access_token = cred_obj.access_token; 206 | _this.config.credentials.expires_in = cred_obj.expires_in; 207 | if (_this.config.credentials.expires_in < 120) _this.config.credentials.expires_in = 180; 208 | _this.config.credentials.expire_in = cred_obj.expire_in; 209 | _this.config.credentials.scope = cred_obj.scope; 210 | _this.config.credentials.refresh_token = cred_obj.refresh_token; 211 | _this.config.tokenCheckInterval = (_this.config.credentials.expires_in - 120) * 1000; 212 | _this.logger.info("Successfully refreshed access token, next check in " + (_this.config.tokenCheckInterval/1000) + ' secs'); 213 | 214 | setTimeout(function () { _this.refreshToken(_this); }, _this.config.tokenCheckInterval); 215 | }); 216 | }).on('error', function(err) { 217 | _this.emit('error', err); 218 | }).end(auth_data); 219 | }; 220 | 221 | Netatmo.prototype.invoke = function(path, callback) { 222 | var _this = this; 223 | 224 | if (!callback) { 225 | callback = function(err, msg) { 226 | if (err) _this.logger.error('invoke', { exception: err }); else _this.logger.info(msg); 227 | }; 228 | } 229 | 230 | _this.config.api_options.path = path; 231 | 232 | https.request(_this.config.api_options, function(response) { 233 | var content = ''; 234 | 235 | response.setEncoding('utf8'); 236 | response.on('data', function(chunk) { 237 | content += chunk.toString(); 238 | }).on('end', function() { 239 | var results; 240 | 241 | try { 242 | results = JSON.parse(content); 243 | if ((!_this.devices) && (!!results.body) && (util.isArray(results.body.devices))) _this.devices = results.body.devices; 244 | 245 | callback(null, results); 246 | } catch(ex) { callback(ex); } 247 | }); 248 | }).on('error', function(err) { 249 | callback(err); 250 | }).end(); 251 | }; 252 | 253 | Netatmo.prototype.getUser = function(callback) { 254 | this.invoke("/api/getuser?access_token=" + this.config.credentials.access_token, callback); 255 | }; 256 | 257 | Netatmo.prototype.getDevices = function(callback) { 258 | this.invoke("/api/devicelist?access_token=" + this.config.credentials.access_token, callback); 259 | }; 260 | 261 | Netatmo.prototype.getMeasurement = function(params, callback) { 262 | var path; 263 | 264 | params = params || {}; 265 | if (typeof params === 'function') { 266 | callback = params; 267 | params = {}; 268 | } 269 | if (!params.device_id) params.device_id = this.devices[0]._id; 270 | // module_id: optional 271 | if (!params.scale) params.scale = '1day'; 272 | if (!params.type) params.type = [ 'Temperature', 'CO2', 'Humidity', 'Pressure', 'Noise' ]; 273 | if (util.isArray(params.type)) params.type = params.type.join(','); 274 | 275 | if (!params.device_id) return callback(new Error('getDevices must be called first')); 276 | 277 | path = "/api/getmeasure?access_token=" + this.config.credentials.access_token + '&' + querystring.stringify(params); 278 | 279 | this.invoke(path, callback); 280 | }; 281 | 282 | exports.Netatmo = Netatmo; 283 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-netatmo", 3 | "version": "0.0.6", 4 | "description": "This is an NPM module to interface with the Netatmo Personal Weather Station API. (http://dev.netatmo.com)", 5 | "main": "netatmo.js", 6 | "scripts": { 7 | "test": "test.js" 8 | }, 9 | "dependencies": { 10 | "querystring": "0.2.0", 11 | "events": "0.5.0", 12 | "util": "0.4.9" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git://github.com/cmenscher/node-netatmo.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/cmenscher/node-netatmo/issues" 20 | }, 21 | "keywords": [ 22 | "netatmo" 23 | ], 24 | "author": { 25 | "name": "Corey Menscher" 26 | }, 27 | "license": "BSD", 28 | "readmeFilename": "README.md", 29 | "_id": "node-netatmo@0.0.3", 30 | "dist": { 31 | "shasum": "01a1e91225df91a909a83900a4e37066eb8fda05" 32 | }, 33 | "_from": "node-netatmo@0.0.3" 34 | } 35 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var Netatmo = require('./netatmo') 2 | , netatmo = new Netatmo.Netatmo(); 3 | 4 | var zero = 0; 5 | 6 | netatmo.on('error', function(err) { 7 | console.log('netatmo error'); console.error (err); 8 | }).setConfig( '$CLIENT_ID' 9 | , '$CLIENT_SECRET' 10 | , '$USERNAME' 11 | , '$PASSWORD' 12 | ).getToken(function(err) { 13 | if (err) return console.log('getToken: ' + err.message); 14 | 15 | netatmo.getUser(function(err, results) { 16 | var user; 17 | 18 | if (err) return console.log('getUser: ' + err.message); 19 | if (results.status !== 'ok') { console.log('getUser not ok'); return console.log(results); } 20 | user = results.body; 21 | 22 | netatmo.getDevices(function(err, results) { 23 | var deviceID, devices, i, j, modules; 24 | 25 | if (err) return console.log('getDevices: ' + err.message); 26 | if (results.status !== 'ok') { console.log('getDevices not ok'); return console.log(results); } 27 | devices = {}; 28 | modules = {}; 29 | for (i = 0; i < results.body.devices.length; i++) devices[results.body.devices[i]._id] = results.body.devices[i]; 30 | for (i = 0; i < results.body.modules.length; i++) modules[results.body.modules[i]._id] = results.body.modules[i]; 31 | 32 | for (i = 0; i < user.devices.length; i++) { 33 | deviceID = user.devices[i]; 34 | getInfo(netatmo, 0, deviceID); 35 | for (j = 0; j < devices[deviceID].modules.length; j++) getInfo(netatmo, 0, deviceID, devices[deviceID].modules[j]); 36 | } 37 | for (i = 0; i < user.friend_devices.length; i++) { 38 | deviceID = user.friend_devices[i]; 39 | getInfo(netatmo, 1, deviceID); 40 | for (j = 0; j < devices[deviceID].modules.length; j++) getInfo(netatmo, 1, deviceID, devices[deviceID].modules[j]); 41 | } 42 | }); 43 | }); 44 | }); 45 | 46 | var getInfo = function(self, friendP, deviceID, moduleID) { 47 | var params = { device_id : deviceID 48 | , module_id : moduleID 49 | , scale : 'max' 50 | , date_end : 'last' 51 | }; 52 | 53 | zero++; 54 | self.getMeasurement(params, function(err, results) { 55 | if (err) console.log('getMeasurements: ' + err.message); 56 | else if (results.status !== 'ok') { console.log('getMeasurements not ok'); return console.log(results); } 57 | else { 58 | console.log('deviceID: ' + deviceID + ' moduleID: ' + (moduleID || '[]') + ' friendP=' + friendP); 59 | console.log(JSON.stringify(results.body)); 60 | } 61 | 62 | if (--zero === 0) console.log('finished.'); 63 | }); 64 | }; 65 | --------------------------------------------------------------------------------