diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..e284467 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,45 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" + ], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "ignorePatterns": ["dist"], + "rules": { + "quotes": ["warn", "double", {"avoidEscape": true}], + "indent": [ + "warn", + 2, + { + "SwitchCase": 1 + } + ], + "linebreak-style": ["warn", "unix"], + "semi": ["warn", "always"], + "comma-dangle": ["warn", "always-multiline"], + "dot-notation": "warn", + "eqeqeq": "warn", + "curly": ["warn", "all"], + "brace-style": ["warn"], + "prefer-arrow-callback": ["warn"], + "max-len": ["warn", 140], + "no-console": ["warn"], + "lines-between-class-members": [ + "warn", + "always", + { + "exceptAfterSingleLine": true + } + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-this-alias": "off" + } +} diff --git a/.gitignore b/.gitignore index af50d98..f5c4034 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .DS_Store - node_modules/ - package-lock.json - dist/ +node_modules/ +package-lock.json +dist/ diff --git a/package.json b/package.json index cd428b6..4409678 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "scripts": { "clean": "rimraf ./dist", "build": "rimraf ./dist && tsc", - "prepublishOnly": "npm run build", + "lint": "eslint src/**.ts", + "prepublishOnly": "npm run lint && npm run build", "postpublish": "npm run clean", "test": "echo \"Error: no test specified\" && exit 1" }, @@ -41,10 +42,13 @@ }, "homepage": "https://github.com/hjdhjd/homebridge-myq2#readme", "devDependencies": { - "@types/node": "10.17.19", - "typescript": "^3.8.3", + "@types/node": "10.17.25", + "@typescript-eslint/eslint-plugin": "^3.5.0", + "@typescript-eslint/parser": "^3.5.0", + "eslint": "^7.4.0", + "homebridge": "^1.1.1", "rimraf": "^3.0.2", - "homebridge": "^1.0.4" + "typescript": "^3.9.6" }, "dependencies": { "@types/node-fetch": "^2.5.7", diff --git a/src/index.ts b/src/index.ts index 9824e79..b2a41fb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,7 +15,7 @@ import { PlatformConfig, } from "homebridge"; -import { myQ, myQDevice } from './myq'; +import { myQ, myQDevice } from "./myq"; const PLUGIN_NAME = "homebridge-myq2"; const PLATFORM_NAME = "myQ"; @@ -35,7 +35,7 @@ class myQPlatform implements DynamicPlatformPlugin { private readonly api: API; private myQ!: myQ; private myQOBSTRUCTED = 8675309; - + private configOptions: string[] = []; private configPoll = { @@ -45,7 +45,12 @@ class myQPlatform implements DynamicPlatformPlugin { closeDuration: 25, shortPollDuration: 600, maxCount: 0, - count: 0 + count: 0, + }; + + private configDevices = { + gateways: [], + openers: [], }; private pollingTimer!: NodeJS.Timeout; @@ -56,9 +61,9 @@ class myQPlatform implements DynamicPlatformPlugin { [hap.Characteristic.CurrentDoorState.OPENING]: "opening", [hap.Characteristic.CurrentDoorState.CLOSING]: "closing", [hap.Characteristic.CurrentDoorState.STOPPED]: "stopped", - [this.myQOBSTRUCTED]: "obstructed" + [this.myQOBSTRUCTED]: "obstructed", }; - + private readonly accessories: PlatformAccessory[] = []; constructor(log: Logging, config: PlatformConfig, api: API) { @@ -78,9 +83,9 @@ class myQPlatform implements DynamicPlatformPlugin { // Capture configuration parameters. if(config.options) { - this.configOptions = config.options; + this.configOptions = config.options; } - + if(config.longPoll) { this.configPoll.longPoll = config.longPoll; } @@ -131,9 +136,10 @@ class myQPlatform implements DynamicPlatformPlugin { // Add the garage door opener service to the accessory. const gdService = new hap.Service.GarageDoorOpener(accessory.displayName); - // The initial door state when we first startup. The bias functions will help us figure out what to do if we're caught in a tweener state. - var doorCurrentState = this.doorCurrentBias(accessory.context.doorState); - var doorTargetState = this.doorTargetBias(doorCurrentState); + // The initial door state when we first startup. The bias functions will help us + // figure out what to do if we're caught in a tweener state. + const doorCurrentState = this.doorCurrentBias(accessory.context.doorState); + const doorTargetState = this.doorTargetBias(doorCurrentState); // Add all the events to our accessory so we can act on HomeKit actions. We also set the current and target door states // based on our saved state from previous sessions. @@ -143,19 +149,19 @@ class myQPlatform implements DynamicPlatformPlugin { .setCharacteristic(hap.Characteristic.TargetDoorState, doorTargetState) .getCharacteristic(hap.Characteristic.TargetDoorState)! .on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - var myQState = this.doorStatus(accessory); + const myQState = this.doorStatus(accessory); // If we are already opening or closing the garage door, we error out. myQ doesn't appear to allow // interruptions to an open or close command that is currently executing - it must be allowed to - // complete it's action before accepting a new one. - if((myQState == hap.Characteristic.CurrentDoorState.OPENING) || (myQState == hap.Characteristic.CurrentDoorState.CLOSING)) { - var actionExisting = myQState == hap.Characteristic.CurrentDoorState.OPENING ? "opening" : "closing"; - var actionAttempt = value == hap.Characteristic.TargetDoorState.CLOSED ? "close" : "open"; + // complete its action before accepting a new one. + if((myQState === hap.Characteristic.CurrentDoorState.OPENING) || (myQState === hap.Characteristic.CurrentDoorState.CLOSING)) { + const actionExisting = myQState === hap.Characteristic.CurrentDoorState.OPENING ? "opening" : "closing"; + const actionAttempt = value === hap.Characteristic.TargetDoorState.CLOSED ? "close" : "open"; - this.log("%s - unable to %s door while currently trying to finish %s. myQ must complete it's existing action before attmepting a new one.", accessory.displayName, actionAttempt, actionExisting); + this.log("%s - unable to %s door while currently trying to finish %s. myQ must complete its existing action before attmepting a new one.", accessory.displayName, actionAttempt, actionExisting); callback(new Error("Unable to accept a new set event while another is completing.")); - } else if (value == hap.Characteristic.TargetDoorState.CLOSED) { + } else if(value === hap.Characteristic.TargetDoorState.CLOSED) { // HomeKit is informing us to close the door. this.log("%s is closing.", accessory.displayName); this.doorCommand(accessory, "close"); @@ -169,7 +175,7 @@ class myQPlatform implements DynamicPlatformPlugin { accessory .getService(hap.Service.GarageDoorOpener)! .setCharacteristic(hap.Characteristic.CurrentDoorState, hap.Characteristic.CurrentDoorState.CLOSING); - } else if (value == hap.Characteristic.TargetDoorState.OPEN) { + } else if(value === hap.Characteristic.TargetDoorState.OPEN) { // HomeKit is informing us to open the door. this.log("%s is opening.", accessory.displayName); this.doorCommand(accessory, "open"); @@ -187,43 +193,43 @@ class myQPlatform implements DynamicPlatformPlugin { } }); - // Add all the events to our accessory so we can tell HomeKit our state. - accessory - .getService(hap.Service.GarageDoorOpener)! - .getCharacteristic(hap.Characteristic.CurrentDoorState)! - .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - var err = null; - - // If the accessory is reachable, report back with status. Otherwise, appear as - // unreachable. - if(accessory.reachable) { - callback(err, this.doorStatus(accessory)) - } else { - callback(new Error("NO RESPONSE")); - } - }); + // Add all the events to our accessory so we can tell HomeKit our state. + accessory + .getService(hap.Service.GarageDoorOpener)! + .getCharacteristic(hap.Characteristic.CurrentDoorState)! + .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { + const err = null; + + // If the accessory is reachable, report back with status. Otherwise, appear as + // unreachable. + if(accessory.reachable) { + callback(err, this.doorStatus(accessory)); + } else { + callback(new Error("NO RESPONSE")); + } + }); - // Make sure we can detect obstructions. - accessory - .getService(hap.Service.GarageDoorOpener)! - .getCharacteristic(hap.Characteristic.ObstructionDetected)! - .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { - var err = null; + // Make sure we can detect obstructions. + accessory + .getService(hap.Service.GarageDoorOpener)! + .getCharacteristic(hap.Characteristic.ObstructionDetected)! + .on(CharacteristicEventTypes.GET, (callback: NodeCallback) => { + const err = null; - // If the accessory is reachable, report back with status. Otherwise, appear as - // unreachable. - if(accessory.reachable) { - var doorState = this.doorStatus(accessory); + // If the accessory is reachable, report back with status. Otherwise, appear as + // unreachable. + if(accessory.reachable) { + const doorState = this.doorStatus(accessory); - if(doorState == this.myQOBSTRUCTED) { - this.log("%s has detected an obstruction.", accessory.displayName); - } + if(doorState === this.myQOBSTRUCTED) { + this.log("%s has detected an obstruction.", accessory.displayName); + } - callback(err, doorState == this.myQOBSTRUCTED); - } else { - callback(new Error("NO RESPONSE")); - } - }); + callback(err, doorState === this.myQOBSTRUCTED); + } else { + callback(new Error("NO RESPONSE")); + } + }); // Add this to the accessory array so we can track it. this.accessories.push(accessory); @@ -231,51 +237,51 @@ class myQPlatform implements DynamicPlatformPlugin { // Sync our devies between HomeKit and what the myQ API is showing us. async myQUpdateDeviceList() { - // First we check if all the existing accessories we've cached still exist on the myQ API. // Login to myQ and refresh the full device list from the myQ API. - if(!await this.myQ.refreshDevices()) { + if(!(await this.myQ.refreshDevices())) { this.log("Unable to login to the myQ API. Will continue to retry at regular polling intervals."); return 0; - }; + } // Iterate through the list of devices that myQ has returned and sync them with what we show HomeKit. - this.myQ.Devices.forEach((device:myQDevice) => { + this.myQ.Devices.forEach((device: myQDevice) => { // If we have no serial number, something is wrong. if(!device.serial_number) { return; } // We are only interested in garage door openers. Perhaps more types in the future. - if(!device.device_type || (device.device_type.indexOf('garagedooropener') == -1)) { + if(!device.device_type || device.device_type.indexOf("garagedooropener") === -1) { return; } - + // Exclude or include certain openers based on configuration parameters. - if(!this.myQDeviceVisible(device)) { - return; - } + if(!this.myQDeviceVisible(device)) { + return; + } const uuid = hap.uuid.generate(device.serial_number); - var accessory; - var isNew = 0; + let accessory; + let isNew = 0; // See if we already know about this accessory or if it's truly new. - if((accessory = this.accessories.find((x: PlatformAccessory) => x.UUID === uuid)) == undefined) { + if((accessory = this.accessories.find((x: PlatformAccessory) => x.UUID === uuid)) === undefined) { isNew = 1; accessory = new Accessory("myQ " + device.name, uuid); } // Fun fact: This firmware information is stored on the gateway not the opener. - var gwParent = this.myQ.Devices.find((x: myQDevice) => x.serial_number === device.parent_device_id); - var fwVersion = "0.0"; + const gwParent = this.myQ.Devices.find((x: myQDevice) => x.serial_number === device.parent_device_id); + let fwVersion = "0.0"; if(gwParent && gwParent.state && gwParent.state.firmware_version) { fwVersion = gwParent.state.firmware_version; } // Now let's set (or update) the information on this accessory. - accessory.getService(hap.Service.AccessoryInformation)! + accessory + .getService(hap.Service.AccessoryInformation)! .setCharacteristic(hap.Characteristic.FirmwareRevision, fwVersion) .setCharacteristic(hap.Characteristic.Manufacturer, "Liftmaster") .setCharacteristic(hap.Characteristic.Model, "myQ") @@ -284,13 +290,13 @@ class myQPlatform implements DynamicPlatformPlugin { // Only add this device if we previously haven't added it to HomeKit. if(isNew) { this.log("Adding myQ %s device: %s (serial number: %s%s to HomeKit.", device.device_family, device.name, device.serial_number, - device.parent_device_id ? ", gateway: " + device.parent_device_id + ")" : ")"); + device.parent_device_id ? ", gateway: " + device.parent_device_id + ")" : ")"); this.configureAccessory(accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } else { // Refresh the accessory with these values. - this.api.updatePlatformAccessories([accessory]) + this.api.updatePlatformAccessories([accessory]); } // Not strictly needed, but helpful for non-default HomeKit apps. @@ -299,17 +305,18 @@ class myQPlatform implements DynamicPlatformPlugin { // Remove myQ devices that are no longer found in the myQ API, but we still have in HomeKit. this.accessories.forEach((oldAccessory: PlatformAccessory) => { - var device = this.myQ.getDevice(hap, oldAccessory.UUID); + const device = this.myQ.getDevice(hap, oldAccessory.UUID); - // We found this accessory in myQ and we want to see it in HomeKit. + // We found this accessory in myQ and we want to see it in HomeKit. if(device && this.myQDeviceVisible(device)) { return; } - - // Remove the device and inform the user about it. - this.log("Removing myQ %s device: %s (serial number: %s%s from HomeKit.", device.device_family, device.name, device.serial_number, - device.parent_device_id ? ", gateway: " + device.parent_device_id + ")" : ")"); + // Remove the device and inform the user about it. + this.log("Removing myQ %s device: %s (serial number: %s%s from HomeKit.", device.device_family, device.name, device.serial_number, + device.parent_device_id ? ", gateway: " + device.parent_device_id + ")" : ")"); + + this.log("Removing myQ device: %s", oldAccessory.displayName); this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [oldAccessory]); delete this.accessories[this.accessories.indexOf(oldAccessory)]; }); @@ -320,8 +327,7 @@ class myQPlatform implements DynamicPlatformPlugin { // Update HomeKit with the latest status from myQ. private async updateAccessories() { // Refresh our state from the myQ API. - if(!await this.myQ.refreshDevices()) { - + if(!(await this.myQ.refreshDevices())) { // We can't get a connection to the myQ API. Set all our accessories as unnreachable for now. this.accessories.forEach((accessory: PlatformAccessory) => { accessory.updateReachability(false); @@ -330,15 +336,14 @@ class myQPlatform implements DynamicPlatformPlugin { return 0; } - // Iterate through our accessories and update it's status with the corresponding myQ + // Iterate through our accessories and update its status with the corresponding myQ // status. this.accessories.forEach((accessory: PlatformAccessory) => { - var oldState = accessory.context.doorState; - var myQState = this.doorStatus(accessory); - var targetState; + const oldState = accessory.context.doorState; + const myQState = this.doorStatus(accessory); // If we can't get our status, we're probably not able to connect to the myQ API. - if(myQState == undefined) { + if(myQState === undefined) { this.log("Unable to retrieve status for device: %s", accessory.displayName); return; } @@ -346,20 +351,18 @@ class myQPlatform implements DynamicPlatformPlugin { // Mark us as reachable. accessory.updateReachability(true); - if(oldState != myQState) { + if(oldState !== myQState) { this.log("%s is %s.", accessory.displayName, this.myQStateMap[myQState as number]); } // Update the state in HomeKit. Thanks to @dxdc for suggesting looking at using updateValue // here instead of the more intuitive setCharacteristic due to inevitable race conditions and // set loops that can occur in HomeKit if you aren't careful. - accessory.getService(hap.Service.GarageDoorOpener) - ?.getCharacteristic(hap.Characteristic.CurrentDoorState)?.updateValue(myQState); + accessory.getService(hap.Service.GarageDoorOpener)?.getCharacteristic(hap.Characteristic.CurrentDoorState)?.updateValue(myQState); - targetState = this.doorTargetBias(myQState); + const targetState = this.doorTargetBias(myQState); - accessory.getService(hap.Service.GarageDoorOpener) - ?.getCharacteristic(hap.Characteristic.TargetDoorState)?.updateValue(targetState); + accessory.getService(hap.Service.GarageDoorOpener)?.getCharacteristic(hap.Characteristic.TargetDoorState)?.updateValue(targetState); }); // Check for any new or removed accessories from myQ. @@ -368,7 +371,7 @@ class myQPlatform implements DynamicPlatformPlugin { // Periodically poll the myQ API for status. private myQPolling(delay: number) { - var refresh = this.configPoll.longPoll + delay; + let refresh = this.configPoll.longPoll + delay; // Clear the last polling interval out. clearTimeout(this.pollingTimer); @@ -379,15 +382,15 @@ class myQPlatform implements DynamicPlatformPlugin { // shortPollDuration and shortPoll which specify the maximum length of time for this // increased polling frequency (shortPollDuration) and the actual frequency of each // update (shortPoll). - if(this.configPoll.count < this.configPoll.maxCount) { + if(this.configPoll.count < this.configPoll.maxCount) { this.configPoll.count++; refresh = this.configPoll.shortPoll + delay; } // Setup periodic update with our polling interval. - var self = this; + const self = this; - this.pollingTimer = setTimeout(async function() { + this.pollingTimer = setTimeout(async () => { // Refresh our myQ information and gracefully handle myQ errors. if(!self.updateAccessories()) { self.log("Polling error: unable to connect to the myQ API."); @@ -401,7 +404,6 @@ class myQPlatform implements DynamicPlatformPlugin { // Return the status of the door for an accessory. It maps myQ door status to HomeKit door status. private doorStatus(accessory: PlatformAccessory): CharacteristicValue { - // Door state cheat sheet. // autoreverse is how the myQ API communicated an obstruction...go figure. Unfortunately, it // only seems to last the duration of the door reopening (reversal). @@ -411,19 +413,19 @@ class myQPlatform implements DynamicPlatformPlugin { opening: hap.Characteristic.CurrentDoorState.OPENING, closing: hap.Characteristic.CurrentDoorState.CLOSING, stopped: hap.Characteristic.CurrentDoorState.STOPPED, - autoreverse: this.myQOBSTRUCTED + autoreverse: this.myQOBSTRUCTED, }; - var device = this.myQ.getDevice(hap, accessory.UUID); + const device = this.myQ.getDevice(hap, accessory.UUID); if(!device) { this.log("Can't find device: %s - %s", accessory.displayName, accessory.UUID); return 0; } - var myQState = doorStates[device.state.door_state]; + const myQState = doorStates[device.state.door_state]; - if(myQState == undefined) { + if(myQState === undefined) { this.log("Unknown door state encountered on myQ device %s: %s", device.name, device.state.door_state); return 0; } @@ -436,21 +438,20 @@ class myQPlatform implements DynamicPlatformPlugin { // Open or close the door for an accessory. private doorCommand(accessory: PlatformAccessory, command: string) { - // myQ commands and the associated polling intervals to go with them. const myQCommandPolling: {[index: string]: number} = { - open: this.configPoll.openDuration, - close: this.configPoll.closeDuration + open: this.configPoll.openDuration, + close: this.configPoll.closeDuration, }; - var device = this.myQ.getDevice(hap, accessory.UUID); + const device = this.myQ.getDevice(hap, accessory.UUID); if(!device) { this.log("Can't find device: %s - %s", accessory.displayName, accessory.UUID); return; } - if(myQCommandPolling[command] == undefined) { + if(myQCommandPolling[command] === undefined) { this.log("Unknown door commmand encountered on myQ device %s: %s", device.name, command); return; } @@ -469,20 +470,17 @@ class myQPlatform implements DynamicPlatformPlugin { // our target state needs to be the completion of those actions. If we're stopped or // obstructed, we're going to assume the desired target state is to be open, since that // is the typical garage door behavior. - if(myQState == hap.Characteristic.CurrentDoorState.OPEN) { - return hap.Characteristic.CurrentDoorState.OPEN; - } else if(myQState == hap.Characteristic.CurrentDoorState.CLOSED) { - return hap.Characteristic.CurrentDoorState.CLOSED; - } else if(myQState == hap.Characteristic.CurrentDoorState.OPENING) { - return hap.Characteristic.CurrentDoorState.OPEN; - } else if(myQState == hap.Characteristic.CurrentDoorState.CLOSING) { - return hap.Characteristic.CurrentDoorState.CLOSED; - } else if(myQState == hap.Characteristic.CurrentDoorState.STOPPED) { - return hap.Characteristic.CurrentDoorState.OPEN; - } else if(myQState == this.myQOBSTRUCTED) { - return hap.Characteristic.CurrentDoorState.OPEN; - } else { - return hap.Characteristic.CurrentDoorState.CLOSED; + switch(myQState) { + case hap.Characteristic.CurrentDoorState.OPEN: + case hap.Characteristic.CurrentDoorState.OPENING: + case hap.Characteristic.CurrentDoorState.STOPPED: + case this.myQOBSTRUCTED: + return hap.Characteristic.CurrentDoorState.OPEN; + + case hap.Characteristic.CurrentDoorState.CLOSED: + case hap.Characteristic.CurrentDoorState.CLOSING: + default: + return hap.Characteristic.CurrentDoorState.CLOSED; } } @@ -493,23 +491,20 @@ class myQPlatform implements DynamicPlatformPlugin { // our target state needs to be the completion of those actions. If we're stopped or // obstructed, we're going to assume the desired target state is to be open, since that // is the typical garage door behavior. - if(myQState == hap.Characteristic.CurrentDoorState.OPEN) { - return hap.Characteristic.TargetDoorState.OPEN; - } else if(myQState == hap.Characteristic.CurrentDoorState.CLOSED) { - return hap.Characteristic.TargetDoorState.CLOSED; - } else if(myQState == hap.Characteristic.CurrentDoorState.OPENING) { - return hap.Characteristic.TargetDoorState.OPEN; - } else if(myQState == hap.Characteristic.CurrentDoorState.CLOSING) { - return hap.Characteristic.TargetDoorState.CLOSED; - } else if(myQState == hap.Characteristic.CurrentDoorState.STOPPED) { - return hap.Characteristic.TargetDoorState.OPEN; - } else if(myQState == this.myQOBSTRUCTED) { - return hap.Characteristic.TargetDoorState.OPEN; - } else { - return hap.Characteristic.TargetDoorState.CLOSED; + switch(myQState) { + case hap.Characteristic.CurrentDoorState.OPEN: + case hap.Characteristic.CurrentDoorState.OPENING: + case hap.Characteristic.CurrentDoorState.STOPPED: + case this.myQOBSTRUCTED: + return hap.Characteristic.TargetDoorState.OPEN; + + case hap.Characteristic.CurrentDoorState.CLOSED: + case hap.Characteristic.CurrentDoorState.CLOSING: + default: + return hap.Characteristic.TargetDoorState.CLOSED; } } - + // Utility function to let us know if a my! device should be visible in HomeKit or not. private myQDeviceVisible(device: myQDevice): boolean { // There are a couple of ways to hide and show devices that we support. The rules of the road are: @@ -518,47 +513,47 @@ class myQPlatform implements DynamicPlatformPlugin { // into that gateway. So if you have multiple gateways but only want one exposed in this plugin, // you may do so by hiding it. // - // 2. Explicitly hiding, or showing an opener device by it's serial number will always override the above. + // 2. Explicitly hiding, or showing an opener device by its serial number will always override the above. // This means that it's possible to hide a gateway, and all the openers that are attached to it, and then // override that behavior on a single opener device that it's connected to. - // - - // Nothing configured - we show all myQ devices to HomeKit. - if(!this.configOptions) { - return true; - } - - // No device. Sure, we'll show it. - if(!device) { - return true; - } - - // We've explicitly enabled this opener. - if(this.configOptions.indexOf("Show." + (device.serial_number as any)) != -1) { - return true; - } - - // We've explicitly hidden this opener. - if(this.configOptions.indexOf("Hide." + device.serial_number) != -1) { - return false; - } - - // If we don't have a gateway device attached to this opener, we're done here. - if(!device.parent_device_id) { - return true; - } - - // We've explicitly shown the gateway this opener is attached to. - if(this.configOptions.indexOf("Show." + device.parent_device_id) != -1) { - return true; - } - - // We've explicitly hidden the gateway this opener is attached to. - if(this.configOptions.indexOf("Hide." + device.parent_device_id) != -1) { - return false; - } - - // Nothing special to do - make this opener visible. - return true; + // + + // Nothing configured - we show all myQ devices to HomeKit. + if(!this.configOptions) { + return true; + } + + // No device. Sure, we'll show it. + if(!device) { + return true; + } + + // We've explicitly enabled this opener. + if(this.configOptions.indexOf("Show." + (device.serial_number as any)) !== -1) { + return true; + } + + // We've explicitly hidden this opener. + if(this.configOptions.indexOf("Hide." + device.serial_number) !== -1) { + return false; + } + + // If we don't have a gateway device attached to this opener, we're done here. + if(!device.parent_device_id) { + return true; + } + + // We've explicitly shown the gateway this opener is attached to. + if(this.configOptions.indexOf("Show." + device.parent_device_id) !== -1) { + return true; + } + + // We've explicitly hidden the gateway this opener is attached to. + if(this.configOptions.indexOf("Hide." + device.parent_device_id) !== -1) { + return false; + } + + // Nothing special to do - make this opener visible. + return true; } } diff --git a/src/myq.ts b/src/myq.ts index c50848b..d155fc7 100644 --- a/src/myq.ts +++ b/src/myq.ts @@ -1,22 +1,11 @@ /* Copyright(C) 2020, HJD (https://github.com/hjdhjd). All rights reserved. */ -import { - API, - CharacteristicEventTypes, - CharacteristicGetCallback, - CharacteristicSetCallback, - CharacteristicValue, - HAP, - IndependentPlatformPlugin, - Logging, - PlatformAccessory, - PlatformConfig -} from "homebridge"; - -import fetch, { Response } from 'node-fetch'; -import util from 'util'; - -// An incomplete description of the myQ JSON, but enough for our purposes. +import { HAP, Logging } from "homebridge"; + +import fetch from "node-fetch"; +import util from "util"; + +// An incomplete description of the myQ JSON, but enough for our purposes. export interface myQDevice { readonly device_family: string, readonly device_platform: string, @@ -31,7 +20,7 @@ export interface myQDevice { } } -var debug = 0; +let debug = 0; /* * myQ API version information. This is more intricate than it seems because the myQ @@ -41,17 +30,17 @@ var debug = 0; */ const myqVersionMajor = 5; const myqVersionMinor = 1; -const myqVersion = myqVersionMajor + '.' + myqVersionMinor; +const myqVersion = myqVersionMajor + "." + myqVersionMinor; // myQ API base URL, currently v5. -const myqApi = 'https://api.myqdevice.com/api/v' + myqVersionMajor; +const myqApi = "https://api.myqdevice.com/api/v" + myqVersionMajor; // myQ API devices URL, currently v5.1 -const myqApidev = myqApi + '.' + myqVersionMinor; +const myqApidev = myqApi + "." + myqVersionMinor; // myQ app identifier and user agent used to validate against the myQ API. -const myqAppId = 'JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu'; -const myqAgent = 'okhttp/3.10.0'; +const myqAppId = "JVM/G9Nwih5BwKgNCjLxiFUQxQijAebyyg8QUHr7JOrP+tuPb8iHfRHKwTmDzHOu"; +const myqAgent = "okhttp/3.10.0"; /* * The myQ API is undocumented, non-public, and has been derived largely through @@ -94,11 +83,11 @@ export class myQ { private myqHeaders = { "Content-Type": "application/json", "User-Agent": myqAgent, - "ApiVersion": myqVersion, - "BrandId": "2", - "Culture": "en", - "MyQApplicationId": myqAppId, - "SecurityToken": "" + ApiVersion: myqVersion, + BrandId: "2", + Culture: "en", + MyQApplicationId: myqAppId, + SecurityToken: "", }; // Initialize this instance with our login information. @@ -112,16 +101,12 @@ export class myQ { // Log us into myQ and get a security token. private async myqAuthenticate() { - var response, data; - // Login to the myQ API and get a security token for our session. - response = await this.myqFetch(myqApi + '/Login', - { - method: "POST", - headers: this.myqHeaders, - body: JSON.stringify({UserName: this.Email, Password: this.Password}) - } - ); + const response = await this.myqFetch(myqApi + "/Login", { + method: "POST", + headers: this.myqHeaders, + body: JSON.stringify({ UserName: this.Email, Password: this.Password }), + }); if(!response) { this.log("myQ API error: unable to authenticate. Will retry later."); @@ -129,7 +114,7 @@ export class myQ { } // Now let's get our security token. - data = await response.json(); + const data = await response.json(); if(debug) { this.log(data); @@ -151,29 +136,25 @@ export class myQ { } // Add the token to our headers that we will use for subsequent API calls. - this.myqHeaders["SecurityToken"] = this.securityToken; + this.myqHeaders.SecurityToken = this.securityToken; return 1; } // Login and get our account information. async login() { - var response, data, params; - // If we don't have a security token yet, acquire one before proceeding. - if(!this.securityToken && !await this.myqAuthenticate()) { - return 0; + if(!this.securityToken && !(await this.myqAuthenticate())) { + return 0; } // Get the account information. - params = new URLSearchParams({ expand: 'account' }); + const params = new URLSearchParams({ expand: "account" }); - response = await this.myqFetch(myqApi + '/My?' + params, - { - method: 'GET', - headers: this.myqHeaders - } - ); + const response = await this.myqFetch(myqApi + "/My?" + params, { + method: "GET", + headers: this.myqHeaders, + }); if(!response) { this.log("myQ API error: unable to login. Will retry later."); @@ -181,7 +162,7 @@ export class myQ { } // Now let's get our account information. - data = await response.json(); + const data = await response.json(); if(debug) { this.log(data); @@ -189,7 +170,7 @@ export class myQ { // No account information returned. if(!data || !data.Account) { - this.log("Unable to retrieve account information from myQ servers.") + this.log("Unable to retrieve account information from myQ servers."); return 0; } @@ -205,43 +186,34 @@ export class myQ { // Get the list of myQ devices associated with an account. async refreshDevices() { - var items: Array; - var response, data, params; - var now = Date.now(); + const now = Date.now(); // We want to throttle how often we call this API as a failsafe. If we call it more // than once every five seconds or so, bad things can happen on the myQ side leading - // to potential accounnt lockouts. The author was definitely learned this one the - // hard way. - if(this.lastCall && (now - this.lastCall) < (5 * 1000)) { + // to potential account lockouts. The author definitely learned this one the hard way. + if(this.lastCall && ((now - this.lastCall) < (5*1000))) { if(debug) { this.log("Throttling myQ API call."); } - if(!this.Devices) { - return 0 - } - - return 1; + return this.Devices ? 1 : 0; } // Reset the API call time. this.lastCall = now; // If we don't have our account information yet, acquire it before proceeding. - if(!this.accountID && !await this.login()) { - return 0; + if(!this.accountID && !(await this.login())) { + return 0; } // Get the list of device information. - params = new URLSearchParams({ filterOn: 'true' }); + const params = new URLSearchParams({ filterOn: "true" }); - response = await this.myqFetch(myqApidev + '/Accounts/' + this.accountID + '/Devices?' + params, - { - method: 'GET', - headers: this.myqHeaders - } - ); + const response = await this.myqFetch(myqApidev + "/Accounts/" + this.accountID + "/Devices?" + params, { + method: "GET", + headers: this.myqHeaders, + }); if(!response) { this.log("myQ API error: unable to refresh. Will retry later."); @@ -249,29 +221,27 @@ export class myQ { } // Now let's get our account information. - data = await response.json(); + const data = await response.json(); if(debug) { this.log(data); } - items = data.items; + const items: Array = data.items; // Notify the user about any new devices that we've discovered. if(items) { items.forEach((newDevice: any) => { - var existingDevice; - if(this.Devices) { // We already know about this device. - if((existingDevice = this.Devices.find((x: any) => x.serial_number === newDevice.serial_number)) != undefined) { + if(this.Devices.find((x: any) => x.serial_number === newDevice.serial_number) !== undefined) { return; } } // We've discovered a new device. this.log("myQ %s device discovered: %s (serial number: %s%s.", newDevice.device_family, newDevice.name, newDevice.serial_number, - newDevice.parent_device_id ? ", gateway: " + newDevice.parent_device_id + ")" : ")"); + newDevice.parent_device_id ? ", gateway: " + newDevice.parent_device_id + ")" : ")"); if(debug) { this.log(util.inspect(newDevice, { colors: true, sorted: true, depth: 3 })); @@ -282,11 +252,9 @@ export class myQ { // Notify the user about any devices that have disappeared. if(this.Devices) { this.Devices.forEach((existingDevice: any) => { - var newDevice; - if(items) { // This device still is visible. - if((newDevice = items.find((x: any) => x.serial_number === existingDevice.serial_number)) != undefined) { + if(items.find((x: any) => x.serial_number === existingDevice.serial_number) !== undefined) { return; } } @@ -308,22 +276,18 @@ export class myQ { // Query the details of a specific myQ device. async queryDevice(log: Logging, deviceId: string) { - var response, data; - // If we don't have our account information yet, acquire it before proceeding. - if(!this.accountID && !await this.login()) { - return 0; + if(!this.accountID && !(await this.login())) { + return 0; } debug = 1; // Get the list of device information. - response = await this.myqFetch(myqApidev + '/Accounts/' + this.accountID + '/devices/' + deviceId, - { - method: 'GET', - headers: this.myqHeaders - } - ); + const response = await this.myqFetch(myqApidev + "/Accounts/" + this.accountID + "/devices/" + deviceId, { + method: "GET", + headers: this.myqHeaders, + }); if(!response) { this.log("myQ API error: unable to query device. Will retry later."); @@ -331,7 +295,7 @@ export class myQ { } // Now let's get our account information. - data = await response.json(); + const data = await response.json(); if(!data || !data.items) { log("Error querying device '%s'", deviceId); @@ -356,21 +320,16 @@ export class myQ { // Execute an action on a myQ device. async execute(deviceId: string, command: string) { - var response; - // If we don't have our account information yet, acquire it before proceeding. - if(!this.accountID && !await this.login()) { - return 0; + if(!this.accountID && !(await this.login())) { + return 0; } - response = await this.myqFetch(myqApidev + '/Accounts/' + this.accountID + '/Devices/' + - deviceId + '/actions', - { - method: 'PUT', - headers: this.myqHeaders, - body: JSON.stringify({action_type: command}) - } - ); + const response = await this.myqFetch(myqApidev + "/Accounts/" + this.accountID + "/Devices/" + deviceId + "/actions", { + method: "PUT", + headers: this.myqHeaders, + body: JSON.stringify({ action_type: command }), + }); if(!response) { this.log("myQ API error: unable to execute command."); @@ -382,8 +341,8 @@ export class myQ { // Get the details of a specific device in our list. getDevice(hap: HAP, uuid: string) { - var device : any; - var now = Date.now(); + let device: any; + const now = Date.now(); // Check to make sure we have fresh information from myQ. If it's less than a minute // old, it looks good to us. @@ -394,9 +353,15 @@ export class myQ { // Iterate through the list and find the device that matches the UUID we seek. // This works because homebridge always generates the same UUID for a given input - // in this case the device serial number. - if((device = this.Devices.find((x: any) => - x.device_type && (x.device_type.indexOf('garagedooropener') != -1) && - x.serial_number && hap.uuid.generate(x.serial_number) === uuid)) != undefined) { + if( + (device = this.Devices.find( + (x: any) => + x.device_type && + x.device_type.indexOf("garagedooropener") !== -1 && + x.serial_number && + hap.uuid.generate(x.serial_number) === uuid, + )) !== undefined + ) { return device; } @@ -404,14 +369,14 @@ export class myQ { } // Utility to let us streamline error handling and return checking from the myQ API. - private async myqFetch (url: string, options: any) { - var response; + private async myqFetch(url: string, options: any) { + let response; try { response = await fetch(url, options); // Bad username and password. - if(response.status == 401) { + if(response.status === 401) { this.log("Invalid username or password given. Check your login and password."); return undefined; }