/* * Govee Effects Player * * * 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, WIyTHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Date Who Description * ---------- ----------- --------------------------------------------------------- * 18Dec2024 thebearmay Add overRide processing, check for stop during effect list processing, kill switch * 19Dec2024 Add debug logging, additional kill switch checking * 20Dec2024 UI Cleanup * */ import java.time.* import java.time.format.DateTimeFormatter static String version() { return '0.0.8' } import groovy.transform.Field definition ( name: "Govee Effect Player", namespace: "thebearmay", author: "Jean P. May, Jr.", description: "Play back Govee Effects", category: "Utility", importUrl: "https://raw.githubusercontent.com/thebearmay/hubitat/main/apps/gEffects.groovy", installOnOpen: true, oauth: false, iconUrl: "", iconX2Url: "" ) preferences { page name: "mainPage" } mappings { } def installed() { // log.trace "installed()" state?.isInstalled = true initialize() } def updated(){ // log.trace "updated()" if(!state?.isInstalled) { state?.isInstalled = true } if(debugEnable) runIn(1800,logsOff) } def initialize(){ } void logsOff(){ app.updateSetting("debugEnable",[value:"false",type:"bool"]) } def mainPage(){ dynamicPage (name: "mainPage", title: "", install: true, uninstall: true) { if (app.getInstallationState() == 'COMPLETE') { section(name:"Effect_Files",title:"Select Effect List", hideable: false, hidden: false){ fileList = getFiles() effList = [] fileList.each { if("$it".contains('.txt')) effList.add("$it") } input("effFile","enum", title:"Name of Local File for Effects", options:effList, width: 4, submitOnChange:true) } section(name:"Effect_Specifics", title:"Effect Specifics"){ input("minEff","number",title:"Default Effect Time (Minutes)", width:4, submitOnChange: true, defaultValue: 5) input("devList","capability.*", title:"Lights to Send Effects to", width: 4, submitOnChange:true, multiple:true) } section(name:"timeBased", title:"Time Based Effects"){ input("startDate","date",title:"Start Date", width: 4, submitOnChange:true) input("startTime","time",title:"Start Time", width: 4, submitOnChange:true) paragraph " " input("endDate","date",title:"End Date", width: 4, submitOnChange:true) input("endTime","time",title:"End Time", width: 4, submitOnChange:true) paragraph " " input("goBtn","button",title:"Start Timer", width: 4, submitOnChange:true) if(endTime < startTime) paragraph "End Time less than Start Time" if(endDate < startDate) paragraph "End Date less than Start Date" if(state.goBtn){ state.goBtn = false runEffectList() } } section(name:"switchBased", title:"Switch Based Effects"){ input("overRide","capability.switch", title:"Time Overide Switch", width:4, submitOnChange:true) if(overRide) subscribe(overRide,"switch","overRideEffectRun") else unsubscribe() } section("Settings", 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) input("killSw", "bool",title:"Stop/Block Effects from Running",width:4,submitOnChange:true) input("debugEnable", "bool",title:"Debug",width:4,submitOnChange:true) } } else { section("") { paragraph title: "Click Done", "Please click Done to install app before continuing" } } } } void setNextRun(){ unschedule() if(LocalDate.parse(endDate) < LocalDate.now()) return if(LocalDate.parse(startDate) <= LocalDate.now()){ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXX") sTime = LocalTime.parse(startTime, formatter) if(sTime > LocalTime.now()) tDate = LocalDate.now() else tDate = LocalDate.now().plusDays(1) if(debugEnable) log.debug "${tDate.getYear()} ${tDate.getMonthValue()} ${new Date(tDate.getYear()-1900,tDate.getMonthValue()-1,tDate.getDayOfMonth(), sTime.getHour(), sTime.getMinute(), 0)}" runOnce(new Date(tDate.getYear()-1900,tDate.getMonthValue()-1,tDate.getDayOfMonth(), sTime.getHour(), sTime.getMinute(), 0), "runEffectList") } else { tDate = LocalDate.ofEpochDay(startDate.toLong()) if(debugEnable) log.debug "${tDate.getYear()} ${tDate.getMonthValue()}
${new Date(tDate.getYear()-1900,tDate.getMonthValue()-1,tDate.getDayOfMonth(), sTime.getHour(), sTime.getMinute(), 0)}" runOnce(new Date(tDate.getYear()-1900,tDate.getMonthValue()-1,tDate.getDayOfMonth(), sTime.getHour(), sTime.getMinute(), 0), "runEffectList") } } void runEffectList(){ if (!startTime || !endTime || !startDate || !endDate){ log.error "Missing date or time information" return } if(killSw == null) app.updateSetting('killSw',[type:'bool',value:false]) DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXX") sTime = LocalTime.parse(startTime, formatter) if(LocalDate.parse(startDate) > LocalDate.now() || LocalTime.now() < sTime){ setNextRun() return } if((overRide && overRide.currentValue("switch",true) == 'on') || app.getSetting('killSw')==true) { if(debugEnable) log.debug " $overRide:${overRide.currentValue("switch")} kill:$killSw" return } fileRecords = (new String (downloadHubFile("${effFile}"))).split("\n") eTime = LocalTime.parse(endTime, formatter) while (LocalTime.now() < eTime && app.getSetting('killSw')==false){ fileRecords.each { if(debugEnable) log.debug "time:${LocalTime.now() < eTime } kill:${app.getSetting('killSw')}" if(LocalTime.now() < eTime && app.getSetting('killSw')==false) { flds = it.split(":") devList.each { if(app.getSetting('killSw')==false){ if(debugEnable) log.debug "set effect ${flds[0]} on ${it.displayName}" it.setEffect(flds[0]) } } if(flds.size() > 2){ if(debugEnable) log.debug "pausing ${flds[2].toInteger()} minutes" pauseExecution(flds[2].toInteger() * 60 * 1000) } else { if(!minEff) minEff = 5 if(debugEnable) log.debug "pausing ${flds[2].toInteger()} minutes" pause(minEff * 60 * 1000) } } } } setNextRun() } void overRideEffectRun(evt){ if(killSw == null) app.updateSetting('killSw',[type:'bool',value:false]) fileRecords = (new String (downloadHubFile("${effFile}"))).split("\n") while (overRide.currentValue("switch", true) == 'on' && app.getSetting('killSw')==false){ fileRecords.each { if(debugEnable) log.debug "overRide:${overRide.currentValue("switch", true)} kill:${app.getSetting('killSw')}" if(overRide.currentValue("switch", true) == 'on' && app.getSetting('killSw')==false) { flds = it.split(":") devList.each { if(app.getSetting('killSw')==false){ if(debugEnable) log.debug "set effect ${flds[0]} on ${it.displayName}" it.setEffect(flds[0]) } } if(flds.size() > 2){ if(debugEnable) log.debug "pausing ${flds[2].toInteger()} minutes" pauseExecution(flds[2].toInteger() * 60 * 1000) } else { if(!minEff) minEff = 5 if(debugEnable) log.debug "pausing ${flds[2].toInteger()} minutes" pause(minEff * 60 * 1000) } } } } } ArrayList getFiles(){ fileList =[] params = [ uri : "http://127.0.0.1:8080", path : "/hub/fileManager/json", headers: [ accept : "application/json" ], ] httpGet(params) { resp -> resp.data.files.each { fileList.add(it.name) } } return fileList.sort() } String toCamelCase(init) { if (init == null) return null; init = init.replaceAll("[^a-zA-Z0-9]+","") String ret = "" List word = init.split(" ") if(word.size == 1) return init word.each{ ret+=Character.toUpperCase(it.charAt(0)) ret+=it.substring(1).toLowerCase() } ret="${Character.toLowerCase(ret.charAt(0))}${ret.substring(1)}" if(debugEnable) log.debug "toCamelCase return $ret" return ret; } def appButtonHandler(btn) { switch(btn) { case "goBtn": state.goBtn = true break default: log.error "Undefined button $btn pushed" break } }