UNPKG

node-red-contrib-hubitat

Version:
380 lines (363 loc) 16.2 kB
<script type="text/javascript"> /* eslint-disable no-underscore-dangle */ function validatorWebhookPath(rawValue) { let value = rawValue; if (!value) { value = '/hubitat/webhook'; $('#node-config-input-webhookPath').val(value); } if (!value.startsWith('/')) { value = `/${value}`; $('#node-config-input-webhookPath').val(value); } const internalEndpoints = ['/', '/auth/token', '/auth/revoke', '/flows', '/flow', '/nodes']; if (internalEndpoints.includes(value)) { value = `${value}_`; $('#node-config-input-webhookPath').val(value); } const pattern = /^(\/[\w%.~+-]*)*$/; if (!pattern.test(value)) { return false; } RED.nodes.eachConfig((node) => { if ((this.id !== node.id) && (node.type === 'hubitat config') && (node.webhookPath === value)) { value = `${value}_`; $('#node-config-input-webhookPath').val(value); } }); return true; } function validatorNodeRedServer(rawValue) { let value = rawValue.replace(/ /g, ''); if (!value.startsWith('http://') && !value.startsWith('https://')) { value = `http://${value}`; } $('#node-config-input-nodeRedServer').val(value); return true; } function validatorRemoveScheme(rawValue) { const value = rawValue.replace(/^http[s]?:\/\//, ''); $('#node-config-input-host').val(value); return true; } function setNodeColor(node, color) { const workspace = $('#red-ui-workspace-chart').contents(); const nodeRect = workspace.find(`[id="${node.id}"]`).find('.red-ui-flow-node'); nodeRect.attr('fill', color); } function getHubitatNodeColor(configNode, node) { if (configNode && configNode.colorEnabled && configNode.color) { return configNode.color; } return RED.utils.getNodeColor(node.type, node._def); } function updateHubitatNodeAttr(node) { if (node._def.category !== 'hubitat') { return; } const configNode = RED.nodes.node(node.server); const color = getHubitatNodeColor(configNode, node); setNodeColor(node, color); } function updateHubitatConfigNodeAttr(configNode) { configNode.users.forEach((node) => { const color = getHubitatNodeColor(configNode, node); setNodeColor(node, color); }); } RED.events.on('nodes:change', (node) => { if (node.type === 'hubitat config') { updateHubitatConfigNodeAttr(node); } else { updateHubitatNodeAttr(node); } }); RED.events.on('nodes:add', (node) => { setTimeout(updateHubitatNodeAttr.bind(this, node), 5); }); // eslint-disable-next-line no-unused-vars RED.events.on('workspace:change', (workspace) => { setTimeout(() => { RED.nodes.eachConfig((node) => { if (node.type === 'hubitat config') { updateHubitatConfigNodeAttr(node); } }); }, 5); // 0 is enough, but just to be sure }); RED.nodes.registerType('hubitat config', { category: 'config', defaults: { name: { value: '' }, usetls: { value: false }, host: { value: '', required: true, validate: validatorRemoveScheme }, port: { value: 80, required: true, validate: RED.validators.number() }, appId: { value: '', required: true, validate: RED.validators.number() }, nodeRedServer: { value: '', validate: validatorNodeRedServer }, webhookPath: { value: '/hubitat/webhook', validate: validatorWebhookPath }, autoRefresh: { value: true }, delayCommands: { value: '' }, useWebsocket: { value: false }, colorEnabled: { value: false }, color: { value: '#ACE043' }, }, credentials: { token: { required: true, type: 'text' } }, label() { if (this.name) { return this.name; } const scheme = ((this.usetls) ? 'https' : 'http'); if ((this.port === '80') || (this.port === '443')) { return `${scheme}://${this.host}`; } return `${scheme}://${this.host}:${this.port}`; }, oneditprepare() { const tabs = RED.tabs.create({ id: 'node-config-hubitat-config-tabs', onchange(tab) { $('#node-config-hubitat-config-tabs-content').children().hide(); $(`#${tab.id}`).show(); }, }); tabs.addTab({ id: 'hubitat-config-tab-connection', label: 'Connection', }); tabs.addTab({ id: 'hubitat-config-tab-security', label: 'Security', }); tabs.addTab({ id: 'hubitat-config-tab-advanced', label: 'Advanced', }); let { httpNodeRoot } = RED.settings; if (httpNodeRoot.slice(-1) === '/') { httpNodeRoot = httpNodeRoot.slice(0, -1); } const httpNodeRootInput = $('#node-config-input-httpNodeRoot'); if (httpNodeRoot === '') { httpNodeRootInput.hide(); } else { httpNodeRootInput.width((httpNodeRoot.length) * 8); httpNodeRootInput.val(httpNodeRoot); } const nodeRedServer = $('#node-config-input-nodeRedServer'); nodeRedServer.attr('placeholder', window.location.origin); if (!nodeRedServer.val()) { nodeRedServer.val(window.location.origin); } const useWebsocket = $('#node-config-input-useWebsocket'); function disableWebhook() { if (useWebsocket.is(':checked')) { $('#node-config-input-nodeRedServer').attr('disabled', 'disabled'); $('#node-config-input-webhookPath').attr('disabled', 'disabled'); $('#node-config-button-webhookConfigure').attr('disabled', 'disabled'); } else { $('#node-config-input-nodeRedServer').removeAttr('disabled'); $('#node-config-input-webhookPath').removeAttr('disabled'); $('#node-config-button-webhookConfigure').removeAttr('disabled'); } } useWebsocket.click(disableWebhook); disableWebhook(); const colorEnabled = $('#node-config-input-colorEnabled'); function disableColor() { if (colorEnabled.is(':checked')) { $('#node-config-input-color').show(); } else { $('#node-config-input-color').hide(); } } colorEnabled.click(disableColor); disableColor(); }, }); // eslint-disable-next-line no-unused-vars function configureHubitat() { $('#node-config-input-webhookConfigured').html(''); $('#node-config-text-webhookWarning').hide(); const failIcon = '&#10060;'; // Red cross mark const successIcon = '&#9989;'; // Green check mark const host = $('#node-config-input-host').val(); if (!host) { $('#node-config-input-webhookConfigured').html(`${failIcon} Invalid Server`); return; } const port = $('#node-config-input-port').val(); if (!port) { $('#node-config-input-webhookConfigured').html(`${failIcon} Invalid Port`); return; } const appId = $('#node-config-input-appId').val(); if (!appId) { $('#node-config-input-webhookConfigured').html(`${failIcon} Invalid Application ID`); return; } const token = $('#node-config-input-token').val(); if (!token) { $('#node-config-input-webhookConfigured').html(`${failIcon} Invalid Token`); return; } const nodeRedServer = $('#node-config-input-nodeRedServer').val(); if (!nodeRedServer) { $('#node-config-input-webhookConfigured').html(`${failIcon} Invalid Node-RED server`); return; } const webhookPath = $('#node-config-input-webhookPath').val(); if (!webhookPath) { $('#node-config-input-webhookConfigured').html(`${failIcon} Invalid webhook path`); return; } const params = { usetls: $('#node-config-input-usetls').is(':checked'), host, port, appId, token, nodeRedServer, webhookPath, }; // eslint-disable-next-line no-unused-vars $.post('hubitat/configure', params, (res) => { $('#node-config-input-webhookConfigured').html(successIcon); $('#node-config-text-webhookWarning').show(); // eslint-disable-next-line no-unused-vars }).fail((xhr, status, error) => { let reason; if (xhr.status === 401) { reason = 'Invalid Token'; } else if (xhr.status === 404) { reason = 'Invalid Application ID'; } else { reason = 'Invalid connection information (Server, Port, TLS)'; } $('#node-config-input-webhookConfigured').html(`${failIcon} Fail to set webhoook. ${reason}`); }); } </script> <script type="text/html" data-template-name="hubitat config"> <div class="form-row"> <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</span></label> <input type="text" id="node-config-input-name" placeholder="Name"> </div> <div class="form-row"> <ul style="min-width: 600px; margin-bottom: 20px;" id="node-config-hubitat-config-tabs"></ul> </div> <div id="node-config-hubitat-config-tabs-content" style="min-height:150px;"> <div id="hubitat-config-tab-connection" style="display:none"> <div class="form-row"> <label for="node-config-input-host"><i class="fa fa-globe"></i> Server</label> <input type="text" id="node-config-input-host" style="width:40%;" placeholder="e.g. localhost"> <label for="node-config-input-port" style="margin-left:20px; width:43px;"> Port</label> <input type="text" id="node-config-input-port" style="width:55px" placeholder="80"> </div> <div class="form-row"> <label for="node-config-input-usetls" style="width: auto">Enable secure (SSL/TLS) connection</label> <input type="checkbox" id="node-config-input-usetls" style="display: inline-block; width: auto; vertical-align: top;"> </div> <div class="form-row"> <label for="node-config-input-appId" style="width: auto"><i class="fa fa-gear"></i> Application ID</label> <input type="text" id="node-config-input-appId" style="width:55px"> </div> <hr> <div class="form-row"> <p><b>Webhook configuration</b></p> </div> <div class="form-row" style="display: flex; flex-direction: row;"> <label for="node-config-input-nodeRedServer"><i class="fa fa-globe"></i> Node-RED</label> <input type="text" id="node-config-input-nodeRedServer" style="width:205px;"> <input type="text" id="node-config-input-httpNodeRoot" readonly> <input type="text" id="node-config-input-webhookPath" style="width:auto" placeholder="/hubitat/webhook"> </div> <div class="form-row"> <button id="node-config-button-webhookConfigure" onclick="configureHubitat()">Configure webhook</button> <p id="node-config-input-webhookConfigured" style="display: inline-block;"></p> </div> <div class="form-row"> <p id="node-config-text-webhookWarning" hidden> <b>Warning:</b> To ensure that Node-RED receives webhooks, you should create a basic test flow with <code>device</code> or <code>mode</code> node. </p> </div> </div> <div id="hubitat-config-tab-security" style="display:none"> <div class="form-row"> <label for="node-config-input-token"><i class="fa fa-lock"></i> Token</label> <input type="text" id="node-config-input-token" placeholder="00000000-0000-4000-8000-000000000000"> </div> </div> <div id="hubitat-config-tab-advanced" style="display:none"> <div class="form-row"> <label for="node-config-input-autoRefresh" style="width: auto; margin-right: 5px;">Rebuild cache on Hubitat <code>systemStart</code> event</label> <input type="checkbox" id="node-config-input-autoRefresh" style="display: inline-block; width: auto; vertical-align: top;"> </div> <div class="form-row"> <label for="node-config-input-delayCommands" style="width: auto; margin-right: 5px;">Add delay between commands (ms)</label> <input type="text" id="node-config-input-delayCommands" style="display: inline-block; width: auto; vertical-align: top;"> </div> <div class="form-row"> <label for="node-config-input-useWebsocket" style="width: auto; margin-right: 5px;">Use websocket (<b>not recommended</b>)</label> <input type="checkbox" id="node-config-input-useWebsocket" style="display: inline-block; width: auto; vertical-align: top;"> </div> <div class="form-row" style="height:20px; display: flex; align-items:center;"> <label for="node-config-input-color" style="width:auto; margin-right: 5px;"><i class="fa fa-paint-brush"></i> Use custom color</label> <input type="checkbox" id="node-config-input-colorEnabled" style="width:auto; margin-right: 5px;"> <input type="color" id="node-config-input-color" style="width: 39px"> </div> </div> </div> </script> <script type="text/html" data-help-name="hubitat config"> <p>Creates a new Hubitat configuration node</p> <h3>Details</h3> <p> Hubitat Maker API must be installed to use this node. All required fields can be found inside example URL </p> <p> In the following example, the <b>Application ID</b> is <b>12</b>: <code>http://localhost/apps/api/<b>12</b>/devices?access_token=...</code> </p> <p><b>Token</b> correspond to the <b>access_token</b> value. <p><b>Server</b> field must not contains the scheme (<code>http://</code> or <code>https://</code>).</p> <p> The <b>Webhook configuration</b> section allow to configure the URL on which the Hubitat hub will send events. Ensure that Node-RED is reachable by the Hubitat hub. If Node-RED is configured to use authentication, the URL should be <code>http://username:password@node_red_ip</code> with username/password URL encoded for special character. </p> <p> <b>Rebuild cache on Hubitat <code>systemStart</code> event</b> will rebuild the Node-RED cache for the Hubitat nodes. During the rebuild, if a difference is found between the current Hubitat attribute value and the previous cached value a new event msg for each changed attribute will be sent from the applicable nodes. If the current Hubitat attribute value is unchanged from the previous cached value, then no event msg will be sent. The <code>systemStart</code> event is triggered any time a Hubitat hub starts up after a power cycle or reboot. </p> <p> <b>Add delay between commands</b> will send commands sequentially with the delay added between commands. When triggering many commands at the same time, some commands may be dropped depending of the mesh and devices used. Sending them sequentially with a delay may help to improve reliability without needed to change flows. Note: Using value 0 will send command sequentially without delay. To restore the default parallel behavior, remove the value. </p> <p> <b>Use websocket</b> will switch the internal update mechanism to use Hubitat's websocket instead of webhook. Websocket does not perform event de-duplication and will send duplicate events which may require additional handling in Node-RED (such as use of an RBE node) to prevent unintended re-activation of logic. Websocket events contain less information, and may not have all event data that the webhook method does (e,g, lock code used for lock/unlock events, and the event <code>type</code> field). But using websocket should reduce the delay before receiving events by a few tens of milliseconds. The websocket update method should only be used by informed users, and only for specific needs <p> <b>Warning:</b> Using websocket is not officially supported, and it's at your own risk! </p> </p> <p> <b>Color</b> option will define a color for all nodes that use this configuration. </p> <p> <b>Warning:</b> This node add an unauthenticated webhook endpoint (default: <code>/hubitat/webhook</code>), which can be a security issue if you expose it on internet. Please read Node-RED documentation to add security on routes exposed by HTTP. </p> </script>