Sun support (#531)

* Sun

* Add sun support

* Lint

* Updates

* Fix elevation

* Lint

* Update mqtt_climate.cpp
This commit is contained in:
Otto Winter 2019-05-11 12:31:00 +02:00 committed by GitHub
parent f2540bae23
commit f1a0e5a313
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 740 additions and 66 deletions

View file

@ -17,9 +17,9 @@ from esphome.cpp_generator import ( # noqa
MockObjClass) MockObjClass)
from esphome.cpp_helpers import ( # noqa from esphome.cpp_helpers import ( # noqa
gpio_pin_expression, register_component, build_registry_entry, 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 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, std_vector, uint8, uint16, uint32, int32, const_char_ptr, NAN,
esphome_ns, App, Nameable, Component, ComponentPtr, esphome_ns, App, Nameable, Component, ComponentPtr,
PollingComponent, Application, optional, arduino_json_ns, JsonObject, PollingComponent, Application, optional, arduino_json_ns, JsonObject,

View file

@ -88,10 +88,8 @@ std::string Sensor::unique_id() { return ""; }
void Sensor::internal_send_state_to_frontend(float state) { void Sensor::internal_send_state_to_frontend(float state) {
this->has_state_ = true; this->has_state_ = true;
this->state = state; 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, 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->get_unit_of_measurement().c_str(), this->get_accuracy_decimals());
}
this->callback_.call(state); this->callback_.call(state);
} }
bool Sensor::has_state() const { return this->has_state_; } bool Sensor::has_state() const { return this->has_state_; }

View file

@ -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

View file

@ -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))

View file

@ -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

View file

@ -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

View file

@ -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<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);
}
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

View file

@ -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<time::ESPTime> sunrise(double elevation = 0.0);
optional<time::ESPTime> 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<Sun> {
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<time::ESPTime> last_result_{};
};
template<typename... Ts> class SunCondition : public Condition<Ts...>, public Parented<Sun> {
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

View file

@ -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]))

View file

@ -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

View file

@ -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<time::ESPTime> 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

View file

@ -38,11 +38,11 @@ void CronTrigger::loop() {
} }
this->last_check_ = time; this->last_check_ = time;
if (!time.in_range()) { if (!time.fields_in_range()) {
ESP_LOGW(TAG, "Time is out of 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", 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.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)) if (this->matches(time))

View file

@ -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); return ::strftime(buffer, buffer_len, format, &c_tm);
} }
ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) { ESPTime ESPTime::from_c_tm(struct tm *c_tm, time_t c_time) {
return ESPTime{.second = uint8_t(c_tm->tm_sec), ESPTime res{};
.minute = uint8_t(c_tm->tm_min), res.second = uint8_t(c_tm->tm_sec);
.hour = uint8_t(c_tm->tm_hour), res.minute = uint8_t(c_tm->tm_min);
.day_of_week = uint8_t(c_tm->tm_wday + 1), res.hour = uint8_t(c_tm->tm_hour);
.day_of_month = uint8_t(c_tm->tm_mday), res.day_of_week = uint8_t(c_tm->tm_wday + 1);
.day_of_year = uint16_t(c_tm->tm_yday + 1), res.day_of_month = uint8_t(c_tm->tm_mday);
.month = uint8_t(c_tm->tm_mon + 1), res.day_of_year = uint16_t(c_tm->tm_yday + 1);
.year = uint16_t(c_tm->tm_year + 1900), res.month = uint8_t(c_tm->tm_mon + 1);
.is_dst = bool(c_tm->tm_isdst), res.year = uint16_t(c_tm->tm_year + 1900);
.time = c_time}; res.is_dst = bool(c_tm->tm_isdst);
res.timestamp = c_time;
return res;
} }
struct tm ESPTime::to_c_tm() { struct tm ESPTime::to_c_tm() {
struct tm c_tm = tm{.tm_sec = this->second, struct tm c_tm {};
.tm_min = this->minute, c_tm.tm_sec = this->second;
.tm_hour = this->hour, c_tm.tm_min = this->minute;
.tm_mday = this->day_of_month, c_tm.tm_hour = this->hour;
.tm_mon = this->month - 1, c_tm.tm_mday = this->day_of_month;
.tm_year = this->year - 1900, c_tm.tm_mon = this->month - 1;
.tm_wday = this->day_of_week - 1, c_tm.tm_year = this->year - 1900;
.tm_yday = this->day_of_year - 1, c_tm.tm_wday = this->day_of_week - 1;
.tm_isdst = this->is_dst}; c_tm.tm_yday = this->day_of_year - 1;
c_tm.tm_isdst = this->is_dst;
return c_tm; return c_tm;
} }
std::string ESPTime::strftime(const std::string &format) { std::string ESPTime::strftime(const std::string &format) {
@ -70,7 +73,6 @@ std::string ESPTime::strftime(const std::string &format) {
timestr.resize(len); timestr.resize(len);
return timestr; return timestr;
} }
bool ESPTime::is_valid() const { return this->year >= 2018; }
template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end) { template<typename T> bool increment_time_value(T &current, uint16_t begin, uint16_t end) {
current++; current++;
@ -81,8 +83,18 @@ template<typename T> bool increment_time_value(T &current, uint16_t begin, uint1
return false; 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() { void ESPTime::increment_second() {
this->time++; this->timestamp++;
if (!increment_time_value(this->second, 0, 60)) if (!increment_time_value(this->second, 0, 60))
return; return;
@ -97,12 +109,7 @@ void ESPTime::increment_second() {
// hour roll-over, increment day // hour roll-over, increment day
increment_time_value(this->day_of_week, 1, 8); 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}; if (increment_time_value(this->day_of_month, 1, days_in_month(this->month, this->year) + 1)) {
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)) {
// day of month roll-over, increment month // day of month roll-over, increment month
increment_time_value(this->month, 1, 13); increment_time_value(this->month, 1, 13);
} }
@ -113,16 +120,39 @@ void ESPTime::increment_second() {
this->year++; this->year++;
} }
} }
bool ESPTime::operator<(ESPTime other) { return this->time < other.time; } void ESPTime::recalc_timestamp_utc(bool use_day_of_year) {
bool ESPTime::operator<=(ESPTime other) { return this->time <= other.time; } time_t res = 0;
bool ESPTime::operator==(ESPTime other) { return this->time == other.time; }
bool ESPTime::operator>=(ESPTime other) { return this->time >= other.time; } if (!this->fields_in_range()) {
bool ESPTime::operator>(ESPTime other) { return this->time > other.time; } this->timestamp = -1;
bool ESPTime::in_range() const { return;
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;
} }
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 time
} // namespace esphome } // namespace esphome

