/* Locks and Contacts Manager * 2025 T. K. (kampto) * NOTES: Control/Automate Locks and Monitor Contacts. * * 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. * * Change Revision History: * Ver Date: Who: What: * 1.1.9 2026-01-29 kampto More counts options. Add option to reset counts/active times a specified time per week or everyday with option push report. Fix Cron timer bug for all-lock and stats reset. 1.1.9 Seperated reporting formats push vs log * 1.1.6 2025-11-02 kampto Added 3rd different notification maps per device. Cancellable loccking if contact re-opened after delay, then restart timer. Actions 1 and 2. * 1.1.4 2025-09-02 kampto Added 2 different notification maps per device. Added send notifications per Mode option * 1.1.1 2025-08-21 kampto Add section for managing Lock Codes. This section is BETA, may not work for everyones Locks. Rework Notification menu * 1.0.5 2025-08-14 kampto Add Notifications enable per device. Other clean up. Lock All Locks logic. * 1.0.1 2025-06-09 kampto First Build from scratch. */ import groovy.time.TimeCategory import java.text.SimpleDateFormat import groovy.json.JsonSlurper // Retrieving Lock JSON import groovy.json.JsonOutput def titleVersion() {state.name = "Locks and Contacts Manager...(Control/Automate Locks and Monitor Contacts)"; state.version = "1.1.9" } definition ( name: "Locks and Contacts Manager", namespace: "kampto", author: "T. K.", description: "Control/Automate Locks, Use contacts if applicable", category: "Control", iconUrl: "", iconX2Url: "", importUrl: "https://raw.githubusercontent.com/kampto/Hubitat/refs/heads/main/Apps/Locks%20and%20Contacts%20Manager", documentationLink: "https://community.hubitat.com/t/beta-app-locks-and-contacts-manager/155560", singleThreaded: true ) preferences { page(name: "mainPage") } //////////////////////////////////////////////////////////// MAIN PAGE / SET-UP ///////////////////////////////////////////////////////////// def mainPage() { if (app.getInstallationState() != "COMPLETE") {hide=false} else {hide=true} if (state.DeVices == null) state.DeVices = [:] if (state.DeVicesList == null) state.DeVicesList = [] if (statsResetTime == null) {statsResetTime = "000000000000000000000000000000"} if (allLockTime == null) {allLockTime = "000000000000000000000000000000"} if (state.lastPush == null) {state.lastPush = "Waiting"} if (state.lockSetMSG == null) {state.lockSetMSG = "Not Yet"} dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) { displayTitle() section (getFormat("header","Initial Set-Up:"),hideable: true, hidden: hide){ label title: "1. Name this App", required: true, submitOnChange: true, width: 3 input "DeVices", "capability.lock, capability.contactSensor", title: "2. Select Locks and Contacts(Optional) to Control/Monitor", required: true, multiple: true, submitOnChange: true, width: 6 paragraph "Available capabilities include Locks and Contacts. Combine multiple devices in single App table or create multiple instances of this App with differnt names. You must include contact sensor(s) used in the Lock Action cases to get notifications from them. Hit Update/Store button to load the Icons if you see 'null'" DeVices.each {dev -> if(!state.DeVices["$dev.id"]) { state.DeVices["$dev.id"] = [start: dev.currentLock == "unlocked" || dev.currentContact == "open" ? now() : 0, onTime: "NotYet", total: 0, modes: "Any", lockActions: 0, unLockActions: 0, notifyOptions: 0, zone: 0, counts: 0, pause: false] state.DeVicesList += dev.id } } } section { if(DeVices) { if(DeVices.id.sort() != state.DeVicesList.sort()) { state.DeVicesList = DeVices.id Map newState = [:] DeVices.each{dev -> newState["$dev.id"] = state.DeVices["$dev.id"]} state.DeVices = newState } refreshHandler() // get latest times at app open paragraph displayTable() input "refresh", "button", title: "REFRESH Table Times, Counts, & States", width: 3 if (!contactModeBool) {input "allLock", "button", title: "Lock All Locks", width: 2, style: 'margin-left:10px'} input name: "inDaysCBool", type: "bool", title:getFormat("important","Display Total Time in Days?"), defaultValue:false, submitOnChange:true, width: 3 statusBar() } } /////////////////////////////////////////////////////////////////// ADVANCED INPUTS ///////////////////////////////////////////////////////////// section(getFormat("header","Advanced Options:"),hideable: true, hidden: false) { input "updateButton", "button", title: "Update/Store Schedules without hitting Done exiting App", width: 4, newLineAfter: false input name: "contactModeBool", type: "bool", title: getFormat("important","Show only 'Contacts' features (Simplify by hidding Lock Options) ?  
Note: If you have Locks Actions configured, toggling to 'Contacts Only', Lock Actions will not be erased, only hidden and still run in background."), defaultValue:false, submitOnChange:true, newLineAfter: true, width: 7 if (!contactModeBool) { input name: "pauseAllBool", type: "bool", title: getFormat("important","Pause All App Lock Actions?   No Locks will change state from App"), defaultValue:false, width: 7, newLineAfter: true, submitOnChange:true, style: 'margin-left:10px' input name: "allLockBool", type: "bool", title: getFormat("important","Lock All Locks at a Specific daily time?   This must remain checked ON to enable.
Ignores Modes and Device specific Pause. Will not run if 'Pause All' enabled
"), defaultValue:false, width: 7, newLineAfter: true, submitOnChange:true, style: 'margin-left:10px' if (allLockBool) { input name: "allLockTime", type: "time", title:getFormat("blueRegular","Enter All Locks locked time, Applies to all.   Uses 24hr time,   Hit Update"), defaultValue: "", required: false, submitOnChange:true, width: 5, newLineAfter: true, style: 'margin-left:70px' } } ////input name: "formatBool", type: "bool", title: getFormat("important","Enable Alternative UI formatting, dark screen mode?"), defaultValue:false, submitOnChange:true, newLineAfter: true, style: 'margin-left:10px' input name: "countsBothBool", type: "bool", title: getFormat("important","Count both Open/Unlocked and Closed/Locked Activity?   Uncheck to select one or the other"), defaultValue:true, width: 5,submitOnChange:true, style: 'margin-left:10px' if (!countsBothBool) {input name: "countsOpenBool", type: "bool", title: getFormat("important","If checked Count only Open/Unlocked, if unchecked Count only Closed/Locked Activity"), defaultValue:false, width: 5, newLineAfter: true, submitOnChange:true, style: 'margin-left:30px' } input name: "statsResetBool", type: "bool", title: getFormat("important","Reset On Time and Count Stats at a specific time and day weekly?
Applys to all devices. This must remain checked ON to enable. Hit Update/Store button above after set up."), defaultValue:false, submitOnChange:true, style: 'margin-left:10px' if (statsResetBool) { input name: "statsResetTime", type: "time", title:getFormat("blueRegular","Enter Reset Stats time.   Uses 24hr time,   Hit Update"), defaultValue: "", required: false, submitOnChange:true, width: 4, style: 'margin-left:70px' input name: "statsResetDay", type: "enum", title: getFormat("blueRegular","Select Reset Day"), defaultValue: "SUN", submitOnChange: true, options: ["Everyday","SUN","MON","TUE","WED","THU","FRI","SAT"], required: false, newLineAfter: true, width: 2, style: 'margin-left:30px' } input name: "logInfoEnableBool", type: "bool", title: getFormat("important","Enable Info Logging?   Stays active"), defaultValue:true, submitOnChange:true, width: 3, style: 'margin-left:10px' input name: "logDebugEnableBool", type: "bool", title: getFormat("important","Enable Debug Logging?   Shuts off in 1hr"), defaultValue:false, submitOnChange:true, width: 3, style: 'margin-left:10px' } /////////////// PUSH NOTIFY OPTIONS section(getFormat("header","PUSH Notification Options:"),hideable: true, hidden: false) { input "pushDevice", "capability.notification", title: "Select Device(s) to send Notification to (Optional)   Applies to Selected Devices in table.", multiple: true, required: false, submitOnChange: true, width: 8, newLineAfter: true, style: 'margin-left:10px' if (pushDevice) { input "notifyOptions1", "enum", title: "Notify Options ${state.notifyOptionsIcon1}: Select Events that you want to get Notifications for:", required: false, multiple: true, submitOnChange: true, width: 3, style: 'margin-left:60px', options:[ ["pushOpen": "Get Notified of Contacts Opening"], ["pushClosed": "Get Notified of Contacts Closing"], ["pushUnlocked": "Get Notified of Locks Unlocking"], ["pushLocked": "Get Notified of Locks Locking"], ["pushLockFailed": "Get Notified about any Lock possibly Jamming"], ["pushBattery": "Get Notified about Battery Levels below selected level"], ["pushLockSet": "Get Notified about any Lock Code Changes within App"] ] input "notifyOptions2", "enum", title: "Notify Options ${state.notifyOptionsIcon2}: Select Events that you want to get Notifications for:", required: false, multiple: true, submitOnChange: true, width: 3, style: 'margin-left:60px', options:[ ["pushOpen": "Get Notified of Contacts Opening"], ["pushClosed": "Get Notified of Contacts Closing"], ["pushUnlocked": "Get Notified of Locks Unlocking"], ["pushLocked": "Get Notified of Locks Locking"], ["pushLockFailed": "Get Notified about any Lock possibly Jamming"], ["pushBattery": "Get Notified about Battery Levels below selected level"], ["pushLockSet": "Get Notified about any Lock Code Changes within App"] ] input "notifyOptions3", "enum", title: "Notify Options ${state.notifyOptionsIcon3}: Select Events that you want to get Notifications for:", required: false, multiple: true, submitOnChange: true, width: 3, newLineAfter: true, style: 'margin-left:60px', options:[ ["pushOpen": "Get Notified of Contacts Opening"], ["pushClosed": "Get Notified of Contacts Closing"], ["pushUnlocked": "Get Notified of Locks Unlocking"], ["pushLocked": "Get Notified of Locks Locking"], ["pushLockFailed": "Get Notified about any Lock possibly Jamming"], ["pushBattery": "Get Notified about Battery Levels below selected level"], ["pushLockSet": "Get Notified about any Lock Code Changes within App"] ] input name: "pushMode", type: "mode", title: getFormat("blueRegular","Only send Notifications during selected Mode?"), defaultValue: "Any", width: 4, submitOnChange:true, newLineAfter: true, style: 'margin-left:80px' if (settings.notifyOptions1?.contains("pushOpen") || settings.notifyOptions2?.contains("pushOpen") || settings.notifyOptions3?.contains("pushOpen")) {input name: "openDelay", type: "number", title:getFormat("blueRegular","Wait this many Seconds before sending Notification if still Open.  Notify Options 1-3, Default = 1sec"), defaultValue: "1", submitOnChange:true, width:6, newLineAfter: true, style: 'margin-left:80px'} if (settings.notifyOptions1?.contains("pushUnlocked") || settings.notifyOptions2?.contains("pushUnlocked") || settings.notifyOptions3?.contains("pushUnlocked")) {input name: "unlockedDelay", type: "number", title:getFormat("blueRegular","Wait this many Seconds before sending Notification if still Unlocked.  Notify Options 1-3, Default = 1sec"), defaultValue: "1", submitOnChange:true, newLineAfter: true, style: 'margin-left:80px'} if (settings.notifyOptions1?.contains("pushBattery") || settings.notifyOptions2?.contains("pushBattery") || settings.notifyOptions3?.contains("pushBattery")) {input name: "batPercent", type: "number", title:getFormat("blueRegular","Enter Battery % low level to get Notified,  All Devices, Default = 40"), defaultValue: "40", submitOnChange:true, newLineAfter: true, style: 'margin-left:80px'} if (statsResetBool) { input name: "statsReportBool", type: "bool", title: getFormat("blueRegular","If using Reset all Stats at specific Time option, send a push notification Report of all devices Total Active Time and Counts Stats?"), defaultValue:true, submitOnChange:true, newLineAfter: true, style: 'margin-left:80px' } } } /////////////////////////////////////////////////////////////// LOCK OPTIONS ////////////////////////////////////////////////////////////// if (!contactModeBool) { ///////////////// ACTIONS section(getFormat("header","LOCK ACTION Case Options:"),hideable: true, hidden: false) { input name: "autoLockBool", type: "bool", title: getFormat("important","Auto Lock if unlocked after a delay?    Applies to all Locks. Ignores Contact Switches, could lock while door is open! Use case is if door unlocked but never opened or if you dont have a contact sensor on door. Will not run if Paused or Incorrect mode."), defaultValue:false, newLineAfter: true, width: 8, submitOnChange:true, style: 'margin-left:10px' if (autoLockBool) {input name: "lockDelaySec", type: "number", title: getFormat("blueRegular","AutoLock Delay    After unlocked will wait this time till Auto lockiing. Default = 30,   Enter 0 to 600 seconds"), defaultValue: "30", accepts:"0 to 600", range:"0..600", width: 4, newLineAfter: true, submitOnChange:true, style: 'margin-left:60px'} input name: "lockRetryBool", type: "bool", title: getFormat("important","If Auto Lock fails to Lock in 5sec, Retry one more time?    Applies to all Locks."), defaultValue:false, newLineAfter: true, width: 7, submitOnChange:true, style: 'margin-left:10px' input name: "contactLock1", type: "capability.contactSensor", title: getFormat("important","Lock Actions $state.lockActionsIcon1: Select a Contact sensor to Auto Lock.   Will use Actions 1 Delay time after Contact Closed. Applies to devices with 'Actions' case #1 in table. Will not run if Paused or Incorrect mode. Lock will not lock if Conatct is Open. Only Use Action #1 for one Lock."), multiple: false, width: 9, submitOnChange:true, style: 'margin-left:10px' //// ver2.1.0 if (contactLock1) {input name: "contactLock1DelaySec", type: "number", title: getFormat("blueRegular","Actions 1: AutoLock Delay    After selected contact is closed then Lock. Default = 5sec,   Enter 0 to 600 seconds"), defaultValue: "5", accepts:"0 to 600", range:"0..600", newLineAfter: true, submitOnChange:true, style: 'margin-left:60px'} input name: "contactLock2", type: "capability.contactSensor", title: getFormat("important","Lock Actions $state.lockActionsIcon2: Select a Contact sensor to Auto Lock.   Will use Actions 2 Delay time after Contact Closed. Applies to devices with 'Actions' case #2 in table. Will not run if Paused or Incorrect mode. Lock will not lock if Conatct is Open. Only Use Action #2 for one Lock."), multiple: false, width: 9, submitOnChange:true, style: 'margin-left:10px' //// ver2.1.0 if (contactLock2) {input name: "contactLock2DelaySec", type: "number", title: getFormat("blueRegular","Actions 2: AutoLock Delay    After selected contact is closed then Lock. Default = 5sec,   Enter 0 to 600 seconds"), defaultValue: "5", accepts:"0 to 600", range:"0..600", newLineAfter: true, submitOnChange:true, style: 'margin-left:60px'} } section(getFormat("header","UNLOCK ACTION Case Options:"),hideable: true, hidden: false) { input name: "neverUnlockBool", type: "bool", title: getFormat("important","Never UnLock in any circumstance?   Applies to all Locks."), defaultValue:true, width: 6, newLineAfter: true, submitOnChange:true, style: 'margin-left:10px' input name: "remoteSwitch", type: "capability.switch", title: getFormat("important","Unlock Actions $state.unLockActionsIcon1: Select Switch(s) to control lock state.
On = Unlock, Off = Lock. Will not run if Paused or Incorrect mode. Use dashboard, virtual switch, or linked physical switch."), defaultValue:false, multiple: true, submitOnChange:true, width: 9, style: 'margin-left:10px' } ///////////////// LOCK CODES section(getFormat("header","Lock Code Admin   BETA, Results may vary!!"),hideable: true, hidden: false) { input "lockCodeDev", "capability.lockCodes", title: "Select Lock to Modify  For locks with Code capability only.", required: false, multiple: false, submitOnChange: true, newLineAfter: true, width: 7 paragraph getFormat("important","To Set Lock code enter: Slot#, Pin Code, code Name, then push Lock Code Button. Some locks will not support the following; overwrite an existing slot
unless deleted first, Test Unlock Feature. Alternatively you can open the Lock Device in Hub Device menu to verify
") input name: "slotNumber", type: "enum", title: getFormat("blueRegular","Select Slot Number"), defaultValue:0, width: 3, options: ["1","2","3","4","5","6","7","8","9","10"], submitOnChange:true input name: "pinCode", type: "text", title: getFormat("blueRegular","Enter Digit Pin code"), submitOnChange:true, width: 3 input name: "codeName", type: "text", title: getFormat("blueRegular","Enter Code Name (Text)"), defaultValue:"Guest", submitOnChange:true, width: 3 input "lockCodeButton", "button", title: "Set Lock Code", newLineAfter: true, width: 3 input name: "deleteSlotNumber", type: "enum", title: getFormat("blueRegular","Select Slot Number to Delete"), defaultValue:0, width: 3, options: ["0","1","2","3","4","5","6","7","8","9","10"], submitOnChange:true input "deleteSlotButton", "button", title: "Delete Slot Number", width: 3 input name: "testCode", type: "text", title: getFormat("blueRegular","Test Code Pin#"), submitOnChange:true, width: 3 input "testCodeButton", "button", title: "Test Unlock Code", width: 3 input "retrieveCodeButton", "button", title: "Retrieve Codes(s) and Code constraints", width: 4 paragraph getFormat("important","Button Confrimation Message:  ${state.lockSetMSG}") } } /////////////////////////////////////////////////////////////// USAGE NOTES ////////////////////////////////////////////////////////////// section(getFormat("header","Usage Notes / Instructions:"), hideable: true, hidden: hide) { paragraph getFormat("lessImportant","
    "+ "
  • Use for any Lock or Contact capability. Add as many as you want on table.
  • "+ "
  • Table will not auto refresh values or states, you must hit in App Refresh button.
  • "+ "
  • If you make/change a schedule change it wont take unless you hit 'Done' exiting the App or hitting the 'Update/Store' button.
  • "+ "
  • Active On Time and Counts track Locks Open(red) and Contacts Open(red) Times. Can set a Reset Stats time schedule in Options
  • "+ "
  • State: Shows current device state after hitting in app Refresh. Clicking will toggle a Lock unless 'Pause All' enabled.
  • "+ "
  • Reset: From table per row will reset the total On Time and Counts. Configureation will remain.
  • "+ "
  • Lock Modes: If your not using modes keep at 'Any'. If you want Lock to only run during a specific Mode click and select that mode
    If changed and need to get back to 'Any', go to Hub settings / Modes, and add new mode 'Any'"+ "
  • Notify: Click Play in table per device. In Advanced options select device(s) to send notifications too, select events that will trigger a notification message.
  • "+ "
  • Notify Modes: If your not using modes keep at 'Any'. If only want notification during specific mode then select that mode.
    If changed and need to get back to 'Any', go to Hub settings / Modes, and add new mode 'Any'"+ "
  • Lock Pause: Clicking Pause Will stop any App actions from operation that Lock.
  • "+ "
  • Lock All Locks Button: Clicking 'Lock all Locks' will lock regardless of Mode or individually Paused. Will not Lock if 'Pause All' enabled.
  • "+ "
  • Actions Case: Select 'None' or case # in table, set up in Option below table. Only Locks can use Action Cases.
  • "+ "
  • Last State Change Time: The last time anything was open/closed, locked or unlocked. Not resetable.
  • "+ "
  • You must hit DONE at page bottom to save App after first making.
  • "+ "
