/** * Rule Machine Manager * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * View the full changelog here: * https://raw.githubusercontent.com/joshlobe/hubitat/main/rule_machine_manager/changelog.txt */ // Define application definition( name: "Rule Machine Manager", namespace: "ruleMachineManager", author: "Josh Lobe", description: "Visual Interface for Managing Rules from Various Applications.", category: "Convenience", importUrl: "https://raw.githubusercontent.com/joshlobe/hubitat/main/rule_machine_manager/rule_machine_manager.groovy", iconUrl: "", iconX2Url: "" ) // Import helpers import hubitat.helper.RMUtils import hubitat.helper.ColorUtils import groovy.json.JsonSlurper import groovy.json.JsonOutput import groovyx.net.http.HttpResponseException // Define versions def version() { "3.2" } def js_version() { "3.2" } def css_version() { "3.2" } // Define globals (note: do not use def (scope)) ruleMap = [:] ruleMapNames = [:] newRulesCheck = [] populateRuleList() // Begin preferences preferences { // Define mappings to handle javascript requests mappings { path("/updateSettings") { action: [ POST: "updateSettings" ] } } // Begin page page(name: "mainPage", install: true, uninstall: true) { section { /************************************************** // Begin page html **************************************************/ if( logDebugEnable ) log.debug "Beginning Page HTML..." html = "" /************************************************** // Page notices (new/deleted rules, new/deleted machines) **************************************************/ // Check if there are user rules defined userRules = settings?.userArray ? settings?.userArray : '' // If user rules are found if( userRules != '' ) { if( logDebugEnable ) log.debug "Using User Created Rules for Containers..." // Decode rules userRules = new JsonSlurper().parseText( userRules ) // Check for newly supported rule machine types checkNewMachines = checkForNewMachines( userRules ) if( checkNewMachines ) { html += checkNewMachines } // Check for deleted rule machine types checkDeletedMachines = checkForDeletedMachines( userRules ) if( checkDeletedMachines ) { html += checkDeletedMachines } // Check for new rules checkNewRules = checkForNewRules( userRules ) if( checkNewRules ) { html += checkNewRules } // Check for deleted rules checkDeletedRules = checkForDeletedRules( userRules ) if( checkDeletedRules ) { html += checkDeletedRules } // If there are new rules not yet saved in RMM, add them to the original rules container userRules.containers.each{ if( it.slug == "original-rules" ) { def thisRules = it.rules newRulesCheck.each{ thisRules.push( it.key ) } } } } // Else use default rules else { if( logDebugEnable ) log.debug "Using Default Rules for Containers..." // Decode rules buildRules = defaultUserArrayText() userRules = new JsonSlurper().parseText( buildRules ) } /************************************************** // Page header **************************************************/ // Define variables welcome_nag = userRules.welcome_nag ? userRules.welcome_nag : 'true' ruleMachines = userRules.containsKey( 'rule_machines' ) ? JsonOutput.toJson( userRules.rule_machines ) : JsonOutput.toJson( new JsonSlurper().parseText( defaultUserArrayText() ).rule_machines ) checkMachines = userRules.containsKey( 'check_machines' ) ? JsonOutput.toJson( userRules.check_machines ) : JsonOutput.toJson( new JsonSlurper().parseText( defaultUserArrayText() ).check_machines ) activeMachines = userRules && userRules.hide_machines == 'true' ? 'active' : '' machinesText = userRules && userRules.hide_machines == 'true' ? 'Show Machine Names' : 'Hide Machine Names' activeCounts = userRules && userRules.hide_counts == 'true' ? 'active' : '' countsText = userRules && userRules.hide_counts == 'true' ? 'Show Counts' : 'Hide Counts' activeFilters = userRules && userRules.hide_filters == 'true' ? 'active' : '' filtersText = userRules && userRules.hide_filters == 'true' ? 'Show Filters' : 'Hide Filters' hideGlobalFilter = userRules && userRules.hide_filters == 'true' ? 'display:none;' : '' // Begin header area html += "
" html += "
" html += "add_circleCreate Container" html += "" html += "
" html += "
" // Define hidden inputs html += "" html += "" html += "" html += "" html += "" html += "" html += "" // Create hubitat hidden form input and variables html += '' html += '' html += '' // Header area icons html += "settingsApp Options" html += "helpHelp" html += "rocketQuick Save" html += "ballot${countsText}" html += "table_rows${filtersText}" html += "data_array${machinesText}" html += "check_circleDone" html += "
" html += "
" /************************************************** // Page containers **************************************************/ if( logDebugEnable ) log.debug "Creating Page Containers..." // Create temp array for duplication comparisons tempArray = [] // Begin page containers html += "
" // Loop each container userRules.containers.each{ if( logDebugEnable ) log.debug "Creating Container ${it.name}..." // These are applied to the main container, and must be defined first container_color = ( it.container_color && it.container_color != '' && it.container_color != 'null' ) ? it.container_color : '#FFFFFF' container_opacity = ( it.container_opacity && it.container_opacity != '' && it.container_opacity != 'null' ) ? it.container_opacity : '1' // Convert colors to rgb rgb = hubitat.helper.ColorUtils.hexToRGB( container_color ) rgba_string = "rgba(" + rgb[0] + ", " + rgb[1] + ", " + rgb[2] + ", " + container_opacity + ")" // Begin this container html += "
" // Get values from database title_color = ( it.title_color && it.title_color != '' && it.title_color != 'null' ) ? it.title_color : '#000' title_opacity = ( it.title_opacity && it.title_opacity != '' && it.title_opacity != 'null' ) ? it.title_opacity : '1' title_bold = ( it.title_bold && it.title_bold == 'true' ) ? 'true' : 'false' font_weight = ( it.title_bold && it.title_bold == 'true' ) ? 'bold' : 'normal' // Set hidden input fields for this container html += "" html += "" html += "" html += "" html += "" // Container title area html += "