View file

@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include <stdlib.h> #include <stdlib.h>
#include <time.h> #include <time.h>
#include <bitset> #include <bitset>
@ -30,8 +31,11 @@ struct ESPTime {
uint16_t year; uint16_t year;
/// daylight savings time flag /// daylight savings time flag
bool is_dst; bool is_dst;
union {
ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time;
/// unix epoch time (seconds since UTC Midnight January 1, 1970) /// unix epoch time (seconds since UTC Midnight January 1, 1970)
time_t time; time_t timestamp;
};
/** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument. /** Convert this ESPTime struct to a null-terminated c string buffer as specified by the format argument.
* Up to buffer_len bytes are written. * Up to buffer_len bytes are written.
@ -48,13 +52,20 @@ struct ESPTime {
*/ */
std::string strftime(const std::string &format); 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); 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. * @param epoch Seconds since 1st January 1970. In UTC.
* @return The generated ESPTime * @return The generated ESPTime
@ -63,7 +74,7 @@ struct ESPTime {
struct tm *c_tm = ::localtime(&epoch); struct tm *c_tm = ::localtime(&epoch);
return ESPTime::from_c_tm(c_tm, 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. * @param epoch Seconds since 1st January 1970. In UTC.
* @return The generated ESPTime * @return The generated ESPTime
@ -73,8 +84,13 @@ struct ESPTime {
return ESPTime::from_c_tm(c_tm, epoch); 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(); struct tm to_c_tm();
/// Increment this clock instance by one second.
void increment_second(); void increment_second();
bool operator<(ESPTime other); bool operator<(ESPTime other);
bool operator<=(ESPTime other); bool operator<=(ESPTime other);
@ -100,10 +116,10 @@ class RealTimeClock : public Component {
std::string get_timezone() { return this->timezone_; } std::string get_timezone() { return this->timezone_; }
/// Get the time in the currently defined 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. /// 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. /// Get the current time as the UTC epoch since January 1st 1970.
time_t timestamp_now() { return ::time(nullptr); } time_t timestamp_now() { return ::time(nullptr); }

View file

@ -119,15 +119,14 @@ def _lookup_module(domain, is_platform):
path = 'esphome.components.{}'.format(domain) path = 'esphome.components.{}'.format(domain)
try: try:
module = importlib.import_module(path) module = importlib.import_module(path)
except ImportError: except ImportError as e:
import traceback if 'No module named' in str(e):
_LOGGER.error("Unable to import component %s:", domain) _LOGGER.error("Unable to import component %s:", domain)
traceback.print_exc() else:
_LOGGER.error("Unable to import component %s:", domain, exc_info=True)
return None return None
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
import traceback _LOGGER.error("Unable to load component %s:", domain, exc_info=True)
_LOGGER.error("Unable to load component %s:", domain)
traceback.print_exc()
return None return None
else: else:
manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform) manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform)

View file

@ -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) pattern = re.compile(r"^([-+]?[0-9]*\.?[0-9]*)\s*(\w*?)" + regex_suffix + r"$", re.UNICODE)
def validator(value): def validator(value):
if optional_unit:
try:
return float_(value)
except Invalid:
pass
match = pattern.match(string(value)) match = pattern.match(string(value))
if match is None: 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)?") voltage = float_with_unit("voltage", u"(v|V|volt|Volts)?")
distance = float_with_unit("distance", u"(m)") distance = float_with_unit("distance", u"(m)")
framerate = float_with_unit("framerate", u"(FPS|fps|Fps|FpS|Hz)") 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_c = float_with_unit("temperature", u"(°C|° C|°|C)?")
_temperature_k = float_with_unit("temperature", u"(° K|° K|K)?") _temperature_k = float_with_unit("temperature", u"(° K|° K|K)?")
_temperature_f = float_with_unit("temperature", u"(°F|° F|F)?") _temperature_f = float_with_unit("temperature", u"(°F|° F|F)?")

