[RELEASE] Alexa Device Alert Monitor - Alexa Announcements for Hubitat Devices - Update 3.8

[RELEASE] Alexa Device Alert Monitor
Hubitat device monitoring into Alexa announcements - NO TTS device required!


What It Does

Creates virtual contact sensors that can trigger Alexa routine announcements when your doors, locks, or lights are left open/unlocked/on.

Example: Garage door left open for 30 minutes → Virtual contact opens → Alexa announces "Garage door has been left open" < Repeats now for (x) amount of times.

AI Discusses App. : https://www.youtube.com/watch?v=7MpPEkd8aq0

:clipboard: Requirements

  • Hubitat Elevation hub
  • Amazon Echo devices
  • Amazon Echo Skill app configured in Hubitat
  • Virtual devices created by the app (automatic)

:sparkles: Key Features

Individual Device Monitors

Monitor specific devices with custom delays

  • Added Repeat Feature - Allows app to repeat notifications for alerts.
  • Each device gets its own virtual contact sensor
  • Perfect for specific announcements - "Front door is unlocked" or "Garage door 2 is open"
  • Added Separate Repeat Interval - Introduced a "Repeat Interval" input (1-60 mins, default 5) for individual monitors, allowing repeats independent of the initial delay (0 for instant alerts).
  • Enabled Repeats for Zero Delay - Modified repeat logic to work with delay = 0, scheduling repeats based on the new "Repeat Interval" instead of the initial delay.

Group Monitors

Monitor multiple devices as one group

  • Example: 3 garage doors → One alert if ANY is open
  • Simplifies monitoring similar devices
  • Added Repeat Feature - Allows app to repeat notifications for alerts.
  • Added Group Name - Allows you to configure the group name.
  • Added Separate Repeat Interval - Introduced a "Repeat Interval" input (1-60 mins, default 5) for group monitors, allowing repeats independent of the initial delay (0 for instant alerts).
  • Enabled Repeats for Zero Delay - Modified repeat logic to work with delay = 0, scheduling repeats based on the new "Repeat Interval" instead of the initial delay.
  • Fixed Group Monitor log error. 3.3.7

Smart Delays (0-60 minutes)

  • 0 minutes = Immediate alert when device changes state
  • 30 minutes = Wait before alerting (great for garage doors)
  • Auto-cancellation = If you fix it before the delay expires, no alert!
  • Added Repeat Feature = Allows app to repeat notifications for alerts.

Master Status Check

Press a button to check ALL monitored devices at once

  • Creates separate contacts for each device type (Contacts/Locks/Lights)
  • Perfect for bedtime or "leaving house" routines
  • Each type opens only if that category has issues

Enable/Disable Monitors

Pause monitors without deleting settings

  • Great for seasonal use (disable garage alerts in winter)
  • Easy testing and troubleshooting

Auto-Naming & Customization

  • Auto-generates sensible device names
  • All devices prefixed with "V-ADAM" for easy identification
  • Customize names when you need multiple groups

Mode Restrictions

Only alert in specific modes (Away, Night, etc.)


:rocket: Installation

1. Install App Code

  1. Go to Apps Code+ New App
  2. Paste the app code
  3. Click Save

2. Create App Instance

  1. Go to AppsAdd User App
  2. Select "Alexa Device Alert Monitor"
  3. Click Done

3. Configure Monitors

Individual Monitor Example:

  • Device: Front Door Lock
  • Type: Lock
  • Delay: 0 minutes (immediate)
  • Creates: "V-ADAM - Lock Open - Front Door Alert"

Group Monitor Example:

  • Devices: All 3 garage door contacts
  • Type: Contact Sensor
  • Delay: 30 minutes
  • Creates: "V-ADAM - Any Garage Door Open Alert"

4. Expose to Alexa

  1. Open Amazon Echo Skill app in Hubitat
  2. Scroll to alexaDevices section
  3. Select all V-ADAM contacts
  4. Click Done

5. Create Alexa Routines

Example Routine:

  • When: "V-ADAM - Lock Open - Front Door Alert" opens
  • Then: Alexa says "Front door is unlocked"

:question: FAQ

Q: Why contact sensors instead of switches? A: Alexa routines work MUCH better with contact sensor triggers. Switches often don't show up or are unreliable in Alexa routines.

Q: Can I have multiple app instances? A: Yes! Perfect for different scenarios (daily, vacation, seasonal).

Q: Can the same device be in both individual AND group monitors? A: Yes! They work independently. Great for early specific warning + later general reminder.

Q: What happens if I change my mind on naming? A: Use "Remove All Virtual Devices" button in Advanced Options, then click Done to recreate with new settings.

Q: Do the virtual contacts stay open forever? A: No - they close automatically when the device returns to normal state (door closes, lock locks, etc.)

Q: Can I monitor the same device with different delays? A: Yes - create multiple monitors for the same device with different delays.


:inbox_tray: Copy / Paste - Code.

/**
 * ==========================  Alexa Device Alert Monitor 3.3.8 ==========================
 *  Platform: Hubitat Elevation
 *
 *  Copyright 2025
 *
 *  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.
 *
 * Changelog:
 * 1.0.0 (2025-10-15) - Initial release
 *                     - Uses Virtual Contact Sensors (open/closed) instead of switches for better Alexa compatibility
 *                     - Supports Group and Individual monitors
 *                     - Master Status Check feature - creates separate contacts per device type (Contacts, Locks, Lights)
 *                     - Advanced option to remove all virtual devices for troubleshooting
 * 2.0.0 (2025-10-15) - Fix master status check to handle missing contacts and stagger closure scheduling
 * 2.0.1 (2025-10-15) - Improved logging in closeAllMasterContacts for clarity when contacts are already closed
 * 2.1.0 (2025-10-16) - Added "Repeat Alert Until Resolved" option for group and individual monitors
 *                     - Reuses delay setting for repeat interval, disabled for zero-delay monitors
 *                     - Reopens virtual contact for 10 seconds if alert persists, stops when resolved
 * 2.2.0 (2025-10-16) - Added configurable repeat limits (1-10, default 3) to prevent alert fatigue
 *                     - Shows repeat count in monitor descriptions
 *                     - Tracks repeat progress in debug logs
 * 2.2.1 (2025-10-16) - Removed automatic deletion of all virtual devices in initialize() to preserve device IDs for Alexa compatibility
 * 3.3.4 (2025-10-18) - Added separate repeat interval for instant alerts (delay=0)
 * 3.3.5 (2025-10-18) - Added configurable virtual device open duration (1-10 sec, default 5)
                       - Made group name required with no auto-naming
                       - Updated logs to reflect dynamic open duration
 */

definition(
   name: "Alexa Device Alert Monitor",
   namespace: "ADAM 3.3.8 - Warlock-Weary + Claude + Grok",
   author: "Warlock-Weary + Claude + Grok",
   description: "Monitor devices and control virtual contact sensors for Alexa announcements",
   category: "Convenience",
   iconUrl: "",
   iconX2Url: "",
   iconX3Url: ""
)

