"""
render contentType: "text/html", data: html
}
def zwaveUtilsController() {
def javaScript = """
logToConsole = false
function loadAxios() {
return \$.getScript('https://unpkg.com/axios@1.1.2/dist/axios.min.js', function() {
console.log("axios loaded")
});
}
async function getZWaveDeviceIds() {
var devList = await getZwaveList()
if (!window.axios) {
await loadAxios()
}
console.log("Collecting zwave device ids")
var deviceIds = devList.reduce( (acc, val) => {
if (val.hubDeviceId) {
acc.push(val.hubDeviceId);
}
return acc;
}, [])
console.log("DeviceIds: " + deviceIds.toString())
return deviceIds
}
async function refreshDevicesList() {
var deviceIds = await getZWaveDeviceIds()
if ($enableDebug) {
console.log(deviceIds)
}
if ($enableDebug) {
var m = `Setting deviceList from zwave list: \${deviceIds.length}`
console.log(m)
hubLog("debug", m)
}
return updateDevicesInApp(deviceIds)
}
// Get transformed list of devices (see transformDevice) from hubitat zwave details webpage
async function getZwaveList() {
if (!window.axios) {
await loadAxios()
}
// Before 2.3.7, we have to parse html for data
// DEPRECATED - Will be removed when 2.3.8 is released; heMinVersion will become 2.3.7
// XXX: This dictionary comparison will break if version is 2.3.10.X
if ("${location.hub.firmwareVersionString}" <= "2.3.7") {
const instance = axios.create({
timeout: 5000,
responseType: "text" // iOS seems to fail (reason unknown) with document here
});
return instance
.get('/hub/zwaveInfo')
.then(response => {
var doc = new jQuery(response.data)
var deviceRows = doc.find('.device-row')
var results = []
deviceRows.each (
(index,row) => {
results.push(transformZwaveRow(row))
}
)
return results
})
.catch(error => {
console.error(error);
updateLoading("Error", error);
hubLog("error", `zwaveInfo: Error getting zwave Info: \${error}`)
} );
}
const instance = axios.create({
timeout: 5000
});
return instance
.get('/hub/zwaveDetails/json')
.then(response => {
return collectZwaveList(response.data)
})
.catch(error => {
console.error(error);
updateLoading("Error", error);
hubLog("error", `zwaveInfo: Error getting zwave Info: \${error}`)
} );
}
function collectZwaveList(zwaveDetailsJson) {
var zwaveList = [];
var zwNodes = zwaveDetailsJson.nodes;
var zwDevices = zwaveDetailsJson.zwDevices;
return zwNodes.map ( node => {
var zwDevice = zwDevices[node.nodeId]; // This will be null/undefined if there is no assigned device
if (!zwDevice) {
zwDevice = getZWDevicePlaceholder(node)
}
// "01 -> 08 -> 0C -> 1B 100kbps"
var routesText = node.route;
var routers = routesText ? routesText.split(' -> ') : []
var routersForDisplay = []
var routersList = []
var connectionSpeed = "Unknown"
if (routers.length > 0) {
var lastParts = routers.splice(-1,1) // Remove Last element (this device w/ speed)
routers.splice(0,1) // Remove first element (always 01; hub)
connectionSpeed = lastParts[0].split(' ')[1]
routersList = routers
routersForDisplay = routers.map(r => useHex() ? "0x" + r : parseInt("0x"+r))
}
if (routers.length == 0 && connectionSpeed != '') {
routersForDisplay = ['DIRECT']
}
var rtt = node.averageRtt + "ms";
var lwr = node.lwrRssi ? (node.lwrRssi + "dB") : "";
var statMap = {
"PER": node.per,
"RTT Avg": rtt,
"LWR RSSI": lwr,
"Neighbors": node.neighbors,
"Route Changes": node.routeChanges
};
var dni = zwDevice.deviceNetworkId;
var label = zwDevice.displayName;
var hubDeviceId = zwDevice.id;
var deviceLink = hubDeviceId ? "/device/edit/" + hubDeviceId : "";
var deviceData = {
id: dni, // hexId
id2: node.nodeId, // intId
devIdDec: node.nodeId,
metrics: statMap,
routers: routersForDisplay, // ['0x06']
routersList: routersList, // list of routers (hex), not including hub; ['06']
label: label, // device displayName
type: translateDeviceType(node.zwaveType), // "Power Switch Binary"
manufacturer: node.zwaveManufacturer,
deviceLink: deviceLink, // "/device/edit/2193"
hubDeviceId: hubDeviceId, // "2193"
deviceSecurity: node.security, // "None"
routeHtml: routersForDisplay.reduce( (acc, v, i) => (v == 'DIRECT') ? v : acc + ` ->\${v}`, "") + (routersForDisplay[0] == 'DIRECT' ? '' : ` -> \${useHex() ? "0x" + dni : node.nodeId}`) ,
deviceStatus: node.nodeState,
connection: connectionSpeed,
// commandClasses: node.commandClass,
zwNode: node,
zwDevice: zwDevice
}
return deviceData;
});
}
function getZWDevicePlaceholder(node) {
var zwDevice = {
"deviceNetworkId": node.nodeId.toString(16).toUpperCase(),
"isPlaceholder": true
}
return zwDevice;
}
function transformZwaveRow(row) {
var childrenData = row.children
var statsText = childrenData[1].innerHTML.trim().replace(' ',' , ')
var statsList = statsText.split(',').map(e => e.trim())
var statMap = {}
statsList.forEach( s => {
parts = s.split(':')
statMap[parts[0]] = parts[1].trim()
})
// "01 -> 08 -> 0C -> 1B 100kbps"
var routesText = childrenData[6].innerText ? childrenData[6].innerText.trim() : ''
var routers = routesText ? routesText.split(' -> ') : []
var routersForDisplay = []
var routersList = []
var connectionSpeed = "Unknown"
if (routers.length > 0) {
var lastParts = routers.splice(-1,1) // Remove Last element (this device w/ speed)
routers.splice(0,1) // Remove first element (always 01; hub)
connectionSpeed = lastParts[0].split(' ')[1]
routersList = routers
routersForDisplay = routers.map(r => useHex() ? "0x" + r : parseInt("0x"+r))
}
if (routers.length == 0 && connectionSpeed != '') {
routersForDisplay = ['DIRECT']
}
var nodeText = childrenData[0].innerText.trim()
var devId = (nodeText.match(/0x([^ ]+) /))[1]
var devIdDec = (nodeText.match(/\\(([0-9]+)\\)/))[1]
var devId2 = parseInt("0x"+devId)
var label = ""
var deviceLink = ""
var hubDeviceId = null
if (childrenData[4].innerText.trim() != '') {
label = childrenData[4].innerText.trim()
deviceLink = childrenData[4].firstElementChild.getAttribute('href')
hubDeviceId = deviceLink.split('/')[3]
}
var typeParts = childrenData[3].innerHTML.split(" ")
if (typeParts && typeParts.length >= 2) {
var type = translateDeviceType(typeParts[0])
var manufacturer = typeParts[1]
}
var deviceData = {
id: devId,
id2: devId2,
devIdDec: devIdDec,
node: nodeText.replace(' ', ' '),
metrics: statMap,
routers: routersForDisplay,
routersList: routersList,
label: label,
type: type,
manufacturer: manufacturer,
deviceLink: deviceLink,
hubDeviceId: hubDeviceId,
deviceSecurity: childrenData[5].innerText.trim(),
routeHtml: routersForDisplay.reduce( (acc, v, i) => (v == 'DIRECT') ? v : acc + ` -> \${v}`, "") + (routersForDisplay[0] == 'DIRECT' ? '' : ` -> \${useHex() ? "0x" + devId : devId2}`) ,
deviceStatus: childrenData[2].firstChild.data.trim(),
connection: connectionSpeed
}
return deviceData
}
function updateDevicesInApp(devices) {
var updateLink = "/installedapp/update/json"
var appLink = "${getAppLink()}"
var appId = "${getAppId()}"
const instance = axios.create({
timeout: 5000,
config: {headers: {"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"}}
});
var postData = {
"settings[deviceList]": devices.join(','),
formAction: "update",
id: appId,
version: 2,
appTypeId: '',
appTypeName: '',
currentPage: 'devicesPage',
// pageBreadcrumbs: '%5B%5D',
"deviceList.type": 'capability.*',
"deviceList.multiple": 'true',
deviceList: 'deviceList'
// referrer: '',
// url: `/installedapp/configure/\${appId}/devicesPage`
}
if ($enableDebug) {
console.log("Sending deviceList update")
console.log(postData)
}
return instance
.post(updateLink, serializeToURL(postData))
}
function serializeToURL( obj ) {
let str = Object.keys(obj).reduce(function(a, k){
a.push(k + '=' + encodeURIComponent(obj[k]));
return a;
}, []).join('&');
return str;
}
function translateDeviceType(deviceType) {
switch (deviceType) {
case "BASIC_TYPE_CONTROLLER": // 0x00
return "Basic Controler"
case "BASIC_TYPE_STATIC_CONTROLLER": // 0x03
return "Basic Static Controller"
case "BASIC_TYPE_SLAVE": // 0x03
return "Basic Slave"
case "BASIC_TYPE_ROUTING_SLAVE": // 0x04
return "Basic Routing Slave"
case "GENERIC_TYPE_AV_CONTROL_POINT": // 0x03
return "AV Control"
case "SPECIFIC_TYPE_DOORBELL":
return "Doorbell"
case "SPECIFIC_TYPE_SATELLITE_RECEIVER":
return "Satellite Receiver"
case "SPECIFIC_TYPE_SATELLITE_RECEIVER_V2":
return "Satellite Receiver V2"
case "SPECIFIC_TYPE_SOUND_SWITCH":
return "Sound Switch"
case "GENERIC_TYPE_DISPLAY": // 0x04
return "Display"
case "SPECIFIC_TYPE_SIMPLE_DISPLAY":
return "Simple Display"
case "GENERIC_TYPE_ENTRY_CONTROL": // 0x40
return "Entry Control"
case "SPECIFIC_TYPE_DOOR_LOCK":
return "Door Lock"
case "SPECIFIC_TYPE_ADVANCED_DOOR_LOCK":
return "Advanced Door Lock"
case "SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK":
return "Secure Keypad Door Lock"
case "SPECIFIC_TYPE_SECURE_KEYPAD_DOOR_LOCK_DEADBOLT":
return "Door Lock Keypad Deadbolt"
case "SPECIFIC_TYPE_SECURE_DOOR":
return "Secure Door"
case "SPECIFIC_TYPE_SECURE_GATE":
return "Secure Gate"
case "SPECIFIC_TYPE_SECURE_BARRIER_ADDON":
return "Secure Barrier Addon"
case "SPECIFIC_TYPE_SECURE_BARRIER_OPEN_ONLY":
return "Secure Barrier Open Only"
case "SPECIFIC_TYPE_SECURE_BARRIER_CLOSE_ONLY":
return "Secure Barrier Close Only"
case "SPECIFIC_TYPE_SECURE_LOCKBOX":
return "Secure Lockbox"
case "SPECIFIC_TYPE_SECURE_KEYPAD":
return "Secure Keypad"
case "GENERIC_TYPE_GENERIC_CONTROLLER": // 0x01
return "Generic Controller"
case "SPECIFIC_TYPE_PORTABLE_REMOTE_CONTROLLER":
return "Portable Remote Controller"
case "SPECIFIC_TYPE_PORTABLE_SCENE_CONTROLLER":
return "Portable Scene Controller"
case "SPECIFIC_TYPE_PORTABLE_INSTALLER_TOOL":
return "Portable Installer Tool"
case "SPECIFIC_TYPE_REMOTE_CONTROL_AV":
return "Remote Control AV"
case "SPECIFIC_TYPE_REMOTE_CONTROL_SIMPLE":
return "Remote Control Simple"
case "GENERIC_TYPE_METER": // 0x31
return "Generic Meter"
case "SPECIFIC_TYPE_SIMPLE_METER":
return "Simple Meter"
case "SPECIFIC_TYPE_ADV_ENERGY_CONTROL":
return "Adv Energy Control"
case "SPECIFIC_TYPE_WHOLE_HOME_METER_SIMPLE":
return "Whole Home Meter Simple"
case "GENERIC_TYPE_METER_PULSE": // 0x30
return "Generic Meter Pulse"
case "GENERIC_TYPE_REPEATER_SLAVE": //0x0F
return "Repeater Slave"
case "SPECIFIC_TYPE_REPEATER_SLAVE":
return "Repeater Slave"
case "SPECIFIC_TYPE_VIRTUAL_NODE":
return "Virtual Node"
case "GENERIC_TYPE_SECURITY_PANEL": // 0x17
return "Security Panel"
case "SPECIFIC_TYPE_ZONED_SECURITY_PANEL":
return "Zoned Security Panel"
case "GENERIC_TYPE_SEMI_INTEROPERABLE": // 0x50
return "Semi Interoperable"
case "SPECIFIC_TYPE_ENERGY_PRODUCTION":
return "Energy Production"
case "GENERIC_TYPE_SENSOR_ALARM": // 0xA1
return "Alarm Sensor"
case "SPECIFIC_TYPE_ADV_ZENSOR_NET_ALARM_SENSOR":
return "Adv Zensor Net Alarm Sensor"
case "SPECIFIC_TYPE_ADV_ZENSOR_NET_SMOKE_SENSOR":
return "Adv Zensor Net Smoke Sensor"
case "SPECIFIC_TYPE_BASIC_ROUTING_ALARM_SENSOR":
return "Basic Routing Alarm Sensor"
case "SPECIFIC_TYPE_BASIC_ROUTING_SMOKE_SENSOR":
return "Basic Routing Smoke Sensor"
case "SPECIFIC_TYPE_BASIC_ZENSOR_NET_ALARM_SENSOR":
return "Basic Zensor Net Alarm Sensor"
case "SPECIFIC_TYPE_BASIC_ZENSOR_NET_SMOKE_SENSOR":
return "Basic Zensor Net Smoke Sensor"
case "SPECIFIC_TYPE_ROUTING_ALARM_SENSOR":
return "Routing Alarm Sensor"
case "SPECIFIC_TYPE_ROUTING_SMOKE_SENSOR":
return "Routing Smoke Sensor"
case "SPECIFIC_TYPE_ZENSOR_NET_ALARM_SENSOR":
return "Zensor Net Alarm Sensor"
case "SPECIFIC_TYPE_ZENSOR_NET_SMOKE_SENSOR":
return "Zensor Net Smoke Sensor"
case "SPECIFIC_TYPE_ALARM_SENSOR":
return "Alarm Sensor"
case "GENERIC_TYPE_SENSOR_BINARY": // 0x20
return "Binary Sensor"
case "SPECIFIC_TYPE_ROUTING_SENSOR_BINARY":
return "Routing Sensor Binary"
case "GENERIC_TYPE_SENSOR_MULTILEVEL": // 0x21
return "Sensor Multilevel"
case "SPECIFIC_TYPE_ROUTING_SENSOR_MULTILEVEL":
return "Routing Sensor Multilevel"
case "SPECIFIC_TYPE_CHIMNEY_FAN":
return "Chimney Fan"
case "GENERIC_TYPE_STATIC_CONTROLLER": // 0x02
return "Static Controller"
case "SPECIFIC_TYPE_PC_CONTROLLER":
return "Pc Controller"
case "SPECIFIC_TYPE_SCENE_CONTROLLER":
return "Scene Controller"
case "SPECIFIC_TYPE_STATIC_INSTALLER_TOOL":
return "Static Installer Tool"
case "SPECIFIC_TYPE_SET_TOP_BOX":
return "Set Top Box"
case "SPECIFIC_TYPE_SUB_SYSTEM_CONTROLLER":
return "Sub System Controller"
case "SPECIFIC_TYPE_TV":
return "TV"
case "SPECIFIC_TYPE_GATEWAY":
return "Gateway"
case "GENERIC_TYPE_SWITCH_BINARY": // 0x10
return "Switch Binary"
case "SPECIFIC_TYPE_POWER_SWITCH_BINARY":
return "Power Switch Binary"
case "SPECIFIC_TYPE_SCENE_SWITCH_BINARY":
return "Scene Switch Binary"
case "SPECIFIC_TYPE_POWER_STRIP":
return "Power Strip"
case "SPECIFIC_TYPE_SIREN":
return "Siren"
case "SPECIFIC_TYPE_VALVE_OPEN_CLOSE":
return "Valve Open/Close"
case "SPECIFIC_TYPE_COLOR_TUNABLE_BINARY":
return "Binary Tunable Color Light"
case "SPECIFIC_TYPE_IRRIGATION_CONTROLLER":
return "Irrigation Controller"
case "GENERIC_TYPE_SWITCH_MULTILEVEL": // 0x11
return "Switch Multilevel"
case "SPECIFIC_TYPE_CLASS_A_MOTOR_CONTROL":
return "Class A Motor Control"
case "SPECIFIC_TYPE_CLASS_B_MOTOR_CONTROL":
return "Class B Motor Control"
case "SPECIFIC_TYPE_CLASS_C_MOTOR_CONTROL":
return "Class C Motor Control"
case "SPECIFIC_TYPE_MOTOR_MULTIPOSITION":
return "Motor Multiposition"
case "SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL":
return "Power Switch Multilevel"
case "SPECIFIC_TYPE_SCENE_SWITCH_MULTILEVEL":
return "Scene Switch Multilevel"
case "SPECIFIC_TYPE_FAN_SWITCH":
return "Fan Switch"
case "SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL":
return "Multilevel Tunable Color Light"
case "GENERIC_TYPE_SWITCH_REMOTE": // 0x12
return "Switch Remote"
case "SPECIFIC_TYPE_SWITCH_REMOTE_BINARY":
return "Switch Remote Binary"
case "SPECIFIC_TYPE_SWITCH_REMOTE_MULTILEVEL":
return "Switch Remote Multilevel"
case "SPECIFIC_TYPE_SWITCH_REMOTE_TOGGLE_BINARY":
return "Switch Remote Toggle Binary"
case "SPECIFIC_TYPE_SWITCH_REMOTE_TOGGLE_MULTILEVEL":
return "Switch Remote Toggle Multilevel"
case "GENERIC_TYPE_SWITCH_TOGGLE": // 0x13
return "Switch Toggle"
case "SPECIFIC_TYPE_SWITCH_TOGGLE_BINARY":
return "Switch Toggle Binary"
case "SPECIFIC_TYPE_SWITCH_TOGGLE_MULTILEVEL":
return "Switch Toggle Multilevel"
case "GENERIC_TYPE_THERMOSTAT": // 0x08
return "Thermostat"
case "SPECIFIC_TYPE_SETBACK_SCHEDULE_THERMOSTAT":
return "Setback Schedule Thermostat"
case "SPECIFIC_TYPE_SETBACK_THERMOSTAT":
return "Setback Thermostat"
case "SPECIFIC_TYPE_SETPOINT_THERMOSTAT":
return "Setpoint Thermostat"
case "SPECIFIC_TYPE_THERMOSTAT_GENERAL":
return "Thermostat General"
case "SPECIFIC_TYPE_THERMOSTAT_GENERAL_V2":
return "Thermostat General V2"
case "SPECIFIC_TYPE_THERMOSTAT_HEATING":
return "Thermostat Heating"
case "GENERIC_TYPE_VENTILATION": // 0x16
return "Ventilation"
case "SPECIFIC_TYPE_RESIDENTIAL_HRV":
return "Residential Hrv"
case "GENERIC_TYPE_WINDOW_COVERING": // 0x09
return "Window Covering"
case "SPECIFIC_TYPE_SIMPLE_WINDOW_COVERING":
return "Simple Window Covering"
case "GENERIC_TYPE_ZIP_NODE": // 0x15
return "Zip Node"
case "SPECIFIC_TYPE_ZIP_ADV_NODE":
return "Zip Adv Node"
case "SPECIFIC_TYPE_ZIP_TUN_NODE":
return "Zip Tun Node"
case "GENERIC_TYPE_WALL_CONTROLLER": // 0x18
return "Wall Controller"
case "SPECIFIC_TYPE_BASIC_WALL_CONTROLLER":
return "Basic Wall Controller"
case "GENERIC_TYPE_NETWORK_EXTENDER": // 0x05
return "Network Extender"
case "SPECIFIC_TYPE_SECURE_EXTENDER":
return "Secure Extender"
case "GENERIC_TYPE_APPLIANCE": // 0x06
return "Applicance"
case "SPECIFIC_TYPE_GENERAL_APPLIANCE":
return "General Appliance"
case "SPECIFIC_TYPE_KITCHEN_APPLIANCE":
return "Kitchen Appliance"
case "SPECIFIC_TYPE_LAUNDRY_APPLIANCE":
return "Laundry Appliance"
case "GENERIC_TYPE_SENSOR_NOTIFICATION": // 0x07
return "Notification Sensor"
case "SPECIFIC_TYPE_NOTIFICATION_SENSOR":
return "Notification Sensor"
default:
return deviceType
}
}
function hubLog(level,log) {
if (window.axios) {
const instance = axios.create({
timeout: 5000
});
if (logToConsole) { console.log(level + ":" + log)}
return instance
.post("${getAppLink("remoteLog")}", { level: level, log: log})
}
}
function updateLoading(msg1, msg2) {
\$('#loading1').text(msg1);
\$('#loading2').text(msg2);
if ($enableDebug) {
if (msg1 || msg2) {
hubLog("debug", `\${msg1} - \${msg2}`)
}
}
}
function updateHeaderMessage(msg) {
\$('#message1').text(msg)
}
function useHex() {
return "${settings?.nodeBase}" === "base16"
}
function hasDeviceAccess() {
return ${settings.permitDeviceAccess}
}
"""
render contentType: "application/javascript", data: javaScript.replaceAll('\t',' ')
}
def scriptController() {
def javaScript = """
const CMD_CLASS_Names = {
0x20: "Basic",
0x21: "Controller Replication",
0x22: "Application Status",
0x25: "Binary Switch",
0x26: "Multilevel Switch",
0x27: "All Switch (obsoleted)",
0x28: "Binary Toggle Switch (obsoleted)",
0x29: "Multilevel Toggle Switch (deprecated)",
0x2B: "Scene Activation",
0x2C: "Scene Actuator Configuration",
0x2D: "Scene Controller Configuration",
0x30: "Binary Sensor (deprecated)",
0x31: "Multilevel Sensor",
0x32: "Meter",
0x33: "Color Switch",
0x35: "Pulse Meter (deprecated)",
0x36: "Basic Tariff",
0x37: "HRV Status",
0x39: "HRV Control",
0x3A: "Demand Control Plan Configuration",
0x3B: "Demand Control Plan Monitor",
0x3C: "Meter Table Configuration",
0x3D: "Meter Table Monitor",
0x3E: "Meter Table Push Configuration",
0x3F: "Prepayment",
0x40: "Thermostat Mode",
0x41: "Prepayment Encapsulation",
0x42: "Thermostat Operating State",
0x43: "Thermostat Setpoint",
0x44: "Thermostat Fan Mode",
0x45: "Thermostat Fan State",
0x46: "Climate Control Schedule (deprecated)",
0x47: "Thermostat Setback",
0x48: "Rate Table Configuration",
0x49: "Rate Table Monitor",
0x4A: "Tariff Table Configuration",
0x4B: "Tariff Table Monitor",
0x4C: "Door Lock Logging",
0x4E: "Schedule Entry Lock (deprecated)",
0x50: "Basic Window Covering (obsoleted)",
0x51: "Move to Position Window Covering (obsoleted)",
0x53: "Schedule",
0x55: "Transport Service",
0x56: "CRC-16 Encapsulation (deprecated)",
0x57: "Application Capability (obsoleted)",
0x59: "Association Group Info",
0x5A: "Device Reset Locally",
0x5B: "Central Scene",
0x5E: "Z-Wave Plus Info",
0x60: "Multi Channel",
0x62: "Door Lock",
0x63: "User Code",
0x66: "Barrier Operator",
0x6C: "Supervision",
0x70: "Configuration",
0x71: "Notification (Alarm)",
0x72: "Manufacturer Specific",
0x73: "Powerlevel",
0x75: "Protection",
0x76: "Lock (deprecated)",
0x77: "Node Naming and Location",
0x79: "Sound Switch",
0x7A: "Firmware Update Meta Data",
0x7B: "Grouping Name (deprecated)",
0x7C: "Remote Association Activation (obsoleted)",
0x7D: "Remote Association Configuration (obsoleted)",
0x80: "Battery",
0x81: "Clock",
0x82: "Hail (obsoleted)",
0x84: "WakeUp",
0x85: "Association",
0x86: "Version",
0x87: "Indicator",
0x88: "Proprietary (obsoleted)",
0x89: "Language",
0x8A: "Time",
0x8B: "Time Parameters",
0x8C: "Geographic Location",
0x8E: "Multi Channel Association",
0x8F: "Multi Command",
0x90: "Energy Production",
0x92: "Screen Meta Data",
0x93: "Screen Attributes",
0x94: "Simple AV Control",
0x98: "Security",
0x9A: "IP Configuration (obsoleted)",
0x9B: "Association Command Configuration",
0x9C: "Alarm Sensor (deprecated)",
0x9D: "Alarm Silence",
0x9E: "Sensor Configuration (obsoleted)",
0x9F: "Security 2"
}
function loadScripts() {
\$.get('/ui2/js/hubitat.min.js')
updateLoading('Loading...','Getting script sources');
return \$.getScript('https://cdn.datatables.net/v/dt/dt-1.11.3/cr-1.5.5/fh-3.2.1/r-2.2.9/sp-1.4.0/sl-1.3.4/datatables.min.js')
.then(s => {
function numberSort(a,b) {
var token1a = a.split('-',2)[0].trim()
var token1b = b.split('-',2)[0].trim()
var vala = parseInt(token1a)
var valb = parseInt(token1b)
if (!vala && vala !== 0) return 1;
if (!valb && vala !== 0) return -1;
return vala < valb ? -1 : 1
}
jQuery.extend( jQuery.fn.dataTableExt.oSort, {
"initialNumber-asc": function ( a, b ) {
return numberSort(a,b);
},
"initialNumber-desc": function ( a, b ) {
return numberSort(a,b) * -1;
},
})
})
;
}
// Get data from zwaveNodeDetail endpoint (built-in)
function getZwaveNodeDetail() {
const instance = axios.create({
timeout: 5000
});
return instance
.get('/hub/zwaveNodeDetail')
.then(response => {
//if ($enableDebug) console.log (`Response: \${JSON.stringify(response)}`)
return response.data
})
.catch(error => {
console.error(error);
updateLoading("Error", error);
hubLog("error", `zwaveNodeDetail: Error getting zwave details: \${error}`)
} );
}
// Get details from devices app endpoint and merge into devList
function getDeviceDetails() {
const instance = axios.create({
timeout: 5000
});
return instance
.get('${getAppLink("deviceDetails")}')
.then(response => {
//if ($enableDebug) console.log (`Response: \${JSON.stringify(response)}`)
return response.data
})
.catch(error => {
console.error(error);
updateLoading("Error", error);
hubLog("error", `zwaveNodeDetail: Error getting zwave details: \${error}`)
} );
}
async function getData() {
var devList = await getZwaveList()
var fullNameMap = devList.reduce( (acc,val) => {
acc[useHex() ? `0x\${val.id}` : val.id2]= `\${useHex() ? `0x\${val.id}` : val.id2} - \${val.label}`;
return acc;
}, {});
// Build routersFor map
var routersFor = devList.reduce( (acc, val) => {
var myRouters = val.routersList
var fullName = fullNameMap[useHex() ? `0x\${val.id}` : val.id2]
myRouters.map(r => {
//console.log(`\${r} is a router for \${fullName}`)
if (!acc.has(r)) {
acc.set(r, [])
}
l = acc.get(r)
l.push(fullName)
})
return acc
}, new Map())
// Pseudo entry for direct-connected devices
fullNameMap.DIRECT = 'DIRECT'
updateLoading('Loading.','Getting device detail');
var nodeDetails = await getZwaveNodeDetail()
updateLoading('Loading..', 'Building Neighbors Lists')
buildNeighborsLists(fullNameMap, nodeDetails)
var deviceDetails = {}
if (hasDeviceAccess()) {
deviceDetails = await getDeviceDetails()
var missingNonRepeaters = devList.reduce( (acc, val) => {
if (val.hubDeviceId && val.id2) {
var hubId = val.hubDeviceId.toString()
var zwId = val.id2.toString()
var detail = deviceDetails[val.hubDeviceId.toString()]
if (detail && detail.listening === false && !nonRepeaters.has(zwId)) {
acc.push(zwId)
}
}
return acc
}, [])
if (missingNonRepeaters.length > 0) {
hubLog("info", "Non-listening devices missing: Adding to nonRepeaters: " + missingNonRepeaters.toString())
missingNonRepeaters.forEach(item => nonRepeaters.add(item))
}
}
var tableContent = devList.map( dev => {
var routersFull = dev.routers.map(router => fullNameMap[router] || `\${router} - UNKNOWN`)
var detail = nodeDetails[dev.id2.toString()]
var devDetail
if (dev.hubDeviceId && hasDeviceAccess()) {
devDetail = deviceDetails[dev.hubDeviceId.toString()]
if (devDetail) {
dev.commandClasses = devDetail.inCC.concat(devDetail.inCCSec)
}
}
var variance = 0
var stdDev = "0.00"
var count = detail.transmissionCount
if (count > 0) {
var totalSquared = Math.pow(detail.sumOfTransmissionTimes,2)
var sumOfTransmissionTimesSquared = detail.sumOfTransmissionTimesSquared
var ss = (sumOfTransmissionTimesSquared - (totalSquared/count)).toFixed(0)
variance = (ss/count).toFixed(2)
stdDev = Math.sqrt(variance).toFixed(2)
}
dev.metrics.rtt_variance = variance
dev.metrics.std_dev = stdDev
return {...dev, 'routerOf': routersFor.get(dev.id), 'routersFull': routersFull, 'detail': detail, 'devDetail': devDetail}
})
return tableContent
}
var deviceDetailsMap = new Map() // cache/memoize data for each device (deviceId => map)
// Get data from device settings screen if we can't get it somewhere else
function getDeviceInfo(devId) {
console.log("Getting Device Detail for " + devId)
if (!devId) {
hubLog("info", "No hub device for " + devId);
return Promise.resolve({});
}
if (deviceDetailsMap.has(devId)) {
// console.log("Returning details for " + devId + " from cache")
return Promise.resolve(deviceDetailsMap.get(devId))
}
const instance = axios.create({
timeout: 5000,
responseType: "text" // iOS seems to fail (reason unknown) with document here
});
return instance
.get('/device/edit/' + devId)
.then(response => {
var doc = new jQuery(response.data)
var deviceData = doc.find('#data-label ~ td li')
var details = {}
deviceData.map (
(index,row) => {
var kvp = row.innerText.split(":")
details[kvp[0].trim()] = kvp[1].trim()
}
)
deviceDetailsMap.set(devId, details)
return details
})
.catch(error => { console.error(error);
hubLog("error", `Error getting device detail: \${error}`)
} );
}
function findDeviceByDecId(devId) {
return tableContent.find( row => row.id2 == devId)
}
function findDeviceByHexId(devId) {
return tableContent.find( row => row.id == devId)
}
function decodeSpeed(val) {
return val == (undefined || '') ? 'unknown'
: val == '01' ? '9.6 kbps'
: val == '02' ? '40 kbps'
: val == '03' ? '100 kbps'
: 'UNKNOWN'
}
// Map dev id -> [neighbors]
var neighborsMap = new Map()
// Map dev id -> [seen by]
var neighborsMapReverse = new Map()
// List of ids that are not repeaters
var nonRepeaters = new Set()
function buildNeighborsLists(fullnameMap, nodeData) {
neighborsMap = new Map()
neighborsMapReverse = new Map()
nonRepeaters = new Set()
Object.entries(nodeData).forEach( e1 => {
var devId = e1[0]
var detail = e1[1]
if (detail.neighbors) {
var hasNonHubNeighbor = false;
Object.entries(detail.neighbors).forEach( e2 => {
var neighborId = e2[0]
var neighborDetail = e2[1]
if (!neighborsMap.has(devId)) {
neighborsMap.set(devId, [])
}
n = neighborsMap.get(devId)
n.push(neighborId)
if (!neighborsMapReverse.has(neighborId)) {
neighborsMapReverse.set(neighborId, [])
}
r = neighborsMapReverse.get(neighborId)
r.push(devId)
if (neighborDetail.repeater == '0') {
nonRepeaters.add(neighborId)
}
var nHex = ('00'+parseInt(neighborId).toString(16)).slice(-2).toUpperCase()
if (!fullnameMap[nHex]) {
fullnameMap[nHex] = `\${nHex} - UNKNOWN`
}
if (!hasNonHubNeighbor && parseInt(neighborId) > 5) {
hasNonHubNeighbor = true
}
})
if (!hasNonHubNeighbor) {
// hubLog("debug", "No neighbors: Adding to nonRepeaters: " + devId)
nonRepeaters.add(devId)
}
}
})
}
async function displayRowDetail(row) {
var devId = row.id()
var neighborList = []
var deviceData = tableContent.find( row => row.id == devId)
var data = row.data()
// On demand data
if (!data.commandClasses && data.hubDeviceId) {
var detailData = await getDeviceInfo(data.hubDeviceId)
var inClusters = detailData.inClusters && detailData.inClusters.length > 1 ? detailData.inClusters.split(',') : []
var secureInClusters = detailData.secureInClusters && detailData.secureInClusters.length > 1 ? detailData.secureInClusters.split(',') : []
var commandClasses = inClusters.concat(secureInClusters)
// Update data
console.log("Command classes is: " + commandClasses)
data.commandClasses = commandClasses
}
var html = '
'
// Header Row
html += '
'
html += '
Repeaters
'
if (deviceData.routerOf && deviceData.routerOf.length > 0) {
html+= '
Routing For
'
}
html += '
Neighbors
NeighborOf
'
if (data.commandClasses && data.commandClasses.length > 0) {
html += '
Command Classes
'
}
html += '
Actions
'
html += '
'
// End Header Row
html += '
'
// Repeaters
html += '
'
html += deviceData.routersFull.join(' ')
html += '
'
// RoutingFor
if (deviceData.routerOf && deviceData.routerOf.length > 0) {
html += '
'
html += deviceData.routerOf.join(' ')
html += '
'
}
// Neighbors
html += '
'
var neighborListStyle = "list-style-type:none;margin:0;padding:0"
var neighborList = neighborsMap.get(deviceData.id2.toString())
var neighborOfList = neighborsMapReverse.get(deviceData.id2.toString())
if (neighborList && neighborList.length > 0) {
html += `
`
neighborList.forEach( (neighborId) => {
var symetry = false
if (neighborOfList && neighborOfList.includes(neighborId)) {
symetry = true
}
var color
if (!symetry) { color = "orange"}
html += `
`
if (neighborId == 1) {
html += useHex() ? '0x0' : '' // 0-pad for hex value
html += `\${neighborId} - HUB`
} else {
var deviceData = findDeviceByDecId(neighborId)
html += useHex() ? `0x\${deviceData.id}` : deviceData.id2
html += ` - \${deviceData.label}`
if (nonRepeaters.has(deviceData.id2.toString())) {
html += '*'
}
// TODO: If neighborId is a router
}
html += '
'
})
html += '
'
}
html += '
'
// NeighborOf
html += '
'
if (neighborOfList && neighborOfList.length > 0) {
html += `
`
neighborOfList.forEach( (neighborId) => {
var symetry = false
if (neighborList && neighborList.includes(neighborId)) {
symetry = true
}
var color
if (!symetry) { color = "orange"}
html += `
`
if (neighborId == 1) {
html += useHex() ? '0x' : ''
html += `\${neighborId} - HUB`
} else {
var deviceData = findDeviceByDecId(neighborId)
html += useHex() ? `0x\${deviceData.id}` : deviceData.id2
html += ` - \${deviceData.label}`
if (nonRepeaters.has(deviceData.id2.toString())) {
html += '*'
}
// TODO: If deviceData.id is a router for neighborId
}
html += '
'
})
html += '
'
}
html += '
'
// Command Classes
if (data.commandClasses && data.commandClasses.length > 0) {
html += '
'
data.commandClasses.forEach( cc => {
html += cc
var ccVal = Number(cc)
if (CMD_CLASS_Names[ccVal]) {
html += ` - \${CMD_CLASS_Names[ccVal]}`
}
html += " "
});
html += '
'
}
html += '
'
if ($enableDebug) {
html += ''
var pretty = JSON.stringify(data.detail,null,'JSONS')
html += '
zwave NodeDetail
'
html += pretty.replace(/JSONS/g, ' ')
html += '
'
if (data.devDetail) {
pretty = JSON.stringify(data.devDetail,null,'JSONS')
html += '
Device Detail
'
html += pretty.replace(/JSONS/g, ' ')
html += '
'
} else {
html += '
Device Detail
NO DATA - no auth or not zwave?
'
}
}
if (data.commandClasses && !data.commandClasses.includes('0x84')) {
html += ``
}
html += '
'
html += '
'
html += '
*Device is a non-repeater
'
html += '
'
return html
}
function showDetailDebug(btn) {
\$(btn.parentElement).find('.debug-content').show()
}
function zwaveNodeRepair(zwaveNodeId) {
\$("#close-zwave-repair").attr("disabled", true)
\$("#abort-zwave-repair").attr("disabled", false)
if (dialogPolyfill && !zwaveRepairStatus.showModal) {
dialogPolyfill.registerDialog(zwaveRepairStatus);
}
\$.ajax({
url: "/hub/zwaveNodeRepair2?zwaveNodeId="+zwaveNodeId,
type: "GET",
success: function (data) {
repairUpdateInterval = setInterval(checkZwaveRepairStatus, 3000)
\$("#zwave-repair-status").html('')
if (zwaveRepairStatus.showModal) {
zwaveRepairStatus.showModal();
}
},
error: function (data) {
}
});
};
function checkZwaveRepairStatus(){
\$.ajax({
url: "/hub/zwaveRepair2Status",
type: "GET",
dataType: 'JSON',
success: function (data) {
\$("#zwave-repair-status").html(data.html)
if(data.stage === "IDLE"){
\$("#close-zwave-repair").attr("disabled", false)
\$("#abort-zwave-repair").attr("disabled", true)
clearInterval(repairUpdateInterval)
} else {
\$("#close-zwave-repair").attr("disabled", true)
\$("#abort-zwave-repair").attr("disabled", false)
}
},
error: function (data) {
}
});
}
function labelTopologyHeads(sel, ttClass) {
sel.each( (i, data) => {
var td = \$(data)
var str = data.innerHTML
//console.log(str)
if (str.match(/[A-F0-9]/)) {
if (str == "01") {
td.prop("aria-label","HUB")
td.addClass("tooltip")
td.append(`HUB`)
} else {
var d = findDeviceByHexId(str)
if (d != null) {
td.prop("aria-label", d.label)
td.addClass("tooltip")
td.append(`\${d.label}`)
}
}
}
})
}
function labelTopologyCells(index, row, labels, ttClass) {
row.find('td:nth-child(n+2)').each( (i, o) => {
var seen = "not seen";
if (o.bgColor == 'white') return;
if (o.bgColor == 'blue') seen = "seen";
var myLabel = labels[index]
var dstLabel = labels[i]
var td = \$(o)
td.prop("aria-label", myLabel + " -> " + dstLabel + ":" + seen)
td.addClass("tooltip")
td.append(`\${myLabel + " -> " + dstLabel + ":" + seen}`)
})
}
function getTopologyModal() {
if (dialogPolyfill && !topologyDialog.showModal) {
dialogPolyfill.registerDialog(topologyDialog);
}
\$.ajax({
url: "/hub/zwaveTopology",
type: "GET",
success: function(result) {
\$("#zwave-topology-table").html(result);
topologyDialog.showModal();
// Insert tooltips
var topr = \$('#topologyDialog table tbody tr:nth-child(1) td:nth-child(n+2)')
var c1 = \$('#topologyDialog table tbody tr:nth-child(n+1) td:nth-child(1)')
var deviceHexIds = topr.map( function() { return this.innerHTML})
var deviceLabels = deviceHexIds.map( (i,o) => { if (o === '01') {return "HUB" } else return findDeviceByHexId(o).label })
labelTopologyHeads(topr, "tooltiptexttop")
labelTopologyHeads(c1, "tooltiptextright")
var tRows = \$('#topologyDialog table tbody tr:nth-child(n+2)')
tRows.each ( (i,row) => {
labelTopologyCells(i,\$(row),deviceLabels, "tooltiptexttop")
})
}
});
}
function cancelRepair() {
\$.ajax({
url: "/hub/zwaveCancelRepair",
type: "GET",
success: function(result) {
}
});
}
function closeRepair() {
var dialog = document.querySelector('#zwaveRepairStatus')
dialog.close()
}
function closeTopology() {
var dialog = document.querySelector('#topologyDialog')
dialog.close()
}
function showAllTopology() {
\$('#topologyDialog table tbody tr td').show()
\$('#topologyDialog table tbody tr').show()
\$('#hideNonRepeatersBtn').show()
\$('#showNonRepeatersBtn').hide()
}
function hideNonRepeaters() {
topr = \$('#topologyDialog table tbody tr:nth-child(1)') // Get the top row with nodes (hex starting in position 2)
rowItems = topr[0].innerText.split(/\\s+/) // Split into a list
rowItems.slice(2).forEach( (item,index) => {
if(item.match(/[A-F0-9]/)) {
\$('#hideNonRepeatersBtn').hide()
\$('#showNonRepeatersBtn').show()
if ($enableDebug) console.log(`Testing \${item}`)
const d = findDeviceByHexId(item)
if (nonRepeaters.has(d.id2.toString())) {
if ($enableDebug) console.log(`\${item} is not a repeater; hiding`)
\$(`#topologyDialog table tbody tr td:nth-child(\${index+3})`).hide()
\$(`#topologyDialog table tbody tr:nth-child(\${index+3})`).hide()
} else {
if ($enableDebug) console.log('not in nonrepeaters list')
}
var neighborOfMap = neighborsMapReverse.get(d.id2.toString())
if (!neighborOfMap || neighborOfMap.length == 0) {
if ($enableDebug) console.log(`\${item} is not seen by any other device; hiding`)
\$(`#topologyDialog table tbody tr td:nth-child(\${index+3})`).hide()
\$(`#topologyDialog table tbody tr:nth-child(\${index+3})`).hide()
} else {
if ($enableDebug) console.log(`has neighbors: ${neighborOfMap}`)
}
}
})
}
// For embeded mode, load the app into the app screen
function loadApp(appURI) {
const instance = axios.create({
timeout: 5000,
responseType: "document"
});
return instance
.get(appURI)
.then(response => {
var doc = new jQuery(response.data)
// Merge head from fetched content into current page
var h = doc.find('head').children()
\$('head').append(h)
// Hide current page content and add/show the fetched doc
var c = doc.find('body').children()
\$('main > :first-child').children().hide()
\$('main > :first-child').append(c)
var currentPage = \$('#currentPage').val()
history.pushState({currentPage: currentPage, previousPage: null, statsLoaded: true, appURI: appURI}, "View Hub Stats", "?page=view&debug=true")
})
.catch(error => { console.error(error); updateLoading("Error", error);} );
}
window.onpopstate = function(event) {
if (event.state == null) {
return
}
if (event.state.statsLoaded) {
loadScripts().then( r => loadApp(event.state.appURI).then(d => doWork()))
} else {
location.reload()
}
}
\$.ajaxSetup({
cache: true
});
var tableContent;
var tableHandle;
if ( "${settings?.embedStyle}" != 'inline') {
\$(document).ready(doWork())
}
function searchPanesList() {
var panes = ['Repeater', 'Status', 'Security', 'Connection Speed', 'RTT Avg', 'RTT StdDev', 'LWR RSSI', 'Device Type', 'Manufacturer']
if (hasDeviceAccess) {
panes.push('listening')
panes.push('Beaming')
panes.push('FLiRS')
panes.push('Z-Wave Plus')
}
return panes
}
function doWork() {
return loadScripts().then(function() {
hubLog("info", "UserAgent: " + navigator.userAgent)
updateLoading('Loading..','Getting device data');
return getData().then( r => {
// console.log(list)
tableContent = r;
sendDebugData()
// Setup State handler
\$('#mainTable').on('requestChild.dt', async function(e, row) {
if (row.data().hubDeviceId != '') {
var content = await displayRowDetail(row)
row.child(content).show();
}
} );
updateLoading('Loading..','Creating table');
var idCol = useHex() ? 'id' : 'id2';
tableHandle = \$('#mainTable').DataTable({
data: tableContent,
rowId: 'id2',
stateSave: ${settings?.stateSave},
order: [[1,'asc']],
columns: [
//{ data: 'networkType', title: 'Type', searchPanes: { preSelect:['ZWAVE','ZIGBEE']} },
{
"className": 'details-control',
"orderable": false,
"data": null,
"defaultContent": '