Example from Gemini for Randomized "away from home" Lighting

Just thought I would share, I have recently used Gemini for a few apps and have been very happy with the 90-95% (or more) start it's given me from just the prompt. Here is an example, with the prompt I used. All apps so far have run cleanly from the start, and I've also used it to add common things like "timed debug logging".

Prompt

Please make a hubitat groovy app that allows me to select a group of switches and dimmers
which for me will be all the inside house lights, and then between sunset and some time in the
morning (or sunrise) it will turn these lights on and off randomly to mimic someone is home
when the house is empty. This means the lights will turn on and off randomly and not at
exactly the same time because it would take this faux person time to walk around the house. It
would also be good to have less on/off actions past some certain slow down time like
midnight, but still keep a few lights randomly changing.

Call the app "Home Alone" and put the author as "Macaulay Culkin".

Also give me:

  1. a slider from 1 to 10 for how busy the house is
  2. a way to mark lights that are close to each other and could be turned on very close in timetogether
  3. debug logging so I can monitor this app's performance easily
  4. a way to mark which lights are primary lights, since they would be visible from the street the most easily

App

definition(
    name: "Home Alone",
    namespace: "macaulayculkin",
    author: "Macaulay Culkin",
    description: "Keeps the Wet Bandits at bay by simulating an occupied home with randomized, human-like lighting behavior.",
    category: "Safety & Security",
    iconUrl: "",
    iconX2Url: ""
)

preferences {
    page(name: "mainPage")
}

def mainPage() {
    dynamicPage(name: "mainPage", title: "Home Alone Configuration", install: true, uninstall: true) {
        section("The Booby Traps (Lighting Selection)") {
            input "allLights", "capability.switch", title: "All Participating Lights", multiple: true, required: true
            input "primaryLights", "capability.switch", title: "Primary Lights (Visible from street - higher priority)", multiple: true, required: false
            input "groupedLights", "capability.switch", title: "Clustered Lights (Trigger together with a short 2-6s walking delay)", multiple: true, required: false
        }
        section("The Schedule") {
            input "enableSwitch", "capability.switch", title: "Enable Switch (e.g., a virtual switch tied to 'Away' mode)", required: false
            input "startTime", "time", title: "Start Time (e.g., Sunset)", required: true
            input "slowDownTime", "time", title: "Slow Down Time (e.g., Midnight)", required: true
            input "endTime", "time", title: "End Time (e.g., Sunrise)", required: true
            input "busyLevel", "number", title: "Busyness Level (1-10)", range: "1..10", defaultValue: 5, required: true
        }
        section("Logging") {
            input "debugLogging", "bool", title: "Enable Debug Logging", defaultValue: true
        }
    }
}

def installed() {
    logDebug "Home Alone installed."
    initialize()
}

def updated() {
    logDebug "Home Alone updated."
    unsubscribe()
    unschedule()
    initialize()
}

def initialize() {
    schedule(startTime, "startSimulation")
    schedule(endTime, "stopSimulation")
    
    // If we update the app in the middle of the night, kickstart it
    if (timeOfDayIsBetween(startTime, endTime, new Date(), location.timeZone)) {
        startSimulation()
    }
}

def startSimulation() {
    logDebug "Starting Home Alone simulation..."
    state.isRunning = true
    simulationTick()
}

def stopSimulation() {
    logDebug "Stopping Home Alone simulation..."
    state.isRunning = false
    logDebug "Turning off all participating lights."
    allLights?.off()
}

