Another ai generated app - using Claude

I use Claude ai for other coding so I thought I would try it with Hubitat. I asked it to write me an app that would allow me to specify certain lights and have it monitor them. When they change status, to then follow-up and make sure they do as they are told. I have rules that monitor a number of old z-wave lights this way. FYI - Z-Wave Poller doesn't work for most of these switches). Here is what it came up with and it seems to work

I initially asked it for the basic functionality, then asked it to add in a pushover option when it failed and then a debug option.

/**
 *  Light Monitor App
 *
 *  Description: Monitors light switches to ensure they successfully turn on/off as commanded
 *  and retries if necessary.
 *
 *  Copyright 2025
 *
 */

definition(
    name: "Light Monitor",
    namespace: "LightMonitor",
    author: "Claude",
    description: "Monitors lights to confirm they turn on/off as commanded and retries if necessary",
    category: "Convenience",
    iconUrl: "",
    iconX2Url: "",
    iconX3Url: ""
)

preferences {
    page(name: "mainPage")
}

def mainPage() {
    dynamicPage(name: "mainPage", title: "Light Monitor Configuration", install: true, uninstall: true) {
        section("Select Lights to Monitor") {
            input "lightSwitches", "capability.switch", title: "Select Light Switches", multiple: true, required: true
        }
        
        section("Monitoring Settings") {
            input "checkInterval", "number", title: "Status Check Interval (seconds)", defaultValue: 2, required: true
            input "maxRetries", "number", title: "Maximum Retries", defaultValue: 5, required: true
            input "commandTimeout", "number", title: "Command Timeout (seconds)", defaultValue: 15, required: true
        }
        
        section("Notifications") {
            input "sendPushNotification", "bool", title: "Send Push Notification on Failure?", defaultValue: false
            input "notificationDevices", "capability.notification", title: "Notification Devices", multiple: true, required: false
        }
        
        section("Logging") {
            input "debugMode", "bool", title: "Enable Debug Logging?", defaultValue: false
            input "debugDuration", "enum", title: "Debug Log Auto-Disable After", 
                options: [[0:"Never"],[30:"30 minutes"],[60:"1 hour"],[120:"2 hours"],[180:"3 hours"],[360:"6 hours"],[720:"12 hours"],[1440:"24 hours"]], 
                defaultValue: 30, required: true
        }
    }
}

def installed() {
    initialize()
    if (debugMode) {
        scheduleDebugLogDisable()
    }
}

def updated() {
    unsubscribe()
    unschedule()
    initialize()
    if (debugMode) {
        scheduleDebugLogDisable()
    }
}

def initialize() {
    // Store device IDs for verification
    state.configuredDeviceIds = lightSwitches?.collect { it.id.toString() } ?: []
    logDebug("Configured Device IDs: ${state.configuredDeviceIds}")
    
    subscribe(lightSwitches, "switch", lightSwitchHandler)
    state.monitoringState = [:]
    logInfo("Light Monitor initialized with ${lightSwitches.size()} light switches")
    if (debugMode) logDebug("Debug logging enabled")
    
    // Log all configured devices for verification
    if (debugMode) {
        lightSwitches.each { device ->
            logDebug("Configured device: ID=${device.id}, Name=${device.displayName}")
        }
    }
}

def scheduleDebugLogDisable() {
    if (debugDuration.toInteger() > 0) {
        logDebug("Debug logging will be automatically disabled after ${debugDuration} minutes")
        runIn(debugDuration.toInteger() * 60, disableDebugLog)
    }
}

def disableDebugLog() {
    app.updateSetting("debugMode", [value: "false", type: "bool"])
    logInfo("Debug logging automatically disabled")
}

def logDebug(msg) {
    if (debugMode) {
        log.debug "${app.name}: ${msg}"
    }
}

def logInfo(msg) {
    log.info "${app.name}: ${msg}"
}

def logWarn(msg) {
    log.warn "${app.name}: ${msg}"
}

def logError(msg) {
    log.error "${app.name}: ${msg}"
}

