├── test.json ├── resources └── images │ ├── app_logo.png │ └── humidity-icon.png ├── src └── js │ ├── error-window.js │ ├── app.js │ ├── elements.js │ ├── ecobee-api.js │ ├── utils.js │ ├── oauth.js │ ├── menu.js │ └── main-window.js ├── appinfo.json └── README.md /test.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/images/app_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspace/hive/HEAD/resources/images/app_logo.png -------------------------------------------------------------------------------- /resources/images/humidity-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appspace/hive/HEAD/resources/images/humidity-icon.png -------------------------------------------------------------------------------- /src/js/error-window.js: -------------------------------------------------------------------------------- 1 | var UI = require('ui'); 2 | 3 | this.exports = { 4 | show: function(errorText) { 5 | var card = new UI.Card({ 6 | title: 'Error' 7 | }); 8 | card.body(errorText); 9 | card.show(); 10 | card.on('hide', function() { 11 | card.hide(); 12 | }); 13 | } 14 | }; -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | var Settings = require('settings'); 2 | var Oauth = require('oauth'); 3 | var MainWindow = require('main-window'); 4 | 5 | var defaultSettings = { 6 | ecobeeServerUrl: 'https://api.ecobee.com', 7 | ecobeeTokenApi: '/token', 8 | ecobeeApiEndpoint: '/1/thermostat', 9 | paired: false, 10 | refreshToken: null, 11 | oauthToken: null, 12 | oauthTokenExpires: null, 13 | authPin: null, 14 | authCode: null, 15 | authExpires: null, 16 | clientId: 'ABC123' 17 | }; 18 | 19 | var initialCheck = function() { 20 | console.log('Platform: '+JSON.stringify(Pebble.getActiveWatchInfo())); 21 | var data = Settings.data(); 22 | //if (true) { 23 | if (Object.keys(data).length===0) { 24 | Settings.data(defaultSettings); 25 | data = Settings.data(); 26 | } 27 | console.log('Settings: '+JSON.stringify(data)); 28 | 29 | var isPaired = Settings.data('paired'); 30 | if (isPaired) { 31 | Oauth.getAccessToken(false); //Pre-fetch token if expired 32 | MainWindow.show(); 33 | } else { 34 | var successCallback = function() { 35 | initialCheck(); 36 | }; 37 | var pinExpiration = Settings.data('authExpires'); 38 | console.log('Pin expiration: '+pinExpiration); 39 | if (!pinExpiration) { 40 | Oauth.getPin(successCallback); 41 | } else if (Date.now() > pinExpiration-5000) { 42 | Oauth.getPin(successCallback); 43 | } else { 44 | var pin = Settings.data('authPin'); 45 | var code = Settings.data('authCode'); 46 | Oauth.authorizePin(pin, code, successCallback); 47 | } 48 | } 49 | }; 50 | 51 | initialCheck(); 52 | -------------------------------------------------------------------------------- /appinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "appKeys": {}, 3 | "capabilities": [ 4 | "" 5 | ], 6 | "companyName": "Eugene Sukharev", 7 | "longName": "Pebble app for controlling ecobee thermostat", 8 | "projectType": "pebblejs", 9 | "resources": { 10 | "media": [ 11 | { 12 | "file": "images/menu_icon.png", 13 | "name": "IMAGE_MENU_ICON", 14 | "type": "bitmap" 15 | }, 16 | { 17 | "file": "images/logo_splash.png", 18 | "name": "IMAGE_LOGO_SPLASH", 19 | "type": "bitmap" 20 | }, 21 | { 22 | "file": "images/tile_splash.png", 23 | "name": "IMAGE_TILE_SPLASH", 24 | "type": "bitmap" 25 | }, 26 | { 27 | "file": "fonts/UbuntuMono-Regular.ttf", 28 | "name": "MONO_FONT_14", 29 | "type": "font" 30 | }, 31 | { 32 | "file": "images/humidity-icon.png", 33 | "name": "IMAGES_HUMIDITY_ICON_PNG", 34 | "type": "bitmap" 35 | }, 36 | { 37 | "file": "images/app_logo.png", 38 | "menuIcon": true, 39 | "name": "IMAGES_APP_LOGO_PNG", 40 | "type": "bitmap" 41 | } 42 | ] 43 | }, 44 | "sdkVersion": "3", 45 | "shortName": "Hive", 46 | "targetPlatforms": [ 47 | "aplite", 48 | "basalt", 49 | "chalk" 50 | ], 51 | "uuid": "4e475ece-7659-4ecf-9cc7-57964455b0ba", 52 | "versionCode": 1, 53 | "versionLabel": "1.5", 54 | "watchapp": { 55 | "hiddenApp": false, 56 | "watchface": false 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hive 2 | 3 | 4 | ---------- 5 | 6 | Hive is an application to control your [ecobee](www.ecobee.com) thermostat from [pebble](www.pebble.com) smartwatch! 7 | 8 | The application has been [released](https://apps.getpebble.com/en_US/application/56c3897cf3dab7ddce00001c) in February 2016. 9 | Application allows you to control temperature settings, see thermostat sensor values as well as change your thermostat between "Home" and "Away" modes. 10 | 11 | Supported smartwatches: 12 | 13 | - [Pebble Classic](https://www.pebble.com/buy-pebble-smartwatch) 14 | - [Pebble Steel](https://www.pebble.com/buy-pebble-steel-smartwatch) 15 | - [Pebble Time](https://www.pebble.com/buy-pebble-time-smartwatch) 16 | - [Pebble Time Steel](https://www.pebble.com/buy-pebble-time-steel-smartwatch) 17 | - [Pebble Time Round](https://www.pebble.com/buy-pebble-time-round-smartwatch) 18 | 19 | How to use the app: 20 | 21 | 1. Get the application from [Pebble App Market](https://apps.getpebble.com/en_US/application/56c3897cf3dab7ddce00001c) 22 | 2. At the first run, the application display a 4-character code to link your smartwatch to your ecobee account. Write down the code, go to [ecobee.com](ecobee.com) website. Log into your ecobee account and navigate to "My Apps" section. Click "Add Application" button and enter the code. Click "Validate" and then "Add App". 23 | 3. Get back to your smartwatches and click right middle button. Pebble will check if linking has been successful. 24 | 4. Main screen shows identical screen to your ecobee thermostat. 25 | 5. Click "Up" button to increase temperature. 26 | 6. Click "Down" button to lower temperature. 27 | 7. Click "Right middle" button to get to a menu. 28 | 29 | 30 | ---------- 31 | Support the development, [donate if you like the app!](https://www.paypal.me/appspace) 32 | -------------------------------------------------------------------------------- /src/js/elements.js: -------------------------------------------------------------------------------- 1 | var UI = require('ui'); 2 | var Vector2 = require('vector2'); 3 | var Feature = require('platform/feature'); 4 | 5 | this.exports = { 6 | holdTempHeat : function(heatHold, pos) { 7 | return new UI.Text({ 8 | //position: new Vector2(118, 72), 9 | position: pos, 10 | size: new Vector2(28, 20), 11 | color: Feature.color('#FF5500', 'white'), 12 | text: heatHold, 13 | font: 'gothic-18', 14 | textAlign: 'center' 15 | }); 16 | }, 17 | holdTempCool : function(coolHold, pos) { 18 | return new UI.Text({ 19 | //position: new Vector2(118, 72), 20 | position: pos, 21 | size: new Vector2(28, 20), 22 | color: Feature.color('#00AAFF', 'white'), 23 | text: coolHold, 24 | font: 'gothic-18', 25 | textAlign: 'center' 26 | }); 27 | }, 28 | bgHeatDots : function() { 29 | var dots = []; 30 | var heatColor = Feature.color('#FF5500', 'white'); 31 | 32 | for(var x = 0; x < 9; x++){ 33 | dots.push(new UI.Circle({ 34 | backgroundColor: 'white', 35 | position: new Vector2(132, (8.5*x) +2), 36 | radius: (x+1)/3.2 37 | })); 38 | } 39 | 40 | for(var i = 8; i >= 0; i--){ 41 | dots.push(new UI.Circle({ 42 | backgroundColor: heatColor, 43 | position: new Vector2(132, (165 - (i*8.5))), 44 | radius: (i+1)/3.2 45 | })); 46 | } 47 | 48 | return dots; 49 | }, 50 | bgCoolDots : function() { 51 | var dots = []; 52 | var coolColor = Feature.color('#00AAFF', 'white'); 53 | 54 | for(var x = 0; x < 9; x++){ 55 | dots.push(new UI.Circle({ 56 | backgroundColor: coolColor, 57 | position: new Vector2(132, (8.5*x) +2), 58 | radius: (x+1)/3.2 59 | })); 60 | } 61 | 62 | for(var i = 8; i >= 0; i--){ 63 | dots.push(new UI.Circle({ 64 | backgroundColor: 'white', 65 | position: new Vector2(132, (165 - (i*8.5))), 66 | radius: (i+1)/3.2 67 | })); 68 | } 69 | 70 | return dots; 71 | }, 72 | bgAutoDots : function(){ 73 | var dots = []; 74 | var coolColor = Feature.color('#00AAFF', 'white'); 75 | var heatColor = Feature.color('#FF5500', 'white'); 76 | 77 | for(var x = 0; x < 7; x++){ 78 | dots.push(new UI.Circle({ 79 | backgroundColor: coolColor, 80 | position: new Vector2(132, (8.5*x) +2), 81 | radius: (x+1)/2.9 82 | })); 83 | } 84 | 85 | for(var n = 0; n < 2; n++){ 86 | dots.push(new UI.Circle({ 87 | backgroundColor: 'white', 88 | position: new Vector2(132, 78 + (8.5*n)), 89 | radius: 2.8 90 | })); 91 | } 92 | 93 | for(var i = 6; i >= 0; i--){ 94 | dots.push(new UI.Circle({ 95 | backgroundColor: heatColor, 96 | position: new Vector2(132, (165 - (i*8.5))), 97 | radius: (i+1)/2.5 98 | })); 99 | } 100 | 101 | return dots; 102 | } 103 | }; -------------------------------------------------------------------------------- /src/js/ecobee-api.js: -------------------------------------------------------------------------------- 1 | var Settings = require('settings'); 2 | var Oauth = require('oauth'); 3 | var ajax = require('ajax'); 4 | 5 | var jsonRequest = { 6 | "selection": { 7 | "includeAlerts": "false", 8 | "selectionType": "registered", 9 | "selectionMatch": "", 10 | "includeEvents": "true", 11 | "includeSettings": "true", 12 | "includeRuntime": "true", 13 | "includeSensors": "true" 14 | } 15 | }; 16 | 17 | var cache; 18 | var cacheExpiration; 19 | 20 | this.exports = { 21 | loadThermostats: function(onSuccess, onError) { 22 | if (Date.now()>cacheExpiration) { 23 | console.log('Cache expired'); 24 | cache = null; 25 | } 26 | if (cache) { 27 | console.log('Data in cache: '+JSON.stringify(cache)); 28 | onSuccess(cache); 29 | return; 30 | } 31 | 32 | var token = Oauth.getAccessToken(false); 33 | var callUrl = Settings.data('ecobeeServerUrl')+ 34 | Settings.data('ecobeeApiEndpoint')+ 35 | '?json='+encodeURIComponent(JSON.stringify(jsonRequest)); 36 | console.log('GET '+callUrl+' with OAuth token: '+token); 37 | ajax( 38 | { 39 | url: callUrl, 40 | type: 'json', 41 | method: 'get', 42 | headers: { 43 | 'Content-Type': 'application/json;charset=UTF-8', 44 | 'Authorization': 'Bearer '+token 45 | } 46 | }, 47 | function(data) { 48 | console.log('Received data: '+JSON.stringify(data)); 49 | if (data.status.code!==0) { 50 | onError(data.status.message); 51 | } else if (data.thermostatList.length===0) { 52 | onError('No thermostats linked to account'); 53 | } else { 54 | cache = data.thermostatList; 55 | cacheExpiration = Date.now()+30*1000; 56 | onSuccess(cache); 57 | } 58 | }, 59 | function(error) { 60 | console.log('Error receiving ecobee data: '+JSON.stringify(error)); 61 | onError(error); 62 | } 63 | ); 64 | }, 65 | postThermostat: function(req, onSuccess, onError) { 66 | cache = null; //SO that it refreshes 67 | var token = Oauth.getAccessToken(false); 68 | var callUrl = Settings.data('ecobeeServerUrl')+Settings.data('ecobeeApiEndpoint')+'?format=json'; 69 | console.log('POST '+JSON.stringify(req)+' to '+callUrl+' with OAuth token: '+token); 70 | ajax( 71 | { 72 | url: callUrl, 73 | type: 'json', 74 | method: 'post', 75 | headers: { 76 | 'Content-Type': 'application/json;charset=UTF-8', 77 | 'Authorization': 'Bearer '+token 78 | }, 79 | data: req 80 | }, 81 | function(data) { 82 | console.log('Received data: '+JSON.stringify(data)); 83 | if (data.status.code!==0) { 84 | onError(data.status.message); 85 | } else { 86 | onSuccess(); 87 | } 88 | }, 89 | function(error) { 90 | console.log('Error receiving ecobee data: '+JSON.stringify(error)); 91 | onError(error); 92 | } 93 | ); 94 | } 95 | }; -------------------------------------------------------------------------------- /src/js/utils.js: -------------------------------------------------------------------------------- 1 | this.exports = { 2 | 3 | canonicalToFahrenheit: function(canonical) { 4 | return canonical/10; 5 | }, 6 | 7 | canonicalToCelsius: function(canonical) { 8 | var result = Math.round(2*(canonical/10-32)*5/9)/2; 9 | //console.log('Converted canonical '+canonical+' to '+result+'C'); 10 | return result; 11 | }, 12 | 13 | calculateHoldType: function(thermostatSettings){ 14 | switch(thermostatSettings.holdAction){ 15 | case 'nextPeriod': 16 | return 'nextTransition'; 17 | case 'useEndTime4hour': 18 | return 'holdHours'; 19 | case 'useEndTime2hour': 20 | return 'holdHours'; 21 | default: 22 | return 'indefinite'; 23 | } 24 | }, 25 | 26 | calculateHoldHours: function(thermostatSettings){ 27 | switch(thermostatSettings.holdAction){ 28 | case 'useEndTime4hour': 29 | return 4; 30 | case 'useEndTime2hour': 31 | return 2; 32 | default: 33 | return null; 34 | } 35 | }, 36 | 37 | createTemperatureHoldEvent: function(thermostat, newHeat, newCold) { 38 | return { 39 | "selection": { 40 | "selectionType": "thermostats", 41 | "selectionMatch": thermostat.identifier 42 | }, 43 | "functions": [ 44 | { 45 | "type": "setHold", 46 | "params": { 47 | "holdType": this.calculateHoldType(thermostat.settings), 48 | "holdHours": this.calculateHoldHours(thermostat.settings), 49 | "coolHoldTemp": newCold, 50 | "heatHoldTemp": newHeat 51 | } 52 | } 53 | ] 54 | }; 55 | }, 56 | 57 | createAwayHoldEvent: function(thermostat) { 58 | return { 59 | "selection": { 60 | "selectionType": "thermostats", 61 | "selectionMatch": thermostat.identifier 62 | }, 63 | "functions": [{ 64 | "type":"setHold", 65 | "params":{ 66 | "holdType": "indefinite", 67 | "holdClimateRef":"away" 68 | } 69 | }] 70 | }; 71 | }, 72 | 73 | createHomeHoldEvent: function(thermostat) { 74 | return { 75 | "selection": { 76 | "selectionType": "thermostats", 77 | "selectionMatch": thermostat.identifier 78 | }, 79 | "functions": [{ 80 | "type":"setHold", 81 | "params":{ 82 | "holdType": "indefinite", 83 | "holdClimateRef":"home" 84 | } 85 | }] 86 | }; 87 | }, 88 | 89 | createSleepHoldEvent: function(thermostat) { 90 | return { 91 | "selection": { 92 | "selectionType": "thermostats", 93 | "selectionMatch": thermostat.identifier 94 | }, 95 | "functions": [{ 96 | "type":"setHold", 97 | "params":{ 98 | "holdType": "indefinite", 99 | "holdClimateRef":"sleep" 100 | } 101 | }] 102 | }; 103 | }, 104 | 105 | createResumeProgramEvent: function(thermostat) { 106 | return { 107 | "selection": { 108 | "selectionType": "thermostats", 109 | "selectionMatch": thermostat.identifier 110 | }, 111 | "functions": [ 112 | { 113 | "type": "resumeProgram", 114 | "params": { 115 | 'resumeAll': true 116 | } 117 | } 118 | ] 119 | }; 120 | }, 121 | 122 | createChangeModeRequest: function(thermostat, newMode){ 123 | return { 124 | "selection": { 125 | "selectionType": "thermostats", 126 | "selectionMatch": thermostat.identifier 127 | }, 128 | "thermostat": { 129 | "settings": { 130 | "hvacMode": newMode 131 | } 132 | } 133 | }; 134 | }, 135 | 136 | hasHold: function(thermostat) { 137 | if (thermostat.events && thermostat.events.length > 0) 138 | { 139 | var runningEvent = thermostat.events[0]; 140 | console.log('Running event: '+JSON.stringify(runningEvent)); 141 | if ((runningEvent.type === 'hold' || runningEvent.type === 'autoAway' || runningEvent.type === 'autoHome' ) && 142 | runningEvent.running) { 143 | return true; 144 | } 145 | } 146 | return false; 147 | }, 148 | 149 | hasHeatMode: function(thermostat) { 150 | return thermostat.settings.heatStages > 0 || thermostat.settings.hasHeatPump; 151 | }, 152 | 153 | hasCoolMode: function(thermostat) { 154 | return thermostat.settings.coolStages > 0 || thermostat.settings.hasHeatPump; 155 | }, 156 | 157 | hasAuxHeatMode: function(thermostat) { 158 | return thermostat.settings.hasHeatPump && 159 | (thermostat.settings.hasElectric || thermostat.settings.hasBoiler || thermostat.settings.hasForcedAir); 160 | }, 161 | 162 | hasAutoMode: function(thermostat) { 163 | return thermostat.settings.autoHeatCoolFeatureEnabled && this.hasCoolMode(thermostat) && this.hasHeatMode(thermostat); 164 | }, 165 | 166 | hasSensors: function(thermostat) { 167 | return thermostat.remoteSensors && thermostat.remoteSensors.length; 168 | }, 169 | 170 | selectThermostat: function(thermostatId ,thermostatList){ 171 | for (var index = 0; index < thermostatList.length; ++index) { 172 | if(thermostatList[index].identifier === thermostatId){ 173 | return thermostatList[index]; 174 | } 175 | } 176 | } 177 | }; -------------------------------------------------------------------------------- /src/js/oauth.js: -------------------------------------------------------------------------------- 1 | var ajax = require('ajax'); 2 | var Vibe = require('ui/vibe'); 3 | var Settings = require('settings'); 4 | var UI = require('ui'); 5 | var ErrorWindow = require('error-window'); 6 | 7 | var doGetAccessToken = function(asyncReq) { 8 | var oauthTokenExpires = Settings.data('oauthTokenExpires'); 9 | var refreshToken = Settings.data('refreshToken'); 10 | var oauthToken = Settings.data('oauthToken'); 11 | console.log('oauth token '+oauthToken+' expires at: '+oauthTokenExpires+'; refresh token: '+refreshToken); 12 | if (Date.now() > oauthTokenExpires-500) { 13 | var tokenUrl = Settings.data('ecobeeServerUrl') + 14 | Settings.data('ecobeeTokenApi') + 15 | '?grant_type=refresh_token&client_id=' + 16 | Settings.data('clientId') + 17 | "&refresh_token=" + refreshToken; 18 | console.log('Calling '+tokenUrl); 19 | ajax( 20 | { 21 | url: tokenUrl, 22 | type: 'json', 23 | async: asyncReq, 24 | method: 'post' 25 | }, 26 | function(data) { 27 | console.log('Received AUTH data: '+JSON.stringify(data)); 28 | Settings.data('refreshToken', data.refresh_token); 29 | Settings.data('oauthToken', data.access_token); 30 | var tokenExpiresIn = Date.now()+data.expires_in*1000; 31 | console.log('Token '+data.access_token+' will expire at '+tokenExpiresIn); 32 | Settings.data('oauthTokenExpires', tokenExpiresIn); 33 | return data.access_token; 34 | }, 35 | function(error) { 36 | console.log('Error refreshing OAuth token: '+JSON.stringify(error)); 37 | ErrorWindow.show(error.error_description); 38 | } 39 | ); 40 | } else { 41 | return Settings.data('oauthToken'); 42 | } 43 | }; 44 | 45 | var authPin = function(pin, code, onSuccess) { 46 | var card = new UI.Card(); 47 | card.title('PIN: '+pin); 48 | card.body('Log into ecobee.com and go to "My Apps" section.' + 49 | 'Enter the provided pin then '+ 50 | 'press select button.'); 51 | card.show(); 52 | card.on('click', 'select', function(e) { 53 | var tokenUrl = Settings.data('ecobeeServerUrl') + 54 | Settings.data('ecobeeTokenApi') + 55 | '?grant_type=ecobeePin&client_id=' + 56 | Settings.data('clientId') + 57 | "&code=" + code; 58 | console.log('Calling '+tokenUrl); 59 | ajax( 60 | { 61 | url: tokenUrl, 62 | type: 'json', 63 | method: 'post' 64 | }, 65 | function(data) { 66 | Vibe.vibrate('short'); 67 | console.log('Received AUTH data: '+JSON.stringify(data)); 68 | Settings.data('authPin', null); 69 | Settings.data('authCode', null); 70 | Settings.data('authExpires', null); 71 | Settings.data('paired', true); 72 | Settings.data('refreshToken', data.refresh_token); 73 | Settings.data('oauthToken', data.access_token); 74 | var tokenExpiresIn = Date.now()+data.expires_in*1000; 75 | console.log('Token '+data.access_token+' will expire at '+tokenExpiresIn); 76 | Settings.data('oauthTokenExpires', tokenExpiresIn); 77 | card.hide(); 78 | onSuccess(); 79 | }, 80 | function(error) { 81 | console.log('Error receiving AUTH data: '+JSON.stringify(error)); 82 | if (error.error === "authorization_pending") { 83 | card.hide(); 84 | this.authorizePin(pin, code); 85 | } else { 86 | ErrorWindow.show(); 87 | card = new UI.Card(error.error_description); 88 | } 89 | } 90 | ); 91 | }); 92 | }; 93 | 94 | var doGetPin = function(onSuccess) { 95 | Settings.data('authPin', null); 96 | Settings.data('authCode', null); 97 | Settings.data('authExpires', null); 98 | var card = new UI.Card(); 99 | card.title('Authorization'); 100 | card.body('Calling ecobee for authorization. Please wait.'); 101 | card.show(); 102 | var authUrl = Settings.data('ecobeeServerUrl')+ 103 | '/authorize?response_type=ecobeePin&scope=smartWrite&client_id='+ 104 | Settings.data('clientId'); 105 | console.log('Calling '+authUrl); 106 | ajax( 107 | { 108 | url: authUrl, 109 | type: 'json' 110 | }, 111 | function(data) { 112 | Vibe.vibrate('short'); 113 | console.log('Received AUTH data: '+JSON.stringify(data)); 114 | Settings.data('authPin', data.ecobeePin); 115 | Settings.data('authCode', data.code); 116 | var expiresAt = Date.now()+data.expires_in*60*1000; 117 | console.log('Pin will expire at '+expiresAt); 118 | Settings.data('authExpires', expiresAt); 119 | card.hide(); 120 | authPin(data.ecobeePin, data.code, onSuccess); 121 | }, 122 | function(error) { 123 | console.log('Error receiving AUTH data: '+JSON.stringify(error)); 124 | card.hide(); 125 | ErrorWindow.show('Unable to contact ecobee. Try again later.'); 126 | } 127 | ); 128 | }; 129 | 130 | this.exports = { 131 | getAccessToken: function(asyncReq) { 132 | return doGetAccessToken(asyncReq); 133 | }, 134 | 135 | getPin: function(onSuccess) { 136 | doGetPin(onSuccess); 137 | }, 138 | authorizePin: function(pin, code, onSuccess) { 139 | authPin(pin, code, onSuccess); 140 | } 141 | 142 | }; -------------------------------------------------------------------------------- /src/js/menu.js: -------------------------------------------------------------------------------- 1 | var UI = require('ui'); 2 | var Settings = require('settings'); 3 | var ecobeeApi = require('ecobee-api'); 4 | var Utils = require('utils'); 5 | 6 | var menu; 7 | 8 | var resumeProgram = function(thermostat) { 9 | var postRequest = Utils.createResumeProgramEvent(thermostat); 10 | ecobeeApi.postThermostat(postRequest, 11 | function() { 12 | console.log('Successfully resumed program!'); 13 | if (menu) menu.hide(); 14 | }, 15 | function(error) { 16 | console.log('error resuming program: '+error); 17 | }); 18 | }; 19 | 20 | var homeHold = function(thermostat) { 21 | var postRequest = Utils.createHomeHoldEvent(thermostat); 22 | ecobeeApi.postThermostat(postRequest, 23 | function() { 24 | console.log('Successfully set home hold!'); 25 | if (menu) menu.hide(); 26 | }, 27 | function(error) { 28 | console.log('error setting home hold: '+error); 29 | }); 30 | }; 31 | 32 | var awayHold = function(thermostat) { 33 | var postRequest = Utils.createAwayHoldEvent(thermostat); 34 | ecobeeApi.postThermostat(postRequest, 35 | function() { 36 | console.log('Successfully set away hold!'); 37 | if (menu) menu.hide(); 38 | }, 39 | function(error) { 40 | console.log('error setting away hold: '+error); 41 | }); 42 | }; 43 | 44 | var sleepHold = function(thermostat) { 45 | var postRequest = Utils.createSleepHoldEvent(thermostat); 46 | ecobeeApi.postThermostat(postRequest, 47 | function() { 48 | console.log('Successfully set sleep hold!'); 49 | if (menu) menu.hide(); 50 | }, 51 | function(error) { 52 | console.log('error setting sleep hold: '+error); 53 | }); 54 | }; 55 | 56 | var showSensorsMenu = function(thermostat) { 57 | var menuItems = []; 58 | thermostat.remoteSensors.forEach( 59 | function(sensor) { 60 | var sensorName = sensor.name; 61 | var occupied; 62 | if (sensorName.length>11) { 63 | sensorName = sensorName.substring(0, 11); 64 | } 65 | for (var idx in sensor.capability) { 66 | var cap = sensor.capability[idx]; 67 | if (cap.type==='temperature') { 68 | if (thermostat.settings.useCelsius) { 69 | sensorName = sensorName + ' ' + Utils.canonicalToCelsius(cap.value).toPrecision(3)+'\u00B0'; 70 | } else { 71 | sensorName = sensorName + ' ' +Utils.canonicalToFahrenheit(cap.value).toPrecision(3)+'\u00B0'; 72 | } 73 | } 74 | if(cap.type==='occupancy'){ 75 | occupied = cap.value === 'true' ? 'Occupied' : 'Unoccupied'; 76 | } 77 | } 78 | menuItems.push({ 79 | title: sensorName, 80 | subtitle: occupied, 81 | type: sensor.type 82 | }); 83 | } 84 | ); 85 | 86 | menuItems = menuItems.sort(function(a,b){ 87 | if(a.type === 'thermostat'){ 88 | return -1; 89 | } 90 | else if (b.type === 'thermostat'){ 91 | return 1; 92 | } 93 | else{ 94 | return a.title.toUpperCase() > b.title.toUpperCase() ? 1 : -1; 95 | } 96 | }); 97 | 98 | var sensorMenu = new UI.Menu({ 99 | backgroundColor: '#555555', 100 | textColor: 'white', 101 | highlightBackgroundColor: 'black', 102 | highlightTextColor: '#AAFF00', 103 | sections: [{ 104 | items: menuItems 105 | }] 106 | }); 107 | sensorMenu.show(); 108 | }; 109 | 110 | var showThermostatsMenu = function(thermostatList){ 111 | var menuItems = []; 112 | 113 | thermostatList.forEach( 114 | function(thermostat){ 115 | menuItems.push({ 116 | title: thermostat.name, 117 | thermostatId: thermostat.identifier 118 | }); 119 | } 120 | ); 121 | 122 | var thermostatMenu = new UI.Menu({ 123 | backgroundColor: '#555555', 124 | textColor: 'white', 125 | highlightBackgroundColor: 'black', 126 | highlightTextColor: '#AAFF00', 127 | sections: [{ 128 | items: menuItems 129 | }] 130 | }); 131 | 132 | thermostatMenu.on('select', function(e) { 133 | Settings.data('selectedThermostatId', e.item.thermostatId); 134 | thermostatMenu.hide(); 135 | if (menu) menu.hide(); 136 | }); 137 | 138 | thermostatMenu.show(); 139 | }; 140 | 141 | var showHvacModeMenu = function(thermostat){ 142 | var menuItems = []; 143 | 144 | if(Utils.hasHeatMode(thermostat)){ 145 | menuItems.push({ title: 'Heat', value: 'heat'}); 146 | } 147 | if(Utils.hasCoolMode(thermostat)){ 148 | menuItems.push({ title: 'Cool', value: 'cool'}); 149 | } 150 | if(Utils.hasAutoMode(thermostat)){ 151 | menuItems.push({ title: 'Auto', value: 'auto'}); 152 | } 153 | if(Utils.hasAuxHeatMode(thermostat)){ 154 | menuItems.push({ title: 'Aux', value: 'auxHeatOnly'}); 155 | } 156 | menuItems.push({ title: 'Off', value: 'off'}); 157 | 158 | var hvacModeMenu = new UI.Menu({ 159 | backgroundColor: '#555555', 160 | textColor: 'white', 161 | highlightBackgroundColor: 'black', 162 | highlightTextColor: '#AAFF00', 163 | sections: [{ 164 | items: menuItems 165 | }] 166 | }); 167 | 168 | hvacModeMenu.on('select', function(e) { 169 | var postRequest = Utils.createChangeModeRequest(thermostat, e.item.value); 170 | ecobeeApi.postThermostat(postRequest, 171 | function() { 172 | hvacModeMenu.hide(); 173 | if (menu) menu.hide(); 174 | }, 175 | function(error) { 176 | console.log('error setting hvac mode: '+error); 177 | }); 178 | }); 179 | 180 | hvacModeMenu.show(); 181 | }; 182 | 183 | this.exports = { 184 | show: function(thermostatList) { 185 | var thermostat; 186 | var selectedThermostatId = Settings.data('selectedThermostatId'); 187 | if(selectedThermostatId){ 188 | thermostat = Utils.selectThermostat(selectedThermostatId,thermostatList); 189 | } 190 | else{ 191 | thermostat = thermostatList[0]; 192 | } 193 | 194 | var menuItems = []; 195 | var hasHold = Utils.hasHold(thermostat); 196 | var hasSensors = Utils.hasSensors(thermostat); 197 | if (hasSensors) { 198 | menuItems.push({title: 'Sensors'}); 199 | } 200 | if (hasHold) { 201 | menuItems.push({ title: 'Resume Program' }); 202 | } 203 | menuItems.push({ title: 'Home and Hold' }); 204 | menuItems.push({ title: 'Away and Hold'}); 205 | menuItems.push({ title: 'Sleep and Hold'}); 206 | if(thermostatList.length > 1){ 207 | menuItems.push({title: 'Thermostats'}); 208 | } 209 | menuItems.push({ title: 'Change Mode'}); 210 | menu = new UI.Menu({ 211 | backgroundColor: '#555555', 212 | textColor: 'white', 213 | highlightBackgroundColor: 'black', 214 | highlightTextColor: '#AAFF00', 215 | sections: [{ 216 | items: menuItems 217 | }] 218 | }); 219 | menu.on('select', function(e) { 220 | var title = e.item.title; 221 | console.log('Selected item "' + title + '"'); 222 | if (title) { 223 | if (title==='Resume Program') { 224 | resumeProgram(thermostat); 225 | } else if (title==='Home and Hold') { 226 | homeHold(thermostat); 227 | } else if (title==='Away and Hold') { 228 | awayHold(thermostat); 229 | } else if (title==='Sleep and Hold') { 230 | sleepHold(thermostat); 231 | } else if (title==='Sensors') { 232 | showSensorsMenu(thermostat); 233 | } else if (title==='Thermostats') { 234 | showThermostatsMenu(thermostatList); 235 | } else if (title==='Change Mode') { 236 | showHvacModeMenu(thermostat); 237 | } 238 | } 239 | }); 240 | menu.show(); 241 | } 242 | }; -------------------------------------------------------------------------------- /src/js/main-window.js: -------------------------------------------------------------------------------- 1 | var UI = require('ui'); 2 | var Vector2 = require('vector2'); 3 | var ecobeeApi = require('ecobee-api'); 4 | var ErrorWindow = require('error-window'); 5 | var Accel = require('ui/accel'); 6 | var Utils = require('utils'); 7 | var Menu = require('menu'); 8 | var Elements = require('elements'); 9 | var Feature = require('platform/feature'); 10 | var Settings = require('settings'); 11 | 12 | Accel.init(); 13 | 14 | var mainWindow = new UI.Window({ 15 | status: false, 16 | scrollable: false, 17 | clear: false 18 | }); 19 | 20 | var nameText = new UI.Text({ 21 | position: new Vector2(0, 0), 22 | size: new Vector2(144, 20), 23 | text: 'Loading...', 24 | font: 'gothic-18-bold', 25 | color: 'white', 26 | textAlign: 'left' 27 | }); 28 | 29 | var modeText = new UI.Text({ 30 | position: new Vector2(0, 22), 31 | size: new Vector2(144, 18), 32 | text: 'HEAT', 33 | font: 'gothic-18', 34 | color: 'white', 35 | textAlign: 'center' 36 | }); 37 | 38 | var humidityIcon = new UI.Image({ 39 | position: new Vector2(46, 44), 40 | size: new Vector2(20, 20), 41 | image: 'images/humidity-icon.png' 42 | }); 43 | 44 | var humidityText = new UI.Text({ 45 | position: new Vector2(66, 42), 46 | size: new Vector2(74, 18), 47 | text: '', 48 | font: 'gothic-18', 49 | color: 'white', 50 | textAlign: 'left' 51 | }); 52 | 53 | var temperatureText = new UI.Text({ 54 | position: new Vector2(0, 60), 55 | size: new Vector2(144, 40), 56 | text: '...', 57 | font: 'bitham-42-bold', 58 | color: 'white', 59 | textAlign: 'center' 60 | }); 61 | 62 | var holdText = new UI.Text({ 63 | position: new Vector2(40, 130), 64 | size: new Vector2(66, 24), 65 | borderColor: 'black', 66 | text: '', 67 | font: 'gothic-18', 68 | color: 'white', 69 | textAlign: 'center' 70 | }); 71 | 72 | var holdTemp1; 73 | var holdTemp2; 74 | var bgDots; 75 | var myTstat; 76 | var myThermostatList; 77 | 78 | mainWindow.add(modeText); 79 | mainWindow.add(nameText); 80 | mainWindow.add(humidityIcon); 81 | mainWindow.add(humidityText); 82 | mainWindow.add(temperatureText); 83 | mainWindow.add(holdText); 84 | 85 | mainWindow.setTstatName = function(text) { 86 | if (Feature.round()) { 87 | nameText.position(new Vector2(54, 2)); 88 | nameText.size(new Vector2(90, 18)); 89 | //nameText.textAlign('center'); 90 | if (text.length>10) { 91 | text = text.substring(0, 9)+'...'; 92 | } 93 | } else { 94 | if (text.length>15) { 95 | text = text.substring(0, 14)+'...'; 96 | } 97 | } 98 | nameText.text(text); 99 | }; 100 | mainWindow.setTemperature = function(thermostat) { 101 | var canonical = thermostat.runtime.actualTemperature; 102 | if (thermostat.settings.useCelsius) { 103 | temperatureText.text(Utils.canonicalToCelsius(canonical).toPrecision(3)); 104 | } else { 105 | temperatureText.text(Utils.canonicalToFahrenheit(canonical).toPrecision(2)); 106 | } 107 | }; 108 | mainWindow.setHumidity = function(percentage) { 109 | humidityText.text(percentage+'%'); 110 | }; 111 | 112 | mainWindow.displayHold = function(thermostat) { 113 | var hasHold = Utils.hasHold(thermostat); 114 | if (hasHold) { 115 | holdText.borderColor('white'); 116 | holdText.text('Hold'); 117 | } else { 118 | holdText.text(''); 119 | holdText.borderColor('black'); 120 | } 121 | }; 122 | 123 | mainWindow.setHeatMode = function(thermostat) { 124 | if (holdTemp1) holdTemp1.remove(); 125 | if (holdTemp2) holdTemp2.remove(); 126 | if (bgDots && bgDots.length){ 127 | bgDots.forEach(function(element){ 128 | mainWindow.remove(element); 129 | }); 130 | } 131 | holdTemp1 = undefined; 132 | holdTemp2 = undefined; 133 | var hvacMode = thermostat.settings.hvacMode; 134 | console.log('HVac mode: '+hvacMode); 135 | var heatHold; 136 | var coolHold; 137 | var heatDisabled; 138 | var coolDisabled; 139 | 140 | thermostat.events.forEach(function(event){ 141 | if(event.running){ 142 | if(event.isCoolOff){ 143 | coolDisabled = true; 144 | } 145 | if(event.isHeatOff){ 146 | heatDisabled = true; 147 | } 148 | } 149 | }); 150 | 151 | if ((hvacMode==='heat' || hvacMode==='auxHeatOnly') && !heatDisabled) { 152 | modeText.color(Feature.color('#FF5500', 'white')); 153 | modeText.text('HEAT'); 154 | bgDots = Elements.bgHeatDots(); 155 | if (thermostat.settings.useCelsius) { 156 | heatHold = Utils.canonicalToCelsius(thermostat.runtime.desiredHeat).toPrecision(3); 157 | } else { 158 | heatHold = Utils.canonicalToFahrenheit(thermostat.runtime.desiredHeat).toPrecision(2); 159 | } 160 | holdTemp1 = Elements.holdTempHeat(heatHold, new Vector2(118, 72)); 161 | } else if (hvacMode==='cool' && !coolDisabled) { 162 | modeText.color(Feature.color('#00AAFF', 'white')); 163 | modeText.text('COOL'); 164 | bgDots = Elements.bgCoolDots(); 165 | if (thermostat.settings.useCelsius) { 166 | coolHold = Utils.canonicalToCelsius(thermostat.runtime.desiredCool).toPrecision(3); 167 | } else { 168 | coolHold = Utils.canonicalToFahrenheit(thermostat.runtime.desiredCool).toPrecision(2); 169 | } 170 | holdTemp1 = Elements.holdTempCool(coolHold, new Vector2(118, 72)); 171 | } else if (hvacMode==='auto') { 172 | modeText.color('white'); 173 | modeText.text('AUTO'); 174 | bgDots = Elements.bgAutoDots(); 175 | if (thermostat.settings.useCelsius) { 176 | coolHold = !coolDisabled ? Utils.canonicalToCelsius(thermostat.runtime.desiredCool).toPrecision(3) : 'Off'; 177 | heatHold = !heatDisabled ? Utils.canonicalToCelsius(thermostat.runtime.desiredHeat).toPrecision(3) : 'Off'; 178 | } else { 179 | coolHold = !coolDisabled ? Utils.canonicalToFahrenheit(thermostat.runtime.desiredCool).toPrecision(2) : 'Off'; 180 | heatHold = !heatDisabled ? Utils.canonicalToFahrenheit(thermostat.runtime.desiredHeat).toPrecision(2) : 'Off'; 181 | } 182 | holdTemp1 = Elements.holdTempCool(coolHold, new Vector2(118, 54)); 183 | holdTemp2 = Elements.holdTempHeat(heatHold, new Vector2(118, 87)); 184 | } else { 185 | modeText.color('white'); 186 | modeText.text('OFF'); 187 | bgDots = null; 188 | } 189 | if (bgDots && bgDots.length){ 190 | bgDots.forEach(function(element){ 191 | mainWindow.add(element); 192 | }); 193 | } 194 | if (holdTemp1) mainWindow.add(holdTemp1); 195 | if (holdTemp2) mainWindow.add(holdTemp2); 196 | }; 197 | 198 | var refreshData = function() { 199 | ecobeeApi.loadThermostats( 200 | function(thermostatList) { 201 | myThermostatList = thermostatList; 202 | var selectedThermostatId = Settings.data('selectedThermostatId'); 203 | if(selectedThermostatId){ 204 | myTstat = Utils.selectThermostat(selectedThermostatId,thermostatList); 205 | } 206 | else{ 207 | myTstat = thermostatList[0]; 208 | } 209 | mainWindow.setTstatName(myTstat.name); 210 | mainWindow.setTemperature(myTstat); 211 | mainWindow.setHumidity(myTstat.runtime.actualHumidity); 212 | mainWindow.setHeatMode(myTstat); 213 | mainWindow.displayHold(myTstat); 214 | }, 215 | function(error) { 216 | ErrorWindow.show('Cannot load thermostat data'); 217 | }); 218 | }; 219 | 220 | var changeTemperature = function(delta) { 221 | var hvacMode = myTstat.settings.hvacMode; 222 | var newHeatHold = myTstat.runtime.desiredHeat+delta; 223 | var newCoolHold = myTstat.runtime.desiredCool+delta; 224 | if (hvacMode==='heat' || hvacMode==='auxHeatOnly' || hvacMode==='auto') { 225 | if (newHeatHold > myTstat.settings.heatRangeHigh || 226 | newHeatHold < myTstat.settings.heatRangeLow) { 227 | return; 228 | } 229 | } else if (hvacMode==='cool' || hvacMode==='auto') { 230 | if (newCoolHold > myTstat.settings.coolRangeHigh || 231 | newCoolHold < myTstat.settings.coolRangeLow) { 232 | return; 233 | } 234 | } 235 | var postRequest = Utils.createTemperatureHoldEvent(myTstat, newHeatHold, newCoolHold); 236 | ecobeeApi.postThermostat(postRequest, 237 | function() { 238 | console.log('success!'); 239 | refreshData(); 240 | }, 241 | function(error) { 242 | console.log('error: '+error); 243 | }); 244 | }; 245 | 246 | function animateHoldTempText(delta) { 247 | if (holdTemp1) { 248 | var pos = holdTemp1.position(); 249 | var originalPosition = pos.y; 250 | pos.y = pos.y + delta; 251 | holdTemp1.animate('position', pos, 200); 252 | holdTemp1.queue(function(next) { 253 | pos = this.position(); 254 | pos.y = originalPosition; 255 | this.animate('position', pos, 100); 256 | next(); 257 | }); 258 | } 259 | if (holdTemp2) { 260 | var pos2 = holdTemp2.position(); 261 | var originalPosition2 = pos2.y; 262 | pos2.y = pos2.y + delta; 263 | holdTemp2.animate('position', pos2, 200); 264 | holdTemp2.queue(function(next) { 265 | pos2 = this.position(); 266 | pos2.y = originalPosition2; 267 | this.animate('position', pos2, 100); 268 | next(); 269 | }); 270 | } 271 | } 272 | 273 | mainWindow.on('click', 'up', function(event) { 274 | animateHoldTempText(-20); 275 | changeTemperature(10); 276 | }); 277 | 278 | mainWindow.on('click', 'down', function(event) { 279 | animateHoldTempText(20); 280 | changeTemperature(-10); 281 | }); 282 | 283 | mainWindow.on('click', 'select', function(event) { 284 | Menu.show(myThermostatList); 285 | }); 286 | 287 | mainWindow.on('show', function(event) { 288 | console.log('Show event on main winow'); 289 | refreshData(); 290 | 291 | mainWindow.on('accelTap', function(e) { 292 | refreshData(); 293 | }); 294 | }); 295 | 296 | mainWindow.on('hide', function(event) { 297 | console.log('Hiding main window'); 298 | mainWindow.off('accelTap'); 299 | }); 300 | 301 | this.exports = { 302 | window: mainWindow, 303 | show: function() { 304 | mainWindow.show(); 305 | } 306 | }; --------------------------------------------------------------------------------