preferences {
   page name: "pageMain"
   page name: "pageGroupMonitor"
   page name: "pageIndividualMonitor"
   page name: "pageRemoveGroupMonitor"
   page name: "pageRemoveIndividualMonitor"
}

Map pageMain() {
   state.remove("removedMonitor")
   state.remove("removeMonitor")
   dynamicPage(name: "pageMain", title: "<b>Alexa Device Alert Monitor Setup</b>", uninstall: true, install: true) {
      
      section("<hr style='background-color:#000000; height: 2px; border: 0;'><b>📦 Group Monitors:</b>") {
         paragraph "Monitor multiple devices together - if ANY device is in alert state, the virtual contact sensor opens."
         
         state.groupMonitors?.each { Integer monitorNum ->
            String customName = settings."groupMonitor_${monitorNum}_name"
            String type = settings."groupMonitor_${monitorNum}_type"
            String autoName = type ? "Any ${getTypeDisplayName(type)} Open" : "Group Monitor ${monitorNum}"
            String monitorName = customName ?: autoName
            Boolean isEnabled = settings."groupMonitor_${monitorNum}_enabled" != false
            String desc = getGroupMonitorDescription(monitorNum)
            String statusIcon = isEnabled ? "✅" : "⏸️"
            href(name: "pageGroupMonitor${monitorNum}Href",
               page: "pageGroupMonitor",
               params: [monitorNumber: monitorNum],
               title: "${statusIcon} ${monitorName}",
               description: desc ?: "Click/tap to configure...",
               state: desc ? "complete" : null)
         }
         input name: "btnNewGroupMonitor", type: "button", title: "➕ Add New Group Monitor"
      }
      
      section("<hr style='background-color:#000000; height: 2px; border: 0;'><b>🔧 Individual Monitors:</b>") {
         paragraph "Monitor single devices - one device per virtual contact sensor alert."
         
         state.individualMonitors?.each { Integer monitorNum ->
            com.hubitat.app.DeviceWrapper dev = settings."individualMonitor_${monitorNum}_device"
            String monitorName = dev?.displayName ?: "Individual Monitor ${monitorNum}"
            Boolean isEnabled = settings."individualMonitor_${monitorNum}_enabled" != false
            String desc = getIndividualMonitorDescription(monitorNum)
            String statusIcon = isEnabled ? "✅" : "⏸️"
            href(name: "pageIndividualMonitor${monitorNum}Href",
               page: "pageIndividualMonitor",
               params: [monitorNumber: monitorNum],
               title: "${statusIcon} ${monitorName}",
               description: desc ?: "Click/tap to configure...",
               state: desc ? "complete" : null)
         }
         input name: "btnNewIndividualMonitor", type: "button", title: "➕ Add New Individual Monitor"
      }

      section("<hr style='background-color:#000000; height: 2px; border: 0;'><b>🔘 Master Status Check:</b>") {
         paragraph "Trigger a full status check of all monitors with a button press. Creates separate virtual contacts for each device type you're monitoring."
         input name: "masterStatusCheckEnabled", type: "bool", title: "Enable Master Status Check", 
            defaultValue: false, submitOnChange: true
         
         if (settings["masterStatusCheckEnabled"]) {
            input name: "masterStatusCheckButton", type: "capability.pushableButton", 
               title: "Button Device:", submitOnChange: true
            
            if (settings["masterStatusCheckButton"]) {
               input name: "masterStatusCheckButtonNumber", type: "number", 
                  title: "Button Number:", range: "1..16", defaultValue: 1, required: true
            }
            
            paragraph "<b>Virtual Contact Sensors Created:</b>"
            paragraph "<small>The app will automatically create contacts only for device types you're monitoring:</small>"
            paragraph "• <b>V-ADAM - Master Status Check - Contacts</b> (opens if any contacts open)"
            paragraph "• <b>V-ADAM - Master Status Check - Locks</b> (opens if any locks unlocked)"
            paragraph "• <b>V-ADAM - Master Status Check - Lights</b> (opens if any lights on)"
            paragraph "<small>When button ${settings['masterStatusCheckButtonNumber'] ?: '1'} is pushed, each contact will open briefly if that device type has alerts, then close after a few seconds.</small>"
         }
      }

      section("<hr style='background-color:#000000; height: 2px; border: 0;'><b>⚙️ Advanced Options:</b>") {
    // Virtual Device Open Duration - FIRST
    paragraph "<b>🧭 Set Virtual Device Open Duration:</b>"
    paragraph "Set how long the virtual device stays open (1-10 seconds, default 5)"
    input name: "virtualDeviceOpenDuration", type: "number", 
        title: "Virtual Device Open Duration (seconds):", 
        range: "1..10", 
        defaultValue: 5, 
        required: false, 
        width: 6
    
    // Debug Logging Options - SECOND
    paragraph "<hr>"
    paragraph "<b>🔍 Debug Logging:</b>"
    input name: "debugLogging", type: "bool", title: "Enable debug logging", defaultValue: false, submitOnChange: true
    if (settings["debugLogging"]) {
        input name: "debugLoggingAutoDisable", type: "bool", title: "Automatically disable debug logging after 30 minutes", 
            defaultValue: true, width: 8
    }
    
    // Danger Zone - LAST
    paragraph "<hr>"
    paragraph "<b>⚠️ Danger Zone:</b>"
    input name: "btnRemoveAllDevices", type: "button", title: "🗑️ Remove All Virtual Devices", submitOnChange: true
    paragraph "<i>This will delete ALL virtual contact sensors created by this app. Your monitor settings will be preserved, but devices will need to be recreated by clicking Done.</i>"
      }
   }
}

