UNPKG

homebridge-ecobee3-sensors

Version:

Homebridge plugin that exposes Ecobee sensors as HomeKit accessories

460 lines (413 loc) 15.5 kB
/* jshint esversion: 6 */ /* jshint sub: true */ var UUIDGen, Accessory, EcobeeSensor, EcobeeEquipment; var Querystring = require('querystring'); var Https = require('https'); const supportedThermostats = [ 'artemisSmart', 'vulcanSmart', 'athenaSmart', 'apolloSmart', 'nikeSmart', 'aresSmart', ]; const Verbosity = Object.freeze({ None: 0, Error: 1, Warn: 2, Info: 3, Debug: 4 }) class EBLogger { constructor(log) { this.hbLog = log; } setLevel(level) { this.logLevel = level; } debug(args) { if (this.logLevel >= Verbosity.Debug) { this.hbLog.debug(args); } } info(args) { if (this.logLevel >= Verbosity.Info) { this.hbLog.info(args); } } warn(args) { if (this.logLevel >= Verbosity.Warn) { this.hbLog.warn(args); } } error(args) { if (this.logLevel >= Verbosity.Error) { this.hbLog.error(args); } } getLevelStr() { switch (this.logLevel) { case Verbosity.None: return "None"; case Verbosity.Error: return "Error"; case Verbosity.Warn: return "Warn"; case Verbosity.Info: return "Info"; case Verbosity.Debug: return "Debug"; default: return "Unknown"; } } } module.exports = function (uuidGen, accessory, ecobeeSensor, ecobeeEquipment) { UUIDGen = uuidGen Accessory = accessory; EcobeeSensor = ecobeeSensor; EcobeeEquipment = ecobeeEquipment; return EcobeePlatform; }; function EcobeePlatform(log, config, homebridgeAPI) { if (!config) { log.warn(" Ignoring Ecobee Sensor Plugin setup because it is not configured"); this.disabled = true; return; } this.log = new EBLogger(log); this.config = config || {}; this.excludeSensors = this.config.exclude_sensors || false; this.excludeHumiditySensors = this.config.exclude_humidity_sensors || false; this.excludeOccupancySensors = this.config.exclude_occupancy_sensors || false; this.excludeTemperatureSensors = this.config.exclude_temperature_sensors || false; this.excludeEquipmentSensors = this.config.exclude_equipment_sensors || false; this.excludeThermostat = this.config.ex || false; this.updateFrequency = this.config.update_frequency || 30; this.logLevel = this.config.log_level || Verbosity.Info; this.log.setLevel(this.logLevel); log.info("Log level set to " + this.log.getLevelStr()); this.appKey = this.config.app_key || "DALCINnO49EYOmMfQQxmx7PYofM1YEGo"; this.accessToken = null; this.refreshToken = null; this.ecobeeAccessories = {}; this.homebridgeAccessories = {}; this.homebridgeAPI = homebridgeAPI; this.homebridgeAPI.on('didFinishLaunching', this.didFinishLaunching.bind(this)); } EcobeePlatform.prototype.configureAccessory = function (homebridgeAccessory) { this.log.debug("Configuring chached Homebridge accessory..."); this.log.debug(homebridgeAccessory); var sensorCode = homebridgeAccessory.context['code']; homebridgeAccessory.reachable = false; if (homebridgeAccessory.context['refresh_token']) { this.accessToken = homebridgeAccessory.context['access_token']; // This is a bit hackish... this.refreshToken = homebridgeAccessory.context['refresh_token']; // This is a bit hackish... } this.log.info("Cached | " + homebridgeAccessory.displayName + " | " + sensorCode); this.homebridgeAccessories[sensorCode] = homebridgeAccessory; }; EcobeePlatform.prototype.didFinishLaunching = function () { this.log.debug("Finished launching..."); if (!this.accessToken || !this.refreshToken) { this.pin(); } else { this.update(); } }; EcobeePlatform.prototype.pin = function () { this.log.debug("Requesting authorization code..."); var options = { hostname: 'api.ecobee.com', headers: {'content-type': 'application/x-www-form-urlencoded'}, path: '/authorize?' + Querystring.stringify({ 'response_type': 'ecobeePin', 'client_id': this.appKey, 'scope': 'smartRead' }), method: 'GET' }; var request = Https.request(options, function (response) { var data = ''; response.on('data', function (chunk) { data += chunk; }); response.on('end', function () { var reply = JSON.parse(data); this.log.debug(reply); var pin = reply['ecobeePin']; var code = reply['code']; this.log.info("These are the steps authorize this application to access your Ecobee:"); this.log.info(" 1. Go to https://www.ecobee.com/home/ecobeeLogin.jsp"); this.log.info(" 2. Login to your thermostat console "); this.log.info(" 3. Select 'MY APPS' from the menu on the top right."); this.log.info(" 4. Click 'Add Application' "); this.log.info(" 5. Enter the following authorization code:"); this.log.info(" ┌──────┐ "); this.log.info(" │ " + pin + " │ "); this.log.info(" └──────┘ "); this.log.info(" 6. Wait a moment.") this.authorize(code); }.bind(this)); }.bind(this)); request.on('error', function (error) { this.log.error(error + " Retrying request."); setTimeout(this.pin.bind(this), 1000); }.bind(this)); request.end(); }; EcobeePlatform.prototype.authorize = function (code) { this.log.debug("Authorizing plugin to access the thermostat..."); var options = { hostname: 'api.ecobee.com', headers: {'content-type': 'application/x-www-form-urlencoded'}, path: '/token', method: 'POST' }; var request = Https.request(options, function (response) { var data = ''; response.on('data', function (chunk) { data += chunk; }); response.on('end', function () { var reply = JSON.parse(data); this.log.debug(reply); switch (reply['error'] || null) { case null: this.log.info("Authorization successful :-)"); this.accessToken = reply['access_token']; this.refreshToken = reply['refresh_token']; this.update(this.refresh.bind(this)); break; case 'authorization_pending': this.log.warn(reply['error_description'] + " Retrying in 30 seconds."); setTimeout(this.authorize.bind(this), 31 * 1000, code); break; case 'authorization_expired': this.log.error(reply['error_description']); this.log.error("Expire | 10 minutes."); this.pin(); break; default: this.log.warn(reply['error_description']); this.log.warn("Wait | 10 seconds"); setTimeout(this.authorize.bind(this), 10 * 1000, code); break; } }.bind(this)); }.bind(this)); request.write(Querystring.stringify({ 'grant_type': 'ecobeePin', 'client_id': this.appKey, 'code': code })); request.on('error', function (error) { this.log.error(error + " Retrying request."); setTimeout(this.authorize.bind(this), 1000, code); }.bind(this)); this.log.debug(request); request.end(); }; EcobeePlatform.prototype.update = function (callback) { this.log.debug("Updating sensors with fresh data..."); var options = { hostname: 'api.ecobee.com', headers: { 'Content-Type': 'application/json', 'authorization': 'Bearer ' + this.accessToken }, path: '/1/thermostat?' + Querystring.stringify({ json: JSON.stringify({ 'selection': { 'selectionType': 'registered', 'selectionMatch': '', 'includeSensors': true, 'includeEquipmentStatus': true } }) }), method: 'GET' }; var request = Https.request(options, function (response) { var data = ''; response.on('data', function (chunk) { data += chunk; }); response.on('end', function () { var reply = JSON.parse(data); this.log.debug(reply); var status = reply['status'] || {'code' : 'default'}; switch (status['code']) { case 0: this.log.info("Update sensors"); this.sensors(reply); this.log.info("Update equipments"); this.equipments(reply); this.clean(); setTimeout(this.update.bind(this), this.updateFrequency*1000); this.log.info("Wait | " + this.updateFrequency + " seconds"); if (callback) callback(); break; case 14: this.log.info("Refresh"); this.refresh(this.update.bind(this)); break; default: this.log.error(status['message'] + " Re-requesting authorization!"); this.accessToken = null; this.refreshToken = null; this.pin(); break; } }.bind(this)); }.bind(this)); request.on('error', function (error) { this.log.error(error + " Retrying request."); setTimeout(this.update.bind(this), 1000); }.bind(this)); this.log.debug(request); request.end(); }; EcobeePlatform.prototype.sensors = function (reply) { this.log.debug("Setting values of sensors..."); if (!reply.thermostatList || reply.thermostatList.length === 0) { this.log.error("No Ecobee thermostats found. Please, make soure your thermostat is registered."); return; } for (var thermostatConfig of reply.thermostatList) { if (supportedThermostats.indexOf(thermostatConfig.modelNumber) == -1) { this.log.info("Not supported thermostat | " + thermostatConfig.name + " (" + thermostatConfig.modelNumber + ")"); continue } for (var sensorConfig of thermostatConfig.remoteSensors) { if (sensorConfig.type === 'thermostat') { if (this.excludeThermostat) continue; sensorConfig.code = thermostatConfig.identifier; // Hack around missing code for the thermostat itself } if (sensorConfig.type === 'ecobee3_remote_sensor') { if (this.excludeSensors) continue; } if (sensorConfig.capability) { var sensorCode = sensorConfig.code; var sensor = this.ecobeeAccessories[sensorCode]; if (!sensor) { var homebridgeAccessory = this.homebridgeAccessories[sensorCode]; if (!homebridgeAccessory) { this.log.info("Create | " + sensorConfig.name + " | " + sensorCode); homebridgeAccessory = new Accessory(sensorConfig.name, UUIDGen.generate(sensorCode + ' ' + sensorConfig.name)); homebridgeAccessory.context['code'] = sensorCode; this.homebridgeAPI.registerPlatformAccessories("homebridge-ecobee3-sensors", "Ecobee 3 Sensors", [homebridgeAccessory]); } else { this.log.info("Cached | " + sensorConfig.name + " | " + sensorCode); delete this.homebridgeAccessories[sensorCode]; } sensor = new EcobeeSensor(this.log, sensorConfig, this, homebridgeAccessory); this.ecobeeAccessories[sensorCode] = sensor; } else { sensor.update(sensorConfig); } } } } }; EcobeePlatform.prototype.equipments = function (reply) { this.log.debug("Setting values of equipments..."); if (!reply.thermostatList || reply.thermostatList.length === 0) { this.log.error("No Ecobee thermostats found. Please, make soure your thermostat is registered."); return; } var activeEquipments = []; for (var thermostatConfig of reply.thermostatList) { if (supportedThermostats.indexOf(thermostatConfig.modelNumber) == -1) { this.log.info("Not supported thermostat | " + thermostatConfig.name + " (" + thermostatConfig.modelNumber + ")"); continue } if (thermostatConfig.equipmentStatus) { for (var equipmentName of thermostatConfig.equipmentStatus.split(',')) { if (equipmentName === '') continue; equipmentName = "Ecobee " + equipmentName.trim(); var equipment = this.ecobeeAccessories[equipmentName]; if (!equipment) { var homebridgeAccessory = this.homebridgeAccessories[equipmentName]; if (!homebridgeAccessory) { this.log.info("Create | " + equipmentName); homebridgeAccessory = new Accessory(equipmentName, UUIDGen.generate(`equipment ${equipmentName}`)); homebridgeAccessory.context['code'] = equipmentName; this.homebridgeAPI.registerPlatformAccessories("homebridge-ecobee3-sensors", "Ecobee 3 Sensors", [homebridgeAccessory]); } else { this.log.info("Cached | " + equipmentName); delete this.homebridgeAccessories[equipmentName]; } equipment = new EcobeeEquipment(this.log, { name: equipmentName, code: equipmentName }, this, homebridgeAccessory); this.ecobeeAccessories[equipmentName] = equipment; } activeEquipments.push(equipmentName); } } } for (var equipmentName in this.ecobeeAccessories) { if (equipmentName.startsWith("Ecobee ")) { var equipment = this.ecobeeAccessories[equipmentName]; if (equipment.isEquipment) { equipment.update(activeEquipments.includes(equipmentName)); } } } }; EcobeePlatform.prototype.clean = function () { this.log.debug("Cleaning unused cached Homebridge accessories..."); this.log.debug(this.homebridgeAccessories); for (var sensorCode in this.homebridgeAccessories) { var homebridgeAccessory = this.homebridgeAccessories[sensorCode]; this.log.info("Remove | " + homebridgeAccessory.displayName + " - " + sensorCode); try { this.homebridgeAPI.unregisterPlatformAccessories("homebridge-ecobee3-sensors", "Ecobee 3 Sensors", [homebridgeAccessory]); } catch (e) { } delete this.homebridgeAccessories[sensorCode]; } }; EcobeePlatform.prototype.refresh = function (callback) { this.log.debug("Refreshing tokens..."); var options = { hostname: 'api.ecobee.com', headers: {'content-type': 'application/x-www-form-urlencoded'}, path: '/token', method: 'POST' }; var request = Https.request(options, function (response) { var data = ''; response.on('data', function (chunk) { data += chunk; }); response.on('end', function () { var reply = JSON.parse(data); this.log.debug(reply); switch (reply['error'] || null) { case null: this.log.info("Tokens"); this.accessToken = reply['access_token']; this.refreshToken = reply['refresh_token']; for (var sensorCode in this.ecobeeAccessories) { var sensor = this.ecobeeAccessories[sensorCode] var homebridgeAccessory = sensor.homebridgeAccessory; homebridgeAccessory.context['access_token'] = this.accessToken; // This is a bit hackish... homebridgeAccessory.context['refresh_token'] = this.refreshToken; // This is a bit hackish... } if (callback) callback(); break; default: this.log.error(reply['error_description'] + " Re-requesting authorization!"); this.accessToken = null; this.refreshToken = null; this.pin(); break; } }.bind(this)); }.bind(this)); request.write(Querystring.stringify({ 'grant_type': 'refresh_token', 'code': this.refreshToken, 'client_id': this.appKey })); request.on('error', function (error) { this.log.error(error + " Re-requesting authorization!"); setTimeout(this.pin.bind(this), 1000); }.bind(this)); this.log.debug(request); request.end(); };