├── .gitignore ├── package.json ├── README.md └── verisure-api.js /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .idea/* 3 | 4 | node_modules/* 5 | 6 | config.js 7 | 8 | npm-debug.log 9 | 10 | test.js 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "verisure-api", 3 | "version": "0.9.0", 4 | "description": "get status of verisure alarm", 5 | "main": "verisure-api.js", 6 | "scripts": { 7 | "start": "verisure-api.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/suhajdab/verisure-api.git" 12 | }, 13 | "author": "Balázs Suhajda", 14 | "license": "WTFPL", 15 | "dependencies": { 16 | "es6-promise": "^2.0.0", 17 | "object-assign": "^1.0.0", 18 | "request": "^2.47.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | verisure-api 2 | ============ 3 | creating a basic module to poll verisure api to be able to trigger home automation based on alarm status changes 4 | 5 | 6 | Usage: 7 | 8 | var config = { 9 | username: 'yourverisure@email.com', 10 | password: 'yourverisurepassword' 11 | }; 12 | 13 | 14 | var verisureApi = require('./verisure-api').setup( config ); 15 | 16 | 17 | // alarm state changes 18 | verisureApi.on( 'alarmChange', log ); 19 | 20 | // climate measurement changes 21 | verisureApi.on( 'climateChange', log ); 22 | 23 | function log ( data ) { 24 | console.log( data ); 25 | } -------------------------------------------------------------------------------- /verisure-api.js: -------------------------------------------------------------------------------- 1 | /* 2 | TODO: sanity check, require user/pass 3 | */ 4 | 5 | 6 | 7 | var request = require('request'); 8 | // ES6 object cloning 9 | var objectAssign = require('object-assign'); 10 | // ES6 Promises 11 | require('es6-promise').polyfill(); 12 | 13 | var formData, firstAlarmPoll, firstClimatePoll, 14 | authenticated = false, 15 | config = {}, 16 | alarmStatus = {}, 17 | climateData = {}, 18 | listeners = { 19 | climateChange: [], 20 | alarmChange: [] 21 | }; 22 | 23 | var defaults = { 24 | username: '', 25 | password: '', 26 | domain: 'https://mypages.verisure.com', 27 | auth_path: '/j_spring_security_check?locale=sv_SE', 28 | alarmstatus_path: '/remotecontrol?_=', 29 | climatedata_path: '/overview/climatedevice?_=', 30 | alarmFields: [ 'status', 'date' ], 31 | climateFields: [ 'location', 'humidity', 'temperature', 'timestamp' ] 32 | }; 33 | 34 | // request timeouts 35 | var alarmFetchTimeout = 30 * 1000, // 0.5 min 36 | climateFetchTimeout = 30 * 60 * 1000, // 30 min 37 | errorTimeout = 10 * 60 * 1000; // 10 min 38 | 39 | // enabling cookies 40 | request = request.defaults({ jar: true }); 41 | 42 | /* UTILITY */ 43 | 44 | /** 45 | * Utility function to filter out unwanted object properties by key 46 | * @param {Object} obj - Object to be filtered 47 | * @param {Array} keysArr - Array of keys to keep 48 | * @returns {Object} - New, filtered object containing keys keysArr 49 | */ 50 | function filterByKeys ( obj, keysArr ) { 51 | 'use strict'; 52 | var filtered = {}; 53 | 54 | function filter ( key ) { 55 | if ( keysArr.indexOf( key ) !== -1 ) { 56 | filtered[ key ] = obj[ key ]; 57 | } 58 | } 59 | Object.keys( obj ).forEach( filter ); 60 | 61 | return filtered; 62 | } 63 | 64 | /** 65 | * Function to dispatch change event for a service 66 | * @param {String} service - service name 67 | * @param {Object} data - data to dispatch to listeners 68 | */ 69 | function dispatch( service, data ) { 70 | "use strict"; 71 | 72 | listeners[ service ].forEach( function ( listener ) { 73 | listener( data ); 74 | }); 75 | } 76 | 77 | /** 78 | * Request promise 79 | * @param {Object} options - options for request (url, method, .. etc) 80 | * @returns {Promise} - promise for the request 81 | */ 82 | function requestPromise ( options ) { 83 | 'use strict'; 84 | return new Promise( function ( resolve, reject ) { 85 | request( options, function requestCallback( error, response, body ) { 86 | // handle response errors 87 | if ( options.json && response.headers['content-type'] !== 'application/json;charset=UTF-8' ) { 88 | error = { state: 'error', message: 'Expected JSON, but got html' }; 89 | } else if ( body.state === 'error' ) { 90 | error = body; 91 | authenticated = false; 92 | } 93 | 94 | // resolve / reject 95 | if ( error ) { 96 | reject( error ); 97 | } else { 98 | authenticated = true; 99 | resolve( body ); 100 | } 101 | }); 102 | }); 103 | } 104 | 105 | /* PRIVATE */ 106 | 107 | /** 108 | * 109 | * @returns {Promise} 110 | */ 111 | function authenticate () { 112 | 'use strict'; 113 | var auth_url = config.domain + config.auth_path, 114 | requestParams = { 115 | url: auth_url, 116 | form: formData, 117 | method: 'POST' 118 | }; 119 | 120 | return authenticated ? Promise.resolve( true ) : requestPromise( requestParams ); 121 | } 122 | 123 | /** 124 | * 125 | * @returns {Promise} 126 | */ 127 | function fetchAlarmStatus () { 128 | 'use strict'; 129 | 130 | var alarmstatus_url = config.domain + config.alarmstatus_path + Date.now(); 131 | return requestPromise({ url: alarmstatus_url, json: true }); 132 | } 133 | 134 | /** 135 | * 136 | * @returns {Promise} 137 | */ 138 | function fetchClimateData () { 139 | 'use strict'; 140 | 141 | var climatedata_url = config.domain + config.climatedata_path + Date.now(); 142 | return requestPromise({ url: climatedata_url, json: true }); 143 | } 144 | 145 | /** 146 | * 147 | * @param data 148 | * @returns {*} 149 | */ 150 | function parseAlarmData ( data ) { 151 | "use strict"; 152 | 153 | data = filterByKeys( data[ 0 ], config.alarmFields ); 154 | 155 | setTimeout( pollAlarmStatus, alarmFetchTimeout ); 156 | 157 | // check for alarm data changes 158 | if ( JSON.stringify( data ) !== JSON.stringify( alarmStatus ) ) { 159 | alarmStatus = data; 160 | dispatch( 'alarmChange', data ); 161 | } 162 | return Promise.resolve( data ); 163 | } 164 | 165 | /** 166 | * 167 | * @param data 168 | * @returns {*} 169 | */ 170 | function parseClimateData ( data ) { 171 | 'use strict'; 172 | data = data.map( function ( dataSet ) { 173 | return filterByKeys( dataSet, config.climateFields ); 174 | }); 175 | 176 | setTimeout( pollClimateData, climateFetchTimeout ); 177 | 178 | // check for climate data changes 179 | if ( JSON.stringify( data ) !== JSON.stringify( climateData ) ) { 180 | climateData = data; 181 | dispatch( 'climateChange', data ); 182 | } 183 | return Promise.resolve( data ); 184 | } 185 | 186 | function pollAlarmStatus () { 187 | 'use strict'; 188 | return fetchAlarmStatus().then( parseAlarmData ); 189 | } 190 | 191 | function pollClimateData () { 192 | 'use strict'; 193 | return fetchClimateData().then( parseClimateData ); 194 | } 195 | 196 | function gotAlarmStatus () { 197 | "use strict"; 198 | return Object.keys( alarmStatus ).length !== 0; 199 | } 200 | 201 | function gotClimateData () { 202 | "use strict"; 203 | return Object.keys( climateData ).length !== 0; 204 | } 205 | 206 | function getAlarmStatus() { 207 | "use strict"; 208 | 209 | if ( gotAlarmStatus() ) { 210 | return Promise.resolve( alarmStatus ); 211 | } else { 212 | return firstAlarmPoll; 213 | } 214 | } 215 | 216 | function getClimateData() { 217 | "use strict"; 218 | 219 | if ( gotClimateData() ) { 220 | return Promise.resolve( climateData ); 221 | } else { 222 | return firstClimatePoll; 223 | } 224 | } 225 | 226 | /** 227 | * Error handler. When either request causes an error, invalidate authentication and wait to avoid getting blocked 228 | * @param err 229 | */ 230 | function onError ( err ) { 231 | 'use strict'; 232 | 233 | setTimeout( engage, errorTimeout ); 234 | config.onError( err ); 235 | } 236 | 237 | function engage() { 238 | "use strict"; 239 | 240 | firstAlarmPoll = authenticate() 241 | .then( pollAlarmStatus ); 242 | firstClimatePoll = firstAlarmPoll 243 | .then( pollClimateData ) 244 | .catch( onError ); 245 | } 246 | 247 | 248 | /* PUBLIC */ 249 | var publicApi = { 250 | 251 | /** 252 | * Function adds a change event listener to one of the services 253 | * @param {String} service - name of service to watch for changes 254 | * @param {Function} callback - function to execute on change 255 | * @returns {Error} - if something goes wrong 256 | */ 257 | on: function( service, callback ) { 258 | "use strict"; 259 | 260 | if ( !listeners[ service ] ) { 261 | return new Error( 'No such service! Subscribe to alarmChange or climateChange!' ); 262 | } 263 | 264 | if ( typeof callback !== 'function' ) { 265 | return new Error( 'Please provide a function as callback' ); 266 | } 267 | 268 | // we are already subscribed, but no reason to Error 269 | if ( listeners[ service ].indexOf( callback ) === -1 ) { 270 | listeners[ service ].push( callback ); 271 | } 272 | 273 | if ( service === 'alarmChange' && gotAlarmStatus() ) { 274 | callback( alarmStatus ); 275 | } else if ( service === 'climateChange' && gotClimateData() ) { 276 | callback( climateData ); 277 | } 278 | }, 279 | 280 | /** 281 | * Function removes a change event listener for a specified service 282 | * @param {String} service - name of service to stop listening to 283 | * @param {Function=} callback - callback to remove or if not specified, all listeners to service will be removed 284 | * @returns {Error} - if something goes wrong 285 | */ 286 | off: function( service, callback ) { 287 | "use strict"; 288 | 289 | if ( !listeners[ service ] ) { 290 | return new Error( 'No such service! Unsubscribe from alarmChange or climateChange!' ); 291 | } 292 | 293 | if ( typeof callback === 'function' ) { 294 | var i = listeners[ service ].indexOf( callback ); 295 | if ( i !== -1 ) { 296 | listeners[ service ].splice( i, 1 ); 297 | } 298 | } else if ( typeof callback === 'undefined' ) { 299 | listeners[ service ] = []; 300 | } 301 | }, 302 | 303 | /** 304 | * Function a promise for the recent value of the specified service 305 | * @param service 306 | * @returns {*} 307 | */ 308 | get: function( service ) { 309 | "use strict"; 310 | 311 | if ( service === 'alarmStatus' ) { 312 | return getAlarmStatus(); 313 | } 314 | if ( service === 'climateData' ) { 315 | return getClimateData(); 316 | } 317 | 318 | return Promise.reject( 'No such service! Use alarmStatus or climateData' ); 319 | } 320 | }; 321 | 322 | /** 323 | * Verisure api requires username & pass for setup, will then return public api 324 | * @param {Object} options - config options containing at least username & password 325 | * @returns {{on: on, off: off, get: get}} 326 | */ 327 | function setup ( options ) { 328 | "use strict"; 329 | 330 | config = objectAssign( defaults, options ); 331 | 332 | if ( !config.username && !config.password ) { 333 | throw "Missing required username and password for verisure api"; 334 | } 335 | 336 | // form data for login 337 | formData = { 338 | j_username: config.username, 339 | j_password: config.password 340 | }; 341 | 342 | engage(); 343 | 344 | return publicApi; 345 | } 346 | 347 | module.exports.setup = setup; --------------------------------------------------------------------------------