Map pageGroupMonitor(Map params) {
   Integer monitorNum
   if (params?.monitorNumber != null) {
      state.currMonitorNum = params.monitorNumber
      monitorNum = params.monitorNumber
   }
   else {
      monitorNum = state.currMonitorNum
   }
   
   dynamicPage(name: "pageGroupMonitor", title: "<b>Group Monitor Configuration</b>", uninstall: false, install: false, nextPage: "pageMain") {
      section("<hr style='background-color:#000000; height: 2px; border: 0;'><b>Group Settings:</b>") {
         input name: "groupMonitor_${monitorNum}_enabled", type: "bool", title: "Enable this monitor", 
            defaultValue: true, submitOnChange: true
         
         input name: "groupMonitor_${monitorNum}_name", type: "text", title: "Custom Group Name (required):", 
            required: true, submitOnChange: true,
            description: "Enter a custom name to identify this group."
         
         input name: "groupMonitor_${monitorNum}_type", type: "enum", title: "Device Type:", 
            options: ["contact": "Contact Sensor", "lock": "Lock", "switch": "Switch/Light"],
            required: true, submitOnChange: true
         
         if (settings."groupMonitor_${monitorNum}_type") {
            String devType = settings."groupMonitor_${monitorNum}_type"
            String capability = getCapabilityForType(devType)
            input name: "groupMonitor_${monitorNum}_devices", type: "capability.${capability}", 
               title: "Select Devices:", multiple: true, required: true, submitOnChange: true
         }
         
         input name: "groupMonitor_${monitorNum}_delay", type: "number", title: "Initial Delay (minutes, 0=immediate):", 
            range: "0..60", defaultValue: 0, required: true, width: 6
         
         input name: "groupMonitor_${monitorNum}_repeat", type: "bool", title: "Repeat Alert Until Resolved", 
            defaultValue: false, submitOnChange: true
         
         if (settings."groupMonitor_${monitorNum}_repeat") {
            input name: "groupMonitor_${monitorNum}_maxRepeats", type: "number", 
               title: "Maximum number of repeats:", 
               range: "1..10", defaultValue: 3, required: true, width: 6
            input name: "groupMonitor_${monitorNum}_repeatInterval", type: "number", 
               title: "Repeat Interval (minutes):", 
               range: "1..60", defaultValue: 5, required: true, width: 6
         }
         
         input name: "groupMonitor_${monitorNum}_modes", type: "mode", title: "Only alert in these modes (optional):", 
            multiple: true, required: false
         
         if (settings."groupMonitor_${monitorNum}_type" && settings."groupMonitor_${monitorNum}_devices" && settings."groupMonitor_${monitorNum}_name") {
            String displayName = settings."groupMonitor_${monitorNum}_name"
            paragraph "<b>Virtual Contact Sensor:</b> V-ADAM - ${displayName} Alert"
         }
      }
      
      section() {
         href(name: "hrefRemoveGroupMonitor",
            page: "pageRemoveGroupMonitor",
            title: "🗑️ Remove This Group Monitor",
            description: "Delete this monitor and its virtual contact sensor",
            params: [monitorNumber: monitorNum]
         )
      }
   }
}

Map pageIndividualMonitor(Map params) {
   Integer monitorNum
   if (params?.monitorNumber != null) {
      state.currMonitorNum = params.monitorNumber
      monitorNum = params.monitorNumber
   }
   else {
      monitorNum = state.currMonitorNum
   }
   
   dynamicPage(name: "pageIndividualMonitor", title: "<b>Individual Monitor Configuration</b>", uninstall: false, install: false, nextPage: "pageMain") {
      section("<hr style='background-color:#000000; height: 2px; border: 0;'><b>Monitor Settings:</b>") {
         input name: "individualMonitor_${monitorNum}_enabled", type: "bool", title: "Enable this monitor", 
            defaultValue: true, submitOnChange: true
         
         input name: "individualMonitor_${monitorNum}_type", type: "enum", title: "Device Type:", 
            options: ["contact": "Contact Sensor", "lock": "Lock", "switch": "Switch/Light"],
            required: true, submitOnChange: true
         
         if (settings."individualMonitor_${monitorNum}_type") {
            String devType = settings."individualMonitor_${monitorNum}_type"
            String capability = getCapabilityForType(devType)
            input name: "individualMonitor_${monitorNum}_device", type: "capability.${capability}", 
               title: "Select Device:", multiple: false, required: true, submitOnChange: true
         }
         
         input name: "individualMonitor_${monitorNum}_delay", type: "number", title: "Initial Delay (minutes, 0=immediate):", 
            range: "0..60", defaultValue: 0, required: true, width: 6
         
         input name: "individualMonitor_${monitorNum}_repeat", type: "bool", title: "Repeat Alert Until Resolved", 
            defaultValue: false, submitOnChange: true
         
         if (settings."individualMonitor_${monitorNum}_repeat") {
            input name: "individualMonitor_${monitorNum}_maxRepeats", type: "number", 
               title: "Maximum number of repeats:", 
               range: "1..10", defaultValue: 3, required: true, width: 6
            input name: "individualMonitor_${monitorNum}_repeatInterval", type: "number", 
               title: "Repeat Interval (minutes):", 
               range: "1..60", defaultValue: 5, required: true, width: 6
         }
         
         input name: "individualMonitor_${monitorNum}_modes", type: "mode", title: "Only alert in these modes (optional):", 
            multiple: true, required: false
         
         if (settings."individualMonitor_${monitorNum}_device") {
            com.hubitat.app.DeviceWrapper dev = settings."individualMonitor_${monitorNum}_device"
            String devType = settings."individualMonitor_${monitorNum}_type"
            String stateName = getStateNameForType(devType)
            String switchName = "V-ADAM - ${getTypeDisplayName(devType)} ${stateName} - ${dev.displayName} Alert"
            paragraph "<b>Virtual Contact Sensor Name:</b> ${switchName}"
         }
      }
      
      section() {
         href(name: "hrefRemoveIndividualMonitor",
            page: "pageRemoveIndividualMonitor",
            title: "🗑️ Remove This Individual Monitor",
            description: "Delete this monitor and its virtual contact sensor",
            params: [monitorNumber: monitorNum]
         )
      }
   }
}

Map pageRemoveGroupMonitor(Map params) {
   dynamicPage(name: "pageRemoveGroupMonitor", title: "Remove Group Monitor", uninstall: false, install: false, nextPage: "pageMain") {
      if (params?.monitorNumber != null) state.removeMonitor = params.monitorNumber
      section("") {
         if (state.removeMonitor != null && state.removedMonitor != true) {
            String customName = settings."groupMonitor_${state.removeMonitor}_name"
            String type = settings."groupMonitor_${state.removeMonitor}_type"
            String autoName = type ? "Any ${getTypeDisplayName(type)} Open" : "Group Monitor"
            String switchName = customName ?: autoName
            paragraph "⚠️ This will delete the virtual contact sensor 'V-ADAM - ${switchName} Alert' and all settings for this monitor."
            paragraph "Any Alexa routines using this sensor will stop working."
            paragraph "Press the button below to confirm removal, or press 'Next' to cancel."
            input name: "btnRemoveGroupMonitor.${state.removeMonitor}", type: "button", title: "🗑️ Confirm Removal", submitOnChange: true
         }
         else {
            if (state.removedMonitor) paragraph "✅ Monitor removed. Press 'Next' to continue."
            else paragraph "Unknown removal status. Try again if needed. Press 'Next' to continue."
         }
      }
   }
}

Map pageRemoveIndividualMonitor(Map params) {
   dynamicPage(name: "pageRemoveIndividualMonitor", title: "Remove Individual Monitor", uninstall: false, install: false, nextPage: "pageMain") {
      if (params?.monitorNumber != null) state.removeMonitor = params.monitorNumber
      section("") {
         if (state.removeMonitor != null && state.removedMonitor != true) {
            com.hubitat.app.DeviceWrapper dev = settings."individualMonitor_${state.removeMonitor}_device"
            String devType = settings."individualMonitor_${state.removeMonitor}_type"
            String stateName = devType ? getStateNameForType(devType) : "Alert"
            String typeDisplay = devType ? getTypeDisplayName(devType) : "Device"
            String switchName = dev ? "V-ADAM - ${typeDisplay} ${stateName} - ${dev.displayName} Alert" : "Monitor ${state.removeMonitor}"
            paragraph "⚠️ This will delete the virtual contact sensor '${switchName}' and all settings for this monitor."
            paragraph "Any Alexa routines using this sensor will stop working."
            paragraph "Press the button below to confirm removal, or press 'Next' to cancel."
            input name: "btnRemoveIndividualMonitor.${state.removeMonitor}", type: "button", title: "🗑️ Confirm Removal", submitOnChange: true
         }
         else {
            if (state.removedMonitor) paragraph "✅ Monitor removed. Press 'Next' to continue."
            else paragraph "Unknown removal status. Try again if needed. Press 'Next' to continue."
         }
      }
   }
}