def simulationTick() {
    if (!state.isRunning) return

    if (enableSwitch && enableSwitch.currentValue("switch") == "off") {
        logDebug "Simulation paused: Enable switch is off. Checking again in 5 minutes."
        runIn(300, "simulationTick") 
        return
    }

    // Determine if we are past midnight/slow down time
    def isSlowPhase = timeOfDayIsBetween(slowDownTime, endTime, new Date(), location.timeZone)

    // Calculate next run time based on busyLevel (1-10)
    def maxDelayMins = (11 - busyLevel) * 4 // Busy 10 = 4 mins max. Busy 1 = 40 mins max.
    def minDelayMins = maxDelayMins / 2

    if (isSlowPhase) {
        maxDelayMins *= 3
        minDelayMins *= 3
        logDebug "Slow down phase active. Multiplier applied to delays."
    }

    def delaySeconds = (new Random().nextInt(((maxDelayMins - minDelayMins) * 60).intValue())) + (minDelayMins * 60).intValue()

    // Determine if we should turn a light ON or OFF to maintain realistic capacity
    def onLights = allLights.findAll { it.currentValue("switch") == "on" }
    def maxLightsOn = (busyLevel / 2).intValue() + 1
    def action = "on"
    
    if (onLights.size() >= maxLightsOn) {
        // Too many lights on, bias heavily toward turning one off
        action = new Random().nextInt(10) > 2 ? "off" : "on"
    } else {
        // Normal operation, slight bias to turning things on
        action = new Random().nextInt(10) > 3 ? "on" : "off"
    }

    // Build the pool of valid lights for the chosen action
    def pool = action == "on" ? allLights.findAll { it.currentValue("switch") == "off" } : onLights
    
    // Double the weight of primary lights if we are turning a light ON
    if (action == "on" && primaryLights) {
        pool += primaryLights.findAll { it.currentValue("switch") == "off" }
    }

    if (pool.size() > 0) {
        def randomLight = pool[new Random().nextInt(pool.size())]
        
        if (action == "on") {
            randomLight.on()
            logDebug "Turned ON ${randomLight.displayName}"
        } else {
            randomLight.off()
            logDebug "Turned OFF ${randomLight.displayName}"
        }

        // Handle Clustered/Grouped Lights
        if (groupedLights && groupedLights.find { it.id == randomLight.id }) {
            def otherGrouped = groupedLights.findAll { it.id != randomLight.id && it.currentValue("switch") != action }
            if (otherGrouped.size() > 0) {
                def companionLight = otherGrouped[new Random().nextInt(otherGrouped.size())]
                def companionDelay = new Random().nextInt(5) + 2 // 2 to 6 seconds walking delay
                logDebug "Clustered light detected. Triggering ${companionLight.displayName} in ${companionDelay} seconds to mimic walking."
                runIn(companionDelay, "triggerCompanion", [data: [lightId: companionLight.id, action: action]])
            }
        }
    } else {
        logDebug "No valid lights to turn ${action} right now."
    }

    logDebug "Next movement scheduled in ${delaySeconds} seconds."
    runIn(delaySeconds, "simulationTick")
}

def triggerCompanion(data) {
    def light = groupedLights.find { it.id == data.lightId }
    if (light) {
        if (data.action == "on") light.on() else light.off()
        logDebug "Companion light ${light.displayName} turned ${data.action}."
    }
}

def logDebug(msg) {
    if (debugLogging) {
        log.debug "Home Alone: ${msg}"
    }
}
2 Likes

FYI if you're looking for something along this line there are a few in HPM including the newly release Vacation Lighting Simulator; the one I use.

1 Like

+1 I'm the author of that app: [RELEASE] Vacation Lighting Simulator

But this is also very cool. The app (and I'ved used Claude here and there to help) accounts for a lot of the nuances here. Highly recommend running this code you have an ask Gemini to incorporate the goodness of the app I creates as well.

For example you might benefit from less random, ore pseudo-random on/offs, and the tweaks I did to reduce Hubitat load.

I love your "slider" idea there. Today I let people define how many lights should be on at a given time, but like the idea of making that more intuitive from the total available lights.

1 Like

In my honest opinion, when it comes to Groovy. Gemini Pro leads the way with understanding, fixes, and overall quality in what it produces.

2 Likes

I've been busy, but thanks for this link and discussion! I like the idea of "reduce load on the hubitat". I'll have to see how your app approaches that.

So far I haven't had a chance to run this but I was planning to just attach it to the HSM state of "away" which I have quick access to in my personalized Flutter app from the front page to just toggle on/off.

Writing groovy wasn't hard, but for me it's not frequent so every time is just ironing out all the kinks and getting access to the time formats and so on. This pretty much eliminates all that effort.

I also have enjoyed using Gemini for the JavaScript codec to decode the lorawan packets (I use Dragino devices and ChirpStack) I've recently set up. This [RELEASE] Simple MQTT Client Driver was awesome for that!