/** * Advanced Motion Lighting (Parent) * * Author: ShaneAllen */ definition( name: "Advanced Motion Lighting", namespace: "ShaneAllen", author: "ShaneAllen", description: "Parent container for Advanced Motion Lighting child applications.", category: "Convenience", iconUrl: "", iconX2Url: "", singleInstance: true ) preferences { page(name: "mainPage") } def mainPage() { dynamicPage(name: "mainPage", title: "Advanced Motion Lighting", install: true, uninstall: true) { section("Global System Dashboard") { if (pauseSystem) { paragraph "
⚠️ GLOBAL SYSTEM PAUSED ⚠️
All lighting rules are currently disabled.
" } input "btnRefresh", "button", title: "🔄 Refresh Dashboard Data Now" def children = getChildApps() if (children) { def tableHTML = "" tableHTML += "" def healthData = [] def renderedCount = 0 // Variables for ROI Table def roiRenderedCount = 0 def totalToday = 0.0 def totalLifetime = 0.0 def hasROI = false def roiHTML = "
Global Energy Savings & ROI
" roiHTML += "
Zone NameLightsAction / Last TriggerTime Left
" roiHTML += "" children.each { child -> try { def z = child.getZoneStatus() if (z) { def lightColor = z.light.contains("ON") ? "green" : "grey" def rowBg = (renderedCount % 2 == 0) ? "#ffffff" : "#f9f9f9" if (z.health) healthData.addAll(z.health) tableHTML += "" tableHTML += "" tableHTML += "" def triggerText = z.lastTrigger ? "
Triggered by: ${z.lastTrigger}" : "" tableHTML += "" tableHTML += "" tableHTML += "" renderedCount++ // Process ROI Data if (z.roi) { hasROI = true def roiRowBg = (roiRenderedCount % 2 == 0) ? "#ffffff" : "#f9f9f9" roiHTML += "" roiHTML += "" roiHTML += "" roiHTML += "" roiHTML += "" totalToday += z.roi.today.toBigDecimal() totalLifetime += z.roi.lifetime.toBigDecimal() roiRenderedCount++ } } } catch (e) { log.debug "Dashboard error: ${e.message}" } } tableHTML += "
Zone NameToday's SavingsLifetime Savings
${z.name}${z.light}${z.status}${triggerText}${z.timer}
${z.name}$${z.roi.today}$${z.roi.lifetime}
" paragraph tableHTML if (enableGlobalHealth && healthData) { def hTable = "
Sensor Health & Battery Watchdog
" hTable += "" hTable += "" healthData.unique{ it.name }.each { h -> def bColor = (h.battery && h.battery.toInteger() < 25) ? "red" : "black" hTable += "" } hTable += "
Device NameBatteryLast Activity
${h.name}${h.battery ?: '--'}%${h.lastActivity}
" paragraph hTable } if (hasROI) { roiHTML += "" roiHTML += "SYSTEM TOTALS:" roiHTML += "$${totalToday.setScale(2, BigDecimal.ROUND_HALF_UP)}" roiHTML += "$${totalLifetime.setScale(2, BigDecimal.ROUND_HALF_UP)}" roiHTML += "" roiHTML += "" paragraph roiHTML } } else { paragraph "No lighting zones created yet." } } section("Master System Control") { input "pauseSystem", "bool", title: "⏸️ Pause System-Wide (All Rules)?", defaultValue: false, submitOnChange: true input "masterEnableSwitch", "capability.switch", title: "Master Disable Switch", required: false input "enableGlobalHealth", "bool", title: "Enable Global Battery & Health Watchdog?", defaultValue: false, submitOnChange: true } section("Global Color Temperature") { input "globalCTVar", "string", title: "Global CT Hub Variable Name (Exact text)", required: false } section("Arrival Lighting Strategy") { input "arrivalMode", "mode", title: "Trigger Mode (e.g., Arrival/Home)", multiple: false, required: false input "arrivalShadesSensor", "capability.contactSensor", title: "All Shades Contact Sensor", required: false input "arrivalOvercastSwitch", "capability.switch", title: "Overcast Virtual Switch", required: false input "arrivalTimeout", "number", title: "Shade Open Timeout (Seconds)", defaultValue: 30 input "arrivalDuration", "number", title: "Keep Arrival Lights On Duration (Minutes)", defaultValue: 10 input "staggerDelay", "number", title: "Stagger Delay Between Zones (ms)", defaultValue: 500 } section("System Maintenance & Recovery") { input "btnGlobalSweep", "button", title: "Execute Global Sweep Now" input "btnClearOverrides", "button", title: "Clear All Manual Overrides" input "btnResetROI", "button", title: "Reset All ROI Telemetry" } section("Lighting Rules") { app(name: "childApps", appName: "Advanced Motion Lighting Child", namespace: "ShaneAllen", title: "Create New Motion Lighting Rule", multiple: true) } } } def installed() { initialize() } def updated() { unschedule(); unsubscribe(); initialize() } def initialize() { if (arrivalMode) subscribe(location, "mode", modeChangeHandler) if (arrivalShadesSensor) subscribe(arrivalShadesSensor, "contact", shadesContactHandler) if (globalCTVar) subscribe(location, "variable.${globalCTVar}", globalCTHandler) } def isSystemPaused() { return pauseSystem == true } def globalCTHandler(evt) { if (isSystemPaused()) return try { def newCT = Math.round(evt.value.toFloat()).toInteger() getChildApps().each { it.dynamicCTUpdate(newCT) } } catch (ex) { log.error "CT Var Error: ${ex.message}" } } def modeChangeHandler(evt) { if (isSystemPaused()) return if (evt.value == arrivalMode) { state.arrivalPending = true runIn(arrivalTimeout ?: 30, "arrivalTimeoutCheck") } } def shadesContactHandler(evt) { if (isSystemPaused()) return if (state.arrivalPending && evt.value == "open") { unschedule("arrivalTimeoutCheck") state.arrivalPending = false if (arrivalOvercastSwitch?.currentValue("switch") == "on") triggerArrivalLights() } } def arrivalTimeoutCheck() { if (isSystemPaused()) return if (state.arrivalPending) { state.arrivalPending = false def shadeState = arrivalShadesSensor?.currentValue("contact") if (shadeState == "closed" || (shadeState == "open" && arrivalOvercastSwitch?.currentValue("switch") == "on")) triggerArrivalLights() } } def triggerArrivalLights() { if (isSystemPaused()) return def children = getChildApps() children.each { child -> if (child.isArrivalEnabled()) { child.turnOnArrival() pauseExecution(staggerDelay ?: 500) } } runIn((arrivalDuration ?: 10) * 60, "revertArrivalLights") } def revertArrivalLights() { if (isSystemPaused()) return getChildApps().each { if (it.isArrivalEnabled()) it.revertFromArrival() } } def appButtonHandler(btn) { if (btn == "btnRefresh") { // Do nothing, framework refreshes naturally } else if (btn == "btnGlobalSweep") { getChildApps().each { child -> try { child.executeParentSweep((new Random().nextInt(4500) + 500).toLong()) } catch (e) { log.error "Failed to sweep ${child.label}: ${e.message}" } } } else if (btn == "btnClearOverrides") { getChildApps().each { child -> try { child.clearManualOverride() } catch (e) { log.error "Failed to clear override on ${child.label}: ${e.message}" } } } else if (btn == "btnResetROI") { getChildApps().each { child -> try { child.resetROI() } catch (e) { log.error "Failed to reset ROI on ${child.label}: ${e.message}" } } } }