String getGroupMonitorDescription(Integer monitorNum) {
   String desc = ""
   List devices = settings."groupMonitor_${monitorNum}_devices"
   String type = settings."groupMonitor_${monitorNum}_type"
   Integer delay = settings."groupMonitor_${monitorNum}_delay"
   Boolean isEnabled = settings."groupMonitor_${monitorNum}_enabled" != false
   Boolean repeatEnabled = settings."groupMonitor_${monitorNum}_repeat" == true
   
   if (devices && type) {
      desc = "${devices.size()} ${getTypeDisplayName(type)}(s)"
      if (delay != null) {
         desc += delay == 0 ? " • Immediate" : " • ${delay} min delay"
      }
      if (repeatEnabled) {
         Integer repeatInterval = settings."groupMonitor_${monitorNum}_repeatInterval" ?: 5
         Integer maxRepeats = settings."groupMonitor_${monitorNum}_maxRepeats" ?: 3
         desc += " • Repeat every ${repeatInterval} min (${maxRepeats}x)"
      }
      if (!isEnabled) {
         desc += " • DISABLED"
      }
   }
   return desc
}

String getIndividualMonitorDescription(Integer monitorNum) {
   String desc = ""
   com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
   String type = settings."individualMonitor_${monitorNum}_type"
   Integer delay = settings."individualMonitor_${monitorNum}_delay"
   Boolean isEnabled = settings."individualMonitor_${monitorNum}_enabled" != false
   Boolean repeatEnabled = settings."individualMonitor_${monitorNum}_repeat" == true
   
   if (device && type) {
      desc = getTypeDisplayName(type)
      if (delay != null) {
         desc += delay == 0 ? " • Immediate" : " • ${delay} min delay"
      }
      if (repeatEnabled) {
         Integer repeatInterval = settings."individualMonitor_${monitorNum}_repeatInterval" ?: 5
         Integer maxRepeats = settings."individualMonitor_${monitorNum}_maxRepeats" ?: 3
         desc += " • Repeat every ${repeatInterval} min (${maxRepeats}x)"
      }
      if (!isEnabled) {
         desc += " • DISABLED"
      }
   }
   return desc
}

String getCapabilityForType(String type) {
   switch(type) {
      case "contact": return "contactSensor"
      case "lock": return "lock"
      case "switch": return "switch"
      default: return "sensor"
   }
}

String getTypeDisplayName(String type) {
   switch(type) {
      case "contact": return "Garage Door"
      case "lock": return "Lock"
      case "switch": return "Light"
      default: return "Device"
   }
}

String getStateNameForType(String type) {
   switch(type) {
      case "contact": return "Open"
      case "lock": return "Open"
      case "switch": return "On"
      default: return "Alert"
   }
}

String getAttributeForType(String type) {
   switch(type) {
      case "contact": return "contact"
      case "lock": return "lock"
      case "switch": return "switch"
      default: return "state"
   }
}

String getAlertValueForType(String type) {
   switch(type) {
      case "contact": return "open"
      case "lock": return "unlocked"
      case "switch": return "on"
      default: return "active"
   }
}

void removeGroupMonitor(Integer monitorNum) {
   logDebug "removeGroupMonitor($monitorNum)"
   
   // Delete virtual contact sensor
   String customName = settings."groupMonitor_${monitorNum}_name"
   String type = settings."groupMonitor_${monitorNum}_type"
   if (type) {
      String autoName = "Any ${getTypeDisplayName(type)} Open"
      String switchName = customName ?: autoName
      deleteVirtualSwitch("V-ADAM - ${switchName} Alert")
   }
   
   // Remove all settings for this monitor
   def settingNamesToRemove = settings?.keySet()?.findAll { it.startsWith("groupMonitor_${monitorNum}_") }
   logDebug "  Settings to remove: $settingNamesToRemove"
   settingNamesToRemove.each { settingName ->
      app.removeSetting(settingName)
   }
   
   state.groupMonitors.remove(monitorNum as Integer)
   state.remove('removeMonitor')
   state.removedMonitor = true
   logDebug "Finished removing group monitor $monitorNum"
}

void removeIndividualMonitor(Integer monitorNum) {
   logDebug "removeIndividualMonitor($monitorNum)"
   
   // Delete virtual contact sensor
   com.hubitat.app.DeviceWrapper dev = settings."individualMonitor_${monitorNum}_device"
   String devType = settings."individualMonitor_${monitorNum}_type"
   if (dev && devType) {
      String stateName = getStateNameForType(devType)
      String switchName = "V-ADAM - ${getTypeDisplayName(devType)} ${stateName} - ${dev.displayName} Alert"
      deleteVirtualSwitch(switchName)
   }
   
   // Remove all settings for this monitor
   def settingNamesToRemove = settings?.keySet()?.findAll { it.startsWith("individualMonitor_${monitorNum}_") }
   logDebug "  Settings to remove: $settingNamesToRemove"
   settingNamesToRemove.each { settingName ->
      app.removeSetting(settingName)
   }
   
   state.individualMonitors.remove(monitorNum as Integer)
   state.remove('removeMonitor')
   state.removedMonitor = true
   logDebug "Finished removing individual monitor $monitorNum"
}

void appButtonHandler(btn) {
   switch (btn) {
      case "btnNewGroupMonitor":
         if (state.groupMonitors == null) state.groupMonitors = []
         Integer newNum = (state.groupMonitors?.size() > 0) ? ((state.groupMonitors[-1] as Integer) + 1) : 0
         state.groupMonitors << newNum
         break
         
      case "btnNewIndividualMonitor":
         if (state.individualMonitors == null) state.individualMonitors = []
         Integer newNum = (state.individualMonitors?.size() > 0) ? ((state.individualMonitors[-1] as Integer) + 1) : 0
         state.individualMonitors << newNum
         break
         
      case { it.startsWith("btnRemoveGroupMonitor.") }:
         Integer monitorNum = (btn - "btnRemoveGroupMonitor.") as Integer
         removeGroupMonitor(monitorNum)
         break
         
      case { it.startsWith("btnRemoveIndividualMonitor.") }:
         Integer monitorNum = (btn - "btnRemoveIndividualMonitor.") as Integer
         removeIndividualMonitor(monitorNum)
         break
      
      case "btnRemoveAllDevices":
         removeAllVirtualDevices()
         break
         
      default:
         log.debug "Unhandled button press: $btn"
   }
}

