From a5063ed45e109019c1819b6094debc88f251cdcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20L=27hopital?= Date: Mon, 29 Dec 2025 16:55:25 +0100 Subject: [PATCH] [astro] Refresh SeasonCalc and add icon package (#19875) * Refresh SeasonCalc and add icon package Signed-off-by: gael@lhopital.org --- bundles/org.openhab.binding.astro/NOTICE | 3 + bundles/org.openhab.binding.astro/README.md | 5 + .../doc/images/season-autumn.svg | 6 + .../doc/images/season-spring.svg | 8 + .../doc/images/season-summer.svg | 6 + .../doc/images/season-winter.svg | 12 ++ .../doc/images/season.svg | 28 +++ .../astro/internal/AstroIconProvider.java | 20 +- .../astro/internal/calc/SeasonCalc.java | 179 +++--------------- .../binding/astro/internal/calc/SunCalc.java | 8 +- .../astro/internal/job/DailyJobSun.java | 6 +- .../astro/internal/model/Hemisphere.java | 30 +++ .../binding/astro/internal/model/Season.java | 136 +++++++------ .../astro/internal/model/SeasonName.java | 7 +- .../binding/astro/internal/model/Sun.java | 5 +- .../astro/internal/util/DateTimeUtils.java | 20 ++ .../main/resources/OH-INF/thing/channels.xml | 5 + .../src/main/resources/icon/season-autumn.svg | 6 + .../src/main/resources/icon/season-spring.svg | 8 + .../src/main/resources/icon/season-summer.svg | 6 + .../src/main/resources/icon/season-winter.svg | 12 ++ .../src/main/resources/icon/season.svg | 28 +++ .../astro/internal/calc/SeasonCalcTest.java | 145 ++++++++++++++ .../astro/internal/calc/SunCalcTest.java | 45 ----- .../internal/util/DateTimeUtilsTest.java | 73 +++---- 25 files changed, 480 insertions(+), 327 deletions(-) create mode 100644 bundles/org.openhab.binding.astro/doc/images/season-autumn.svg create mode 100644 bundles/org.openhab.binding.astro/doc/images/season-spring.svg create mode 100644 bundles/org.openhab.binding.astro/doc/images/season-summer.svg create mode 100644 bundles/org.openhab.binding.astro/doc/images/season-winter.svg create mode 100644 bundles/org.openhab.binding.astro/doc/images/season.svg create mode 100644 bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Hemisphere.java create mode 100644 bundles/org.openhab.binding.astro/src/main/resources/icon/season-autumn.svg create mode 100644 bundles/org.openhab.binding.astro/src/main/resources/icon/season-spring.svg create mode 100644 bundles/org.openhab.binding.astro/src/main/resources/icon/season-summer.svg create mode 100644 bundles/org.openhab.binding.astro/src/main/resources/icon/season-winter.svg create mode 100644 bundles/org.openhab.binding.astro/src/main/resources/icon/season.svg create mode 100644 bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/SeasonCalcTest.java diff --git a/bundles/org.openhab.binding.astro/NOTICE b/bundles/org.openhab.binding.astro/NOTICE index 630238a2a4..db5de7fefd 100644 --- a/bundles/org.openhab.binding.astro/NOTICE +++ b/bundles/org.openhab.binding.astro/NOTICE @@ -11,6 +11,9 @@ https://www.eclipse.org/legal/epl-2.0/. Zodiac Icon Set provided by SVG Repo under CC0 License (Public Domain) at https://www.svgrepo.com/collection/zodiac-2/ +Season Icon Set provided by Free SVG under Public Domain at +https://freesvg.org/seasons-icons76143 + == Source Code https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.binding.astro/README.md b/bundles/org.openhab.binding.astro/README.md index 27afc5a420..5cdcd1c02a 100644 --- a/bundles/org.openhab.binding.astro/README.md +++ b/bundles/org.openhab.binding.astro/README.md @@ -47,6 +47,11 @@ This binding has its own IconProvider and makes available the following list of | oh:astro:zodiac-scorpio | Yes | ![Scorpio](doc/images/zodiac-scorpio.svg) | | oh:astro:zodiac-taurus | Yes | ![Taurus](doc/images/zodiac-taurus.svg) | | oh:astro:zodiac-virgo | Yes | ![Virgo](doc/images/zodiac-virgo.svg) | +| oh:astro:season | Yes | ![Season](doc/images/season.svg) | +| oh:astro:season-autumn | Yes | ![Autumn](doc/images/season-autumn.svg) | +| oh:astro:season-spring | Yes | ![Spring](doc/images/season-spring.svg) | +| oh:astro:season-summer | Yes | ![Summer](doc/images/season-summer.svg) | +| oh:astro:season-winter | Yes | ![Winter](doc/images/season-winter.svg) | ## Channels diff --git a/bundles/org.openhab.binding.astro/doc/images/season-autumn.svg b/bundles/org.openhab.binding.astro/doc/images/season-autumn.svg new file mode 100644 index 0000000000..262414f35c --- /dev/null +++ b/bundles/org.openhab.binding.astro/doc/images/season-autumn.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.astro/doc/images/season-spring.svg b/bundles/org.openhab.binding.astro/doc/images/season-spring.svg new file mode 100644 index 0000000000..9567894ab8 --- /dev/null +++ b/bundles/org.openhab.binding.astro/doc/images/season-spring.svg @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.astro/doc/images/season-summer.svg b/bundles/org.openhab.binding.astro/doc/images/season-summer.svg new file mode 100644 index 0000000000..85966245e5 --- /dev/null +++ b/bundles/org.openhab.binding.astro/doc/images/season-summer.svg @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.astro/doc/images/season-winter.svg b/bundles/org.openhab.binding.astro/doc/images/season-winter.svg new file mode 100644 index 0000000000..99935152a6 --- /dev/null +++ b/bundles/org.openhab.binding.astro/doc/images/season-winter.svg @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.astro/doc/images/season.svg b/bundles/org.openhab.binding.astro/doc/images/season.svg new file mode 100644 index 0000000000..aa6c9bf537 --- /dev/null +++ b/bundles/org.openhab.binding.astro/doc/images/season.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/AstroIconProvider.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/AstroIconProvider.java index b4bffd037a..cddc7703d4 100755 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/AstroIconProvider.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/AstroIconProvider.java @@ -24,6 +24,7 @@ import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.astro.internal.model.SeasonName; import org.openhab.binding.astro.internal.model.ZodiacSign; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.ui.icon.IconProvider; @@ -47,7 +48,9 @@ import org.slf4j.LoggerFactory; public class AstroIconProvider implements IconProvider { private static final String DEFAULT_LABEL = "Astro Icons"; private static final String DEFAULT_DESCRIPTION = "Icons provided for the Astro Binding"; - private static final String ZODIAC_ICON = "zodiac"; + private static final String ZODIAC_SET = "zodiac"; + private static final String SEASON_SET = "season"; + private static final Set ICON_SETS = Set.of(SEASON_SET, ZODIAC_SET); private final Logger logger = LoggerFactory.getLogger(AstroIconProvider.class); private final TranslationProvider i18nProvider; @@ -79,18 +82,23 @@ public class AstroIconProvider implements IconProvider { @Override public @Nullable Integer hasIcon(String category, String iconSetId, Format format) { - return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID) && category.equals(ZODIAC_ICON) ? 0 : null; + return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID) && ICON_SETS.contains(category) ? 0 : null; } @Override public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) { String iconName = "icon/%s.svg".formatted(category); - if (category.equals(ZODIAC_ICON) && state != null) { + if (ICON_SETS.contains(category) && state != null) { try { - var sign = ZodiacSign.valueOf(state); - iconName = iconName.replace(".", "-%s.".formatted(sign.name().toLowerCase(Locale.US))); - } catch (IllegalArgumentException ignore) { + Enum stateEnum = switch (category) { + case ZODIAC_SET -> ZodiacSign.valueOf(state); + case SEASON_SET -> SeasonName.valueOf(state); + default -> throw new IllegalArgumentException("Category of icon not found: %s".formatted(category)); + }; + iconName = iconName.replace(".", "-%s.".formatted(stateEnum.name().toLowerCase(Locale.US))); + } catch (IllegalArgumentException e) { // Invalid state for the icon set, we'll remain on default icon + logger.warn("Error retrieving icon name '{}' - using default: {}", state, e.getMessage()); } } diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SeasonCalc.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SeasonCalc.java index 8d46b95970..af8dc941fe 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SeasonCalc.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SeasonCalc.java @@ -12,14 +12,11 @@ */ package org.openhab.binding.astro.internal.calc; -import java.util.Calendar; -import java.util.Locale; +import java.time.Instant; import java.util.TimeZone; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.astro.internal.model.Season; -import org.openhab.binding.astro.internal.model.SeasonName; import org.openhab.binding.astro.internal.util.DateTimeUtils; import org.openhab.binding.astro.internal.util.MathUtils; @@ -31,181 +28,61 @@ import org.openhab.binding.astro.internal.util.MathUtils; */ @NonNullByDefault public class SeasonCalc { - private int currentYear; - private @Nullable Season currentSeason; + private static final int[] AMPLITUDE = new int[] { 485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, + 18, 17, 16, 14, 12, 12, 12, 9, 8 }; + private static final double[] PHASE = new double[] { 324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, + 243.58, 119.81, 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, + 227.73, 15.45 }; + private static final double[] FREQUENCY = new double[] { 1934.136, 32964.467, 20.186, 445267.112, 45036.886, + 22518.443, 65928.934, 3034.906, 9037.513, 33718.147, 150.678, 2281.226, 29929.562, 31555.956, 4443.417, + 67555.328, 4562.452, 62894.029, 31436.921, 14577.848, 31931.756, 34777.259, 1222.114, 16859.074 }; /** * Returns the seasons of the year of the specified calendar. */ - public Season getSeason(Calendar calendar, double latitude, boolean useMeteorologicalSeason, TimeZone zone, - Locale locale) { - int year = calendar.get(Calendar.YEAR); - boolean isSouthernHemisphere = latitude < 0.0; - Season season = currentSeason; - if (currentYear != year) { - season = new Season(zone, locale); - if (!isSouthernHemisphere) { - season.setSpring(calcEquiSol(0, year, zone, locale)); - season.setSummer(calcEquiSol(1, year, zone, locale)); - season.setAutumn(calcEquiSol(2, year, zone, locale)); - season.setWinter(calcEquiSol(3, year, zone, locale)); - } else { - season.setSpring(calcEquiSol(2, year, zone, locale)); - season.setSummer(calcEquiSol(3, year, zone, locale)); - season.setAutumn(calcEquiSol(0, year, zone, locale)); - season.setWinter(calcEquiSol(1, year, zone, locale)); - } - currentSeason = season; - currentYear = year; - } - - if (useMeteorologicalSeason && season != null) { - Calendar cal = season.getSpring(); - if (cal != null) { - atMidnightOfFirstMonthDay(cal); - } - cal = season.getSummer(); - if (cal != null) { - atMidnightOfFirstMonthDay(cal); - } - cal = season.getAutumn(); - if (cal != null) { - atMidnightOfFirstMonthDay(cal); - } - cal = season.getWinter(); - if (cal != null) { - atMidnightOfFirstMonthDay(cal); - } - } - - if (season != null) { - season.setName(!isSouthernHemisphere ? getCurrentSeasonNameNorthern(calendar) - : getCurrentSeasonNameSouthern(calendar)); - return season; - } - return new Season(zone, locale); - } - - private void atMidnightOfFirstMonthDay(Calendar calendar) { - calendar.set(Calendar.DAY_OF_MONTH, 1); - calendar.set(Calendar.HOUR_OF_DAY, 0); - calendar.set(Calendar.MINUTE, 0); - calendar.set(Calendar.SECOND, 0); - calendar.set(Calendar.MILLISECOND, 0); - } - - /** - * Returns the current season name for the northern hemisphere. - */ - @Nullable - private SeasonName getCurrentSeasonNameNorthern(Calendar calendar) { - Season currentSeason = this.currentSeason; - if (currentSeason == null) { - return null; - } - long currentMillis = calendar.getTimeInMillis(); - Calendar spring = currentSeason.getSpring(); - Calendar summer = currentSeason.getSummer(); - Calendar autumn = currentSeason.getAutumn(); - Calendar winter = currentSeason.getWinter(); - if ((spring != null && currentMillis < spring.getTimeInMillis()) - || (winter != null && currentMillis >= winter.getTimeInMillis())) { - return SeasonName.WINTER; - } else if (spring != null && summer != null && currentMillis >= spring.getTimeInMillis() - && currentMillis < summer.getTimeInMillis()) { - return SeasonName.SPRING; - } else if (summer != null && autumn != null && currentMillis >= summer.getTimeInMillis() - && currentMillis < autumn.getTimeInMillis()) { - return SeasonName.SUMMER; - } else if (autumn != null && winter != null && currentMillis >= autumn.getTimeInMillis() - && currentMillis < winter.getTimeInMillis()) { - return SeasonName.AUTUMN; - } - return null; - } - - /** - * Returns the current season name for the southern hemisphere. - */ - @Nullable - private SeasonName getCurrentSeasonNameSouthern(Calendar calendar) { - Season currentSeason = this.currentSeason; - if (currentSeason == null) { - return null; - } - long currentMillis = calendar.getTimeInMillis(); - Calendar spring = currentSeason.getSpring(); - Calendar summer = currentSeason.getSummer(); - Calendar autumn = currentSeason.getAutumn(); - Calendar winter = currentSeason.getWinter(); - if ((autumn != null && currentMillis < autumn.getTimeInMillis()) - || (summer != null && currentMillis >= summer.getTimeInMillis())) { - return SeasonName.SUMMER; - } else if (autumn != null && winter != null && currentMillis >= autumn.getTimeInMillis() - && currentMillis < winter.getTimeInMillis()) { - return SeasonName.AUTUMN; - } else if (winter != null && spring != null && currentMillis >= winter.getTimeInMillis() - && currentMillis < spring.getTimeInMillis()) { - return SeasonName.WINTER; - } else if (spring != null && summer != null && currentMillis >= spring.getTimeInMillis() - && currentMillis < summer.getTimeInMillis()) { - return SeasonName.SPRING; - } - return null; + public static Season calculate(int year, double latitude, boolean useMeteorologicalSeason, TimeZone zone) { + return new Season(latitude, useMeteorologicalSeason, zone, calcEquiSol(3, year - 1), calcEquiSol(0, year), + calcEquiSol(1, year), calcEquiSol(2, year), calcEquiSol(3, year), calcEquiSol(0, year + 1)); } /** * Calculates the date of the season. */ - @Nullable - private Calendar calcEquiSol(int season, int year, TimeZone zone, Locale locale) { + private static Instant calcEquiSol(int season, int year) { double estimate = calcInitial(season, year); double t = DateTimeUtils.toJulianCenturies(estimate); double w = 35999.373 * t - 2.47; double dl = 1 + 0.0334 * MathUtils.cosDeg(w) + 0.0007 * MathUtils.cosDeg(2 * w); double s = periodic24(t); double julianDate = estimate + ((0.00001 * s) / dl); - return DateTimeUtils.toCalendar(julianDate, zone, locale); + return DateTimeUtils.jdToInstant(julianDate); } /** * Calculate an initial guess of the Equinox or Solstice of a given year. */ - private double calcInitial(int season, int year) { + private static double calcInitial(int season, int year) { double y = (year - 2000) / 1000d; - switch (season) { - case 0: - return 2451623.80984 + 365242.37404 * y + 0.05169 * Math.pow(y, 2) - 0.00411 * Math.pow(y, 3) - - 0.00057 * Math.pow(y, 4); - case 1: - return 2451716.56767 + 365241.62603 * y + 0.00325 * Math.pow(y, 2) + 0.00888 * Math.pow(y, 3) - - 0.00030 * Math.pow(y, 4); - case 2: - return 2451810.21715 + 365242.01767 * y - 0.11575 * Math.pow(y, 2) + 0.00337 * Math.pow(y, 3) - + 0.00078 * Math.pow(y, 4); - case 3: - return 2451900.05952 + 365242.74049 * y - 0.06223 * Math.pow(y, 2) - 0.00823 * Math.pow(y, 3) - + 0.00032 * Math.pow(y, 4); - } - return 0; + return switch (season) { + case 0 -> 2451623.80984 + 365242.37404 * y + 0.05169 * Math.pow(y, 2) - 0.00411 * Math.pow(y, 3) + - 0.00057 * Math.pow(y, 4); + case 1 -> 2451716.56767 + 365241.62603 * y + 0.00325 * Math.pow(y, 2) + 0.00888 * Math.pow(y, 3) + - 0.00030 * Math.pow(y, 4); + case 2 -> 2451810.21715 + 365242.01767 * y - 0.11575 * Math.pow(y, 2) + 0.00337 * Math.pow(y, 3) + + 0.00078 * Math.pow(y, 4); + case 3 -> 2451900.05952 + 365242.74049 * y - 0.06223 * Math.pow(y, 2) - 0.00823 * Math.pow(y, 3) + + 0.00032 * Math.pow(y, 4); + default -> throw new IllegalArgumentException("Unexpected value: " + season); + }; } /** * Calculate 24 periodic terms */ - private double periodic24(double T) { - int[] a = new int[] { 485, 203, 199, 182, 156, 136, 77, 74, 70, 58, 52, 50, 45, 44, 29, 18, 17, 16, 14, 12, 12, - 12, 9, 8 }; - double[] b = new double[] { 324.96, 337.23, 342.08, 27.85, 73.14, 171.52, 222.54, 296.72, 243.58, 119.81, - 297.17, 21.02, 247.54, 325.15, 60.93, 155.12, 288.79, 198.04, 199.76, 95.39, 287.11, 320.81, 227.73, - 15.45 }; - double[] c = new double[] { 1934.136, 32964.467, 20.186, 445267.112, 45036.886, 22518.443, 65928.934, 3034.906, - 9037.513, 33718.147, 150.678, 2281.226, 29929.562, 31555.956, 4443.417, 67555.328, 4562.452, 62894.029, - 31436.921, 14577.848, 31931.756, 34777.259, 1222.114, 16859.074 }; - + private static double periodic24(double t) { double result = 0; for (int i = 0; i < 24; i++) { - result += a[i] * MathUtils.cosDeg(b[i] + (c[i] * T)); + result += AMPLITUDE[i] * MathUtils.cosDeg(PHASE[i] + (FREQUENCY[i] * t)); } return result; } diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SunCalc.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SunCalc.java index 3298294903..55eda869f7 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SunCalc.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/SunCalc.java @@ -30,6 +30,7 @@ import org.openhab.binding.astro.internal.model.EclipseType; import org.openhab.binding.astro.internal.model.Position; import org.openhab.binding.astro.internal.model.Radiation; import org.openhab.binding.astro.internal.model.Range; +import org.openhab.binding.astro.internal.model.Season; import org.openhab.binding.astro.internal.model.Sun; import org.openhab.binding.astro.internal.model.SunPhaseName; import org.openhab.binding.astro.internal.util.DateTimeUtils; @@ -272,8 +273,11 @@ public class SunCalc { sun.setZodiac(ZodiacCalc.calculate(lsun, calendar.toInstant())); - SeasonCalc seasonCalc = new SeasonCalc(); - sun.setSeason(seasonCalc.getSeason(calendar, latitude, useMeteorologicalSeason, zone, locale)); + Season season = sun.getSeason(); + var year = calendar.get(Calendar.YEAR); + if (season == null || season.getYear() != year) { + sun.setSeason(SeasonCalc.calculate(year, latitude, useMeteorologicalSeason, zone)); + } // phase for (Entry rangeEntry : sortByValue(sun.getAllRanges()).entrySet()) { diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/DailyJobSun.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/DailyJobSun.java index 0f1154b854..0a088f0d40 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/DailyJobSun.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/job/DailyJobSun.java @@ -26,6 +26,7 @@ import org.openhab.binding.astro.internal.handler.AstroThingHandler; import org.openhab.binding.astro.internal.model.Eclipse; import org.openhab.binding.astro.internal.model.Planet; import org.openhab.binding.astro.internal.model.Range; +import org.openhab.binding.astro.internal.model.Season; import org.openhab.binding.astro.internal.model.Sun; /** @@ -127,7 +128,10 @@ public final class DailyJobSun extends AbstractJob { if (sun.getZodiac().getEnd() instanceof Instant when) { schedulePublishPlanet(handler, when); } - schedulePublishPlanet(handler, sun.getSeason().getNextSeason(), zone, locale); + + if (sun.getSeason() instanceof Season season) { + schedulePublishPlanet(handler, season.getNextSeason()); + } // schedule phase jobs Calendar cal = sun.getRise().getStart(); diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Hemisphere.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Hemisphere.java new file mode 100644 index 0000000000..643bf4559a --- /dev/null +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Hemisphere.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2010-2025 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.astro.internal.model; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Earth hemispheres + * + * @author Gaƫl L'hopital - Initial contribution + */ +@NonNullByDefault +public enum Hemisphere { + NORTHERN, + SOUTHERN; + + public static Hemisphere getHemisphere(double latitude) { + return latitude < 0.0 ? SOUTHERN : NORTHERN; + } +} diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Season.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Season.java index e80ce7d5e0..9cdda5f2a7 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Season.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Season.java @@ -13,15 +13,19 @@ package org.openhab.binding.astro.internal.model; import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.Calendar; -import java.util.Locale; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; import java.util.TimeZone; import javax.measure.quantity.Time; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.astro.internal.util.DateTimeUtils; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.Units; @@ -33,122 +37,112 @@ import org.openhab.core.library.unit.Units; */ @NonNullByDefault public class Season { - private @Nullable Calendar spring; - private @Nullable Calendar summer; - private @Nullable Calendar autumn; - private @Nullable Calendar winter; + private static final Map> SEASON_ORDER = Map.of(Hemisphere.NORTHERN, + List.of(SeasonName.WINTER, SeasonName.SPRING, SeasonName.SUMMER, SeasonName.AUTUMN, SeasonName.WINTER, + SeasonName.SPRING), + Hemisphere.SOUTHERN, List.of(SeasonName.SUMMER, SeasonName.AUTUMN, SeasonName.WINTER, SeasonName.SPRING, + SeasonName.SUMMER, SeasonName.AUTUMN)); - private @Nullable SeasonName name; + private record LocalSeason(SeasonName name, Instant startsOn, Instant endsOn, int year) { + boolean contains(Instant when) { + return !startsOn.isAfter(when) && endsOn.isAfter(when); + } + } - private TimeZone timeZone; - private Locale locale; + private final List seasons = new ArrayList<>(5); + private final int year; - public Season(TimeZone timeZone, Locale locale) { - this.timeZone = timeZone; - this.locale = locale; + public Season(double latitude, boolean useMeteorologicalSeason, TimeZone zone, Instant... equiSols) { + // Expect to receive last of previous year, all from current year, first of next year + if (equiSols.length != SeasonName.values().length + 2) { + throw new IllegalArgumentException("Incorrect number of seasons provided"); + } + var hemisphere = Hemisphere.getHemisphere(latitude); + List moments = Arrays.stream(equiSols).sorted() + .map(i -> useMeteorologicalSeason ? DateTimeUtils.atMidnightOfFirstMonthDay(i, zone) : i).toList(); + for (int i = 0; i < moments.size() - 1; i++) { + var current = moments.get(i); + var next = moments.get(i + 1); + var seasonName = Objects.requireNonNull(SEASON_ORDER.get(hemisphere)).get(i); + ZonedDateTime zonedDateTime = current.atZone(zone.toZoneId()); + seasons.add(new LocalSeason(seasonName, current, next, zonedDateTime.getYear())); + } + year = seasons.stream().mapToInt(LocalSeason::year).max().orElseThrow(NoSuchElementException::new); + } + + public int getYear() { + return year; + } + + private Instant getSeasonStart(SeasonName season) { + return seasons.stream().filter(s -> s.name.equals(season) && s.year == year).map(s -> s.startsOn).findFirst() + .orElseThrow(NoSuchElementException::new); } /** * Returns the date of the beginning of spring. */ - @Nullable - public Calendar getSpring() { - return spring; - } - - /** - * Sets the date of the beginning of spring. - */ - public void setSpring(@Nullable Calendar spring) { - this.spring = spring; + public Instant getSpring() { + return getSeasonStart(SeasonName.SPRING); } /** * Returns the date of the beginning of summer. */ - @Nullable - public Calendar getSummer() { - return summer; - } - - /** - * Sets the date of the beginning of summer. - */ - public void setSummer(@Nullable Calendar summer) { - this.summer = summer; + public Instant getSummer() { + return getSeasonStart(SeasonName.SUMMER); } /** * Returns the date of the beginning of autumn. */ - @Nullable - public Calendar getAutumn() { - return autumn; - } - - /** - * Sets the date of the beginning of autumn. - */ - public void setAutumn(@Nullable Calendar autumn) { - this.autumn = autumn; + public Instant getAutumn() { + return getSeasonStart(SeasonName.AUTUMN); } /** * Returns the date of the beginning of winter. */ - @Nullable - public Calendar getWinter() { - return winter; + public Instant getWinter() { + return getSeasonStart(SeasonName.WINTER); } - /** - * Returns the date of the beginning of winter. - */ - public void setWinter(@Nullable Calendar winter) { - this.winter = winter; + private LocalSeason getSeason(Instant when) { + return seasons.stream().filter(s -> s.contains(when)).findFirst().orElseThrow(NoSuchElementException::new); } /** * Returns the current season name. */ - @Nullable public SeasonName getName() { - return name; - } - - /** - * Sets the current season name. - */ - public void setName(@Nullable SeasonName name) { - this.name = name; + return getSeason(Instant.now()).name; } /** * Returns the next season. */ - public Calendar getNextSeason() { - return DateTimeUtils.getNextFromToday(timeZone, locale, spring, summer, autumn, winter); + public Instant getNextSeason() { + return getSeason(Instant.now()).endsOn; } /** * Returns the next season name. */ public SeasonName getNextName() { - int ordinal = name == null ? 0 : name.ordinal() + 1; - if (ordinal > 3) { - ordinal = 0; - } - return SeasonName.values()[ordinal]; + return getSeason(Instant.now()).name.next(); } /** * Returns the time left for current season */ public QuantityType