/**
* 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 += "| Zone Name | Lights | Action / Last Trigger | Time Left |
"
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 += ""
roiHTML += "| Zone Name | Today's Savings | Lifetime Savings |
"
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 += "| ${z.name} | "
tableHTML += "${z.light} | "
def triggerText = z.lastTrigger ? "
Triggered by: ${z.lastTrigger}" : ""
tableHTML += "${z.status}${triggerText} | "
tableHTML += "${z.timer} | "
tableHTML += "
"
renderedCount++
// Process ROI Data
if (z.roi) {
hasROI = true
def roiRowBg = (roiRenderedCount % 2 == 0) ? "#ffffff" : "#f9f9f9"
roiHTML += ""
roiHTML += "| ${z.name} | "
roiHTML += "$${z.roi.today} | "
roiHTML += "$${z.roi.lifetime} | "
roiHTML += "
"
totalToday += z.roi.today.toBigDecimal()
totalLifetime += z.roi.lifetime.toBigDecimal()
roiRenderedCount++
}
}
} catch (e) { log.debug "Dashboard error: ${e.message}" }
}
tableHTML += "
"
paragraph tableHTML
if (enableGlobalHealth && healthData) {
def hTable = "Sensor Health & Battery Watchdog
"
hTable += ""
hTable += "| Device Name | Battery | Last Activity |
"
healthData.unique{ it.name }.each { h ->
def bColor = (h.battery && h.battery.toInteger() < 25) ? "red" : "black"
hTable += "| ${h.name} | ${h.battery ?: '--'}% | ${h.lastActivity} |
"
}
hTable += "
"
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}"
}
}
}
}