/**
* Advanced Mode Manager
*/
definition(
name: "Advanced Mode Manager",
namespace: "ShaneAllen",
author: "ShaneAllen",
description: "Commercial-grade Mode engine. Handles timed transitions AND instant state enforcement. Features modular UI for Mode-based Device Control and Mode-to-Mode Transitions.",
category: "Convenience",
iconUrl: "",
iconX2Url: "",
iconX3Url: ""
)
preferences {
page(name: "mainPage")
}
def mainPage() {
dynamicPage(name: "mainPage", title: "Advanced Mode Manager", install: true, uninstall: true) {
section("Live Mode Dashboard & History") {
paragraph "
What it does: Provides a real-time view of your current Hubitat location mode and logs recent activity.
"
def statusExplanation = getHumanReadableStatus()
paragraph "" +
"Engine Status: ${statusExplanation}
"
def currentMode = location.mode ?: "Unknown"
def pendingActionStr = "None (Stable)"
if (state.pendingTargetMode && state.pendingTargetTime) {
def remainingMins = Math.max(0, Math.round((state.pendingTargetTime - now()) / 60000))
pendingActionStr = "Shifting to '${state.pendingTargetMode}' in ${remainingMins} minutes"
}
def dashHTML = """
| Real-Time Mode Metrics |
| Current Hub Mode | ${currentMode} |
| Pending Automated Transition | ${pendingActionStr} |
"""
paragraph dashHTML
input "sweepModeBtn", "button", title: "Sweep Mode (Force Immediate Enforcement)"
if (state.pendingTargetMode) input "abortTransition", "button", title: "Abort Pending Transition"
paragraph "
Recent Action History"
input "txtEnable", "bool", title: "Enable Description Text Logging", defaultValue: true
if (state.actionHistory) {
def historyStr = state.actionHistory.join("
")
paragraph "${historyStr}
"
}
input "clearHistory", "button", title: "Clear Action History"
}
// ==============================================================================
// SECTION 1: DEVICE CONTROL PER MODE
// ==============================================================================
section("Device Control per Mode") {
paragraph "Instant Enforcement: Define what devices should turn on, turn off, or lock the exact moment a specific mode becomes active. Click a rule below to expand it.
"
input "enableColorRefresh", "bool", title: "Enable 30-Minute Constant Color Refresh (Re-applies colors/effects if blockers are off)", defaultValue: true, submitOnChange: true
}
for (int i = 1; i <= 8; i++) {
def dynamicTitle = (settings["dcEnable${i}"] && settings["dcMode${i}"]) ? "Device Control Rule ${i} (${settings["dcMode${i}"]})" : "Device Control Rule ${i}"
section(dynamicTitle, hideable: true, hidden: true) {
input "dcEnable${i}", "bool", title: "Enable Device Control ${i}", defaultValue: false, submitOnChange: true
if (settings["dcEnable${i}"]) {
paragraph "State Sweep / Device Enforcement
"
input "dcMode${i}", "mode", title: "[TRIGGER] When mode becomes...", required: true, submitOnChange: true
input "dcSwitchesOff${i}", "capability.switch", title: "Turn OFF these switches", required: false, multiple: true
input "dcSwitchesOn${i}", "capability.switch", title: "Turn ON these switches", required: false, multiple: true
input "dcLocksLock${i}", "capability.lock", title: "Lock these doors", required: false, multiple: true
input "dcGarageClose${i}", "capability.garageDoorControl", title: "Close these garages", required: false, multiple: true
// --- DELAYED ACTIONS ---
paragraph "Delayed Actions
"
input "dcDelayedSwitchesOn${i}", "capability.switch", title: "Turn ON these switches after a delay", required: false, multiple: true
input "dcDelayMins${i}", "number", title: "Delay time (minutes)", required: false, defaultValue: 5
// --- WEATHER FORECAST SWITCH ---
paragraph "Weather Integrations
"
input "dcWeatherSwitch${i}", "capability.switch", title: "Weather Forecast Switch (Turns ON, waits 5 minutes, then turns OFF)", required: false, multiple: true
input "dcWeatherDelay${i}", "number", title: "Wait this many minutes before turning Weather Switch ON", required: false, defaultValue: 0
// --- AUDIO NOTIFICATIONS ---
paragraph "Audio Announcements
"
input "dcTtsSpeakers${i}", "capability.speechSynthesis", title: "Target Smart Speakers (Sonos, etc.)", required: false, multiple: true, submitOnChange: true
input "dcTtsMessage${i}", "text", title: "TTS Announcement Message", required: false, submitOnChange: true
input "dcTtsBlocker${i}", "capability.switch", title: "Block Announcements if this switch is ON (e.g., TV)", required: false, multiple: false
if (settings["dcTtsSpeakers${i}"] && settings["dcTtsMessage${i}"]) {
input "testTts${i}", "button", title: "Test TTS Announcement"
}
input "dcZoozChimes${i}", "capability.chime", title: "Target Zooz Chime Devices", required: false, multiple: true, submitOnChange: true
input "dcZoozSound${i}", "number", title: "Zooz Chime Sound File #", required: false, submitOnChange: true
if (settings["dcZoozChimes${i}"] && settings["dcZoozSound${i}"] != null) {
input "testZooz${i}", "button", title: "Test Zooz Chime"
}
// --- INOVELLI LED CONTROL ---
paragraph "Inovelli LED Notifications
"
input "dcInovelli${i}", "capability.configuration", title: "Set Inovelli LED Notifications for these switches", required: false, multiple: true, submitOnChange: true
if (settings["dcInovelli${i}"]) {
input "dcInovelliTarget${i}", "enum", title: "Target LEDs", required: true, defaultValue: "All", options: [
"All":"All LEDs", "7":"LED 7 (Top)", "6":"LED 6", "5":"LED 5", "4":"LED 4 (Middle)", "3":"LED 3", "2":"LED 2", "1":"LED 1 (Bottom)"
]
input "dcInovelliColor${i}", "enum", title: "LED Color", required: true, options: [
"0":"Red", "14":"Orange", "35":"Lemon", "64":"Lime",
"85":"Green", "106":"Teal", "127":"Cyan", "149":"Aqua",
"170":"Blue", "191":"Violet", "212":"Magenta", "234":"Pink", "255":"White"
]
input "dcInovelliLevel${i}", "number", title: "LED Light % (0-100)", required: true, defaultValue: 100
input "dcInovelliEffect${i}", "enum", title: "Notification Effect", required: true, options: [
"0":"Off", "1":"Solid", "2":"Fast Blink", "3":"Slow Blink", "4":"Pulse", "5":"Chase", "6":"Falling", "7":"Rising", "8":"Blink"
], defaultValue: "1"
input "dcInovelliBlocker${i}", "capability.switch", title: "Block LED color changes if this switch is ON (e.g., Mail Arrived Virtual Switch)", required: false, multiple: false
}
}
}
}
// ==============================================================================
// SECTION 2: MODE TO MODE TRANSITIONS
// ==============================================================================
section("Mode to Mode Transitions") {
paragraph "Automated Timers: Configure delayed transitions between modes. Example: When mode changes to 'Arrival', wait 15 minutes, then change to 'Home'. Click a rule below to expand it.
"
}
for (int i = 1; i <= 8; i++) {
def dynamicTitle = (settings["transEnable${i}"] && settings["transTriggerMode${i}"] && settings["transTargetMode${i}"]) ? "Transition Rule ${i} (${settings["transTriggerMode${i}"]} ➔ ${settings["transTargetMode${i}"]})" : "Transition Rule ${i}"
section(dynamicTitle, hideable: true, hidden: true) {
input "transEnable${i}", "bool", title: "Enable Transition Rule ${i}", defaultValue: false, submitOnChange: true
if (settings["transEnable${i}"]) {
input "transTriggerMode${i}", "mode", title: "[TRIGGER] If mode becomes...", required: true, submitOnChange: true
input "transDelay${i}", "number", title: "[TIMER] Wait this many minutes", required: true, defaultValue: 15
input "transTargetMode${i}", "mode", title: "[TRANSITION] Then change mode to...", required: true, submitOnChange: true
// Modular Toggle: Presence Tethering
input "transUseTether${i}", "bool", title: "Enable Presence Tethering?", defaultValue: false, submitOnChange: true
if (settings["transUseTether${i}"]) {
paragraph "Presence Tethering (Timer Intercept)
"
input "transTetherPresence${i}", "capability.presenceSensor", title: "Tether to Presence Sensor", required: true
input "transTetherFallbackMode${i}", "mode", title: "If sensor departs, force mode to...", required: true
}
// Modular Toggle: Condition Gates
input "transUseGates${i}", "bool", title: "Enable Condition Gates?", defaultValue: false, submitOnChange: true
if (settings["transUseGates${i}"]) {
paragraph "Condition Gates (Pre-Transition Check)
"
input "transConditionMotion${i}", "capability.motionSensor", title: "Abort if Motion active", required: false, multiple: true
input "transConditionPower${i}", "capability.powerMeter", title: "Abort if Power > Threshold", required: false
if (settings["transConditionPower${i}"]) input "transPowerThreshold${i}", "decimal", title: "Watts", defaultValue: 15.0
}
}
}
}
// ==============================================================================
// SECTION 3: INOVELLI SCHEDULED DIMMING
// ==============================================================================
section("Scheduled Inovelli LED Dimming") {
paragraph "Daily Timers: Automatically adjust the default brightness of your Inovelli LED light bars at specific times (e.g., dim at night, brighten in morning).
"
input "ledDimEnable", "bool", title: "Enable Scheduled Dimming", defaultValue: false, submitOnChange: true
if (settings["ledDimEnable"]) {
input "ledDimSwitches", "capability.configuration", title: "Target Inovelli Switches", required: true, multiple: true
paragraph "Timer 1 (e.g., Sunset/Dim)"
input "ledTime1", "time", title: "Time", required: true
input "ledLevel1", "number", title: "LED Light % (0-100)", required: true, defaultValue: 50
paragraph "Timer 2 (e.g., Sunrise/Bright)"
input "ledTime2", "time", title: "Time", required: true
input "ledLevel2", "number", title: "LED Light % (0-100)", required: true, defaultValue: 100
}
}
}
}
// ==============================================================================
// INTERNAL LOGIC ENGINE
// ==============================================================================
def installed() { initialize() }
def updated() { unsubscribe(); unschedule(); initialize() }
def initialize() {
if (!state.actionHistory) state.actionHistory = []
clearPendingTransition()
subscribe(location, "mode", modeChangeHandler)
// Schedule Inovelli LED Timers
if (settings["ledDimEnable"] && settings["ledTime1"]) {
schedule(settings["ledTime1"], "executeLedTimer1")
}
if (settings["ledDimEnable"] && settings["ledTime2"]) {
schedule(settings["ledTime2"], "executeLedTimer2")
}
// Schedule 30-Minute Refresh for Inovelli Colors
if (settings["enableColorRefresh"] != false) {
runEvery30Minutes("refreshInovelliColor")
} else {
unschedule("refreshInovelliColor")
}
for (int i = 1; i <= 8; i++) {
// Subscribe to Presence Tethers
if (settings["transEnable${i}"] && settings["transUseTether${i}"] && settings["transTetherPresence${i}"]) {
subscribe(settings["transTetherPresence${i}"], "presence", presenceTetherHandler)
}
// Subscribe to LED Blocker Switches
if (settings["dcEnable${i}"] && settings["dcInovelliBlocker${i}"]) {
subscribe(settings["dcInovelliBlocker${i}"], "switch.off", blockerSwitchHandler)
}
// Subscribe to Inovelli switches turning off
if (settings["dcEnable${i}"] && settings["dcInovelli${i}"]) {
subscribe(settings["dcInovelli${i}"], "switch.off", inovelliSwitchOffHandler)
}
}
logAction("Mode Manager Initialized.")
}
String getHumanReadableStatus() {
if (state.pendingTargetMode) return "Countdown Active. Monitoring tethers and waiting to transition."
return "Idle. Waiting for a trigger mode to activate."
}
def appButtonHandler(btn) {
if (btn == "abortTransition") {
logAction("User aborted transition to '${state.pendingTargetMode}'.")
clearPendingTransition()
}
else if (btn == "clearHistory") {
state.actionHistory = []
logAction("History cleared.")
}
else if (btn == "sweepModeBtn") {
logAction("Sweep Triggered. Enforcing rules for current mode...")
executeSweep()
}
else if (btn.startsWith("testTts")) {
def idx = btn.replaceAll("\\D+", "").toInteger()
def speakers = settings["dcTtsSpeakers${idx}"]
def msg = settings["dcTtsMessage${idx}"]
if (speakers && msg) {
speakers.speak(msg)
logAction("Tested TTS for Rule ${idx}: ${msg}")
}
}
else if (btn.startsWith("testZooz")) {
def idx = btn.replaceAll("\\D+", "").toInteger()
def chimes = settings["dcZoozChimes${idx}"]
def sound = settings["dcZoozSound${idx}"]
if (chimes && sound != null) {
// FIX APPLIED: Routing test button through safe Zooz Helper
playZoozSound(chimes, sound)
logAction("Tested Zooz Chime for Rule ${idx}: Sound ${sound}")
}
}
}
// ------------------------------------------------------------------------------
// HANDLERS
// ------------------------------------------------------------------------------
def presenceTetherHandler(evt) {
if (state.pendingRuleNumber == null || evt.value != "not present") return
def ruleIdx = state.pendingRuleNumber
if (settings["transUseTether${ruleIdx}"] && settings["transTetherPresence${ruleIdx}"]?.id == evt.device.id) {
def fallback = settings["transTetherFallbackMode${ruleIdx}"]
logAction("🚨 Tether Broken: ${evt.device.displayName} left. Forcing mode to '${fallback}'.")
clearPendingTransition()
location.setMode(fallback)
}
}
// This runs when a blocking switch (like Mail Arrived) turns off
def blockerSwitchHandler(evt) {
if (evt.value == "off") {
logAction("LED Blocker Switch '${evt.device.displayName}' turned off. Re-evaluating LED colors for current mode...")
def currentMode = location.mode
// Loop through all rules to find the one matching the CURRENT mode
for (int i = 1; i <= 8; i++) {
if (settings["dcEnable${i}"] && settings["dcMode${i}"] == currentMode) {
if (settings["dcInovelli${i}"] && settings["dcInovelliColor${i}"] != null) {
// Verify the rule's blocker is indeed off before applying
def ruleBlocker = settings["dcInovelliBlocker${i}"]
if (!ruleBlocker || ruleBlocker.currentValue("switch") != "on") {
enforceInovelliLEDs(i)
def colorMap = ["0":"Red", "14":"Orange", "35":"Lemon", "64":"Lime", "85":"Green", "106":"Teal", "127":"Cyan", "149":"Aqua", "170":"Blue", "191":"Violet", "212":"Magenta", "234":"Pink", "255":"White"]
def colorName = colorMap[settings["dcInovelliColor${i}"]] ?: settings["dcInovelliColor${i}"]
def levelStr = settings["dcInovelliLevel${i}"] ?: 100
def effectStr = settings["dcInovelliEffect${i}"] ?: "1"
def targetStr = settings["dcInovelliTarget${i}"] == "All" ? "All LEDs" : "LED ${settings["dcInovelliTarget${i}"]}"
logAction("Deferred LED Update -> [Inovelli: ${targetStr} ${colorName} at ${levelStr}% (Effect: ${effectStr})]")
}
}
}
}
}
}
// This runs when an Inovelli switch itself physically or digitally turns off
def inovelliSwitchOffHandler(evt) {
def currentMode = location.mode
for (int i = 1; i <= 8; i++) {
if (settings["dcEnable${i}"] && settings["dcMode${i}"] == currentMode) {
// Check if the device that triggered the event is used in this rule
if (settings["dcInovelli${i}"]?.find { it.id == evt.device.id }) {
def blocker = settings["dcInovelliBlocker${i}"]
// Only re-apply the mode color if the Mail Switch (Blocker) is OFF
if (!blocker || blocker.currentValue("switch") != "on") {
logAction("Switch '${evt.device.displayName}' turned off. Re-applying Mode LED settings.")
enforceInovelliLEDs(i)
}
}
}
}
}
def modeChangeHandler(evt) {
def newMode = evt.value
if (state.pendingTargetMode != null) {
logAction("Intervention: Mode changed to '${newMode}'. Aborting timer for '${state.pendingTargetMode}'.")
clearPendingTransition()
}
// 1. Process Instant Device Controls
for (int i = 1; i <= 8; i++) {
if (settings["dcEnable${i}"] && settings["dcMode${i}"] == newMode) {
logAction("Device Control ${i}: Instant Enforcement triggered for mode '${newMode}'.")
enforceDevices(i)
}
}
// 2. Process Transition Timers
for (int i = 1; i <= 8; i++) {
if (settings["transEnable${i}"] && settings["transTriggerMode${i}"] == newMode) {
def delayMins = settings["transDelay${i}"] ?: 0
if (delayMins > 0) {
def target = settings["transTargetMode${i}"]
state.pendingTriggerMode = newMode
state.pendingTargetMode = target
state.pendingTargetTime = now() + (delayMins * 60000)
state.pendingRuleNumber = i
logAction("Transition Rule ${i}: Delay active. Shifting '${newMode}' to '${target}' in ${delayMins} mins.")
runIn((delayMins * 60).toInteger(), "executeTransition")
break // Only allow one transition timer to run at a time
}
}
}
}
// ------------------------------------------------------------------------------
// EXECUTION LOGIC
// ------------------------------------------------------------------------------
def executeSweep() {
def currentMode = location.mode
def matchFound = false
for (int i = 1; i <= 8; i++) {
if (settings["dcEnable${i}"] && settings["dcMode${i}"] == currentMode) {
logAction("Sweep Match (Device Control ${i}): Executing enforcement for '${currentMode}'.")
enforceDevices(i)
matchFound = true
}
}
if (!matchFound) logAction("Sweep: No Device Control rules found for mode '${currentMode}'.")
}
def executeTransition() {
def ruleIdx = state.pendingRuleNumber
if (location.mode == state.pendingTriggerMode && ruleIdx != null) {
// Condition Gates Check
if (settings["transUseGates${ruleIdx}"]) {
if (settings["transConditionMotion${ruleIdx}"]?.any { it.currentValue("motion") == "active" }) {
logAction("🛑 Aborted: Motion active.")
clearPendingTransition()
return
}
def pSens = settings["transConditionPower${ruleIdx}"]
if (pSens && (pSens.currentValue("power") ?: 0) > (settings["transPowerThreshold${ruleIdx}"] ?: 15)) {
logAction("🛑 Aborted: Power threshold exceeded.")
clearPendingTransition()
return
}
}
def target = state.pendingTargetMode
logAction("Transitioning '${location.mode}' to '${target}'.")
clearPendingTransition()
location.setMode(target)
} else {
logAction("Failsafe: Mode mismatch. Aborting.")
clearPendingTransition()
}
}
def enforceDevices(ruleIdx) {
def logMsg = "State Sweep -> "
if (settings["dcSwitchesOff${ruleIdx}"]) { settings["dcSwitchesOff${ruleIdx}"].off(); logMsg += "[OFF] " }
if (settings["dcSwitchesOn${ruleIdx}"]) { settings["dcSwitchesOn${ruleIdx}"].on(); logMsg += "[ON] " }
if (settings["dcLocksLock${ruleIdx}"]) { settings["dcLocksLock${ruleIdx}"].lock(); logMsg += "[Locked] " }
if (settings["dcGarageClose${ruleIdx}"]) { settings["dcGarageClose${ruleIdx}"].close(); logMsg += "[Closed] " }
// --- Delayed Switches ON Logic ---
if (settings["dcDelayedSwitchesOn${ruleIdx}"] && settings["dcDelayMins${ruleIdx}"] != null) {
def delaySecs = (settings["dcDelayMins${ruleIdx}"] * 60).toInteger()
runIn(delaySecs, "turnOnDelayedSwitches", [data: [ruleIdx: ruleIdx], overwrite: false])
logMsg += "[Delayed ON: ${settings["dcDelayMins${ruleIdx}"]}-min timer started] "
}
// --- Weather Forecast Auto Logic ---
if (settings["dcWeatherSwitch${ruleIdx}"]) {
def weatherDelayMins = settings["dcWeatherDelay${ruleIdx}"] ?: 0
if (weatherDelayMins > 0) {
def delaySecs = (weatherDelayMins * 60).toInteger()
runIn(delaySecs, "turnOnWeatherSwitch", [data: [ruleIdx: ruleIdx], overwrite: false])
logMsg += "[Weather Forecast: Scheduled ON in ${weatherDelayMins} min] "
} else {
settings["dcWeatherSwitch${ruleIdx}"].on()
logMsg += "[Weather Forecast Switch ON] "
runIn(300, "turnOffWeatherSwitch", [data: [ruleIdx: ruleIdx], overwrite: false])
}
}
// --- Audio Announcements ---
def ruleTtsSpeakers = settings["dcTtsSpeakers${ruleIdx}"]
def ttsMsg = settings["dcTtsMessage${ruleIdx}"]
def ttsBlocker = settings["dcTtsBlocker${ruleIdx}"]
def ruleZoozChimes = settings["dcZoozChimes${ruleIdx}"]
def chimeSound = settings["dcZoozSound${ruleIdx}"]
if (ttsMsg && ruleTtsSpeakers) {
if (ttsBlocker && ttsBlocker.currentValue("switch") == "on") {
logMsg += "[TTS: Blocked by '${ttsBlocker.displayName}'] "
} else {
ruleTtsSpeakers.speak(ttsMsg)
logMsg += "[TTS: ${ttsMsg}] "
}
}
// Delayed execution for Zooz Chimes
if (chimeSound != null && ruleZoozChimes) {
runInMillis(2000, "playDelayedZoozChimes", [data: [ruleIdx: ruleIdx]])
logMsg += "[Zooz Chime: Scheduled File ${chimeSound} (Delayed 2s)] "
}
// Process Inovelli LED color changes
if (settings["dcInovelli${ruleIdx}"] && settings["dcInovelliColor${ruleIdx}"] != null) {
def blocker = settings["dcInovelliBlocker${ruleIdx}"]
// Check if the user defined a blocker switch and if it is currently ON
if (blocker && blocker.currentValue("switch") == "on") {
logMsg += "[Inovelli LEDs: Blocked by '${blocker.displayName}'] "
} else {
// Safe to change colors
enforceInovelliLEDs(ruleIdx)
def colorMap = ["0":"Red", "14":"Orange", "35":"Lemon", "64":"Lime", "85":"Green", "106":"Teal", "127":"Cyan", "149":"Aqua", "170":"Blue", "191":"Violet", "212":"Magenta", "234":"Pink", "255":"White"]
def colorName = colorMap[settings["dcInovelliColor${ruleIdx}"]] ?: settings["dcInovelliColor${ruleIdx}"]
def level = settings["dcInovelliLevel${ruleIdx}"] ?: 100
def effect = settings["dcInovelliEffect${ruleIdx}"] ?: "1"
def targetStr = settings["dcInovelliTarget${ruleIdx}"] == "All" ? "All LEDs" : "LED ${settings["dcInovelliTarget${ruleIdx}"]}"
logMsg += "[Inovelli: ${targetStr} ${colorName} at ${level}% (Effect: ${effect})] "
}
}
if (logMsg != "State Sweep -> ") logAction(logMsg)
}
// Helper function to process native driver commands for Inovelli LEDs
def enforceInovelliLEDs(ruleIdx) {
def rawColor = settings["dcInovelliColor${ruleIdx}"]
def rawLevel = settings["dcInovelliLevel${ruleIdx}"]
def rawEffect = settings["dcInovelliEffect${ruleIdx}"]
// Fix: Explicitly check for null instead of using Elvis operator (?:)
// because Groovy treats 0 (Red or Effect Off) as a "false" value.
def colorVal = rawColor != null ? rawColor.toInteger() : 170
def levelVal = rawLevel != null ? rawLevel.toInteger() : 100
def effectVal = rawEffect != null ? rawEffect.toInteger() : 1
def targetVal = settings["dcInovelliTarget${ruleIdx}"] ?: "All"
def durationVal = 255 // 255 = Indefinite duration
settings["dcInovelli${ruleIdx}"].each { dev ->
// 1. Update the base "Idle" colors and intensities using standard setParameter
// The driver's setParameter method handles size natively
dev.setParameter(95, colorVal) // LED Color (When On)
dev.setParameter(96, colorVal) // LED Color (When Off)
dev.setParameter(97, levelVal) // LED Intensity (When On)
dev.setParameter(98, levelVal) // LED Intensity (When Off)
// 2. Trigger the active Notification Effect using the driver's native custom commands
if (targetVal == "All") {
if (dev.hasCommand("ledEffectAll")) {
dev.ledEffectAll(effectVal, colorVal, levelVal, durationVal)
} else {
log.warn "Advanced Mode Manager: Device ${dev.displayName} does not support ledEffectAll command."
}
} else {
// Target is a specific LED (1-7)
if (dev.hasCommand("ledEffectOne")) {
dev.ledEffectOne(targetVal, effectVal, colorVal, levelVal, durationVal)
} else {
log.warn "Advanced Mode Manager: Device ${dev.displayName} does not support ledEffectOne command."
}
}
}
}
// Helper function to routinely refresh the colors and effects
def refreshInovelliColor() {
def currentMode = location.mode
def refreshCount = 0
for (int i = 1; i <= 8; i++) {
if (settings["dcEnable${i}"] && settings["dcMode${i}"] == currentMode) {
if (settings["dcInovelli${i}"] && settings["dcInovelliColor${i}"] != null) {
def blocker = settings["dcInovelliBlocker${i}"]
// Ensure no active blocker before reissuing the color/effect
if (!blocker || blocker.currentValue("switch") != "on") {
enforceInovelliLEDs(i)
refreshCount++
}
}
}
}
if (refreshCount > 0) {
logAction("30-Min Refresh: Reissued LED settings for current mode '${currentMode}'.")
}
}
// ------------------------------------------------------------------------------
// SCHEDULED LED TIMERS
// ------------------------------------------------------------------------------
def executeLedTimer1() {
if (!settings["ledDimEnable"] || !settings["ledDimSwitches"]) return
def level = settings["ledLevel1"] != null ? settings["ledLevel1"].toInteger() : 50
logAction("Scheduled LED Timer 1 triggered. Setting Inovelli LEDs to ${level}%.")
settings["ledDimSwitches"].each { dev ->
dev.setParameter(97, level) // LED Intensity (When On)
dev.setParameter(98, level) // LED Intensity (When Off)
}
}
def executeLedTimer2() {
if (!settings["ledDimEnable"] || !settings["ledDimSwitches"]) return
def level = settings["ledLevel2"] != null ? settings["ledLevel2"].toInteger() : 100
logAction("Scheduled LED Timer 2 triggered. Setting Inovelli LEDs to ${level}%.")
settings["ledDimSwitches"].each { dev ->
dev.setParameter(97, level) // LED Intensity (When On)
dev.setParameter(98, level) // LED Intensity (When Off)
}
}
// ------------------------------------------------------------------------------
// UTILITY FUNCTIONS
// ------------------------------------------------------------------------------
// FIX APPLIED: New safe play function handling playSound, playTrack, and chime with mesh protection
def playZoozSound(devices, sound) {
if (!devices || sound == null) return
def soundInt = sound.toInteger()
def devList = devices instanceof List ? devices : [devices]
devList.eachWithIndex { dev, index ->
if (index > 0) {
pauseExecution(1000)
}
try {
if (dev.hasCommand("playSound")) {
dev.playSound(soundInt)
} else if (dev.hasCommand("playTrack")) {
dev.playTrack(sound.toString())
} else if (dev.hasCommand("chime")) {
dev.chime(soundInt)
} else {
log.warn "Advanced Mode Manager: Device ${dev.displayName} does not support standard sound commands (playSound, playTrack, or chime)."
}
} catch (e) {
log.error "Failed to play audio on ${dev.displayName}: ${e}"
}
}
}
def turnOnDelayedSwitches(data) {
def ruleIdx = data?.ruleIdx
if (ruleIdx && settings["dcDelayedSwitchesOn${ruleIdx}"]) {
settings["dcDelayedSwitchesOn${ruleIdx}"].on()
logAction("Delayed Switches for Rule ${ruleIdx} turned ON after scheduled delay.")
}
}
def turnOnWeatherSwitch(data) {
def ruleIdx = data?.ruleIdx
if (ruleIdx && settings["dcWeatherSwitch${ruleIdx}"]) {
settings["dcWeatherSwitch${ruleIdx}"].on()
logAction("Weather Forecast Switch for Rule ${ruleIdx} turned ON after delay. Scheduling 5-minute auto-off.")
runIn(300, "turnOffWeatherSwitch", [data: [ruleIdx: ruleIdx], overwrite: false])
}
}
def turnOffWeatherSwitch(data) {
def ruleIdx = data?.ruleIdx
if (ruleIdx && settings["dcWeatherSwitch${ruleIdx}"]) {
settings["dcWeatherSwitch${ruleIdx}"].off()
logAction("Weather Forecast Switch for Rule ${ruleIdx} turned OFF automatically after 5 minutes.")
}
}
// FIX APPLIED: Routing delayed mesh execution through safe Zooz Helper
def playDelayedZoozChimes(data) {
def ruleIdx = data?.ruleIdx
if (ruleIdx && settings["dcZoozChimes${ruleIdx}"]) {
def sound = settings["dcZoozSound${ruleIdx}"]
playZoozSound(settings["dcZoozChimes${ruleIdx}"], sound)
}
}
def clearPendingTransition() {
unschedule("executeTransition")
state.pendingTargetMode = null
state.pendingTriggerMode = null
state.pendingTargetTime = null
state.pendingRuleNumber = null
}
def logAction(msg) {
if(txtEnable) log.info "${app.label}: ${msg}"
def h = state.actionHistory ?: []
h.add(0, "[${new Date().format("MM/dd hh:mm a", location.timeZone)}] ${msg}")
if (h.size() > 30) h = h[0..29]
state.actionHistory = h
}
def logInfo(msg) { if(txtEnable) log.info "${app.label}: ${msg}" }