" html += '
' // Container title html += "${it.name}" html += "" // Add rule count (hide if user selected ) hide_counts = userRules.hide_counts == 'true' ? 'display: none;' : '' html += "" // Add container filter (hide if user selected ) hide_filters = userRules.hide_filters == 'true' ? 'display: none;' : '' html += "" html += '
' html += '
' // Add container options icon html += "" html += "settings" html += "Container Options" html += "" // Add Expand/Collapse container icon toggle_icon = it.visible == true ? 'file_upload' : 'file_download' toggle_text = it.visible == true ? 'Collapse' : 'Expand' html += "" html += "${toggle_icon}" html += "${toggle_text}" html += "" // Add move icon html += "" html += "open_with" html += "Move" html += "" // Begin submenu dropdown menu html += '' html += '
' html += "

" /************************************************** // Container rules **************************************************/ if( logDebugEnable ) log.debug "Creating Rule List For Container ${it.name}..." html += "" if( logDebugEnable ) log.debug "Finished Creating Rule List for Container ${it.name}..." html += "
" if( logDebugEnable ) log.debug "Finished Creating Container ${it.name}..." } html += '
' /************************************************** // Include scripts and styles **************************************************/ // Include jquery html += "" html += "" html += "" // Include confirm overlay html += "" // Include material icons html += "" // Add scripts/styles for page html += "" html += "" /************************************************** // Page render html **************************************************/ // Display page paragraph "${html}" paragraph "
" /************************************************** // Page footer area **************************************************/ if( logDebugEnable ) log.debug "Creating Page Footer..." // Log file paragraph "

Use the debug tool to generate information for the log file.

" input "logDebugEnable", "bool", required: false, title: "Enable Debug Logging (auto off in 15 minutes)", defaultValue: false paragraph "
" // Footer area grid = "
" grid += "
" grid += "

" // Footer notes grid += "NOTE: Remember to click \"Done\" after modifying any options or resetting the rules.
" grid += "GET HELP: " grid += "Hubitat Community Thread
" grid += "