com.hubitat.app.DeviceWrapper createVirtualSwitch(String switchName) {
   logDebug "Creating virtual contact sensor: ${switchName}"
   try {
      com.hubitat.app.DeviceWrapper existingDevice = getChildDevice("AlexaAlert_${app.id}_${switchName}")
      if (existingDevice) {
         logDebug "Virtual contact sensor already exists: ${switchName}"
         return existingDevice
      }
      
      com.hubitat.app.DeviceWrapper device = addChildDevice(
         "hubitat",
         "Virtual Contact Sensor",
         "AlexaAlert_${app.id}_${switchName}",
         [
            name: switchName,
            label: switchName
         ]
      )
      logDebug "Created virtual contact sensor: ${switchName}"
      return device
   }
   catch (Exception e) {
      log.error "Error creating virtual contact sensor ${switchName}: ${e.message}"
      return null
   }
}

void deleteVirtualSwitch(String switchName) {
   logDebug "Deleting virtual contact sensor: ${switchName}"
   try {
      com.hubitat.app.DeviceWrapper device = getChildDevice("AlexaAlert_${app.id}_${switchName}")
      if (device) {
         deleteChildDevice("AlexaAlert_${app.id}_${switchName}")
         logDebug "Deleted virtual contact sensor: ${switchName}"
      }
   }
   catch (Exception e) {
      log.error "Error deleting virtual contact sensor ${switchName}: ${e.message}"
   }
}

void removeAllVirtualDevices() {
   log.warn "Removing ALL virtual devices created by this app..."
   Integer deletedCount = 0
   
   try {
      List<com.hubitat.app.DeviceWrapper> childDevices = getChildDevices()
      childDevices.each { device ->
         try {
            String deviceName = device.label ?: device.name
            deleteChildDevice(device.deviceNetworkId)
            log.info "Deleted: ${deviceName}"
            deletedCount++
         }
         catch (Exception e) {
            log.error "Error deleting device ${device.deviceNetworkId}: ${e.message}"
         }
      }
      
      log.warn "Removed ${deletedCount} virtual device(s). Click Done to recreate them based on current monitor settings."
   }
   catch (Exception e) {
      log.error "Error removing devices: ${e.message}"
   }
}

void deviceEventHandler(evt) {
   logDebug "Device event: ${evt.device.displayName} ${evt.name} = ${evt.value}"
   
   // Check all group monitors
   state.groupMonitors?.each { Integer monitorNum ->
      List devices = settings."groupMonitor_${monitorNum}_devices"
      if (devices?.find { it.id == evt.device.id }) {
         handleGroupMonitorEvent(monitorNum)
      }
   }
   
   // Check all individual monitors
   state.individualMonitors?.each { Integer monitorNum ->
      com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
      if (device?.id == evt.device.id) {
         handleIndividualMonitorEvent(monitorNum)
      }
   }
}

void buttonEventHandler(evt) {
   logDebug "Button event: ${evt.device.displayName} button ${evt.value} pushed"
   
   if (settings["masterStatusCheckEnabled"]) {
      Integer buttonNum = settings["masterStatusCheckButtonNumber"] ?: 1
      Integer pushedButton = evt.value as Integer
      
      if (pushedButton == buttonNum) {
         logDebug "Master status check triggered by button ${buttonNum}"
         performMasterStatusCheck()
      }
   }
}

void performMasterStatusCheck() {
   logDebug "Performing master status check..."
   
   Map<String, Boolean> typeAlerts = [contact: false, lock: false, switch: false]
   Set<String> monitoredTypes = []
   
   state.groupMonitors?.each { Integer monitorNum ->
      if (settings."groupMonitor_${monitorNum}_enabled" != false) {
         String type = settings."groupMonitor_${monitorNum}_type"
         if (type) monitoredTypes.add(type)
         List devices = settings."groupMonitor_${monitorNum}_devices"
         if (devices && type) {
            String attribute = getAttributeForType(type)
            String alertValue = getAlertValueForType(type)
            if (devices.any { it.currentValue(attribute) == alertValue }) {
               typeAlerts[type] = true
            }
         }
      }
   }
   
   state.individualMonitors?.each { Integer monitorNum ->
      if (settings."individualMonitor_${monitorNum}_enabled" != false) {
         String type = settings."individualMonitor_${monitorNum}_type"
         if (type) monitoredTypes.add(type)
         com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
         if (device && type) {
            String attribute = getAttributeForType(type)
            String alertValue = getAlertValueForType(type)
            if (device.currentValue(attribute) == alertValue) {
               typeAlerts[type] = true
            }
         }
      }
   }
   
   logDebug "Master status check results - Contacts: ${typeAlerts.contact}, Locks: ${typeAlerts.lock}, Lights: ${typeAlerts.switch}"
   
   // Open contacts immediately (all types that are monitored)
   if (monitoredTypes.contains("contact")) {
      handleMasterContactByType("Contacts", typeAlerts.contact)
   }
   if (monitoredTypes.contains("lock")) {
      handleMasterContactByType("Locks", typeAlerts.lock)
   }
   if (monitoredTypes.contains("switch")) {
      handleMasterContactByType("Lights", typeAlerts.switch)
   }
   
   // Schedule a single closure for all contacts
   runIn(10, "closeAllMasterContacts", [data: [monitoredTypes: monitoredTypes]])
}

void handleMasterContactByType(String typeName, Boolean hasAlert) {
   String contactName = "V-ADAM - Master Status Check - ${typeName}"
   com.hubitat.app.DeviceWrapper contact = getChildDevice("AlexaAlert_${app.id}_${contactName}")
   
   if (!contact) {
      logDebug "Master contact for ${typeName} not found (may not be monitoring this type)"
      return
   }
   
   if (hasAlert) {
      if (contact.currentValue("contact") != "open") {
         contact.open()
         logDebug "Opened master contact: ${contactName}"
      }
   } else {
      if (contact.currentValue("contact") == "open") {
         contact.close()
         logDebug "Closed master contact: ${contactName} (no alerts)"
      }
   }
}

void closeAllMasterContacts(Map data) {
   Set<String> monitoredTypes = data.monitoredTypes
   logDebug "Closing all master contacts"
   
   monitoredTypes.each { type ->
      String typeName = type == "contact" ? "Contacts" : (type == "lock" ? "Locks" : "Lights")
      String contactName = "V-ADAM - Master Status Check - ${typeName}"
      com.hubitat.app.DeviceWrapper contact = getChildDevice("AlexaAlert_${app.id}_${contactName}")
      
      if (!contact) {
         logDebug "Failed to close master contact: ${contactName} (device not found)"
         return
      }
      if (contact.currentValue("contact") != "open") {
         logDebug "Master contact: ${contactName} is already closed"
         return
      }
      contact.close()
      logDebug "Closed master contact: ${contactName}"
   }
}

void verifyClosure(Map data) {
   String contactName = data.contactName
   com.hubitat.app.DeviceWrapper contact = getChildDevice("AlexaAlert_${app.id}_${contactName}")
   if (contact) {
      logDebug "Verification: ${contactName} state is ${contact.currentValue('contact')}"
   } else {
      logDebug "Verification failed: ${contactName} not found"
   }
}