View file

@ -470,6 +470,9 @@ ICON_ROTATE_RIGHT = 'mdi:rotate-right'
ICON_SCALE = 'mdi:scale' ICON_SCALE = 'mdi:scale'
ICON_SCREEN_ROTATION = 'mdi:screen-rotation' ICON_SCREEN_ROTATION = 'mdi:screen-rotation'
ICON_SIGNAL = 'mdi:signal' 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_THERMOMETER = 'mdi:thermometer'
ICON_TIMER = 'mdi:timer' ICON_TIMER = 'mdi:timer'
ICON_WATER_PERCENT = 'mdi:water-percent' ICON_WATER_PERCENT = 'mdi:water-percent'

View file

@ -294,8 +294,6 @@ void HighFrequencyLoopRequester::stop() {
bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; } bool HighFrequencyLoopRequester::is_high_frequency() { return high_freq_num_requests > 0; }
float clamp(float val, float min, float max) { float clamp(float val, float min, float max) {
if (min > max)
std::swap(min, max);
if (val < min) if (val < min)
return min; return min;
if (val > max) if (val > max)

View file

@ -254,6 +254,18 @@ template<typename T> class Deduplicator {
T last_value_{}; T last_value_{};
}; };
template<typename T> 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); uint32_t fnv1_hash(const std::string &str);
} // namespace esphome } // namespace esphome

View file

@ -424,10 +424,14 @@ def new_Pvariable(id, # type: ID
return Pvariable(id, rhs) return Pvariable(id, rhs)
def add(expression, # type: Union[SafeExpType, Statement] def add(expression, # type: Union[Expression, Statement]
): ):
# type: (...) -> None # 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) CORE.add(expression)

View file

@ -1,7 +1,7 @@
from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \ from esphome.const import CONF_INVERTED, CONF_MODE, CONF_NUMBER, CONF_SETUP_PRIORITY, \
CONF_UPDATE_INTERVAL, CONF_TYPE_ID CONF_UPDATE_INTERVAL, CONF_TYPE_ID
from esphome.core import coroutine from esphome.core import coroutine, ID
from esphome.cpp_generator import RawExpression, add from esphome.cpp_generator import RawExpression, add, get_variable
from esphome.cpp_types import App, GPIOPin from esphome.cpp_types import App, GPIOPin
@ -42,6 +42,15 @@ def register_component(var, config):
yield var 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): def extract_registry_entry_config(registry, full_config):
# type: (Registry, ConfigType) -> RegistryEntry # type: (Registry, ConfigType) -> RegistryEntry
key, config = next((k, v) for k, v in full_config.items() if k in registry) key, config = next((k, v) for k, v in full_config.items() if k in registry)

View file

@ -4,6 +4,7 @@ global_ns = MockObj('', '')
void = global_ns.namespace('void') void = global_ns.namespace('void')
nullptr = global_ns.namespace('nullptr') nullptr = global_ns.namespace('nullptr')
float_ = global_ns.namespace('float') float_ = global_ns.namespace('float')
double = global_ns.namespace('double')
bool_ = global_ns.namespace('bool') bool_ = global_ns.namespace('bool')
std_ns = global_ns.namespace('std') std_ns = global_ns.namespace('std')
std_string = std_ns.class_('string') std_string = std_ns.class_('string')