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 |  |
| oh:astro:zodiac-taurus | Yes |  |
| oh:astro:zodiac-virgo | Yes |  |
+| oh:astro:season | Yes |  |
+| oh:astro:season-autumn | Yes |  |
+| oh:astro:season-spring | Yes |  |
+| oh:astro:season-summer | Yes |  |
+| oh:astro:season-winter | Yes |  |
## 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