void cleanupOpenContacts() {
   logDebug "Running cleanup for open master contacts"
   ["Contacts", "Locks", "Lights"].each { typeName ->
      String contactName = "V-ADAM - Master Status Check - ${typeName}"
      com.hubitat.app.DeviceWrapper contact = getChildDevice("AlexaAlert_${app.id}_${contactName}")
      if (contact && contact.currentValue("contact") == "open") {
         contact.close()
         logDebug "Cleanup: Closed master contact: ${contactName}"
      }
   }
}

void handleGroupMonitorEvent(Integer monitorNum) {
   // Check if monitor is enabled
   Boolean isEnabled = settings."groupMonitor_${monitorNum}_enabled" != false
   if (!isEnabled) {
      logDebug "Group monitor ${monitorNum} is disabled, skipping"
      return
   }
   
   String type = settings."groupMonitor_${monitorNum}_type"
   List devices = settings."groupMonitor_${monitorNum}_devices"
   Integer delay = settings."groupMonitor_${monitorNum}_delay" ?: 0
   Boolean repeatEnabled = settings."groupMonitor_${monitorNum}_repeat" == true
   List modes = settings."groupMonitor_${monitorNum}_modes"
   String customName = settings."groupMonitor_${monitorNum}_name"
   
   if (!devices || !type) return
   
   // Auto-generate name if not provided
   String autoName = devices.collect { it.displayName }.join(", ")
   String switchName = customName ?: autoName
   String fullSwitchName = "V-ADAM - ${switchName} Alert"
   
   // Check if we're in allowed mode
   if (modes && !modes.contains(location.mode)) {
      logDebug "Group monitor ${monitorNum}: Not in allowed mode, skipping"
      return
   }
   
   String attribute = getAttributeForType(type)
   String alertValue = getAlertValueForType(type)
   
   // Check if ANY device is in alert state
   Boolean anyInAlertState = devices.any { device ->
      device.currentValue(attribute) == alertValue
   }
   
   com.hubitat.app.DeviceWrapper virtualSwitch = getChildDevice("AlexaAlert_${app.id}_${fullSwitchName}")
   
   if (anyInAlertState) {
      logDebug "Group monitor ${monitorNum}: Device(s) in alert state"
      if (delay > 0) {
         logDebug "Scheduling switch ON in ${delay} minutes"
         runIn(delay * 60, "turnOnGroupSwitch", [data: [monitorNum: monitorNum]])
      } else {
         turnOnGroupSwitch([monitorNum: monitorNum])
      }
      // Schedule repeat if enabled
      if (repeatEnabled) {
         Integer maxRepeats = settings."groupMonitor_${monitorNum}_maxRepeats" ?: 3
         Integer repeatInterval = settings."groupMonitor_${monitorNum}_repeatInterval" ?: 5
         logDebug "Scheduling repeat alert for group monitor ${monitorNum} every ${repeatInterval} minutes (max ${maxRepeats}x)"
         runIn((repeatInterval * 60) + 10, "repeatGroupAlert", [data: [monitorNum: monitorNum, repeatCount: 0]])
      }
   } else {
      logDebug "Group monitor ${monitorNum}: All devices OK"
      // Note: We don't unschedule() here because it would cancel scheduled events for ALL monitors.
      // The scheduled methods (turnOnGroupSwitch, repeatGroupAlert) already check state before acting.
      if (virtualSwitch?.currentValue("contact") == "open") {
         virtualSwitch.close()
         logDebug "Closed contact: ${fullSwitchName}"
      }
   }
}

void turnOnGroupSwitch(Map data) {
   Integer monitorNum = data.monitorNum
   String type = settings."groupMonitor_${monitorNum}_type"
   List devices = settings."groupMonitor_${monitorNum}_devices"
   String customName = settings."groupMonitor_${monitorNum}_name"
   
   if (!devices || !type) return
   
   // Auto-generate name if not provided
   String autoName = devices.collect { it.displayName }.join(", ")
   String switchName = customName ?: autoName
   String fullSwitchName = "V-ADAM - ${switchName} Alert"
   
   String attribute = getAttributeForType(type)
   String alertValue = getAlertValueForType(type)
   
   // Double-check devices are still in alert state
   Boolean anyInAlertState = devices.any { device ->
      device.currentValue(attribute) == alertValue
   }
   
   if (anyInAlertState) {
      com.hubitat.app.DeviceWrapper virtualSwitch = getChildDevice("AlexaAlert_${app.id}_${fullSwitchName}")
      if (virtualSwitch && virtualSwitch.currentValue("contact") != "open") {
         virtualSwitch.open()
         logDebug "Opened contact: ${fullSwitchName}"
         Integer duration = settings.virtualDeviceOpenDuration ?: 5
         runIn(duration, "closeGroupSwitch", [data: [monitorNum: monitorNum]])
      }
   }
}

void turnOnIndividualSwitch(Map data) {
   Integer monitorNum = data.monitorNum
   String type = settings."individualMonitor_${monitorNum}_type"
   com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
   
   if (!device || !type) return
   
   String attribute = getAttributeForType(type)
   String alertValue = getAlertValueForType(type)
   String stateName = getStateNameForType(type)
   String switchName = "V-ADAM - ${getTypeDisplayName(type)} ${stateName} - ${device.displayName} Alert"
   
   // Double-check device is still in alert state
   if (device.currentValue(attribute) == alertValue) {
      com.hubitat.app.DeviceWrapper virtualSwitch = getChildDevice("AlexaAlert_${app.id}_${switchName}")
      if (virtualSwitch && virtualSwitch.currentValue("contact") != "open") {
         virtualSwitch.open()
         logDebug "Opened contact: ${switchName}"
         Integer duration = settings.virtualDeviceOpenDuration ?: 5
         runIn(duration, "closeIndividualSwitch", [data: [monitorNum: monitorNum]])
      }
   }
}

void repeatGroupAlert(Map data) {
   Integer monitorNum = data.monitorNum
   Integer repeatCount = data.repeatCount ?: 0
   Boolean isEnabled = settings."groupMonitor_${monitorNum}_enabled" != false
   Boolean repeatEnabled = settings."groupMonitor_${monitorNum}_repeat" == true
   String type = settings."groupMonitor_${monitorNum}_type"
   List devices = settings."groupMonitor_${monitorNum}_devices"
   Integer repeatInterval = settings."groupMonitor_${monitorNum}_repeatInterval" ?: 5
   Integer maxRepeats = settings."groupMonitor_${monitorNum}_maxRepeats" ?: 3
   List modes = settings."groupMonitor_${monitorNum}_modes"
   
   // Ensure maxRepeats is within valid range
   if (maxRepeats < 1) maxRepeats = 1
   if (maxRepeats > 10) maxRepeats = 10
   
   // Check if we've hit the limit
   if (repeatCount >= maxRepeats) {
      logDebug "Group monitor ${monitorNum}: Completed ${maxRepeats} repeats, stopping"
      return
   }
   
   if (!isEnabled || !repeatEnabled || !devices || !type) {
      logDebug "Repeat stopped for group monitor ${monitorNum}: disabled or invalid settings"
      return
   }
   
   if (modes && !modes.contains(location.mode)) {
      logDebug "Group monitor ${monitorNum}: Not in allowed mode, skipping repeat"
      return
   }
   
   String attribute = getAttributeForType(type)
   String alertValue = getAlertValueForType(type)
   
   Boolean anyInAlertState = devices.any { device ->
      device.currentValue(attribute) == alertValue
   }
   
   if (anyInAlertState) {
      logDebug "Repeat ${repeatCount + 1}/${maxRepeats} for group monitor ${monitorNum}"
      turnOnGroupSwitch([monitorNum: monitorNum])
      Integer duration = settings.virtualDeviceOpenDuration ?: 5
      runIn((repeatInterval * 60) + duration, "repeatGroupAlert", [data: [monitorNum: monitorNum, repeatCount: repeatCount + 1]])
   } else {
      logDebug "Group monitor ${monitorNum}: Alert resolved after ${repeatCount} repeat(s)"
   }
}