") } } } ////////////////////////////////////////////////////////////////// MAIN TABLE BUILD /////////////////////////////////////////////////////////////// String displayTable() { //////////////////////////// Table Input Fields ///////////////////////////// if(state.newMode) { //////// Input Modes input name: "newMode", type: "mode", title: getFormat("noticable","Select during which mode this device will only run.
Home, Away, etc..   Or Any if No Selection"), defaultValue: "", submitOnChange:true, width: 7, required: false, newLineAfter: true, style: 'margin-left:10px' if(newMode) { state.DeVices[state.newMode].modes = newMode state.remove("newMode") app.removeSetting("newMode") paragraph "" } } ////////////////////////////// Table Buttons / Entries ///////////////////////////// if (state.resetTotal) { def dev = DeVices.find{"$it.id" == state.resetTotal} //// Reset Cumulative time and counts per device Button state.DeVices[state.resetTotal].start = now() state.DeVices[state.resetTotal].total = 0 state.DeVices[state.resetTotal].counts = 0 state.remove("resetTotal") } if (state.deviceState) { def dev = DeVices.find{"$it.id" == state.deviceState} if (!pauseAllBool && dev.currentLock == "unlocked") {dev.lock()} else if (!pauseAllBool && dev.currentLock == "locked") {dev.unlock()} //&& !neverUnlockBool endif state.remove("deviceState") app.removeSetting("deviceState") paragraph "" } if (state.lockActions0) {def dev = DeVices.find{"$it.id" == state.lockActions0} state.DeVices[state.lockActions0].lockActions = 0; state.remove("lockActions0") } else if (state.lockActions1) {def dev = DeVices.find{"$it.id" == state.lockActions1} state.DeVices[state.lockActions1].lockActions = 1; state.remove("lockActions1") } else if (state.lockActions2) {def dev = DeVices.find{"$it.id" == state.lockActions2} state.DeVices[state.lockActions2].lockActions = 2; state.remove("lockActions2") } else if (state.lockActions3) {def dev = DeVices.find{"$it.id" == state.lockActions3} state.DeVices[state.lockActions3].lockActions = 3; state.remove("lockActions3") } else if (state.lockActions4) {def dev = DeVices.find{"$it.id" == state.lockActions4} state.DeVices[state.lockActions4].lockActions = 4; state.remove("lockActions4") } endif if (state.unLockActions0) {def dev = DeVices.find{"$it.id" == state.lockActions0} state.DeVices[state.unLockActions0].unLockActions = 0; state.remove("unLockActions0") } else if (state.unLockActions1) {def dev = DeVices.find{"$it.id" == state.unLockActions1} state.DeVices[state.unLockActions1].unLockActions = 1; state.remove("unLockActions1") } else if (state.unLockActions2) {def dev = DeVices.find{"$it.id" == state.unLockActions2} state.DeVices[state.unLockActions2].unLockActions = 2; state.remove("unLockActions2") } else if (state.unLockActions3) {def dev = DeVices.find{"$it.id" == state.unLockActions3} state.DeVices[state.unLockActions3].unLockActions = 3; state.remove("unLockActions3") } else if (state.unLockActions4) {def dev = DeVices.find{"$it.id" == state.unLockActions4} state.DeVices[state.unLockActions4].unLockActions = 4; state.remove("unLockActions4") } endif if (state.notifyOptions0) {def dev = DeVices.find{"$it.id" == state.notifyOptions0} state.DeVices[state.notifyOptions0].notifyOptions = 0; state.remove("notifyOptions0") } else if (state.notifyOptions1) {def dev = DeVices.find{"$it.id" == state.notifyOptions1} state.DeVices[state.notifyOptions1].notifyOptions = 1; state.remove("notifyOptions1") } else if (state.notifyOptions2) {def dev = DeVices.find{"$it.id" == state.notifyOptions2} state.DeVices[state.notifyOptions2].notifyOptions = 2; state.remove("notifyOptions2") } else if (state.notifyOptions3) {def dev = DeVices.find{"$it.id" == state.notifyOptions3} state.DeVices[state.notifyOptions3].notifyOptions = 3; state.remove("notifyOptions3") } else if (state.notifyOptions4) {def dev = DeVices.find{"$it.id" == state.notifyOptions4} state.DeVices[state.notifyOptions4].notifyOptions = 4; state.remove("notifyOptions4") } endif if (state.pauseCheckedBox) {def dev = DeVices.find{"$it.id" == state.pauseCheckedBox} //// ver1.0.2 state.DeVices[state.pauseCheckedBox].pause = true; state.remove("pauseCheckedBox") } else if (state.pauseUnCheckedBox) {def dev = DeVices.find{"$it.id" == state.pauseUnCheckedBox} state.DeVices[state.pauseUnCheckedBox].pause = false; state.remove("pauseUnCheckedBox") } endif ////////////////////////////////////////////////////////// TABLE HEADER BUILD //////////////////////////////////////////////////// String str = "" str += "
" + "" + "" + "" if (!contactModeBool) { str += ""+ ""+ ""+ "" } str += ""+ ""+ ""+ "" int zone = 0 DeVices.sort{it.displayName.toLowerCase()}.each {dev -> zone += 1 state.DeVices["$dev.id"].zone = zone ////////////////////////// Active/On Time Calc int counts = state.DeVices["$dev.id"].counts int total = state.DeVices["$dev.id"].total / 1000 float totalDays = (total / 86400) as float //// ver1.1.1 int hours = total / 3600 total = total % 3600 int mins = total / 60 int secs = total % 60 String time = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs" if (inDaysCBool) {time = String.format("%.3f", totalDays)} // For table Only //// ver1.1.1 String devLink = "$dev" String deviceStateT = buttonLink("E$dev.id", dev.currentLock, "16px") String resetTotal = buttonLink("C$dev.id", state.resetIcon, "16px") String mode = buttonLink("D$dev.id", state.DeVices["$dev.id"].modes, "MediumBlue", "16px") String lockActionsT if (state.DeVices["$dev.id"].lockActions == 0) {lockActionsT = buttonLink("G$dev.id", "None", "blue", "16px")} else if (state.DeVices["$dev.id"].lockActions == 1) {lockActionsT = buttonLink("H$dev.id", state.lockActionsIcon1)} else if (state.DeVices["$dev.id"].lockActions == 2) {lockActionsT = buttonLink("I$dev.id", state.lockActionsIcon2)} else if (state.DeVices["$dev.id"].lockActions == 3) {lockActionsT = buttonLink("J$dev.id", state.lockActionsIcon3)} else if (state.DeVices["$dev.id"].lockActions == 4) {lockActionsT = buttonLink("K$dev.id", state.lockActionsIcon4)} endif String unLockActionsT if (state.DeVices["$dev.id"].unLockActions == 0) {unLockActionsT = buttonLink("L$dev.id", "None", "blue", "16px")} else if (state.DeVices["$dev.id"].unLockActions == 1) {unLockActionsT = buttonLink("M$dev.id", state.unLockActionsIcon1)} else if (state.DeVices["$dev.id"].unLockActions == 2) {unLockActionsT = buttonLink("N$dev.id", state.unLockActionsIcon2)} else if (state.DeVices["$dev.id"].unLockActions == 3) {unLockActionsT = buttonLink("O$dev.id", state.unLockActionsIcon3)} else if (state.DeVices["$dev.id"].unLockActions == 4) {unLockActionsT = buttonLink("P$dev.id", state.unLockActionsIcon4)} endif String notifyOptionsT if (state.DeVices["$dev.id"].notifyOptions == 0) {notifyOptionsT = buttonLink("Q$dev.id", state.pauseIcon)} else if (state.DeVices["$dev.id"].notifyOptions == 1) {notifyOptionsT = buttonLink("R$dev.id", state.notifyOptionsIcon1)} else if (state.DeVices["$dev.id"].notifyOptions == 2) {notifyOptionsT = buttonLink("S$dev.id", state.notifyOptionsIcon2)} else if (state.DeVices["$dev.id"].notifyOptions == 3) {notifyOptionsT = buttonLink("T$dev.id", state.notifyOptionsIcon3)} else if (state.DeVices["$dev.id"].notifyOptions == 4) {notifyOptionsT = buttonLink("U$dev.id", state.notifyOptionsIcon4)} endif String pauseCheckBoxT = (state.DeVices["$dev.id"].pause) ? buttonLink("A$dev.id", state.pauseIcon) : buttonLink("B$dev.id", state.playIcon) /String notifyCheckBoxT = (state.DeVices["$dev.id"].notify) ? buttonLink("Q$dev.id", state.playIcon) : buttonLink("R$dev.id", state.pauseIcon) /////////////////////////////////////////////////////////////// TABLE ROWS BUILD ////////////////////////////////////////////////// str += "" + "" if (dev.currentLock) {str += ""} else if (dev.currentContact) {str += ""} endif if (!contactModeBool) { if (dev.currentLock) {str += ""} else {str += ""} if (dev.currentLock) {str += ""} else {str += ""} if (dev.currentLock) {str += ""} else {str += ""} if (dev.currentLock) {str += ""} else {str += ""} } str += "" str += ""+ ""+ "" if (dev.currentLock) {str += "" } else if (dev.currentContact) {str += "" } //// ver1.20 endif str += "" } str += "
#App ver${state.version}
Device
StateRun
Mode
Lock
Actions
Unlock
Actions
Lock
Pause?
Notifi-
cations?
Battery
Level%
Last State
Change Time" if (countsBothBool) {str += "
Active
Counts"} else if (countsOpenBool) {str += "
Open
Counts"} else {str += "
Closed
Counts"} str += "
Active
OnTime
Reset
OnTime
${state.DeVices["$dev.id"].zone}$devLink$deviceStateT${dev.currentContact}$modeN/A$lockActionsTN/A$unLockActionsTN/A$pauseCheckBoxTN/A$notifyOptionsT${dev.currentBattery}${state.DeVices["$dev.id"].onTime}${state.DeVices["$dev.id"].counts}$time$time$resetTotal
" } String buttonLink(String btnName, String linkText, color = SteelBlue, font = "13px") { //// Device Link Format "
$linkText
" } /////////////////////////////////////////////////////////// SCHEDULES AND SUBSCRIBES ////////////////////////////////////////////////////////// void initialize() { subscribe(DeVices, "lock.unlocked", onTimeHandler) subscribe(DeVices, "lock.locked", offTimeHandler) subscribe(DeVices, "contact.open", onTimeHandler) subscribe(DeVices, "contact.closed", offTimeHandler) subscribe(DeVices, "battery", batHandler) subscribe(contactLock1, "contact.closed", contactClosedHandler) subscribe(contactLock1, "contact.open", contactOpenHandler) subscribe(contactLock2, "contact.closed", contactClosedHandler) subscribe(contactLock2, "contact.open", contactOpenHandler) subscribe(remoteSwitch, "switch.on", remoteSwitchHandler) subscribe(remoteSwitch, "switch.off", remoteSwitchHandler) if (allLockBool) {schedule("${state.allLockCron}", allLockHandler)} // Set All Lock Time else {unschedule (allLockHandler)} if (statsResetBool) {schedule("${state.statsResetCron}", statsResetHandler)} // Reset Ontimes and counts else {unschedule (statsResetHandler)} if(logDebugEnableBool) runIn(3600, logsOff) // Disable all debug Logging after time elapsed else unschedule(logsOff) } ////////////////////////////////////////////////////////////////// HANDLERS ////////////////////////////////////////////////////////////////// def onTimeHandler(evt) { //// Called by; Subscribes only OPEN state.DeVices[evt.device.id].start = now() if (countsBothBool || countsOpenBool) {state.DeVices[evt.device.id].counts += 1 } state.DeVices[evt.device.id].onTime = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") // Last Update if (logInfoEnableBool) {log.info "App: ${app.label} -- $evt.descriptionText"} if (pushDevice) { if (((settings.notifyOptions1?.contains("pushOpen") && state.DeVices[evt.device.id].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushOpen") && state.DeVices[evt.device.id].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushOpen") && state.DeVices[evt.device.id].notifyOptions == 3)) && evt.device.currentContact == "open") { if (openDelay > 1) {dataMap = [notifyCase:1, value:evt.value, displayName:evt.displayName, descriptionText:evt.descriptionText, zone:state.DeVices[evt.device.id].zone] runIn(openDelay, notifyDataHandler, [data:dataMap, overwrite:true]) } else {notifyEvtHandler(evt)} } if (((settings.notifyOptions1?.contains("pushUnlocked") && state.DeVices[evt.device.id].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushUnlocked") && state.DeVices[evt.device.id].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushUnlocked") && state.DeVices[evt.device.id].notifyOptions == 3)) && evt.device.currentLock == "unlocked") { if (unlockedDelay > 1) {dataMap = [notifyCase:2, value:evt.value, displayName:evt.displayName, descriptionText:evt.descriptionText, zone:state.DeVices[evt.device.id].zone] runIn(unlockedDelay, notifyDataHandler, [data:dataMap, overwrite:true]) } else {notifyEvtHandler(evt)} } } if (autoLockBool && !pauseAllBool && evt.device.currentLock == "unlocked") {zone = state.DeVices[evt.device.id].zone; runIn(lockDelaySec, lockHandler, [data:zone, overwrite:false]) } if (logDebugEnableBool) {log.debug "TEST App: ${app.label} -- dataMap-notifyCase = ${dataMap.notifyCase}....evt = $evt -------- dev = $device" } } def offTimeHandler(evt) { //// Called by; Subscribes only CLOSED state.DeVices[evt.device.id].total += now() - state.DeVices[evt.device.id].start if (countsBothBool || !countsOpenBool) {state.DeVices[evt.device.id].counts += 1 } state.DeVices[evt.device.id].onTime = new Date().format("MM-dd-yyyy ${location.timeFormat == "12" ? "h:mm:ss a" : "HH:mm:ss"}") // Last Update if (logInfoEnableBool) {log.info "App: ${app.label} -- $evt.descriptionText"} if (pushDevice) { if (((settings.notifyOptions1?.contains("pushClosed") && state.DeVices[evt.device.id].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushClosed") && state.DeVices[evt.device.id].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushClosed") && state.DeVices[evt.device.id].notifyOptions == 3)) && evt.device.currentContact == "closed") {notifyEvtHandler(evt) } if (((settings.notifyOptions1?.contains("pushLocked") && state.DeVices[evt.device.id].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushLocked") && state.DeVices[evt.device.id].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushLocked") && state.DeVices[evt.device.id].notifyOptions == 3)) && evt.device.currentLock == "locked") {notifyEvtHandler(evt) } } } void lockHandler(data) { if (pauseAllBool) {return} // All paused, get out, dont run if (logDebugEnableBool) {log.debug "App: ${app.label} -- LockHandler.... data.value = ${data.value} .. "} DeVices.each { dev -> if (dev.currentLock == "unlocked" && !state.DeVices["$dev.id"].pause && ((state.DeVices["$dev.id"].modes == location.mode) || state.DeVices["$dev.id"].modes == "Any") && data.value == state.DeVices["$dev.id"].zone ) { if (autoLockBool && !pauseAllBool && dev.currentLock == "unlocked") {dev.lock(); state.lockCancel = false } // Lock regardless of contacts if ((state.DeVices["$dev.id"].lockActions == 1 && contactLock1.currentContact == "closed") || (state.DeVices["$dev.id"].lockActions == 2 && contactLock2.currentContact == "closed")) { dev.lock(); state.lockCancel = false} else {state.lockCancel = true; log.info "App: ${app.label} -- Locking Cancelled... ${contactLock1} is ${contactLock1.currentContact} when AutoLock delay was up. Will restart Actions (1 or 2) delay when contact closed again"} if (lockRetryBool && !state.lockCancel) {zone = state.DeVices["$dev.id"].zone; runIn(5, lockRetryHandler, [data:zone, overwrite:true])} } } } void lockRetryHandler(data) { DeVices.each { dev -> if (dev.currentLock == "unlocked" && data.value == state.DeVices["$dev.id"].zone ) { dev.lock() if (logInfoEnableBool) {log.info "App: ${app.label} -- Locking Retry, **${dev}** Possible Failure"} if (pushDevice && ((settings.notifyOptions1?.contains("pushLockFailed") && state.DeVices["$dev.id"].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushLockFailed") && state.DeVices["$dev.id"].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushLockFailed") && state.DeVices["$dev.id"].notifyOptions == 3))) {dataMap=[notifyCase:3, value:dev.currentLock, displayName:dev.displayName, zone:state.DeVices["$dev.id"].zone]; runIn(1, notifyDataHandler, [data:dataMap, overwrite:true])} } } } void unLockHandler(data) { if (neverUnlockBool || pauseAllBool) {return} DeVices.each { dev -> if (dev.currentLock == "locked" && !state.DeVices["$dev.id"].pause && ((state.DeVices["$dev.id"].modes == location.mode) || state.DeVices["$dev.id"].modes == "Any") && data.value == state.DeVices["$dev.id"].zone) { dev.unlock() } } } void contactClosedHandler(evt) { if (pauseAllBool) {return} // All paused, get out, dont run if (logDebugEnableBool) {log.debug "App: ${app.label} -- evt.device.currentContact = ${evt.device.currentContact}.... evt.deviceId = ${evt.deviceId} ... evt.device = ${evt.device}...contactLock1 = ${contactLock1}"} DeVices.each { dev -> if (dev.currentLock && evt.device.currentContact == "closed") { zone = state.DeVices["$dev.id"].zone String x = evt.device if (state.DeVices["$dev.id"].lockActions == 1) { String y = contactLock1 if (x == y) {runIn(contactLock1DelaySec, lockHandler, [data:zone, overwrite:true])} } else if (state.DeVices["$dev.id"].lockActions == 2) { String y = contactLock2 if (x == y) {runIn(contactLock2DelaySec, lockHandler, [data:zone, overwrite:true])} } endif } } } void contactOpendHandler(evt) { DeVices.each { dev -> if (dev.currentLock) { ////////////////// TBD //////////////////////// } } } void notifyEvtHandler(evt) { // evt Descritption if (logDebugEnableBool) {log.debug "evtHandler App: ${app.label} -- id = $evt.displayName -- evt.descriptionText = $evt.descriptionText"} state.lastPush = "App: ${app.label} -- $evt.descriptionText" if (pushMode == location.mode || pushMode == "Any") {pushDevice.deviceNotification(state.lastPush)} } void notifyDataHandler(data) { // special cases, Data if (logDebugEnableBool) {log.debug "dataHandler App: ${app.label} -- notify-Case = ${data.notifyCase} ... data.displayName = ${data.displayName}"} DeVices.each { dev -> if (data.zone == state.DeVices["$dev.id"].zone && (pushMode == location.mode || pushMode == "Any")) { if (data.notifyCase == 1 && dev.currentContact == "open") {state.lastPush = "App: ${app.label} -- **$dev** has been Open for > $openDelay seconds"; pushDevice.deviceNotification(state.lastPush)} else if (data.notifyCase == 2 && dev.currentLock == "unlocked") {state.lastPush = "App: ${app.label} -- **$dev** has been Unlocked for > $unlockedDelay seconds"; pushDevice.deviceNotification(state.lastPush)} else if (data.notifyCase == 3) {state.lastPush = "App: ${app.label} -- Locking Retry, **${dev}** Possible Failure"; pushDevice.deviceNotification(state.lastPush)} else if (data.notifyCase == 4) {state.lastPush = "App: ${app.label} -- Lock Code Modified or Checked, ${state.lockSetMSG}"; pushDevice.deviceNotification(state.lastPush)} else return } } } def allLockHandler(evt) {//// Called by Sched All off time or App button if (pauseAllBool) {return} // All paused, get out, dont run DeVices.each { dev -> if (dev.currentLock == "unlocked") { dev.lock() } } if (logInfoEnableBool) {log.info "App: ${app.label} -- All Locks Locking"} } def batHandler(evt) { if (evt.device.currentBattery <= batPercent) { if (pushDevice && (pushMode == location.mode || pushMode == "Any") && ((settings.notifyOptions1?.contains("pushBattery") && state.DeVices[evt.device.id].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushBattery") && state.DeVices[evt.device.id].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushBattery") && state.DeVices[evt.device.id].notifyOptions == 3))) { pushDevice.deviceNotification("${app.label} -- Device **${evt.device}** Battery level ${evt.device.currentBattery}%, warning is ${batPercent}%") } if (logInfoEnableBool) {log.info "App: ${app.label} -- Device **${evt.device}** Battery level ${evt.device.currentBattery}%, warning is ${batPercent}%"} } } def remoteSwitchHandler(evt) {//// Called by Subscribes only. if (pauseAllBool) {return} // All paused, get out, dont run DeVices.each { dev -> if (dev.currentLock && state.DeVices["$dev.id"].unLockActions == 1 && !state.DeVices["$dev.id"].pause && (state.DeVices["$dev.id"].modes == location.mode || state.DeVices["$dev.id"].modes == "Any")) { if (evt.device.currentSwitch == "on") {runIn(1, unLockHandler, [data:state.DeVices["$dev.id"].zone, overwrite:true])} else if (evt.device.currentSwitch == "off") {runIn(1, lockHandler, [data:state.DeVices["$dev.id"].zone, overwrite:true])} if (logInfoEnableBool) {log.info "App: ${app.label} -- Remote Switch **${evt.device}** toggling Lock"} } } } void appButtonHandler(btn) {//// Called by; In app Button pushes if (btn == "refresh") refreshHandler() else if (btn == "allLock") allLockHandler() else if (btn == "updateButton") updated() else if (btn == "lockCodeButton") setLockCode(btn) else if (btn == "deleteSlotButton") setLockCode(btn) else if (btn == "testCodeButton") setLockCode(btn) else if (btn == "retrieveCodeButton") setLockCode(btn) else if (btn.startsWith("A")) state.pauseUnCheckedBox = btn.minus("A") else if (btn.startsWith("B")) state.pauseCheckedBox = btn.minus("B") else if (btn.startsWith("C")) state.resetTotal = btn.minus("C") else if (btn.startsWith("D")) state.newMode = btn.minus("D") else if (btn.startsWith("E")) state.deviceState = btn.minus("E") else if (btn.startsWith("K")) state.lockActions0 = btn.minus("K") else if (btn.startsWith("G")) state.lockActions1 = btn.minus("G") else if (btn.startsWith("H")) state.lockActions2 = btn.minus("H") else if (btn.startsWith("I")) state.lockActions3 = btn.minus("I") else if (btn.startsWith("J")) state.lockActions4 = btn.minus("J") else if (btn.startsWith("P")) state.unLockActions0 = btn.minus("P") else if (btn.startsWith("L")) state.unLockActions1 = btn.minus("L") else if (btn.startsWith("M")) state.unLockActions2 = btn.minus("M") else if (btn.startsWith("N")) state.unLockActions3 = btn.minus("N") else if (btn.startsWith("O")) state.unLockActions4 = btn.minus("O") else if (btn.startsWith("U")) state.notifyOptions0 = btn.minus("U") else if (btn.startsWith("Q")) state.notifyOptions1 = btn.minus("Q") else if (btn.startsWith("R")) state.notifyOptions2= btn.minus("R") else if (btn.startsWith("S")) state.notifyOptions3= btn.minus("S") else if (btn.startsWith("T")) state.notifyOptions4= btn.minus("T") endif } def refreshHandler() {//// Called by; In App Button push and within functions // Update Times if Active/On state.DeVices.each {k, v -> def dev = DeVices.find{"$it.id" == k} if (dev.currentLock == "unlocked" || dev.currentContact == "open") { state.DeVices[k].total += now() - state.DeVices[k].start state.DeVices[k].start = now() } } } def statsResetHandler() { //// Called by; Sched Reset time String statsReportPush = "${app.label} -- All Device On Times and Counts Stats Scheduled Reset, Stats Report: \r\nDevice.....TotalActiveTime(HH:MM:SS).....Counts: \r\n" //// Format for Push String statsReportLog = "${app.label} -- All Device On Times and Counts Stats Scheduled Reset, Stats Report:
Device.....TotalActiveTime(HH:MM:SS).....Counts:
" //// Format for Log DeVices.each { dev -> if (statsReportBool) { //// calc total active time, build report int total = state.DeVices[dev.id].total / 1000 float totalDays = (total / 86400) as float int hours = total / 3600 total = total % 3600 int mins = total / 60 int secs = total % 60 String time = "$hours:${mins < 10 ? "0" : ""}$mins:${secs < 10 ? "0" : ""}$secs" statsReportPush = "$statsReportPush" + "${dev}.....${time}.....${state.DeVices[dev.id].counts} \r\n" //// Format for Push statsReportLog = "$statsReportLog" + "${dev}.....${time}.....${state.DeVices[dev.id].counts}
" //// Format for Log } state.DeVices[dev.id].start = now() state.DeVices[dev.id].total = 0 state.DeVices[dev.id].counts = 0 } state.lastPush = statsReportLog //// For status Bar if (statsReportBool && pushDevice && (pushMode == location.mode || pushMode == "Any")) {pushDevice.deviceNotification(statsReportPush)} if (logInfoEnableBool) {log.info "${statsReportLog}"} } def buildCron() {//// Build Cron for All Locked Time if used if (allLockBool) { String formattedTimeOff = allLockTime.substring(11, allLockTime.length() - 12) String hoursOff = formattedTimeOff.substring(0, formattedTimeOff.length() - 3) // Chop off the last 3 in string String minutesOff = formattedTimeOff.substring(3) // Chop off the first 3 in string state.allLockCron = "0 ${minutesOff} ${hoursOff} ? * * * " } //// Stats Reset Cron if (statsResetBool) { String formattedTimeReset = statsResetTime.substring(11, statsResetTime.length() - 12) String hoursReset = formattedTimeReset.substring(0, formattedTimeReset.length() - 3) // Chop off the last 3 in string String minutesReset = formattedTimeReset.substring(3) // Chop off the first 3 in string if (statsResetDay == "Everyday") {state.statsResetCron = "0 ${minutesReset} ${hoursReset} ? * * *" } else state.statsResetCron = "0 ${minutesReset} ${hoursReset} ? * ${statsResetDay} *" } if (logDebugEnableBool) {log.debug "App: ${app.label} -- Cron Schedule(s) Successfully Built"} } ///////////////////////////////////////////////////////////// LOCK CODES ///////////////////////////////////////////////////////// def setLockCode(btn) { if (btn == "lockCodeButton") { int slotNumberInt = Integer.valueOf(slotNumber) lockCodeDev.setCode(slotNumberInt, pinCode, codeName) // Call the setCode() command on the lock device state.lockSetMSG = "**${lockCodeDev}**; Code Set: slot=${slotNumber}, code=${pinCode}, name=${codeName}.   Lock State = ${lockCodeDev.currentLock}" } else if (btn == "deleteSlotButton") { int deleteSlotNumberInt = Integer.valueOf(deleteSlotNumber) lockCodeDev.deleteCode(deleteSlotNumberInt) state.lockSetMSG = "**${lockCodeDev}**; Code Deleted: Slot number ${deleteSlotNumber} and associated code.   Lock State = ${lockCodeDev.currentLock}" } else if (btn == "testCodeButton") { lockCodeDev.testUnlockWithCode(testCode) state.lockSetMSG = "**${lockCodeDev}**; Code ${testCode} Tested.   Lock State = ${lockCodeDev.currentLock}" } else if (btn == "retrieveCodeButton") { String lockCodes = lockCodeDev.currentValue("lockCodes") String codeLength = lockCodeDev.currentValue("codeLength") String maxCodes = lockCodeDev.currentValue("maxCodes") Map resultCode = [:] if (lockCodes) { if (lockCodes[0] == "{") resultCode = new JsonSlurper().parseText(lockCodes) else resultCode = new JsonSlurper().parseText(decrypt(lockCodes)) //decrypt codes if they're encrypted } state.lockSetMSG = "**${lockCodeDev}**; Retrieved Code(s) = ${resultCode}
Code digit Length = ${codeLength}, Max # of Codes = ${maxCodes}.   Lock State = ${lockCodeDev.currentLock}" } if (pushDevice && ((settings.notifyOptions1?.contains("pushLockSet") && state.DeVices["$lockCodeDev.id"].notifyOptions == 1) || (settings.notifyOptions2?.contains("pushLockSet") && state.DeVices["$lockCodeDev.id"].notifyOptions == 2) || (settings.notifyOptions3?.contains("pushLockSet") && state.DeVices["$lockCodeDev.id"].notifyOptions == 3))) { dataMap = [notifyCase:4, zone:state.DeVices["$lockCodeDev.id"].zone]; runIn(1, notifyDataHandler, [data:dataMap, overwrite:true])} if (logInfoEnableBool) {log.info "App: ${app.label} -- Lock Code Button Confrimation: ${state.lockSetMSG}"} } //////////////////////////////////////////////////////////////////// STATUS BAR //////////////////////////////////////////////////////////////////// String statusBar() { if (pauseAllBool) {paragraph getFormat("noticable","All App Lock Actions Paused!!!")} paragraph getFormat("smallLineSpace","Current Hub Mode:   ${location.mode}") if (pushDevice) {paragraph getFormat("important","Last Push Notification:   ${state.lastPush}" )} } ////////////////////////////////////////////////////////////////// ICON GRAPHICS ////////////////////////////////////////////////////////////////// def icons() { state.playIcon = "" state.pauseIcon = "" state.resetIcon = "" state.lockActionsIcon1 = "" state.lockActionsIcon2 = "" state.lockActionsIcon3 = "" state.lockActionsIcon4 = "" state.unLockActionsIcon1 = "" state.unLockActionsIcon2 = "" state.unLockActionsIcon3 = "" state.unLockActionsIcon4 = "" state.notifyOptionsIcon1 = "" state.notifyOptionsIcon2 = "" state.notifyOptionsIcon3 = "" state.notifyOptionsIcon4 = "" if (logDebugEnableBool) {log.debug "App: ${app.label} -- Get Icon Graphics"} } ////////////////////////////////////////////////////////////////// OTHER STUFF ////////////////////////////////////////////////////////////////// def updated() { // runs every 'Done' on already installed app unsubscribe() //// unschedule() // cancels all(or one) scheduled jobs including runIn icons() // Load Icon graphic states buildCron() initialize() // set schedules and subscribes } def installed() { updated() } // only runs once for new app 'Done' or first time open def logsOff() {log.info "${app.label} -- All App 'debug' logging auto disabling itself"; app?.updateSetting("logDebugEnableBool",[value:"false",type:"bool"]) } def getFormat(type, myText="") { if(type == "title") return "

${myText}

" // Steel-Blue if(type == "blueRegular") return "
${myText}
" // Steel-Blue if(type == "noticable") return "
${myText}
" // Burnt-Orange if(type == "lessImportant") return "
${myText}
" // Green if(type == "smallLineSpace") return "
${myText}
" // smallLineSpace if (formatBool) {if(type == "header") {return "
${myText}
"} // Burgandy-Red //// ver1.2.0 if(type == "important") return "
${myText}
" // Lime-Green } else {if(type == "header") return "
${myText}
" // Black if(type == "important") return "
${myText}
" // Black } } def displayTitle() {titleVersion(); section (getFormat("title", "App: ${state.name} - ${"ver " + state.version}")) {} }