def lightSwitchHandler(evt) {
    def deviceId = evt.deviceId.toString()
    def deviceName = evt.displayName
    def newState = evt.value
    
    // Always log when monitoring starts, regardless of debug mode
    log.info "${app.name}: MONITORING STARTED - Light ${deviceName} (ID: ${deviceId}) changing to ${newState} at ${new Date().format('yyyy-MM-dd HH:mm:ss')}"
    
    // Verify the device exists in our list - first with direct check
    def deviceExists = lightSwitches.find { it.id.toString() == deviceId }
    
    // Diagnostic logging for device verification
    if (!deviceExists) {
        logWarn("Device with ID ${deviceId} triggered an event but may not be in our monitored devices list")
        logDebug("Event details: deviceId=${deviceId}, name=${deviceName}, value=${newState}, source=${evt.source}")
        logDebug("Current configured device IDs: ${state.configuredDeviceIds}")
        
        // Check if ID is in our stored list (cross-check)
        if (state.configuredDeviceIds.contains(deviceId)) {
            logWarn("Device ID ${deviceId} is in our configuration list but couldn't be found directly - attempting to proceed")
            // Since we know this device should be monitored, we'll continue with the event
        } else {
            // Try to recover by checking all devices
            def foundDevice = false
            lightSwitches.each { device ->
                logDebug("Checking device: ${device.id} vs event ${deviceId}")
                if (device.id.toString() == deviceId) {
                    foundDevice = true
                    deviceExists = device
                }
            }
            
            if (!foundDevice) {
                logWarn("Could not find device with ID ${deviceId} in configuration - ignoring event")
                return
            }
        }
    }
    
    logDebug("Device event details - ID: ${deviceId}, Name: ${deviceName}, New State: ${newState}, Event Source: ${evt.source}")
    
    // Initialize monitoring state for this device if it doesn't exist
    if (!state.monitoringState.containsKey(deviceId)) {
        state.monitoringState[deviceId] = [:]
        logDebug("Creating new monitoring state for device ${deviceName}")
    } else {
        logDebug("Updating existing monitoring state for device ${deviceName}")
    }
    
    // Get the current state directly before setting up monitoring
    def currentValue = null
    try {
        if (deviceExists) {
            currentValue = deviceExists.currentValue("switch")
            logDebug("Initial device state verification - Current value: ${currentValue}")
        } else {
            // Try to get the device from all available devices if we couldn't find it in our list
            def allDevices = getChildDevices()
            def alternateDevice = allDevices.find { it.id.toString() == deviceId }
            if (alternateDevice) {
                currentValue = alternateDevice.currentValue("switch")
                logDebug("Found device in alternate list - Current value: ${currentValue}")
            }
        }
    } catch (Exception e) {
        logError("Error getting current state for device ${deviceName}: ${e.message}")
    }
    
    // Set up monitoring for this device
    state.monitoringState[deviceId] = [
        desiredState: newState,
        checkCount: 0,
        lastCommand: newState,
        lastCheck: now(),
        startTime: now(),
        initialState: currentValue,
        deviceName: deviceName // Store the name for reference even if device becomes unavailable
    ]
    
    logDebug("Monitoring state initialized: ${state.monitoringState[deviceId]}")
    
    // Schedule the first check
    logDebug("Scheduling first check for device ${deviceName} in ${checkInterval} seconds")
    runIn(checkInterval, "checkLightStatus", [data: [deviceId: deviceId]])
    
    // Set up timeout check
    logDebug("Setting up timeout check for device ${deviceName} in ${commandTimeout} seconds")
    runIn(commandTimeout, "timeoutCheck", [data: [deviceId: deviceId]])
}

