Last active
April 26, 2022 02:00
-
-
Save howellcc/a72c8d934937dc5d0bb9b5e1f6d5e015 to your computer and use it in GitHub Desktop.
Hubitat userscript for Grease/Tamper Monkey
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Hubitat UI enhancements | |
// @description Hubitat UI enhancements. This is a fork from https://github.com/surfingbytes/hubitat/blob/master/hubitat-ui-enhancements.js. All credit goes to surfingbytes. For more information see this post: https://community.hubitat.com/t/ui-enhancements/89773 | |
// @version 1.2 | |
// @grant unsafeWindow | |
// @include http://192.168.0.100/* | |
// @require https://code.highcharts.com/stock/highstock.js | |
// @require https://code.highcharts.com/stock/modules/data.js | |
// @require https://code.highcharts.com/stock/highcharts-more.js | |
// @require https://code.highcharts.com/stock/modules/exporting.js | |
// @require https://momentjs.com/downloads/moment.js | |
// @run-at document-end | |
// ==/UserScript== | |
const appsWithRules = ['Rule Machine', 'Rule Machine Legacy','Basic Rules', 'Simple Automation Rules']; | |
const capabilitiesToIgnoreInGraph = ['driver', 'batteryLastReplaced', 'lastCheckin', 'notPresentCounter', 'restoredCounter', 'colorName', 'mediaSource', 'status']; | |
(function () { | |
onLocationChanged(); | |
if (!document.title.includes('Hubitat')) { | |
document.title = `Hubitat - ${document.title}`; | |
} | |
var nav = document.getElementsByTagName("nav")[0]; | |
if (nav) { | |
if (![...nav.getElementsByClassName('mdl-navigation__link')].find(item => item.innerText.includes('Rule'))) { | |
var link = document.createElement("a"); | |
link.classList.add("mdl-navigation__link"); | |
link.href = "/installedapp/list?display=rulemachine"; | |
link.innerHTML = "<i class=\"material-icons he-apps_21\"></i>Rules"; | |
nav.insertBefore(link, nav.childNodes[8]); | |
} | |
} | |
if (window.location.href.endsWith('/installedapp/list?display=rulemachine')) { | |
document.title = 'Hubitat - Rules'; | |
document.getElementById('divHeaderPageName').innerHTML = 'Rules'; | |
nav.getElementsByClassName('is-active')[0].classList.remove('is-active'); | |
link.classList.add('is-active'); | |
var appTable = document.getElementById('app-table'); | |
var divs = appTable.getElementsByClassName('app-row-link'); | |
var ruleMachine = [...divs].find(div => div.children[0].innerText == 'Rule Machine'); | |
var ruleMachineId = ruleMachine.parentElement.getAttribute('data-id'); | |
var buttonsContainer = document.getElementById('buttonsContainer'); | |
buttonsContainer.children[0].remove(); | |
buttonsContainer.children[0].innerHTML = `<a href="/installedapp/createchild/hubitat/Rule-5.1/parent/${ruleMachineId}" class="btn btn-default btn-lg btn-block hrefElem mdl-button--raised mdl-shadow--2dp" style="text-align:left"> | |
<span class="he-add_2"></span> <span class="pre">Create New Rule</span> | |
</a>`; | |
var appRows = [...divs].filter(div => appsWithRules.some(app => app == div.children[0].innerText)); | |
var rules = appRows.flatMap(appRow => [...appRow.parentElement.parentElement.children].slice(1).map(rule => rule.children[2])).sort( | |
function(x, y) { | |
if (x.innerText < y.innerText) { return -1; } | |
if (x.innerText > y.innerText) { return 1; } | |
return 0; | |
}); | |
//appTable.children[0].children[0].children[1].remove(); | |
appTable.children[0].style.display = 'none'; | |
var tbody = appTable.children[1]; | |
tbody.innerHTML = ''; | |
var lastRoom = ''; | |
rules.forEach(rule => { | |
if (rule) { | |
var ruleName = rule.children[0].innerText; | |
var splitter = ruleName.includes('-') ? '-' : ' '; | |
var room = ruleName.split(splitter)[0].trim(); | |
if (room != lastRoom) { | |
var trRoom = document.createElement("tr") | |
trRoom.classList.add("group"); | |
trRoom.innerHTML = `<td style="display: none"></td><td><b>${room}</b></td>`; | |
tbody.append(trRoom); | |
} | |
lastRoom = room; | |
var tr = document.createElement("tr") | |
tr.innerHTML = `<td style="display: none"></td><td><div style="padding-left: 15px">${rule.innerHTML}</div></td>`; | |
tbody.append(tr); | |
} | |
}); | |
} | |
else if (window.location.href.endsWith('/mainPage/selectActions')) { | |
waitForRuleEditor(false); | |
} | |
else if (window.location.pathname == '/dashboards') { | |
var headers = document.body.getElementsByTagName('header'); | |
if (headers && headers[0]) headers[0].remove(); | |
} | |
else if (window.location.href.includes('/device/edit/')) { | |
var navigation = document.getElementsByTagName('main')[0].children[1].children[0].children[0].children[0]; | |
var graphLink = document.createElement('span'); | |
var href = navigation.children[navigation.children.length - 1].href; | |
graphLink.innerHTML = `<a href="${href}?display=graph" class=""><button class="mdl-button mdl-js-button mdl-button--raised " data-upgraded=",MaterialButton"><i class="he-info_1"></i> Graph</button></a>`; | |
navigation.appendChild(graphLink); | |
} | |
else if (window.location.href.includes('/device/events/') && window.location.href.includes('display=graph')) { | |
var deviceId = document.getElementById('events-table').getAttribute('data-device-id'); | |
var mdlTtable = document.getElementById('mdl-table'); | |
mdlTtable.innerHTML = '<div id="chart"></div>'; | |
try { | |
var req = new XMLHttpRequest(); | |
req.responseType = 'json'; | |
req.onload = function () { | |
try { | |
var capabilities = req.response.data.map(d => d[1]).filter((v, i, a) => a.indexOf(v) === i && capabilitiesToIgnoreInGraph.indexOf(v) === -1); | |
var chart = { | |
chart: { type: 'area' }, | |
title: { text: "Events" }, | |
//subtitle: { text: document.ontouchstart === undefined ? 'Click and drag in the plot area to zoom in' : 'Pinch the chart to zoom in' }, | |
//zoomType: 'x', | |
xAxis: { type: 'datetime', ordinal: false }, | |
yAxis: capabilities.map(capability => getYAxis(capability)), | |
series: capabilities.map(capability => { | |
var data = req.response.data.filter(d => d[1] == capability).map(d => { | |
var result = [(new moment(d[7], 'YYYY-MM-DD HH:mm:ss ZZZ')).valueOf(), getCapabilityValue(capability, d[2])]; | |
return result; | |
}).sort((d1, d2) => d1[0] - d2[0]); | |
var series = | |
{ | |
name: capability, | |
color: "#0071A7", | |
type: "area", | |
data: data, | |
step: "left", | |
yAxis: capabilities.indexOf(capability) | |
}; | |
return series; | |
}), | |
rangeSelector: { | |
//selected: 2 | |
buttons: [ | |
{ | |
type: 'minute', | |
count: 5, | |
text: '5m', | |
title: 'View 5 minutes' | |
}, { | |
type: 'minute', | |
count: 30, | |
text: '30m', | |
title: 'View 30 minutes' | |
}, { | |
type: 'hour', | |
count: 1, | |
text: '1h', | |
title: 'View 1 hour' | |
}, { | |
type: 'hour', | |
count: 4, | |
text: '4h', | |
title: 'View 4 hours' | |
}, { | |
type: 'hour', | |
count: 12, | |
text: '12h', | |
title: 'View 12 hours' | |
}, { | |
type: 'day', | |
count: 1, | |
text: '1d', | |
title: 'View 1 day' | |
}, { | |
type: 'week', | |
count: 1, | |
text: '1w', | |
title: 'View 1 week' | |
}, { | |
type: 'month', | |
count: 1, | |
text: '1M', | |
title: 'View 1 month' | |
}, { | |
type: 'month', | |
count: 3, | |
text: '3M', | |
title: 'View 3 months' | |
}, { | |
type: 'month', | |
count: 6, | |
text: '6M', | |
title: 'View 6 months' | |
}, { | |
type: 'ytd', | |
text: 'YTD', | |
title: 'View year to date' | |
}, { | |
type: 'year', | |
count: 1, | |
text: '1y', | |
title: 'View 1 year' | |
}, { | |
type: 'all', | |
text: 'All', | |
title: 'View all' | |
}] | |
}, | |
credits: { enabled: false }, | |
legend: { enabled: true }, | |
navigator: { | |
series: { type: 'area', step: 'left' } | |
} | |
}; | |
window.requestAnimationFrame = unsafeWindow.requestAnimationFrame; | |
Highcharts.stockChart("chart", chart); | |
} catch (ex) { | |
alert(ex); | |
} | |
}; | |
req.open('GET', `/device/events/${deviceId}/dataTablesJson?draw=1&columns[0][search][regex]=false&columns[1][search][regex]=false&columns[2][search][regex]=false&columns[3][search][regex]=false&columns[4][search][regex]=false&columns[5][search][regex]=false&columns[6][search][regex]=false&columns[7][name]=DATE&columns[7][search][regex]=false&order[0][column]=7&order[0][dir]=desc&start=0&length=200`); | |
req.send(); | |
} catch (ex) { | |
alert(ex); | |
} | |
} | |
else if (window.location.href.includes('/device/events/')) { | |
var searchBox = document.getElementById('searchBox').parentElement; | |
var navigationEvents = document.getElementsByTagName('main')[0].children[0].children[0].children[0]; | |
navigationEvents.appendChild(searchBox); | |
//this doesn't work and the horizontal scrollbar is still there (width stays at 100%) | |
//document.getElementById('events-table').style.width = 'calc(100% - 2px)'; | |
} | |
})(); | |
//can't be in DOMContentLoaded, it's plain text | |
if (window.location.pathname == '/hub/zigbee/getChildAndRouteInfo') { | |
if (!document.title) { | |
document.title = 'Hubitat - Route info'; | |
} else if (!document.title.includes('Hubitat')) { | |
document.title = `Hubitat - ${document.title}`; | |
} | |
//encoding fix doesn't work, it would have to change to proper HTML instead of plain text | |
//document.head.innerHTML = '<meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> '; | |
} | |
function getYAxis(capability) { | |
var axis = | |
{ | |
title: { | |
text: capability | |
} | |
}; | |
switch (capability) { | |
case 'switch': | |
case 'contact': | |
case 'motion': | |
case 'presence': | |
case 'mute': | |
axis.min = 0; | |
axis.max = 1; | |
axis.tickAmount = 2; | |
//axis.allowDecimals = false; | |
break; | |
case 'level': | |
case 'hue': | |
case 'saturation': | |
case 'position': | |
case 'battery': | |
axis.min = 0; | |
axis.max = 100; | |
axis.tickAmount = 5; | |
break; | |
case 'windowShade': | |
axis.min = 0; | |
axis.max = 2; | |
axis.tickAmount = 3; | |
//axis.allowDecimals = false; | |
break; | |
} | |
return axis; | |
} | |
function getCapabilityValue(capability, value) { | |
switch (capability) { | |
case 'switch': return value == 'on' ? 1 : 0; | |
case 'contact': return value == 'open' ? 1 : 0; | |
case 'motion': return value == 'active' ? 1 : 0; | |
case 'presence': return value == 'present' ? 1 : 0; | |
case 'windowShade': return value == 'open' ? 2 : value == 'closed' ? 0 : 1; | |
case 'mute': return value == 'muted' ? 1 : 0; | |
default: return parseInt(value); | |
} | |
} | |
function waitForRuleEditor(scroll) { | |
if (!document.getElementById('formApp')) { | |
setTimeout(function () { waitForRuleEditor(scroll); }, 300); | |
return; | |
} | |
var parent = document.getElementById('formApp').getElementsByClassName('mdl-grid')[0].children[0].children[0]; | |
var span = parent.children[0]; | |
var ruleText = span.innerHTML; | |
var rules = ruleText.split(/\r?\n/).filter(x => x !== ''); | |
var script = document.createElement("script"); | |
script.innerHTML = | |
`function ruleEdit(action, index) { | |
var ddl = document.getElementById(\`settings[$\{action\}Act]\`); | |
ddl.selectedIndex = action == 'delete' ? index : index + 1; | |
changeSubmit(ddl); | |
return false; | |
}`; | |
document.head.appendChild(script); | |
var style = document.createElement("style"); | |
style.innerHTML = | |
` | |
#ruleEditor { | |
border-collapse: collapse; | |
} | |
#ruleEditor a { | |
display: none; | |
} | |
#ruleEditor tr:hover { | |
background-color: #ffff99; | |
} | |
#ruleEditor tr:hover a{ | |
display: block; | |
} | |
`; | |
document.head.appendChild(style); | |
var rows = ''; | |
var table = document.createElement("table"); | |
table.id = "ruleEditor"; | |
rules.forEach((rule, i) => { | |
rows += `<tr> | |
<td>${rule}</td> | |
<td><a href="#" onclick="ruleEdit('insert', ${i})" title="Insert action before"><i class=\"material-icons\">add</i></a></td> | |
<td><a href="#" onclick="ruleEdit('edit', ${i})" title="Edit action"><i class=\"material-icons\">edit</i></a></td> | |
<td><a href="#" onclick="ruleEdit('delete', ${i})" title="Delete action"><i class=\"material-icons\">delete</i></a></td> | |
</tr>`; | |
}); | |
table.innerHTML = rows; | |
parent.replaceChild(table, span); | |
var a = document.createElement('a'); | |
a.id = 'actions'; | |
parent.appendChild(a, 1); | |
onRemove(table, function () { waitForRuleEditor(true); }); | |
if (scroll) { | |
document.getElementById('actions').scrollIntoView(); | |
} | |
} | |
function onLocationChanged() { | |
var oldHref = document.location.href; | |
var bodyList = document.querySelector("body"); | |
var observer = new MutationObserver(function (mutations) { | |
mutations.forEach(function (mutation) { | |
if (oldHref != document.location.href && document.location.href.endsWith('/mainPage/selectActions')) { | |
oldHref = document.location.href; | |
waitForRuleEditor(false); | |
} | |
}); | |
}); | |
var config = { | |
childList: true, | |
subtree: true | |
}; | |
observer.observe(bodyList, config); | |
} | |
function onRemove(element, callback) { | |
try { | |
const obs = new MutationObserver(mutations => { | |
for (const mutation of mutations) { | |
for (const el of mutation.removedNodes) { | |
var parent = element; | |
while (parent) { | |
if (el === parent) { | |
obs.disconnect(); | |
callback(); | |
} | |
parent = parent.parentElement; | |
} | |
} | |
} | |
}); | |
obs.observe(document.body, { | |
subtree: true, | |
childList: true, | |
}); | |
} catch (ex) { | |
alert(ex); | |
} | |
} |
1.2 updated by the original author 4/10/2022
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
1.1 was copied as is from the original author, I merely added "basic rules" and "simple automation rules" to the list of supported rules.