From f1176c65cc16d9e66b1be290fed9737ccb19a33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20L=27hopital?= Date: Tue, 23 Dec 2025 20:43:28 +0100 Subject: [PATCH] [astro] Add Circadian light (#19789) * Adding solar midnight and Circadian light Signed-off-by: gael@lhopital.org --- bundles/org.openhab.binding.astro/README.md | 5 + .../astro/internal/calc/CircadianCalc.java | 98 ++++++++++++++ .../binding/astro/internal/calc/SunCalc.java | 7 +- .../astro/internal/handler/SunHandler.java | 9 +- .../astro/internal/model/Circadian.java | 50 +++++++ .../binding/astro/internal/model/Sun.java | 10 ++ .../resources/OH-INF/i18n/astro.properties | 2 + .../main/resources/OH-INF/thing/channels.xml | 10 ++ .../src/main/resources/OH-INF/thing/sun.xml | 5 + .../resources/OH-INF/update/instructions.xml | 16 +++ .../internal/calc/CircadianCalcTest.java | 78 +++++++++++ .../astro/internal/calc/SunCalcTest.java | 124 +++++++++++------- 12 files changed, 365 insertions(+), 49 deletions(-) create mode 100644 bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/CircadianCalc.java create mode 100644 bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Circadian.java create mode 100644 bundles/org.openhab.binding.astro/src/main/resources/OH-INF/update/instructions.xml create mode 100644 bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/CircadianCalcTest.java diff --git a/bundles/org.openhab.binding.astro/README.md b/bundles/org.openhab.binding.astro/README.md index d59443ed98..27afc5a420 100644 --- a/bundles/org.openhab.binding.astro/README.md +++ b/bundles/org.openhab.binding.astro/README.md @@ -78,6 +78,11 @@ This binding has its own IconProvider and makes available the following list of - **group** `phase` - **channel** - `name` (String), values: `SUN_RISE, ASTRO_DAWN, NAUTIC_DAWN, CIVIL_DAWN, CIVIL_DUSK, NAUTIC_DUSK, ASTRO_DUSK, SUN_SET, DAYLIGHT, NOON, NIGHT` + - **group** `circadian`: provides automatically calculated values that follow a daily circadian rhythm based on the position of the sun. + - **channel** + - `brightness` (Dimmer): represents a recommended light brightness level as a percentage. It ranges from **0–100%**, where 0% is fully off and 100% is maximum brightness. The value follows the solar cycle, generally increasing towards **solar noon** and decreasing towards **midnight**. + - `temperature` (Number:Temperature): represents a recommended color temperature for white light in **Kelvin**, ranging from **2500 K** (warm white) to **5500 K** (cool white). Around solar noon the value tends towards the higher, cooler temperatures, while during the night and around midnight it shifts towards lower, warmer temperatures. + - **thing** `moon` - **group** `rise, set` - **channel** diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/CircadianCalc.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/CircadianCalc.java new file mode 100644 index 0000000000..aaa454e38b --- /dev/null +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/calc/CircadianCalc.java @@ -0,0 +1,98 @@ +/* + * 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.calc; + +import static org.openhab.binding.astro.internal.model.Circadian.*; + +import java.util.Calendar; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.astro.internal.model.Circadian; +import org.openhab.binding.astro.internal.model.Range; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Calculates the color temperature and brightness depending upon sun positional information + * + * @author Gaël L'hopital - Initial contribution + * @implNote based on the calculations of + * https://github.com/claytonjn/hass-circadian_lighting/blob/ed03e159b9a1db8f08a94b7de8c3b6b73fa0eb92/README.md + */ +@NonNullByDefault +public class CircadianCalc { + private static final long DELTA_TEMP = MAX_COLOR_TEMP - MIN_COLOR_TEMP; + private static final long TWELVE_HOURS_MS = 12 * 60 * 60 * 1000; + + private static final Logger logger = LoggerFactory.getLogger(CircadianCalc.class); + + public static Circadian calculate(Calendar calendar, Range riseRange, Range setRange, @Nullable Range noonRange) { + var rise = riseRange.getStart(); + var set = setRange.getStart(); + var noon = noonRange != null ? noonRange.getStart() : null; + + // If we have no rise or no set, there's no point calculating a Circadian Cycle + if (rise == null || set == null || noon == null) { + return Circadian.DEFAULT; + } + return calculate(calendar, rise, set, noon); + } + + public static Circadian calculate(Calendar calendar, Calendar rise, Calendar set, Calendar noon) { + // Figure out where we are in time so we know which half of the parabola to calculate. + // We're generating a different sunset-sunrise parabola for before and after solar midnight, + // because solar midnight might not be exactly halfway between sunrise and sunset. + // We're also generating a different parabola for sunrise-sunset. + + var now = calendar.getTimeInMillis(); + var sunRise = rise.getTimeInMillis(); + var sunSet = set.getTimeInMillis(); + + long h, x; + double k; + + if (sunRise < now && now < sunSet) { + // Sunrise -> Sunset parabola + k = 100.0; + h = noon.getTimeInMillis(); + // parabola before solar_noon else after solar_noon + x = now < h ? sunRise : sunSet; + } else { + k = -100.0; + if (now < sunRise) { + // Before sunrise we are still in the sunset -> sunrise cycle of the previous day + h = noon.getTimeInMillis() - TWELVE_HOURS_MS; + x = sunRise; + } else { + // sunset -> sunrise parabola + h = noon.getTimeInMillis() + TWELVE_HOURS_MS; + x = sunSet; + } + } + + double y = 0.0; + long dx = h - x; + if (dx == 0L) { + logger.debug("Degenerate circadian parabola (h == x), returning default values"); + return Circadian.DEFAULT; + } + double a = (y - k) / (dx * dx); + double percentage = a * Math.pow(now - h, 2) + k; + double colorTemp = percentage > 0 ? (DELTA_TEMP * percentage / 100) + MIN_COLOR_TEMP : MIN_COLOR_TEMP; + + logger.debug("Percentage: {}, ColorTemp: {}", percentage, colorTemp); + + return new Circadian(Math.min(100, Math.abs(percentage)), colorTemp); + } +} 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 3a47851f01..3298294903 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 @@ -65,7 +65,6 @@ public class SunCalc { private static final double H1 = Math.toRadians(-6.0); // nautical twilight angle private static final double H2 = Math.toRadians(-12.0); // astronomical twilight angle private static final double H3 = Math.toRadians(-18.0); // darkness angle - private static final double MINUTES_PER_DAY = 60 * 24; private static final int CURVE_TIME_INTERVAL = 20; // 20 minutes private static final double JD_ONE_MINUTE_FRACTION = 1.0 / 60 / 24; @@ -130,9 +129,11 @@ public class SunCalc { * Returns true, if the sun is up all day (no rise and set). */ private boolean isSunUpAllDay(Calendar calendar, double latitude, double longitude, @Nullable Double altitude) { - Calendar cal = DateTimeUtils.truncateToMidnight(calendar); Sun sun = new Sun(); - for (int minutes = 0; minutes <= MINUTES_PER_DAY; minutes += CURVE_TIME_INTERVAL) { + Calendar start = DateTimeUtils.truncateToMidnight(calendar); + Calendar cal = (Calendar) start.clone(); + var numberOfSamples = 24 * 60 / CURVE_TIME_INTERVAL; + for (int i = 0; i <= numberOfSamples; i++) { setPositionalInfo(cal, latitude, longitude, altitude, sun); if (sun.getPosition().getElevationAsDouble() < SUN_ANGLE) { return false; diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/SunHandler.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/SunHandler.java index e708f2a1c3..cfdef924c9 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/SunHandler.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/handler/SunHandler.java @@ -21,6 +21,7 @@ import java.util.TimeZone; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.astro.internal.calc.CircadianCalc; import org.openhab.binding.astro.internal.calc.SunCalc; import org.openhab.binding.astro.internal.job.DailyJobSun; import org.openhab.binding.astro.internal.job.Job; @@ -66,10 +67,14 @@ public class SunHandler extends AstroThingHandler { Double latitude = thingConfig.latitude; Double longitude = thingConfig.longitude; Double altitude = thingConfig.altitude; - sunCalc.setPositionalInfo(Calendar.getInstance(zone, locale), latitude != null ? latitude : 0, - longitude != null ? longitude : 0, altitude != null ? altitude : 0, sun); + Calendar calendar = Calendar.getInstance(zone, locale); + sunCalc.setPositionalInfo(calendar, latitude != null ? latitude : 0, longitude != null ? longitude : 0, + altitude != null ? altitude : 0, sun); sun.getEclipse().setElevations(this, timeZoneProvider); + + sun.setCircadian(CircadianCalc.calculate(calendar, sun.getRise(), sun.getSet(), sun.getNoon())); + this.sun = sun; publishPlanet(); diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Circadian.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Circadian.java new file mode 100644 index 0000000000..de644fbd3d --- /dev/null +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Circadian.java @@ -0,0 +1,50 @@ +/* + * 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; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.Units; + +/** + * Holds the calculated brightness and color temperature. + * + * @author Gaël L'hopital - Initial contribution + */ +@NonNullByDefault +public record Circadian(int brightness, int temperature) { + + public static final long MIN_COLOR_TEMP = 2500; + public static final long MAX_COLOR_TEMP = 5500; + + public static final Circadian DEFAULT = new Circadian(0, MIN_COLOR_TEMP); + + public Circadian { + if (brightness < 0 || brightness > 100) { + throw new IllegalArgumentException("Brightness level out of range"); + } + } + + public Circadian(double percentage, double colorTemp) { + this((int) Math.round(percentage), (int) colorTemp); + } + + public QuantityType getTemperature() { + return new QuantityType<>(temperature, Units.KELVIN); + } + + public PercentType getBrightness() { + return new PercentType(brightness); + } +} diff --git a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Sun.java b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Sun.java index 213f52b0b6..d25f03e307 100644 --- a/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Sun.java +++ b/bundles/org.openhab.binding.astro/src/main/java/org/openhab/binding/astro/internal/model/Sun.java @@ -42,6 +42,8 @@ public class Sun extends RiseSet implements Planet { private SunPhase phase = new SunPhase(); + private Circadian circadian = Circadian.DEFAULT; + /** * Returns the astro dawn range. */ @@ -308,4 +310,12 @@ public class Sun extends RiseSet implements Planet { public Map getAllRanges() { return ranges; } + + public Circadian getCircadian() { + return circadian; + } + + public void setCircadian(Circadian circadian) { + this.circadian = circadian; + } } diff --git a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/i18n/astro.properties b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/i18n/astro.properties index 579768275f..6cd9a7bd1f 100644 --- a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/i18n/astro.properties +++ b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/i18n/astro.properties @@ -62,6 +62,8 @@ thing-type.config.astro.sunconfig.useMeteorologicalSeason.description = Uses met # channel group types +channel-group-type.astro.circadian.label = Circadian Cycle +channel-group-type.astro.circadian.description = Circadian cycle computed values channel-group-type.astro.distance.label = Distance channel-group-type.astro.distance.description = Distance data channel-group-type.astro.moonEclipse.label = Eclipses diff --git a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/channels.xml b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/channels.xml index 749681a1c0..4064b8c0dc 100644 --- a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/channels.xml +++ b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/channels.xml @@ -293,6 +293,16 @@ + + + + Circadian cycle computed values + + + + + + DateTime diff --git a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/sun.xml b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/sun.xml index 75b2f46d52..672a7cbc31 100644 --- a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/sun.xml +++ b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/thing/sun.xml @@ -70,8 +70,13 @@ + + + 1 + + geolocation diff --git a/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/update/instructions.xml new file mode 100644 index 0000000000..c5f0a99f1e --- /dev/null +++ b/bundles/org.openhab.binding.astro/src/main/resources/OH-INF/update/instructions.xml @@ -0,0 +1,16 @@ + + + + + + + system:brightness + + + system:color-temperature-abs + + + + diff --git a/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/CircadianCalcTest.java b/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/CircadianCalcTest.java new file mode 100644 index 0000000000..9600787902 --- /dev/null +++ b/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/CircadianCalcTest.java @@ -0,0 +1,78 @@ +/* + * 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.calc; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +import org.junit.jupiter.api.Test; +import org.openhab.binding.astro.internal.model.Circadian; +import org.openhab.binding.astro.internal.model.Range; + +/** + * Tests for {@link CircadianCalc}. + * + * @author Gaël L'hopital - Initial contribution + */ +public class CircadianCalcTest { + + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + @Test + public void calculateUsesPreviousSolarMidnightBeforeSunrise() { + Calendar noon = newCalendar(2024, Calendar.JANUARY, 1, 13, 0); + Calendar sunrise = newCalendar(2024, Calendar.JANUARY, 1, 7, 0); + Calendar sunset = newCalendar(2024, Calendar.JANUARY, 1, 19, 0); + + Circadian beforeSunrise = CircadianCalc.calculate(newCalendar(2024, Calendar.JANUARY, 1, 5, 0), sunrise, sunset, + noon); + assertEquals(56, beforeSunrise.brightness()); + Circadian afterSunset = CircadianCalc.calculate(newCalendar(2024, Calendar.JANUARY, 1, 21, 0), sunrise, sunset, + noon); + assertEquals(afterSunset, beforeSunrise); + + assertEquals(2500, beforeSunrise.temperature()); + } + + @Test + public void calculateUsesRiseAndSetRangeStarts() { + Calendar noon = newCalendar(2024, Calendar.JANUARY, 1, 13, 0); + Calendar sunrise = newCalendar(2024, Calendar.JANUARY, 1, 7, 0); + Calendar sunset = newCalendar(2024, Calendar.JANUARY, 1, 19, 0); + Calendar now = newCalendar(2024, Calendar.JANUARY, 1, 21, 0); + + Range riseRange = new Range(sunrise, newCalendar(2024, Calendar.JANUARY, 1, 8, 0)); + Range setRange = new Range(sunset, newCalendar(2024, Calendar.JANUARY, 1, 20, 0)); + Range noonRange = new Range(noon, newCalendar(2024, Calendar.JANUARY, 1, 14, 0)); + + Circadian expected = CircadianCalc.calculate(now, sunrise, sunset, noon); + Circadian actual = CircadianCalc.calculate(now, riseRange, setRange, noonRange); + + assertEquals(expected, actual); + assertNotEquals(CircadianCalc.calculate(now, sunrise, sunrise, noon), actual); + + actual = CircadianCalc.calculate(now, new Range(), new Range(), null); + assertEquals(Circadian.DEFAULT, actual); + } + + private static Calendar newCalendar(int year, int month, int day, int hour, int minute) { + Calendar calendar = new GregorianCalendar(UTC); + calendar.clear(); + calendar.set(year, month, day, hour, minute, 0); + calendar.set(Calendar.MILLISECOND, 0); + return calendar; + } +} diff --git a/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/SunCalcTest.java b/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/SunCalcTest.java index 69a77a3715..b50550c972 100644 --- a/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/SunCalcTest.java +++ b/bundles/org.openhab.binding.astro/src/test/java/org/openhab/binding/astro/internal/calc/SunCalcTest.java @@ -14,6 +14,7 @@ package org.openhab.binding.astro.internal.calc; import static org.junit.jupiter.api.Assertions.*; +import java.lang.reflect.Method; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.Locale; @@ -45,12 +46,21 @@ import org.openhab.binding.astro.internal.util.DateTimeUtils; */ public class SunCalcTest { - private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("Europe/Amsterdam"); - private static final Calendar FEB_27_2019 = SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 1, 0, TIME_ZONE); + private static final TimeZone AMSTERDAM_TIME_ZONE = TimeZone.getTimeZone("Europe/Amsterdam"); + private static final Calendar FEB_27_2019 = SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 1, 0, + AMSTERDAM_TIME_ZONE); private static final double AMSTERDAM_LATITUDE = 52.367607; private static final double AMSTERDAM_LONGITUDE = 4.8978293; private static final double AMSTERDAM_ALTITUDE = 0.0; private static final int ACCURACY_IN_MILLIS = 3 * 60 * 1000; + private static final TimeZone BARROW_TIME_ZONE = TimeZone.getTimeZone("America/Anchorage"); + private static final double BARROW_LATITUDE = 71.2906; + private static final double BARROW_LONGITUDE = -156.7886; + private static final double BARROW_ALTITUDE = 0.0; + private static final Calendar SUMMER_SOLSTICE_AMSTERDAM = SunCalcTest.newCalendar(2024, Calendar.JUNE, 21, 12, 0, + AMSTERDAM_TIME_ZONE); + private static final Calendar SUMMER_SOLSTICE_BARROW = SunCalcTest.newCalendar(2024, Calendar.JUNE, 21, 12, 0, + BARROW_TIME_ZONE); private SunCalc sunCalc; @@ -62,7 +72,7 @@ public class SunCalcTest { @Test public void testGetSunInfoForOldDate() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); assertNotNull(sun.getNight()); @@ -91,150 +101,157 @@ public class SunCalcTest { @Test public void testGetSunInfoForAstronomicalDawnAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAstroDawn(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 05:39 till 06:18 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 5, 39, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 5, 39, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); cal = range.getEnd(); assertNotNull(cal); - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 18, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 18, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForNauticDawnAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getNauticDawn(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 06:18 till 06:58 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 18, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 18, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); cal = range.getEnd(); assertNotNull(cal); - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 58, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 58, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForCivilDawnAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getCivilDawn(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 06:58 till 07:32 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 58, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 6, 58, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); cal = range.getEnd(); assertNotNull(cal); - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 7, 32, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 7, 32, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForRiseAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getRise(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 07:32 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 7, 32, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 7, 32, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForSunNoonAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getNoon(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 12:54 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 12, 54, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 12, 54, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForSetAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getSet(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 18:15 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 15, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 15, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForCivilDuskAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getCivilDusk(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 18:15 till 18:50 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 15, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 15, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); cal = range.getEnd(); assertNotNull(cal); - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 50, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 50, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForNauticDuskAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getNauticDusk(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 18:50 till 19:29 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 50, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 18, 50, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); cal = range.getEnd(); assertNotNull(cal); - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 19, 29, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 19, 29, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @Test public void testGetSunInfoForAstronomicalDuskAccuracy() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAstroDusk(); assertNotNull(range); Calendar cal = range.getStart(); assertNotNull(cal); // expected result from haevens-above.com is 27 Feb 2019 19:29 till 20:09 - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 19, 29, TIME_ZONE).getTimeInMillis(), + assertEquals( + SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 19, 29, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); cal = range.getEnd(); assertNotNull(cal); - assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 20, 9, TIME_ZONE).getTimeInMillis(), + assertEquals(SunCalcTest.newCalendar(2019, Calendar.FEBRUARY, 27, 20, 9, AMSTERDAM_TIME_ZONE).getTimeInMillis(), cal.getTimeInMillis(), ACCURACY_IN_MILLIS); } @@ -242,7 +259,7 @@ public class SunCalcTest { @Disabled public void testRangesForCoherenceBetweenNightEndAndAstroDawnStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.NIGHT); assertNotNull(range); @@ -254,7 +271,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenMorningNightEndAndAstroDawnStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.MORNING_NIGHT); assertNotNull(range); @@ -266,7 +283,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenAstroDownEndAndNauticDawnStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.ASTRO_DAWN); assertNotNull(range); @@ -278,7 +295,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenNauticDawnEndAndCivilDawnStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.NAUTIC_DAWN); assertNotNull(range); @@ -290,7 +307,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenCivilDawnEndAndSunRiseStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.CIVIL_DAWN); assertNotNull(range); @@ -302,7 +319,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenSunRiseEndAndDaylightStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.SUN_RISE); assertNotNull(range); @@ -314,7 +331,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenDaylightEndAndSunSetStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.DAYLIGHT); assertNotNull(range); @@ -326,7 +343,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenSunSetEndAndCivilDuskStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.SUN_SET); assertNotNull(range); @@ -338,7 +355,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenCivilDuskEndAndNauticDuskStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.CIVIL_DUSK); assertNotNull(range); @@ -350,7 +367,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenNauticDuskEndAndAstroDuskStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.NAUTIC_DUSK); assertNotNull(range); @@ -362,7 +379,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenAstroDuskEndAndNightStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.ASTRO_DUSK); assertNotNull(range); @@ -374,7 +391,7 @@ public class SunCalcTest { @Test public void testRangesForCoherenceBetweenAstroDuskEndAndEveningNightStart() { Sun sun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, false, - TIME_ZONE, Locale.ROOT); + AMSTERDAM_TIME_ZONE, Locale.ROOT); Range range = sun.getAllRanges().get(SunPhaseName.ASTRO_DUSK); assertNotNull(range); @@ -388,7 +405,7 @@ public class SunCalcTest { TimeZone tZone = TimeZone.getTimeZone("Europe/London"); Calendar tDate = SunCalcTest.newCalendar(2020, Calendar.MAY, 13, 5, 12, tZone); - Sun sun = sunCalc.getSunInfo(tDate, 53.524695, -2.4, 0.0, true, TIME_ZONE, Locale.ROOT); + Sun sun = sunCalc.getSunInfo(tDate, 53.524695, -2.4, 0.0, true, AMSTERDAM_TIME_ZONE, Locale.ROOT); assertEquals(SunPhaseName.CIVIL_DAWN, sun.getPhase().getName()); } @@ -399,7 +416,7 @@ public class SunCalcTest { Calendar tDate = SunCalcTest.newCalendar(2020, Calendar.MAY, 13, 5, 13, tZone); tDate.set(Calendar.SECOND, 4); - Sun sun = sunCalc.getSunInfo(tDate, 53.524695, -2.4, 0.0, true, TIME_ZONE, Locale.ROOT); + Sun sun = sunCalc.getSunInfo(tDate, 53.524695, -2.4, 0.0, true, AMSTERDAM_TIME_ZONE, Locale.ROOT); assertEquals(SunPhaseName.SUN_RISE, sun.getPhase().getName()); } @@ -408,7 +425,7 @@ public class SunCalcTest { TimeZone tZone = TimeZone.getTimeZone("Europe/London"); Calendar tDate = SunCalcTest.newCalendar(2020, Calendar.MAY, 13, 5, 18, tZone); - Sun sun = sunCalc.getSunInfo(tDate, 53.524695, -2.4, 0.0, true, TIME_ZONE, Locale.ROOT); + Sun sun = sunCalc.getSunInfo(tDate, 53.524695, -2.4, 0.0, true, AMSTERDAM_TIME_ZONE, Locale.ROOT); assertEquals(SunPhaseName.DAYLIGHT, sun.getPhase().getName()); } @@ -445,9 +462,9 @@ public class SunCalcTest { @Test public void testAstroAndMeteoSeasons() { Sun meteoSun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, - true, TIME_ZONE, Locale.ROOT); + true, AMSTERDAM_TIME_ZONE, Locale.ROOT); Sun equiSun = sunCalc.getSunInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE, - false, TIME_ZONE, Locale.ROOT); + false, AMSTERDAM_TIME_ZONE, Locale.ROOT); Calendar cal = meteoSun.getSeason().getSpring(); assertNotNull(cal); @@ -458,4 +475,23 @@ public class SunCalcTest { assertEquals(1, cal.get(Calendar.DAY_OF_MONTH)); assertFalse(cal.get(Calendar.DAY_OF_MONTH) == cal2.get(Calendar.DAY_OF_MONTH)); } + + @Test + public void testIsSunUpAllDayForBarrowAlaska() throws Exception { + Method isSunUpAllDay = SunCalc.class.getDeclaredMethod("isSunUpAllDay", Calendar.class, double.class, + double.class, Double.class); + isSunUpAllDay.setAccessible(true); + + // At summer solstice in Barrow, Alaska, sun stays up all day + boolean result = (boolean) isSunUpAllDay.invoke(sunCalc, SUMMER_SOLSTICE_BARROW, BARROW_LATITUDE, + BARROW_LONGITUDE, BARROW_ALTITUDE); + + assertTrue(result); + + // It's not the case in Amsterdam + result = (boolean) isSunUpAllDay.invoke(sunCalc, SUMMER_SOLSTICE_AMSTERDAM, AMSTERDAM_LATITUDE, + AMSTERDAM_LONGITUDE, AMSTERDAM_ALTITUDE); + + assertFalse(result); + } }