def checkLightStatus(data) {
    def deviceId = data.deviceId.toString()
    
    // Check if monitoring state exists
    if (!state.monitoringState.containsKey(deviceId)) {
        logDebug("No monitoring state found for device ID: ${deviceId} - Monitoring may have completed successfully")
        return
    }
    
    // Get device info
    def monitorData = state.monitoringState[deviceId]
    def deviceName = monitorData.deviceName ?: "Unknown Device"
    
    // Find device using multiple methods for reliability
    def device = null
    
    // Method 1: Check configured devices
    device = lightSwitches.find { it.id.toString() == deviceId }
    
    // Method 2: If not found, try child devices
    if (!device) {
        logDebug("Device ${deviceId} not found in primary list, checking all devices")
        def allDevices = getChildDevices()
        device = allDevices.find { it.id.toString() == deviceId }
    }
    
    // Method 3: If still not found, try to find by display name
    if (!device && deviceName != "Unknown Device") {
        logDebug("Attempting to find device by name: ${deviceName}")
        device = lightSwitches.find { it.displayName == deviceName }
    }
    
    // Check if device still exists
    if (!device) {
        logWarn("Device with ID ${deviceId} (${deviceName}) not found - Device may have been removed or is inaccessible")
        logDebug("Configured device IDs: ${state.configuredDeviceIds}")
        
        // Check if we should continue attempting to send commands or remove from monitoring
        if (monitorData.checkCount >= maxRetries / 2) {
            logWarn("MONITORING ABANDONED - Could not access device ${deviceName} after multiple attempts")
            state.monitoringState.remove(deviceId)
        } else {
            // Increase check count and try again
            monitorData.checkCount = monitorData.checkCount + 1
            state.monitoringState[deviceId] = monitorData
            
            // Schedule next check
            logDebug("Scheduling next check for device ${deviceName} in ${checkInterval} seconds")
            runIn(checkInterval, "checkLightStatus", [data: [deviceId: deviceId]])
        }
        return
    }
    
    // Safely get current state
    def currentState = null
    try {
        currentState = device.currentValue("switch")
    } catch (Exception e) {
        logError("Error getting current state for device ${deviceName}: ${e.message}")
        // Continue with null state - we'll handle this below
    }
    
    def desiredState = monitorData.desiredState
    def elapsedTime = (now() - monitorData.startTime) / 1000
    
    // Log current check status
    if (currentState == null) {
        logWarn("Unable to determine current state for device ${deviceName} - Device may be offline")
    } else {
        logDebug("Checking device ${deviceName} - Current state: ${currentState}, Desired state: ${desiredState}, Elapsed time: ${elapsedTime}s")
    }
    
    // Update last check time
    monitorData.lastCheck = now()
    state.monitoringState[deviceId] = monitorData
    
    // Check if we've reached the desired state
    if (currentState == desiredState) {
        // Success! Light is in desired state
        logInfo("MONITORING COMPLETED - Light ${deviceName} successfully changed to ${desiredState} after ${elapsedTime.toInteger()} seconds and ${monitorData.checkCount} retries")
        logDebug("Removing monitoring state for device ${deviceName}")
        state.monitoringState.remove(deviceId)
    } else {
        // Check if we've reached max retries
        if (monitorData.checkCount >= maxRetries) {
            logWarn("MONITORING FAILED - Light ${deviceName} failed to change to ${desiredState} after ${maxRetries} attempts and ${elapsedTime.toInteger()} seconds")
            
            // Provide more detailed diagnostics
            if (currentState == null) {
                logWarn("Could not determine final device state - Device may be offline or unresponsive")
            } else {
                logDebug("Final device state: ${currentState}, Retry count: ${monitorData.checkCount}")
            }
            
            if (sendPushNotification && notificationDevices) {
                logDebug("Sending failure notification to ${notificationDevices.size()} devices")
                notificationDevices.each { 
                    it.deviceNotification("Warning: Light ${deviceName} failed to change to ${desiredState} state after ${maxRetries} attempts")
                }
            }
            
            logDebug("Removing monitoring state for device ${deviceName}")
            state.monitoringState.remove(deviceId)
        } else {
            // Increment retry count and try again
            monitorData.checkCount = monitorData.checkCount + 1
            state.monitoringState[deviceId] = monitorData
            
            logInfo("Light ${deviceName} not in desired state (${desiredState}). Current state: ${currentState ?: 'Unknown'}. Retry attempt ${monitorData.checkCount}")
            
            // Send the command again
            try {
                if (desiredState == "on") {
                    logDebug("Sending ON command to device ${deviceName}")
                    device.on()
                } else {
                    logDebug("Sending OFF command to device ${deviceName}")
                    device.off()
                }
            } catch (Exception e) {
                logError("Error sending command to device ${deviceName}: ${e.message}")
            }
            
            // Schedule next check
            logDebug("Scheduling next check for device ${deviceName} in ${checkInterval} seconds")
            runIn(checkInterval, "checkLightStatus", [data: [deviceId: deviceId]])
        }
    }
}

