/*
* Hub Failover
*
* 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
* ---- --- ----
* 21Mar2024 thebearmay Update the applist endpoint for the new UI
*/
static String version() { return '1.0.1'}
import java.text.SimpleDateFormat
import java.util.Date
import groovy.json.JsonSlurper
import groovy.json.JsonOutput
import groovy.transform.Field
definition (
name: "Hub Failover",
namespace: "thebearmay",
author: "Jean P. May, Jr.",
description: "Monitors Production Hub heartbeat, and turns on the radios if the production hub does not respond.",
category: "Utility",
importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/heHa/heFailover.groovy",
installOnOpen: true,
oauth: false,
iconUrl: "",
iconX2Url: ""
)
preferences {
page name: "mainPage"
}
mappings {
path("/heartbeat") {
action: [POST: "beatCheck"]
}
path("/shutdown") {
action: [POST: "hubShutdown"]
}
}
void installed() {
if(debugEnabled) log.trace "${app.getLabel()} installed()"
state?.isInstalled = true
initialize()
}
void updated(){
if(debugEnabled) log.trace "${app.getLabel()} updated()"
if(!state?.isInstalled) { state?.isInstalled = true }
if(debugEnabled) runIn(1800,logsOff)
}
void initialize(){
}
void logsOff(){
app.updateSetting("debugEnabled",[value:"false",type:"bool"])
}
def mainPage(){
dynamicPage (name: "mainPage", title: "${app.getLabel()} v${version()}", install: true, uninstall: true) {
if (app.getInstallationState() == 'COMPLETE') {
section("") {
input "debugEnabled", "bool", title:"Enable Debug Logging", submitOnChange:true, required:false, defaultValue:false, width:4
if(debugEnabled) {
unschedule("logsOff")
runIn(1800,logsOff)
}
}
section("Failover Settings", hideable: true, hidden: false){
input "prodHub", "string", title:"IP Address of Production Hub", width:5, submitOnChange:true
input "heartbeatInterval", "number", title: "Hearbeat Interval", width:2, constraints:["NUMBER"], submitOnChange:true, defaultValue:1
input "heartbeatUnit", "enum", title: "Heartbeat Interval Units", options: ["Seconds","Minutes"], width:4, submitOnChange:true, defaultValue:"Minutes"
input "missed", "number", title: "How many check-ins can be missed", constraints: ["NUMBER"], width:2, submitOnChange:true, defaultValue:5
if(location.hub.localIP != prodHub){
state.hubRole = "Monitor"
input "pauseApps", "button", title: "Disable/Enable Apps", width:2
if(state.pauseApps == true){
toggleApps()
state.pauseApps = false
}
paragraph "Apps state is showing: ${state.appToggle}", width:2
input "hbEnabled", "bool", title: "Turn off radios and start heartbeat monitoring", defaultValue:false, submitOnChange: true, width:4
input "monitorOnly", "bool", title: "Leave radios on and monitor heartbeat
(could cause a conflict if Hub Protectâ„¢ restore has been done on this hub)", defaultValue:false, submitOnChange: true, width:4
if(heartbeatUnit.toString() == null) app.updateSetting("hearbeatUnit",[value:"Minutes",type:"enum"])
if(monitorOnly){
if(hbEnabled) {
app.updateSetting("hbEnabled",[value:"false",type:"bool"])
zwPost("enabled")
zbPost("enabled")
}
if(heartbeatUnit == "Minutes") multiplier = 60
else multiplier = 1
state.hbMissed = 0
runIn(heartbeatInterval.toInteger()*multiplier,"heartbeat")
} else if(hbEnabled) {
app.updateSetting("monitorOnly",[value:"false",type:"bool"])
zwPost("disabled")
zbPost("disabled")
if(heartbeatUnit == "Minutes") multiplier = 60
else multiplier = 1
state.hbMissed = 0
runIn(heartbeatInterval.toInteger()*multiplier,"heartbeat")
} else
unschedule("heartbeat")
} else {
state.hubRole = "Source"
unschedule("heartbeat")
}
input "notifyDev", "capability.notification", title: "Device(s) to notify", multiple:true, width:6, submitOnChange:true
}
section("Monitored Hub Security", hideable: true, hidden: true){
if(state.accessToken == null) createAccessToken()
if(state.hubRole == "Source"){
tDefault = state.accessToken
aDefault = getFullLocalApiServerUrl()
}
input "remoteAPI", "text", title:"Source Server API:",submitOnChange:true, defaultValue: aDefault
input "token","text", title:"Source Access Token:",submitOnChange:true, defaultValue: tDefault
input "sourceSecurity", "bool", title: "Source Hub Security Enabled", defaultValue: false, submitOnChange: true, width:4
if (sourceSecurity) {
input("sUsername", "string", title: "Source Hub Security Username", required: false)
input("sPassword", "password", title: "Source Hub Security Password", required: false)
}
}
section("Local Hub Information", hideable: true, hidden: true){
paragraph "Local Server API: ${getFullLocalApiServerUrl()}"
paragraph "Cloud Server API: ${getFullApiServerUrl()}"
if(state.accessToken == null) createAccessToken()
paragraph "Access Token: ${state.accessToken}"
input "resetToken", "button", title:"Reset Token"
}
} else {
section("") {
paragraph title: "Click Done", "Please click Done to install app before continuing"
}
}
}
}
def zwPost(eOrD){
try{
params = [
uri: "http://127.0.0.1:8080/hub/zwave/update",
//uri: "http://127.0.0.1:8080/hub/zigbee/update",
headers: [
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
],
body:[zwaveStatus:"$eOrD"],//[zigbeeStatus:"disabled"], //
followRedirects: false
]
if(debugEnabled) log.debug "$params"
httpPost(params){ resp ->
//if(debugEnabled) log.debug "$resp.data"
}
}catch (e){
}
}
def zbPost(eOrD){
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){
}
}
def heartbeat(){
jsonText = JsonOutput.toJson([heartbeat:"${location.hub.localIP}"])
Map requestParams =
[
uri: "$remoteAPI/heartbeat?access_token=$token",
requestContentType: 'application/json',
contentType: 'application/json',
body: "$jsonText"
]
if(debug.enabled) log.debug "HB: $requestParams"
httpPost(requestParams) { resp ->
try {
if(debugEnabled)
log.debug "$resp.properties ${resp.getStatus()}"
if(resp.getStatus() == 200 || resp.getStatus() == 207){
if(resp.data) {
respMap = (HashMap) resp.data
state.zwave = respMap.zwave
state.zigbee = respMap.zigbee
state.alive = respMap.alive
state.hbMissed = 0
} else {
state.hbMissed = state.hbMissed.toInteger + 1
state.alive = "unknown"
state.zigbee = "unknown"
state.zwave = "unknown"
sendNotice("$prodHub missed heartbeat check, count = $state.missed")
}
}
} catch (Exception ex) {
log.error "$ex
$ex.getResponse()"
}
}
if(state.hbMissed.toInteger() > missed.toInteger() && !monitorOnly){
zwPost("enabled")
zbPost("enabled")
if(state.appToggle == "disabled")
toggleApps()
sendNotice("Hub Failover for $prodHub is ACTIVE")
return
}
if(hbEnabled || monitorOnly) {
if(heartbeatUnit == "Minutes") multiplier = 60
else multiplier = 1
runIn(heartbeatInterval.toInteger()*multiplier,"heartbeat")
} else
unschedule("heartbeat")
}
def sendNotice(msg){
notifyDev.each {
if(debugEnable) log.debug "Sending notification to $it, text: $msg"
it.deviceNotification("$msg")
}
}
def beatCheck(){
if (debugEnabled) log.debug "beatCheck()"
Map params =
[
uri : "http://${location.hub.localIP}:8080",
path : "/hub2/hubData"
]
httpGet(params) { resp ->
try{
if(debugEnabled) log.debug resp.data
h2Data = (Map) resp.data
if(h2Data.baseModel.zwaveStatus == "false")
state.hbZwStatus="enabled"
else
state.hbZwStatus="disabled"
if(h2Data.baseModel.zigbeeStatus == "false")
state.hbZbStatus="enabled"
else
state.hbZbStatus="disabled"
} catch (e) {
log.error e
}
}
jsonText = JsonOutput.toJson([alive:true, zwave:"${state.hbZwStatus}", zigbee:"${state.hbZbStatus}"] )
if(debugEnabled) log.debug "rendering $jsonText"
render contentType:'application/json', data: "$jsonText", status:200
}
def toggleApps(){
getAppsList()
state.appsList.each{
if(it.id != app.id){
if(state.appToggle == "disabled"){
appsPost("enable", "${it.id}")
if(debugEnabled) log.debug "enable, $it.id"
}else{
appsPost("disable", "${it.id}")
if(debugEnabled) log.debug "disable, $it.id"
}
}
}
if(state.appToggle == "disabled") state.appToggle = "enabled"
else state.appToggle = "disabled"
}
def appsPost(String eOrD, String aId){
if(eOrD == "enable") tOrF = false
else tOrF = true
try{
params = [
uri: "http://127.0.0.1:8080/installedapp/disable",
headers: [
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json"
],
body:[id:"$aId", disable:"$tOrF"], //
followRedirects: true
]
if(debugEnabled) log.debug "$params"
httpPost(params){ resp ->
if(debugEnabled) log.debug "appsPost response: $resp.data"
}
}catch (e){
}
}
def getAppsList() {
// if (security)
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))
}
state.appsList = allAppsList.sort { a, b -> a.title <=> b.title }
}
def hubShutdown(){
log.info "Hub Reboot requested"
String cookie=(String)null
if(sourceSecurity) cookie = getCookie()
httpPost(
[
uri: "http://${location.hub.localIP}:8080",
path: "/hub/shutdown",
headers:[
"Cookie": cookie
]
]
) { resp -> }
}
@SuppressWarnings('unused')
String getCookie(){
try{
httpPost(
[
uri: "http://127.0.0.1:8080",
path: "/login",
query: [ loginRedirect: "/" ],
body: [
username: username,
password: password,
submit: "Login"
]
]
) { resp ->
cookie = ((List)((String)resp?.headers?.'Set-Cookie')?.split(';'))?.getAt(0)
if(debugEnable)
log.debug "$cookie"
}
} catch (e){
cookie = ""
}
return "$cookie"
}
def appButtonHandler(btn) {
switch(btn) {
case "pauseApps":
state.pauseApps = true
break
case "resetToken":
createAccessToken()
break
default:
log.error "Undefined button $btn pushed"
break
}
}