ESPHome 2026.6.0-dev
Loading...
Searching...
No Matches
sun.cpp
Go to the documentation of this file.
1#include "sun.h"
2#include "esphome/core/log.h"
3#include <numbers>
4
5/*
6The formulas/algorithms in this module are based on the book
7"Astronomical algorithms" by Jean Meeus (2nd edition)
8
9The target accuracy of this implementation is ~1min for sunrise/sunset calculations,
10and 6 arcminutes for elevation/azimuth. As such, some of the advanced correction factors
11like exact nutation are not included. But in some testing the accuracy appears to be within range
12for random spots around the globe.
13*/
14
15namespace esphome::sun {
16
17using namespace esphome::sun::internal;
18
19static const char *const TAG = "sun";
20
21#undef degrees
22#undef radians
23#undef sq
24
25inline num_t degrees(num_t rad) { return rad * 180 / std::numbers::pi; }
26inline num_t radians(num_t deg) { return deg * std::numbers::pi / 180; }
27inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; }
28inline num_t sq(num_t x) { return x * x; }
29inline num_t cb(num_t x) { return x * x * x; }
30
37
39 // p. 59
40 // UT -> JD, TT -> JDE
41 int y = moment.year;
42 int m = moment.month;
43 num_t d = moment.day_of_month;
44 d += moment.hour / 24.0;
45 d += moment.minute / (24.0 * 60);
46 d += moment.second / (24.0 * 60 * 60);
47 if (m <= 2) {
48 y -= 1;
49 m += 12;
50 }
51 int a = y / 100;
52 int b = 2 - a + a / 4;
53 return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5;
54}
56 // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html)
57 int t = moment.year - 2000;
58 return 62.92 + t * (0.32217 + t * 0.005589);
59}
60// Perform a fractional module operation where the result will always be positive (wrapping around)
62 num_t res = fmod(x, y);
63 if (res < 0)
64 res += y;
65 return res;
66}
67
68num_t internal::Moment::jd() const { return julian_day(dt); }
69
71 // dt is in UT1, but JDE is based on TT
72 // so add deltaT factor
73 return jd() + delta_t(dt) / (60 * 60 * 24);
74}
75
76struct SunAtTime {
77 num_t jde;
78 num_t t;
79
80 // eq 25.1, p. 163; julian centuries from the epoch J2000.0
81 SunAtTime(num_t jde) : jde(jde), t((jde - 2451545) / 36525.0) {}
82
83 num_t mean_obliquity() const {
84 // eq. 22.2, p. 147; mean obliquity of the ecliptic
85 num_t epsilon_0 = (+arcdeg(23, 26, 21.448) - arcdeg(0, 0, 46.8150) * t - arcdeg(0, 0, 0.00059) * sq(t) +
86 arcdeg(0, 0, 0.001813) * cb(t));
87 return epsilon_0;
88 }
89
90 num_t omega() const {
91 // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic
92 // in degrees
93 num_t omega = 125.05 - 1934.136 * t;
94 return omega;
95 }
96
97 num_t true_obliquity() const {
98 // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic
99 num_t delta_epsilon = 0.00256 * cos(radians(omega()));
100 num_t epsilon = mean_obliquity() + delta_epsilon;
101 return epsilon;
102 }
103
104 num_t mean_longitude() const {
105 // eq 25.2, p. 163; geometric mean longitude = mean equinox of the date in degrees
106 num_t l0 = 280.46646 + 36000.76983 * t + 0.0003032 * sq(t);
107 return wmod(l0, 360);
108 }
109
110 num_t eccentricity() const {
111 // eq 25.4, p. 163; eccentricity of earth's orbit
112 num_t e = 0.016708634 - 0.000042037 * t - 0.0000001267 * sq(t);
113 return e;
114 }
115
116 num_t mean_anomaly() const {
117 // eq 25.3, p. 163; mean anomaly of the sun in degrees
118 num_t m = 357.52911 + 35999.05029 * t - 0.0001537 * sq(t);
119 return wmod(m, 360);
120 }
121
122 num_t equation_of_center() const {
123 // p. 164; sun's equation of the center c in degrees
124 num_t m_rad = radians(mean_anomaly());
125 num_t c = ((1.914602 - 0.004817 * t - 0.000014 * sq(t)) * sin(m_rad) + (0.019993 - 0.000101 * t) * sin(2 * m_rad) +
126 0.000289 * sin(3 * m_rad));
127 return wmod(c, 360);
128 }
129
130 num_t true_longitude() const {
131 // p. 164; sun's true longitude in degrees
132 num_t x = mean_longitude() + equation_of_center();
133 return wmod(x, 360);
134 }
135
136 num_t true_anomaly() const {
137 // p. 164; sun's true anomaly in degrees
138 num_t x = mean_anomaly() + equation_of_center();
139 return wmod(x, 360);
140 }
141
142 num_t apparent_longitude() const {
143 // p. 164; sun's apparent longitude = true equinox in degrees
144 num_t x = true_longitude() - 0.00569 - 0.00478 * sin(radians(omega()));
145 return wmod(x, 360);
146 }
147
148 EquatorialCoordinate equatorial_coordinate() const {
149 num_t epsilon_rad = radians(true_obliquity());
150 // eq. 25.6; p. 165; sun's right ascension alpha
151 num_t app_lon_rad = radians(apparent_longitude());
152 num_t right_ascension_rad = atan2(cos(epsilon_rad) * sin(app_lon_rad), cos(app_lon_rad));
153 num_t declination_rad = asin(sin(epsilon_rad) * sin(app_lon_rad));
154 return EquatorialCoordinate{degrees(right_ascension_rad), degrees(declination_rad)};
155 }
156
157 num_t equation_of_time() const {
158 // chapter 28, p. 185
159 num_t epsilon_half = radians(true_obliquity() / 2);
160 num_t y = sq(tan(epsilon_half));
161 num_t l2 = 2 * mean_longitude();
162 num_t l2_rad = radians(l2);
163 num_t e = eccentricity();
164 num_t m = mean_anomaly();
165 num_t m_rad = radians(m);
166 num_t sin_m = sin(m_rad);
167 num_t eot = (y * sin(l2_rad) - 2 * e * sin_m + 4 * e * y * sin_m * cos(l2_rad) - 1 / 2.0 * sq(y) * sin(2 * l2_rad) -
168 5 / 4.0 * sq(e) * sin(2 * m_rad));
169 return degrees(eot);
170 }
171
172 void debug() const {
173 // debug output like in example 25.a, p. 165
174 auto eq = equatorial_coordinate();
175 ESP_LOGV(TAG,
176 "Sun position:\n"
177 " jde: %f\n"
178 " T: %f\n"
179 " L_0: %f\n"
180 " M: %f\n"
181 " e: %f\n"
182 " C: %f\n"
183 " Odot: %f\n"
184 " Omega: %f\n"
185 " lambda: %f\n"
186 " epsilon_0: %f\n"
187 " epsilon: %f\n"
188 " v: %f\n"
189 " right_ascension: %f\n"
190 " declination: %f",
191 jde, t, mean_longitude(), mean_anomaly(), eccentricity(), equation_of_center(), true_longitude(), omega(),
192 apparent_longitude(), mean_obliquity(), true_obliquity(), true_anomaly(), eq.right_ascension,
193 eq.declination);
194 }
195};
196
197struct SunAtLocation {
198 GeoLocation location;
199
200 num_t greenwich_sidereal_time(Moment moment) const {
201 // Return the greenwich mean sidereal time for this instant in degrees
202 // see chapter 12, p. 87
203 num_t jd = moment.jd();
204 // eq 12.1, p.87; jd for 0h UT of this date
205 ESPTime moment_0h = moment.dt;
206 moment_0h.hour = moment_0h.minute = moment_0h.second = 0;
207 num_t jd0 = Moment{moment_0h}.jd();
208 num_t t = (jd0 - 2451545) / 36525;
209 // eq. 12.4, p.88
210 num_t gmst = (+280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * sq(t) - (1 / 38710000.0) * cb(t));
211 return wmod(gmst, 360);
212 }
213
214 HorizontalCoordinate true_coordinate(Moment moment) const {
215 auto eq = SunAtTime(moment.jde()).equatorial_coordinate();
216 num_t gmst = greenwich_sidereal_time(moment);
217 // do not apply any nutation correction (not important for our target accuracy)
218 num_t nutation_corr = 0;
219
220 num_t ra = eq.right_ascension;
221 num_t alpha = gmst + nutation_corr + location.longitude - ra;
222 alpha = wmod(alpha, 360);
223 num_t alpha_rad = radians(alpha);
224
225 num_t sin_lat = sin(location.latitude_rad());
226 num_t cos_lat = cos(location.latitude_rad());
227 num_t sin_elevation = (+sin_lat * sin(eq.declination_rad()) + cos_lat * cos(eq.declination_rad()) * cos(alpha_rad));
228 num_t elevation_rad = asin(sin_elevation);
229 num_t azimuth_rad = atan2(sin(alpha_rad), cos(alpha_rad) * sin_lat - tan(eq.declination_rad()) * cos_lat);
230 return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180};
231 }
232
233 optional<ESPTime> sunrise(ESPTime date, num_t zenith) const { return event(true, date, zenith); }
234 optional<ESPTime> sunset(ESPTime date, num_t zenith) const { return event(false, date, zenith); }
235 optional<ESPTime> event(bool rise, ESPTime date, num_t zenith) const {
236 // couldn't get the method described in chapter 15 to work,
237 // so instead this is based on the algorithm in time4j
238 // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java
239 auto m = local_event_(date, 12); // noon
240 num_t jde = julian_day(m);
241 num_t new_h = 0, old_h;
242 do {
243 old_h = new_h;
244 auto x = local_hour_angle_(jde + old_h / 86400, rise, zenith);
245 if (!x.has_value())
246 return {};
247 new_h = *x;
248 } while (std::abs(new_h - old_h) >= 15);
249 time_t new_timestamp = m.timestamp + (time_t) new_h;
250 return ESPTime::from_epoch_local(new_timestamp);
251 }
252
253 protected:
254 optional<num_t> local_hour_angle_(num_t jde, bool rise, num_t zenith) const {
255 auto pos = SunAtTime(jde).equatorial_coordinate();
256 num_t dec_rad = pos.declination_rad();
257 num_t lat_rad = location.latitude_rad();
258 num_t num = cos(radians(zenith)) - (sin(dec_rad) * sin(lat_rad));
259 num_t denom = cos(dec_rad) * cos(lat_rad);
260 num_t cos_h = num / denom;
261 if (cos_h > 1 || cos_h < -1)
262 return {};
263 num_t hour_angle = degrees(acos(cos_h)) * 240;
264 if (rise)
265 hour_angle *= -1;
266 return hour_angle;
267 }
268
269 ESPTime local_event_(ESPTime date, int hour) const {
270 // input date should be in UTC, and hour/minute/second fields 0
271 num_t added_d = hour / 24.0 - location.longitude / 360;
272 num_t jd = julian_day(date) + added_d;
273
274 num_t eot = SunAtTime(jd).equation_of_time() * 240;
275 time_t new_timestamp = (time_t) (date.timestamp + added_d * 86400 - eot);
276 return ESPTime::from_epoch_utc(new_timestamp);
277 }
278};
279
281 SunAtLocation sun{location_};
282 Moment m{time_->utcnow()};
283 if (!m.dt.is_valid())
284 return HorizontalCoordinate{NAN, NAN};
285
286 // uncomment to print some debug output
287 /*
288 SunAtTime st{m.jde()};
289 st.debug();
290 */
291 return sun.true_coordinate(m);
292}
293optional<ESPTime> Sun::calc_event_(ESPTime date, bool rising, double zenith) {
294 SunAtLocation sun{location_};
295 if (!date.is_valid())
296 return {};
297 // Calculate UT1 timestamp at 0h
298 auto today = date;
299 today.hour = today.minute = today.second = 0;
300 today.recalc_timestamp_utc();
301
302 auto it = sun.event(rising, today, zenith);
303 if (it.has_value() && it->timestamp < date.timestamp) {
304 // We're calculating *next* sunrise/sunset, but calculated event
305 // is today, so try again tomorrow
306 time_t new_timestamp = today.timestamp + 24 * 60 * 60;
307 today = ESPTime::from_epoch_utc(new_timestamp);
308 it = sun.event(rising, today, zenith);
309 }
310 return it;
311}
312optional<ESPTime> Sun::calc_event_(bool rising, double zenith) {
313 auto it = Sun::calc_event_(this->time_->utcnow(), rising, zenith);
314 return it;
315}
316
317optional<ESPTime> Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); }
318optional<ESPTime> Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); }
319optional<ESPTime> Sun::sunrise(ESPTime date, double elevation) { return this->calc_event_(date, true, 90 - elevation); }
320optional<ESPTime> Sun::sunset(ESPTime date, double elevation) { return this->calc_event_(date, false, 90 - elevation); }
321double Sun::elevation() { return this->calc_coords_().elevation; }
322double Sun::azimuth() { return this->calc_coords_().azimuth; }
323
324} // namespace esphome::sun
uint8_t m
Definition bl0906.h:1
optional< ESPTime > calc_event_(bool rising, double zenith)
Definition sun.cpp:312
double elevation()
Definition sun.cpp:321
internal::HorizontalCoordinate calc_coords_()
Definition sun.cpp:280
optional< ESPTime > sunset(double elevation)
Definition sun.cpp:318
optional< ESPTime > sunrise(double elevation)
Definition sun.cpp:317
double azimuth()
Definition sun.cpp:322
uint8_t hour
static float float float t
const char *const TAG
Definition spi.cpp:7
num_t delta_t(ESPTime moment)
Definition sun.cpp:55
num_t wmod(num_t x, num_t y)
Definition sun.cpp:61
num_t degrees(num_t rad)
Definition sun.cpp:25
num_t arcdeg(num_t deg, num_t minutes, num_t seconds)
Definition sun.cpp:27
num_t radians(num_t deg)
Definition sun.cpp:26
num_t cb(num_t x)
Definition sun.cpp:29
num_t julian_day(ESPTime moment)
Definition sun.cpp:38
num_t sq(num_t x)
Definition sun.cpp:28
size_t size_t pos
Definition helpers.h:1038
A more user-friendly version of struct tm from time.h.
Definition time.h:23
uint8_t minute
minutes after the hour [0-59]
Definition time.h:32
uint8_t second
seconds after the minute [0-60]
Definition time.h:30
uint8_t hour
hours since midnight [0-23]
Definition time.h:34
time_t timestamp
unix epoch time (seconds since UTC Midnight January 1, 1970)
Definition time.h:48
bool is_valid(bool check_day_of_week=true, bool check_day_of_year=true) const
Check if this ESPTime is valid (year >= 2019 and the requested fields are in range).
Definition time.h:82
static ESPTime from_epoch_utc(time_t epoch)
Convert an UTC epoch timestamp to a UTC time ESPTime instance.
Definition time.h:143
uint8_t day_of_month
day of the month [1-31]
Definition time.h:38
uint16_t year
year
Definition time.h:44
uint8_t month
month; january=1 [1-12]
Definition time.h:42
uint16_t x
Definition tt21100.cpp:5
uint16_t y
Definition tt21100.cpp:6