/** * Lift Off * Space-X Launch Schedule Integration * Copyright 2021 Justin Leonard * * 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: * v1.1.0 Full feature Beta * v1.1.1 Improved update around launch time * v1.1.2 Fixed issue with inactivity timing * v1.1.3 patch * v1.1.4 Fixed success/failure spacing * v1.1.5 Fixed success/failure bug * v1.1.6 Fixed handling of partial dates * v2.0.0 Switched to new API after old API deprecated */ import java.text.SimpleDateFormat import groovy.transform.Field import groovy.time.TimeCategory metadata { definition(name: "Lift Off", namespace: "lnjustin", author: "lnjustin", importUrl: "") { capability "Configuration" capability "Refresh" capability "Actuator" capability "Switch" attribute "tile", "string" attribute "time", "number" attribute "timeStr", "string" attribute "name", "string" attribute "location", "string" attribute "rocket", "string" attribute "description", "string" attribute "status", "string" attribute "statusDetail", "string" } } preferences { section { // input name: "launchAgencyFilter", type: "text", title: "Name of Launch Agency based on which to Filter Results (default 'SpaceX')", defaultValue: "SpaceX" input name: "clearWhenInactive", type: "bool", title: "Clear Tile When Inactive?", defaultValue: false input name: "hoursInactive", type: "number", title: "Inactivity Threshold (In Hours)", defaultValue: 24 input name: "refreshInterval", type: "number", title: "Refresh Interval (In Minutes) (Mininum 5 mins)", defaultValue: 120 input name: "showName", type: "bool", title: "Show Launch Name on Tile?", defaultValue: false input name: "showLocality", type: "bool", title: "Show Launch Location on Tile?", defaultValue: false // input name: "showRocket", type: "bool", title: "Show Rocket Name on Tile?", defaultValue: false input name: "dashboardType", type: "enum", options: ["Native Hubitat", "Sharptools"], title: "Dashboard Type for Which to Configure Tile", defaultValue: "Native Hubitat" input name: "textColor", type: "text", title: "Tile Text Color (Hex)", defaultValue: "#000000" input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true } } def logDebug(msg) { if (logEnable) { log.debug(msg) } } def getDashboardType() { return (dashboardType != null) ? dashboardType : "Native Hubitat" } def configure() { logDebug("Configuring Lift Off...") state.clear() unschedule() refresh() } def refresh() { setState() updateDisplayedLaunch() unschedule() scheduleUpdate() def refreshSecs = refreshInterval ? refreshInterval * 60 : 120 * 60 runIn(refreshSecs, refresh, [overwrite: false]) } def setState() { // setLatestLaunch() // setNextLaunch() def launches = httpGetExec("launch/upcoming/")?.results def now = new Date() def latest = null def next = null for (launch in launches) { def launchTime = toDateTime(launch.net) if (latest == null && launchTime < now) latest = launch if (launchTime >= now) { next = launch break } } logDebug("Latest Launch: ${latest}") logDebug("Next Launch: ${next}") if (latestLaunch != null) { def launch = latest def launchTime = toDateTime(launch.net) def patch = (launch.mission_patches != null) ? launch.mission_patches[0]?.image_url : null state.latestLaunch = [time: launchTime.getTime() , timeStr: getTimeStr(launchTime), name: launch.mission?.name, provider: launch.launch_service_provider?.name, description: launch.mission?.description, locality: launch.pad?.location.name, rocket: launch.rocket?.configuration?.name + " " + launch.rocket?.configuration?.variant, patch: patch, status: launch.status.abbrev, statusDetail: launch.status.description] } if (next != null) { def launch = next def launchTime = toDateTime(launch.net) def patch = (launch.mission_patches != null) ? launch.mission_patches[0]?.image_url : null state.nextLaunch = [time: launchTime.getTime() , timeStr: getTimeStr(launchTime), name: launch.mission?.name, provider: launch.launch_service_provider?.name, description: launch.mission?.description, locality: launch.pad?.location.name, rocket: launch.rocket?.configuration?.name + " " + launch.rocket?.configuration?.variant, patch: patch, status: launch.status.abbrev, statusDetail: launch.status.description] } else state.nextLaunch = null } def updateDisplayedLaunch() { def launch = getLaunchToDisplay() def switchValue = getSwitchValue() def tile = getTile(launch) updateDevice([launch: launch, switchValue: switchValue, tile: tile]) } def updateDevice(data) { sendEvent(name: "time", value: (data.launch != null) ? data.launch.time : "No Launch Data") sendEvent(name: "timeStr", value: data.launch != null ? data.launch.timeStr : "No Launch Data") sendEvent(name: "name", value: data.launch != null ? data.launch.name : "No Launch Data") sendEvent(name: "location", value: data.launch != null ? data.launch.locality : "No Launch Data") sendEvent(name: "rocket", value: data.launch != null ? data.launch.rocket : "No Launch Data") def description = "" if (data.launch == null) description = "No Launch Data" else if (data.launch.description == null) description = "No Description Available" else description = data.launch.description sendEvent(name: "description", value: description) sendEvent(name: "status", value: data.launch != null ? data.launch.status : "No Launch Data") sendEvent(name: "statusDetail", value: data.launch != null ? data.launch.statusDetail : "No Launch Data") sendEvent(name: "tile", value: data.tile) sendEvent(name: "switch", value: data.switchValue) } def updateLatestLaunchStatus() { if (state.updateAttempts == null) state.updateAttempts = 1 else state.updateAttempts++ def storedStatus = state.latestLaunch.status setState() if (storedStatus == state.latestLaunch.status && state.updateAttempts <= 24) { // Keep checking for update every 10 minutes until max attempts reached runIn(600, updateLatestLaunchStatus) } else if (storedStatus != state.latestLaunch.status) { updateDisplayedLaunch() state.updateAttempts = 0 } else if (storedStatus == state.latestLaunch.status && state.updateAttempts > 24) { // max update attempts reached. Reset for next time and abort update. state.updateAttempts = 0 } } def scheduleUpdate() { Date now = new Date() // update when time to switch to display next launch Date updateAtDate = getDateToSwitchFromLastToNextLaunch() if (updateAtDate != null && now.before(updateAtDate)) runOnce(updateAtDate, updateDisplayedLaunch) // update after next launch if (state.nextLaunch) { def nextLaunchTime = new Date(state.nextLaunch.time) def delayAfterLaunch = null // update launch when API likely to have new data use(TimeCategory ) { delayAfterLaunch = nextLaunchTime + 10.minutes } runOnce(delayAfterLaunch, refresh, [overwrite: false]) } if (state.latestLaunch) { def lastLaunchTime = new Date(state.latestLaunch.time) def secsSinceLaunch = getSecondsBetweenDates(now, lastLaunchTime) if ((state.latestLaunch.status == "" || state.latestLaunch.status == null) && secsSinceLaunch < (3600 * 3)) { // schedule another update to occur in 10 minutes if the launch happened within the past 3 hours but the success/failure status has not yet been updated runIn(600, updateLatestLaunchStatus) } } // schedule update to occur based on inactivity threshold (after latest launch and before next launch) if (state.latestLaunch && hoursInactive) { def lastLaunchTime = new Date(state.latestLaunch.time) Calendar cal = Calendar.getInstance() cal.setTimeZone(location.timeZone) cal.setTime(lastLaunchTime) cal.add(Calendar.HOUR, hoursInactive as Integer) Date inactiveDateTime = cal.time if (now.before(inactiveDateTime)) runOnce(inactiveDateTime, refresh, [overwrite: false]) } if (state.nextLaunch && hoursInactive) { def nextLaunchTime = new Date(state.nextLaunch.time) Calendar cal = Calendar.getInstance() cal.setTimeZone(location.timeZone) cal.setTime(nextLaunchTime) cal.add(Calendar.HOUR, (hoursInactive * -1 as Integer)) Date activeDateTime = cal.time if (now.before(activeDateTime)) runOnce(activeDateTime, refresh, [overwrite: false]) } } def getSwitchValue() { def switchValue = "off" if (state.latestLaunch && isToday(new Date(state.latestLaunch.time))) switchValue = "on" if (state.nextLaunch && isToday(new Date(state.nextLaunch.time))) switchValue = "on" return switchValue } def getTileParameters(launch) { def scalableFont = false def margin = "-25%" def imageWidth = "100%" def dashboard = getDashboardType() def numLines = 1 if (launch.name) numLines++ // if (showRocket) numLines++ if (showLocality) numLines++ if (launch.status != "Go" && launch.status != "TBC" && launch.status != "TBD" && launch.status != null && launch.status != "null") numLines++ if (dashboard == "Sharptools") { scalableFont = true margin = "-10%" if (numLines == 2) imageWidth = "90%" else if (numLines == 3) imageWidth = "70%" else if (numLines == 4) imageWidth = "50%" } else { if (numLines == 2) imageWidth = "90%" else if (numLines == 3) imageWidth = "90%" else if (numLines == 4) imageWidth = "70%" } def color = "" if (textColor != "#000000") color = "color: $textColor" return [scalableFont: scalableFont, imageWidth: imageWidth, margin: margin, color: color] } def getTile(launch) { def tile = "
${launch.name}
" tile += "${launch.timeStr}
" // if (showRocket) tile += "${launch.rocket}
" if (showLocality) tile += "${launch.locality}
" if (launch.status != null && launch.status != "null" && (launch.status.contains("Success") || launch.status.contains("Failure"))) { tile += "