UNPKG

homebridge-lutron-caseta-leap-fast

Version:
486 lines (427 loc) 20.5 kB
<div class="card card-body text-center"> <p class="h2 card-title">Lutron Device Configuration</p> <div id="options" class="card-body my-3 w-75 mx-auto"> <h4 class="card-title">Global Options</h4> <form id='optionsForm' class="form-horizontal"> <div class="form-check row py-sm-3 mb-0"> <input class="form-check-input col-sm-1" type="checkbox" value="" id="filterPicoChk"/> <label class="form-check-label col-sm-11 text-left" for="filterPicoChk"> Exclude Pico remotes that are associated in the Lutron app </label> <br/> </div> <div class="form-check row py-sm-3 mb-0"> <input class="form-check-input col-sm-1" type="checkbox" value="" id="filterBlindsChk"/> <label class="form-check-label col-sm-11 text-left" for="filterBlindsChk"> Disable support for Serena wood blinds in this plugin </label> <br/> </div> <div class="form-group row py-sm-3 mb-0"> <label for='clickSpeedLongSelect' class='col-sm-5 col-form-label'>Long press speed</label> <select class="form-control-sm custom-select col-sm-7" id="clickSpeedLongSelect"> <option value="quick">Quick</option> <option selected value="default">Default</option> <option value="relaxed">Relaxed</option> <option value="disabled">Disabled</option> </select> </div> <div class="form-group row py-sm-3 mb-0"> <label for='clickSpeedDoubleSelect' class='col-sm-5 col-form-label'>Double press speed</label> <select class="form-control-sm custom-select col-sm-7" id="clickSpeedDoubleSelect"> <option value="quick">Quick</option> <option value="default" selected>Default</option> <option value="relaxed">Relaxed</option> <option value="disabled">Disabled</option> </select> </div> </form> </div> <div id="bridges"> <div class="card my-2" id="searching">Searching for bridges...</div> </div> <div id="schemapop"> <button type="button" class="btn btn-secondary" id="schemapopbtn">Use legacy view</button> </div> </div> <script> function bootstrapConfig(pluginConfig) { if (pluginConfig.length === 0) { console.log("no configuration detected. bootstrapping."); pluginConfig = [{name: "Lutron", platform: "LutronCasetaLeap", options: { filterPico: false, filterBlinds: false, clickSpeedDouble: "default", clickSpeedLong: "default"}, secrets: []}]; } if (pluginConfig[0].secrets === undefined) { console.log("configuration with no secrets. bootstrapping."); pluginConfig[0].secrets = new Array(); } if (pluginConfig[0].options === undefined) { console.log("configuration with no options. bootstrapping."); pluginConfig[0].options = { filterPico: false, filterBlinds: false, clickSpeedDouble: "default", clickSpeedLong: "default", }; } return pluginConfig; } document.getElementById('schemapopbtn').addEventListener('click', () => { homebridge.getPluginConfig().then((pluginConfig) => { document.getElementById('bridges').innerHTML = ''; document.getElementById('schemapop').innerHTML = ''; pluginConfig = bootstrapConfig(pluginConfig); homebridge.updatePluginConfig(pluginConfig).then(() => homebridge.showSchemaForm()); }); }); async function updateConfigFromMap(pluginConfig, secretsMap) { pluginConfig = bootstrapConfig(pluginConfig); pluginConfig[0].secrets = Array.from(secretsMap.values()); await homebridge.updatePluginConfig(pluginConfig); } (async () => { let pluginConfig = await homebridge.getPluginConfig(); pluginConfig = bootstrapConfig(pluginConfig); if (pluginConfig.length > 1) { homebridge.toast.error('Too many config objects'); return; } console.log(pluginConfig[0]); console.log(document.querySelector('#filterPicoChk')); document.querySelector('#filterPicoChk').checked = pluginConfig[0].options.filterPico; document.querySelector('#filterBlindsChk').checked = pluginConfig[0].options.filterBlinds; document.querySelector('#clickSpeedLongSelect').value = pluginConfig[0].options.clickSpeedLong; document.querySelector('#clickSpeedDoubleSelect').value = pluginConfig[0].options.clickSpeedDouble; document.getElementById('optionsForm').addEventListener('input', () => { console.log("options clicked"); pluginConfig = bootstrapConfig(pluginConfig); pluginConfig[0].options.filterPico = document.querySelector('#filterPicoChk').checked; pluginConfig[0].options.filterBlinds = document.querySelector('#filterBlindsChk').checked; pluginConfig[0].options.clickSpeedLong = document.querySelector('#clickSpeedLongSelect').value; pluginConfig[0].options.clickSpeedDouble = document.querySelector('#clickSpeedDoubleSelect').value; console.log(pluginConfig[0].options); homebridge.updatePluginConfig(pluginConfig); }); let secretMap = new Map(); secretMap = new Map(pluginConfig[0].secrets.map((o) => [o.bridgeid.toUpperCase(), o])); let bridgeMap = new Map(); homebridge.showSpinner(); try { const bridgeList = document.getElementById('bridges'); bridgeList.innerHTML = ''; function handleDiscovered(bridgeInfo) { bridgeInfo = bridgeInfo.data; console.log('found bridge', bridgeInfo); const bridge = new Bridge(bridgeInfo.systype, bridgeInfo.bridgeid, bridgeInfo.ipAddr); bridgeList.append(bridge.getElem()); bridgeMap.set(bridgeInfo.bridgeid, bridge); const s = secretMap.get(bridge.bridgeid); if (s !== undefined) { console.log('sending connect request for', bridge.bridgeid, bridge.ipAddr); doConnect(bridge, secretMap); } else { console.log('unknown bridge', bridge.bridgeid, bridge.ipAddr); bridge.toUnassociated(() => { doAssociate(bridge); }); } homebridge.hideSpinner(); } homebridge.addEventListener('discovered', handleDiscovered); await homebridge.request('/search'); // begin searching for bridges function handleConnected(event) { console.log('success connected to bridge', event.data); const b = bridgeMap.get(event.data); if (b === undefined) { console.error('failed trying to get connected bridge', event.data, 'from map'); console.dir(bridgeMap); throw new Error("could not find connected bridge " + event.data + " in map"); } b.toAssociated(() => { deleteBridgeSecret(event.data, secretMap); }); } console.log('listening for connected'); homebridge.addEventListener('connected', handleConnected); function handleFailed(event) { console.log('failed connected to bridge', event.data); const b = bridgeMap.get(event.data.bridgeid); b.toFailed( event.data.reason, () => { deleteBridgeSecret(event.data, secretMap); }, () => { doConnect(b, secretMap); }, ); } console.log('listening for failed'); homebridge.addEventListener('failed', handleFailed); function handleAssociated(event) { const { bridgeid, ipAddr, secrets } = event.data; const b = bridgeMap.get(bridgeid); console.log('associated with bridge', bridgeid); secretMap.set(bridgeid, secrets); homebridge .getPluginConfig() .then(async (config) => { await updateConfigFromMap(config, secretMap); }) .then(() => console.log('added secret')); doConnect(b, secretMap); } console.log('listening for associated'); homebridge.addEventListener('associated', handleAssociated); console.log('setup done'); } catch (e) { console.log(e.error, e.message, e.stack); homebridge.toast.error(e.error, e.message); } console.log('exiting'); })(); function doAssociate(bridge) { homebridge .request('/associate', { bridgeid: bridge.bridgeid, ipAddr: bridge.ipAddr, }) .then(() => console.log('associate request complete for', bridge.bridgeid, bridge.ipAddr)) .catch((e) => { // TODO inspect this to return something more user-friendly // if it's a quasi-leap w/ a 401 code, say something about "didn't press button // in time" console.log('associate request failed', bridge.bridgeid, bridge.ipAddr, e); homebridge.toast.error(e.message); bridge.toUnassociated(() => { doAssociate(bridge); }); }); } function doConnect(bridge, secretMap) { homebridge .request('/connect', { secrets: secretMap.get(bridge.bridgeid), bridgeid: bridge.bridgeid, ipAddr: bridge.ipAddr, }) .then(() => console.log('connect request complete for', bridge.bridgeid, bridge.ipAddr)); bridge.toConnecting(); } function deleteBridgeSecret(bridgeid, secretMap) { secretMap.delete(bridgeid); homebridge .getPluginConfig() .then(async (config) => { await updateConfigFromMap(config, secretMap); }) .then(() => { console.log('removed secret', bridgeid); console.dir(secretMap); }); } class Bridge { constructor(systype, bridgeid, ipAddr) { console.log('new bridge', bridgeid, ipAddr); this.systype = systype; this.bridgeid = bridgeid; this.ipAddr = ipAddr; const bridgeElem = document.createElement('div'); bridgeElem.id = bridgeid; bridgeElem.className = 'card mx-auto my-2'; bridgeElem.style.width = '24rem'; bridgeElem.innerHTML = bridgeStates.INITIAL(this.systype, this.bridgeid, this.ipAddr); this.elem = bridgeElem; this.pendTimeout = undefined; this.connTimeout = undefined; } getElem() { return this.elem; } // CONNECTING -> ASSOCIATED (external) // CONNECTING -> FAILED (timeout) // CONNECTING -> FAILED (external) toConnecting(onTimeout) { console.log('toConnecting', this.bridgeid); this.elem.innerHTML = bridgeStates.CONNECTING(this.systype, this.bridgeid, this.ipAddr); this.connTimeout = setTimeout(() => { console.log('CONNECTING', this.bridgeid, 'timeout'); if (onTimeout !== undefined) { console.log('calling CONNECTING timeout helper', this.bridgeid); onTimeout(); } this.toFailed('Connection timed out'); }, 5000); } // ASSOCIATED -> UNASSOCIATED (internal) toAssociated(onResetClick) { console.log('toAssociated', this.bridgeid); this.elem.innerHTML = bridgeStates.ASSOCIATED(this.systype, this.bridgeid, this.ipAddr); // clear the timeout set in CONNECTING state now that we're connected if (this.connTimeout !== undefined) { console.log('ASSOCIATED clearing CONNECTING timeout', this.bridgeid); clearTimeout(this.connTimeout); this.connTimeout = undefined; } // clear the timeout set in PENDING since we have creds now if (this.pendTimeout !== undefined) { console.log('clearing PENDING timeout', this.bridgeid); clearTimeout(this.pendTimeout); this.pendTimeout = undefined; } document.getElementById(this.bridgeid + 'reset').addEventListener( 'click', (() => { console.log('reset from ASSOCIATED clicked', this.bridgeid); if (onResetClick !== undefined) { console.log('calling reset ASSOCIATED helper', this.bridgeid); onResetClick(); } this.toUnassociated(() => { doAssociate(this); }); }).bind(this), ); } // UNASSOCIATED -> PENDING (internal) toUnassociated(onAssociateClick) { console.log('toUnassociated', this.bridgeid, this.ipAddr); this.elem.innerHTML = bridgeStates.UNASSOCIATED(this.systype, this.bridgeid, this.ipAddr); document.getElementById(this.bridgeid + 'assoc').addEventListener( 'click', (() => { console.log('associate from UNASSOCIATED clicked', this.bridgeid); if (onAssociateClick !== undefined) { console.log('calling associate from UNASSOCIATED helper', this.bridgeid); onAssociateClick(); } this.toPending(() => { homebridge.toast.error('Association with bridge ' + this.bridgeid + ' timed out.'); }); }).bind(this), ); } // PENDING -> UNASSOCIATED (timeout) // PENDING -> ASSOCIATED (external) // PENDING -> UNASSOCIATED (internal) toPending(onTimeout, onCancelClick) { console.log('toPending', this.bridgeid); this.elem.innerHTML = bridgeStates.PENDING(this.systype, this.bridgeid, this.ipAddr); this.pendTimeout = setTimeout(() => { console.log('PENDING', this.bridgeid, 'timeout'); if (onTimeout !== undefined) { console.log('calling PENDING timeout helper', this.bridgeid); onTimeout(); } //show toast this.toUnassociated(() => { doAssociate(this); }); }, 30000); document.getElementById(this.bridgeid + 'cancel').addEventListener( 'click', (() => { console.log('cancel from PENDING clicked', this.bridgeid); if (onCancelClick !== undefined) { console.log('calling PENDING cancel helper', this.bridgeid); onCancelClick(); } this.toUnassociated(() => { doAssociate(this); }); }).bind(this), ); } // FAILED -> UNASSOCIATED (internal) toFailed(reason, onResetClick, onRetryClick) { console.log('toFailed', this.bridgeid, reason); this.elem.innerHTML = bridgeStates.FAILED(this.systype, this.bridgeid, this.ipAddr); homebridge.toast.error('Connection failed: ' + reason); // clear the timeout set in CONNECTING state now that it's failed if (this.connTimeout !== undefined) { console.log('in FAILED clearing CONNECTING timeout', this.bridgeid); clearTimeout(this.connTimeout); this.connTimeout = undefined; } document.getElementById(this.bridgeid + 'retry').addEventListener( 'click', (() => { console.log('retry from FAILED clicked', this.bridgeid); if (onRetryClick !== undefined) { console.log('calling retry FAILED helper', this.bridgeid); onRetryClick(); } this.toConnecting(); }).bind(this), ); document.getElementById(this.bridgeid + 'reset').addEventListener( 'click', (() => { console.log('reset from FAILED clicked', this.bridgeid); if (onResetClick !== undefined) { console.log('calling reset FAILED helper', this.bridgeid); onResetClick(); } this.toUnassociated(() => { doAssociate(this); }); }).bind(this), ); } } const bridgeStates = Object.freeze({ INITIAL: (systype, bridgeid, ipAddr) => `<div class="card-body"> <h4 class="card-title">${systype} ${bridgeid}</h4> <h6 class="card-subtitle">${ipAddr}</h6> <div class="alert alert-light" role="alert">${systype} discovered!</div> </div>`, CONNECTING: (systype, bridgeid, ipAddr) => `<div class="card-body"> <h4 class="card-title">${systype} ${bridgeid}</h4> <h6 class="card-subtitle">${ipAddr}</h6> <div class="alert alert-light" role="alert">Connecting...</div> <!-- <div class="card-footer text-muted"> <button type="button" class="btn btn-dark">Cancel</button> </div> --> </div>`, ASSOCIATED: (systype, bridgeid, ipAddr) => `<div class="card-body"> <h4 class="card-title">${systype} ${bridgeid}</h4> <h6 class="card-subtitle">${ipAddr}</h6> <div class="alert alert-success" role="alert">Connected!</div> <div class="card-footer text-muted"> <button type="button" class="btn btn-danger" id="${bridgeid}reset">Reset</button> </div> </div>`, FAILED: (systype, bridgeid, ipAddr) => `<div class="card-body"> <h4 class="card-title">${systype} ${bridgeid}</h4> <h6 class="card-subtitle">${ipAddr}</h6> <div class="alert alert-danger" role="alert">Connection failed!</div> <div class="card-footer text-muted"> <button type="button" class="btn btn-primary" id="${bridgeid}retry">Retry</button> <button type="button" class="btn btn-danger" id="${bridgeid}reset">Reset</button> </div> </div>`, UNASSOCIATED: (systype, bridgeid, ipAddr) => `<div class="card-body"> <h4 class="card-title">${systype} ${bridgeid}</h4> <h6 class="card-subtitle">${ipAddr}</h6> <button type="button" class="btn btn-primary" id="${bridgeid}assoc">Associate</button> </div>`, PENDING: (systype, bridgeid, ipAddr) => `<div class="card-body"> <h4 class="card-title">${systype} ${bridgeid}</h4> <h6 class="card-subtitle">${ipAddr}</h6> <p class="card-text">Press the button on the back of the bridge within 30 seconds...</p> <!-- <!-- TODO add the ability to cancel an association <div class="card-footer text-muted"> <button type="button" class="btn btn-dark" id="${bridgeid}cancel">Cancel</button> </div> --> </div>`, }); </script>