void handleIndividualMonitorEvent(Integer monitorNum) {
   // Check if monitor is enabled
   Boolean isEnabled = settings."individualMonitor_${monitorNum}_enabled" != false
   if (!isEnabled) {
      logDebug "Individual monitor ${monitorNum} is disabled, skipping"
      return
   }
   
   String type = settings."individualMonitor_${monitorNum}_type"
   com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
   Integer delay = settings."individualMonitor_${monitorNum}_delay" ?: 0
   Boolean repeatEnabled = settings."individualMonitor_${monitorNum}_repeat" == true
   List modes = settings."individualMonitor_${monitorNum}_modes"
   
   if (!device || !type) return
   
   // Check if we're in allowed mode
   if (modes && !modes.contains(location.mode)) {
      logDebug "Individual monitor ${monitorNum}: Not in allowed mode, skipping"
      return
   }
   
   String attribute = getAttributeForType(type)
   String alertValue = getAlertValueForType(type)
   String stateName = getStateNameForType(type)
   String switchName = "V-ADAM - ${getTypeDisplayName(type)} ${stateName} - ${device.displayName} Alert"
   
   com.hubitat.app.DeviceWrapper virtualSwitch = getChildDevice("AlexaAlert_${app.id}_${switchName}")
   
   if (device.currentValue(attribute) == alertValue) {
      logDebug "Individual monitor ${monitorNum}: Device in alert state"
      if (delay > 0) {
         logDebug "Scheduling switch ON in ${delay} minutes"
         runIn(delay * 60, "turnOnIndividualSwitch", [data: [monitorNum: monitorNum]])
      } else {
         turnOnIndividualSwitch([monitorNum: monitorNum])
      }
      // Schedule repeat if enabled
      if (repeatEnabled) {
         Integer maxRepeats = settings."individualMonitor_${monitorNum}_maxRepeats" ?: 3
         Integer repeatInterval = settings."individualMonitor_${monitorNum}_repeatInterval" ?: 5
         logDebug "Scheduling repeat alert for individual monitor ${monitorNum} every ${repeatInterval} minutes (max ${maxRepeats}x)"
         runIn((repeatInterval * 60) + 10, "repeatIndividualAlert", [data: [monitorNum: monitorNum, repeatCount: 0]])
      }
   } else {
      logDebug "Individual monitor ${monitorNum}: Device OK"
      // Note: We don't unschedule() here because it would cancel scheduled events for ALL monitors.
      // The scheduled methods (turnOnIndividualSwitch, repeatIndividualAlert) already check state before acting.
      if (virtualSwitch?.currentValue("contact") == "open") {
         virtualSwitch.close()
         logDebug "Closed contact: ${switchName}"
      }
   }
}

void closeGroupSwitch(Map data) {
   Integer monitorNum = data.monitorNum
   String customName = settings."groupMonitor_${monitorNum}_name"
   List devices = settings."groupMonitor_${monitorNum}_devices"
   String type = settings."groupMonitor_${monitorNum}_type"
   
   if (!devices || !type) return
   
   String autoName = devices.collect { it.displayName }.join(", ")
   String switchName = customName ?: autoName
   String fullSwitchName = "V-ADAM - ${switchName} Alert"
   
   com.hubitat.app.DeviceWrapper virtualSwitch = getChildDevice("AlexaAlert_${app.id}_${fullSwitchName}")
   if (virtualSwitch && virtualSwitch.currentValue("contact") == "open") {
      virtualSwitch.close()
      Integer duration = settings.virtualDeviceOpenDuration ?: 5
      logDebug "Closed contact: ${fullSwitchName} after ${duration} seconds"
   }
}

void closeIndividualSwitch(Map data) {
   Integer monitorNum = data.monitorNum
   String type = settings."individualMonitor_${monitorNum}_type"
   com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
   
   if (!device || !type) return
   
   String stateName = getStateNameForType(type)
   String switchName = "V-ADAM - ${getTypeDisplayName(type)} ${stateName} - ${device.displayName} Alert"
   
   com.hubitat.app.DeviceWrapper virtualSwitch = getChildDevice("AlexaAlert_${app.id}_${switchName}")
   if (virtualSwitch && virtualSwitch.currentValue("contact") == "open") {
      virtualSwitch.close()
      Integer duration = settings.virtualDeviceOpenDuration ?: 5
      logDebug "Closed contact: ${switchName} after ${duration} seconds"
   }
}

void repeatIndividualAlert(Map data) {
   Integer monitorNum = data.monitorNum
   Integer repeatCount = data.repeatCount ?: 0
   Boolean isEnabled = settings."individualMonitor_${monitorNum}_enabled" != false
   Boolean repeatEnabled = settings."individualMonitor_${monitorNum}_repeat" == true
   String type = settings."individualMonitor_${monitorNum}_type"
   com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
   Integer repeatInterval = settings."individualMonitor_${monitorNum}_repeatInterval" ?: 5
   Integer maxRepeats = settings."individualMonitor_${monitorNum}_maxRepeats" ?: 3
   List modes = settings."individualMonitor_${monitorNum}_modes"
   
   // Ensure maxRepeats is within valid range
   if (maxRepeats < 1) maxRepeats = 1
   if (maxRepeats > 10) maxRepeats = 10
   
   // Check if we've hit the limit
   if (repeatCount >= maxRepeats) {
      logDebug "Individual monitor ${monitorNum}: Completed ${maxRepeats} repeats, stopping"
      return
   }
   
   if (!isEnabled || !repeatEnabled || !device || !type) {
      logDebug "Repeat stopped for individual monitor ${monitorNum}: disabled or invalid settings"
      return
   }
   
   if (modes && !modes.contains(location.mode)) {
      logDebug "Individual monitor ${monitorNum}: Not in allowed mode, skipping repeat"
      return
   }
   
   String attribute = getAttributeForType(type)
   String alertValue = getAlertValueForType(type)
   
   if (device.currentValue(attribute) == alertValue) {
      logDebug "Repeat ${repeatCount + 1}/${maxRepeats} for individual monitor ${monitorNum}"
      turnOnIndividualSwitch([monitorNum: monitorNum])
      Integer duration = settings.virtualDeviceOpenDuration ?: 5
      runIn((repeatInterval * 60) + duration, "repeatIndividualAlert", [data: [monitorNum: monitorNum, repeatCount: repeatCount + 1]])
   } else {
      logDebug "Individual monitor ${monitorNum}: Alert resolved after ${repeatCount} repeat(s)"
   }
}

