├── .npmignore ├── .gitignore ├── src ├── instance.js ├── index.js ├── chamberlain-accessory.js └── api.js ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── feature-request.md │ ├── issue_template.md │ └── bug-report.md └── issue_template.md ├── package.json ├── config.schema.json ├── config-example.MD └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /src/instance.js: -------------------------------------------------------------------------------- 1 | // This module simply serves as a bucket to store the homebridge instance. 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 3 | about: 4 | title: Feature Name Here 5 | labels: featureRequest 6 | assignees: iRayanKhan 7 | 8 | --- 9 | 10 | # Explain the feature you want added 11 | (Give a description of the feature you want added.) 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Describe what you would like to be added 4 | title: Feature Name Here 5 | labels: featureRequest 6 | assignees: iRayanKhan 7 | 8 | --- 9 | 10 | # Explain the feature you want added 11 | (Give a description of the feature you want added.) 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const ChamberlainAccessory = require('./chamberlain-accessory'); 2 | const instance = require('./instance'); 3 | 4 | module.exports = homebridge => { 5 | instance.homebridge = homebridge; 6 | 7 | homebridge.registerAccessory( 8 | 'homebridge-chamberlain', 9 | 'Chamberlain', 10 | ChamberlainAccessory 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Issue Name' 3 | labels: bug 4 | assignees: iRayanKhan 5 | --- 6 | # Explain the issue occuring: 7 | (Give a description of the occuring behaviour.) 8 | 9 | # Expected Result 10 | (Give a description of the expected behaviour.) 11 | 12 | # Info 13 | iOS Version: 14 | Plugin Version: 15 | HomeBridge Version: 16 | Node/Npm Version: 17 | 18 | # Log Output: 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 3 | about: 4 | title: Issue Name 5 | labels: bug 6 | assignees: iRayanKhan 7 | 8 | --- 9 | 10 | # Explain the issue occuring: 11 | (Give a description of the occuring behaviour.) 12 | 13 | # Expected Result 14 | (Give a description of the expected behaviour.) 15 | 16 | # Info 17 | iOS Version: 18 | Plugin Version: 19 | HomeBridge Version: 20 | Node/Npm Version: 21 | 22 | # Log Output: 23 | (If applicable) 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-chamberlain", 3 | "version": "1.6.1", 4 | "author": "Rayan Khan ", 5 | "license": "MIT", 6 | "main": "src", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/iRayanKhan/homebridge-chamberlain" 10 | }, 11 | "keywords": [ 12 | "homebridge-plugin" 13 | ], 14 | "dependencies": { 15 | "node-fetch": "2", 16 | "underscore": "1.8.3" 17 | }, 18 | "engines": { 19 | "homebridge": "x" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: If something isn't going as expected, file an issue here. 4 | title: Issue Name 5 | labels: bug 6 | assignees: iRayanKhan 7 | 8 | --- 9 | 10 | # Explain the issue occuring: 11 | (Give a description of the occuring behaviour.) 12 | 13 | # Expected Result 14 | (Give a description of the expected behaviour.) 15 | 16 | # Info 17 | iOS Version: 18 | Plugin Version: 19 | HomeBridge Version: 20 | Node/Npm Version: 21 | 22 | # Log Output: 23 | (If applicable) 24 | -------------------------------------------------------------------------------- /config.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginAlias": "Chamberlain", 3 | "pluginType": "accessory", 4 | "singular": false, 5 | "footerDisplay": "If you have multiple garage doors, the plugin will throw an error and list the controllable device IDs. Use those IDs to create individual accessories. Be sure to uniquely name each door.", 6 | "schema": { 7 | "type": "object", 8 | "properties": { 9 | "name": { 10 | "title": "Name", 11 | "type": "string", 12 | "default": "Garage Door", 13 | "required": true 14 | }, 15 | "username": { 16 | "title": "Email", 17 | "type": "string", 18 | "required": true, 19 | "description": "Your MyChamberlain.com email." 20 | }, 21 | "password": { 22 | "title": "Password", 23 | "type": "string", 24 | "required": true, 25 | "description": "Your MyChamberlain.com password." 26 | }, 27 | "deviceId": { 28 | "title": "Device ID", 29 | "type": "string", 30 | "description": "For a single door, the Device ID will be autodetected." 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config-example.MD: -------------------------------------------------------------------------------- 1 | # Config 2 | Note: Run Homebridge with the deviceID's blank to generate the ID's. 3 | If you get multiple ID's or it says it's invalid, use the one with CG. 4 | ```json 5 | { 6 | "accessory": "Chamberlain", 7 | "name": "Garage Door", 8 | "username": "your mychamberlain.com email", 9 | "password": "your mychamberlain.com password" 10 | } 11 | ``` 12 | 13 | If you have multiple garage doors, the plugin will throw an error and list the controllable device IDs. Use those IDs to create individual accessories. Be sure to uniquely name the door via the "name" field, otherwise you'll get a UUID error in the console (`Error: Cannot add a bridged Accessory with the same UUID as another bridged Accessory`). 14 | 15 | ```json 16 | { 17 | "accessory": "Chamberlain", 18 | "name": "Main Garage Door", 19 | "username": "your mychamberlain.com email", 20 | "password": "your mychamberlain.com password", 21 | "deviceId": "xxx" 22 | }, 23 | { 24 | "accessory": "Chamberlain", 25 | "name": "Side Garage Door", 26 | "username": "your mychamberlain.com email", 27 | "password": "your mychamberlain.com password", 28 | "deviceId": "xxx" 29 | }, 30 | ... 31 | ``` 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | homebridge-verified 4 | 5 | # Homebridge-Chamberlain 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | # Note: 15 | This plugin has now been deprecated. Please use [this](https://github.com/hjdhjd/homebridge-myq) plugin. 16 | 17 | # Installation 18 | 1) Install Homebridge: ```sudo npm i -g homebridge --unsafe-perm``` 19 | 2) Download this plugin: ```sudo npm i -g homebridge-chamberlain``` 20 | 3) Add the [config parameters](https://github.com/iRayanKhan/homebridge-chamberlain/blob/master/config-example.MD) to your [config.json](https://github.com/nfarina/homebridge/blob/master/config-sample.json) file. 21 | 4) Run the plugin without the ```deviceID``` field to generate your deviceID's 22 | 5) Add the ```deviceID's``` 23 | 6) Restart Homebridge 24 | 25 | # Issues 26 | If you experience any issues, please check the [common issues page](https://github.com/iRayanKhan/homebridge-chamberlain/wiki/Common-Issues) before opening an issue. 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/chamberlain-accessory.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const Api = require('./api'); 3 | const instance = require('./instance'); 4 | 5 | const ACTIVE_DELAY = 1000 * 2; 6 | const IDLE_DELAY = 1000 * 30; 7 | 8 | module.exports = class { 9 | constructor(log, {deviceId, name, password, username}) { 10 | this.log = log; 11 | this.api = new Api({MyQDeviceId: deviceId, password, username}); 12 | 13 | const {Service, Characteristic} = instance.homebridge.hap; 14 | const {CurrentDoorState, TargetDoorState} = Characteristic; 15 | 16 | this.apiToHap = { 17 | 'open': CurrentDoorState.OPEN, 18 | 'closed': CurrentDoorState.CLOSED, 19 | }; 20 | 21 | this.hapToApi = { 22 | [TargetDoorState.OPEN]: 'open', 23 | [TargetDoorState.CLOSED]: 'close' 24 | }; 25 | 26 | this.hapToEnglish = { 27 | [CurrentDoorState.OPEN]: 'open', 28 | [CurrentDoorState.CLOSED]: 'closed', 29 | [CurrentDoorState.OPENING]: 'opening', 30 | [CurrentDoorState.CLOSING]: 'closing' 31 | }; 32 | 33 | this.currentToTarget = { 34 | [CurrentDoorState.OPEN]: TargetDoorState.OPEN, 35 | [CurrentDoorState.CLOSED]: TargetDoorState.CLOSED, 36 | [CurrentDoorState.OPENING]: TargetDoorState.OPEN, 37 | [CurrentDoorState.CLOSING]: TargetDoorState.CLOSED 38 | }; 39 | 40 | const service = this.service = new Service.GarageDoorOpener(name); 41 | 42 | this.states = { 43 | doorstate: 44 | service 45 | .getCharacteristic(Characteristic.CurrentDoorState) 46 | .on('get', this.getCurrentDoorState.bind(this)) 47 | .on('change', this.logChange.bind(this, 'doorstate')), 48 | desireddoorstate: 49 | service 50 | .getCharacteristic(Characteristic.TargetDoorState) 51 | .on('set', this.setTargetDoorState.bind(this)) 52 | .on('change', this.logChange.bind(this, 'desireddoorstate')) 53 | }; 54 | 55 | this.states.doorstate.value = CurrentDoorState.CLOSED; 56 | this.states.desireddoorstate.value = TargetDoorState.CLOSED; 57 | 58 | (this.poll = this.poll.bind(this))(); 59 | } 60 | 61 | poll() { 62 | clearTimeout(this.pollTimeoutId); 63 | const {doorstate, desireddoorstate} = this.states; 64 | return new Promise((resolve, reject) => 65 | doorstate.getValue(er => er ? reject(er) : resolve()) 66 | ).then(() => 67 | doorstate.value !== desireddoorstate.value ? ACTIVE_DELAY : IDLE_DELAY 68 | ).catch(_.noop).then((delay = IDLE_DELAY) => { 69 | clearTimeout(this.pollTimeoutId); 70 | this.pollTimeoutId = setTimeout(this.poll, delay); 71 | }); 72 | } 73 | 74 | logChange(name, {oldValue, newValue}) { 75 | const from = this.hapToEnglish[oldValue]; 76 | const to = this.hapToEnglish[newValue]; 77 | this.log.info(`${name} changed from ${from} to ${to}`); 78 | 79 | if (name === 'doorstate') { 80 | this.reactiveSetTargetDoorState = true; 81 | this.states.desireddoorstate.updateValue(this.currentToTarget[newValue]); 82 | delete this.reactiveSetTargetDoorState; 83 | } 84 | } 85 | 86 | getErrorHandler(cb) { 87 | return er => { 88 | this.log.error(er); 89 | cb(er); 90 | }; 91 | } 92 | 93 | getCurrentDoorState(cb) { 94 | return this.api.getDeviceAttribute({name: 'door_state'}) 95 | .then(value =>{ 96 | cb(null, this.apiToHap[value]) 97 | }) 98 | .catch(this.getErrorHandler(cb)); 99 | } 100 | 101 | setTargetDoorState(value, cb) { 102 | if (this.reactiveSetTargetDoorState) return cb(); 103 | 104 | const action_type = this.hapToApi[value]; 105 | this.targetDoorState = value; 106 | 107 | return this.api.actOnDevice({action_type}) 108 | .then(() => { 109 | this.poll(); 110 | this.targetDoorState = null; 111 | cb(); 112 | }) 113 | .catch(this.getErrorHandler(cb)); 114 | } 115 | 116 | getServices() { 117 | return [this.service]; 118 | } 119 | }; 120 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const _ = require('underscore'); 2 | const fetch = require('node-fetch'); 3 | const url = require('url'); 4 | 5 | const MyQApplicationId = 6 | 'Vj8pQggXLhLy0WHahglCD4N1nAkkXQtGYpq2HrHD7H1nvmbT55KqtN6RSF4ILB/i'; 7 | const protocol = 'https:'; 8 | const host = 'api.myqdevice.com'; 9 | 10 | const GATEWAY_ID = 1; 11 | 12 | const req = ({body, headers, method, pathname, query}) => 13 | fetch(url.format({host, pathname, protocol, query}), { 14 | body: body == null ? body : JSON.stringify(body), 15 | headers: _.extend({ 16 | 'Content-Type': 'application/json', 17 | 'User-Agent': 'myQ/19569 CFNetwork/1107.1 Darwin/19.0.0', 18 | ApiVersion: '5.1', 19 | BrandId: '2', 20 | Culture: 'en', 21 | MyQApplicationId 22 | }, headers), 23 | method 24 | }).then((res) => { 25 | if (res.status < 200 || res.status >= 300) { 26 | return res.text().then(body => { 27 | throw new Error('invalid response, got HTTP ' + res.status + ': ' + body) 28 | }) 29 | } 30 | return res.text() 31 | }).then(data => { 32 | if (data) { 33 | return JSON.parse(data); 34 | } else { 35 | return null 36 | } 37 | }); 38 | 39 | module.exports = class { 40 | constructor(options = {}) { 41 | this.options = options; 42 | } 43 | 44 | getSecurityToken(options = {}) { 45 | options = _.extend({}, this.options, options); 46 | const {password, SecurityToken, username} = options; 47 | if (SecurityToken) return Promise.resolve(SecurityToken); 48 | 49 | return req({ 50 | method: 'POST', 51 | pathname: '/api/v5/Login', 52 | body: {Password: password, UserName: username} 53 | }).then(({SecurityToken}) => { 54 | this.options = _.extend({}, this.options, {SecurityToken}); 55 | return SecurityToken; 56 | }); 57 | } 58 | 59 | getAccountId(options = {}) { 60 | options = _.extend({}, this.options, options); 61 | const {SecurityToken, AccountID} = options; 62 | if (AccountID) return Promise.resolve(AccountID); 63 | 64 | return this.getSecurityToken(options).then(SecurityToken => 65 | req({ 66 | method: 'GET', 67 | pathname: '/api/v5/My', 68 | query: {expand: 'account'}, 69 | headers: {SecurityToken} 70 | }) 71 | ).then(({Account}) => { 72 | this.options = _.extend({}, this.options, {AccountID: Account.Id}); 73 | return Account.Id 74 | }) 75 | } 76 | 77 | getDeviceList(options = {}) { 78 | options = _.extend({}, this.options, options); 79 | return this.getSecurityToken(options).then(SecurityToken => 80 | this.getAccountId(options).then(AccountId => 81 | req({ 82 | method: 'GET', 83 | pathname: '/api/v5.1/Accounts/' + AccountId + '/Devices', 84 | headers: {SecurityToken}, 85 | query: {filterOn: 'true'} 86 | }) 87 | ) 88 | ).then(({items}) => items); 89 | } 90 | 91 | getDeviceId(options = {}) { 92 | options = _.extend({}, this.options, options); 93 | const {MyQDeviceId} = options; 94 | if (MyQDeviceId) return Promise.resolve(MyQDeviceId); 95 | 96 | return this.getDeviceList(options).then(devices => { 97 | const withoutGateways = _.reject(devices, {device_type: 'hub'}); 98 | const ids = _.map(withoutGateways, 'serial_number'); 99 | if (ids.length === 0) throw new Error('No controllable devices found'); 100 | 101 | if (ids.length === 1) { 102 | this.options = _.extend({}, this.options, {MyQDeviceId: ids[0]}); 103 | return ids[0]; 104 | } 105 | 106 | throw new Error(`Multiple controllable devices found: ${ids.join(', ')}`); 107 | }); 108 | } 109 | 110 | maybeRetry(fn) { 111 | return fn().catch(er => { 112 | if (er.message.indexOf('Security Token has expired') === -1) throw er; 113 | 114 | this.options = _.omit(this.options, 'SecurityToken'); 115 | return fn(); 116 | }); 117 | } 118 | 119 | getSecurityTokenAccountIdAndMyQDeviceId(options = {}) { 120 | return this.maybeRetry(() => 121 | this.getSecurityToken(options).then(SecurityToken => 122 | this.getAccountId(options).then(AccountId => 123 | this.getDeviceId(options).then(MyQDeviceId => ({ 124 | SecurityToken, 125 | AccountId, 126 | MyQDeviceId 127 | })) 128 | ) 129 | ) 130 | ); 131 | } 132 | 133 | getDeviceAttribute(options = {}) { 134 | const {name} = options; 135 | return this.maybeRetry(() => 136 | this.getSecurityTokenAccountIdAndMyQDeviceId(options).then( 137 | ({SecurityToken, AccountId, MyQDeviceId}) => 138 | req({ 139 | method: 'GET', 140 | pathname: '/api/v5.1/Accounts/' + AccountId + '/devices/' + MyQDeviceId, 141 | headers: {SecurityToken}, 142 | }).then(({state}) => state[name]) 143 | ) 144 | ); 145 | } 146 | 147 | actOnDevice(options = {}) { 148 | const {action_type} = options; 149 | return this.maybeRetry(() => 150 | this.getSecurityTokenAccountIdAndMyQDeviceId(options).then( 151 | ({SecurityToken, AccountId, MyQDeviceId}) => 152 | req({ 153 | method: 'PUT', 154 | pathname: '/api/v5.1/Accounts/' + AccountId + '/Devices/' + MyQDeviceId + '/actions', 155 | body: {action_type}, 156 | headers: {SecurityToken} 157 | }) 158 | ) 159 | ); 160 | } 161 | }; 162 | --------------------------------------------------------------------------------