/* * Meteorological & Astronomical Seasons * * 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 * * Change History: * * Date Source Version What URL * ---- ------ ------- ---- --- * 2021-09-30 jshimota 0.1.0 Starting version * ... ... ... ... * 2026-05-04 Gemini 0.3.0 Added Astronomical Seasons & Dual-Tracking * 2026-05-12 jshimota 0.3.1 Cleanup of preferences info * 2026-05-12 jshimota 0.3.2 Typo fixed. * 2026-05-12 jshimota 0.3.3 Trying to fix HPM stuck * 2026-05-14 jshimota 0.3.4 Fix to Package Json required true * 2026-07-23 jshimota 0.3.5 review and update to Iniitialize issue * 2026-07-23 jshimota 0.3.6 Improved logging and cut back on sendEvents * 2026-07-23 jshimota 0.3.7 Updated icon path preference and default URL * */ import java.text.SimpleDateFormat static String version() { return '0.3.7' } metadata { definition( name: "Astronomical & Meteorological Seasons", namespace: "jshimota", author: "James Shimota", importUrl: "https://raw.githubusercontent.com/jshimota01/hubitat/main/Drivers/astro_meteor_seasons/astroMeteorSeasons.groovy" ) { capability "Actuator" capability "Refresh" capability "Initialize" command "updateSeason" command "autumn" command "fall" command "winter" command "spring" command "summer" attribute "seasonName", "string" attribute "meteorologicalSeason", "string" attribute "astronomicalSeason", "string" attribute "seasonNum", "number" attribute "seasonBegin", "string" attribute "seasonEnd", "string" attribute "seasonTile", "string" attribute "seasonImg", "string" attribute "todaysFormattedDate", "string" attribute "hemisphereName", "string" attribute "autumnFallName", "string" } } preferences { input name: "primaryDisplay", type: "enum", title: "Primary Dashboard Display", options: ["Meteorological", "Astronomical"], defaultValue: "Meteorological", description:\ "Select Season Type: Astronomical which is determined by the Earth's position in its orbit relative to the Sun - or - Meteorological, which groups the year into four equal, three-month static blocks" input name: "iconPathOvr", type: "string", title: "Override path to season icons", defaultValue: "https://raw.githubusercontent.com/jshimota01/hubitat/main/Icons-Images/season_icons/", description:\ "Use this if you have a different set of images. Should end with /. eg; http://192.168.1.12/myicons/seasons_icons/" input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false, description:\ "Enable for problem solving - recommended OFF" input name: "txtEnable", type: "bool", title: "Enable Info logging", defaultValue: true, description:\ "Enable for general log entries - recommended ON" input name: "fontSize", type: "number", title: "HTML Tile Font Size (%)", defaultValue: 100, description:\ "Adjust the text height of tile output by percentage" input name: "vertPos", type: "number", title: "HTML Tile Word Position (%)", defaultValue: 55, description:\ "Adjust the text position relative to top of tile by percentage" input name: "fontColor", type: "string", title: "HTML Tile Text Color (Hex)", defaultValue: "#ffffffff", description:\ "Adjust the text color. Can be color name or hex value -eg; #ffffff or white. 8 char hex is supported for opacity!" input name: "autoUpdate", type: "bool", title: "Enable automatic update at 6am", defaultValue: true, description:\ "On (Recommended) to enable automatic update. Disable for testing or to have tile static for other purposes" input name: "isNorthern", type: "bool", title: "Hemisphere (On=Northern / Off=Southern)", defaultValue: true, description:\ "For our friends in the south - Seasons are reversed, and this accounts for their needs." input name: "useAutumn", type: "bool", title: "Season Naming (On=Autumn / Off=Fall)", defaultValue: false, description:\ "Choose which word to use for the 3rd season, default is Fall" } def installed() { refresh() } def updated() { logInfo "Settings updated." if (logEnable) runIn(1800, logsOff) unschedule() if (autoUpdate) schedule("0 0 6 * * ?", refresh) refresh() } def logsOff() { device.updateSetting("logEnable", [value: "false", type: "bool"]) } def refresh() { updateSeason() } def initialize() { unschedule() if (autoUpdate) schedule("0 0 6 * * ?", refresh) refresh() } // Shortcut commands for manual override def fall() { setSeasonManual("Fall") } def autumn() { setSeasonManual("Autumn") } def winter() { setSeasonManual("Winter") } def spring() { setSeasonManual("Spring") } def summer() { setSeasonManual("Summer") } def setSeasonManual(String sName) { logInfo "Manual override: Setting season to ${sName}" processSeasonUpdate(sName, sName, sName) // Overrides both to the manual selection } def updateSeason() { Date now = new Date() String dateStr = new SimpleDateFormat("dd-MM-yyyy").format(now) int mNum = new SimpleDateFormat("MM").format(now).toInteger() int dNum = new SimpleDateFormat("dd").format(now).toInteger() logDebug "Calculating season for Date: ${dateStr} (Month: ${mNum}, Day: ${dNum})" sendIfChanged(name: "todaysFormattedDate", value: dateStr) String hName = isNorthern ? "Northern" : "Southern" String afName = useAutumn ? "Autumn" : "Fall" sendIfChanged(name: "hemisphereName", value: hName) sendIfChanged(name: "autumnFallName", value: afName) String meteoSeason = "" String astroSeason = "" // 1. Logic Map for Meteorological Seasons (Strict Months) if (isNorthern) { if (mNum in 9..11) meteoSeason = afName else if (mNum == 12 || mNum in 1..2) meteoSeason = "Winter" else if (mNum in 3..5) meteoSeason = "Spring" else meteoSeason = "Summer" } else { if (mNum in 3..5) meteoSeason = afName else if (mNum in 6..8) meteoSeason = "Winter" else if (mNum in 9..11) meteoSeason = "Spring" else meteoSeason = "Summer" } // 2. Logic Map for Astronomical Seasons (Equinox/Solstice boundaries) if (isNorthern) { if ((mNum == 3 && dNum >= 20) || mNum in 4..5 || (mNum == 6 && dNum <= 20)) astroSeason = "Spring" else if ((mNum == 6 && dNum >= 21) || mNum in 7..8 || (mNum == 9 && dNum <= 21)) astroSeason = "Summer" else if ((mNum == 9 && dNum >= 22) || mNum == 10 || mNum == 11 || (mNum == 12 && dNum <= 20)) astroSeason = afName else astroSeason = "Winter" } else { if ((mNum == 3 && dNum >= 20) || mNum in 4..5 || (mNum == 6 && dNum <= 20)) astroSeason = afName else if ((mNum == 6 && dNum >= 21) || mNum in 7..8 || (mNum == 9 && dNum <= 21)) astroSeason = "Winter" else if ((mNum == 9 && dNum >= 22) || mNum == 10 || mNum == 11 || (mNum == 12 && dNum <= 20)) astroSeason = "Spring" else astroSeason = "Summer" } sendIfChanged(name: "meteorologicalSeason", value: meteoSeason) sendIfChanged(name: "astronomicalSeason", value: astroSeason) // Determine which calculation drives the primary tile String targetSeason = (primaryDisplay == "Astronomical") ? astroSeason : meteoSeason processSeasonUpdate(targetSeason, meteoSeason, astroSeason) } def processSeasonUpdate(String sName, String meteoName, String astroName) { // Extended Data Definitions mapping both Meteorological and Astronomical dates def seasonData = [ "Fall": [num: 1, img: "fall", meteoNStart: "Sept 1", meteoNEnd: "Nov 30", meteoSStart: "March 1", meteoSEnd: "May 31", astroNStart: "Sept 22", astroNEnd: "Dec 20", astroSStart: "March 20", astroSEnd: "June 20"], "Autumn": [num: 1, img: "autumn", meteoNStart: "Sept 1", meteoNEnd: "Nov 30", meteoSStart: "March 1", meteoSEnd: "May 31", astroNStart: "Sept 22", astroNEnd: "Dec 20", astroSStart: "March 20", astroSEnd: "June 20"], "Winter": [num: 2, img: "winter", meteoNStart: "Dec 1", meteoNEnd: "Feb 28/29", meteoSStart: "June 1", meteoSEnd: "Aug 31", astroNStart: "Dec 21", astroNEnd: "March 19", astroSStart: "June 21", astroSEnd: "Sept 21"], "Spring": [num: 3, img: "spring", meteoNStart: "March 1", meteoNEnd: "May 31", meteoSStart: "Sept 1", meteoSEnd: "Nov 30", astroNStart: "March 20", astroNEnd: "June 20", astroSStart: "Sept 22", astroSEnd: "Dec 20"], "Summer": [num: 4, img: "summer", meteoNStart: "June 1", meteoNEnd: "Aug 31", meteoSStart: "Dec 1", meteoSEnd: "Feb 28/29", astroNStart: "June 21", astroNEnd: "Sept 21", astroSStart: "Dec 21", astroSEnd: "March 19"] ] def data = seasonData[sName] // Pick the correct start/end dates based on user's preference of Primary Display and Hemisphere String start = "" String end = "" if (primaryDisplay == "Astronomical") { start = isNorthern ? data.astroNStart : data.astroSStart end = isNorthern ? data.astroNEnd : data.astroSEnd } else { start = isNorthern ? data.meteoNStart : data.meteoSStart end = isNorthern ? data.meteoNEnd : data.meteoSEnd } String iconBase = iconPathOvr ?: "https://raw.githubusercontent.com/jshimota01/hubitat/main/Icons-Images/season_icons/" String imgUrl = "${iconBase}${data.img}.svg" // Build Tile HTML String tileHtml = "