From f1a0e5a313ccd6fd77bab161198d9984018ebbf8 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 11 May 2019 12:31:00 +0200 Subject: [PATCH] Sun support (#531) * Sun * Add sun support * Lint * Updates * Fix elevation * Lint * Update mqtt_climate.cpp --- esphome/codegen.py | 4 +- esphome/components/sensor/sensor.cpp | 6 +- esphome/components/sun/__init__.py | 103 +++++++++++ esphome/components/sun/sensor/__init__.py | 30 ++++ esphome/components/sun/sensor/sun_sensor.cpp | 12 ++ esphome/components/sun/sensor/sun_sensor.h | 41 +++++ esphome/components/sun/sun.cpp | 168 ++++++++++++++++++ esphome/components/sun/sun.h | 146 +++++++++++++++ .../components/sun/text_sensor/__init__.py | 45 +++++ .../sun/text_sensor/sun_text_sensor.cpp | 12 ++ .../sun/text_sensor/sun_text_sensor.h | 41 +++++ esphome/components/time/automation.cpp | 4 +- esphome/components/time/real_time_clock.cpp | 102 +++++++---- esphome/components/time/real_time_clock.h | 32 +++- esphome/config.py | 13 +- esphome/config_validation.py | 8 +- esphome/const.py | 3 + esphome/core/helpers.cpp | 2 - esphome/core/helpers.h | 12 ++ esphome/cpp_generator.py | 8 +- esphome/cpp_helpers.py | 13 +- esphome/cpp_types.py | 1 + 22 files changed, 740 insertions(+), 66 deletions(-) create mode 100644 esphome/components/sun/__init__.py create mode 100644 esphome/components/sun/sensor/__init__.py create mode 100644 esphome/components/sun/sensor/sun_sensor.cpp create mode 100644 esphome/components/sun/sensor/sun_sensor.h create mode 100644 esphome/components/sun/sun.cpp create mode 100644 esphome/components/sun/sun.h create mode 100644 esphome/components/sun/text_sensor/__init__.py create mode 100644 esphome/components/sun/text_sensor/sun_text_sensor.cpp create mode 100644 esphome/components/sun/text_sensor/sun_text_sensor.h diff --git a/esphome/codegen.py b/esphome/codegen.py index 30f9ce6d2f..c2dea04995 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -17,9 +17,9 @@ from esphome.cpp_generator import ( # noqa MockObjClass) from esphome.cpp_helpers import ( # noqa gpio_pin_expression, register_component, build_registry_entry, - build_registry_list, extract_registry_entry_config) + build_registry_list, extract_registry_entry_config, register_parented) from esphome.cpp_types import ( # noqa - global_ns, void, nullptr, float_, bool_, std_ns, std_string, + global_ns, void, nullptr, float_, double, bool_, std_ns, std_string, std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN, esphome_ns, App, Nameable, Component, ComponentPtr, PollingComponent, Application, optional, arduino_json_ns, JsonObject, diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 0f54f80c6d..ca6f4c23bb 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -88,10 +88,8 @@ std::string Sensor::unique_id() { return ""; } void Sensor::internal_send_state_to_frontend(float state) { this->has_state_ = true; this->state = state; - if (this->filter_list_ != nullptr) { - ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, - this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); - } + ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state, + this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals()); this->callback_.call(state); } bool Sensor::has_state() const { return this->has_state_; } diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py new file mode 100644 index 0000000000..625e64dcc2 --- /dev/null +++ b/esphome/components/sun/__init__.py @@ -0,0 +1,103 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import time +from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID + +sun_ns = cg.esphome_ns.namespace('sun') + +Sun = sun_ns.class_('Sun') +SunTrigger = sun_ns.class_('SunTrigger', cg.PollingComponent, automation.Trigger.template()) +SunCondition = sun_ns.class_('SunCondition', automation.Condition) + +CONF_SUN_ID = 'sun_id' +CONF_LATITUDE = 'latitude' +CONF_LONGITUDE = 'longitude' +CONF_ELEVATION = 'elevation' +CONF_ON_SUNRISE = 'on_sunrise' +CONF_ON_SUNSET = 'on_sunset' + +ELEVATION_MAP = { + 'sunrise': 0.0, + 'sunset': 0.0, + 'civil': -6.0, + 'nautical': -12.0, + 'astronomical': -18.0, +} + + +def elevation(value): + if isinstance(value, str): + try: + value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')] + except cv.Invalid: + pass + value = cv.angle(value) + return cv.float_range(min=-180, max=180)(value) + + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Sun), + cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), + cv.Required(CONF_LATITUDE): cv.float_range(min=-90, max=90), + cv.Required(CONF_LONGITUDE): cv.float_range(min=-180, max=180), + + cv.Optional(CONF_ON_SUNRISE): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), + cv.Optional(CONF_ELEVATION, default=0.0): elevation, + }), + cv.Optional(CONF_ON_SUNSET): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), + cv.Optional(CONF_ELEVATION, default=0.0): elevation, + }), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + time_ = yield cg.get_variable(config[CONF_TIME_ID]) + cg.add(var.set_time(time_)) + cg.add(var.set_latitude(config[CONF_LATITUDE])) + cg.add(var.set_longitude(config[CONF_LONGITUDE])) + + for conf in config.get(CONF_ON_SUNRISE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield cg.register_component(trigger, conf) + yield cg.register_parented(trigger, var) + cg.add(trigger.set_sunrise(True)) + cg.add(trigger.set_elevation(conf[CONF_ELEVATION])) + yield automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_SUNSET, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield cg.register_component(trigger, conf) + yield cg.register_parented(trigger, var) + cg.add(trigger.set_sunrise(False)) + cg.add(trigger.set_elevation(conf[CONF_ELEVATION])) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_condition('sun.is_above_horizon', SunCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(Sun), + cv.Optional(CONF_ELEVATION, default=0): cv.templatable(elevation), +})) +def sun_above_horizon_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double) + cg.add(var.set_elevation(templ)) + cg.add(var.set_above(True)) + yield var + + +@automation.register_condition('sun.is_below_horizon', SunCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(Sun), + cv.Optional(CONF_ELEVATION, default=0): cv.templatable(elevation), +})) +def sun_below_horizon_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + templ = yield cg.templatable(config[CONF_ELEVATION], args, cg.double) + cg.add(var.set_elevation(templ)) + cg.add(var.set_above(False)) + yield var diff --git a/esphome/components/sun/sensor/__init__.py b/esphome/components/sun/sensor/__init__.py new file mode 100644 index 0000000000..5ca315888d --- /dev/null +++ b/esphome/components/sun/sensor/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import UNIT_DEGREES, ICON_WEATHER_SUNSET, CONF_ID, CONF_TYPE +from .. import sun_ns, CONF_SUN_ID, Sun + +DEPENDENCIES = ['sun'] + +SunSensor = sun_ns.class_('SunSensor', sensor.Sensor, cg.PollingComponent) +SensorType = sun_ns.enum('SensorType') +TYPES = { + 'elevation': SensorType.SUN_SENSOR_ELEVATION, + 'azimuth': SensorType.SUN_SENSOR_AZIMUTH, +} + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_DEGREES, ICON_WEATHER_SUNSET, 1).extend({ + cv.GenerateID(): cv.declare_id(SunSensor), + cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), + cv.Required(CONF_TYPE): cv.enum(TYPES, lower=True), +}).extend(cv.polling_component_schema('60s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + cg.add(var.set_type(config[CONF_TYPE])) + paren = yield cg.get_variable(config[CONF_SUN_ID]) + cg.add(var.set_parent(paren)) diff --git a/esphome/components/sun/sensor/sun_sensor.cpp b/esphome/components/sun/sensor/sun_sensor.cpp new file mode 100644 index 0000000000..63b7715287 --- /dev/null +++ b/esphome/components/sun/sensor/sun_sensor.cpp @@ -0,0 +1,12 @@ +#include "sun_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sun { + +static const char *TAG = "sun.sensor"; + +void SunSensor::dump_config() { LOG_SENSOR("", "Sun Sensor", this); } + +} // namespace sun +} // namespace esphome diff --git a/esphome/components/sun/sensor/sun_sensor.h b/esphome/components/sun/sensor/sun_sensor.h new file mode 100644 index 0000000000..2bd33375ef --- /dev/null +++ b/esphome/components/sun/sensor/sun_sensor.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sun/sun.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace sun { + +enum SensorType { + SUN_SENSOR_ELEVATION, + SUN_SENSOR_AZIMUTH, +}; + +class SunSensor : public sensor::Sensor, public PollingComponent { + public: + void set_parent(Sun *parent) { parent_ = parent; } + void set_type(SensorType type) { type_ = type; } + void dump_config() override; + void update() override { + double val; + switch (this->type_) { + case SUN_SENSOR_ELEVATION: + val = this->parent_->elevation(); + break; + case SUN_SENSOR_AZIMUTH: + val = this->parent_->azimuth(); + break; + default: + return; + } + this->publish_state(val); + } + + protected: + sun::Sun *parent_; + SensorType type_; +}; + +} // namespace sun +} // namespace esphome diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp new file mode 100644 index 0000000000..e0da63bb4b --- /dev/null +++ b/esphome/components/sun/sun.cpp @@ -0,0 +1,168 @@ +#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 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 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); +} +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 = clamp(tot * v, 0, 1); + 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 = clamp(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 = clamp(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 diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h new file mode 100644 index 0000000000..8e477e9196 --- /dev/null +++ b/esphome/components/sun/sun.h @@ -0,0 +1,146 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/automation.h" +#include "esphome/components/time/real_time_clock.h" + +namespace esphome { +namespace sun { + +class Sun { + public: + void set_time(time::RealTimeClock *time) { time_ = time; } + time::RealTimeClock *get_time() const { return time_; } + void set_latitude(double latitude) { latitude_ = latitude; } + void set_longitude(double longitude) { longitude_ = longitude; } + + optional sunrise(double elevation = 0.0); + optional sunset(double elevation = 0.0); + + double elevation(); + double azimuth(); + + protected: + double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); } + + /** Calculate the declination of the sun in rad. + * + * See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth + * + * Accuracy: ±0.2° + * + * @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_. + * @return Sun declination in degrees + */ + double sun_declination_(double sun_time); + + double elevation_ratio_(double sun_time); + + /** Calculate the hour angle based on the sun time of day in hours. + * + * Positive in morning, 0 at noon, negative in afternoon. + * + * @param sun_time Sun time, see calc_sun_time_. + * @return Hour angle in rad. + */ + double hour_angle_(double sun_time); + + double elevation_(double sun_time); + + double elevation_rad_(double sun_time); + + double zenith_rad_(double sun_time); + + double azimuth_rad_(double sun_time); + + double azimuth_(double sun_time); + + /** Return the sun time given by the time_ object. + * + * Sun time is defined as doubleing point day of year. + * Integer part encodes the day of the year (1=January 1st) + * Decimal part encodes time of day (1/24 = 1 hour) + */ + double calc_sun_time_(const time::ESPTime &time); + + uint32_t calc_epoch_(time::ESPTime base, double sun_time); + + /** Calculate the sun time of day + * + * @param day_of_year + * @param elevation + * @param rising + * @return + */ + double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising); + + double latitude_rad_(); + + time::RealTimeClock *time_; + /// Latitude in degrees, range: -90 to 90. + double latitude_; + /// Longitude in degrees, range: -180 to 180. + double longitude_; +}; + +class SunTrigger : public Trigger<>, public PollingComponent, public Parented { + public: + SunTrigger() : PollingComponent(1000) {} + + void set_sunrise(bool sunrise) { sunrise_ = sunrise; } + void set_elevation(double elevation) { elevation_ = elevation; } + + void update() override { + auto now = this->parent_->get_time()->utcnow(); + if (!now.is_valid()) + return; + + if (!this->last_result_.has_value() || this->last_result_->day_of_year != now.day_of_year) { + this->recalc_(); + return; + } + + if (this->prev_check_ != -1) { + auto res = *this->last_result_; + // now >= sunrise > prev_check + if (now.timestamp >= res.timestamp && res.timestamp > this->prev_check_) { + this->trigger(); + } + } + this->prev_check_ = now.timestamp; + } + + protected: + void recalc_() { + if (this->sunrise_) + this->last_result_ = this->parent_->sunrise(this->elevation_); + else + this->last_result_ = this->parent_->sunset(this->elevation_); + } + bool sunrise_; + double elevation_; + time_t prev_check_{-1}; + optional last_result_{}; +}; + +template class SunCondition : public Condition, public Parented { + public: + TEMPLATABLE_VALUE(double, elevation); + void set_above(bool above) { above_ = above; } + + bool check(Ts... x) override { + double elevation = this->elevation_.value(x...); + double current = this->parent_->elevation(); + if (this->above_) + return current > elevation; + else + return current < elevation; + } + + protected: + bool above_; +}; + +} // namespace sun +} // namespace esphome diff --git a/esphome/components/sun/text_sensor/__init__.py b/esphome/components/sun/text_sensor/__init__.py new file mode 100644 index 0000000000..8102a24da5 --- /dev/null +++ b/esphome/components/sun/text_sensor/__init__.py @@ -0,0 +1,45 @@ +from esphome.components import text_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ICON, ICON_WEATHER_SUNSET_DOWN, ICON_WEATHER_SUNSET_UP, CONF_TYPE, \ + CONF_ID, CONF_FORMAT +from .. import sun_ns, CONF_SUN_ID, Sun, CONF_ELEVATION, elevation + +DEPENDENCIES = ['sun'] + +SunTextSensor = sun_ns.class_('SunTextSensor', text_sensor.TextSensor, cg.PollingComponent) +SUN_TYPES = { + 'sunset': False, + 'sunrise': True, +} + + +def validate_optional_icon(config): + if CONF_ICON not in config: + config = config.copy() + config[CONF_ICON] = { + 'sunset': ICON_WEATHER_SUNSET_DOWN, + 'sunrise': ICON_WEATHER_SUNSET_UP, + }[config[CONF_TYPE]] + return config + + +CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SunTextSensor), + cv.GenerateID(CONF_SUN_ID): cv.use_id(Sun), + cv.Required(CONF_TYPE): cv.one_of(*SUN_TYPES, lower=True), + cv.Optional(CONF_ELEVATION, default=0): elevation, + cv.Optional(CONF_FORMAT, default='%X'): cv.string_strict, +}).extend(cv.polling_component_schema('60s')) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield text_sensor.register_text_sensor(var, config) + + paren = yield cg.get_variable(config[CONF_SUN_ID]) + cg.add(var.set_parent(paren)) + cg.add(var.set_sunrise(SUN_TYPES[config[CONF_TYPE]])) + cg.add(var.set_elevation(config[CONF_ELEVATION])) + cg.add(var.set_format(config[CONF_FORMAT])) diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.cpp b/esphome/components/sun/text_sensor/sun_text_sensor.cpp new file mode 100644 index 0000000000..ee949584cc --- /dev/null +++ b/esphome/components/sun/text_sensor/sun_text_sensor.cpp @@ -0,0 +1,12 @@ +#include "sun_text_sensor.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sun { + +static const char *TAG = "sun.text_sensor"; + +void SunTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Sun Text Sensor", this); } + +} // namespace sun +} // namespace esphome diff --git a/esphome/components/sun/text_sensor/sun_text_sensor.h b/esphome/components/sun/text_sensor/sun_text_sensor.h new file mode 100644 index 0000000000..e4f5beca9c --- /dev/null +++ b/esphome/components/sun/text_sensor/sun_text_sensor.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sun/sun.h" +#include "esphome/components/text_sensor/text_sensor.h" + +namespace esphome { +namespace sun { + +class SunTextSensor : public text_sensor::TextSensor, public PollingComponent { + public: + void set_parent(Sun *parent) { parent_ = parent; } + void set_elevation(double elevation) { elevation_ = elevation; } + void set_sunrise(bool sunrise) { sunrise_ = sunrise; } + void set_format(const std::string &format) { format_ = format; } + + void update() override { + optional res; + if (this->sunrise_) + res = this->parent_->sunrise(this->elevation_); + else + res = this->parent_->sunset(this->elevation_); + if (!res) { + this->publish_state(""); + return; + } + + this->publish_state(res->strftime(this->format_)); + } + + void dump_config() override; + + protected: + std::string format_{}; + Sun *parent_; + double elevation_; + bool sunrise_; +}; + +} // namespace sun +} // namespace esphome diff --git a/esphome/components/time/automation.cpp b/esphome/components/time/automation.cpp index 67b86e67ea..b6032ca2a2 100644 --- a/esphome/components/time/automation.cpp +++ b/esphome/components/time/automation.cpp @@ -38,11 +38,11 @@ void CronTrigger::loop() { } this->last_check_ = time; - if (!time.in_range()) { + if (!time.fields_in_range()) { ESP_LOGW(TAG, "Time is out of range!"); ESP_LOGD(TAG, "Second=%02u Minute=%02u Hour=%02u DayOfWeek=%u DayOfMonth=%u DayOfYear=%u Month=%u time=%ld", time.second, time.minute, time.hour, time.day_of_week, time.day_of_month, time.day_of_year, time.month, - time.time); + time.timestamp); } if (this->matches(time)) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index f20a9fcaa2..81524826be 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -35,27 +35,30 @@ size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { return ::strftime(buffer, buffer_len, format, &c_tm); } ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { - return ESPTime{.second = uint8_t(c_tm->tm_sec), - .minute = uint8_t(c_tm->tm_min), - .hour = uint8_t(c_tm->tm_hour), - .day_of_week = uint8_t(c_tm->tm_wday + 1), - .day_of_month = uint8_t(c_tm->tm_mday), - .day_of_year = uint16_t(c_tm->tm_yday + 1), - .month = uint8_t(c_tm->tm_mon + 1), - .year = uint16_t(c_tm->tm_year + 1900), - .is_dst = bool(c_tm->tm_isdst), - .time = c_time}; + ESPTime res{}; + res.second = uint8_t(c_tm->tm_sec); + res.minute = uint8_t(c_tm->tm_min); + res.hour = uint8_t(c_tm->tm_hour); + res.day_of_week = uint8_t(c_tm->tm_wday + 1); + res.day_of_month = uint8_t(c_tm->tm_mday); + res.day_of_year = uint16_t(c_tm->tm_yday + 1); + res.month = uint8_t(c_tm->tm_mon + 1); + res.year = uint16_t(c_tm->tm_year + 1900); + res.is_dst = bool(c_tm->tm_isdst); + res.timestamp = c_time; + return res; } struct tm ESPTime::to_c_tm() { - struct tm c_tm = tm{.tm_sec = this->second, - .tm_min = this->minute, - .tm_hour = this->hour, - .tm_mday = this->day_of_month, - .tm_mon = this->month - 1, - .tm_year = this->year - 1900, - .tm_wday = this->day_of_week - 1, - .tm_yday = this->day_of_year - 1, - .tm_isdst = this->is_dst}; + struct tm c_tm {}; + c_tm.tm_sec = this->second; + c_tm.tm_min = this->minute; + c_tm.tm_hour = this->hour; + c_tm.tm_mday = this->day_of_month; + c_tm.tm_mon = this->month - 1; + c_tm.tm_year = this->year - 1900; + c_tm.tm_wday = this->day_of_week - 1; + c_tm.tm_yday = this->day_of_year - 1; + c_tm.tm_isdst = this->is_dst; return c_tm; } std::string ESPTime::strftime(const std::string &format) { @@ -70,7 +73,6 @@ std::string ESPTime::strftime(const std::string &format) { timestr.resize(len); return timestr; } -bool ESPTime::is_valid() const { return this->year >= 2018; } template bool increment_time_value(T ¤t, uint16_t begin, uint16_t end) { current++; @@ -81,8 +83,18 @@ template bool increment_time_value(T ¤t, uint16_t begin, uint1 return false; } +static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } + +static bool days_in_month(uint8_t month, uint16_t year) { + static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + uint8_t days_in_month = DAYS_IN_MONTH[month]; + if (month == 2 && is_leap_year(year)) + days_in_month = 29; + return days_in_month; +} + void ESPTime::increment_second() { - this->time++; + this->timestamp++; if (!increment_time_value(this->second, 0, 60)) return; @@ -97,12 +109,7 @@ void ESPTime::increment_second() { // hour roll-over, increment day increment_time_value(this->day_of_week, 1, 8); - static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days_in_month = DAYS_IN_MONTH[this->month]; - if (this->month == 2 && this->year % 4 == 0) - days_in_month = 29; - - if (increment_time_value(this->day_of_month, 1, days_in_month + 1)) { + if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) { // day of month roll-over, increment month increment_time_value(this->month, 1, 13); } @@ -113,16 +120,39 @@ void ESPTime::increment_second() { this->year++; } } -bool ESPTime::operator<(ESPTime other) { return this->time < other.time; } -bool ESPTime::operator<=(ESPTime other) { return this->time <= other.time; } -bool ESPTime::operator==(ESPTime other) { return this->time == other.time; } -bool ESPTime::operator>=(ESPTime other) { return this->time >= other.time; } -bool ESPTime::operator>(ESPTime other) { return this->time > other.time; } -bool ESPTime::in_range() const { - return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && this->day_of_week < 8 && - this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && this->day_of_year < 367 && - this->month > 0 && this->month < 13; +void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { + time_t res = 0; + + if (!this->fields_in_range()) { + this->timestamp = -1; + return; + } + + for (uint16_t i = 1970; i < this->year; i++) + res += is_leap_year(i) ? 366 : 365; + + if (use_day_of_year) { + res += this->day_of_year - 1; + } else { + for (uint8_t i = 1; i < this->month; ++i) + res += days_in_month(i, this->year); + + res += this->day_of_month - 1; + } + + res *= 24; + res += this->hour; + res *= 60; + res += this->minute; + res *= 60; + res += this->second; + this->timestamp = res; } +bool ESPTime::operator<(ESPTime other) { return this->timestamp < other.timestamp; } +bool ESPTime::operator<=(ESPTime other) { return this->timestamp <= other.timestamp; } +bool ESPTime::operator==(ESPTime other) { return this->timestamp == other.timestamp; } +bool ESPTime::operator>=(ESPTime other) { return this->timestamp >= other.timestamp; } +bool ESPTime::operator>(ESPTime other) { return this->timestamp > other.timestamp; } } // namespace time } // namespace esphome diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index b87636d20e..9f40fdc5b5 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -1,6 +1,7 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/helpers.h" #include #include #include @@ -30,8 +31,11 @@ struct ESPTime { uint16_t year; /// daylight savings time flag bool is_dst; - /// unix epoch time (seconds since UTC Midnight January 1, 1970) - time_t time; + union { + ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time; + /// unix epoch time (seconds since UTC Midnight January 1, 1970) + time_t timestamp; + }; /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. * Up to buffer_len bytes are written. @@ -48,13 +52,20 @@ struct ESPTime { */ std::string strftime(const std::string &format); - bool is_valid() const; + /// Check if this ESPTime is valid (all fields in range and year is greater than 2018) + bool is_valid() const { return this->year >= 2019 && this->fields_in_range(); } - bool in_range() const; + /// Check if all time fields of this ESPTime are in range. + bool fields_in_range() const { + return this->second < 61 && this->minute < 60 && this->hour < 24 && this->day_of_week > 0 && + this->day_of_week < 8 && this->day_of_month > 0 && this->day_of_month < 32 && this->day_of_year > 0 && + this->day_of_year < 367 && this->month > 0 && this->month < 13; + } + /// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance. static ESPTime from_c_tm(struct tm *c_tm, time_t c_time); - /** Convert an epoch timestamp to an ESPTime instance of local time. + /** Convert an UTC epoch timestamp to a local time ESPTime instance. * * @param epoch Seconds since 1st January 1970. In UTC. * @return The generated ESPTime @@ -63,7 +74,7 @@ struct ESPTime { struct tm *c_tm = ::localtime(&epoch); return ESPTime::from_c_tm(c_tm, epoch); } - /** Convert an epoch timestamp to an ESPTime instance of UTC time. + /** Convert an UTC epoch timestamp to a UTC time ESPTime instance. * * @param epoch Seconds since 1st January 1970. In UTC. * @return The generated ESPTime @@ -73,8 +84,13 @@ struct ESPTime { return ESPTime::from_c_tm(c_tm, epoch); } + /// Recalculate the timestamp field from the other fields of this ESPTime instance (must be UTC). + void recalc_timestamp_utc(bool use_day_of_year = true); + + /// Convert this ESPTime instance back to a tm struct. struct tm to_c_tm(); + /// Increment this clock instance by one second. void increment_second(); bool operator<(ESPTime other); bool operator<=(ESPTime other); @@ -100,10 +116,10 @@ class RealTimeClock : public Component { std::string get_timezone() { return this->timezone_; } /// Get the time in the currently defined timezone. - ESPTime now() { return ESPTime::from_epoch_utc(this->timestamp_now()); } + ESPTime now() { return ESPTime::from_epoch_local(this->timestamp_now()); } /// Get the time without any time zone or DST corrections. - ESPTime utcnow() { return ESPTime::from_epoch_local(this->timestamp_now()); } + ESPTime utcnow() { return ESPTime::from_epoch_utc(this->timestamp_now()); } /// Get the current time as the UTC epoch since January 1st 1970. time_t timestamp_now() { return ::time(nullptr); } diff --git a/esphome/config.py b/esphome/config.py index 671253b31e..c969641422 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -119,15 +119,14 @@ def _lookup_module(domain, is_platform): path = 'esphome.components.{}'.format(domain) try: module = importlib.import_module(path) - except ImportError: - import traceback - _LOGGER.error("Unable to import component %s:", domain) - traceback.print_exc() + except ImportError as e: + if 'No module named' in str(e): + _LOGGER.error("Unable to import component %s:", domain) + else: + _LOGGER.error("Unable to import component %s:", domain, exc_info=True) return None except Exception: # pylint: disable=broad-except - import traceback - _LOGGER.error("Unable to load component %s:", domain) - traceback.print_exc() + _LOGGER.error("Unable to load component %s:", domain, exc_info=True) return None else: manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index e591a5d951..7676f2816a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -570,10 +570,15 @@ METRIC_SUFFIXES = { } -def float_with_unit(quantity, regex_suffix): +def float_with_unit(quantity, regex_suffix, optional_unit=False): pattern = re.compile(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*?)" + regex_suffix + r"$", re.UNICODE) def validator(value): + if optional_unit: + try: + return float_(value) + except Invalid: + pass match = pattern.match(string(value)) if match is None: @@ -595,6 +600,7 @@ current = float_with_unit("current", u"(a|A|amp|Amp|amps|Amps|ampere|Ampere)?") voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?") distance = float_with_unit("distance", u"(m)") framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)") +angle = float_with_unit("angle", u"(°|deg)", optional_unit=True) _temperature_c = float_with_unit("temperature", u"(°C|° C|°|C)?") _temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") _temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") diff --git a/esphome/const.py b/esphome/const.py index fa9bae4304..4e98dee394 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -470,6 +470,9 @@ ICON_ROTATE_RIGHT = 'mdi:rotate-right' ICON_SCALE = 'mdi:scale' ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SIGNAL = 'mdi:signal' +ICON_WEATHER_SUNSET = 'mdi:weather-sunset' +ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' +ICON_WEATHER_SUNSET_UP = 'mdi:weather-sunset-up' ICON_THERMOMETER = 'mdi:thermometer' ICON_TIMER = 'mdi:timer' ICON_WATER_PERCENT = 'mdi:water-percent' diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index c42585cab1..093f0f7d30 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -294,8 +294,6 @@ void HighFrequencyLoopRequester::stop() { bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } float clamp(float val, float min, float max) { - if (min > max) - std::swap(min, max); if (val < min) return min; if (val > max) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index cc046a6962..95fdf972e4 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -254,6 +254,18 @@ template class Deduplicator { T last_value_{}; }; +template class Parented { + public: + Parented() {} + Parented(T *parent) : parent_(parent) {} + + T *get_parent() const { return parent_; } + void set_parent(T *parent) { parent_ = parent; } + + protected: + T *parent_{nullptr}; +}; + uint32_t fnv1_hash(const std::string &str); } // namespace esphome diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index 651d748934..38217ea9bb 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -424,10 +424,14 @@ def new_Pvariable(id, # type: ID return Pvariable(id, rhs) -def add(expression, # type: Union[SafeExpType, Statement] +def add(expression, # type: Union[Expression, Statement] ): # type: (...) -> None - """Add an expression to the codegen setup() storage.""" + """Add an expression to the codegen section. + + After this is called, the given given expression will + show up in the setup() function after this has been called. + """ CORE.add(expression) diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index ec0c8f88f2..8e310ac44b 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -1,7 +1,7 @@ from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \ CONF_UPDATE_INTERVAL, CONF_TYPE_ID -from esphome.core import coroutine -from esphome.cpp_generator import RawExpression, add +from esphome.core import coroutine, ID +from esphome.cpp_generator import RawExpression, add, get_variable from esphome.cpp_types import App, GPIOPin @@ -42,6 +42,15 @@ def register_component(var, config): yield var +@coroutine +def register_parented(var, value): + if isinstance(value, ID): + paren = yield get_variable(value) + else: + paren = value + add(var.set_parent(paren)) + + def extract_registry_entry_config(registry, full_config): # type: (Registry, ConfigType) -> RegistryEntry key, config = next((k, v) for k, v in full_config.items() if k in registry) diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index a59137834e..d3e5b2d561 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -4,6 +4,7 @@ global_ns = MockObj('', '') void = global_ns.namespace('void') nullptr = global_ns.namespace('nullptr') float_ = global_ns.namespace('float') +double = global_ns.namespace('double') bool_ = global_ns.namespace('bool') std_ns = global_ns.namespace('std') std_string = std_ns.class_('string')