" grid += "
" grid += "
" // Check if js and css files are stored locally js_installed = 'Not Found in File Manager' css_installed = 'Not Found in File Manager' try { httpGet([ uri: "http://127.0.0.1:8080/local/rule_machine_manager.js", contentType: "text/html" ]) { resp -> if (resp.success) { js_installed = 'Found in File Manager' } } } catch (Exception e) { log.warn "Call to check js file in file manager failed: ${e.message}" } try { httpGet([ uri: "http://127.0.0.1:8080/local/rule_machine_manager.css", contentType: "text/html" ]) { resp -> if (resp.success) { css_installed = 'Found in File Manager' } } } catch (Exception e) { log.warn "Call to check css file in file manager failed: ${e.message}" } // Footer details area def getStateSpan = app.getInstallationState() == 'COMPLETE' ? 'Enabled' : 'Not Enabled' def getTokenSpan = state.accessToken != null && state.accessToken != '' ? 'Enabled' : "Not Enabled" grid += "" grid += "" grid += "" grid += "" grid += "" grid += "" grid += "
App Version: ${version()}
App State: ${getStateSpan}
JS File: ${js_installed}
CSS File: ${css_installed}
oAuth: ${getTokenSpan}
" grid += "
" grid += "
" // Display footer paragraph "${grid}" if( logDebugEnable ) log.debug "Finished Page Footer..." if( logDebugEnable ) log.debug "Finished Page HTML..." } } } // Define default container and array of rules not yet saved in the user settings def defaultUserArrayText() { // Build names for hidden input buildNames = '[' if( ruleMapNames ) { ruleMapNames.each{ buildNames += '"' + it.key + '",' } buildNames = buildNames.substring( 0, buildNames.lastIndexOf( "," ) ) } buildNames += ']' // Build string of rule ids, remove trailing comma buildRules = '[' if( ruleMap ) { ruleMap.each{ buildRules += '"' + it.key + '",' } buildRules = buildRules.substring( 0, buildRules.lastIndexOf( "," ) ) } buildRules += ']' // Create text string of plugin defaults text = '{' text += '"hide_counts":"false",' text += '"hide_filters":"false",' text += '"hide_machines":"false",' text += '"welcome_nag":"true",' text += '"rule_machines": ' + buildNames + ',' text += '"check_machines": ' + buildNames + ',' text += '"containers":[{' text += '"name":"Original Rules",' text += '"slug":"original-rules",' text += '"title_color":"",' text += '"title_opacity":"",' text += '"title_bold":"",' text += '"container_color":"",' text += '"container_opacity":"",' text += '"visible":true,' text += '"rules": ' + buildRules text += '}]' text += '}' return text } // Check for new rule machines def checkForNewMachines( userRules ) { // Get default and user values defaults = new JsonSlurper().parseText( defaultUserArrayText() ).check_machines saved = userRules.check_machines ? userRules.check_machines : ["ruleMachine"] // Remove from defaults any values already saved saved.each{ defaults -= it } // If there are remaining values if( defaults ) { // Loop each removed machine type and make human readable message of machine types defaultsString = '' defaults.each { defaultsString += splitCamelCase( it ) + ', ' } defaultsString = defaultsString.substring( 0, defaultsString.lastIndexOf( "," ) ) // Message message = "
" message += "notifications" message += "New rule machine type found (" + defaultsString + ")! Please visit App Options -> Global Options to enable." message += "
" return message } else { if( logDebugEnable ) log.debug "No Deleted Machines Found..." return false; } } // Check for deleted machines def checkForDeletedMachines( userRules ) { // Get default and user values defaults = new JsonSlurper().parseText( defaultUserArrayText() ).check_machines saved = userRules.check_machines ? userRules.check_machines : ["ruleMachine"] // Loop each defaults and remove if checked defaults.each{ saved -= it } // If any machines remaining; they have been removed if( saved ) { // Loop each removed machine type and make human readable message of machine types savedString = '' saved.each { savedString += splitCamelCase( it ) + ', ' } savedString = savedString.substring( 0, savedString.lastIndexOf( "," ) ) // Message message = "
" message += "notifications" message += "A machine type has been removed (" + savedString + "). Please save the page options to update." message += "
" return message } else { if( logDebugEnable ) log.debug "No New Machines Found..." return false; } } // Check for new rules def checkForNewRules( userRules ) { // Iterate user rules to build final array of keys newRules = [] userRules.containers.each{ it.rules.each{ newRules.push( it.toString() ) } } // Get any new rules that have been added ruleMap.each{ if( ! newRules.contains( it.key.toString() ) ) { newRulesCheck.push( it ) } } // If any new rules found, add notice if( ! newRulesCheck.isEmpty() ) { if( logDebugEnable ) log.debug "New Rules Found..." // Build new rules found message messageNew = "
" messageNew += "notifications" messageNew += "New rules (${newRulesCheck.size()}) have been discovered in the Rule Manager App and added to the \"Original Rules\" container. Please click the \"Done\" button to save after any modifications." messageNew += "

