UNPKG

@homebridge-plugins/homebridge-smarthq

Version:

The SmartHQ plugin allows you to interact with SmartHQ Devices in HomeKit and with Siri.

586 lines (544 loc) 25.7 kB
<p class="text-center"> <img src="https://raw.githubusercontent.com/homebridge-plugins/homebridge-smarthq/latest/branding/Homebridge_x_SmartHQ.svg" alt="homebridge-smarthq logo" style="width: 40%" /> </p> <div id="pageIntro" style="display: none"> <p class="lead text-center">Thank you for installing <strong>homebridge-smarthq</strong></p> <p class="lead text-center">Needed to Config:</p> <ol> <li class="mb-3"><strong>Username</strong>.</li> <li class="mb-3"><strong>Password</strong>.</li> </ol> <div class="text-center"> <button type="button" class="btn btn-primary" id="introLink">Continue &rarr;</button> </div> </div> <div id="menuWrapper" class="btn-group w-100 mb-0" role="group" aria-label="UI Menu" style="display: none"> <button type="button" class="btn btn-primary" id="menuSettings">Settings</button> <button type="button" class="btn btn-primary" id="menuDevices">Devices</button> <button type="button" class="btn btn-primary mr-0" id="menuHome">Support</button> </div> <div id="disabledBanner" class="alert alert-secondary mb-0 mt-3" role="alert" style="display: none"> Plugin is currently disabled <button id="disabledEnable" type="button" class="btn btn-link p-0 m-0 float-right">Enable</button> </div> <div id="pageDevices" class="mt-4" style="display: none"> <div id="deviceInfo"> <form> <div class="form-group"> <select class="form-control" id="deviceSelect"></select> </div> </form> <table class="table w-100" id="deviceTable" style="display: none"> <thead> <tr class="table-active"> <th scope="col" style="width: 40%">Device Name</th> <th scope="col" style="width: 60%" id="displayName"></th> </tr> </thead> <tbody> <tr> <th scope="row">Brand</th> <td id="brand"></td> </tr> <tr> <th scope="row">Model</th> <td id="model"></td> </tr> <tr> <th scope="row">Serial</th> <td id="serial"></td> </tr> <tr> <th scope="row">Appliance ID</th> <td id="applianceId"></td> </tr> <tr> <th scope="row">Firmware Version</th> <td id="firmware"></td> </tr> </tbody> <tbody id="deviceAdvancedOptions"></tbody> </table> <p class="text-center">External Accessories Will Not Display Here.</p> </div> </div> <div id="pageSupport" class="mt-4" style="display: none"> <p class="text-center lead">Thank you for using <strong>homebridge-smarthq</strong></p> <p class="text-center">The links below will take you to our GitHub wiki</p> <h5>Setup</h5> <ul> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/wiki" target="_blank">Wiki Home</a> </li> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/wiki/Configuration" target="_blank">Configuration</a> </li> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/wiki/Beta-Version" target="_blank">Beta Version</a> </li> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/wiki/Node-Version" target="_blank">Node Version</a> </li> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/wiki/Uninstallation" target="_blank">Uninstallation</a> </li> </ul> <h5>Features</h5> <ul> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/wiki/Supported-Devices" target="_blank">Supported Devices</a> </li> </ul> <h5>Help/About</h5> <ul> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/issues/new/choose" target="_blank">Support Request</a> </li> <li> <a href="https://github.com/homebridge-plugins/homebridge-smarthq/blob/latest/CHANGELOG.md" target="_blank">Changelog</a> </li> </ul> <style> /* iOS Switch Style */ .ios-switch { position: relative; display: inline-block; width: 42px; height: 24px; margin: 0; } .ios-switch input { opacity: 0; width: 0; height: 0; } .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #e4e4e4; transition: .3s; border-radius: 34px; } .slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 2px; bottom: 2px; background-color: white; transition: .3s; border-radius: 50%; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.2); } input:checked+.slider { background-color: #FFA002; } input:focus+.slider { box-shadow: 0 0 1px #FFA002; } input:checked+.slider:before { transform: translateX(18px); } /* Add this to ensure the switch works with your form handling */ .ios-switch input:disabled+.slider { opacity: 0.5; cursor: not-allowed; } </style> </div> <script> (async () => { try { const currentConfig = await homebridge.getPluginConfig(); showIntro = () => { const introLink = document.getElementById('introLink'); introLink.addEventListener('click', () => { homebridge.showSpinner(); document.getElementById('pageIntro').style.display = 'none'; document.getElementById('menuWrapper').style.display = 'inline-flex'; showSettings(); homebridge.hideSpinner(); }); document.getElementById('pageIntro').style.display = 'block'; }; showDevices = async () => { homebridge.showSpinner(); homebridge.hideSchemaForm(); document.getElementById('menuHome').classList.remove('btn-elegant'); document.getElementById('menuHome').classList.add('btn-primary'); document.getElementById('menuDevices').classList.add('btn-elegant'); document.getElementById('menuDevices').classList.remove('btn-primary'); document.getElementById('menuSettings').classList.remove('btn-elegant'); document.getElementById('menuSettings').classList.add('btn-primary'); document.getElementById('pageSupport').style.display = 'none'; document.getElementById('pageDevices').style.display = 'block'; const cachedAccessories = typeof homebridge.getCachedAccessories === 'function' ? await homebridge.getCachedAccessories() : await homebridge.request('/getCachedAccessories'); if (cachedAccessories.length > 0) { cachedAccessories.sort((a, b) => { return a.displayName.toLowerCase() > b.displayName.toLowerCase() ? 1 : b.displayName.toLowerCase() > a.displayName.toLowerCase() ? -1 : 0; }); } const deviceSelect = document.getElementById('deviceSelect'); deviceSelect.innerHTML = ''; cachedAccessories.forEach((a) => { const option = document.createElement('option'); option.text = a.displayName; option.value = a.UUID; deviceSelect.add(option); }); showDeviceInfo = async (UUID) => { homebridge.showSpinner(); const thisAcc = cachedAccessories.find((x) => x.UUID === UUID); const context = thisAcc.context; document.getElementById('displayName').innerHTML = thisAcc.displayName; document.getElementById('brand').innerHTML = context.device.brand; document.getElementById('model').innerHTML = context.device.model; document.getElementById('serial').innerHTML = context.device.serial; document.getElementById('applianceId').innerHTML = context.device.applianceId; document.getElementById('firmware').innerHTML = context.device.firmware; document.getElementById('deviceTable').style.display = 'inline-table'; document.getElementById('deviceAdvancedOptions').innerHTML = '' const advancedOptions = thisAcc.services.filter((svc) => !!svc.characteristics.find((char) => char.displayName === 'Product Data')) if (advancedOptions.length) { // currently only supporting one accessory for now - opal const raw = advancedOptions[0].characteristics.find((char) => char.displayName === 'Product Data').value.split(',') raw.forEach((r) => { const tr = document.createElement('tr') const params = new URLSearchParams(r); const metadata = Object.fromEntries(params) const th = document.createElement('th') th.scope = 'row' th.style.width = '50%' th.innerHTML = metadata.label?.replaceAll('_', ' ') tr.appendChild(th) const td = document.createElement('td') td.style.borderTop = '1px solid white' if (metadata.type === 'scheduler') { // Create a button to open the scheduler dialog const button = document.createElement('button'); button.className = 'btn btn-sm btn-primary'; button.innerHTML = 'Configure Schedule'; button.style.color = 'white' // Create a unique ID for this dialog const dialogId = `scheduler-dialog-${metadata.device}-${metadata.indicator}`; // Get existing schedule data or create a default one // Structure includes enabled property for each day const defaultSchedule = { Monday: { time: '08:00', enabled: false }, Tuesday: { time: '08:00', enabled: false }, Wednesday: { time: '08:00', enabled: false }, Thursday: { time: '08:00', enabled: false }, Friday: { time: '08:00', enabled: false }, Saturday: { time: '08:00', enabled: false }, Sunday: { time: '08:00', enabled: false } }; // Parse existing value if available let scheduleData = defaultSchedule; try { if (currentConfig[0].deviceOptions?.[metadata.device]?.[metadata.indicator]) { scheduleData = currentConfig[0].deviceOptions[metadata.device][metadata.indicator] } } catch (e) { console.error('Error parsing existing schedule data', e); } // Create the dialog element with a custom overlay const dialogOverlay = document.createElement('div'); dialogOverlay.id = dialogId; dialogOverlay.style.display = 'none'; dialogOverlay.style.position = 'fixed'; dialogOverlay.style.top = '0'; dialogOverlay.style.left = '0'; dialogOverlay.style.width = '100%'; dialogOverlay.style.height = '100%'; dialogOverlay.style.backgroundColor = 'rgba(0,0,0,0.5)'; dialogOverlay.style.zIndex = '1000'; dialogOverlay.style.overflow = 'auto'; // Create the dialog content with Bootstrap styling classes dialogOverlay.innerHTML = ` <div style="display: flex; align-items: center; justify-content: center; height: 100%;"> <div class="card" style="width: 90%; max-width: 500px;"> <div class="card-header"> <h5 class="mb-0">${metadata.label?.replaceAll('_', ' ') || 'Weekly Schedule'}</h5> <button type="button" class="close" id="close-${dialogId}" aria-label="Close" style="position: absolute; right: .5rem; top: .5rem; border: none; background-color: transparent;"> <span aria-hidden="true">&times;</span> </button> </div> <div class="card-body"> <div class="form-group row align-items-center table-active" style="background-color: #6c757d; display: flex;"> <label class="col-md-3 col-form-label" style="flex: .35; border: 1.5px solid rgb(91.8, 99.45, 106.25); padding: .25rem;">Day</label> <label class="col-sm-3 col-form-label" style="flex: .25; border: 1.5px solid rgb(91.8, 99.45, 106.25); padding: .25rem;">Enabled</label> <label class="col-sm-3 col-form-label" style="flex: 1; border: 1.5px solid rgb(91.8, 99.45, 106.25); padding: .25rem;">Start Time</label> </div> <form id="scheduler-form-${dialogId}"> ${Object.keys(scheduleData).map(day => ` <div class="form-group row align-items-center" style="display: flex;"> <label class="col-sm-3 col-form-label" style="border: 1.5px solid rgb(91.8, 99.45, 106.25); flex: .35; height: 2.75rem; padding: .25rem;">${day}</label> <div class="col-sm-2 d-flex align-items-center" style="border: 1.5px solid rgb(91.8, 99.45, 106.25); flex: .25; height: 2.75rem; padding: .25rem;"> <label class="ios-switch" style="margin-bottom: 0; width: 42px; height: 24px;"> <input type="checkbox" id="enable-${dialogId}-${day}" name="enable-${day}" ${scheduleData[day].enabled ? 'checked' : ''}> <span class="slider"></span> </label> </div> <div class="col-sm-7" style="display: flex; flex: 1; height: 2.75rem; padding: .25rem; border: 1.5px solid #6c757d;"> <input type="time" class="form-control" name="time-${day}" value="${scheduleData[day].time}" ${!scheduleData[day].enabled ? 'disabled' : ''}> </div> </div> `).join('')} </form> </div> <div class="card-footer d-flex justify-content-end" style="column-gap: .5rem;"> <button type="button" class="btn btn-secondary mr-2" id="cancel-${dialogId}">Cancel</button> <button type="button" class="btn btn-primary" id="save-${dialogId}">Save</button> </div> </div> </div> `; // Add the dialog to the document document.body.appendChild(dialogOverlay); // Show dialog function const showDialog = () => { dialogOverlay.style.display = 'block'; document.body.style.overflow = 'hidden'; // Prevent scrolling while dialog is open }; // Hide dialog function const hideDialog = () => { dialogOverlay.style.display = 'none'; document.body.style.overflow = ''; // Restore scrolling }; // Add event listeners button.addEventListener('click', showDialog); // Close button handler document.getElementById(`close-${dialogId}`).addEventListener('click', hideDialog); // Cancel button handler document.getElementById(`cancel-${dialogId}`).addEventListener('click', hideDialog); // Add event listeners to enable/disable time inputs based on checkbox state Object.keys(scheduleData).forEach(day => { const enableCheckbox = document.getElementById(`enable-${dialogId}-${day}`); const timeInput = enableCheckbox.closest('.form-group').querySelector('input[type="time"]'); enableCheckbox.addEventListener('change', () => { timeInput.disabled = !enableCheckbox.checked; }); }); // Save button handler document.getElementById(`save-${dialogId}`).addEventListener('click', async () => { // Show spinner while saving homebridge.showSpinner(); try { // Collect the schedule data from the form const form = document.getElementById(`scheduler-form-${dialogId}`); const formElements = form.elements; const newScheduleData = {}; // Process the form data Object.keys(scheduleData).forEach(day => { const enabled = formElements[`enable-${day}`].checked; const time = formElements[`time-${day}`].value; newScheduleData[day] = { time: time, enabled: enabled }; }); // Update the config currentConfig[0] = { ...currentConfig[0], deviceOptions: { ...currentConfig[0].deviceOptions, [metadata.device]: { ...currentConfig[0].deviceOptions?.[metadata.device], [metadata.indicator]: newScheduleData } } }; // Save the config await homebridge.updatePluginConfig([...currentConfig]); await homebridge.savePluginConfig(); // Show a success message homebridge.toast.success('Schedule saved successfully'); } catch (error) { homebridge.toast.error(`Failed to save schedule: ${error.message}`); } finally { // Hide spinner and dialog homebridge.hideSpinner(); hideDialog(); } }); // Close dialog when clicking outside of it dialogOverlay.addEventListener('click', (event) => { if (event.target === dialogOverlay) { hideDialog(); } }); td.appendChild(button); tr.appendChild(td) document.getElementById('deviceAdvancedOptions').appendChild(tr) } else if (metadata.type === 'number') { const input = document.createElement('input'); input.type = 'number'; input.placeholder = metadata.placeholder?.replaceAll('_', ' ') || ''; const existingDefaultValue = Number(currentConfig[0].deviceOptions?.[metadata.device]?.[metadata.indicator] ?? 0); input.defaultValue = existingDefaultValue; td.appendChild(input); tr.appendChild(td) document.getElementById('deviceAdvancedOptions').appendChild(tr) input.addEventListener('change', async () => { const value = Number(event.target.value); currentConfig[0] = { ...currentConfig[0], deviceOptions: { ...currentConfig[0].deviceOptions, [metadata.device]: { ...currentConfig[0].deviceOptions?.[metadata.device], [metadata.indicator]: value } } }; await homebridge.updatePluginConfig([...currentConfig]); await homebridge.savePluginConfig(); }); } else if (metadata.type === 'boolean') { const switchDiv = document.createElement('div'); const existingDefaultValue = Boolean(currentConfig[0].deviceOptions?.[metadata.device]?.[metadata.indicator] ?? false); // input.type = 'checkbox'; switchDiv.innerHTML = ` <label class="ios-switch" style="margin-bottom: 0; width: 42px; height: 24px;"> <input type="checkbox" id="enable-${metadata.device}-${metadata.indicator}" name="enable-${metadata.indicator}" ${existingDefaultValue ? 'checked' : ''}> <span class="slider"></span> </label> ` td.appendChild(switchDiv); tr.appendChild(td) document.getElementById('deviceAdvancedOptions').appendChild(tr) const enableCheckbox = document.getElementById(`enable-${metadata.device}-${metadata.indicator}`); enableCheckbox.addEventListener('change', async () => { const value = Boolean(enableCheckbox.checked ?? false); currentConfig[0] = { ...currentConfig[0], deviceOptions: { ...currentConfig[0].deviceOptions, [metadata.device]: { ...currentConfig[0].deviceOptions?.[metadata.device], [metadata.indicator]: value } } }; await homebridge.updatePluginConfig([...currentConfig]); await homebridge.savePluginConfig(); }); } else { const input = document.createElement('input'); input.type = 'text'; input.placeholder = metadata.placeholder?.replaceAll('_', ' ') || ''; const existingDefaultValue = currentConfig[0].deviceOptions?.[metadata.device]?.[metadata.indicator] ?? ""; input.defaultValue = existingDefaultValue; td.appendChild(input); tr.appendChild(td) document.getElementById('deviceAdvancedOptions').appendChild(tr) input.addEventListener('change', async () => { const value = event.target.value ?? ""; currentConfig[0] = { ...currentConfig[0], deviceOptions: { ...currentConfig[0].deviceOptions, [metadata.device]: { ...currentConfig[0].deviceOptions?.[metadata.device], [metadata.indicator]: value } } }; await homebridge.updatePluginConfig([...currentConfig]); await homebridge.savePluginConfig(); }); } }) } homebridge.hideSpinner(); }; deviceSelect.addEventListener('change', (event) => showDeviceInfo(event.target.value)); if (cachedAccessories.length > 0) { showDeviceInfo(cachedAccessories[0].UUID); } else { const option = document.createElement('option'); option.text = 'No Devices'; deviceSelect.add(option); deviceSelect.disabled = true; } homebridge.hideSpinner(); }; showSupport = () => { homebridge.showSpinner(); homebridge.hideSchemaForm(); document.getElementById('menuHome').classList.add('btn-elegant'); document.getElementById('menuHome').classList.remove('btn-primary'); document.getElementById('menuDevices').classList.remove('btn-elegant'); document.getElementById('menuDevices').classList.add('btn-primary'); document.getElementById('menuSettings').classList.remove('btn-elegant'); document.getElementById('menuSettings').classList.add('btn-primary'); document.getElementById('pageSupport').style.display = 'block'; document.getElementById('pageDevices').style.display = 'none'; homebridge.hideSpinner(); }; showSettings = () => { homebridge.showSpinner(); document.getElementById('menuHome').classList.remove('btn-elegant'); document.getElementById('menuHome').classList.add('btn-primary'); document.getElementById('menuDevices').classList.remove('btn-elegant'); document.getElementById('menuDevices').classList.add('btn-primary'); document.getElementById('menuSettings').classList.add('btn-elegant'); document.getElementById('menuSettings').classList.remove('btn-primary'); document.getElementById('pageSupport').style.display = 'none'; document.getElementById('pageDevices').style.display = 'none'; homebridge.showSchemaForm(); homebridge.hideSpinner(); }; showDisabledBanner = () => { document.getElementById('disabledBanner').style.display = 'block'; }; enablePlugin = async () => { homebridge.showSpinner(); document.getElementById('disabledBanner').style.display = 'none'; currentConfig[0].disablePlugin = false; await homebridge.updatePluginConfig(currentConfig); await homebridge.savePluginConfig(); homebridge.hideSpinner(); }; menuHome.addEventListener('click', () => showSupport()); menuDevices.addEventListener('click', () => showDevices()); menuSettings.addEventListener('click', () => showSettings()); disabledEnable.addEventListener('click', () => enablePlugin()); if (currentConfig.length) { document.getElementById('menuWrapper').style.display = 'inline-flex'; showSettings(); if (currentConfig[0].disablePlugin) { showDisabledBanner(); } } else { currentConfig.push({ name: 'SmartHQ' }); await homebridge.updatePluginConfig(currentConfig); showIntro(); } } catch (err) { homebridge.toast.error(err.message, 'Error'); } finally { homebridge.hideSpinner(); } })(); </script>