UNPKG

homebridge-smartthings-v2

Version:
415 lines (385 loc) 20.7 kB
const { pluginName, platformName, platformDesc, pluginVersion } = require("./libs/Constants"), events = require('events'), myUtils = require("./libs/MyUtils"), SmartThingsClient = require("./ST_Client"), SmartThingsAccessories = require("./ST_Accessories"), express = require("express"), bodyParser = require("body-parser"), chalk = require('chalk'), Logging = require("./libs/Logger"), webApp = express(), // os = require('os'), portFinderSync = require('portfinder-sync'); var PlatformAccessory; module.exports = class ST_Platform { constructor(log, config, api) { this.config = config; this.homebridge = api; this.Service = api.hap.Service; this.Characteristic = api.hap.Characteristic; PlatformAccessory = api.platformAccessory; this.uuid = api.hap.uuid; if (config === undefined || config === null || config.app_url === undefined || config.app_url === null || config.app_id === undefined || config.app_id === null) { log(`${platformName} Plugin is not Configured | Skipping...`); return; } this.ok2Run = true; this.direct_port = this.findDirectPort(); this.logConfig = this.getLogConfig(); this.appEvts = new events.EventEmitter(); this.logging = new Logging(this, this.config["name"], this.logConfig); this.log = this.logging.getLogger(); this.log.info(`Homebridge Version: ${api.version}`); this.log.info(`${platformName} Plugin Version: ${pluginVersion}`); this.polling_seconds = config.polling_seconds || 3600; this.excludedAttributes = this.config.excluded_attributes || []; this.excludedCapabilities = this.config.excluded_capabilities || []; this.update_method = this.config.update_method || "direct"; this.temperature_unit = this.config.temperature_unit || "F"; this.local_commands = this.config.local_commands || false; this.local_hub_ip = undefined; this.myUtils = new myUtils(this); this.configItems = this.getConfigItems(); this.unknownCapabilities = []; this.client = new SmartThingsClient(this); this.SmartThingsAccessories = new SmartThingsAccessories(this); this.homebridge.on("didFinishLaunching", this.didFinishLaunching.bind(this)); this.appEvts.emit('event:plugin_upd_status'); } getLogConfig() { let config = this.config; return (config.logConfig) ? { debug: (config.logConfig.debug === true), showChanges: (config.logConfig.showChanges === true), hideTimestamp: (config.logConfig.hideTimestamp === true), hideNamePrefix: (config.logConfig.hideNamePrefix === true), file: { enabled: (config.logConfig.file.enabled === true), level: (config.logConfig.file.level || 'good') } } : { debug: false, showChanges: true, hideTimestamp: false, hideNamePrefix: false }; } findDirectPort() { let port = this.config.direct_port || 8000; if (port) port = portFinderSync.getPort(port); return this.direct_port = port; } getConfigItems() { return { app_url: this.config.app_url, app_id: this.config.app_id, access_token: this.config.access_token, update_seconds: this.config.update_seconds || 30, direct_port: this.direct_port, direct_ip: this.config.direct_ip || this.myUtils.getIPAddress(), debug: (this.config.debug === true), local_commands: (this.config.local_commands === true), validateTokenId: (this.config.validateTokenId === true) }; } updateTempUnit(unit) { this.log.notice(`Temperature Unit is Now: (${unit})`); this.temperature_unit = unit; } getTempUnit() { return this.temperature_unit; } didFinishLaunching() { this.log.info(`Fetching ${platformName} Devices. NOTICE: This may take a moment if you have a large number of device data is being loaded!`); setInterval(this.refreshDevices.bind(this), this.polling_seconds * 1000); let that = this; this.refreshDevices('First Launch') .then(() => { that.WebServerInit(that) .catch(err => that.log.error("WebServerInit Error: ", err)) .then(resp => { if (resp && resp.status === "OK") this.appEvts.emit('event:plugin_start_direct');; }); }) .catch(err => { that.log.error(`didFinishLaunching | refreshDevices Exception:`, err); }); } refreshDevices(src = undefined) { let that = this; let starttime = new Date(); return new Promise((resolve, reject) => { try { that.log.good(`Refreshing All Device Data${src ? ' | Source: (' + src + ')' : ""}`); this.client.getDevices() .catch(err => { that.log.error('getDevices Exception:', err); reject(err.message); }) .then(resp => { if (resp && resp.location) { that.updateTempUnit(resp.location.temperature_scale); if (resp.location.hubIP) { that.local_hub_ip = resp.location.hubIP; that.local_commands = resp.location.local_commands === true; that.client.updateGlobals(that.local_hub_ip, that.local_commands); } } if (resp && resp.deviceList && resp.deviceList instanceof Array) { // that.log.debug("Received All Device Data"); const toCreate = this.SmartThingsAccessories.diffAdd(resp.deviceList); const toUpdate = this.SmartThingsAccessories.intersection(resp.deviceList); const toRemove = this.SmartThingsAccessories.diffRemove(resp.deviceList); that.log.warn(`Devices to Remove: (${Object.keys(toRemove).length})`, toRemove.map(i => i.name)); that.log.info(`Devices to Update: (${Object.keys(toUpdate).length})`); that.log.good(`Devices to Create: (${Object.keys(toCreate).length})`, toCreate.map(i => i.name)); toRemove.forEach(accessory => this.removeAccessory(accessory)); toUpdate.forEach(device => this.updateDevice(device)); toCreate.forEach(device => this.addDevice(device)); } that.log.alert(`Total Initialization Time: (${Math.round((new Date() - starttime) / 1000)} seconds)`); that.log.notice(`Unknown Capabilities: ${JSON.stringify(that.unknownCapabilities)}`); that.log.info(`${platformDesc} DeviceCache Size: (${Object.keys(this.SmartThingsAccessories.getAllAccessoriesFromCache()).length})`); if (src !== 'First Launch') this.appEvts.emit('event:plugin_upd_status'); resolve(true); }); } catch (ex) { this.log.error("refreshDevices Error: ", ex); resolve(false); } }); } getNewAccessory(device, UUID) { let accessory = new PlatformAccessory(device.name, UUID); accessory.context.deviceData = device; this.SmartThingsAccessories.initializeAccessory(accessory); return accessory; } addDevice(device) { let accessory; const new_uuid = this.uuid.generate(`smartthings_v2_${device.deviceid}`); device.excludedCapabilities = this.excludedCapabilities[device.deviceid] || []; this.log.debug(`Initializing New Device (${device.name} | ${device.deviceid})`); accessory = this.getNewAccessory(device, new_uuid); this.homebridge.registerPlatformAccessories(pluginName, platformName, [accessory]); this.SmartThingsAccessories.addAccessoryToCache(accessory); this.log.info(`Added Device: (${accessory.name} | ${accessory.deviceid})`); } updateDevice(device) { let cachedAccessory = this.SmartThingsAccessories.getAccessoryFromCache(device); device.excludedCapabilities = this.excludedCapabilities[device.deviceid] || []; cachedAccessory.context.deviceData = device; this.log.debug(`Loading Existing Device (${device.name}) | (${device.deviceid})`); cachedAccessory = this.SmartThingsAccessories.initializeAccessory(cachedAccessory); this.SmartThingsAccessories.addAccessoryToCache(cachedAccessory); } removeAccessory(accessory) { if (this.SmartThingsAccessories.removeAccessoryFromCache(accessory)) { this.homebridge.unregisterPlatformAccessories(pluginName, platformName, [accessory]); this.log.info(`Removed: ${accessory.context.name} (${accessory.context.deviceid})`); } } configureAccessory(accessory) { if (!this.ok2Run) return; this.log.debug(`Configure Cached Accessory: ${accessory.displayName}, UUID: ${accessory.UUID}`); let cachedAccessory = this.SmartThingsAccessories.initializeAccessory(accessory, true); this.SmartThingsAccessories.addAccessoryToCache(cachedAccessory); } processIncrementalUpdate(data, that) { that.log.debug("new data: " + data); if (data && data.attributes && data.attributes instanceof Array) { for (let i = 0; i < data.attributes.length; i++) { that.processDeviceAttributeUpdate(data.attributes[i], that); } } } isValidRequestor(access_token, app_id, src) { if (this.configItems.validateTokenId !== true) { return true; } if (app_id && access_token && (access_token === this.getConfigItems().access_token) && (app_id === this.getConfigItems().app_id)) return true; this.log.error(`(${src}) | We received a request from a client that didn't provide a valid access_token and app_id`); return false; } WebServerInit() { let that = this; // Get the IP address that we will send to the SmartApp. This can be overridden in the config file. return new Promise(resolve => { try { let ip = that.configItems.direct_ip || that.myUtils.getIPAddress(); that.log.info("WebServer Initiated..."); // Start the HTTP Server webApp.listen(that.configItems.direct_port, () => { that.log.info(`Direct Connect Active | Listening at ${ip}:${that.configItems.direct_port}`); }); webApp.use(bodyParser.urlencoded({ extended: false })); webApp.use(bodyParser.json()); webApp.use((req, res, next) => { res.header("Access-Control-Allow-Origin", "*"); res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); next(); }); webApp.get("/", (req, res) => { res.send("WebApp is running..."); }); webApp.post("/initial", (req, res) => { let body = JSON.parse(JSON.stringify(req.body)); if (body && that.isValidRequestor(body.access_token, body.app_id, 'initial')) { that.log.info(`${platformName} Hub Communication Established`); res.send({ status: "OK" }); } else { res.send({ status: "Failed: Missing access_token or app_id" }); } }); webApp.get("/debugOpts", (req, res) => { that.log.info(`${platformName} Debug Option Request(${req.query.option})...`); if (req.query && req.query.option) { let accs = this.SmartThingsAccessories.getAllAccessoriesFromCache(); // let accsKeys = Object.keys(accs); // console.log(accsKeys); switch (req.query.option) { case 'allAccData': res.send(JSON.stringify(accs)); break; // case 'accServices': // var o = accsKeys.forEach(s => s.services.forEach(s1 => s1.UUID)); // res.send(JSON.stringify(o)); // break; // case 'accCharacteristics': // var o = accsKeys.forEach(s => s.services.forEach(s1 => s1.characteristics.forEach(c => c.displayName))); // res.send(JSON.stringify(o)); // break; // case 'accContext': // res.send(JSON.stringify(this.SmartThingsAccessories.getAllAccessoriesFromCache())); // break; default: res.send(`Error: Invalid Option Parameter Received | Option: ${req.query.option}`); break; } } else { res.send('Error: Missing Valid Debug Query Parameter'); } }); webApp.post("/restartService", (req, res) => { let body = JSON.parse(JSON.stringify(req.body)); if (body && that.isValidRequestor(body.access_token, body.app_id, 'restartService')) { let delay = 10 * 1000; that.log.info(`Received request from ${platformName} to restart homebridge service in (${(delay / 1000)} seconds) | NOTICE: If you using PM2 or Systemd the Homebridge Service should start back up`); setTimeout(() => { process.exit(1); }, parseInt(delay)); res.send({ status: "OK" }); } else { res.send({ status: "Failed: Missing access_token or app_id" }); } }); webApp.post("/refreshDevices", (req, res) => { let body = JSON.parse(JSON.stringify(req.body)); if (body && that.isValidRequestor(body.access_token, body.app_id, 'refreshDevices')) { that.log.good(`Received request from ${platformName} to refresh devices`); that.refreshDevices("ST Requested"); res.send({ status: "OK" }); } else { that.log.error(`Unable to start device refresh because we didn't receive a valid access_token and app_id`); res.send({ status: "Failed: Missing access_token or app_id" }); } }); webApp.post("/updateprefs", (req, res) => { let body = JSON.parse(JSON.stringify(req.body)); if (body && that.isValidRequestor(body.access_token, body.app_id, 'updateprefs')) { that.log.info(platformName + " Hub Sent Preference Updates"); let sendUpd = false; if (body.local_commands && that.local_commands !== body.local_commands) { sendUpd = true; that.log.info(`${platformName} Updated Local Commands Preference | Before: ${that.local_commands} | Now: ${body.local_commands}`); that.local_commands = body.local_commands; } if (body.local_hub_ip && that.local_hub_ip !== body.local_hub_ip) { sendUpd = true; that.log.info(`${platformName} Updated Hub IP Preference | Before: ${that.local_hub_ip} | Now: ${body.local_hub_ip}`); that.local_hub_ip = body.local_hub_ip; } if (sendUpd) { that.client.updateGlobals(that.local_hub_ip, that.local_commands); } res.send({ status: "OK" }); } else { res.send({ status: "Failed: Missing access_token or app_id" }); } }); webApp.post("/update", (req, res) => { if (req.body.length < 3) return; let body = JSON.parse(JSON.stringify(req.body)); if (body && that.isValidRequestor(body.access_token, body.app_id, 'update')) { if (Object.keys(body).length > 3) { let newChange = { deviceid: body.change_device, attribute: body.change_attribute, value: body.change_value, data: body.change_data, date: body.change_date }; that.SmartThingsAccessories.processDeviceAttributeUpdate(newChange) .then((resp) => { if (that.logConfig.showChanges) { that.log.info(chalk `[{keyword('orange') Device Event}]: ({blueBright ${body.change_name}}) [{yellow.bold ${(body.change_attribute ? body.change_attribute.toUpperCase() : "unknown")}}] is {keyword('pink') ${body.change_value}}`); } res.send({ evtSource: `Homebridge_${platformName}_${this.configItems.app_id}`, evtType: 'attrUpdStatus', evtDevice: body.change_name, evtAttr: body.change_attribute, evtStatus: resp ? "OK" : "Failed" }); }); } else { res.send({ evtSource: `Homebridge_${platformName}_${this.configItems.app_id}`, evtType: 'attrUpdStatus', evtDevice: body.change_name, evtAttr: body.change_attribute, evtStatus: "Failed" }); } } else { res.send({ status: "Failed: Missing access_token or app_id" }); } }); resolve({ status: "OK" }); } catch (ex) { that.log.error('WebServerInit Exception: ', ex.message); resolve({ status: ex.message }); } }); } };