[astro] Review Eclipses calculations (#20077)

* Instant and Immutable Eclipse

Signed-off-by: Gaël L'hopital <gael@lhopital.org>
Co-authored-by: Nadahar <Nadahar@users.noreply.github.com>
This commit is contained in:
Gaël L'hopital
2026-01-17 23:51:04 +01:00
committed by GitHub
co-authored by Nadahar
parent 88c9e0b514
commit c5b618ccda
37 changed files with 800 additions and 486 deletions
@@ -35,6 +35,7 @@ This binding has its own IconProvider and makes available the following list of
| Icon Name | Dynamic | Illustration |
| --------------------------- | ------- | ------------------------------------------------- |
| oh:astro:moon_day | Yes | ![Moon Age](doc/images/moon_day.svg) |
| oh:astro:moon_eclipse | Yes | ![Moon Age](doc/images/moon_eclipse.svg) |
| oh:astro:moon_phase | Yes | ![Moon Phase](doc/images/moon_day.svg) |
| oh:astro:season | Yes | ![Season](doc/images/season.svg) |
| oh:astro:sun_eclipse | Yes | ![Sun Eclipse](doc/images/sun_eclipse.svg) |
@@ -0,0 +1,14 @@
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
<circle fill="#66757F" cx="18" cy="18" r="18" />
<g fill="#5B6876">
<circle cx="10.5" cy="8.5" r="3.5" />
<circle cx="20" cy="16" r="3" />
<circle cx="21.5" cy="27.5" r="3.5" />
<circle cx="21" cy="6" r="2" />
<circle cx="3" cy="18" r="1" />
<circle cx="30" cy="9" r="1" />
<circle cx="15" cy="31" r="1" />
<circle cx="32" cy="19" r="2" />
<circle cx="10" cy="23" r="2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 552 B

@@ -52,12 +52,13 @@ 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 MOON_DAY_SET = "moon_day";
private static final String MOON_ECLIPSE_SET = "moon_eclipse";
private static final String MOON_PHASE_SET = "moon_phase";
private static final String SEASON_SET = "season";
private static final String SUN_ECLIPSE_SET = "sun_eclipse";
private static final String ZODIAC_SET = "zodiac";
private static final Set<String> ICON_SETS = Set.of(MOON_DAY_SET, MOON_PHASE_SET, SEASON_SET, SUN_ECLIPSE_SET,
ZODIAC_SET);
private static final Set<String> ICON_SETS = Set.of(MOON_DAY_SET, MOON_ECLIPSE_SET, MOON_PHASE_SET, SEASON_SET,
SUN_ECLIPSE_SET, ZODIAC_SET);
private final Logger logger = LoggerFactory.getLogger(AstroIconProvider.class);
private final TranslationProvider i18nProvider;
@@ -104,7 +105,7 @@ public class AstroIconProvider implements IconProvider {
String iconState = switch (category) {
case SEASON_SET -> SeasonName.valueOf(state).name();
case ZODIAC_SET -> ZodiacSign.valueOf(state).name();
case SUN_ECLIPSE_SET -> EclipseKind.valueOf(state).name();
case MOON_ECLIPSE_SET, SUN_ECLIPSE_SET -> EclipseKind.valueOf(state).name();
case MOON_PHASE_SET -> {
yield Integer.toString(MoonPhaseName.valueOf(state).getAgeDays());
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) 2010-2026 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.util.DateTimeUtils;
/**
* Calculates the eclipses for the astro object
*
* @author Gerhard Riegler - Initial contribution
* @author Gaël L'hopital - Extracted from MoonCalc
*/
@NonNullByDefault
public class AstroCalc {
protected static double varF(double k, double t) {
return 160.7108 + 390.67050274 * k - .0016341 * t * t - .00000227 * t * t * t + .000000011 * t * t * t * t;
}
protected static double varO(double k, double t) {
return 124.7746 - 1.5637558 * k + .0020691 * t * t + .00000215 * t * t * t;
}
protected static double varE(double t) {
return 1 - .002516 * t - .0000074 * t * t;
}
protected static double varM1(double k, double t) {
return 201.5643 + 385.81693528 * k + .1017438 * t * t + .00001239 * t * t * t - .000000058 * t * t * t * t;
}
protected static double varM(double k, double t) {
return 2.5534 + 29.10535669 * k - .0000218 * t * t - .00000011 * t * t * t;
}
protected static double varJde(double k, double t) {
return 2451550.09765 + 29.530588853 * k + .0001337 * t * t - .00000015 * t * t * t
+ .00000000073 * t * t * t * t;
}
protected static double varK(double jd, double tz) {
return ((jd + tz - DateTimeUtils.JD_2000_01_01) / 365.0) * 12.3685;
}
}
@@ -44,7 +44,7 @@ public class CircadianCalc {
// 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 Circadian.NONE;
}
return calculate(calendar, rise, set, noon);
}
@@ -85,7 +85,7 @@ public class CircadianCalc {
long dx = h - x;
if (dx == 0L) {
LOGGER.debug("Degenerate circadian parabola (h == x), returning default values");
return Circadian.DEFAULT;
return Circadian.NONE;
}
double a = (y - k) / (dx * dx);
double percentage = Math.max(Math.min(a * Math.pow(now - h, 2) + k, 100.0), 0.0);
@@ -0,0 +1,106 @@
/*
* Copyright (c) 2010-2026 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.util.MathUtils.*;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.model.EclipseKind;
import org.openhab.binding.astro.internal.model.Position;
import org.openhab.binding.astro.internal.util.DateTimeUtils;
/**
* Calculates the eclipses for the astro object
*
* @author Gerhard Riegler - Initial contribution
* @author Gaël L'hopital - Extracted from MoonCalc
*/
@NonNullByDefault
public abstract class EclipseCalc extends AstroCalc {
public record Eclipse(EclipseKind kind, double when) {
public LocalizedEclipse withPosition(Position position) {
return new LocalizedEclipse(this, position.getElevationAsDouble());
}
}
public record LocalizedEclipse(Eclipse eclipse, double elevation) {
public Instant when() {
return DateTimeUtils.jdToInstant(eclipse.when);
}
public EclipseKind kind() {
return eclipse.kind;
}
public boolean matches(EclipseKind otherKind) {
return eclipse.kind.equals(otherKind);
}
}
public List<Eclipse> getNextEclipses(double startJd) {
return validEclipses().stream().map(ek -> new Eclipse(ek, calculate(startJd, ek))).toList();
}
public double calculate(double midnightJd, EclipseKind eclipse) {
double tz = 0;
double eclipseJd = 0;
do {
double k = varK(midnightJd, tz);
tz += 1;
eclipseJd = getEclipse(Math.floor(k) + getJDAjust(), eclipse);
} while (eclipseJd <= midnightJd);
return eclipseJd;
}
protected abstract double getJDAjust();
protected abstract double astroAdjust(EclipseKind ek, double e, double m, double m1, double g, double u, double jd);
protected abstract Set<EclipseKind> validEclipses();
/**
* Calculates the eclipse.
*/
protected double getEclipse(double kMod, EclipseKind eclipse) {
double t = kMod / 1236.85;
double f = varF(kMod, t);
if (sinDeg(Math.abs(f)) > .36) {
return 0;
}
double o = varO(kMod, t);
double f1 = f - .02665 * sinDeg(o);
double a1 = 299.77 + .107408 * kMod - .009173 * t * t;
double e = varE(t);
double m = varM(kMod, t);
double m1 = varM1(kMod, t);
double p = .207 * e * sinDeg(m) + .0024 * e * sinDeg(2 * m) - .0392 * sinDeg(m1) + .0116 * sinDeg(2 * m1)
- .0073 * e * sinDeg(m1 + m) + .0067 * e * sinDeg(m1 - m) + .0118 * sinDeg(2 * f1);
double q = 5.2207 - .0048 * e * cosDeg(m) + .002 * e * cosDeg(2 * m) - .3299 * cosDeg(m1)
- .006 * e * cosDeg(m1 + m) + .0041 * e * cosDeg(m1 - m);
double g = (p * cosDeg(f1) + q * sinDeg(f1)) * (1 - .0048 * cosDeg(Math.abs(f1)));
double u = .0059 + .0046 * e * cosDeg(m) - .0182 * cosDeg(m1) + .0004 * cosDeg(2 * m1) - .0005 * cosDeg(m + m1);
double jd = varJde(kMod, t);
jd += .0161 * sinDeg(2 * m1) - .0097 * sinDeg(2 * f1) + .0073 * e * sinDeg(m1 - m) - .005 * e * sinDeg(m1 + m)
- .0023 * sinDeg(m1 - 2 * f1) + .0021 * e * sinDeg(2 * m);
jd += .0012 * sinDeg(m1 + 2 * f1) + .0006 * e * sinDeg(2 * m1 + m) - .0004 * sinDeg(3 * m1)
- .0003 * e * sinDeg(m + 2 * f1) + .0003 * sinDeg(a1) - .0002 * e * sinDeg(m - 2 * f1)
- .0002 * e * sinDeg(2 * m1 - m) - .0002 * sinDeg(o);
return astroAdjust(eclipse, e, m, m1, g, u, jd);
}
}
@@ -25,16 +25,15 @@ import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.model.DistanceType;
import org.openhab.binding.astro.internal.model.Eclipse;
import org.openhab.binding.astro.internal.model.EclipseKind;
import org.openhab.binding.astro.internal.model.EclipseType;
import org.openhab.binding.astro.internal.model.EclipseSet;
import org.openhab.binding.astro.internal.model.Moon;
import org.openhab.binding.astro.internal.model.MoonPhase;
import org.openhab.binding.astro.internal.model.MoonPhaseName;
import org.openhab.binding.astro.internal.model.MoonPosition;
import org.openhab.binding.astro.internal.model.Position;
import org.openhab.binding.astro.internal.model.Range;
import org.openhab.binding.astro.internal.util.AstroConstants;
import org.openhab.binding.astro.internal.util.DateTimeUtils;
import org.openhab.binding.astro.internal.util.MathUtils;
/**
* Calculates the phase, eclipse, rise, set, distance, illumination and age of
@@ -47,7 +46,9 @@ import org.openhab.binding.astro.internal.util.DateTimeUtils;
* zodiac based on http://lexikon.astronomie.info/java/sunmoon/
*/
@NonNullByDefault
public class MoonCalc {
public class MoonCalc extends AstroCalc {
private static final double FL = 1.0 - AstroConstants.WGS84_EARTH_FLATTENING;
private static final EclipseCalc ECLIPSE_CALC = new MoonEclipseCalc();
private final InstantSource instantSource;
@@ -66,9 +67,6 @@ public class MoonCalc {
public Moon getMoonInfo(Calendar calendar, double latitude, double longitude, TimeZone zone, Locale locale) {
Moon moon = new Moon();
double julianDate = DateTimeUtils.dateToJulianDate(calendar);
double julianDateMidnight = DateTimeUtils.midnightDateToJulianDate(calendar);
double[] riseSet = getRiseSet(calendar, latitude, longitude);
Calendar rise = DateTimeUtils.timeToCalendar(calendar, riseSet[0]);
Calendar set = DateTimeUtils.timeToCalendar(calendar, riseSet[1]);
@@ -90,17 +88,16 @@ public class MoonCalc {
moon.setSet(new Range(set, set));
MoonPhase phase = moon.getPhase();
double julianDateMidnight = DateTimeUtils.midnightDateToJulianDate(calendar);
phase.remarkablePhases().forEach(mp -> phase.setPhase(mp,
DateTimeUtils.toCalendar(getPhase(calendar, julianDateMidnight, mp, true), zone, locale)));
DateTimeUtils.toCalendar(getPhase(julianDateMidnight, mp, true), zone, locale)));
Eclipse eclipse = moon.getEclipse();
eclipse.getKinds().forEach(eclipseKind -> {
double jdate = getEclipse(calendar, EclipseType.MOON, julianDateMidnight, eclipseKind);
Calendar eclipseDate = DateTimeUtils.toCalendar(jdate, zone, locale);
if (eclipseDate != null) {
eclipse.set(eclipseKind, eclipseDate, Position.NULL);
}
});
double julianDate = DateTimeUtils.dateToJulianDate(calendar);
if (moon.getEclipseSet().needsRecalc(julianDate)) {
moon.setEclipseSet(new EclipseSet(ECLIPSE_CALC.getNextEclipses(julianDate).stream()
.map(eclipse -> eclipse.withPosition(getMoonPosition(eclipse.when(), latitude, longitude)))));
}
Set.of(DistanceType.APOGEE, DistanceType.PERIGEE)
.forEach(type -> moon.setDistance(type, MoonDistanceCalc.get(type, julianDate)));
@@ -113,10 +110,12 @@ public class MoonCalc {
*/
public void setPositionalInfo(Calendar calendar, double latitude, double longitude, Moon moon, TimeZone zone,
Locale locale) {
double julianDate = DateTimeUtils.dateToJulianDate(calendar);
setMoonPhase(calendar, moon, zone, locale);
setAzimuthElevationZodiac(julianDate, latitude, longitude, moon);
double julianDate = DateTimeUtils.dateToJulianDate(calendar);
MoonPosition moonPosition = getMoonPosition(julianDate, latitude, longitude);
moon.setPosition(moonPosition);
moon.setZodiac(ZodiacCalc.calculate(moonPosition.getLongitude(), null));
moon.setDistance(DistanceType.CURRENT, MoonDistanceCalc.calculate(julianDate));
}
@@ -126,7 +125,7 @@ public class MoonCalc {
private void setMoonPhase(Calendar calendar, Moon moon, TimeZone zone, Locale locale) {
MoonPhase phase = moon.getPhase();
double julianDate = DateTimeUtils.dateToJulianDate(calendar);
double parentNewMoon = getPhase(calendar, julianDate, MoonPhaseName.NEW, false);
double parentNewMoon = getPhase(julianDate, MoonPhaseName.NEW, false);
double age = Math.abs(parentNewMoon - julianDate);
Calendar parentNewMoonCal = DateTimeUtils.toCalendar(parentNewMoon, zone, locale);
if (parentNewMoonCal == null) {
@@ -186,8 +185,7 @@ public class MoonCalc {
} else {
utset = hour + quadRet[1];
}
}
if (quadRet[3] == 2) {
} else if (quadRet[3] == 2) {
if (quadRet[0] < 0) {
utrise = hour + quadRet[2];
utset = hour + quadRet[1];
@@ -298,88 +296,17 @@ public class MoonCalc {
return moonCorrection(jd, t, kMod);
}
/**
* Calculates the eclipse.
*/
private double getEclipse(double k, EclipseType typ, EclipseKind eclipse) {
double kMod = Math.floor(k) + ((typ == EclipseType.SUN) ? 0 : 0.5);
double t = kMod / 1236.85;
double f = varF(kMod, t);
double jd = 0;
double ringTest = 0;
if (sinDeg(Math.abs(f)) <= .36) {
double o = varO(kMod, t);
double f1 = f - .02665 * sinDeg(o);
double a1 = 299.77 + .107408 * kMod - .009173 * t * t;
double e = varE(t);
double m = varM(kMod, t);
double m1 = varM1(kMod, t);
double p = .207 * e * sinDeg(m) + .0024 * e * sinDeg(2 * m) - .0392 * sinDeg(m1) + .0116 * sinDeg(2 * m1)
- .0073 * e * sinDeg(m1 + m) + .0067 * e * sinDeg(m1 - m) + .0118 * sinDeg(2 * f1);
double q = 5.2207 - .0048 * e * cosDeg(m) + .002 * e * cosDeg(2 * m) - .3299 * cosDeg(m1)
- .006 * e * cosDeg(m1 + m) + .0041 * e * cosDeg(m1 - m);
double g = (p * cosDeg(f1) + q * sinDeg(f1)) * (1 - .0048 * cosDeg(Math.abs(f1)));
double u = .0059 + .0046 * e * cosDeg(m) - .0182 * cosDeg(m1) + .0004 * cosDeg(2 * m1)
- .0005 * cosDeg(m + m1);
jd = varJde(kMod, t);
jd += (typ == EclipseType.MOON) ? -.4065 * sinDeg(m1) + .1727 * e * sinDeg(m)
: -.4075 * sinDeg(m1) + .1721 * e * sinDeg(m);
jd += .0161 * sinDeg(2 * m1) - .0097 * sinDeg(2 * f1) + .0073 * e * sinDeg(m1 - m)
- .005 * e * sinDeg(m1 + m) - .0023 * sinDeg(m1 - 2 * f1) + .0021 * e * sinDeg(2 * m);
jd += .0012 * sinDeg(m1 + 2 * f1) + .0006 * e * sinDeg(2 * m1 + m) - .0004 * sinDeg(3 * m1)
- .0003 * e * sinDeg(m + 2 * f1) + .0003 * sinDeg(a1) - .0002 * e * sinDeg(m - 2 * f1)
- .0002 * e * sinDeg(2 * m1 - m) - .0002 * sinDeg(o);
switch (typ) {
case MOON:
if ((1.0248 - u - Math.abs(g)) / .545 <= 0) {
jd = 0; // no moon eclipse
}
if (eclipse == EclipseKind.PARTIAL && (1.0128 - u - Math.abs(g)) / .545 > 0
&& (.4678 - u) * (.4678 - u) - g * g > 0) {
jd = 0; // no partial moon eclipse
}
if (eclipse == EclipseKind.TOTAL
&& ((1.0128 - u - Math.abs(g)) / .545 <= 0 != (.4678 - u) * (.4678 - u) - g * g <= 0)) {
jd = 0; // no total moon eclipse
}
break;
case SUN:
if (Math.abs(g) > 1.5433 + u) {
jd = 0; // no sun eclipse
}
if (eclipse == EclipseKind.PARTIAL && ((g >= -.9972 && g <= .9972)
|| (Math.abs(g) >= .9972 && Math.abs(g) < .9972 + Math.abs(u)))) {
jd = 0; // no partial sun eclipse
}
if (eclipse != EclipseKind.PARTIAL) {
if ((g < -.9972 || g > .9972) || (Math.abs(g) < .9972 && Math.abs(g) > .9972 + Math.abs(u))) {
jd = 0; // no ring or total sun eclipse
}
if (u > .0047 || u >= .00464 * Math.sqrt(1 - g * g)) {
ringTest = 1; // no total sun eclipse
}
if (ringTest == 1 && eclipse == EclipseKind.TOTAL) {
jd = 0;
}
if (ringTest == 0 && eclipse == EclipseKind.RING) {
jd = 0;
}
}
break;
}
}
return jd;
}
/**
* Calculates the illumination.
*/
private double getIllumination(double jd) {
double t = (jd - 2451545) / 36525;
double d = 297.8502042 + 445267.11151686 * t - .00163 * t * t + t * t * t / 545868 - t * t * t * t / 113065000;
double m = 357.5291092 + 35999.0502909 * t - .0001536 * t * t + t * t * t / 24490000;
double m1 = 134.9634114 + 477198.8676313 * t + .008997 * t * t + t * t * t / 69699 - t * t * t * t / 14712000;
double t = DateTimeUtils.toJulianCenturies(jd);
double t2 = t * t;
double t3 = t2 * t;
double t4 = t3 * t;
double d = 297.8502042 + 445267.11151686 * t - .00163 * t2 + t3 / 545868 - t4 / 113065000;
double m = AstroConstants.E05_0 + 35999.0502909 * t - .0001536 * t2 + t3 / 24490000;
double m1 = 134.9634114 + 477198.8676313 * t + .008997 * t2 + t3 / 69699 - t4 / 14712000;
double i = 180 - d - 6.289 * sinDeg(m1) + 2.1 * sinDeg(m) - 1.274 * sinDeg(2 * d - m1) - .658 * sinDeg(2 * d)
- .241 * sinDeg(2 * m1) - .110 * sinDeg(d);
return (1 + cosDeg(i)) / 2 * 100.0;
@@ -388,31 +315,17 @@ public class MoonCalc {
/**
* Searches the next moon phase in a given direction
*/
private double getPhase(Calendar cal, double jd, MoonPhaseName phase, boolean forward) {
private double getPhase(double jd, MoonPhaseName phase, boolean forward) {
double tz = 0;
double phaseJd = 0;
do {
double k = varK(cal, tz);
double k = varK(jd, tz);
tz += forward ? 1 : -1;
phaseJd = calcMoonPhase(k, phase);
} while (forward ? phaseJd <= jd : phaseJd > jd);
return phaseJd;
}
/**
* Calculates the next eclipse.
*/
protected double getEclipse(Calendar cal, EclipseType type, double midnightJd, EclipseKind eclipse) {
double tz = 0;
double eclipseJd = 0;
do {
double k = varK(cal, tz);
tz += 1;
eclipseJd = getEclipse(k, type, eclipse);
} while (eclipseJd <= midnightJd);
return eclipseJd;
}
private double[] calcMoon(double t) {
double p2 = 6.283185307;
double arc = 206264.8062;
@@ -450,7 +363,7 @@ public class MoonCalc {
private double sinAlt(double moonJd, int hour, double lambda, double cphi, double sphi) {
double jdo = moonJd + hour / 24.0;
double t = (jdo - 51544.5) / 36525.0;
double t = (jdo - DateTimeUtils.MJD_JD2000) / DateTimeUtils.JULIAN_CENTURY_DAYS;
double[] decra = calcMoon(t);
double tau = 15.0 * (localMeanSiderealTime(jdo, lambda) - decra[1]);
return sphi * sinDeg(decra[0]) + cphi * cosDeg(decra[0]) * cosDeg(tau);
@@ -459,27 +372,18 @@ public class MoonCalc {
private double localMeanSiderealTime(double moonJd, double lambda) {
double moonJdo = Math.floor(moonJd);
double ut = (moonJd - moonJdo) * 24.0;
double t = (moonJdo - 51544.5) / 36525.0;
double t = (moonJdo - DateTimeUtils.MJD_JD2000) / DateTimeUtils.JULIAN_CENTURY_DAYS;
double gmst = 6.697374558 + 1.0027379093 * ut + (8640184.812866 + (.093104 - .0000062 * t) * t) * t / 3600.0;
return 24.0 * frac((gmst - lambda / 15.0) / 24.0);
}
private double frac(double x) {
double ret = x - (int) (x);
if (ret < 0) {
ret += 1;
}
return ret;
}
private double[] quad(double yminus, double yo, double yplus) {
double nz = 0;
double a = .5 * (yminus + yplus) - yo;
double b = .5 * (yplus - yminus);
double c = yo;
double xe = -b / (2 * a);
double ye = (a * xe + b) * xe + c;
double dis = b * b - 4 * a * c;
double ye = (a * xe + b) * xe + yo;
double dis = b * b - 4 * a * yo;
double zero1 = 0;
double zero2 = 0;
if (dis >= 0) {
@@ -499,35 +403,6 @@ public class MoonCalc {
return new double[] { ye, zero1, zero2, nz };
}
private double varO(double k, double t) {
return 124.7746 - 1.5637558 * k + .0020691 * t * t + .00000215 * t * t * t;
}
private double varF(double k, double t) {
return 160.7108 + 390.67050274 * k - .0016341 * t * t - .00000227 * t * t * t + .000000011 * t * t * t * t;
}
private double varM1(double k, double t) {
return 201.5643 + 385.81693528 * k + .1017438 * t * t + .00001239 * t * t * t - .000000058 * t * t * t * t;
}
private double varM(double k, double t) {
return 2.5534 + 29.10535669 * k - .0000218 * t * t - .00000011 * t * t * t;
}
private double varE(double t) {
return 1 - .002516 * t - .0000074 * t * t;
}
private double varJde(double k, double t) {
return 2451550.09765 + 29.530588853 * k + .0001337 * t * t - .00000015 * t * t * t
+ .00000000073 * t * t * t * t;
}
private double varK(Calendar cal, double tz) {
return (cal.get(Calendar.YEAR) + (cal.get(Calendar.DAY_OF_YEAR) + tz) / 365 - 2000) * 12.3685;
}
private double moonCorrection(double jd, double t, double k) {
double ret = jd;
ret += .000325 * sinDeg(299.77 + .107408 * k - .009173 * t * t) + .000165 * sinDeg(251.88 + .016321 * k)
@@ -544,16 +419,17 @@ public class MoonCalc {
/**
* Sets the azimuth, elevation and zodiac in the moon object.
*/
private void setAzimuthElevationZodiac(double julianDate, double latitude, double longitude, Moon moon) {
private MoonPosition getMoonPosition(double julianDate, double latitude, double longitude) {
double lat = Math.toRadians(latitude);
double lon = Math.toRadians(longitude);
double gmst = toGMST(julianDate);
double lmst = toLMST(gmst, lon) * Math.toRadians(15);
double gmst = DateTimeUtils.toGMST(julianDate);
double lmst = DateTimeUtils.toLMST(gmst, lon) * Math.toRadians(15);
double d = julianDate - 2447891.5;
double anomalyMean = Math.toRadians(360) / 365.242191 * d + 4.87650757829735 - 4.935239984568769;
double nu = anomalyMean + Math.toRadians(360.0) / Math.PI * 0.016713 * Math.sin(anomalyMean);
double anomalyMean = MathUtils.TWO_PI / AstroConstants.TROPICAL_YEAR_DAYS * d + 4.87650757829735
- 4.935239984568769;
double nu = anomalyMean + Math.PI * 0.016713 * Math.sin(anomalyMean);
double sunLon = mod2Pi(nu + 4.935239984568769);
double l0 = Math.toRadians(318.351648);
@@ -585,17 +461,7 @@ public class MoonCalc {
double[] raDecTopo = geoEqu2TopoEqu(raDec, distance, lat, lmst);
double[] azAlt = equ2AzAlt(raDecTopo[0], raDecTopo[1], lat, lmst);
moon.setPosition(
new MoonPosition(Math.toDegrees(azAlt[0]), Math.toDegrees(azAlt[1]) + refraction(azAlt[1]), moonLon));
moon.setZodiac(ZodiacCalc.calculate(moonLon, null));
}
private double mod2Pi(double x) {
return (mod(x, 2. * Math.PI));
}
private double mod(double a, double b) {
return (a - Math.floor(a / b) * b);
return new MoonPosition(Math.toDegrees(azAlt[0]), Math.toDegrees(azAlt[1]) + refraction(azAlt[1]), moonLon);
}
/**
@@ -607,11 +473,10 @@ public class MoonCalc {
double sindec = Math.sin(dec);
double lha = lmst - ra;
double coslha = Math.cos(lha);
double sinlha = Math.sin(lha);
double coslat = Math.cos(geolat);
double sinlat = Math.sin(geolat);
double n = -cosdec * sinlha;
double n = -cosdec * Math.sin(lha);
double d = sindec * coslat - cosdec * coslha * sinlat;
double az = mod2Pi(Math.atan2(n, d));
double alt = Math.asin(sindec * sinlat + cosdec * coslha * coslat);
@@ -624,7 +489,7 @@ public class MoonCalc {
* (ra/dec)
*/
private double[] ecl2Equ(double lat, double lon, double jd) {
double t = (jd - 2451545.0) / 36525.0;
double t = DateTimeUtils.toJulianCenturies(jd);
double eps = Math
.toRadians(23. + (26 + 21.45 / 60.) / 60. + t * (-46.815 + t * (-0.0006 + t * 0.00181)) / 3600.);
double coseps = Math.cos(eps);
@@ -643,16 +508,12 @@ public class MoonCalc {
*/
private double[] geoEqu2TopoEqu(double[] raDec, double distance, double observerLat, double lmst) {
double cosdec = Math.cos(raDec[1]);
double sindec = Math.sin(raDec[1]);
double coslst = Math.cos(lmst);
double sinlst = Math.sin(lmst);
double coslat = Math.cos(observerLat);
double sinlat = Math.sin(observerLat);
double rho = getCenterDistance(observerLat);
double x = distance * cosdec * Math.cos(raDec[0]) - rho * coslat * coslst;
double y = distance * cosdec * Math.sin(raDec[0]) - rho * coslat * sinlst;
double z = distance * sindec - rho * sinlat;
double x = distance * cosdec * Math.cos(raDec[0]) - rho * coslat * Math.cos(lmst);
double y = distance * cosdec * Math.sin(raDec[0]) - rho * coslat * Math.sin(lmst);
double z = distance * Math.sin(raDec[1]) - rho * Math.sin(observerLat);
double distanceTopocentric = Math.sqrt(x * x + y * y + z * z);
double raTopo = mod2Pi(Math.atan2(y, x));
@@ -661,37 +522,19 @@ public class MoonCalc {
return new double[] { raTopo, decTopo };
}
/**
* Convert julian date to greenwich mean sidereal time.
*/
private double toGMST(double jd) {
double ut = (jd - 0.5 - Math.floor(jd - 0.5)) * 24.;
double jdMod = Math.floor(jd - 0.5) + 0.5;
double t = (jdMod - 2451545.0) / 36525.0;
double t0 = 6.697374558 + t * (2400.051336 + t * 0.000025862);
return (mod(t0 + ut * 1.002737909, 24.));
}
/**
* Convert greenwich mean sidereal time to local mean sidereal time.
*/
private double toLMST(double gmst, double lon) {
return mod(gmst + Math.toDegrees(lon) / 15., 24.);
}
/**
* Returns geocentric distance from earth center.
*/
private double getCenterDistance(double lat) {
double co = Math.cos(lat);
co = co * co;
double si = Math.sin(lat);
double fl = 1.0 - 1.0 / 298.257223563;
fl = fl * fl;
si = si * si;
double u = 1.0 / Math.sqrt(co * co + fl * si);
double a = 6378.137 * u;
double b = 6378.137 * fl * u;
return Math.sqrt(a * a * co * co + b * b * si);
double fl = FL * FL;
double u = 1.0 / Math.sqrt(co + fl * si);
double a = AstroConstants.EARTH_EQUATORIAL_RADIUS * u;
double b = a * fl;
return Math.sqrt(a * a * co + b * b * si);
}
/**
@@ -0,0 +1,56 @@
/*
* Copyright (c) 2010-2026 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.util.MathUtils.sinDeg;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.model.EclipseKind;
/**
* Adjust the eclipses calculations for the moon
*
* @author Gerhard Riegler - Initial contribution
* @author Gaël L'hopital - Extracted from MoonCalc
*/
@NonNullByDefault
public class MoonEclipseCalc extends EclipseCalc {
@Override
protected double astroAdjust(EclipseKind eclipse, double e, double m, double m1, double g, double u, double jd) {
if ((1.0248 - u - Math.abs(g)) / .545 <= 0) {
return 0; // no moon eclipse
}
double u2 = (.4678 - u) * (.4678 - u) - g * g;
double ug = (1.0128 - u - Math.abs(g)) / .545;
if (EclipseKind.PARTIAL.equals(eclipse) && ug > 0 && u2 > 0) {
return 0; // no partial moon eclipse
}
if (EclipseKind.TOTAL.equals(eclipse) && (ug <= 0 != u2 <= 0)) {
return 0; // no total moon eclipse
}
return jd + -.4065 * sinDeg(m1) + .1727 * e * sinDeg(m);
}
@Override
protected double getJDAjust() {
return 0.5;
}
@Override
protected Set<EclipseKind> validEclipses() {
return Set.of(EclipseKind.PARTIAL, EclipseKind.TOTAL);
}
}
@@ -22,17 +22,18 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.astro.internal.model.Eclipse;
import org.openhab.binding.astro.internal.model.EclipseType;
import org.openhab.binding.astro.internal.model.EclipseSet;
import org.openhab.binding.astro.internal.model.Position;
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.AstroConstants;
import org.openhab.binding.astro.internal.util.DateTimeUtils;
import org.openhab.binding.astro.internal.util.MathUtils;
@@ -56,7 +57,7 @@ public class SunCalc {
private static final double P = Math.toRadians(102.9372);
private static final double E = Math.toRadians(23.45);
private static final double TH0 = Math.toRadians(280.1600);
private static final double TH1 = Math.toRadians(360.9856235);
private static final double TH1 = Math.toRadians(AstroConstants.W_DOT);
private static final double SUN_ANGLE = -0.83;
private static final double SUN_DIAMETER = Math.toRadians(0.53); // sun diameter
private static final double H0 = Math.toRadians(SUN_ANGLE);
@@ -64,7 +65,7 @@ public class SunCalc {
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 int CURVE_TIME_INTERVAL = 20; // 20 minutes
private static final double JD_ONE_MINUTE_FRACTION = 1.0 / 60 / 24;
private static final EclipseCalc ECLIPSE_CALC = new SunEclipseCalc();
private final InstantSource instantSource;
@@ -82,6 +83,11 @@ public class SunCalc {
*/
public void setPositionalInfo(Calendar calendar, double latitude, double longitude, @Nullable Double altitude,
Sun sun) {
Position sunPosition = getPosition(calendar, latitude, longitude, altitude);
sun.setPosition(sunPosition);
}
public Position getPosition(Calendar calendar, double latitude, double longitude, @Nullable Double altitude) {
double lw = Math.toRadians(-longitude);
double phi = Math.toRadians(latitude);
@@ -95,8 +101,7 @@ public class SunCalc {
double azimuth = Math.toDegrees(getAzimuth(th, a, phi, d));
double elevation = Math.toDegrees(getElevation(th, a, phi, d));
sun.setPosition(new Position(azimuth + 180, elevation));
return new Position(azimuth, elevation);
}
/**
@@ -165,7 +170,7 @@ public class SunCalc {
}
sun.setNoon(new Range(DateTimeUtils.toCalendar(jtransit, zone, locale),
DateTimeUtils.toCalendar(jtransit + JD_ONE_MINUTE_FRACTION, zone, locale)));
DateTimeUtils.toCalendar(jtransit + DateTimeUtils.JD_ONE_MINUTE_FRACTION, zone, locale)));
sun.setRise(new Range(DateTimeUtils.toCalendar(jrise, zone, locale),
DateTimeUtils.toCalendar(jriseend, zone, locale)));
sun.setSet(new Range(DateTimeUtils.toCalendar(jsetstart, zone, locale),
@@ -188,7 +193,7 @@ public class SunCalc {
if (sun.getRise().getStart() == null && sun.getRise().getEnd() == null) {
if (isSunUpAllDay) {
daylightRange = new Range(DateTimeUtils.truncateToMidnight(calendar),
DateTimeUtils.truncateToMidnight(addDays(calendar, 1)));
DateTimeUtils.truncateToMidnight(DateTimeUtils.addDays(calendar, 1)));
}
} else {
daylightRange = new Range(sun.getRise().getEnd(), sun.getSet().getStart());
@@ -196,7 +201,7 @@ public class SunCalc {
sun.setDaylight(daylightRange);
// morning night
Sun sunYesterday = getSunInfo(addDays(calendar, -1), latitude, longitude, altitude, true,
Sun sunYesterday = getSunInfo(DateTimeUtils.addDays(calendar, -1), latitude, longitude, altitude, true,
useMeteorologicalSeason, zone, locale);
Range morningNightRange = null;
Range range, range2;
@@ -216,7 +221,8 @@ public class SunCalc {
Range eveningNightRange = null;
if ((range = sun.getAstroDusk()) != null && range.getEnd() != null
&& DateTimeUtils.isSameDay(range.getEnd(), calendar)) {
eveningNightRange = new Range(range.getEnd(), DateTimeUtils.truncateToMidnight(addDays(calendar, 1)));
eveningNightRange = new Range(range.getEnd(),
DateTimeUtils.truncateToMidnight(DateTimeUtils.addDays(calendar, 1)));
} else {
eveningNightRange = new Range();
}
@@ -226,23 +232,20 @@ public class SunCalc {
if (isSunUpAllDay) {
sun.setNight(new Range());
} else {
Sun sunTomorrow = getSunInfo(addDays(calendar, 1), latitude, longitude, altitude, true,
Sun sunTomorrow = getSunInfo(DateTimeUtils.addDays(calendar, 1), latitude, longitude, altitude, true,
useMeteorologicalSeason, zone, locale);
sun.setNight(new Range((range = sun.getAstroDusk()) == null ? null : range.getEnd(),
(range2 = sunTomorrow.getAstroDawn()) == null ? null : range2.getStart()));
}
// eclipse
Eclipse eclipse = sun.getEclipse();
MoonCalc mc = new MoonCalc(instantSource);
eclipse.getKinds().forEach(eclipseKind -> {
double jdate = mc.getEclipse(calendar, EclipseType.SUN, j, eclipseKind);
Calendar eclipseDate = DateTimeUtils.toCalendar(jdate, zone, locale);
if (eclipseDate != null) {
eclipse.set(eclipseKind, eclipseDate, Position.NULL);
}
});
if (sun.getEclipseSet().needsRecalc(j)) {
sun.setEclipseSet(new EclipseSet(ECLIPSE_CALC.getNextEclipses(j).stream().map(eclipse -> {
Calendar eclipseCal = Objects.requireNonNull(DateTimeUtils.toCalendar(eclipse.when(), zone, locale));
return eclipse.withPosition(getPosition(eclipseCal, latitude, longitude, altitude));
})));
}
sun.setZodiac(ZodiacCalc.calculate(lsun, calendar.toInstant()));
@@ -267,15 +270,6 @@ public class SunCalc {
return sun;
}
/**
* Adds the specified days to the calendar.
*/
private Calendar addDays(Calendar calendar, int days) {
Calendar cal = (Calendar) calendar.clone();
cal.add(Calendar.DAY_OF_MONTH, days);
return cal;
}
// all the following methods are translated to java based on the javascript
// calculations of http://www.suncalc.net
private double getJulianCycle(double j, double lw) {
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2010-2026 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.util.MathUtils.sinDeg;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.model.EclipseKind;
/**
* Adjust the eclipses calculations for the sun
*
* @author Gerhard Riegler - Initial contribution
* @author Gaël L'hopital - Extracted from MoonCalc
*/
@NonNullByDefault
public class SunEclipseCalc extends EclipseCalc {
@Override
protected double astroAdjust(EclipseKind eclipse, double e, double m, double m1, double g, double u, double jd) {
if (Math.abs(g) > 1.5433 + u) {
return 0; // no sun eclipse
}
if (!EclipseKind.PARTIAL.equals(eclipse)) {
if ((g < -.9972 || g > .9972) || (Math.abs(g) < .9972 && Math.abs(g) > .9972 + Math.abs(u))) {
return 0; // no ring or total sun eclipse
}
double ringTest = u > .0047 || u >= .00464 * Math.sqrt(1 - g * g) ? 1 : 0;
if (ringTest == 1 && EclipseKind.TOTAL.equals(eclipse)) {
return 0;
}
if (ringTest == 0 && EclipseKind.RING.equals(eclipse)) {
return 0;
}
} else if ((g >= -.9972 && g <= .9972) || (Math.abs(g) >= .9972 && Math.abs(g) < .9972 + Math.abs(u))) {
return 0; // no partial sun eclipse
}
return jd + -.4075 * sinDeg(m1) + .1721 * e * sinDeg(m);
}
@Override
protected double getJDAjust() {
return 0;
}
@Override
protected Set<EclipseKind> validEclipses() {
return Set.of(EclipseKind.PARTIAL, EclipseKind.TOTAL, EclipseKind.RING);
}
}
@@ -52,7 +52,7 @@ public class ZodiacCalc {
return new Zodiac(index, start, end);
} catch (IllegalArgumentException e) {
LOGGER.warn("Error defining Zodiac: {}", e.getMessage());
return Zodiac.NULL;
return Zodiac.NONE;
}
}
@@ -420,16 +420,14 @@ public abstract class AstroThingHandler extends BaseThingHandler {
*/
protected abstract Job getDailyJob(TimeZone zone, Locale locale);
public abstract @Nullable Position getPositionAt(ZonedDateTime date);
public abstract Position getPositionAt(ZonedDateTime date);
public State getAzimuth(ZonedDateTime date) {
Position position = getPositionAt(date);
return position != null ? position.getAzimuth() : UnDefType.NULL;
return getPositionAt(date).getAzimuth();
}
public State getElevation(ZonedDateTime date) {
Position position = getPositionAt(date);
return position != null ? position.getElevation() : UnDefType.NULL;
return getPositionAt(date).getElevation();
}
@Override
@@ -26,6 +26,8 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.astro.internal.calc.MoonCalc;
import org.openhab.binding.astro.internal.job.DailyJobMoon;
import org.openhab.binding.astro.internal.job.Job;
import org.openhab.binding.astro.internal.model.DistanceType;
import org.openhab.binding.astro.internal.model.EclipseKind;
import org.openhab.binding.astro.internal.model.Moon;
import org.openhab.binding.astro.internal.model.Planet;
import org.openhab.binding.astro.internal.model.Position;
@@ -75,8 +77,6 @@ public class MoonHandler extends AstroThingHandler {
Double longitude = thingConfig.longitude;
moonCalc.setPositionalInfo(DateTimeUtils.calFromInstantSource(instantSource, zone, locale),
latitude != null ? latitude : 0, longitude != null ? longitude : 0, moon, zone, locale);
moon.getEclipse().setElevations(this, timeZoneProvider);
this.moon = moon;
publishPlanet();
@@ -131,25 +131,25 @@ public class MoonHandler extends AstroThingHandler {
case CHANNEL_ID_MOON_PHASE_NAME:
return toState(moon.getPhase().getName(), channel);
case CHANNEL_ID_MOON_ECLIPSE_TOTAL:
return toState(moon.getEclipse().getTotal(), channel);
return toState(moon.getEclipseSet().getDate(EclipseKind.TOTAL), channel);
case CHANNEL_ID_MOON_ECLIPSE_TOTAL_ELEVATION:
return toState(moon.getEclipse().getTotalElevation(), channel);
return toState(moon.getEclipseSet().getElevation(EclipseKind.TOTAL), channel);
case CHANNEL_ID_MOON_ECLIPSE_PARTIAL:
return toState(moon.getEclipse().getPartial(), channel);
return toState(moon.getEclipseSet().getDate(EclipseKind.PARTIAL), channel);
case CHANNEL_ID_MOON_ECLIPSE_PARTIAL_ELEVATION:
return toState(moon.getEclipse().getPartialElevation(), channel);
return toState(moon.getEclipseSet().getElevation(EclipseKind.PARTIAL), channel);
case CHANNEL_ID_MOON_DISTANCE_DATE:
return toState(moon.getDistance().getDate(), channel);
return toState(moon.getDistanceType(DistanceType.CURRENT).getDate(), channel);
case CHANNEL_ID_MOON_DISTANCE_DISTANCE:
return toState(moon.getDistance().getDistance(), channel);
return toState(moon.getDistanceType(DistanceType.CURRENT).getDistance(), channel);
case CHANNEL_ID_MOON_PERIGEE_DATE:
return toState(moon.getPerigee().getDate(), channel);
return toState(moon.getDistanceType(DistanceType.PERIGEE).getDate(), channel);
case CHANNEL_ID_MOON_PERIGEE_DISTANCE:
return toState(moon.getPerigee().getDistance(), channel);
return toState(moon.getDistanceType(DistanceType.PERIGEE).getDistance(), channel);
case CHANNEL_ID_MOON_APOGEE_DATE:
return toState(moon.getApogee().getDate(), channel);
return toState(moon.getDistanceType(DistanceType.APOGEE).getDate(), channel);
case CHANNEL_ID_MOON_APOGEE_DISTANCE:
return toState(moon.getApogee().getDistance(), channel);
return toState(moon.getDistanceType(DistanceType.APOGEE).getDistance(), channel);
case CHANNEL_ID_MOON_POSITION_AZIMUTH:
return toState(moon.getPosition().getAzimuth(), channel);
case CHANNEL_ID_MOON_POSITION_ELEVATION:
@@ -183,7 +183,7 @@ public class MoonHandler extends AstroThingHandler {
}
@Override
public @Nullable Position getPositionAt(ZonedDateTime date) {
public Position getPositionAt(ZonedDateTime date) {
Moon localMoon = getMoonAt(date, Locale.ROOT);
Double latitude = thingConfig.latitude;
Double longitude = thingConfig.longitude;
@@ -29,6 +29,7 @@ import org.openhab.binding.astro.internal.calc.RadiationCalc;
import org.openhab.binding.astro.internal.calc.SunCalc;
import org.openhab.binding.astro.internal.job.DailyJobSun;
import org.openhab.binding.astro.internal.job.Job;
import org.openhab.binding.astro.internal.model.EclipseKind;
import org.openhab.binding.astro.internal.model.Planet;
import org.openhab.binding.astro.internal.model.Position;
import org.openhab.binding.astro.internal.model.Radiation;
@@ -85,8 +86,6 @@ public class SunHandler extends AstroThingHandler {
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()));
sun.setRadiation(RadiationCalc.calculate(now, sun.getPosition().getElevationAsDouble(), altitude));
@@ -266,17 +265,17 @@ public class SunHandler extends AstroThingHandler {
s = sun.getSeason();
return s == null ? UnDefType.UNDEF : toState(s.getTimeLeft(), channel);
case CHANNEL_ID_SUN_ECLIPSE_TOTAL:
return toState(sun.getEclipse().getTotal(), channel);
return toState(sun.getEclipseSet().getDate(EclipseKind.TOTAL), channel);
case CHANNEL_ID_SUN_ECLIPSE_TOTAL_ELEVATION:
return toState(sun.getEclipse().getTotalElevation(), channel);
return toState(sun.getEclipseSet().getElevation(EclipseKind.TOTAL), channel);
case CHANNEL_ID_SUN_ECLIPSE_PARTIAL:
return toState(sun.getEclipse().getPartial(), channel);
return toState(sun.getEclipseSet().getDate(EclipseKind.PARTIAL), channel);
case CHANNEL_ID_SUN_ECLIPSE_PARTIAL_ELEVATION:
return toState(sun.getEclipse().getPartialElevation(), channel);
return toState(sun.getEclipseSet().getElevation(EclipseKind.PARTIAL), channel);
case CHANNEL_ID_SUN_ECLIPSE_RING:
return toState(sun.getEclipse().getRing(), channel);
return toState(sun.getEclipseSet().getDate(EclipseKind.RING), channel);
case CHANNEL_ID_SUN_ECLIPSE_RING_ELEVATION:
return toState(sun.getEclipse().getRingElevation(), channel);
return toState(sun.getEclipseSet().getElevation(EclipseKind.RING), channel);
case CHANNEL_ID_SUN_PHASE_NAME:
return toState(sun.getPhase().getName(), channel);
case CHANNEL_ID_SUN_CIRCADIAN_BRIGHTNESS:
@@ -330,9 +329,8 @@ public class SunHandler extends AstroThingHandler {
}
@Override
public @Nullable Position getPositionAt(ZonedDateTime date) {
Sun localSun = getPositionedSunAt(date);
return localSun.getPosition();
public Position getPositionAt(ZonedDateTime date) {
return getPositionedSunAt(date).getPosition();
}
public @Nullable Radiation getRadiationAt(ZonedDateTime date) {
@@ -24,7 +24,6 @@ import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.handler.AstroThingHandler;
import org.openhab.binding.astro.internal.model.DistanceType;
import org.openhab.binding.astro.internal.model.Eclipse;
import org.openhab.binding.astro.internal.model.Moon;
import org.openhab.binding.astro.internal.model.MoonPhase;
import org.openhab.binding.astro.internal.model.Planet;
@@ -96,13 +95,9 @@ public final class DailyJobMoon extends AbstractJob {
scheduleEvent(handler, cal, EVENT_PHASE_NEW, EVENT_CHANNEL_ID_MOON_PHASE, false, zone, locale);
}
Eclipse eclipse = moon.getEclipse();
eclipse.getKinds().forEach(eclipseKind -> {
Calendar eclipseDate = eclipse.getDate(eclipseKind);
if (eclipseDate != null) {
scheduleEvent(handler, eclipseDate, eclipseKind.toString(), EVENT_CHANNEL_ID_ECLIPSE, false, zone,
locale);
}
moon.getEclipseSet().getEclipses().forEach(eclipse -> {
scheduleEvent(handler, eclipse.when(), eclipse.kind().toString(), EVENT_CHANNEL_ID_ECLIPSE, false,
zone.toZoneId());
});
Set.of(DistanceType.APOGEE, DistanceType.PERIGEE).forEach(type -> {
@@ -24,7 +24,6 @@ import java.util.TimeZone;
import org.eclipse.jdt.annotation.NonNullByDefault;
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;
@@ -121,13 +120,9 @@ public final class DailyJobSun extends AbstractJob {
scheduleRange(handler, range, EVENT_CHANNEL_ID_DAYLIGHT, zone, locale, instantSource);
}
Eclipse eclipse = sun.getEclipse();
eclipse.getKinds().forEach(eclipseKind -> {
Calendar eclipseDate = eclipse.getDate(eclipseKind);
if (eclipseDate != null) {
scheduleEvent(handler, eclipseDate, eclipseKind.toString(), EVENT_CHANNEL_ID_ECLIPSE, false, zone,
locale);
}
sun.getEclipseSet().getEclipses().forEach(eclipse -> {
scheduleEvent(handler, eclipse.when(), eclipse.kind().toString(), EVENT_CHANNEL_ID_ECLIPSE, false,
zone.toZoneId());
});
// schedule republish jobs
@@ -28,7 +28,7 @@ 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 static final Circadian NONE = new Circadian(0, MIN_COLOR_TEMP);
public Circadian {
if (brightness < 0 || brightness > 100) {
@@ -1,112 +0,0 @@
/*
* Copyright (c) 2010-2026 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 java.util.AbstractMap.SimpleEntry;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.astro.internal.handler.AstroThingHandler;
import org.openhab.core.i18n.TimeZoneProvider;
/**
* Holds eclipse informations.
*
* @author Gerhard Riegler - Initial contribution
*/
@NonNullByDefault
public class Eclipse {
private final Map<EclipseKind, @Nullable Entry<Calendar, @Nullable Double>> entries = new HashMap<>();
public Eclipse(EclipseKind... eclipses) {
for (EclipseKind eclipseKind : eclipses) {
entries.put(eclipseKind, null);
}
}
public Set<EclipseKind> getKinds() {
return entries.keySet();
}
/**
* Returns the date of the next total eclipse.
*/
public @Nullable Calendar getTotal() {
return getDate(EclipseKind.TOTAL);
}
/**
* Returns the date of the next partial eclipse.
*/
public @Nullable Calendar getPartial() {
return getDate(EclipseKind.PARTIAL);
}
/**
* Returns the date of the next ring eclipse.
*/
public @Nullable Calendar getRing() {
return getDate(EclipseKind.RING);
}
/**
* Returns the elevation of the next total eclipse.
*/
public @Nullable Double getTotalElevation() {
return getElevation(EclipseKind.TOTAL);
}
/**
* Returns the elevation of the next partial eclipse.
*/
public @Nullable Double getPartialElevation() {
return getElevation(EclipseKind.PARTIAL);
}
/**
* Returns the elevation of the next ring eclipse.
*/
public @Nullable Double getRingElevation() {
return getElevation(EclipseKind.RING);
}
public @Nullable Calendar getDate(EclipseKind eclipseKind) {
Entry<Calendar, @Nullable Double> entry = entries.get(eclipseKind);
return entry != null ? entry.getKey() : null;
}
private @Nullable Double getElevation(EclipseKind eclipseKind) {
Entry<Calendar, @Nullable Double> entry = entries.get(eclipseKind);
return entry != null ? entry.getValue() : null;
}
public void set(EclipseKind eclipseKind, Calendar eclipseDate, @Nullable Position position) {
entries.put(eclipseKind, new SimpleEntry<Calendar, @Nullable Double>(eclipseDate,
position != null ? position.getElevationAsDouble() : null));
}
public void setElevations(AstroThingHandler astroHandler, TimeZoneProvider timeZoneProvider) {
getKinds().forEach(eclipseKind -> {
Calendar eclipseDate = getDate(eclipseKind);
if (eclipseDate != null) {
set(eclipseKind, eclipseDate,
astroHandler.getPositionAt(eclipseDate.toInstant().atZone(timeZoneProvider.getTimeZone())));
}
});
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2010-2026 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 java.time.Instant;
import java.util.List;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.astro.internal.calc.EclipseCalc.LocalizedEclipse;
import org.openhab.binding.astro.internal.util.DateTimeUtils;
/**
* Holds eclipse information.
*
* @author Gerhard Riegler - Initial contribution
* @author Gaël L'hopital - Immutable and Instant
*/
@NonNullByDefault
public class EclipseSet {
public static final EclipseSet NONE = new EclipseSet();
private final List<LocalizedEclipse> eclipses;
private EclipseSet() {
this.eclipses = List.of();
}
public EclipseSet(Stream<LocalizedEclipse> eclipseDatas) {
this.eclipses = eclipseDatas.toList();
}
public boolean needsRecalc(double jdNow) {
Instant now = DateTimeUtils.jdToInstant(jdNow);
return eclipses.isEmpty() || eclipses.stream().map(LocalizedEclipse::when).anyMatch(when -> when.isBefore(now));
}
public Stream<LocalizedEclipse> getEclipses() {
return eclipses.stream();
}
private LocalizedEclipse internalGet(EclipseKind eclipseKind) {
return getEclipses().filter(ed -> ed.matches(eclipseKind)).findFirst().orElseThrow(
() -> new IllegalArgumentException("This EclipseSet does not contain %s".formatted(eclipseKind)));
}
public Instant getDate(EclipseKind eclipseKind) {
return internalGet(eclipseKind).when();
}
public Double getElevation(EclipseKind eclipseKind) {
return internalGet(eclipseKind).elevation();
}
}
@@ -1,26 +0,0 @@
/*
* Copyright (c) 2010-2026 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;
/**
* Astro objects susceptible of being eclipsed.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum EclipseType {
SUN,
MOON
}
@@ -28,13 +28,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class Moon extends RiseSet implements Planet {
private final Map<DistanceType, MoonDistance> distances = new HashMap<>(DistanceType.values().length);
private EclipseSet eclipseSet = EclipseSet.NONE;
private MoonPhase phase = new MoonPhase();
private Eclipse eclipse = new Eclipse(EclipseKind.PARTIAL, EclipseKind.TOTAL);
private Position position = MoonPosition.NULL;
private Zodiac zodiac = Zodiac.NULL;
private Position position = MoonPosition.NONE;
private Zodiac zodiac = Zodiac.NONE;
public Moon() {
EnumSet.allOf(DistanceType.class).forEach(d -> distances.put(d, MoonDistance.NULL));
EnumSet.allOf(DistanceType.class).forEach(d -> distances.put(d, MoonDistance.NONE));
}
/**
@@ -55,20 +55,6 @@ public class Moon extends RiseSet implements Planet {
return Objects.requireNonNull(distances.get(type));
}
/**
* Returns the apogee.
*/
public MoonDistance getApogee() {
return getDistanceType(DistanceType.APOGEE);
}
/**
* Returns the perigee.
*/
public MoonDistance getPerigee() {
return getDistanceType(DistanceType.PERIGEE);
}
public void setDistance(DistanceType type, MoonDistance moonDistance) {
distances.put(type, moonDistance);
}
@@ -76,22 +62,12 @@ public class Moon extends RiseSet implements Planet {
/**
* Returns the eclipses.
*/
public Eclipse getEclipse() {
return eclipse;
public EclipseSet getEclipseSet() {
return eclipseSet;
}
/**
* Sets the eclipses.
*/
public void setEclipse(Eclipse eclipse) {
this.eclipse = eclipse;
}
/**
* Returns the current distance.
*/
public MoonDistance getDistance() {
return getDistanceType(DistanceType.CURRENT);
public void setEclipseSet(EclipseSet eclipseSet) {
this.eclipseSet = eclipseSet;
}
/**
@@ -32,7 +32,7 @@ import org.openhab.core.library.types.QuantityType;
*/
@NonNullByDefault
public class MoonDistance {
public static final MoonDistance NULL = new MoonDistance();
public static final MoonDistance NONE = new MoonDistance();
private final @Nullable Instant date;
private final double distance;
@@ -25,6 +25,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* Holds the calculates moon phase informations.
@@ -118,8 +120,9 @@ public class MoonPhase {
/**
* Returns the illumination.
*/
public QuantityType<Dimensionless> getIllumination() {
return new QuantityType<>(illumination, Units.PERCENT);
public State getIllumination() {
return Double.isNaN(illumination) ? UnDefType.UNDEF
: illumination < 0 ? UnDefType.NULL : new QuantityType<>(illumination, Units.PERCENT);
}
/**
@@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@NonNullByDefault
public class MoonPosition extends Position {
public static final Position NULL = new MoonPosition();
public static final MoonPosition NONE = new MoonPosition();
private final double longitude;
private MoonPosition() {
@@ -29,7 +29,7 @@ import org.openhab.core.types.UnDefType;
*/
@NonNullByDefault
public class Position {
public static final Position NULL = new Position();
public static final Position NONE = new Position();
private final double azimuth;
private final double elevation;
@@ -28,7 +28,7 @@ import org.openhab.core.types.UnDefType;
*/
@NonNullByDefault
public class Radiation {
public static final Radiation NULL = new Radiation();
public static final Radiation NONE = new Radiation();
private final double direct;
private final double diffuse;
@@ -28,18 +28,16 @@ public class Sun extends RiseSet implements Planet {
private Map<SunPhaseName, Range> ranges = new HashMap<>();
private Position position = Position.NULL;
private Zodiac zodiac = Zodiac.NULL;
private Position position = Position.NONE;
private Zodiac zodiac = Zodiac.NONE;
private EclipseSet eclipseSet = EclipseSet.NONE;
private Radiation radiation = Radiation.NONE;
private @Nullable Season season = null;
private Eclipse eclipse = new Eclipse(EclipseKind.PARTIAL, EclipseKind.TOTAL, EclipseKind.RING);
private Radiation radiation = Radiation.NULL;
private SunPhase phase = new SunPhase();
private Circadian circadian = Circadian.DEFAULT;
private Circadian circadian = Circadian.NONE;
/**
* Returns the astro dawn range.
@@ -281,15 +279,12 @@ public class Sun extends RiseSet implements Planet {
/**
* Returns the eclipses.
*/
public Eclipse getEclipse() {
return eclipse;
public EclipseSet getEclipseSet() {
return eclipseSet;
}
/**
* Sets the eclipses.
*/
public void setEclipse(Eclipse eclipse) {
this.eclipse = eclipse;
public void setEclipseSet(EclipseSet eclipseSet) {
this.eclipseSet = eclipseSet;
}
/**
@@ -24,7 +24,7 @@ import org.eclipse.jdt.annotation.Nullable;
*/
@NonNullByDefault
public class Zodiac {
public static final Zodiac NULL = new Zodiac();
public static final Zodiac NONE = new Zodiac();
private final @Nullable ZodiacSign sign;
private final @Nullable Instant start;
@@ -27,10 +27,17 @@ public class AstroConstants {
public static final double TROPICAL_YEAR_SECONDS = TROPICAL_YEAR_DAYS * SECONDS_PER_DAY;
public static final double SOLAR_MEAN_MOTION_PER_SECOND = MathUtils.TWO_PI / TROPICAL_YEAR_SECONDS;
public static final double LUNAR_SYNODIC_MONTH_DAYS = 29.530588853;
public static final double EARTH_EQUATORIAL_RADIUS = 6378.137; // WGS-84 reference in km
/** Constant term of the E5 angle. */
public static final double E05_0 = 357.52910918;
/** Earth flattening from WGS84 model: 1.0 / 298.257223563. */
public static final double WGS84_EARTH_FLATTENING = 1.0 / 298.257223563;
/** Rate term of the prime meridian. */
public static final double W_DOT = 360.9856235;
/** Constructor */
private AstroConstants() {
throw new IllegalAccessError("Non-instantiable");
@@ -12,6 +12,8 @@
*/
package org.openhab.binding.astro.internal.util;
import static org.openhab.binding.astro.internal.util.MathUtils.mod;
import java.time.Instant;
import java.time.InstantSource;
import java.time.ZonedDateTime;
@@ -38,12 +40,33 @@ public class DateTimeUtils {
private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeUtils.class);
private static final Pattern HHMM_PATTERN = Pattern.compile("^([0-1][0-9]|2[0-3])(:[0-5][0-9])$");
public static final double JD_2000_01_01 = 2451544.5; // JD on January 1st 2000 00:00 UTC
public static final double MJD_JD2000 = 51544.5;
public static final double JD_J2000 = 2451545.0; // 2000-01-01 12:00
public static final double JD_UNIX_EPOCH = 2440587.5; // 1970-01-01 00:00 UTC
public static final int JULIAN_CENTURY_DAYS = 36525; // Length of a Julian Century in days
public static final double JD_ONE_MINUTE_FRACTION = 1.0 / 60 / 24;
private static final double J1970 = JD_UNIX_EPOCH + 0.5; // 1970-01-01 12:00 UTC (julian solar noon)
private static final int JULIAN_CENTURY_DAYS = 36525; // Length of a Julian Century in days
private static final double SECONDS_PER_DAY = 60 * 60 * 24;
/**
* Convert julian date to greenwich mean sidereal time.
*/
public static double toGMST(double jd) {
double ut = (jd - 0.5 - Math.floor(jd - 0.5)) * 24.;
double jdMod = Math.floor(jd - 0.5) + 0.5;
double t = toJulianCenturies(jdMod);
double t0 = 6.697374558 + t * (2400.051336 + t * 0.000025862);
return mod(t0 + ut * 1.002737909, 24.);
}
/**
* Convert greenwich mean sidereal time to local mean sidereal time.
*/
public static double toLMST(double gmst, double lon) {
return mod(gmst + Math.toDegrees(lon) / 15., 24.);
}
/** Constructor */
private DateTimeUtils() {
throw new IllegalAccessError("Non-instantiable");
@@ -468,4 +491,13 @@ public class DateTimeUtils {
result.setTimeInMillis(instantSource.millis());
return result;
}
/**
* Adds the specified days to the calendar.
*/
public static Calendar addDays(Calendar calendar, int days) {
Calendar cal = (Calendar) calendar.clone();
cal.add(Calendar.DAY_OF_MONTH, days);
return cal;
}
}
@@ -56,4 +56,8 @@ public class MathUtils {
public static double sinDeg(double deg) {
return Math.sin(Math.toRadians(deg));
}
public static double frac(double x) {
return x - Math.floor(x);
}
}
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="64"
width="64"
viewBox="0 0 36 36"
version="1.1"
id="svg10"
sodipodi:docname="moon_phase-waning_crescent.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="2.2428543"
inkscape:cx="28.089207"
inkscape:cy="63.312181"
inkscape:window-width="1920"
inkscape:window-height="1043"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg10" />
<circle
fill="#ffd983"
cx="24.587683"
cy="-6.5899482"
r="18"
id="circle1"
transform="rotate(60)" />
<path
fill="#66757f"
d="M 27.000905,33.58704 C 18.391745,38.557542 7.3829463,35.607743 2.4124465,26.998585 2.2654499,26.743973 2.1475615,26.48179 2.0146857,26.223641 1.2896746,19.839887 5.2851812,12.638305 12.500905,8.4723053 19.715762,4.3068039 27.951133,4.4468867 33.116256,8.267141 c 0.157125,0.2441477 0.326106,0.4768313 0.473105,0.7314425 4.970501,8.6091585 2.020702,19.6179575 -6.588456,24.5884565 z"
id="path1"
style="fill:#ff792e;fill-opacity:1" />
<circle
fill="#ffcc4d"
cx="12.587683"
cy="-15.589949"
r="1"
id="circle7"
transform="rotate(60)" />
<circle
fill="#ffcc4d"
cx="10.587683"
cy="-5.5899482"
r="2"
id="circle9"
transform="rotate(60)" />
<g
id="g10"
style="fill:#756b4d;fill-opacity:1">
<circle
fill="#5b6876"
cx="32.087685"
cy="-16.089949"
r="3.5"
id="circle2"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
<circle
fill="#5b6876"
cx="22.587683"
cy="-8.5899487"
r="3"
id="circle3"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
<circle
fill="#5b6876"
cx="21.087683"
cy="2.9100547"
r="3.5"
id="circle4"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
<circle
fill="#5b6876"
cx="21.587683"
cy="-18.589949"
r="2"
id="circle5"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
<circle
fill="#5b6876"
cx="39.587681"
cy="-6.5899482"
r="1"
id="circle6"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
<circle
fill="#5b6876"
cx="27.587683"
cy="6.4100528"
r="1"
id="circle8"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
<circle
fill="#5b6876"
cx="32.587685"
cy="-1.5899471"
r="2"
id="circle10"
transform="rotate(60)"
style="fill:#756b4d;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="64"
width="64"
viewBox="0 0 36 36"
version="1.1"
id="svg10"
sodipodi:docname="moon_phase-full.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs10" />
<sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="8.9714173"
inkscape:cx="27.866277"
inkscape:cy="29.203858"
inkscape:window-width="1920"
inkscape:window-height="1043"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg10" />
<circle
fill="#FFD983"
cx="18"
cy="18"
r="18"
id="circle1"
style="fill:#ff7830;fill-opacity:1" />
<g
fill="#FFCC4D"
id="g10"
style="fill:#756a4d;fill-opacity:1">
<circle
cx="10.5"
cy="8.5"
r="3.5"
id="circle2"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="20"
cy="17"
r="3"
id="circle3"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="24.5"
cy="28.5"
r="3.5"
id="circle4"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="22"
cy="5"
r="2"
id="circle5"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="3"
cy="18"
r="1"
id="circle6"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="30"
cy="9"
r="1"
id="circle7"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="15"
cy="31"
r="1"
id="circle8"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="32"
cy="19"
r="2"
id="circle9"
style="fill:#756a4d;fill-opacity:1" />
<circle
cx="10"
cy="23"
r="2"
id="circle10"
style="fill:#756a4d;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,14 @@
<svg height="64" width="64" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36">
<circle fill="#66757F" cx="18" cy="18" r="18" />
<g fill="#5B6876">
<circle cx="10.5" cy="8.5" r="3.5" />
<circle cx="20" cy="16" r="3" />
<circle cx="21.5" cy="27.5" r="3.5" />
<circle cx="21" cy="6" r="2" />
<circle cx="3" cy="18" r="1" />
<circle cx="30" cy="9" r="1" />
<circle cx="15" cy="31" r="1" />
<circle cx="32" cy="19" r="2" />
<circle cx="10" cy="23" r="2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 552 B

@@ -68,7 +68,7 @@ public class CircadianCalcTest {
assertNotEquals(CircadianCalc.calculate(now, sunrise, sunrise, noon), actual);
actual = CircadianCalc.calculate(now, new Range(), new Range(), null);
assertEquals(Circadian.DEFAULT, actual);
assertEquals(Circadian.NONE, actual);
}
private static Calendar newCalendar(int year, int month, int day, int hour, int minute) {
@@ -26,6 +26,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openhab.binding.astro.internal.model.DistanceType;
import org.openhab.binding.astro.internal.model.Moon;
import org.openhab.binding.astro.internal.util.DateTimeUtils;
@@ -64,11 +65,11 @@ public class MoonCalcTest {
Moon moon = Objects.requireNonNull(moonCalc).getMoonInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE,
TIME_ZONE, Locale.ROOT);
assertNotNull(moon.getApogee());
assertNotNull(moon.getPerigee());
assertNotNull(moon.getDistanceType(DistanceType.APOGEE));
assertNotNull(moon.getDistanceType(DistanceType.PERIGEE));
assertNotNull(moon.getDistanceType(DistanceType.CURRENT));
assertNotNull(moon.getDistance());
assertNotNull(moon.getEclipse());
assertNotNull(moon.getEclipseSet());
assertNotNull(moon.getPhase());
assertNotNull(moon.getPosition());
@@ -111,6 +112,11 @@ public class MoonCalcTest {
Moon moon = moonCalc.getMoonInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, TIME_ZONE, Locale.ROOT);
moonCalc.setPositionalInfo(FEB_27_2019, AMSTERDAM_LATITUDE, AMSTERDAM_LONGITUDE, moon, TIME_ZONE, Locale.ROOT);
var azimuth = moon.getPosition().getAzimuth();
var elevation = moon.getPosition().getElevation();
assertNotNull(azimuth);
assertNotNull(elevation);
// expected result from heavens-above.com is Azimuth: 100.5, altitude -17
assertEquals(100.5, moon.getPosition().getAzimuthAsDouble(), ACCURACY_IN_DEGREE);
assertEquals(-17, moon.getPosition().getElevationAsDouble(), ACCURACY_IN_DEGREE);
@@ -147,6 +147,21 @@ public final class ParametrizedStateTestCases {
new DateTimeType("2016-02-29T23:00:00+03:00"), BAGHDAD_ZONE, SHAYBAH_LOC),
new StateTestCase(TEST_MOON_THING_ID, "rise#duration", LEAP_DAY_2016_SA,
new QuantityType<>(0, Units.MINUTE), BAGHDAD_ZONE, SHAYBAH_LOC),
// Total Solar Eclipse on August 21, 2017
new StateTestCase(TEST_SUN_THING_ID, "eclipse#total", Instant.parse("2017-01-01T00:00:00Z"),
new DateTimeType("2017-08-21T18:27:10+00:00"), BAGHDAD_ZONE, SHAYBAH_LOC),
// Partial Solar Eclipse on August 11, 2018
new StateTestCase(TEST_SUN_THING_ID, "eclipse#partial", Instant.parse("2018-07-15T00:00:00Z"),
new DateTimeType("2018-08-11T11:47:57+02:00"), BAGHDAD_ZONE, SHAYBAH_LOC),
// Ring Solar Eclipse on December 26, 2019
new StateTestCase(TEST_SUN_THING_ID, "eclipse#ring", Instant.parse("2019-11-01T00:00:00Z"),
new DateTimeType("2019-12-26T06:19:23+01:00"), BAGHDAD_ZONE, SHAYBAH_LOC),
// Total Lunar Eclipse on January 21, 2019
new StateTestCase(TEST_MOON_THING_ID, "eclipse#total", Instant.parse("2018-12-01T00:00:00Z"),
new DateTimeType("2019-01-21T05:13:02+00:00"), BAGHDAD_ZONE, SHAYBAH_LOC),
// Partial Lunar Eclipse on July 16, 2019
new StateTestCase(TEST_MOON_THING_ID, "eclipse#partial", Instant.parse("2019-06-01T00:00:00Z"),
new DateTimeType("2019-07-16T23:31:59+02:00"), BAGHDAD_ZONE, SHAYBAH_LOC),
new StateTestCase(TEST_SUN_THING_ID, CHANNEL_ID_SUN_CIRCADIAN_BRIGHTNESS, MARCH_16_2022_UA,
new PercentType(93), KYIV_ZONE, MARIUPOL_LOC),
new StateTestCase(TEST_SUN_THING_ID, CHANNEL_ID_SUN_CIRCADIAN_TEMPERATURE, MARCH_16_2022_UA,