def timeoutCheck(data) {
    def deviceId = data.deviceId.toString()
    
    // Check if monitoring state still exists
    if (!state.monitoringState.containsKey(deviceId)) {
        logDebug("Timeout check - Device ${deviceId} is no longer being monitored, likely completed successfully")
        return
    }
    
    // Get device info from multiple sources for reliability
    def device = lightSwitches.find { it.id.toString() == deviceId }
    def monitorData = state.monitoringState[deviceId]
    def deviceName = monitorData.deviceName ?: "Unknown Device"
    
    // If not found in primary list, try other methods
    if (!device) {
        logDebug("Timeout check: Device ${deviceId} not found in primary list, checking alternative sources")
        def allDevices = getChildDevices()
        device = allDevices.find { it.id.toString() == deviceId }
        
        if (!device && deviceName != "Unknown Device") {
            device = lightSwitches.find { it.displayName == deviceName }
        }
    }
    
    logDebug("Timeout check for device ${deviceName} - Desired state: ${monitorData.desiredState}, Check count: ${monitorData.checkCount}")
    
    // Check if we've had a recent check
    def timeSinceLastCheck = now() - monitorData.lastCheck
    logDebug("Time since last check: ${timeSinceLastCheck/1000} seconds")
    
    if (timeSinceLastCheck > (checkInterval * 1000 * 2)) {
        // We haven't checked recently, the monitoring might be stuck
        logWarn("MONITORING TIMEOUT - Light ${deviceName} command timed out after ${commandTimeout} seconds")
        logDebug("Monitoring appears stuck - last check was ${timeSinceLastCheck/1000} seconds ago")
        
        // Provide more detailed diagnostics
        if (device) {
            try {
                def finalState = device.currentValue("switch")
                logWarn("Final device state at timeout: ${finalState ?: 'Unknown'}")
            } catch (Exception e) {
                logError("Error getting final state for device ${deviceName}: ${e.message}")
            }
        } else {
            logWarn("Device ${deviceName} not found at timeout check - Device may have been removed from Hubitat")
        }
        
        if (sendPushNotification && notificationDevices) {
            logDebug("Sending timeout notification to ${notificationDevices.size()} devices")
            notificationDevices.each { 
                it.deviceNotification("Light ${deviceName} command timed out while attempting to change to ${monitorData.desiredState} state")
            }
        }
        
        // Clean up monitoring state for this device
        logDebug("Removing monitoring state for device ${deviceName} due to timeout")
        state.monitoringState.remove(deviceId)
    } else {
        logDebug("Timeout check passed - device ${deviceName} is still being actively monitored")
    }
}

So has this been working better than the Z wave polling? I finally got on here again to see if there was a better option for Z wave polling cause it rarely correctly shows the right on/off status of my older GE Z wave dimmers or my newer zooz dimmers

Polling should only be necessary for ZW devices that pre-date z-wave plus. Older GE switches and dimmers are perhaps the most common culprit.

But any newer Zooz switches and dimmers (or any other z-wave plus device) do not need polling, so if you are having status issues with those, that likely points to problems with your ZW mesh.

1 Like

Confirmed - it is only an issue with old switches. Of which I have a number throughout the house in low traffic areas where I don't need anything special. A lot of outdoor lights are good examples. Come on at dusk and go off at sleep.

1 Like

Very cool. I've been wanting to try doing that for webcore pistons as well.

I discovered somethi9ng cool. I have access to claude, and gemini 3 pro, so I exported a few of my rules and asked copilot (in vscode) to write me an app that combined the function and logic of a few rules that control my Christmas lights. It popped it right out.

I first had it read and analyze all of the hubitat documentation.

Kind of fun.

I have written a ton of custom drivers and apps using ai support. My biggest win so far is in logging. I use drivers to forward all syslog information from all of my habitats, plus z-wave and zigbee logging to a free copy of New Relic. I then point cursor ai at it and ask questions in natural language such as "what lights were on after we went to bed last night" or "what errors are you seeing over the last month and which are serious"

6 Likes

This is exactly what I want to do. Be able to use a ChatGPT agent or something to ask questions.

Can you explain a bit more how you did this?

I direct all of my syslogs to New Relic. Not only does it track hubitat logs, z-wave logs, zigbee logs but also logs from other systems such as my unraid, etc. I had been using splunk before this but it is very complicated. And originally I was using Synology Syslog but my Synology is so old it was choking. A new Synology would probably work really well plus it is local. I did write a relay that runs in python on a local machine and direct everything to that. I only did this to do some things like time correction locally etc. But not needed. Then I just trained Cursor to use the New Relic API.