void installed() {
   log.info "Installed"
   initialize()
}

void updated() {
   log.info "Updated"
   unsubscribe()
   unschedule()
   initialize()
   
   // Persist default virtual device open duration if not set
   if (settings.virtualDeviceOpenDuration == null) {
      app.updateSetting("virtualDeviceOpenDuration", [type: "number", value: 5])
      log.debug "Persisted default virtual device open duration: 5 seconds"
   }
}

void initialize() {
   log.info "Initializing Alexa Device Alert Monitor"
   
   if (settings["debugLogging"]) {
      if (settings["debugLoggingAutoDisable"] != false) {
         log.debug "Debug logging enabled for 30 minutes"
         runIn(1800, disableDebugLogging)
      } else {
         log.debug "Debug logging enabled (will not auto-disable)"
      }
   }
   
   // Create master status check contact sensors if enabled
   if (settings["masterStatusCheckEnabled"]) {
      Set<String> monitoredTypes = []
      
      state.groupMonitors?.each { Integer monitorNum ->
         Boolean isEnabled = settings."groupMonitor_${monitorNum}_enabled" != false
         if (isEnabled) {
            String type = settings."groupMonitor_${monitorNum}_type"
            if (type) monitoredTypes.add(type)
         }
      }
      
      state.individualMonitors?.each { Integer monitorNum ->
         Boolean isEnabled = settings."individualMonitor_${monitorNum}_enabled" != false
         if (isEnabled) {
            String type = settings."individualMonitor_${monitorNum}_type"
            if (type) monitoredTypes.add(type)
         }
      }
      
      // Create virtual contacts for each monitored type
      if (monitoredTypes.contains("contact")) {
         String contactName = "V-ADAM - Master Status Check - Contacts"
         createVirtualSwitch(contactName)
         logDebug "Created/verified master contact for Contacts"
      }
      
      if (monitoredTypes.contains("lock")) {
         String contactName = "V-ADAM - Master Status Check - Locks"
         createVirtualSwitch(contactName)
         logDebug "Created/verified master contact for Locks"
      }
      
      if (monitoredTypes.contains("switch")) {
         String contactName = "V-ADAM - Master Status Check - Lights"
         createVirtualSwitch(contactName)
         logDebug "Created/verified master contact for Lights"
      }
      
      // Subscribe to button events
      if (settings["masterStatusCheckButton"]) {
         subscribe(settings["masterStatusCheckButton"], "pushed", buttonEventHandler)
         logDebug "Subscribed to button: ${settings['masterStatusCheckButton'].displayName}"
      }
   }
   
   // Create/update virtual contact sensors for group monitors
   state.groupMonitors?.each { Integer monitorNum ->
      Boolean isEnabled = settings."groupMonitor_${monitorNum}_enabled" != false
      if (!isEnabled) {
         logDebug "Skipping disabled group monitor ${monitorNum}"
         return
      }
      
      String customName = settings."groupMonitor_${monitorNum}_name"
      List devices = settings."groupMonitor_${monitorNum}_devices"
      String type = settings."groupMonitor_${monitorNum}_type"
      
      if (devices && type) {
         String autoName = devices.collect { it.displayName }.join(", ")
         String switchName = customName ?: autoName
         String fullSwitchName = "V-ADAM - ${switchName} Alert"
         com.hubitat.app.DeviceWrapper virtualSwitch = createVirtualSwitch(fullSwitchName)
         
         String attribute = getAttributeForType(type)
         devices.each { device ->
            subscribe(device, attribute, deviceEventHandler)
         }
         
         handleGroupMonitorEvent(monitorNum)
      }
   }
   
   // Create/update virtual contact sensors for individual monitors
   state.individualMonitors?.each { Integer monitorNum ->
      Boolean isEnabled = settings."individualMonitor_${monitorNum}_enabled" != false
      if (!isEnabled) {
         logDebug "Skipping disabled individual monitor ${monitorNum}"
         return
      }
      
      com.hubitat.app.DeviceWrapper device = settings."individualMonitor_${monitorNum}_device"
      String type = settings."individualMonitor_${monitorNum}_type"
      
      if (device && type) {
         String stateName = getStateNameForType(type)
         String switchName = "V-ADAM - ${getTypeDisplayName(type)} ${stateName} - ${device.displayName} Alert"
         com.hubitat.app.DeviceWrapper virtualSwitch = createVirtualSwitch(switchName)
         
         String attribute = getAttributeForType(type)
         subscribe(device, attribute, deviceEventHandler)
         
         handleIndividualMonitorEvent(monitorNum)
      }
   }
   
   log.info "Initialization complete"
}

void disableDebugLogging() {
   log.info "Debug logging automatically disabled after 30 minutes"
   app.updateSetting("debugLogging", [type: "bool", value: false])
}

void logDebug(String msg) {
   if (settings["debugLogging"]) {
      log.debug msg
   }
}



6 Likes

Will check this out, I've been building these manually -- so looking forward to an automated approach.

1 Like

As @briguy wrote above, I've been doing this manually in rule machine with virtual contact sensors and Alexa routines since Echo Speaks went sideways a couple of years ago. This will make it much easier for others to implement.

1 Like

Looks great, and takes the aggro out of alexa alerts. Thank you.

Did notice that this happens:

i.e. the name for the virtual contact seems to include "garage door" as default.

-=edit=-

I tried the 'repeat'. Issue with this maybe is that I can't seem to have a repeat notification on immediate - only works if there's a delay set. If it waits 3 mins to tell me the back door has been opened, the repeat only works every 3 mins too.

In reality, I'd like to be able to be able to have an instant alert, but then also set prefs to say how often to remind me too. Not sure if that's an easy fix.

For now, it's still webcore for me. Thank you though :slight_smile:

1 Like

@WarlockWeary Have you considered having this App published in HPM or at least publishing the Github link (vs. pasting code into this post) to allow for easier upgrades and so forth in the future?

1 Like

I have and will .. if we get a few more users / testers.

you can pick a group name now anything you want just don't leave it blank If you leave it blank it automatically picks whatever it thinks that's best which isn't always correct Also it added your request for your zero repeat as well as multiple repeats after that test out the new code let me know if it works.

Hi,

Thanks for that.

Tried the code for an individual instance (not group) and there's no way to alter the group name. So the sensor gets called:

Not the end of the world. Looks like it can only be altered in the group mode.

Code however, works fine :slight_smile: Thank you

1 Like

I joined just to say thank you for this. Installed today and it works great for my use case, which is getting notified when one of my garage doors has been left open. I had been trying to set these up manually when I discovered this app. Thank you!

1 Like

Thanks, I appreciate it.