/*
* Power Outage Manager
*
* Licensed Virtual the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
* Change History:
*
* Date Who What
* ---- --- ----
* 07Jan2023 thebearmay v0.1.3 - Add trigger device refresh option on system restart
* v0.1.4 - Fix Security Prompt
* 18Jan2023 v0.1.5 - Add presence sensors as trigger
* 21Feb2023 v0.2.0 - Add device on/off capabilities
* Add RM interface
* 05Mar2023 v0.2.1 - Fix typo
* 20Mar2024 v0.2.2 - unschedule pending actions if power is restored
* update Apps List logic to reflect new UI
* 20Mar2024 v0.2.3 - Add PowerMeter with healthStatus offline/online
* v0.2.4 - handle null queue selections
*/
import hubitat.helper.RMUtils
static String version() { return '0.2.4' }
definition (
name: "Power Outage Manager",
namespace: "thebearmay",
author: "Jean P. May, Jr.",
description: "Provides an interface to define actions to take when power goes down and when it is later restored.",
category: "Utility",
importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/powOutMgr.groovy",
installOnOpen: true,
oauth: false,
singleThreaded: false,
iconUrl: "",
iconX2Url: ""
)
preferences {
page name: "mainPage"
page name: "outAction"
page name: "upAction"
}
def installed() {
// log.trace "installed()"
state?.isInstalled = true
initialize()
}
def updated(){
// log.trace "updated()"
if(!state?.isInstalled) { state?.isInstalled = true }
if(debugEnabled) runIn(1800,logsOff)
}
def initialize(){
}
void logsOff(){
app.updateSetting("debugEnable",[value:"false",type:"bool"])
}
def mainPage(){
dynamicPage (name: "mainPage", title: "
Power Outage Manager
v${version()}
", install: true, uninstall: true) {
if (app.getInstallationState() == 'COMPLETE') {
section("Main
"){
input "triggerDevs", "capability.powerSource,capability.presenceSensor,capability.powerMeter", title:"Devices with PowerSource/Presence to act as Triggers", submitOnChange:true, multiple:true
if(triggerDevs != null) {
unsubscribe()
state.onMains=[:]
state.onBattery=[:]
triggerDevs.each{
if(it.hasCapability("PowerSource"))
subscribe(it, "powerSource", "triggerOccurrence")
else if(it.hasCapability("PresenceSensor"))
subscribe(it, "presence", "triggerOccurrence")
else if(it.hasCapability("PowerMeter"))
subscribe(it, "healthStatus", "triggerOccurrence")
}
subscribe(location, "systemStart", "systemStartCheck")
pollDevices()
paragraph "On Mains: ${state.onMains.size()} On Battery: ${state.onBattery.size()}"
} else {
unsubscribe()
state.onMains = [:]
state.onBattery = [:]
}
input "triggerDelay", "number", title:"Number of minutes to delay before taking action", defaultValue:0, width:3, submitOnChange:true
input "agreement", "number", title: "Number of devices that must agree before taking action", defaultValue:1, width:3, submitOnChange:true
if(agreement != null && (triggerDevs == null || agreement.toInteger() > triggerDevs.size())){
paragraph "Agreement count ($agreement) exceeds number of devices (${triggerDevs==null ? 0:triggerDevs.size()}) - trigger will never occur"
state.outage = false
}
input "refreshOnStart", "bool", title: "Refresh Trigger Devices on System Start", width:3, submitOnChange:true
input "notifyDev", "capability.notification", title: "Send notifications to", submitOnChange:true, multiple:true
input "notifyMsgOut", "text", title: "Notification Message - Power Out", defaultValue: "${app.getLabel()} - Power Outage Detected", submitOnChange:true
input "notifyMsgUp", "text", title: "Notification Message - Power Restored", defaultValue: "${app.getLabel()} - Power Restored", submitOnChange:true
href "outAction", title: "Power Outage Actions", required: false, width:6, submitOnChange:true
href "upAction", title: "Power Restored Actions", required: false, width:6, submitOnChange:true
input "debugEnabled", "bool", title: "Turn on Debug Logging", submitOnChange:true
input("security", "bool", title: "Hub Security Enabled", defaultValue: false, submitOnChange: true)
if (security) {
input("username", "text", title: "Hub Security Username", required: false, submitOnChange: true)
input("password", "password", title: "Hub Security Password", required: false, submitOnChange: true)
if(username != null && password != null){
login = getCookie()
if(login.cookie != null)
paragraph "Login successful: ${login.result}\n${login.cookie}"
}
}
if(debugEnabled) runIn(1800,"logsOff")
else unschedule("logsOff")
}
section("Change Application Name
", hideable: true, hidden: true){
input "nameOverride", "text", title: "New Name for Application", multiple: false, required: false, submitOnChange: true, defaultValue: app.getLabel()
if(nameOverride != app.getLabel()) app.updateLabel(nameOverride)
}
}
}
}
def outAction(){
dynamicPage (name: "outAction", title: "Power Outage Actions
v${version()}
", install: false, uninstall: false) {
section(title:"General Information",hideable:true,hidden:true){
paragraph "
Outage actions have 3 Outage Response Queues available. These queues allow for the desired actions to staggered to accommodate an ongoing outage. For example, when the outage is detected you may want to disable integrations that no longer are available. Later you may want to disable other apps, or turn off the radios, and if theoutage goes on long enough, you may want to shutdown the hub gracefully before the UPS runs out of power.
First step is to decide what your checkpoints are (how many minutes before taking each set of actions) and enter those. Then go to the bottom of the screen and assign actions to the queues - if you don't want to take an action noted, set its queue number to zero. If you chose to disable apps, assigning the action to a queue will give you the option to select All apps or individal ones.
"
}
section("Queue Timers
"){
input "oaDelay1", "number", title:"Minutes before executing actions selected for Outage Response Queue 1", submitOnChange:true, width:4
input "oaDelay2", "number", title:"Minutes before executing actions selected for Outage Response Queue 2", submitOnChange:true, width:4
input "oaDelay3", "number", title:"Minutes before executing actions selected for Outage Repsonse Queue 3", submitOnChange:true, width:4
}
section ("Queue Actions
"){
paragraph "Assign each of the below to a Response Queue, items assigned to Queue 0 will not be scheduled"
appsList = [0:"All"]
getAppsList().each{
appsList["$it.id"]=it.title
}
input "zbDisable", "enum", title: "Turn off the ZigBee Radio", options: [0,1,2,3], submitOnChange:true, width:4
input "zwDisable", "enum", title: "Turn off the ZWave Radio", options: [0,1,2,3], submitOnChange:true, width:4
input "appDisable", "enum", title: "Disable Rules/Apps", options: [0,1,2,3], submitOnChange:true, width:4
if(appDisable == "0" || appDisable == null){
app.updateSetting("appDisableList",[value:"",type:"enum"])
}else {
input "appDisableList", "enum", title: "Select Rules/Apps", options: appsList, multiple:true, width:4, submitOnChange:true
}
input "turnOffDevs","enum", title: "Turn off Devices", options: [0,1,2,3], submitOnChange:true, width:4
if(turnOffDevs == "0" || turnOffDevs == null){
app.updateSetting("turnOffDevsList",[value:"-1",type:"capability.switch"])
app.updateSetting("turnOnDevsList1",[value:"-1",type:"capability.switch"])
app.updateSetting("turnOnDevsList2",[value:"-1",type:"capability.switch"])
}else {
input "turnOffDevsList", "capability.switch", title: "Devices to turn off", multiple: true, width:4, submitOnChange:true
}
input "rmRuleO", "enum", title:"Run a RM Rule", options: [0,1,2,3], submitOnChange:true,width:4
if(rmRuleO) {
rList = RMUtils.getRuleList("5.0")
input "rmRuleOList", "enum", title: "Rule(s) to run", options: rList, submitOnChange:true, width:4, multiple:true
}
input "rebootHubO", "enum", title: "Reboot the hub", options: [0,1,2,3], submitOnChange:true, width:4
input "shutdownHub", "enum", title: "Shutdown the hub", options: [0,1,2,3], submitOnChange:true, width:4
}
}
}
def upAction(){
dynamicPage (name: "upAction", title: "Power Restore Actions
v${version()}
", install: false, uninstall: false) {
section(""){
input "zbEnable", "bool", title: "Turn on the ZigBee Radio", submitOnChange:true, width:4
input "zwEnable", "bool", title: "Turn on the ZWave Radio", submitOnChange:true, width:4
input "appEnable", "bool", title: "Enable all Rules/Apps", submitOnChange:true, width:4
input "rebootHub", "bool", title: "Reboot the hub (2 minutes after trigger delay)", submitOnChange:true, width:4
if(turnOffDevs != "0" && turnOffDevs != null){
if(turnOnDevsList1 == null){
app.updateSetting("turnOnDevsList1",[value:turnOffDevsList,type:"capability.switch"])
app.updateSetting("onDelay1",[value:0,type:"number"])
}
if(turnOnDevsList2 == null){
app.updateSetting("turnOnDevsList2",[value:turnOffDevsList,type:"capability.switch"])
app.updateSetting("onDelay2",[value:0,type:"number"])
}
input("onDelay1", "number", title:"Delay in minutes before turning on first set of devices, zero to disable", submitOnChange:true, width:4)
if(onDelay1)
input("turnOnDevsList1", "capability.switch",title:"First set of Devices to Turn On", constraints:turnOffDevsList, multiple:true, submitOnChange:true, width:4)
input("onDelay2", "number", title:"Delay in minutes before turning on second set of devices, zero to disable", submitOnChange:true, width:4)
if(onDelay2)
input("turnOnDevsList2", "capability.switch",title:"Second set of Devices to Turn On", multiple:true, submitOnChange:true, width:4)
}
input "rmRuleR", "number", title:"Delay in minutes before running RM Rule(s), zero to disable", submitOnChange:true,width:4
if(rmRuleR) {
rList = RMUtils.getRuleList("5.0")
input "rmRuleRList", "enum", title: "Rule(s) to run", options: rList, submitOnChange:true, multiple:true
}
}
}
}
void triggerOccurrence(evt){
if(debugEnabled) log.debug "Time: ${evt.unixTime} Device: ${evt.deviceId}:${evt.displayName} Value: ${evt.value}"
if(state.onMains == null) state.onMains = [:]
if(state.onBattery == null) state.onBattery = [:]
if(evt.value.toString().trim() == "battery" || evt.value.toString().trim() == "not present" || evt.value.toString().trim() == "offline") {
state.onBattery["dev${evt.deviceId}"] = true
mainsTemp = [:]
state.onMains.each{
if(it.key != "dev${evt.deviceId}")
mainsTemp[it.key] = state.onMains[it.key]
}
state.onMains = mainsTemp
if(debugEnabled) log.debug "${state.onMains}
${state.onBattery}"
if(state.onBattery.size() >= agreement) startOutActions()
} else if(evt.value.toString().trim() == "mains" || evt.value.toString().trim() == "present"|| evt.value.toString().trim() == "online") {
state.onMains["dev${evt.deviceId}"] = true
batteryTemp = [:]
state.onBattery.each{
if(it.key != "dev${evt.deviceId}")
batteryTemp[it.key] = state.onBattery[it.key]
}
state.onBattery = batteryTemp
if(debugEnabled) log.debug "${state.onMains}
${state.onBattery}"
if(state.onMains.size() >= agreement) startUpActions()
}
}
void systemStartCheck(evt){
unschedule("reboot")
unschedule("shutdown")
if(refreshOnStart){
refreshTriggers()
pauseExecution(3000)
}
pollDevices() // verify the powerSource value in case it changed
if(state.onBattery.size() >= agreement) startOutActions() // should only occur if something triggered a reboot during the outage or an outage occurred during reboot
}
void refreshTriggers(){
triggerDevs.each { dev ->
if(dev.hasCommand("refresh"))
dev.refresh()
}
}
void pollDevices(){
if(state.onMains == null) state.onMains = [:]
if(state.onBattery == null) state.onBattery = [:]
triggerDevs.each { dev ->
if(debugEnabled) log.debug dev.currentValue("powerSource")
if(dev.currentValue("powerSource") == "mains" || dev.currentValue("presence") == "present"|| dev.currentValue.toString().trim() == "online"){
state.onMains["dev${dev.id}"] = true
batteryTemp = [:]
state.onBattery.each{
if(it.key != "dev${dev.id}")
batteryTemp[it.key] = state.onBattery[it.key]
}
state.onBattery = batteryTemp
} else if(dev.currentValue("powerSource") == "battery" || dev.currentValue("presence") == "not present" || dev.currentValue.toString().trim() == "offline"){
state.onBattery["dev${dev.id}"] = true
mainsTemp = [:]
state.onMains.each{
if(debugEnabled) log.debug "${dev.id} ${it.key}"
if(it.key != "dev${dev.id}"){
mainsTemp[it.key] = state.onMains[it.key]
}
}
state.onMains = mainsTemp
}
}
}
void startOutActions(){
if(state.outage == true) return// already processed
state.outage = true
runIn(triggerDelay.toInteger()*60, "startOutage")
}
void startOutage(){
if(!state.outage) return // check if conditions have changed to recovered
notifyDev.each {
it.deviceNotification(notifyMsgOut)
}
delayList = []
if(oaDelay1 == null) oaDelay1 = 0
if(oaDelay2 == null) oaDelay2 = 0
if(oaDelay3 == null) oaDelay3 = 0
delayList[1] = oaDelay1.toInteger()*60
delayList[2] = oaDelay2.toInteger()*60
delayList[3] = oaDelay3.toInteger()*60
if(zbDisable == null) zbDisable = 0
if(zwDisable == null) zwDisable = 0
if(appDisable == null) appDisable = 0
if(turnOffDevs == null) turnOffDevs = 0
if(rebootHubO == null) rebootHubO = 0
if(shutdownHub == null) shutdownHub = 0
if(rmRuleO == null) rmRuleO = 0
if(zbDisable.toInteger() > 0) runIn(delayList[zbDisable.toInteger()], "disableZb")
if(zwDisable.toInteger() > 0) runIn(delayList[zwDisable.toInteger()], "disableZw")
if(appDisable.toInteger() > 0) runIn(delayList[appDisable.toInteger()], "disableApps")
if(turnOffDevs.toInteger() > 0) runIn(delayList[turnOffDevs.toInteger()], "devsOff")
if(rebootHubO.toInteger() > 0) runIn(delayList[rebootHub.toInteger()], "reboot")
if(shutdownHub.toInteger() > 0) runIn(delayList[shutdownHub.toInteger()], "shutdown")
if(rmRuleO.toInteger()) runIn(delayList[rmRuleO.toInteger()], "outageRunRM")
}
void disableZb(){
zbPost("disabled")
}
void disableZw(){
zwPost("disabled")
}
void disableApps(){
appsPost("disable")
}
void devsOff() {
turnOffDevsList.each {
it.off()
}
}
void outageRunRM(){
rmRuleOList.each{
RMUtils.sendAction([it.toInteger()],"runRuleAct", app.getLabel(), "5.0")
}
}
void startUpActions(){
if(state.outage == false) return //already started processing
unschedule() //stop any pending shutdown activity
state.outage = false
runIn(triggerDelay.toInteger()*60, "startRecover")
}
void startRecover(){
if(state.outage) return //check if conditions have changed to outage
notifyDev.each {
it.deviceNotification(notifyMsgUp)
}
if(zbEnabled) zbPost("enabled")
if(zwEnabled) zwPost("enabled")
if(appEnabled) appsPost("enable")
if(rebootHub) runIn(120,"reboot")//allow time for the other actions to complete
if(onDelay1) runIn(onDelay1.toInteger()*60,"devicesOn1")
if(onDelay2) runIn(onDelay2.toInteger()*60,"devicesOn2")
if(rmRuleR) runIn(rmRuleR.toInteger()*60,"restoreRunRM")
}
@SuppressWarnings('unused')
HashMap getCookie(){
def result = false
try{
httpPost(
[
uri: "http://127.0.0.1:8080",
path: "/login",
query:
[
loginRedirect: "/"
],
body:
[
username: username,
password: password,
submit: "Login"
],
textParser: true,
ignoreSSLIssues: true
]
)
{ resp ->
if (resp.data?.text?.contains("The login information you supplied was incorrect."))
result = false
else {
cookie = resp?.headers?.'Set-Cookie'?.split(';')?.getAt(0)
result = true
}
}
}catch (e){
log.error "Error logging in: ${e}"
result = false
cookie = null
}
return [result: result, cookie: cookie]
}
def appButtonHandler(btn) {
switch(btn) {
case "saveAction":
state.saveActions = true
break
default:
log.error "Undefined button $btn pushed"
break
}
}
def zwPost(eOrD){//"enabled"/"disabled"
try{
params = [
uri: "http://127.0.0.1:8080/hub/zwave/update",
headers: [
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
],
body:[zwaveStatus:"$eOrD"],
followRedirects: false
]
if(debugEnabled) log.debug "$params"
httpPost(params){ resp ->
if(debugEnabled) log.debug "$resp.data"
}
}catch (e){
}
}
def zbPost(eOrD){"enabled"/"disabled"
try{
params = [
uri: "http://127.0.0.1:8080/hub/zigbee/update",
headers: [
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
],
body:[zigbeeStatus:"$eOrD"],
followRedirects: false
]
if(debugEnabled) log.debug "$params"
httpPost(params){ resp ->
if(debugEnabled) log.debug "$resp.data"
}
}catch (e){
}
}
void reboot(){
String cookie=(String)null
if(security) cookie = getCookie().cookie
httpPost(
[
uri: "http://127.0.0.1:8080",
path: "/hub/reboot",
headers:[
"Cookie": cookie
]
]
) { resp -> }
}
@SuppressWarnings('unused')
void shutdown() {
String cookie=(String)null
if(security) cookie = getCookie().cookie
httpPost(
[
uri: "http://127.0.0.1:8080",
path: "/hub/shutdown",
headers:[
"Cookie": cookie
]
]
) { resp -> }
}
def appsPost(String eOrD){
if(eOrD == "enable") tOrF = false
else tOrF = true
if(appDisableList.size() < 1)
return
if(appDisableList[0] == 0 || appDisableList[0] == "0")
appList = appDisableList
else
appList = getAppsList().id
appList.each(){
if(it.id != this.getId()){
try{
params = [
uri: "http://127.0.0.1:8080/installedapp/disable",
headers: [
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
],
body:[id:"${it.id}", disable:"$tOrF"], //
followRedirects: true
]
if(debugEnabled) log.debug "$params"
httpPost(params){ resp ->
if(debugEnabled) log.debug "appsPost response: $resp.data"
}
}catch (e){
}
}
}
}
void restoreRunRM(){
rmRuleRList.each{
RMUtils.sendAction([it.toInteger()],"runRuleAct", app.getLabel(), "5.0")
}
}
HashMap [] getAppsList() {
def params = [
uri: "http://127.0.0.1:8080/hub2/appsList",
headers: [
accept:"application/json"
],
textParser: false
]
def allAppsList = []
def allAppNames = []
try {
httpGet(params) { resp ->
resp.data.apps.data.each {
allAppsList.add([id:it.id,title:it.name])
allAppNames.add( it.name )
}
}
} catch (e) {
log.error "Error retrieving installed apps: ${e}"
log.error(getExceptionMessageWithLine(e))
}
return allAppsList
}
void devicesOn1(){
turnOnDevsList1.each{
it.on()
}
}
void devicesOn2(){
turnOnDevsList2.each{
it.on()
}
}