mirror of
https://github.com/esphome/esphome.git
synced 2024-12-03 12:14:13 +01:00
003326f2eb
Fixes https://github.com/esphome/issues/issues/793 Also adds a clampd function that operates with doubles, not floats
176 lines
5.5 KiB
C++
176 lines
5.5 KiB
C++
#include "sun.h"
|
|
#include "esphome/core/log.h"
|
|
|
|
namespace esphome {
|
|
namespace sun {
|
|
|
|
static const char *TAG = "sun";
|
|
|
|
#undef PI
|
|
|
|
/* Usually, ESPHome uses single-precision floating point values
|
|
* because those tend to be accurate enough and are more efficient.
|
|
*
|
|
* However, some of the data in this class has to be quite accurate, so double is
|
|
* used everywhere.
|
|
*/
|
|
static const double PI = 3.141592653589793;
|
|
static const double TAU = 6.283185307179586;
|
|
static const double TO_RADIANS = PI / 180.0;
|
|
static const double TO_DEGREES = 180.0 / PI;
|
|
static const double EARTH_TILT = 23.44 * TO_RADIANS;
|
|
|
|
optional<time::ESPTime> Sun::sunrise(double elevation) {
|
|
auto time = this->time_->now();
|
|
if (!time.is_valid())
|
|
return {};
|
|
double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true);
|
|
if (isnan(sun_time))
|
|
return {};
|
|
uint32_t epoch = this->calc_epoch_(time, sun_time);
|
|
return time::ESPTime::from_epoch_local(epoch);
|
|
}
|
|
optional<time::ESPTime> Sun::sunset(double elevation) {
|
|
auto time = this->time_->now();
|
|
if (!time.is_valid())
|
|
return {};
|
|
double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false);
|
|
if (isnan(sun_time))
|
|
return {};
|
|
uint32_t epoch = this->calc_epoch_(time, sun_time);
|
|
return time::ESPTime::from_epoch_local(epoch);
|
|
}
|
|
double Sun::elevation() {
|
|
auto time = this->current_sun_time_();
|
|
if (isnan(time))
|
|
return NAN;
|
|
return this->elevation_(time);
|
|
}
|
|
double Sun::azimuth() {
|
|
auto time = this->current_sun_time_();
|
|
if (isnan(time))
|
|
return NAN;
|
|
return this->azimuth_(time);
|
|
}
|
|
// like clamp, but with doubles
|
|
double clampd(double val, double min, double max) {
|
|
if (val < min)
|
|
return min;
|
|
if (val > max)
|
|
return max;
|
|
return val;
|
|
}
|
|
double Sun::sun_declination_(double sun_time) {
|
|
double n = sun_time - 1.0;
|
|
// maximum declination
|
|
const double tot = -sin(EARTH_TILT);
|
|
|
|
// eccentricity of the earth's orbit (ellipse)
|
|
double eccentricity = 0.0167;
|
|
|
|
// days since perihelion (January 3rd)
|
|
double days_since_perihelion = n - 2;
|
|
// days since december solstice (december 22)
|
|
double days_since_december_solstice = n + 10;
|
|
const double c = TAU / 365.24;
|
|
double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion));
|
|
// Make sure value is in range (double error may lead to results slightly larger than 1)
|
|
double x = clampd(tot * v, -1.0, 1.0);
|
|
return asin(x);
|
|
}
|
|
double Sun::elevation_ratio_(double sun_time) {
|
|
double decl = this->sun_declination_(sun_time);
|
|
double hangle = this->hour_angle_(sun_time);
|
|
double a = sin(this->latitude_rad_()) * sin(decl);
|
|
double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle);
|
|
double val = clampd(a + b, -1.0, 1.0);
|
|
return val;
|
|
}
|
|
double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; }
|
|
double Sun::hour_angle_(double sun_time) {
|
|
double time_of_day = fmod(sun_time, 1.0) * 24.0;
|
|
return -PI * (time_of_day - 12) / 12;
|
|
}
|
|
double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; }
|
|
double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); }
|
|
double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); }
|
|
double Sun::azimuth_rad_(double sun_time) {
|
|
double hangle = -this->hour_angle_(sun_time);
|
|
double decl = this->sun_declination_(sun_time);
|
|
double zen = this->zenith_rad_(sun_time);
|
|
double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl);
|
|
double denom = sin(zen) * cos(this->latitude_rad_());
|
|
double v = clampd(nom / denom, -1.0, 1.0);
|
|
double az = PI - acos(v);
|
|
if (hangle > 0)
|
|
az = -az;
|
|
if (az < 0)
|
|
az += TAU;
|
|
return az;
|
|
}
|
|
double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; }
|
|
double Sun::calc_sun_time_(const time::ESPTime &time) {
|
|
// Time as seen at 0° longitude
|
|
if (!time.is_valid())
|
|
return NAN;
|
|
|
|
double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0);
|
|
// Add longitude correction
|
|
double add = this->longitude_ / 360.0;
|
|
return base + add;
|
|
}
|
|
uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) {
|
|
sun_time -= this->longitude_ / 360.0;
|
|
base.day_of_year = uint32_t(floor(sun_time));
|
|
|
|
sun_time = (sun_time - base.day_of_year) * 24.0;
|
|
base.hour = uint32_t(floor(sun_time));
|
|
|
|
sun_time = (sun_time - base.hour) * 60.0;
|
|
base.minute = uint32_t(floor(sun_time));
|
|
|
|
sun_time = (sun_time - base.minute) * 60.0;
|
|
base.second = uint32_t(floor(sun_time));
|
|
|
|
base.recalc_timestamp_utc(true);
|
|
return base.timestamp;
|
|
}
|
|
double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) {
|
|
// Use binary search, newton's method would be better but binary search already
|
|
// converges quite well (19 cycles) and much simpler. Function is guaranteed to be
|
|
// monotonous.
|
|
double lo, hi;
|
|
if (rising) {
|
|
lo = day_of_year + 0.0;
|
|
hi = day_of_year + 0.5;
|
|
} else {
|
|
lo = day_of_year + 1.0;
|
|
hi = day_of_year + 0.5;
|
|
}
|
|
|
|
double min_elevation = this->elevation_(lo);
|
|
double max_elevation = this->elevation_(hi);
|
|
if (elevation < min_elevation || elevation > max_elevation)
|
|
return NAN;
|
|
|
|
// Accuracy: 0.1s
|
|
const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0);
|
|
|
|
while (fabs(hi - lo) > accuracy) {
|
|
double mid = (lo + hi) / 2.0;
|
|
double value = this->elevation_(mid) - elevation;
|
|
if (value < 0) {
|
|
lo = mid;
|
|
} else if (value > 0) {
|
|
hi = mid;
|
|
} else {
|
|
lo = hi = mid;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return (lo + hi) / 2.0;
|
|
}
|
|
|
|
} // namespace sun
|
|
} // namespace esphome
|