/*********************************************************************************************************************** * * A SmartThings child smartapp which creates the "room" device using the rooms occupancy DTH and allows executing * various rules based on occupancy state. this alllows lights and other devices to be turned on and off based on * occupancy. it also allows many other actions like executing a routine or piston, turning on/off music and much * more. see the wiki for more details. (note wiki is still in progress. ok there is really no content in the wiki. * yet. but this is to reinforce my intention of putting the wiki together. ;-) will update with link once in place.) * * Copyright (C) 2018 bangali * * License: * This program is free software: you can redistribute it and/or modify it under the terms of the GNU * General Public License as published by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. * * You should have received a copy of the GNU General Public License along with this program. * If not, see . * * Name: Rooms Vacation * Source: https://github.com/adey/bangali/blob/master/smartapps/bangali/rooms-vacation.src/rooms-vacation.groovy * ***********************************************************************************************************************/ public static String version() { return "v1.2.0" } boolean isDebug() { return false } definition ( name: "rooms vacation", namespace: "bangali", parent: "bangali:rooms manager", author: "bangali", description: "DO NOT INSTALL DIRECTLY. Rooms manager will create a new vacation manager app from this code.", category: "My Apps", iconUrl: "https://cdn.rawgit.com/adey/bangali/master/resources/icons/roomOccupancy.png", iconX2Url: "https://cdn.rawgit.com/adey/bangali/master/resources/icons/roomOccupancy@2x.png", iconX3Url: "https://cdn.rawgit.com/adey/bangali/master/resources/icons/roomOccupancy@3x.png" ) preferences { page(name: "mainPage", content: "mainPage") } def mainPage() { if (state.vacaDisabled == null) state.vacaDisabled = true; app.updateLabel('# ' + app.name + (!state.vacaDisabled ? ' is enabled' : '')) def roomNames = parent.getRoomNames(app.id) def hT = getHubType() dynamicPage(name:"mainPage", title:"Vacation Settings", install:true, uninstall:false, submitOnChange:true) { section { if (hT == 'HE') if (state.vacaDisabled) input "enableVaca", "button", title:"Enable Vacation Mode" else input "disableVaca", "button", title:"Disable Vacation Mode" else input 'vacaState', 'bool', title:'Vacation mode enabled?', required:true input 'vacaRooms', 'enum', title:'Replay state for which rooms?', required:false, multiple:true, options:roomNames input 'vacaStates', 'enum', title:'Replay which states?', required:true, multiple:true, defaultValue:['occupied', 'engaged'], options:['asleep', 'occupied', 'engaged'] input 'replayAsIs', 'bool', title:'Replay as is?', required:true paragraph "VACATION MODE IS STILL WIP. WHILE SETTINGS AND ROOMS STATE DATA FOR THE SELECTED ROOMS WILL BE SAVED, THE APP ITSELF IS NONFUNCTIONAL." } } } private subHeaders(str) { if (str.size() > 50) str = str.substring(0, 50); return "
${str.toUpperCase().center(50)}
" } def installed() { initialize() } def updated() { def nowTime = now() initialize() def hT = getHubType() def pVer = parent.version() def ver = version() if (pVer != ver) { ifDebug("Rooms Vacation app verion does not match Rooms Manager app version. Parent version is $pVer and this app version is ${ver}. Please update app code and save${(hT == _SmartThings ? '/publish' : '')} before trying again.", 'error') return } ifDebug("updated", 'info') if (hT == 'ST') state.vacaDisabled = (vacaState ? false : true); state.vacaRoomDevices = (vacaRooms ? parent.getRoomDevices(vacaRooms) : [:]) if (!state.vacaDisabled) { if (state.rSH) { state.lastRun = now() - 1980000L for (def vRD : state.vacaRoomDevices) for (def i = 1; i <= 7; i++) if (state?.rSH[vRD.key]?."$i") state.rSH[vRD.key]."$i" = state.rSH[vRD.key]."$i".sort{ it.key } replayRecover() runEvery30Minutes(replayRecover) } else log.error "rooms manager: rooms state history not found. vacation mode will not replay." } parent.triggerSubscribeToVaca() log.debug "perf updated: ${now() - nowTime} ms" } def initialize() { ifDebug("initialize", 'info') unsubscribe() unschedule() } def subscribeToRooms() { // if (!state.vacaDisabled) return; unsubscribe() def nowTime = now() def rDOs = parent.getChildRoomOccupancyDeviceObjects() ifDebug("there are ${rDOs.size()} devices.", 'info') log.debug "there are ${rDOs.size()} devices." for (def rD : rDOs) { ifDebug("initialize: room device: ${rD.label} id: ${rD.id}", 'info') subscribe(rD, "occupancy", roomStateHistory) } log.debug "perf subscribeToRooms: ${now() - nowTime} ms" } def unSubscribeToRoom(rD) { unsubscribe(rD) } // since hubitat does not support timeTodayAfter(...) 2018-04-08 private timeTodayA(whichDate, thisDate, timeZone) { def newDate if (thisDate.before(whichDate)) { newDate = thisDate.plus(((whichDate.getTime() - thisDate.getTime()) / 86400000L).intValue() + 1) } else newDate = thisDate return newDate } private getHubType() { if (!state.hubId) state.hubId = location.hubs[0].id.toString(); return (state.hubId.length() > 5 ? 'ST' : 'HE') } def unsubscribeChildRoomDevice(appChildDevice) { ifDebug("unsubcribe: room: ${appChildDevice.label} id: ${appChildDevice.id}", 'info') if (getHubType() == 'HE') ifDebug("Hubitat does not yet support unsubscribing to a single device so removing a room requires a manual step.\n\ From Hubitat portal please go to devices and find the corresponding rooms occupancy device and remove it.\n\ Once the device is removed, from rooms manager app remove the room to complete uninstallation of the room.") else unsubscribe(appChildDevice) } def roomStateHistory(evt) { log.debug "evt.value: $evt.value | evt.device.name: $evt.device.name" if (state.vacaDisabled) return; if (!(['asleep', 'engaged', 'occupied', 'vacant'].contains(evt.value))) return; def nowTime = now() def dNI = evt.device.deviceNetworkId.toString() // state.remove('rSH'); if (state.vacaRoomDevices?.containsKey(dNI)) { def dow = getDoW().toString() def nowDate = new Date(nowTime) int hhMM = ((nowDate.format("HH", location.timeZone).toInteger() * 60) + nowDate.format("mm", location.timeZone).toInteger()) if (!state.rSH) { state.rSH = [:]; ifDebug("rSH initialized") } if (!state.rSH[dNI]) { state.rSH[dNI] = [:]; log.debug "dNI initialized"; } if (!state.rSH[dNI]."$dow") { state.rSH[dNI]."$dow" = [:]; log.debug "dow initialized"; } state.rSH[dNI]."$dow" << [(hhMM.toString()):(evt.value[0..0])] // def deleteTime = (long) ((nowTime - 86400000000) / 1000) // while (state.rSH["$dNI"][0] && state.rSH["$dNI"][0].t < deleteTime) { state.rSH["$dNI"].remove(0); } } else state.rSH.remove(dNI) log.debug "perf roomStateHistory: ${now() - nowTime} ms" } def replayRecover() { def nowTime = now() def prvRSt = null def removeHHmm def stateStoreSize = 0 for (def vRD : state.vacaRoomDevices) { removeHHmm = [] for (def i = 1; i <= 7; i++) { for (def rSt : state.rSH[vRD.key]?."$i") { if (rSt.value == 'v' && !['a', 'e', 'o'].contains(prvRSt)) removeHHmm << [(i.toString()), (rSt.key)]; else stateStoreSize = stateStoreSize + 8 prvRSt = rSt.value } } log.debug removeHHmm for (def rmv : removeHHmm) state.rSH[vRD.key]."${rmv[0]}".remove(rmv[1]); } if (stateStoreSize > 81920) ifDebug('App state size is greater than 80,000 characters, there may be too many rooms selected for vacation replay. Check with @bangali if not sure.', 'warn'); if ((nowTime - state.lastRun) >= 1980000L) if (replayAsIs) runEvery1Minute(replaySchedule); else runEvery30Minutes(replayRandom); log.debug "perf replayRecover: ${now() - nowTime} ms" } def replayRandom() { def nowTime = now() state.lastRun = nowTime def dow = getDoW() def nowDate = new Date(nowTime) for (def vRD : state.vacaRoomDevices) { int hh = nowDate.format("HH", location.timeZone).toInteger() int mm = nowDate.format("mm", location.timeZone).toInteger() def replayList = [] for (def i = 1; i < 30; i++) { int hhMM = ((hh * 60) + mm) if (mm == 59) break; mm = mm + 1 def toReplay = state.rSH[vRD.key]?."$dow"?."$hhMM" if (toReplay && toReplay.value != 'v') replayList << [(toReplay.key):(toReplay.value)] } if (!replayList) continue; log.debug replayList def pickOne = replayList[(new Random().nextInt(replayList.size() - 1) + 1)] def sRSt = pickOne.value if (!sRST) continue; log.debug sRST def roomState = null switch(sRSt) { case 'a': roomState = 'asleep'; break case 'e': roomState = 'engaged'; break case 'o': roomState = 'occupied'; break case 'v': roomState = 'vacant'; break } if (roomState) parent.setRoomState(state.vacaRoomDevices[vRD.key].id); } log.debug "perf replayRandom: ${now() - nowTime} ms" } def replaySchedule() { def nowTime = now() state.lastRun = nowTime def dow = getDoW().toString() def nowDate = new Date(nowTime) int hhMM = ((nowDate.format("HH", location.timeZone).toInteger() * 60) + nowDate.format("mm", location.timeZone).toInteger()) for (def vRD : state.vacaRoomDevices) { if (state.rSH[vRD.key]?."$dow") { def sRSt = state.rSH[vRD.key]."$dow"[(hhMM)] if (!sRST) continue; def roomState = null switch(sRSt) { case 'a': roomState = 'asleep'; break case 'e': roomState = 'engaged'; break case 'o': roomState = 'occupied'; break case 'v': roomState = 'vacant'; break } if (roomState) parent.setRoomState(state.vacaRoomDevices[vRD.key].id); } } log.debug "perf replayRandom: ${now() - nowTime} ms" } private getDoW() { long timestamp = now() return ((new Date(timestamp + location.timeZone.getOffset(timestamp))).day ?: 7) } private ifDebug(msg = null, level = null) { if (msg && (isDebug() || level == 'error')) log."${level ?: 'debug'}" " $app.label: " + msg } def appButtonHandler(btn) { if (btn == 'enableVaca') state.vacaDisabled = false; else if (btn == 'disableVaca') state.vacaDisabled = true; } //--------------------------------------------------------------------------------------------------------------------------------------------------//