[astro] Add Circadian light (#19789)

* Adding solar midnight and Circadian light

Signed-off-by: gael@lhopital.org <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital
2025-12-23 20:43:28 +01:00
committed by GitHub
parent 1163f4ce11
commit f1176c65cc
12 changed files with 365 additions and 49 deletions
@@ -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 **0100%**, 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**
@@ -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);
}
}
@@ -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;
@@ -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();
@@ -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);
}
}
@@ -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<SunPhaseName, Range> getAllRanges() {
return ranges;
}
public Circadian getCircadian() {
return circadian;
}
public void setCircadian(Circadian circadian) {
this.circadian = circadian;
}
}
@@ -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
@@ -293,6 +293,16 @@
</channels>
</channel-group-type>
<!-- Circadian cycle -->
<channel-group-type id="circadian">
<label>Circadian Cycle</label>
<description>Circadian cycle computed values</description>
<channels>
<channel id="brightness" typeId="system.brightness"/>
<channel id="temperature" typeId="system.color-temperature-abs"/>
</channels>
</channel-group-type>
<channel-type id="total">
<item-type>DateTime</item-type>
<label>Total Eclipse</label>
@@ -70,8 +70,13 @@
<channel-group id="season" typeId="season"/>
<channel-group id="eclipse" typeId="sunEclipse"/>
<channel-group id="phase" typeId="sunPhase"/>
<channel-group id="circadian" typeId="circadian"/>
</channel-groups>
<properties>
<property name="thingTypeVersion">1</property>
</properties>
<representation-property>geolocation</representation-property>
<config-description-ref uri="thing-type:astro:sunconfig"/>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
<thing-type uid="astro:sun">
<instruction-set targetVersion="1">
<add-channel id="brightness" groupIds="circadian">
<type>system:brightness</type>
</add-channel>
<add-channel id="temperature" groupIds="circadian">
<type>system:color-temperature-abs</type>
</add-channel>
</instruction-set>
</thing-type>
</update:update-descriptions>
@@ -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;
}
}
@@ -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);
}
}