NOTE: If the new rules are not visible in the original rules container; please visit App Options -> Global Options to ensure all rule machine types are displayed." messageNew += "
" return messageNew } // Else no new rules found else { if( logDebugEnable ) log.debug "No New Rules Found..." return false } } // Check for deleted rules def checkForDeletedRules( userRules ) { // Define final array, compare with new rules and extract deleted rules deletedRules = [] userRules.containers.each{ it.rules.each{ deletedRules.push( it.toString() ) } } ruleMap.each{ deletedRules.removeAll( it.key.toString() ) } // Check what is left in this array against allowed rules; to allow duplicates through ruleMap.each{ it.each{ // If this is a duplicate rule; it still exists in the main rm array if( deletedRules.contains( it.key.toString() ) ) { deletedRules.remove( it.key.toString() ) } }} // If any deleted rules found, add notice if( ! deletedRules.isEmpty() ) { if( logDebugEnable ) log.debug "Deleted Rules Found..." // Build deleted rules message messageDeleted = "
" messageDeleted += "notifications" messageDeleted += "Some rules (${deletedRules.size()}) have been deleted in the Rule Manager App and removed from any containers. Please click the \"Done\" button to save after any modifications." messageDeleted += "
" return messageDeleted } // Else no deleted rules found else { if( logDebugEnable ) log.debug "No Deleted Rules Found..." return false } } // Endpoint used from javascript for quick saving options def updateSettings() { // Get user options from ajax request userOpts = params.userOpts // If user options are found if( userOpts && userOpts != '' ) { // Update app settings with user options app.updateSetting( 'userArray', params.userOpts ) if( logDebugEnable ) log.debug "Updated Settings Successfully: " + params.userOpts return [status: "success", message: "Options saved successfully." ] } // Else no user options were found else { // Return error code if( logDebugEnable ) log.debug "Settings Error: Could Not Update" return [status: "error", message: "Data passed from ajax is empty." ] } } // Create access token def createThisAccessToken() { if( logDebugEnable ) log.debug "Checking Access Token..." // Create token if not already found if ( ! state.accessToken ) { try { def accessToken = createAccessToken() if( accessToken ) { state.accessToken = accessToken if( logDebugEnable ) log.debug "Access Token Not Found; Created New Token: " + state.accessToken } } catch(e) { state.accessToken = null if( logDebugEnable ) log.warn "Access Token Was Not Created... Token: " + state.accessToken } } // Else token was already found else { if( logDebugEnable ) log.debug "Found Exisiting Access Token: " + state.accessToken } } // Return a camel case string in capitals with spaces def splitCamelCase(s) { return s.replaceAll( String.format("%s|%s|%s", "(?<=[A-Z])(?=[A-Z][a-z])", "(?<=[^A-Z])(?=[A-Z])", "(?<=[A-Za-z])(?=[^A-Za-z])" ), " " ).capitalize(); } // Helper function to define rule list def createRulelistSubmap( ruleName, camelName, array ) { attSubMap = [:] attSubMap['name'] = array.data.name attSubMap['disabled'] = array.data.disabled attSubMap['paused'] = array.data.name.contains( '(Paused)' ) ? true : false attSubMap['ruleType'] = camelName // Add submap to rulemap ruleMap[array.id] = attSubMap } // Populate rule list def populateRuleList() { // Define rule map; built from httpget call and rebuilding array try { httpGet([ uri: "http://127.0.0.1:8080/hub2/appsList" ]) { resp -> if (resp.success) { if( logDebugEnable ) log.debug "Getting Apps List..." // Set array of allowed machine types machineTypes = [:] machineTypes['ruleMachine'] = 'Rule Machine' machineTypes['roomLighting'] = 'Room Lighting' machineTypes['buttonControllers'] = 'Button Controllers' machineTypes['basicButtonControllers'] = 'Basic Button Controllers' machineTypes['motionAndModeLightingApps'] = 'Motion and Mode Lighting Apps' // Loop each app type resp.data.apps.each { // If this app is in our allowed list if( machineTypes.containsValue( it.data.type ) ) { // Define variables ruleName = it.data.type camelName = machineTypes.find{ it.value == ruleName }?.key if( logDebugEnable ) log.debug "Adding ${ruleName} Rules..." // Add to names map ruleMapNames[camelName] = ruleName // If this apps rules are in the grandchildren if( ruleName == "Button Controllers" ) { // Loop children it.children.each{ // Loop children again it.children.each{ // Create submap (define any needed variables) createRulelistSubmap( ruleName, camelName, it ) } } } // Else app rules are in the children else { // Loop children it.children.each{ // Create submap (define any needed variables) createRulelistSubmap( ruleName, camelName, it ) } } } } if( logDebugEnable ) log.debug "Finished Getting Apps List..." } } } catch (Exception e) { log.warn "Get Apps List Failed: ${e.message}" } } def installed() { log.trace "Installed Rule Machine Manager Application" updated() } def updated() { // Create access token if( ! state.accessToken) { createThisAccessToken() } log.trace "Updated Rule Machine Manager Application" if( logDebugEnable ) log.debug "Updated User Array: " + app.getSetting( 'userArray' ) if( logDebugEnable ) runIn( 900, logDebugOff ) } def logDebugOff() { log.warn "Debug Logging Disabled..." app.updateSetting("logDebugEnable", [value: "false", type: "bool"]) }