From 766f6c045d56e929b3592df52e6166d22b68da3d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 24 Apr 2019 23:49:02 +0200 Subject: [PATCH] Updates --- esphome/automation.py | 18 +- esphome/codegen.py | 2 +- esphome/components/binary_sensor/__init__.py | 10 +- esphome/components/binary_sensor/automation.h | 14 +- esphome/components/climate/__init__.py | 4 +- esphome/components/climate/automation.h | 3 +- esphome/components/cover/__init__.py | 3 +- esphome/components/display/__init__.py | 3 +- esphome/components/fan/__init__.py | 3 +- esphome/components/globals/__init__.py | 26 +- .../components/globals/globals_component.h | 15 + esphome/components/i2c/__init__.py | 5 +- esphome/components/integration/__init__.py | 0 .../integration/integration_sensor.cpp | 68 ++++ .../integration/integration_sensor.h | 85 ++++ esphome/components/integration/sensor.py | 58 +++ esphome/components/json/__init__.py | 2 + esphome/components/light/__init__.py | 375 +----------------- .../components/light/addressable_light.cpp | 84 ++++ esphome/components/light/addressable_light.h | 309 +++++++++------ .../light/addressable_light_effect.h | 46 +-- esphome/components/light/automation.h | 24 ++ esphome/components/light/automation.py | 105 +++++ esphome/components/light/effects.py | 249 ++++++++++++ esphome/components/light/types.py | 38 ++ esphome/components/mqtt/__init__.py | 8 +- esphome/components/sensor/__init__.py | 3 +- esphome/components/spi/__init__.py | 6 +- esphome/components/stepper/__init__.py | 18 +- esphome/components/stepper/stepper.h | 17 + esphome/components/switch/__init__.py | 3 +- esphome/components/text_sensor/__init__.py | 18 +- esphome/components/text_sensor/automation.cpp | 10 - esphome/components/text_sensor/automation.h | 12 + esphome/components/time/__init__.py | 3 +- esphome/config.py | 5 + esphome/config_validation.py | 30 +- esphome/const.py | 2 + esphome/core.py | 3 + esphome/core/base_automation.h | 26 ++ esphome/core/helpers.cpp | 5 + esphome/core/helpers.h | 2 + esphome/core_config.py | 2 +- esphome/cpp_generator.py | 72 +++- 44 files changed, 1202 insertions(+), 592 deletions(-) create mode 100644 esphome/components/integration/__init__.py create mode 100644 esphome/components/integration/integration_sensor.cpp create mode 100644 esphome/components/integration/integration_sensor.h create mode 100644 esphome/components/integration/sensor.py create mode 100644 esphome/components/light/addressable_light.cpp create mode 100644 esphome/components/light/automation.py create mode 100644 esphome/components/light/effects.py create mode 100644 esphome/components/light/types.py delete mode 100644 esphome/components/text_sensor/automation.cpp diff --git a/esphome/automation.py b/esphome/automation.py index 16bdf0daf9..d6d568f7ef 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_AUTOMATION_ID, CONF_CONDITION, CONF_ELSE, CONF_ID, CONF_THEN, \ - CONF_TRIGGER_ID, CONF_TYPE_ID + CONF_TRIGGER_ID, CONF_TYPE_ID, CONF_TIME from esphome.core import coroutine from esphome.util import Registry @@ -55,6 +55,7 @@ UpdateComponentAction = cg.esphome_ns.class_('UpdateComponentAction', Action) Automation = cg.esphome_ns.class_('Automation') LambdaCondition = cg.esphome_ns.class_('LambdaCondition', Condition) +ForCondition = cg.esphome_ns.class_('ForCondition', Condition) def validate_automation(extra_schema=None, extra_validators=None, single=False): @@ -78,6 +79,8 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): try: return cv.Schema([schema])(value) except cv.Invalid as err2: + if u'extra keys not allowed' in str(err2) and len(err2.path) == 2: + raise err if u'Unable to find action' in str(err): raise err2 raise cv.MultipleInvalid([err, err2]) @@ -137,6 +140,19 @@ def lambda_condition_to_code(config, condition_id, template_arg, args): yield cg.new_Pvariable(condition_id, template_arg, lambda_) +@register_condition('for', ForCondition, cv.Schema({ + cv.Required(CONF_TIME): cv.templatable(cv.positive_time_period_milliseconds), + cv.Required(CONF_CONDITION): validate_potentially_and_condition, +}).extend(cv.COMPONENT_SCHEMA)) +def for_condition_to_code(config, condition_id, template_arg, args): + condition = yield build_condition(config[CONF_CONDITION], cg.TemplateArguments(), []) + var = cg.new_Pvariable(condition_id, template_arg, condition) + yield cg.register_component(var, config) + templ = yield cg.templatable(config[CONF_TIME], args, cg.uint32) + cg.add(var.set_time(templ)) + yield var + + @register_action('delay', DelayAction, cv.templatable(cv.positive_time_period_milliseconds)) def delay_action_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) diff --git a/esphome/codegen.py b/esphome/codegen.py index 73e3f9da97..883d5f8636 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -13,7 +13,7 @@ from esphome.cpp_generator import ( # noqa StructInitializer, ArrayInitializer, safe_exp, Statement, progmem_array, statement, variable, Pvariable, new_Pvariable, add, add_global, add_library, add_build_flag, add_define, - get_variable, process_lambda, is_template, templatable, MockObj, + get_variable, get_variable_with_full_id, process_lambda, is_template, templatable, MockObj, MockObjClass) from esphome.cpp_helpers import ( # noqa gpio_pin_expression, register_component, build_registry_entry, diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 6d0f756821..336870392d 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -8,7 +8,7 @@ from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ CONF_MAX_LENGTH, CONF_MIN_LENGTH, CONF_ON_CLICK, \ CONF_ON_DOUBLE_CLICK, CONF_ON_MULTI_CLICK, CONF_ON_PRESS, CONF_ON_RELEASE, CONF_ON_STATE, \ CONF_STATE, CONF_TIMING, CONF_TRIGGER_ID, CONF_FOR, CONF_NAME, CONF_MQTT_ID -from esphome.core import CORE, coroutine +from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.py_compat import string_types from esphome.util import Registry @@ -282,7 +282,8 @@ def new_binary_sensor(config): BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(BinarySensor), - cv.Optional(CONF_FOR): cv.positive_time_period_milliseconds, + cv.Optional(CONF_FOR): cv.invalid("This option has been removed in 1.13, please use the " + "'for' condition instead."), }) @@ -290,16 +291,17 @@ BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id({ BINARY_SENSOR_CONDITION_SCHEMA) def binary_sensor_is_on_to_code(config, condition_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren, True, config.get(CONF_FOR)) + yield cg.new_Pvariable(condition_id, template_arg, paren, True) @automation.register_condition('binary_sensor.is_off', BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA) def binary_sensor_is_off_to_code(config, condition_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) - yield cg.new_Pvariable(condition_id, template_arg, paren, False, config.get(CONF_FOR)) + yield cg.new_Pvariable(condition_id, template_arg, paren, False) +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_BINARY_SENSOR') cg.add_global(binary_sensor_ns.using) diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index 47db748488..e9ff37446d 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -125,22 +125,12 @@ class StateTrigger : public Trigger { template class BinarySensorCondition : public Condition { public: - BinarySensorCondition(BinarySensor *parent, bool state, uint32_t for_time = 0) - : parent_(parent), state_(state), for_time_(for_time) { - parent->add_on_state_callback([this](bool state) { this->last_state_time_ = millis(); }); - } - bool check(Ts... x) override { - if (this->parent_->state != this->state_) - return false; - - return millis() - this->last_state_time_ >= this->for_time_; - } + BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} + bool check(Ts... x) override { return this->parent_->state == this->state_; } protected: BinarySensor *parent_; bool state_; - uint32_t last_state_time_{0}; - uint32_t for_time_{0}; }; template class BinarySensorPublishAction : public Action { diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 976ce38b7a..ba0b37ed8c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -6,7 +6,7 @@ from esphome.const import CONF_AWAY, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATUR CONF_MIN_TEMPERATURE, CONF_MODE, CONF_TARGET_TEMPERATURE, \ CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL, \ CONF_MQTT_ID, CONF_NAME -from esphome.core import CORE, coroutine +from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -15,7 +15,6 @@ climate_ns = cg.esphome_ns.namespace('climate') ClimateDevice = climate_ns.class_('Climate', cg.Nameable) ClimateCall = climate_ns.class_('ClimateCall') ClimateTraits = climate_ns.class_('ClimateTraits') -# MQTTClimateComponent = climate_ns.class_('MQTTClimateComponent', mqtt.MQTTComponent) ClimateMode = climate_ns.enum('ClimateMode') CLIMATE_MODES = { @@ -100,6 +99,7 @@ def climate_control_to_code(config, action_id, template_arg, args): yield var +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_CLIMATE') cg.add_global(climate_ns.using) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index b1668c9d98..845773a0ab 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -18,7 +18,8 @@ template class ControlAction : public Action { void play(Ts... x) override { auto call = this->climate_->make_call(); - call.set_target_temperature(this->mode_.optional_value(x...)); + call.set_mode(this->mode_.optional_value(x...)); + call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); call.set_away(this->away_.optional_value(x...)); diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 041be0a2bc..ed6ebe98fa 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -5,7 +5,7 @@ from esphome.automation import maybe_simple_id, Condition from esphome.components import mqtt from esphome.const import CONF_ID, CONF_INTERNAL, CONF_DEVICE_CLASS, CONF_STATE, \ CONF_POSITION, CONF_TILT, CONF_STOP, CONF_MQTT_ID, CONF_NAME -from esphome.core import CORE, coroutine +from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -124,6 +124,7 @@ def cover_control_to_code(config, action_id, template_arg, args): yield var +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_COVER') cg.add_global(cover_ns.using) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 28f8cdd328..5c204fc7a4 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -4,7 +4,7 @@ import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION, CONF_UPDATE_INTERVAL -from esphome.core import coroutine +from esphome.core import coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -98,5 +98,6 @@ def display_page_show_previous_to_code(config, action_id, template_arg, args): yield cg.new_Pvariable(action_id, template_arg, paren) +@coroutine_with_priority(100.0) def to_code(config): cg.add_global(display_ns.using) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 46ad78946b..ffddf83acc 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -6,7 +6,7 @@ from esphome.components import mqtt from esphome.const import CONF_ID, CONF_INTERNAL, CONF_MQTT_ID, CONF_OSCILLATING, \ CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_SPEED, \ CONF_SPEED_COMMAND_TOPIC, CONF_SPEED_STATE_TOPIC, CONF_NAME -from esphome.core import CORE, coroutine +from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -107,6 +107,7 @@ def fan_turn_on_to_code(config, action_id, template_arg, args): yield var +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_FAN') cg.add_global(fan_ns.using) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 4bcbc80999..4dce6e7583 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -1,23 +1,26 @@ import hashlib -from esphome import config_validation as cv +from esphome import config_validation as cv, automation from esphome import codegen as cg -from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE +from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE +from esphome.core import coroutine_with_priority from esphome.py_compat import IS_PY3 globals_ns = cg.esphome_ns.namespace('globals') GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) +GlobalVarSetAction = globals_ns.class_('GlobalVarSetAction', automation.Action) MULTI_CONF = True - CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_ID): cv.declare_id(GlobalsComponent), cv.Required(CONF_TYPE): cv.string_strict, cv.Optional(CONF_INITIAL_VALUE): cv.string_strict, - cv.Optional(CONF_RESTORE_VALUE): cv.boolean, + cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, }).extend(cv.COMPONENT_SCHEMA) +# Run with low priority so that namespaces are registered first +@coroutine_with_priority(-100.0) def to_code(config): type_ = cg.RawExpression(config[CONF_TYPE]) template_args = cg.TemplateArguments(type_) @@ -31,9 +34,22 @@ def to_code(config): glob = cg.Pvariable(config[CONF_ID], rhs, type=res_type) yield cg.register_component(glob, config) - if config.get(CONF_RESTORE_VALUE, False): + if config[CONF_RESTORE_VALUE]: value = config[CONF_ID].id if IS_PY3 and isinstance(value, str): value = value.encode() hash_ = int(hashlib.md5(value).hexdigest()[:8], 16) cg.add(glob.set_restore_value(hash_)) + + +@automation.register_action('globals.set', GlobalVarSetAction, cv.Schema({ + cv.Required(CONF_ID): cv.use_id(GlobalsComponent), + cv.Required(CONF_VALUE): cv.templatable(cv.string_strict), +})) +def globals_set_to_code(config, action_id, template_arg, args): + full_id, paren = yield cg.get_variable_with_full_id(config[CONF_ID]) + template_arg = cg.TemplateArguments(full_id.type, *template_arg) + var = cg.new_Pvariable(action_id, template_arg, paren) + templ = yield cg.templatable(config[CONF_VALUE], args, None) + cg.add(var.set_value(templ)) + yield var diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index af5435a056..c7d2a18d84 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -8,6 +8,7 @@ namespace globals { template class GlobalsComponent : public Component { public: + using value_type = T; explicit GlobalsComponent() = default; explicit GlobalsComponent(T initial_value) : value_(initial_value) {} explicit GlobalsComponent(std::array::type, std::extent::value> initial_value) { @@ -49,5 +50,19 @@ template class GlobalsComponent : public Component { ESPPreferenceObject rtc_; }; +template class GlobalVarSetAction : public Action { + public: + explicit GlobalVarSetAction(C *parent) : parent_(parent) {} + + using T = typename C::value_type; + + TEMPLATABLE_VALUE(T, value); + + void play(Ts... x) override { this->parent_->value() = this->value_.value(x...); } + + protected: + C *parent_; +}; + } // namespace globals } // namespace esphome diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 733dcc6490..0c71f18019 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_SCAN, CONF_SCL, CONF_SDA, CONF_ADDRESS, \ CONF_I2C_ID -from esphome.core import coroutine +from esphome.core import coroutine, coroutine_with_priority i2c_ns = cg.esphome_ns.namespace('i2c') I2CComponent = i2c_ns.class_('I2CComponent', cg.Component) @@ -20,7 +20,9 @@ CONFIG_SCHEMA = cv.Schema({ }).extend(cv.COMPONENT_SCHEMA) +@coroutine_with_priority(1.0) def to_code(config): + cg.add_global(i2c_ns.using) var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) @@ -29,7 +31,6 @@ def to_code(config): cg.add(var.set_frequency(int(config[CONF_FREQUENCY]))) cg.add(var.set_scan(config[CONF_SCAN])) cg.add_library('Wire', None) - cg.add_global(i2c_ns.using) def i2c_device_schema(default_address): diff --git a/esphome/components/integration/__init__.py b/esphome/components/integration/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp new file mode 100644 index 0000000000..9ddfd2ad0b --- /dev/null +++ b/esphome/components/integration/integration_sensor.cpp @@ -0,0 +1,68 @@ +#include "integration_sensor.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace integration { + +static const char *TAG = "integration"; + +void IntegrationSensor::setup() { + if (this->restore_) { + this->rtc_ = global_preferences.make_preference(this->get_object_id_hash()); + this->rtc_.load(&this->result_); + } + + this->last_update_ = millis(); + this->publish_and_save_(this->result_); + this->sensor_->add_on_state_callback([this](float state) { this->process_sensor_value_(state); }); +} +void IntegrationSensor::dump_config() { LOG_SENSOR("", "Integration Sensor", this); } +std::string IntegrationSensor::unit_of_measurement() { + std::string suffix; + switch (this->time_) { + case INTEGRATION_SENSOR_TIME_MILLISECOND: + suffix = "ms"; + break; + case INTEGRATION_SENSOR_TIME_SECOND: + suffix = "s"; + break; + case INTEGRATION_SENSOR_TIME_MINUTE: + suffix = "min"; + break; + case INTEGRATION_SENSOR_TIME_HOUR: + suffix = "h"; + break; + case INTEGRATION_SENSOR_TIME_DAY: + suffix = "d"; + break; + } + std::string base = this->sensor_->get_unit_of_measurement(); + if (str_endswith(base, "/" + suffix)) { + return base.substr(0, base.size() - suffix.size() - 1); + } + return base + suffix; +} +void IntegrationSensor::process_sensor_value_(float value) { + const uint32_t now = millis(); + const float old_value = this->last_value_; + const float new_value = value; + const uint32_t dt_ms = now - this->last_update_; + const float dt = dt_ms * this->get_time_factor_(); + float area = 0.0f; + switch (this->method_) { + case INTEGRATION_METHOD_TRAPEZOID: + area = dt * (old_value + new_value) / 2.0f; + break; + case INTEGRATION_METHOD_LEFT: + area = dt * old_value; + break; + case INTEGRATION_METHOD_RIGHT: + area = dt * new_value; + break; + } + this->publish_and_save_(this->last_value_ + area); +} + +} // namespace integration +} // namespace esphome diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h new file mode 100644 index 0000000000..6b1f4ccf1b --- /dev/null +++ b/esphome/components/integration/integration_sensor.h @@ -0,0 +1,85 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace integration { + +enum IntegrationSensorTime { + INTEGRATION_SENSOR_TIME_MILLISECOND = 0, + INTEGRATION_SENSOR_TIME_SECOND, + INTEGRATION_SENSOR_TIME_MINUTE, + INTEGRATION_SENSOR_TIME_HOUR, + INTEGRATION_SENSOR_TIME_DAY, +}; + +enum IntegrationMethod { + INTEGRATION_METHOD_TRAPEZOID = 0, + INTEGRATION_METHOD_LEFT, + INTEGRATION_METHOD_RIGHT, +}; + +class IntegrationSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_sensor(Sensor *sensor) { sensor_ = sensor; } + void set_time(IntegrationSensorTime time) { time_ = time; } + void set_method(IntegrationMethod method) { method_ = method; } + void set_restore(bool restore) { restore_ = restore; } + void reset() { this->publish_and_save_(0.0f); } + + protected: + void process_sensor_value_(float value); + float get_time_factor_() { + switch (this->time_) { + case INTEGRATION_SENSOR_TIME_MILLISECOND: + return 1.0f; + case INTEGRATION_SENSOR_TIME_SECOND: + return 1.0f / 1000.0f; + case INTEGRATION_SENSOR_TIME_MINUTE: + return 1.0f / 60000.0f; + case INTEGRATION_SENSOR_TIME_HOUR: + return 1.0f / 3600000.0f; + case INTEGRATION_SENSOR_TIME_DAY: + return 1.0f / 86400000.0f; + default: + return 0.0f; + } + } + void publish_and_save_(float result) { + this->result_ = result; + this->publish_state(result); + this->rtc_.save(&result); + } + std::string unit_of_measurement() override; + std::string icon() override { return this->sensor_->get_icon(); } + int8_t accuracy_decimals() override { return this->sensor_->get_accuracy_decimals() + 2; } + + sensor::Sensor *sensor_; + IntegrationSensorTime time_; + IntegrationMethod method_; + bool restore_; + ESPPreferenceObject rtc_; + + uint32_t last_update_; + float result_{0.0f}; + float last_value_{0.0f}; +}; + +template class ResetAction : public Action { + public: + explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} + + void play(Ts... x) override { this->parent_->reset(); } + + protected: + IntegrationSensor *parent_; +}; + +} // namespace integration +} // namespace esphome diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py new file mode 100644 index 0000000000..13c69f81f2 --- /dev/null +++ b/esphome/components/integration/sensor.py @@ -0,0 +1,58 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_TIME_ID, CONF_SENSOR, CONF_RESTORE + +integration_ns = cg.esphome_ns.namespace('integration') +IntegrationSensor = integration_ns.class_('IntegrationSensor', sensor.Sensor, cg.Component) +ResetAction = integration_ns.class_('ResetAction', automation.Action) + +IntegrationSensorTime = integration_ns.enum('IntegrationSensorTime') +INTEGRATION_TIMES = { + 'ms': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_MILLISECOND, + 's': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_SECOND, + 'min': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_MINUTE, + 'h': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_HOUR, + 'd': IntegrationSensorTime.INTEGRATION_SENSOR_TIME_DAY, +} +IntegrationMethod = integration_ns.enum('IntegrationMethod') +INTEGRATION_METHODS = { + 'trapezoid': IntegrationMethod.INTEGRATION_METHOD_TRAPEZOID, + 'left': IntegrationMethod.INTEGRATION_METHOD_LEFT, + 'right': IntegrationMethod.INTEGRATION_METHOD_RIGHT, +} + +CONF_TIME_UNIT = 'time_unit' +CONF_INTEGRATION_METHOD = 'integration_method' + + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(IntegrationSensor), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_TIME_UNIT): cv.enum(INTEGRATION_TIMES, lower=True), + cv.Optional(CONF_INTEGRATION_METHOD, default='trapezoid'): + cv.enum(INTEGRATION_METHODS, lower=True), + cv.Optional(CONF_RESTORE, default=True): cv.boolean, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + cg.add(var.set_time(config[CONF_TIME_ID])) + cg.add(var.set_method(config[CONF_INTEGRATION_METHOD])) + cg.add(var.set_restore(config[CONF_RESTORE])) + + +@automation.register_action('sensor.integration.reset', ResetAction, automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(IntegrationSensor), +})) +def sensor_integration_reset_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index f6a4a7f294..f719b05340 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,8 +1,10 @@ import esphome.codegen as cg +from esphome.core import coroutine_with_priority json_ns = cg.esphome_ns.namespace('json') +@coroutine_with_priority(1.0) def to_code(config): cg.add_library('ArduinoJson-esphomelib', '5.13.3') cg.add_define('USE_JSON') diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index db0601496a..858475f8cf 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,296 +1,17 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import automation -from esphome.automation import maybe_simple_id from esphome.components import mqtt -from esphome.const import CONF_ALPHA, CONF_BLUE, CONF_BRIGHTNESS, CONF_COLORS, CONF_COLOR_CORRECT, \ - CONF_COLOR_TEMPERATURE, CONF_DEFAULT_TRANSITION_LENGTH, CONF_DURATION, CONF_EFFECT, \ - CONF_EFFECTS, CONF_FLASH_LENGTH, CONF_GAMMA_CORRECT, CONF_GREEN, CONF_ID, \ - CONF_INTERNAL, CONF_LAMBDA, CONF_NAME, CONF_NUM_LEDS, CONF_RANDOM, CONF_RED, \ - CONF_SPEED, CONF_STATE, CONF_TRANSITION_LENGTH, CONF_UPDATE_INTERVAL, CONF_WHITE, CONF_WIDTH, \ - CONF_MQTT_ID -from esphome.core import coroutine -from esphome.util import Registry +from esphome.const import CONF_COLOR_CORRECT, \ + CONF_DEFAULT_TRANSITION_LENGTH, CONF_EFFECTS, CONF_GAMMA_CORRECT, CONF_ID, \ + CONF_INTERNAL, CONF_NAME, CONF_MQTT_ID +from esphome.core import coroutine, coroutine_with_priority +from .automation import light_control_to_code # noqa +from .effects import validate_effects, BINARY_EFFECTS, \ + MONOCHROMATIC_EFFECTS, RGB_EFFECTS, ADDRESSABLE_EFFECTS, EFFECTS_REGISTRY +from .types import ( # noqa + LightState, AddressableLightState, light_ns, LightOutput, AddressableLight) IS_PLATFORM_COMPONENT = True - -# Base -light_ns = cg.esphome_ns.namespace('light') -LightState = light_ns.class_('LightState', cg.Nameable, cg.Component) -# Fake class for addressable lights -AddressableLightState = light_ns.class_('LightState', LightState) -LightOutput = light_ns.class_('LightOutput') -AddressableLight = light_ns.class_('AddressableLight') -AddressableLightRef = AddressableLight.operator('ref') - -# Actions -ToggleAction = light_ns.class_('ToggleAction', automation.Action) -LightControlAction = light_ns.class_('LightControlAction', automation.Action) - -LightColorValues = light_ns.class_('LightColorValues') - -# Effects -LightEffect = light_ns.class_('LightEffect') -RandomLightEffect = light_ns.class_('RandomLightEffect', LightEffect) -LambdaLightEffect = light_ns.class_('LambdaLightEffect', LightEffect) -StrobeLightEffect = light_ns.class_('StrobeLightEffect', LightEffect) -StrobeLightEffectColor = light_ns.class_('StrobeLightEffectColor', LightEffect) -FlickerLightEffect = light_ns.class_('FlickerLightEffect', LightEffect) -AddressableLightEffect = light_ns.class_('AddressableLightEffect', LightEffect) -AddressableLambdaLightEffect = light_ns.class_('AddressableLambdaLightEffect', - AddressableLightEffect) -AddressableRainbowLightEffect = light_ns.class_('AddressableRainbowLightEffect', - AddressableLightEffect) -AddressableColorWipeEffect = light_ns.class_('AddressableColorWipeEffect', AddressableLightEffect) -AddressableColorWipeEffectColor = light_ns.struct('AddressableColorWipeEffectColor') -AddressableScanEffect = light_ns.class_('AddressableScanEffect', AddressableLightEffect) -AddressableTwinkleEffect = light_ns.class_('AddressableTwinkleEffect', AddressableLightEffect) -AddressableRandomTwinkleEffect = light_ns.class_('AddressableRandomTwinkleEffect', - AddressableLightEffect) -AddressableFireworksEffect = light_ns.class_('AddressableFireworksEffect', AddressableLightEffect) -AddressableFlickerEffect = light_ns.class_('AddressableFlickerEffect', AddressableLightEffect) - -CONF_STROBE = 'strobe' -CONF_FLICKER = 'flicker' -CONF_ADDRESSABLE_LAMBDA = 'addressable_lambda' -CONF_ADDRESSABLE_RAINBOW = 'addressable_rainbow' -CONF_ADDRESSABLE_COLOR_WIPE = 'addressable_color_wipe' -CONF_ADDRESSABLE_SCAN = 'addressable_scan' -CONF_ADDRESSABLE_TWINKLE = 'addressable_twinkle' -CONF_ADDRESSABLE_RANDOM_TWINKLE = 'addressable_random_twinkle' -CONF_ADDRESSABLE_FIREWORKS = 'addressable_fireworks' -CONF_ADDRESSABLE_FLICKER = 'addressable_flicker' - -CONF_ADD_LED_INTERVAL = 'add_led_interval' -CONF_REVERSE = 'reverse' -CONF_MOVE_INTERVAL = 'move_interval' -CONF_TWINKLE_PROBABILITY = 'twinkle_probability' -CONF_PROGRESS_INTERVAL = 'progress_interval' -CONF_SPARK_PROBABILITY = 'spark_probability' -CONF_USE_RANDOM_COLOR = 'use_random_color' -CONF_FADE_OUT_RATE = 'fade_out_rate' -CONF_INTENSITY = 'intensity' - -BINARY_EFFECTS = [CONF_LAMBDA, CONF_STROBE] -MONOCHROMATIC_EFFECTS = BINARY_EFFECTS + [CONF_FLICKER] -RGB_EFFECTS = MONOCHROMATIC_EFFECTS + [CONF_RANDOM] -ADDRESSABLE_EFFECTS = RGB_EFFECTS + [CONF_ADDRESSABLE_LAMBDA, CONF_ADDRESSABLE_RAINBOW, - CONF_ADDRESSABLE_COLOR_WIPE, CONF_ADDRESSABLE_SCAN, - CONF_ADDRESSABLE_TWINKLE, CONF_ADDRESSABLE_RANDOM_TWINKLE, - CONF_ADDRESSABLE_FIREWORKS, CONF_ADDRESSABLE_FLICKER] - -EFFECTS_REGISTRY = Registry() - - -def register_effect(name, effect_type, default_name, schema, *extra_validators): - schema = cv.Schema(schema).extend({ - cv.Optional(CONF_NAME, default=default_name): cv.string_strict, - }) - validator = cv.All(schema, *extra_validators) - return EFFECTS_REGISTRY.register(name, effect_type, validator) - - -@register_effect('lambda', LambdaLightEffect, "Lambda", { - cv.Required(CONF_LAMBDA): cv.lambda_, - cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.update_interval, -}) -def lambda_effect_to_code(config, effect_id): - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.void) - yield cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, - config[CONF_UPDATE_INTERVAL]) - - -@register_effect('random', RandomLightEffect, "Random", { - cv.Optional(CONF_TRANSITION_LENGTH, default='7.5s'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_UPDATE_INTERVAL, default='10s'): cv.positive_time_period_milliseconds, -}) -def random_effect_to_code(config, effect_id): - effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) - cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) - yield effect - - -@register_effect('strobe', StrobeLightEffect, "Strobe", { - cv.Optional(CONF_COLORS, default=[ - {CONF_STATE: True, CONF_DURATION: '0.5s'}, - {CONF_STATE: False, CONF_DURATION: '0.5s'}, - ]): cv.All(cv.ensure_list(cv.Schema({ - cv.Optional(CONF_STATE, default=True): cv.boolean, - cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, - cv.Optional(CONF_RED, default=1.0): cv.percentage, - cv.Optional(CONF_GREEN, default=1.0): cv.percentage, - cv.Optional(CONF_BLUE, default=1.0): cv.percentage, - cv.Optional(CONF_WHITE, default=1.0): cv.percentage, - cv.Required(CONF_DURATION): cv.positive_time_period_milliseconds, - }), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, - CONF_WHITE)), cv.Length(min=2)), -}) -def strobe_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - colors = [] - for color in config.get(CONF_COLORS, []): - colors.append(cg.StructInitializer( - StrobeLightEffectColor, - ('color', LightColorValues(color[CONF_STATE], color[CONF_BRIGHTNESS], - color[CONF_RED], color[CONF_GREEN], color[CONF_BLUE], - color[CONF_WHITE])), - ('duration', color[CONF_DURATION]), - )) - cg.add(var.set_colors(colors)) - yield var - - -@register_effect('flicker', FlickerLightEffect, "Flicker", { - cv.Optional(CONF_ALPHA, default=0.95): cv.percentage, - cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage, -}) -def flicker_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_alpha(config[CONF_ALPHA])) - cg.add(var.set_intensity(config[CONF_INTENSITY])) - yield var - - -@register_effect('addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { - cv.Required(CONF_LAMBDA): cv.lambda_, - cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, -}) -def addressable_lambda_effect_to_code(config, effect_id): - args = [(AddressableLightRef, 'it')] - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) - var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, - config[CONF_UPDATE_INTERVAL]) - yield var - - -@register_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { - cv.Optional(CONF_SPEED, default=10): cv.uint32_t, - cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, -}) -def addressable_rainbow_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_speed(config[CONF_SPEED])) - cg.add(var.set_width(config[CONF_WIDTH])) - yield var - - -@register_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { - cv.Optional(CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]): cv.ensure_list({ - cv.Optional(CONF_RED, default=1.0): cv.percentage, - cv.Optional(CONF_GREEN, default=1.0): cv.percentage, - cv.Optional(CONF_BLUE, default=1.0): cv.percentage, - cv.Optional(CONF_WHITE, default=1.0): cv.percentage, - cv.Optional(CONF_RANDOM, default=False): cv.boolean, - cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)), - }), - cv.Optional(CONF_ADD_LED_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_REVERSE, default=False): cv.boolean, -}) -def addressable_color_wipe_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL])) - cg.add(var.set_reverse(config[CONF_REVERSE])) - colors = [] - for color in config.get(CONF_COLORS, []): - colors.append(cg.StructInitializer( - AddressableColorWipeEffectColor, - ('r', int(round(color[CONF_RED] * 255))), - ('g', int(round(color[CONF_GREEN] * 255))), - ('b', int(round(color[CONF_BLUE] * 255))), - ('w', int(round(color[CONF_WHITE] * 255))), - ('random', color[CONF_RANDOM]), - ('num_leds', color[CONF_NUM_LEDS]), - )) - cg.add(var.set_colors(colors)) - yield var - - -@register_effect('addressable_scan', AddressableScanEffect, "Scan", { - cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, -}) -def addressable_scan_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL])) - yield var - - -@register_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { - cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, - cv.Optional(CONF_PROGRESS_INTERVAL, default='4ms'): cv.positive_time_period_milliseconds, -}) -def addressable_twinkle_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) - cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) - yield var - - -@register_effect('addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { - cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, - cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, -}) -def addressable_random_twinkle_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) - cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) - yield var - - -@register_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { - cv.Optional(CONF_UPDATE_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_SPARK_PROBABILITY, default='10%'): cv.percentage, - cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean, - cv.Optional(CONF_FADE_OUT_RATE, default=120): cv.uint8_t, -}) -def addressable_fireworks_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) - cg.add(var.set_spark_probability(config[CONF_SPARK_PROBABILITY])) - cg.add(var.set_use_random_color(config[CONF_USE_RANDOM_COLOR])) - cg.add(var.set_fade_out_rate(config[CONF_FADE_OUT_RATE])) - yield var - - -@register_effect('addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { - cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, - cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, -}) -def addressable_flicker_effect_to_code(config, effect_id): - var = cg.new_Pvariable(effect_id, config[CONF_NAME]) - cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) - cg.add(var.set_intensity(config[CONF_INTENSITY])) - yield var - - -def validate_effects(allowed_effects): - def validator(value): - value = cv.validate_registry('effect', EFFECTS_REGISTRY)(value) - errors = [] - names = set() - for i, x in enumerate(value): - key = next(it for it in x.keys()) - if key not in allowed_effects: - errors.append( - cv.Invalid("The effect '{}' is not allowed for this " - "light type".format(key), [i]) - ) - continue - name = x[key][CONF_NAME] - if name in names: - errors.append( - cv.Invalid(u"Found the effect name '{}' twice. All effects must have " - u"unique names".format(name), [i]) - ) - continue - names.add(name) - if errors: - raise cv.MultipleInvalid(errors) - return value - - return validator - - LIGHT_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(LightState), cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_id(mqtt.MQTTJSONLightComponent), @@ -344,83 +65,7 @@ def register_light(output_var, config): yield setup_light_core_(light_var, output_var, config) -@automation.register_action('light.toggle', ToggleAction, maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), -})) -def light_toggle_to_code(config, action_id, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if CONF_TRANSITION_LENGTH in config: - template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) - cg.add(var.set_transition_length(template_)) - yield var - - -LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Optional(CONF_STATE): cv.templatable(cv.boolean), - cv.Exclusive(CONF_TRANSITION_LENGTH, 'transformer'): - cv.templatable(cv.positive_time_period_milliseconds), - cv.Exclusive(CONF_FLASH_LENGTH, 'transformer'): - cv.templatable(cv.positive_time_period_milliseconds), - cv.Exclusive(CONF_EFFECT, 'transformer'): cv.templatable(cv.string), - cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), - cv.Optional(CONF_RED): cv.templatable(cv.percentage), - cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), - cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), - cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), - cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature), -}) -LIGHT_TURN_OFF_ACTION_SCHEMA = maybe_simple_id({ - cv.Required(CONF_ID): cv.use_id(LightState), - cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), - cv.Optional(CONF_STATE, default=False): False, -}) -LIGHT_TURN_ON_ACTION_SCHEMA = maybe_simple_id(LIGHT_CONTROL_ACTION_SCHEMA.extend({ - cv.Optional(CONF_STATE, default=True): True, -})) - - -@automation.register_action('light.turn_off', LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA) -@automation.register_action('light.turn_on', LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA) -@automation.register_action('light.control', LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA) -def light_control_to_code(config, var, template_arg, args): - paren = yield cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(var, template_arg, paren) - if CONF_STATE in config: - template_ = yield cg.templatable(config[CONF_STATE], args, bool) - cg.add(var.set_state(template_)) - if CONF_TRANSITION_LENGTH in config: - template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) - cg.add(var.set_transition_length(template_)) - if CONF_FLASH_LENGTH in config: - template_ = yield cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) - cg.add(var.set_flash_length(template_)) - if CONF_BRIGHTNESS in config: - template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, float) - cg.add(var.set_brightness(template_)) - if CONF_RED in config: - template_ = yield cg.templatable(config[CONF_RED], args, float) - cg.add(var.set_red(template_)) - if CONF_GREEN in config: - template_ = yield cg.templatable(config[CONF_GREEN], args, float) - cg.add(var.set_green(template_)) - if CONF_BLUE in config: - template_ = yield cg.templatable(config[CONF_BLUE], args, float) - cg.add(var.set_blue(template_)) - if CONF_WHITE in config: - template_ = yield cg.templatable(config[CONF_WHITE], args, float) - cg.add(var.set_white(template_)) - if CONF_COLOR_TEMPERATURE in config: - template_ = yield cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) - cg.add(var.set_color_temperature(template_)) - if CONF_EFFECT in config: - template_ = yield cg.templatable(config[CONF_EFFECT], args, cg.std_string) - cg.add(var.set_effect(template_)) - yield var - - +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_LIGHT') cg.add_global(light_ns.using) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp new file mode 100644 index 0000000000..f794b4b17f --- /dev/null +++ b/esphome/components/light/addressable_light.cpp @@ -0,0 +1,84 @@ +#include "addressable_light.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace light { + +ESPColor ESPHSVColor::to_rgb() const { + // based on FastLED's hsv rainbow to rgb + const uint8_t hue = this->hue; + const uint8_t sat = this->saturation; + const uint8_t val = this->value; + // upper 3 hue bits are for branch selection, lower 5 are for values + const uint8_t offset8 = (hue & 0x1F) << 3; // 0..248 + // third of the offset, 255/3 = 85 (actually only up to 82; 164) + const uint8_t third = esp_scale8(offset8, 85); + const uint8_t two_thirds = esp_scale8(offset8, 170); + ESPColor rgb(255, 255, 255, 0); + switch (hue >> 5) { + case 0b000: + rgb.r = 255 - third; + rgb.g = third; + rgb.b = 0; + break; + case 0b001: + rgb.r = 171; + rgb.g = 85 + third; + rgb.b = 0; + break; + case 0b010: + rgb.r = 171 - two_thirds; + rgb.g = 170 + third; + rgb.b = 0; + break; + case 0b011: + rgb.r = 0; + rgb.g = 255 - third; + rgb.b = third; + break; + case 0b100: + rgb.r = 0; + rgb.g = 171 - two_thirds; + rgb.b = 85 + two_thirds; + break; + case 0b101: + rgb.r = third; + rgb.g = 0; + rgb.b = 255 - third; + break; + case 0b110: + rgb.r = 85 + third; + rgb.g = 0; + rgb.b = 171 - third; + break; + case 0b111: + rgb.r = 170 + third; + rgb.g = 0; + rgb.b = 85 - third; + break; + default: + break; + } + // low saturation -> add uniform color to orig. hue + // high saturation -> use hue directly + // scales with square of saturation + // (r,g,b) = (r,g,b) * sat + (1 - sat)^2 + rgb *= sat; + const uint8_t desat = 255 - sat; + rgb += esp_scale8(desat, desat); + // (r,g,b) = (r,g,b) * val + rgb *= val; + return rgb; +} + +void ESPRangeView::set(const ESPColor &color) { + for (int32_t i = this->begin_; i < this->end_; i++) { + (*this->parent_)[i] = color; + } +} +ESPColorView ESPRangeView::operator[](int32_t index) const { return (*this->parent_)[index]; } + +ESPColorView ESPRangeView::Iterator::operator*() const { return (*this->range_->parent_)[this->i_]; } + +} // namespace light +} // namespace esphome diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index a46d5d46da..904f507e1d 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -143,6 +143,10 @@ struct ESPColor { return ESPColor(uint8_t((uint16_t(r) * 255U / max_rgb)), uint8_t((uint16_t(g) * 255U / max_rgb)), uint8_t((uint16_t(b) * 255U / max_rgb)), w); } + ESPColor fade_to_white(uint8_t amnt) { return ESPColor(255, 255, 255, 255) - (*this * amnt); } + ESPColor fade_to_black(uint8_t amnt) { return *this * amnt; } + ESPColor lighten(uint8_t delta) { return *this + delta; } + ESPColor darken(uint8_t delta) { return *this - delta; } }; struct ESPHSVColor { @@ -168,72 +172,7 @@ struct ESPHSVColor { inline ESPHSVColor(uint8_t hue, uint8_t saturation, uint8_t value) ALWAYS_INLINE : hue(hue), saturation(saturation), value(value) {} - ESPColor to_rgb() const { - // based on FastLED's hsv rainbow to rgb - const uint8_t hue = this->hue; - const uint8_t sat = this->saturation; - const uint8_t val = this->value; - // upper 3 hue bits are for branch selection, lower 5 are for values - const uint8_t offset8 = (hue & 0x1F) << 3; // 0..248 - // third of the offset, 255/3 = 85 (actually only up to 82; 164) - const uint8_t third = esp_scale8(offset8, 85); - const uint8_t two_thirds = esp_scale8(offset8, 170); - ESPColor rgb(255, 255, 255, 0); - switch (hue >> 5) { - case 0b000: - rgb.r = 255 - third; - rgb.g = third; - rgb.b = 0; - break; - case 0b001: - rgb.r = 171; - rgb.g = 85 + third; - rgb.b = 0; - break; - case 0b010: - rgb.r = 171 - two_thirds; - rgb.g = 170 + third; - rgb.b = 0; - break; - case 0b011: - rgb.r = 0; - rgb.g = 255 - third; - rgb.b = third; - break; - case 0b100: - rgb.r = 0; - rgb.g = 171 - two_thirds; - rgb.b = 85 + two_thirds; - break; - case 0b101: - rgb.r = third; - rgb.g = 0; - rgb.b = 255 - third; - break; - case 0b110: - rgb.r = 85 + third; - rgb.g = 0; - rgb.b = 171 - third; - break; - case 0b111: - rgb.r = 170 + third; - rgb.g = 0; - rgb.b = 85 - third; - break; - default: - break; - } - // low saturation -> add uniform color to orig. hue - // high saturation -> use hue directly - // scales with square of saturation - // (r,g,b) = (r,g,b) * sat + (1 - sat)^2 - rgb *= sat; - const uint8_t desat = 255 - sat; - rgb += esp_scale8(desat, desat); - // (r,g,b) = (r,g,b) * val - rgb *= val; - return rgb; - } + ESPColor to_rgb() const; }; class ESPColorCorrection { @@ -321,75 +260,85 @@ class ESPColorCorrection { uint8_t local_brightness_{255}; }; -class ESPColorView { +class ESPColorSettable { public: - inline ESPColorView(uint8_t *red, uint8_t *green, uint8_t *blue, uint8_t *white, uint8_t *effect_data, - const ESPColorCorrection *color_correction) ALWAYS_INLINE : red_(red), - green_(green), - blue_(blue), - white_(white), - effect_data_(effect_data), - color_correction_(color_correction) {} - inline const ESPColorView &operator=(const ESPColor &rhs) const ALWAYS_INLINE { - this->set(rhs); - return *this; - } - inline const ESPColorView &operator=(const ESPHSVColor &rhs) const ALWAYS_INLINE { - this->set(rhs); - return *this; - } - inline void set(const ESPColor &color) const ALWAYS_INLINE { this->set_rgbw(color.r, color.g, color.b, color.w); } - inline void set(const ESPHSVColor &color) const ALWAYS_INLINE { + virtual void set(const ESPColor &color) = 0; + virtual void set_red(uint8_t red) = 0; + virtual void set_green(uint8_t green) = 0; + virtual void set_blue(uint8_t blue) = 0; + virtual void set_white(uint8_t white) = 0; + virtual void set_effect_data(uint8_t effect_data) = 0; + virtual void fade_to_white(uint8_t amnt) = 0; + virtual void fade_to_black(uint8_t amnt) = 0; + virtual void lighten(uint8_t delta) = 0; + virtual void darken(uint8_t delta) = 0; + void set(const ESPHSVColor &color) { this->set_hsv(color); } + void set_hsv(const ESPHSVColor &color) { ESPColor rgb = color.to_rgb(); this->set_rgb(rgb.r, rgb.g, rgb.b); } - inline void set_red(uint8_t red) const ALWAYS_INLINE { - *this->red_ = this->color_correction_->color_correct_red(red); - } - inline void set_green(uint8_t green) const ALWAYS_INLINE { - *this->green_ = this->color_correction_->color_correct_green(green); - } - inline void set_blue(uint8_t blue) const ALWAYS_INLINE { - *this->blue_ = this->color_correction_->color_correct_blue(blue); - } - inline void set_white(uint8_t white) const ALWAYS_INLINE { - if (this->white_ == nullptr) - return; - *this->white_ = this->color_correction_->color_correct_white(white); - } - inline void set_rgb(uint8_t red, uint8_t green, uint8_t blue) const ALWAYS_INLINE { + void set_rgb(uint8_t red, uint8_t green, uint8_t blue) { this->set_red(red); this->set_green(green); this->set_blue(blue); } - inline void set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) const ALWAYS_INLINE { + void set_rgbw(uint8_t red, uint8_t green, uint8_t blue, uint8_t white) { this->set_rgb(red, green, blue); this->set_white(white); } - inline void set_effect_data(uint8_t effect_data) const ALWAYS_INLINE { +}; + +class ESPColorView : public ESPColorSettable { + public: + ESPColorView(uint8_t *red, uint8_t *green, uint8_t *blue, uint8_t *white, uint8_t *effect_data, + const ESPColorCorrection *color_correction) + : red_(red), + green_(green), + blue_(blue), + white_(white), + effect_data_(effect_data), + color_correction_(color_correction) {} + ESPColorView &operator=(const ESPColor &rhs) { + this->set(rhs); + return *this; + } + ESPColorView &operator=(const ESPHSVColor &rhs) { + this->set_hsv(rhs); + return *this; + } + void set(const ESPColor &color) override { this->set_rgbw(color.r, color.g, color.b, color.w); } + void set_red(uint8_t red) override { *this->red_ = this->color_correction_->color_correct_red(red); } + void set_green(uint8_t green) override { *this->green_ = this->color_correction_->color_correct_green(green); } + void set_blue(uint8_t blue) override { *this->blue_ = this->color_correction_->color_correct_blue(blue); } + void set_white(uint8_t white) override { + if (this->white_ == nullptr) + return; + *this->white_ = this->color_correction_->color_correct_white(white); + } + void set_effect_data(uint8_t effect_data) override { if (this->effect_data_ == nullptr) return; *this->effect_data_ = effect_data; } - inline ESPColor get() const ALWAYS_INLINE { - return ESPColor(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); - } - inline uint8_t get_red() const ALWAYS_INLINE { return this->color_correction_->color_uncorrect_red(*this->red_); } - inline uint8_t get_green() const ALWAYS_INLINE { - return this->color_correction_->color_uncorrect_green(*this->green_); - } - inline uint8_t get_blue() const ALWAYS_INLINE { return this->color_correction_->color_uncorrect_blue(*this->blue_); } - inline uint8_t get_white() const ALWAYS_INLINE { + void fade_to_white(uint8_t amnt) override { this->set(this->get().fade_to_white(amnt)); } + void fade_to_black(uint8_t amnt) override { this->set(this->get().fade_to_black(amnt)); } + void lighten(uint8_t delta) override { this->set(this->get().lighten(delta)); } + void darken(uint8_t delta) override { this->set(this->get().darken(delta)); } + ESPColor get() const { return ESPColor(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); } + uint8_t get_red() const { return this->color_correction_->color_uncorrect_red(*this->red_); } + uint8_t get_green() const { return this->color_correction_->color_uncorrect_green(*this->green_); } + uint8_t get_blue() const { return this->color_correction_->color_uncorrect_blue(*this->blue_); } + uint8_t get_white() const { if (this->white_ == nullptr) return 0; return this->color_correction_->color_uncorrect_white(*this->white_); } - inline uint8_t get_effect_data() const ALWAYS_INLINE { + uint8_t get_effect_data() const { if (this->effect_data_ == nullptr) return 0; return *this->effect_data_; } - inline void raw_set_color_correction(const ESPColorCorrection *color_correction) ALWAYS_INLINE { + void raw_set_color_correction(const ESPColorCorrection *color_correction) { this->color_correction_ = color_correction; } @@ -402,11 +351,142 @@ class ESPColorView { const ESPColorCorrection *color_correction_; }; +class AddressableLight; + +class ESPRangeView : public ESPColorSettable { + public: + class Iterator { + public: + Iterator(ESPRangeView *range, int32_t i) : range_(range), i_(i) {} + Iterator operator++() { + this->i_++; + return *this; + } + bool operator!=(const Iterator &other) const { return this->i_ != other.i_; } + ESPColorView operator*() const; + + protected: + ESPRangeView *range_; + int32_t i_; + }; + + ESPRangeView(AddressableLight *parent, int32_t begin, int32_t an_end) : parent_(parent), begin_(begin), end_(an_end) { + if (this->end_ < this->begin_) { + this->end_ = this->begin_; + } + } + + ESPColorView operator[](int32_t index) const; + Iterator begin() { return {this, this->begin_}; } + Iterator end() { return {this, this->end_}; } + + void set(const ESPColor &color) override; + ESPRangeView &operator=(const ESPColor &rhs) { + this->set(rhs); + return *this; + } + ESPRangeView &operator=(const ESPHSVColor &rhs) { + this->set_hsv(rhs); + return *this; + } + ESPRangeView &operator=(const ESPRangeView &rhs) { + // If size doesn't match, error (todo warning) + if (rhs.size() != this->size()) + return *this; + + if (this->parent_ != rhs.parent_) { + for (int32_t i = 0; i < this->size(); i++) + (*this)[i].set(rhs[i].get()); + return *this; + } + + // If both equal, already done + if (rhs.begin_ == this->begin_) + return *this; + + if (rhs.begin_ < this->begin_) { + // Copy into rhs + for (int32_t i = 0; i < this->size(); i++) + rhs[i].set((*this)[i].get()); + } else { + // Copy into this + for (int32_t i = 0; i < this->size(); i++) + (*this)[i].set(rhs[i].get()); + } + + return *this; + } + void set_red(uint8_t red) override { + for (auto c : *this) + c.set_red(red); + } + void set_green(uint8_t green) override { + for (auto c : *this) + c.set_green(green); + } + void set_blue(uint8_t blue) override { + for (auto c : *this) + c.set_blue(blue); + } + void set_white(uint8_t white) override { + for (auto c : *this) + c.set_white(white); + } + void set_effect_data(uint8_t effect_data) override { + for (auto c : *this) + c.set_effect_data(effect_data); + } + void fade_to_white(uint8_t amnt) override { + for (auto c : *this) + c.fade_to_white(amnt); + } + void fade_to_black(uint8_t amnt) override { + for (auto c : *this) + c.fade_to_white(amnt); + } + void lighten(uint8_t delta) override { + for (auto c : *this) + c.lighten(delta); + } + void darken(uint8_t delta) override { + for (auto c : *this) + c.darken(delta); + } + int32_t size() const { return this->end_ - this->begin_; } + + protected: + AddressableLight *parent_; + int32_t begin_; + int32_t end_; +}; + class AddressableLight : public LightOutput { public: virtual int32_t size() const = 0; virtual ESPColorView operator[](int32_t index) const = 0; virtual void clear_effect_data() = 0; + ESPRangeView range(int32_t from, int32_t to) { return ESPRangeView(this, from, to); } + ESPRangeView all() { return ESPRangeView(this, 0, this->size()); } + ESPRangeView::Iterator begin() { return this->all().begin(); } + ESPRangeView::Iterator end() { return this->all().end(); } + void shift_left(int32_t amnt) { + if (amnt < 0) { + this->shift_right(-amnt); + return; + } + if (amnt > this->size()) + amnt = this->size(); + this->range(0, this->size() - amnt) = this->range(amnt, this->size()); + } + void shift_right(int32_t amnt) { + if (amnt < 0) { + this->shift_left(-amnt); + return; + } + if (amnt > this->size()) + amnt = this->size(); + this->range(amnt, this->size()) = this->range(0, this->size() - amnt); + } bool is_effect_active() const { return this->effect_active_; } void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } void write_state(LightState *state) override { @@ -423,10 +503,7 @@ class AddressableLight : public LightOutput { // white is not affected by brightness; so manually scale by state uint8_t(roundf(val.get_white() * val.get_state() * 255.0f))); - for (int i = 0; i < this->size(); i++) { - (*this)[i] = color; - } - + this->all() = color; this->schedule_show(); } void set_correction(float red, float green, float blue, float white = 1.0f) { diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 4218a8f54c..ec95e714e1 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -76,9 +76,9 @@ class AddressableRainbowLightEffect : public AddressableLightEffect { hsv.saturation = 240; uint16_t hue = (millis() * this->speed_) % 0xFFFF; const uint16_t add = 0xFFFF / this->width_; - for (int i = 0; i < it.size(); i++) { + for (auto var : it) { hsv.hue = hue >> 8; - it[i] = hsv; + var = hsv; hue += add; } } @@ -107,15 +107,10 @@ class AddressableColorWipeEffect : public AddressableLightEffect { if (now - this->last_add_ < this->add_led_interval_) return; this->last_add_ = now; - if (!this->reverse_) { - for (int i = 0; i < it.size() - 1; i++) { - it[i] = it[i + 1].get(); - } - } else { - for (int i = it.size() - 1; i > 0; i--) { - it[i] = it[i - 1].get(); - } - } + if (this->reverse_) + it.shift_left(1); + else + it.shift_right(1); const AddressableColorWipeEffectColor color = this->colors_[this->at_color_]; const ESPColor esp_color = ESPColor(color.r, color.g, color.b, color.w); if (!this->reverse_) { @@ -149,18 +144,14 @@ class AddressableScanEffect : public AddressableLightEffect { public: explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } - void apply(AddressableLight &addressable, const ESPColor ¤t_color) override { - for (int i = 0; i < addressable.size(); i++) { - if (i == this->at_led_) - addressable[i] = current_color; - else - addressable[i] = ESPColor(0, 0, 0, 0); - } + void apply(AddressableLight &it, const ESPColor ¤t_color) override { + it.all() = ESPColor(0, 0, 0, 0); + it[this->at_led_] = current_color; const uint32_t now = millis(); if (now - this->last_move_ > this->move_interval_) { if (direction_) { this->at_led_++; - if (this->at_led_ == addressable.size() - 1) + if (this->at_led_ == it.size() - 1) this->direction_ = false; } else { this->at_led_--; @@ -189,8 +180,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { pos_add = pos_add32; this->last_progress_ += pos_add32 * this->progress_interval_; } - for (int i = 0; i < addressable.size(); i++) { - ESPColorView view = addressable[i]; + for (auto view : addressable) { if (view.get_effect_data() != 0) { const uint8_t sine = half_sin8(view.get_effect_data()); view = current_color * sine; @@ -230,8 +220,8 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { this->last_progress_ = now; } uint8_t subsine = ((8 * (now - this->last_progress_)) / this->progress_interval_) & 0b111; - for (int i = 0; i < it.size(); i++) { - ESPColorView view = it[i]; + for (auto &&i : it) { + ESPColorView view = i; if (view.get_effect_data() != 0) { const uint8_t x = (view.get_effect_data() >> 3) & 0b11111; const uint8_t color = view.get_effect_data() & 0b111; @@ -282,11 +272,11 @@ class AddressableFireworksEffect : public AddressableLightEffect { this->last_update_ = now; // "invert" the fade out parameter so that higher values make fade out faster const uint8_t fade_out_mult = 255u - this->fade_out_rate_; - for (int i = 0; i < it.size(); i++) { - ESPColor target = it[i].get() * fade_out_mult; + for (auto view : it) { + ESPColor target = view.get() * fade_out_mult; if (target.r < 64) target *= 170; - it[i] = target; + view = target; } int last = it.size() - 1; it[0].set(it[0].get() + (it[1].get() * 128)); @@ -326,9 +316,9 @@ class AddressableFlickerEffect : public AddressableLightEffect { return; this->last_update_ = now; fast_random_set_seed(random_uint32()); - for (int i = 0; i < it.size(); i++) { + for (auto var : it) { const uint8_t flicker = fast_random_8() % this->intensity_; - it[i] = (it[i].get() * delta_intensity) + (current_color * flicker); + var = (var.get() * delta_intensity) + (current_color * flicker); } } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index c6ea678f3c..4d6e1c094a 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -56,6 +56,30 @@ template class LightControlAction : public Action { LightState *parent_; }; +template class DimRelativeAction : public Action { + public: + explicit DimRelativeAction(LightState *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(float, relative_brightness) + TEMPLATABLE_VALUE(uint32_t, transition_length) + + void play(Ts... x) override { + auto call = this->parent_->make_call(); + float rel = this->relative_brightness_.value(x...); + float cur; + this->parent_->remote_values.as_brightness(&cur); + float new_brightness = clamp(cur + rel, 0.0f, 1.0f); + call.set_state(new_brightness != 0.0f); + call.set_brightness(new_brightness); + + call.set_transition_length(this->transition_length_.optional_value(x...)); + call.perform(); + } + + protected: + LightState *parent_; +}; + template class LightIsOnCondition : public Condition { public: explicit LightIsOnCondition(LightState *state) : state_(state) {} diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py new file mode 100644 index 0000000000..4718a22052 --- /dev/null +++ b/esphome/components/light/automation.py @@ -0,0 +1,105 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRANSITION_LENGTH, CONF_STATE, CONF_FLASH_LENGTH, \ + CONF_EFFECT, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, CONF_WHITE, \ + CONF_COLOR_TEMPERATURE +from .types import DimRelativeAction, ToggleAction, LightState, LightControlAction + + +@automation.register_action('light.toggle', ToggleAction, automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), +})) +def light_toggle_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_TRANSITION_LENGTH in config: + template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + cg.add(var.set_transition_length(template_)) + yield var + + +LIGHT_CONTROL_ACTION_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_STATE): cv.templatable(cv.boolean), + cv.Exclusive(CONF_TRANSITION_LENGTH, 'transformer'): + cv.templatable(cv.positive_time_period_milliseconds), + cv.Exclusive(CONF_FLASH_LENGTH, 'transformer'): + cv.templatable(cv.positive_time_period_milliseconds), + cv.Exclusive(CONF_EFFECT, 'transformer'): cv.templatable(cv.string), + cv.Optional(CONF_BRIGHTNESS): cv.templatable(cv.percentage), + cv.Optional(CONF_RED): cv.templatable(cv.percentage), + cv.Optional(CONF_GREEN): cv.templatable(cv.percentage), + cv.Optional(CONF_BLUE): cv.templatable(cv.percentage), + cv.Optional(CONF_WHITE): cv.templatable(cv.percentage), + cv.Optional(CONF_COLOR_TEMPERATURE): cv.templatable(cv.color_temperature), +}) +LIGHT_TURN_OFF_ACTION_SCHEMA = automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), + cv.Optional(CONF_STATE, default=False): False, +}) +LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(LIGHT_CONTROL_ACTION_SCHEMA.extend({ + cv.Optional(CONF_STATE, default=True): True, +})) + + +@automation.register_action('light.turn_off', LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA) +@automation.register_action('light.turn_on', LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA) +@automation.register_action('light.control', LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA) +def light_control_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + if CONF_STATE in config: + template_ = yield cg.templatable(config[CONF_STATE], args, bool) + cg.add(var.set_state(template_)) + if CONF_TRANSITION_LENGTH in config: + template_ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + cg.add(var.set_transition_length(template_)) + if CONF_FLASH_LENGTH in config: + template_ = yield cg.templatable(config[CONF_FLASH_LENGTH], args, cg.uint32) + cg.add(var.set_flash_length(template_)) + if CONF_BRIGHTNESS in config: + template_ = yield cg.templatable(config[CONF_BRIGHTNESS], args, float) + cg.add(var.set_brightness(template_)) + if CONF_RED in config: + template_ = yield cg.templatable(config[CONF_RED], args, float) + cg.add(var.set_red(template_)) + if CONF_GREEN in config: + template_ = yield cg.templatable(config[CONF_GREEN], args, float) + cg.add(var.set_green(template_)) + if CONF_BLUE in config: + template_ = yield cg.templatable(config[CONF_BLUE], args, float) + cg.add(var.set_blue(template_)) + if CONF_WHITE in config: + template_ = yield cg.templatable(config[CONF_WHITE], args, float) + cg.add(var.set_white(template_)) + if CONF_COLOR_TEMPERATURE in config: + template_ = yield cg.templatable(config[CONF_COLOR_TEMPERATURE], args, float) + cg.add(var.set_color_temperature(template_)) + if CONF_EFFECT in config: + template_ = yield cg.templatable(config[CONF_EFFECT], args, cg.std_string) + cg.add(var.set_effect(template_)) + yield var + + +CONF_RELATIVE_BRIGHTNESS = 'relative_brightness' +LIGHT_DIM_RELATIVE_ACTION_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.use_id(LightState), + cv.Required(CONF_RELATIVE_BRIGHTNESS): cv.templatable(cv.percentage), + cv.Optional(CONF_TRANSITION_LENGTH): cv.templatable(cv.positive_time_period_milliseconds), +}) + + +@automation.register_action('light.dim_relative', DimRelativeAction, + LIGHT_DIM_RELATIVE_ACTION_SCHEMA) +def light_dim_relative_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + templ = yield cg.templatable(config[CONF_RELATIVE_BRIGHTNESS], args, float) + cg.add(var.set_relative_brightness(templ)) + if CONF_TRANSITION_LENGTH in config: + templ = yield cg.templatable(config[CONF_TRANSITION_LENGTH], args, cg.uint32) + cg.add(var.set_transition_length(templ)) + yield var diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py new file mode 100644 index 0000000000..b77a355c5d --- /dev/null +++ b/esphome/components/light/effects.py @@ -0,0 +1,249 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_NAME, CONF_LAMBDA, CONF_UPDATE_INTERVAL, CONF_TRANSITION_LENGTH, \ + CONF_COLORS, CONF_STATE, CONF_DURATION, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, \ + CONF_WHITE, CONF_ALPHA, CONF_INTENSITY, CONF_SPEED, CONF_WIDTH, CONF_NUM_LEDS, CONF_RANDOM +from esphome.util import Registry +from .types import LambdaLightEffect, RandomLightEffect, StrobeLightEffect, \ + StrobeLightEffectColor, LightColorValues, AddressableLightRef, AddressableLambdaLightEffect, \ + FlickerLightEffect, AddressableRainbowLightEffect, AddressableColorWipeEffect, \ + AddressableColorWipeEffectColor, AddressableScanEffect, AddressableTwinkleEffect, \ + AddressableRandomTwinkleEffect, AddressableFireworksEffect, AddressableFlickerEffect + +CONF_ADD_LED_INTERVAL = 'add_led_interval' +CONF_REVERSE = 'reverse' +CONF_MOVE_INTERVAL = 'move_interval' +CONF_TWINKLE_PROBABILITY = 'twinkle_probability' +CONF_PROGRESS_INTERVAL = 'progress_interval' +CONF_SPARK_PROBABILITY = 'spark_probability' +CONF_USE_RANDOM_COLOR = 'use_random_color' +CONF_FADE_OUT_RATE = 'fade_out_rate' +CONF_STROBE = 'strobe' +CONF_FLICKER = 'flicker' +CONF_ADDRESSABLE_LAMBDA = 'addressable_lambda' +CONF_ADDRESSABLE_RAINBOW = 'addressable_rainbow' +CONF_ADDRESSABLE_COLOR_WIPE = 'addressable_color_wipe' +CONF_ADDRESSABLE_SCAN = 'addressable_scan' +CONF_ADDRESSABLE_TWINKLE = 'addressable_twinkle' +CONF_ADDRESSABLE_RANDOM_TWINKLE = 'addressable_random_twinkle' +CONF_ADDRESSABLE_FIREWORKS = 'addressable_fireworks' +CONF_ADDRESSABLE_FLICKER = 'addressable_flicker' + +BINARY_EFFECTS = ['lambda', 'strobe'] +MONOCHROMATIC_EFFECTS = BINARY_EFFECTS + ['flicker'] +RGB_EFFECTS = MONOCHROMATIC_EFFECTS + ['random'] +ADDRESSABLE_EFFECTS = RGB_EFFECTS + [CONF_ADDRESSABLE_LAMBDA, CONF_ADDRESSABLE_RAINBOW, + CONF_ADDRESSABLE_COLOR_WIPE, CONF_ADDRESSABLE_SCAN, + CONF_ADDRESSABLE_TWINKLE, CONF_ADDRESSABLE_RANDOM_TWINKLE, + CONF_ADDRESSABLE_FIREWORKS, CONF_ADDRESSABLE_FLICKER] + +EFFECTS_REGISTRY = Registry() + + +def register_effect(name, effect_type, default_name, schema, *extra_validators): + schema = cv.Schema(schema).extend({ + cv.Optional(CONF_NAME, default=default_name): cv.string_strict, + }) + validator = cv.All(schema, *extra_validators) + return EFFECTS_REGISTRY.register(name, effect_type, validator) + + +@register_effect('lambda', LambdaLightEffect, "Lambda", { + cv.Required(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.update_interval, +}) +def lambda_effect_to_code(config, effect_id): + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [], return_type=cg.void) + yield cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, + config[CONF_UPDATE_INTERVAL]) + + +@register_effect('random', RandomLightEffect, "Random", { + cv.Optional(CONF_TRANSITION_LENGTH, default='7.5s'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_UPDATE_INTERVAL, default='10s'): cv.positive_time_period_milliseconds, +}) +def random_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) + cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) + yield effect + + +@register_effect('strobe', StrobeLightEffect, "Strobe", { + cv.Optional(CONF_COLORS, default=[ + {CONF_STATE: True, CONF_DURATION: '0.5s'}, + {CONF_STATE: False, CONF_DURATION: '0.5s'}, + ]): cv.All(cv.ensure_list(cv.Schema({ + cv.Optional(CONF_STATE, default=True): cv.boolean, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, + cv.Optional(CONF_RED, default=1.0): cv.percentage, + cv.Optional(CONF_GREEN, default=1.0): cv.percentage, + cv.Optional(CONF_BLUE, default=1.0): cv.percentage, + cv.Optional(CONF_WHITE, default=1.0): cv.percentage, + cv.Required(CONF_DURATION): cv.positive_time_period_milliseconds, + }), cv.has_at_least_one_key(CONF_STATE, CONF_BRIGHTNESS, CONF_RED, CONF_GREEN, CONF_BLUE, + CONF_WHITE)), cv.Length(min=2)), +}) +def strobe_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + colors = [] + for color in config.get(CONF_COLORS, []): + colors.append(cg.StructInitializer( + StrobeLightEffectColor, + ('color', LightColorValues(color[CONF_STATE], color[CONF_BRIGHTNESS], + color[CONF_RED], color[CONF_GREEN], color[CONF_BLUE], + color[CONF_WHITE])), + ('duration', color[CONF_DURATION]), + )) + cg.add(var.set_colors(colors)) + yield var + + +@register_effect('flicker', FlickerLightEffect, "Flicker", { + cv.Optional(CONF_ALPHA, default=0.95): cv.percentage, + cv.Optional(CONF_INTENSITY, default=0.015): cv.percentage, +}) +def flicker_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_alpha(config[CONF_ALPHA])) + cg.add(var.set_intensity(config[CONF_INTENSITY])) + yield var + + +@register_effect('addressable_lambda', AddressableLambdaLightEffect, "Addressable Lambda", { + cv.Required(CONF_LAMBDA): cv.lambda_, + cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, +}) +def addressable_lambda_effect_to_code(config, effect_id): + args = [(AddressableLightRef, 'it')] + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) + var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, + config[CONF_UPDATE_INTERVAL]) + yield var + + +@register_effect('addressable_rainbow', AddressableRainbowLightEffect, "Rainbow", { + cv.Optional(CONF_SPEED, default=10): cv.uint32_t, + cv.Optional(CONF_WIDTH, default=50): cv.uint32_t, +}) +def addressable_rainbow_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_speed(config[CONF_SPEED])) + cg.add(var.set_width(config[CONF_WIDTH])) + yield var + + +@register_effect('addressable_color_wipe', AddressableColorWipeEffect, "Color Wipe", { + cv.Optional(CONF_COLORS, default=[{CONF_NUM_LEDS: 1, CONF_RANDOM: True}]): cv.ensure_list({ + cv.Optional(CONF_RED, default=1.0): cv.percentage, + cv.Optional(CONF_GREEN, default=1.0): cv.percentage, + cv.Optional(CONF_BLUE, default=1.0): cv.percentage, + cv.Optional(CONF_WHITE, default=1.0): cv.percentage, + cv.Optional(CONF_RANDOM, default=False): cv.boolean, + cv.Required(CONF_NUM_LEDS): cv.All(cv.uint32_t, cv.Range(min=1)), + }), + cv.Optional(CONF_ADD_LED_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REVERSE, default=False): cv.boolean, +}) +def addressable_color_wipe_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_add_led_interval(config[CONF_ADD_LED_INTERVAL])) + cg.add(var.set_reverse(config[CONF_REVERSE])) + colors = [] + for color in config.get(CONF_COLORS, []): + colors.append(cg.StructInitializer( + AddressableColorWipeEffectColor, + ('r', int(round(color[CONF_RED] * 255))), + ('g', int(round(color[CONF_GREEN] * 255))), + ('b', int(round(color[CONF_BLUE] * 255))), + ('w', int(round(color[CONF_WHITE] * 255))), + ('random', color[CONF_RANDOM]), + ('num_leds', color[CONF_NUM_LEDS]), + )) + cg.add(var.set_colors(colors)) + yield var + + +@register_effect('addressable_scan', AddressableScanEffect, "Scan", { + cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, +}) +def addressable_scan_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL])) + yield var + + +@register_effect('addressable_twinkle', AddressableTwinkleEffect, "Twinkle", { + cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, + cv.Optional(CONF_PROGRESS_INTERVAL, default='4ms'): cv.positive_time_period_milliseconds, +}) +def addressable_twinkle_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) + cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) + yield var + + +@register_effect('addressable_random_twinkle', AddressableRandomTwinkleEffect, "Random Twinkle", { + cv.Optional(CONF_TWINKLE_PROBABILITY, default='5%'): cv.percentage, + cv.Optional(CONF_PROGRESS_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, +}) +def addressable_random_twinkle_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_twinkle_probability(config[CONF_TWINKLE_PROBABILITY])) + cg.add(var.set_progress_interval(config[CONF_PROGRESS_INTERVAL])) + yield var + + +@register_effect('addressable_fireworks', AddressableFireworksEffect, "Fireworks", { + cv.Optional(CONF_UPDATE_INTERVAL, default='32ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SPARK_PROBABILITY, default='10%'): cv.percentage, + cv.Optional(CONF_USE_RANDOM_COLOR, default=False): cv.boolean, + cv.Optional(CONF_FADE_OUT_RATE, default=120): cv.uint8_t, +}) +def addressable_fireworks_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + cg.add(var.set_spark_probability(config[CONF_SPARK_PROBABILITY])) + cg.add(var.set_use_random_color(config[CONF_USE_RANDOM_COLOR])) + cg.add(var.set_fade_out_rate(config[CONF_FADE_OUT_RATE])) + yield var + + +@register_effect('addressable_flicker', AddressableFlickerEffect, "Addressable Flicker", { + cv.Optional(CONF_UPDATE_INTERVAL, default='16ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_INTENSITY, default='5%'): cv.percentage, +}) +def addressable_flicker_effect_to_code(config, effect_id): + var = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) + cg.add(var.set_intensity(config[CONF_INTENSITY])) + yield var + + +def validate_effects(allowed_effects): + def validator(value): + value = cv.validate_registry('effect', EFFECTS_REGISTRY)(value) + errors = [] + names = set() + for i, x in enumerate(value): + key = next(it for it in x.keys()) + if key not in allowed_effects: + errors.append( + cv.Invalid("The effect '{}' is not allowed for this " + "light type".format(key), [i]) + ) + continue + name = x[key][CONF_NAME] + if name in names: + errors.append( + cv.Invalid(u"Found the effect name '{}' twice. All effects must have " + u"unique names".format(name), [i]) + ) + continue + names.add(name) + if errors: + raise cv.MultipleInvalid(errors) + return value + + return validator diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py new file mode 100644 index 0000000000..90ff877f26 --- /dev/null +++ b/esphome/components/light/types.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome import automation + +# Base +light_ns = cg.esphome_ns.namespace('light') +LightState = light_ns.class_('LightState', cg.Nameable, cg.Component) +# Fake class for addressable lights +AddressableLightState = light_ns.class_('LightState', LightState) +LightOutput = light_ns.class_('LightOutput') +AddressableLight = light_ns.class_('AddressableLight') +AddressableLightRef = AddressableLight.operator('ref') +LightColorValues = light_ns.class_('LightColorValues') + +# Actions +ToggleAction = light_ns.class_('ToggleAction', automation.Action) +LightControlAction = light_ns.class_('LightControlAction', automation.Action) +DimRelativeAction = light_ns.class_('DimRelativeAction', automation.Action) + +# Effects +LightEffect = light_ns.class_('LightEffect') +RandomLightEffect = light_ns.class_('RandomLightEffect', LightEffect) +LambdaLightEffect = light_ns.class_('LambdaLightEffect', LightEffect) +StrobeLightEffect = light_ns.class_('StrobeLightEffect', LightEffect) +StrobeLightEffectColor = light_ns.class_('StrobeLightEffectColor', LightEffect) +FlickerLightEffect = light_ns.class_('FlickerLightEffect', LightEffect) +AddressableLightEffect = light_ns.class_('AddressableLightEffect', LightEffect) +AddressableLambdaLightEffect = light_ns.class_('AddressableLambdaLightEffect', + AddressableLightEffect) +AddressableRainbowLightEffect = light_ns.class_('AddressableRainbowLightEffect', + AddressableLightEffect) +AddressableColorWipeEffect = light_ns.class_('AddressableColorWipeEffect', AddressableLightEffect) +AddressableColorWipeEffectColor = light_ns.struct('AddressableColorWipeEffectColor') +AddressableScanEffect = light_ns.class_('AddressableScanEffect', AddressableLightEffect) +AddressableTwinkleEffect = light_ns.class_('AddressableTwinkleEffect', AddressableLightEffect) +AddressableRandomTwinkleEffect = light_ns.class_('AddressableRandomTwinkleEffect', + AddressableLightEffect) +AddressableFireworksEffect = light_ns.class_('AddressableFireworksEffect', AddressableLightEffect) +AddressableFlickerEffect = light_ns.class_('AddressableFlickerEffect', AddressableLightEffect) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 5ac1c98960..16a88f39e3 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -150,6 +150,10 @@ def exp_mqtt_message(config): @coroutine_with_priority(40.0) def to_code(config): + cg.add_library('AsyncMqttClient', '0.8.2') + cg.add_define('USE_MQTT') + cg.add_global(mqtt_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) cg.add(var.set_broker_address(config[CONF_BROKER])) @@ -217,10 +221,6 @@ def to_code(config): trig = cg.new_Pvariable(conf[CONF_TRIGGER_ID], conf[CONF_TOPIC], conf[CONF_QOS]) yield automation.build_automation(trig, [(cg.JsonObjectConstRef, 'x')], conf) - cg.add_library('AsyncMqttClient', '0.8.2') - cg.add_define('USE_MQTT') - cg.add_global(mqtt_ns.using) - MQTT_PUBLISH_ACTION_SCHEMA = cv.Schema({ cv.GenerateID(): cv.use_id(MQTTClientComponent), diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index ff1d056a6e..a233468762 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -10,7 +10,7 @@ from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_B CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_TO, CONF_TRIGGER_ID, \ CONF_UNIT_OF_MEASUREMENT, \ CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID -from esphome.core import CORE, coroutine +from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry IS_PLATFORM_COMPONENT = True @@ -305,6 +305,7 @@ def fit_linear(x, y): return k, b +@coroutine_with_priority(40.0) def to_code(config): cg.add_define('USE_SENSOR') cg.add_global(sensor_ns.using) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 1f0fc91277..69899d1e84 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_CLK_PIN, CONF_ID, CONF_MISO_PIN, CONF_MOSI_PIN, CONF_SPI_ID, \ CONF_CS_PIN -from esphome.core import coroutine +from esphome.core import coroutine, coroutine_with_priority spi_ns = cg.esphome_ns.namespace('spi') SPIComponent = spi_ns.class_('SPIComponent', cg.Component) @@ -18,7 +18,9 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ }), cv.has_at_least_one_key(CONF_MISO_PIN, CONF_MOSI_PIN)) +@coroutine_with_priority(1.0) def to_code(config): + cg.add_global(spi_ns.using) var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) @@ -31,8 +33,6 @@ def to_code(config): mosi = yield cg.gpio_pin_expression(config[CONF_MOSI_PIN]) cg.add(var.set_mosi(mosi)) - cg.add_global(spi_ns.using) - SPI_DEVICE_SCHEMA = cv.Schema({ cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), diff --git a/esphome/components/stepper/__init__.py b/esphome/components/stepper/__init__.py index 723ad5c1a5..087fb18137 100644 --- a/esphome/components/stepper/__init__.py +++ b/esphome/components/stepper/__init__.py @@ -2,8 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ACCELERATION, CONF_DECELERATION, CONF_ID, CONF_MAX_SPEED, \ - CONF_POSITION, CONF_TARGET -from esphome.core import CORE, coroutine + CONF_POSITION, CONF_TARGET, CONF_SPEED +from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -13,6 +13,7 @@ Stepper = stepper_ns.class_('Stepper') SetTargetAction = stepper_ns.class_('SetTargetAction', automation.Action) ReportPositionAction = stepper_ns.class_('ReportPositionAction', automation.Action) +SetSpeedAction = stepper_ns.class_('SetSpeedAction', automation.Action) def validate_acceleration(value): @@ -103,5 +104,18 @@ def stepper_report_position_to_code(config, action_id, template_arg, args): yield var +@automation.register_action('stepper.set_speed', SetSpeedAction, cv.Schema({ + cv.Required(CONF_ID): cv.use_id(Stepper), + cv.Required(CONF_SPEED): cv.templatable(validate_speed), +})) +def stepper_set_speed_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_SPEED], args, cg.int32) + cg.add(var.set_speed(template_)) + yield var + + +@coroutine_with_priority(100.0) def to_code(config): cg.add_global(stepper_ns.using) diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 568cbf3d21..33777dce83 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -19,6 +19,7 @@ class Stepper { void set_acceleration(float acceleration) { this->acceleration_ = acceleration; } void set_deceleration(float deceleration) { this->deceleration_ = deceleration; } void set_max_speed(float max_speed) { this->max_speed_ = max_speed; } + virtual void on_update_speed() {} bool has_reached_target() { return this->current_position == this->target_position; } int32_t current_position{0}; @@ -60,5 +61,21 @@ template class ReportPositionAction : public Action { Stepper *parent_; }; +template class SetSpeedAction : public Action { + public: + explicit SetSpeedAction(Stepper *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(float, speed); + + void play(Ts... x) override { + float speed = this->speed_.value(x...); + this->parent_->set_max_speed(speed); + this->parent_->on_update_speed(); + } + + protected: + Stepper *parent_; +}; + } // namespace stepper } // namespace esphome diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index a83845d142..6425a364a1 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -5,7 +5,7 @@ from esphome.automation import Condition, maybe_simple_id from esphome.components import mqtt from esphome.const import CONF_ICON, CONF_ID, CONF_INTERNAL, CONF_INVERTED, CONF_ON_TURN_OFF, \ CONF_ON_TURN_ON, CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME -from esphome.core import CORE, coroutine +from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -92,6 +92,7 @@ def switch_is_off_to_code(config, condition_id, template_arg, args): yield cg.new_Pvariable(condition_id, template_arg, paren) +@coroutine_with_priority(100.0) def to_code(config): cg.add_global(switch_ns.using) cg.add_define('USE_SWITCH') diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 742ac20e0e..f138f38d2f 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -3,8 +3,8 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import mqtt from esphome.const import CONF_ICON, CONF_ID, CONF_INTERNAL, CONF_ON_VALUE, \ - CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME -from esphome.core import CORE, coroutine + CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME, CONF_STATE +from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -16,6 +16,7 @@ TextSensorPtr = TextSensor.operator('ptr') TextSensorStateTrigger = text_sensor_ns.class_('TextSensorStateTrigger', automation.Trigger.template(cg.std_string)) TextSensorPublishAction = text_sensor_ns.class_('TextSensorPublishAction', automation.Action) +TextSensorStateCondition = text_sensor_ns.class_('TextSensorStateCondition', automation.Condition) icon = cv.icon @@ -53,6 +54,19 @@ def register_text_sensor(var, config): yield setup_text_sensor_core_(var, config) +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_TEXT_SENSOR') cg.add_global(text_sensor_ns.using) + + +@automation.register_condition('text_sensor.state', TextSensorStateCondition, cv.Schema({ + cv.Required(CONF_ID): cv.use_id(TextSensor), + cv.Required(CONF_STATE): cv.templatable(cv.string_strict), +})) +def text_sensor_state_to_code(config, condition_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(condition_id, template_arg, paren) + templ = yield cg.templatable(config[CONF_STATE], args, cg.std_string) + cg.add(var.set_state(templ)) + yield var diff --git a/esphome/components/text_sensor/automation.cpp b/esphome/components/text_sensor/automation.cpp deleted file mode 100644 index eddcf1f8d5..0000000000 --- a/esphome/components/text_sensor/automation.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "automation.h" -#include "esphome/core/log.h" - -namespace esphome { -namespace text_sensor { - -static const char *TAG = "text_sensor.automation"; - -} // namespace text_sensor -} // namespace esphome diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 3e87aa2d1c..496efb1cc3 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -14,6 +14,18 @@ class TextSensorStateTrigger : public Trigger { } }; +template class TextSensorStateCondition : public Condition { + public: + explicit TextSensorStateCondition(TextSensor *parent) : parent_(parent) {} + + TEMPLATABLE_VALUE(std::string, state) + + bool check(Ts... x) override { return this->parent_->state == this->state_.value(x...); } + + protected: + TextSensor *parent_; +}; + template class TextSensorPublishAction : public Action { public: TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 9fbfd95b6d..82dc750486 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -12,7 +12,7 @@ from esphome import automation from esphome.const import CONF_CRON, CONF_DAYS_OF_MONTH, CONF_DAYS_OF_WEEK, CONF_HOURS, \ CONF_MINUTES, CONF_MONTHS, CONF_ON_TIME, CONF_SECONDS, CONF_TIMEZONE, CONF_TRIGGER_ID, \ CONF_AT, CONF_SECOND, CONF_HOUR, CONF_MINUTE -from esphome.core import coroutine +from esphome.core import coroutine, coroutine_with_priority from esphome.py_compat import string_types _LOGGER = logging.getLogger(__name__) @@ -293,6 +293,7 @@ def register_time(time_var, config): yield setup_time_core_(time_var, config) +@coroutine_with_priority(100.0) def to_code(config): cg.add_define('USE_TIME') cg.add_global(time_ns.using) diff --git a/esphome/config.py b/esphome/config.py index 646108c648..901a78ea9a 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -120,6 +120,9 @@ def _lookup_module(domain, is_platform): try: module = importlib.import_module(path) except ImportError: + import traceback + _LOGGER.error("Unable to import component %s:", domain) + traceback.print_exc() return None except Exception: # pylint: disable=broad-except import traceback @@ -390,6 +393,7 @@ def validate_config(config): if component is None: result.add_str_error(u"Component not found: {}".format(domain), path) continue + CORE.loaded_integrations.add(domain) # Process AUTO_LOAD for load in component.auto_load: @@ -432,6 +436,7 @@ def validate_config(config): if platform is None: result.add_str_error(u"Platform not found: '{}'".format(p_domain), path) continue + CORE.loaded_integrations.add(p_name) # Process AUTO_LOAD for load in platform.auto_load: diff --git a/esphome/config_validation.py b/esphome/config_validation.py index a2da05af7a..56fb260679 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -60,8 +60,6 @@ RESERVED_IDS = [ 'App', 'pinMode', 'delay', 'delayMicroseconds', 'digitalRead', 'digitalWrite', 'INPUT', 'OUTPUT', 'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t', 'int8_t', 'int16_t', 'int32_t', 'int64_t', - 'display', 'i2c', 'spi', 'uart', 'sensor', 'binary_sensor', 'climate', 'cover', 'text_sensor', - 'api', 'fan', 'light', 'gpio', 'mqtt', 'ota', 'power_supply', 'wifi' ] @@ -127,6 +125,8 @@ def string(value): check_not_templatable(value) if isinstance(value, (dict, list)): raise Invalid("string value cannot be dictionary or list.") + if isinstance(value, bool): + raise Invalid("Auto-converted this value to boolean, please wrap the value in quotes.") if isinstance(value, text_type): return value if value is not None: @@ -168,7 +168,7 @@ def boolean(value): check_not_templatable(value) if isinstance(value, bool): return value - if isinstance(value, str): + if isinstance(value, string_types): value = value.lower() if value in ('true', 'yes', 'on', 'enable'): return True @@ -285,7 +285,10 @@ def validate_id_name(value): u"character and numbers. The character '{}' cannot be used" u"".format(char)) if value in RESERVED_IDS: - raise Invalid(u"ID {} is reserved internally and cannot be used".format(value)) + raise Invalid(u"ID '{}' is reserved internally and cannot be used".format(value)) + if value in CORE.loaded_integrations: + raise Invalid(u"ID '{}' conflicts with the name of an esphome integration, please use " + u"another ID name.".format(value)) return value @@ -330,7 +333,7 @@ def templatable(other_validators): def validator(value): if isinstance(value, Lambda): - return value + return lambda_(value) if isinstance(other_validators, dict): return schema(value) return schema(value) @@ -953,11 +956,22 @@ def enum(mapping, **kwargs): return validator +LAMBDA_ENTITY_ID_PROG = re.compile(r'id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)') + + def lambda_(value): """Coerce this configuration option to a lambda.""" - if isinstance(value, Lambda): - return value - return Lambda(string_strict(value)) + if not isinstance(value, Lambda): + value = Lambda(string_strict(value)) + entity_id_parts = re.split(LAMBDA_ENTITY_ID_PROG, value.value) + if len(entity_id_parts) != 1: + entity_ids = ' '.join("'{}'".format(entity_id_parts[i]) + for i in range(1, len(entity_id_parts), 2)) + raise Invalid("Lambda contains reference to entity-id-style ID {}. " + "The id() wrapper only works for ESPHome-internal types. For importing " + "states from Home Assistant use the 'homeassistant' sensor platforms." + "".format(entity_ids)) + return value def dimensions(value): diff --git a/esphome/const.py b/esphome/const.py index 623e07c891..10e2781b44 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -327,6 +327,7 @@ CONF_REPOSITORY = 'repository' CONF_RESET_PIN = 'reset_pin' CONF_RESIZE = 'resize' CONF_RESOLUTION = 'resolution' +CONF_RESTORE = 'restore' CONF_RESTORE_MODE = 'restore_mode' CONF_RESTORE_STATE = 'restore_state' CONF_RESTORE_VALUE = 'restore_value' @@ -397,6 +398,7 @@ CONF_THEN = 'then' CONF_THRESHOLD = 'threshold' CONF_THROTTLE = 'throttle' CONF_TILT = 'tilt' +CONF_TIME = 'time' CONF_TIMEOUT = 'timeout' CONF_TIMES = 'times' CONF_TIMEZONE = 'timezone' diff --git a/esphome/core.py b/esphome/core.py index 408b10e80e..464782368e 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -502,6 +502,8 @@ class EsphomeCore(object): # A dictionary of started coroutines, used to warn when a coroutine was not # awaited. self.active_coroutines = {} # type: Dict[int, Any] + # A set of strings of names of loaded integrations, used to find namespace ID conflicts + self.loaded_integrations = set() def reset(self): self.dashboard = False @@ -521,6 +523,7 @@ class EsphomeCore(object): self.build_flags = set() self.defines = set() self.active_coroutines = {} + self.loaded_integrations = set() @property def address(self): # type: () -> str diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 53d24cbd84..67834430c8 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -55,6 +55,32 @@ template class LambdaCondition : public Condition { std::function f_; }; +template class ForCondition : public Condition, public Component { + public: + explicit ForCondition(Condition<> *condition) : condition_(condition) {} + + TEMPLATABLE_VALUE(uint32_t, time); + + void loop() override { this->check_internal(); } + float get_setup_priority() const override { return setup_priority::DATA; } + bool check_internal() { + bool cond = this->condition_->check(); + if (!cond) + this->last_inactive_ = millis(); + return cond; + } + + bool check(Ts... x) override { + if (!this->check_internal()) + return false; + return millis() - this->last_inactive_ < this->time_.value(x...); + } + + protected: + Condition<> *condition_; + uint32_t last_inactive_{0}; +}; + class StartupTrigger : public Trigger<>, public Component { public: explicit StartupTrigger(float setup_priority) : setup_priority_(setup_priority) {} diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 64a056e9cc..c42585cab1 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -304,4 +304,9 @@ float clamp(float val, float min, float max) { } float lerp(float completion, float start, float end) { return start + (end - start) * completion; } +bool str_startswith(const std::string &full, const std::string &start) { return full.rfind(start, 0) == 0; } +bool str_endswith(const std::string &full, const std::string &ending) { + return full.rfind(ending) == (full.size() - ending.size()); +} + } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index babd14ff53..c9ee402a39 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -52,6 +52,8 @@ std::string to_lowercase_underscore(std::string s); /// Compare string a to string b (ignoring case) and return whether they are equal. bool str_equals_case_insensitive(const std::string &a, const std::string &b); +bool str_startswith(const std::string &full, const std::string &start); +bool str_endswith(const std::string &full, const std::string &ending); class HighFrequencyLoopRequester { public: diff --git a/esphome/core_config.py b/esphome/core_config.py index ae55ac8d24..9a636a28da 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -151,7 +151,7 @@ def add_includes(includes): # Add includes at the very end, so that the included files can access global variables for include in includes: path = CORE.relative_config_path(include) - res = os.path.relpath(path, CORE.relative_build_path('src')) + res = os.path.relpath(path, CORE.relative_build_path('src')).replace(os.path.sep, '/') cg.add_global(cg.RawExpression(u'#include "{}"'.format(res))) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index f6d1b63a04..651d748934 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -72,6 +72,9 @@ class ExpressionList(Expression): text = u", ".join(text_type(x) for x in self.args) return indent_all_but_first_and_last(text) + def __iter__(self): + return iter(self.args) + class TemplateArguments(Expression): def __init__(self, *args): # type: (*SafeExpType) -> None @@ -81,6 +84,9 @@ class TemplateArguments(Expression): def __str__(self): return u'<{}>'.format(self.args) + def __iter__(self): + return iter(self.args) + class CallExpression(Expression): def __init__(self, base, *args): # type: (Expression, *SafeExpType) -> None @@ -400,7 +406,7 @@ def Pvariable(id, # type: ID def new_Pvariable(id, # type: ID - *args # type: SafeExpType + *args # type: *SafeExpType ): """Declare a new pointer variable in the code generation by calling it's constructor with the given arguments. @@ -480,6 +486,21 @@ def get_variable(id): # type: (ID) -> Generator[MockObj] yield var +@coroutine +def get_variable_with_full_id(id): # type: (ID) -> Generator[ID, MockObj] + """ + Wait for the given ID to be defined in the code generation and + return it as a MockObj. + + This is a coroutine, you need to await it with a 'yield' expression! + + :param id: The ID to retrieve + :return: The variable as a MockObj. + """ + full_id, var = yield CORE.get_variable_with_full_id(id) + yield full_id, var + + @coroutine def process_lambda(value, # type: Lambda parameters, # type: List[Tuple[SafeExpType, str]] @@ -572,7 +593,7 @@ class MockObj(Expression): attr = attr[1:] return MockObj(u'{}{}{}'.format(self.base, self.op, attr), next_op) - def __call__(self, *args, **kwargs): # type: (*Any, **Any) -> MockObj + def __call__(self, *args): # type: (SafeExpType) -> MockObj call = CallExpression(self.base, *args) return MockObj(call, self.op) @@ -583,37 +604,32 @@ class MockObj(Expression): return u'MockObj<{}>'.format(text_type(self.base)) @property - def _(self): + def _(self): # type: () -> MockObj return MockObj(u'{}{}'.format(self.base, self.op)) @property - def new(self): + def new(self): # type: () -> MockObj return MockObj(u'new {}'.format(self.base), u'->') - def template(self, *args): # type: (Tuple[Union[TemplateArguments, Expression]]) -> MockObj + def template(self, *args): # type: (*SafeExpType) -> MockObj if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: args = args[0] - obj = MockObj(u'{}{}'.format(self.base, args)) - return obj + return MockObj(u'{}{}'.format(self.base, args)) def namespace(self, name): # type: (str) -> MockObj - obj = MockObj(u'{}{}{}'.format(self.base, self.op, name), u'::') - return obj + return MockObj(u'{}{}'.format(self._, name), u'::') def class_(self, name, *parents): # type: (str, *MockObjClass) -> MockObjClass op = '' if self.op == '' else '::' - obj = MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) - return obj + return MockObjClass(u'{}{}{}'.format(self.base, op, name), u'.', parents=parents) def struct(self, name): # type: (str) -> MockObjClass return self.class_(name) def enum(self, name, is_class=False): # type: (str, bool) -> MockObj - if is_class: - return self.namespace(name) - return self + return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op) def operator(self, name): # type: (str) -> MockObj if name == 'ref': @@ -625,7 +641,7 @@ class MockObj(Expression): raise NotImplementedError @property - def using(self): + def using(self): # type: () -> MockObj assert self.op == '::' return MockObj(u'using namespace {}'.format(self.base)) @@ -637,6 +653,26 @@ class MockObj(Expression): return MockObj(u'{}[{}]'.format(self.base, item), next_op) +class MockObjEnum(MockObj): + def __init__(self, *args, **kwargs): + self._enum = kwargs.pop('enum') + self._is_class = kwargs.pop('is_class') + base = kwargs.pop('base') + if self._is_class: + base = base + '::' + self._enum + kwargs['op'] = '::' + kwargs['base'] = base + MockObj.__init__(self, *args, **kwargs) + + def __str__(self): # type: () -> unicode + if self._is_class: + return super(MockObjEnum, self).__str__() + return u'{}{}{}'.format(self.base, self.op, self._enum) + + def __repr__(self): + return u'MockObj<{}>'.format(text_type(self.base)) + + class MockObjClass(MockObj): def __init__(self, *args, **kwargs): parens = kwargs.pop('parents') @@ -657,10 +693,8 @@ class MockObjClass(MockObj): return True return False - def template(self, - *args # type: Tuple[Union[TemplateArguments, Expression]] - ): - # type: (...) -> MockObjClass + def template(self, *args): + # type: (*SafeExpType) -> MockObjClass if len(args) != 1 or not isinstance(args[0], TemplateArguments): args = TemplateArguments(*args) else: