From da390d32f8cda845cbc9fb17c1baec366df35d05 Mon Sep 17 00:00:00 2001 From: Andrew Zaborowski Date: Fri, 1 May 2020 00:11:26 +0200 Subject: [PATCH 001/200] Move stop/is_running implementation to Action base class Try to fix issue #1105. Until now if an UpdateComponentAction, a LambdaAction, or any action that can contain those inside their "else" or "then" action lists, resulted in a call to script.stop on the script that contains them, the script would continue running, because they didn't implement a stop() method. Basically only the asynchronous ones did: DelayAction, WaitUntilAction and ScriptWaitAction. With this change num_running_ in Action replaces DelayAction::num_running_ and WaitUntilAction::triggered_ to provide the same is_running logic to other actions. --- esphome/components/script/script.h | 13 ++------ esphome/core/automation.h | 17 ++++++++--- esphome/core/base_automation.h | 49 +++++++++++------------------- 3 files changed, 34 insertions(+), 45 deletions(-) diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 3b97327da8..a6b208167f 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -53,41 +53,34 @@ template class ScriptWaitAction : public Action, public C public: ScriptWaitAction(Script *script) : script_(script) {} - void play(Ts... x) { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { - this->triggered_ = false; this->play_next(x...); return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (this->script_->is_running()) return; - this->triggered_ = false; this->play_next_tuple(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } - protected: Script *script_; - bool triggered_{false}; std::tuple var_{}; }; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index cbe96a749e..6e595fc458 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -77,17 +77,24 @@ template class Action { public: virtual void play(Ts... x) = 0; virtual void play_complex(Ts... x) { + this->num_running_++; this->play(x...); this->play_next(x...); } void play_next(Ts... x) { - if (this->next_ != nullptr) { - this->next_->play_complex(x...); + if (this->num_running_ > 0) { + this->num_running_--; + if (this->next_ != nullptr) { + this->next_->play_complex(x...); + } } } virtual void stop() {} virtual void stop_complex() { - this->stop(); + if (num_running_) { + this->stop(); + this->num_running_ = 0; + } this->stop_next(); } void stop_next() { @@ -95,7 +102,7 @@ template class Action { this->next_->stop_complex(); } } - virtual bool is_running() { return this->is_running_next(); } + virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next(); } bool is_running_next() { if (this->next_ == nullptr) return false; @@ -114,6 +121,8 @@ template class Action { } Action *next_ = nullptr; + + int num_running_{0}; }; template class ActionList { diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index add3df0bb5..bd34009790 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -110,27 +110,17 @@ template class DelayAction : public Action, public Compon void stop() override { this->cancel_timeout(""); - this->num_running_ = 0; } void play(Ts... x) override { /* ignore - see play_complex */ } void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::delay_end_, this, x...); + auto f = std::bind(&Action::play_next, this, x...); this->num_running_++; this->set_timeout(this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } - - bool is_running() override { return this->num_running_ > 0 || this->is_running_next(); } - - protected: - void delay_end_(Ts... x) { - this->num_running_--; - this->play_next(x...); - } - int num_running_{0}; }; template class LambdaAction : public Action { @@ -160,17 +150,18 @@ template class IfAction : public Action { } void play_complex(Ts... x) override { + this->num_running_++; bool res = this->condition_->check(x...); if (res) { if (this->then_.empty()) { this->play_next(x...); - } else { + } else if (this->num_running_ > 0) { this->then_.play(x...); } } else { if (this->else_.empty()) { this->play_next(x...); - } else { + } else if (this->num_running_ > 0) { this->else_.play(x...); } } @@ -181,8 +172,6 @@ template class IfAction : public Action { this->else_.stop(); } - bool is_running() override { return this->then_.is_running() || this->else_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -196,9 +185,11 @@ template class WhileAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->condition_->check_tuple(this->var_)) { + if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { // play again - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } } else { // condition false, play next this->play_next_tuple(this->var_); @@ -210,6 +201,7 @@ template class WhileAction : public Action { } void play_complex(Ts... x) override { + this->num_running_++; // Store loop parameters this->var_ = std::make_tuple(x...); // Initial condition check @@ -220,13 +212,13 @@ template class WhileAction : public Action { return; } - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } } void stop() override { this->then_.stop(); } - bool is_running() override { return this->then_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -237,42 +229,37 @@ template class WaitUntilAction : public Action, public Co public: WaitUntilAction(Condition *condition) : condition_(condition) {} - void play(Ts... x) { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { - this->triggered_ = false; - this->play_next(x...); + if (this->num_running_ > 0) { + this->play_next(x...); + } return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (!this->condition_->check_tuple(this->var_)) { return; } - this->triggered_ = false; this->play_next_tuple(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } - protected: Condition *condition_; - bool triggered_{false}; std::tuple var_{}; }; From a62b6548d238d71fc43cba86b809b5d091577919 Mon Sep 17 00:00:00 2001 From: Andrew Zaborowski Date: Fri, 1 May 2020 12:38:34 +0200 Subject: [PATCH 002/200] Make some Action methods protected Apparently play()/stop() etc. are not meant to be called directly by users of the class and if they're called directly that would not give the expected result for the classes that have an empty play(). Make all methods except play_complex, stop_comples and is_running protected. While there also make RemoteTransmitterActionBase::encode protected. --- .../components/api/homeassistant_service.h | 5 +- esphome/components/binary_sensor/automation.h | 5 +- esphome/components/climate/automation.h | 4 +- esphome/components/cover/automation.h | 36 +++++----- .../deep_sleep/deep_sleep_component.h | 8 +-- esphome/components/dfplayer/dfplayer.h | 24 +++++-- esphome/components/display/display_buffer.h | 10 ++- esphome/components/esp8266_pwm/esp8266_pwm.h | 4 +- esphome/components/fan/automation.h | 12 ++-- .../components/globals/globals_component.h | 4 +- .../components/http_request/http_request.h | 4 +- .../integration/integration_sensor.h | 4 +- esphome/components/ledc/ledc_output.h | 4 +- esphome/components/light/automation.h | 16 ++--- esphome/components/mhz19/mhz19.h | 9 ++- esphome/components/mqtt/mqtt_client.h | 10 +-- esphome/components/output/automation.h | 11 ++-- esphome/components/pid/pid_climate.h | 12 ++-- esphome/components/remote_base/jvc_protocol.h | 4 +- esphome/components/remote_base/lg_protocol.h | 4 +- esphome/components/remote_base/nec_protocol.h | 4 +- .../remote_base/panasonic_protocol.h | 4 +- .../components/remote_base/pioneer_protocol.h | 4 +- esphome/components/remote_base/raw_protocol.h | 4 +- esphome/components/remote_base/rc5_protocol.h | 4 +- .../remote_base/rc_switch_protocol.h | 15 +++-- esphome/components/remote_base/remote_base.h | 16 ++--- .../components/remote_base/samsung_protocol.h | 4 +- .../components/remote_base/sony_protocol.h | 4 +- esphome/components/rf_bridge/rf_bridge.h | 8 +-- .../rotary_encoder/rotary_encoder.h | 2 +- esphome/components/script/script.h | 18 ++--- esphome/components/sensor/automation.h | 2 +- esphome/components/servo/servo.h | 4 +- esphome/components/sim800l/sim800l.h | 4 +- esphome/components/stepper/stepper.h | 12 ++-- esphome/components/switch/automation.h | 14 ++-- esphome/components/text_sensor/automation.h | 2 +- esphome/components/tm1651/tm1651.h | 17 +++-- esphome/components/uart/automation.h | 5 +- esphome/core/automation.h | 51 ++++++++------- esphome/core/base_automation.h | 65 ++++++++++--------- 42 files changed, 250 insertions(+), 203 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d68dac3b61..0877efbf26 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -29,7 +29,9 @@ template class HomeAssistantServiceCallAction : public Action void add_variable(std::string key, T value) { this->variables_.push_back(TemplatableKeyValuePair(key, value)); } - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { HomeassistantServiceResponse resp; resp.service = this->service_.value(x...); resp.is_event = this->is_event_; @@ -54,7 +56,6 @@ template class HomeAssistantServiceCallAction : public Actionparent_->send_homeassistant_service_call(resp); } - protected: APIServer *parent_; bool is_event_; std::vector> data_; diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index e9ff37446d..b76fbdae0c 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -137,12 +137,13 @@ template class BinarySensorPublishAction : public Action public: explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); } - protected: BinarySensor *sensor_; }; diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 0cd52b1036..2fd9d81e6b 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -18,7 +18,8 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto call = this->climate_->make_call(); call.set_mode(this->mode_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...)); @@ -30,7 +31,6 @@ template class ControlAction : public Action { call.perform(); } - protected: Climate *climate_; }; diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index a8eb0cdf99..bad641c150 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -11,9 +11,9 @@ template class OpenAction : public Action { public: explicit OpenAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->open(); } - protected: + void play_(Ts... x) override { this->cover_->open(); } + Cover *cover_; }; @@ -21,9 +21,9 @@ template class CloseAction : public Action { public: explicit CloseAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->close(); } - protected: + void play_(Ts... x) override { this->cover_->close(); } + Cover *cover_; }; @@ -31,9 +31,9 @@ template class StopAction : public Action { public: explicit StopAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { this->cover_->stop(); } - protected: + void play_(Ts... x) override { this->cover_->stop(); } + Cover *cover_; }; @@ -41,7 +41,12 @@ template class ControlAction : public Action { public: explicit ControlAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { + TEMPLATABLE_VALUE(bool, stop) + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + + protected: + void play_(Ts... x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -52,18 +57,18 @@ template class ControlAction : public Action { call.perform(); } - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - - protected: Cover *cover_; }; template class CoverPublishAction : public Action { public: CoverPublishAction(Cover *cover) : cover_(cover) {} - void play(Ts... x) override { + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + TEMPLATABLE_VALUE(CoverOperation, current_operation) + + protected: + void play_(Ts... x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); if (this->tilt_.has_value()) @@ -73,11 +78,6 @@ template class CoverPublishAction : public Action { this->cover_->publish_state(); } - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - TEMPLATABLE_VALUE(CoverOperation, current_operation) - - protected: Cover *cover_; }; diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 4372a3f66c..6287a2259a 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -85,9 +85,9 @@ template class EnterDeepSleepAction : public Action { public: EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} - void play(Ts... x) override { this->deep_sleep_->begin_sleep(true); } - protected: + void play_(Ts... x) override { this->deep_sleep_->begin_sleep(true); } + DeepSleepComponent *deep_sleep_; }; @@ -95,9 +95,9 @@ template class PreventDeepSleepAction : public Action { public: PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} - void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } - protected: + void play_(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } + DeepSleepComponent *deep_sleep_; }; diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 22ca11c3be..89f0fb691c 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -104,8 +104,8 @@ class DFPlayer : public uart::UARTDevice, public Component { #define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ template class ACTION_CLASS : public Action, public Parented { \ - public: \ - void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + protected: \ + void play_(Ts... x) override { this->parent_->ACTION_METHOD(); } \ }; DFPLAYER_SIMPLE_ACTION(NextAction, next) @@ -115,7 +115,9 @@ template class PlayFileAction : public Action, public Par public: TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); if (loop) { @@ -131,7 +133,9 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -146,7 +150,9 @@ template class PlayFolderAction : public Action, public P template class SetDeviceAction : public Action, public Parented { public: TEMPLATABLE_VALUE(Device, device) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); } @@ -155,7 +161,9 @@ template class SetDeviceAction : public Action, public Pa template class SetVolumeAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, volume) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); } @@ -164,7 +172,9 @@ template class SetVolumeAction : public Action, public Pa template class SetEqAction : public Action, public Parented { public: TEMPLATABLE_VALUE(EqPreset, eq) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b12fad8c8a..2a6ead3306 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -391,7 +391,9 @@ class Image { template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto *page = this->page_.value(x...); if (page != nullptr) { page->show(); @@ -402,18 +404,20 @@ template class DisplayPageShowAction : public Action { template class DisplayPageShowNextAction : public Action { public: DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_next_page(); } protected: + void play_(Ts... x) override { this->buffer_->show_next_page(); } + DisplayBuffer *buffer_; }; template class DisplayPageShowPrevAction : public Action { public: DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} - void play(Ts... x) override { this->buffer_->show_prev_page(); } protected: + void play_(Ts... x) override { this->buffer_->show_prev_page(); } + DisplayBuffer *buffer_; }; diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index b6839985b0..5871b0dcbc 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -38,12 +38,12 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + protected: + void play_(Ts... x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } - protected: ESP8266PWM *parent_; }; diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index dfa72a3ea6..f2435a220f 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -14,7 +14,8 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(bool, oscillating) TEMPLATABLE_VALUE(FanSpeed, speed) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto call = this->state_->turn_on(); if (this->oscillating_.has_value()) { call.set_oscillating(this->oscillating_.value(x...)); @@ -25,7 +26,6 @@ template class TurnOnAction : public Action { call.perform(); } - protected: FanState *state_; }; @@ -33,9 +33,9 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(FanState *state) : state_(state) {} - void play(Ts... x) override { this->state_->turn_off().perform(); } - protected: + void play_(Ts... x) override { this->state_->turn_off().perform(); } + FanState *state_; }; @@ -43,9 +43,9 @@ template class ToggleAction : public Action { public: explicit ToggleAction(FanState *state) : state_(state) {} - void play(Ts... x) override { this->state_->toggle().perform(); } - protected: + void play_(Ts... x) override { this->state_->toggle().perform(); } + FanState *state_; }; diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index 397c55f6c4..bf839b4256 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -59,9 +59,9 @@ template class GlobalVarSetAction : public Actionparent_->value() = this->value_.value(x...); } - protected: + void play_(Ts... x) override { this->parent_->value() = this->value_.value(x...); } + C *parent_; }; diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index e6c0510b32..4f164f3409 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -71,7 +71,8 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } - void play(Ts... x) override { + protected: + void play_(Ts... x) override { this->parent_->set_url(this->url_.value(x...)); this->parent_->set_method(this->method_.value(x...)); if (this->body_.has_value()) { @@ -106,7 +107,6 @@ template class HttpRequestSendAction : public Action { this->parent_->close(); } - protected: void encode_json_(Ts... x, JsonObject &root) { for (const auto &item : this->json_) { auto val = item.second; diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 2fcec069b2..85a89f6e43 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -76,9 +76,9 @@ template class ResetAction : public Action { public: explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - void play(Ts... x) override { this->parent_->reset(); } - protected: + void play_(Ts... x) override { this->parent_->reset(); } + IntegrationSensor *parent_; }; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index 3f56f502b0..7ff0987e0e 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -43,12 +43,12 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - void play(Ts... x) { + protected: + void play_(Ts... x) { float freq = this->frequency_.value(x...); this->parent_->apply_frequency(freq); } - protected: LEDCOutput *parent_; }; diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index dfab780658..1c641562d4 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -13,13 +13,13 @@ template class ToggleAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto call = this->state_->toggle(); call.set_transition_length(this->transition_length_.optional_value(x...)); call.perform(); } - protected: LightState *state_; }; @@ -38,7 +38,8 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, color_temperature) TEMPLATABLE_VALUE(std::string, effect) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto call = this->parent_->make_call(); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); @@ -53,7 +54,6 @@ template class LightControlAction : public Action { call.perform(); } - protected: LightState *parent_; }; @@ -64,7 +64,8 @@ template class DimRelativeAction : public Action { TEMPLATABLE_VALUE(float, relative_brightness) TEMPLATABLE_VALUE(uint32_t, transition_length) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto call = this->parent_->make_call(); float rel = this->relative_brightness_.value(x...); float cur; @@ -77,7 +78,6 @@ template class DimRelativeAction : public Action { call.perform(); } - protected: LightState *parent_; }; @@ -143,7 +143,8 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(uint8_t, blue) TEMPLATABLE_VALUE(uint8_t, white) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = this->range_from_.value_or(x..., 0); int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; @@ -159,7 +160,6 @@ template class AddressableSet : public Action { out->schedule_show(); } - protected: LightState *parent_; }; diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 2201fc87f0..bdb2c50d19 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -37,27 +37,30 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->calibrate_zero(); } protected: + void play_(Ts... x) override { this->mhz19_->calibrate_zero(); } + MHZ19Component *mhz19_; }; template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_enable(); } protected: + void play_(Ts... x) override { this->mhz19_->abc_enable(); } + MHZ19Component *mhz19_; }; template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - void play(Ts... x) override { this->mhz19_->abc_disable(); } protected: + void play_(Ts... x) override { this->mhz19_->abc_disable(); } + MHZ19Component *mhz19_; }; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 6f14b0c92c..fbd2435bfc 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -299,12 +299,12 @@ template class MQTTPublishAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...), this->retain_.value(x...)); } - protected: MQTTClientComponent *parent_; }; @@ -316,15 +316,15 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(bool, retain) void set_payload(std::function payload) { this->payload_ = payload; } - void play(Ts... x) override { + + protected: + void play_(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); auto retain = this->retain_.value(x...); this->parent_->publish_json(topic, f, qos, retain); } - - protected: void encode_(Ts... x, JsonObject &root) { this->payload_(x..., root); } std::function payload_; MQTTClientComponent *parent_; diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 8c8a5ab61b..c65b0d3f6e 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -12,9 +12,9 @@ template class TurnOffAction : public Action { public: TurnOffAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_off(); } - protected: + void play_(Ts... x) override { this->output_->turn_off(); } + BinaryOutput *output_; }; @@ -22,9 +22,9 @@ template class TurnOnAction : public Action { public: TurnOnAction(BinaryOutput *output) : output_(output) {} - void play(Ts... x) override { this->output_->turn_on(); } - protected: + void play_(Ts... x) override { this->output_->turn_on(); } + BinaryOutput *output_; }; @@ -33,9 +33,10 @@ template class SetLevelAction : public Action { SetLevelAction(FloatOutput *output) : output_(output) {} TEMPLATABLE_VALUE(float, level) - void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } protected: + void play_(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } + FloatOutput *output_; }; diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 8f379c47b4..0c92cee798 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -71,7 +71,12 @@ template class PIDAutotuneAction : public Action { public: PIDAutotuneAction(PIDClimate *parent) : parent_(parent) {} - void play(Ts... x) { + void set_noiseband(float noiseband) { noiseband_ = noiseband; } + void set_positive_output(float positive_output) { positive_output_ = positive_output; } + void set_negative_output(float negative_output) { negative_output_ = negative_output; } + + protected: + void play_(Ts... x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); tuner->set_output_negative(this->negative_output_); @@ -79,11 +84,6 @@ template class PIDAutotuneAction : public Action { this->parent_->start_autotune(std::move(tuner)); } - void set_noiseband(float noiseband) { noiseband_ = noiseband; } - void set_positive_output(float positive_output) { positive_output_ = positive_output; } - void set_negative_output(float negative_output) { negative_output_ = negative_output; } - - protected: float noiseband_; float positive_output_; float negative_output_; diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index 8a216f5348..af666bf131 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -23,7 +23,9 @@ DECLARE_REMOTE_PROTOCOL(JVC) template class JVCAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) - void encode(RemoteTransmitData *dst, Ts... x) override { + + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { JVCData data{}; data.data = this->data_.value(x...); JVCProtocol().encode(dst, data); diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index b810115f58..a2e9489d09 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -26,7 +26,9 @@ template class LGAction : public RemoteTransmitterActionBasedata_.value(x...); data.nbits = this->nbits_.value(x...); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index c794991eab..8c1db632b6 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -25,7 +25,9 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index b13bd3e92d..1a09b2ffd6 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -26,7 +26,9 @@ template class PanasonicAction : public RemoteTransmitterActionB public: TEMPLATABLE_VALUE(uint16_t, address) TEMPLATABLE_VALUE(uint32_t, command) - void encode(RemoteTransmitData *dst, Ts... x) override { + + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { PanasonicData data{}; data.address = this->address_.value(x...); data.command = this->command_.value(x...); diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index f93e51a033..8761762232 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -25,7 +25,9 @@ template class PioneerAction : public RemoteTransmitterActionBas public: TEMPLATABLE_VALUE(uint16_t, rc_code_1) TEMPLATABLE_VALUE(uint16_t, rc_code_2) - void encode(RemoteTransmitData *dst, Ts... x) override { + + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { PioneerData data{}; data.rc_code_1 = this->rc_code_1_.value(x...); data.rc_code_2 = this->rc_code_2_.value(x...); diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 1d9f1c5acc..39da5fa8fb 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -46,7 +46,8 @@ template class RawAction : public RemoteTransmitterActionBasecode_static_ != nullptr) { for (size_t i = 0; i < this->code_static_len_; i++) { auto val = this->code_static_[i]; @@ -61,7 +62,6 @@ template class RawAction : public RemoteTransmitterActionBaseset_carrier_frequency(this->carrier_frequency_.value(x...)); } - protected: std::function(Ts...)> code_func_{}; const int32_t *code_static_{nullptr}; int32_t code_static_len_{0}; diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index 2e1da74d9f..f90284c23c 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -26,7 +26,9 @@ template class RC5Action : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 8362899cec..480f49ea92 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -71,7 +71,8 @@ template class RCSwitchRawAction : public RemoteTransmitterActio TEMPLATABLE_VALUE(RCSwitchBase, protocol); TEMPLATABLE_VALUE(std::string, code); - void encode(RemoteTransmitData *dst, Ts... x) override { + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { auto code = this->code_.value(x...); uint64_t the_code = decode_binary_string(code); uint8_t nbits = code.size(); @@ -88,7 +89,8 @@ template class RCSwitchTypeAAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(std::string, device); TEMPLATABLE_VALUE(bool, state); - void encode(RemoteTransmitData *dst, Ts... x) override { + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { auto group = this->group_.value(x...); auto device = this->device_.value(x...); auto state = this->state_.value(x...); @@ -111,7 +113,8 @@ template class RCSwitchTypeBAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(uint8_t, channel); TEMPLATABLE_VALUE(bool, state); - void encode(RemoteTransmitData *dst, Ts... x) override { + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { auto address = this->address_.value(x...); auto channel = this->channel_.value(x...); auto state = this->state_.value(x...); @@ -133,7 +136,8 @@ template class RCSwitchTypeCAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(uint8_t, device); TEMPLATABLE_VALUE(bool, state); - void encode(RemoteTransmitData *dst, Ts... x) override { + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { auto family = this->family_.value(x...); auto group = this->group_.value(x...); auto device = this->device_.value(x...); @@ -156,7 +160,8 @@ template class RCSwitchTypeDAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(uint8_t, device); TEMPLATABLE_VALUE(bool, state); - void encode(RemoteTransmitData *dst, Ts... x) override { + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { auto group = this->group_.value(x...); auto device = this->device_.value(x...); auto state = this->state_.value(x...); diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 250b59e55e..c9cae96c7a 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -323,20 +323,20 @@ template class RemoteTransmitterActionBase : public Actionparent_ = parent; } - void play(Ts... x) override { + TEMPLATABLE_VALUE(uint32_t, send_times); + TEMPLATABLE_VALUE(uint32_t, send_wait); + + protected: + virtual void encode_(RemoteTransmitData *dst, Ts... x) = 0; + + void play_(Ts... x) override { auto call = this->parent_->transmit(); - this->encode(call.get_data(), x...); + this->encode_(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); call.set_send_wait(this->send_wait_.value_or(x..., 0)); call.perform(); } - virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; - - TEMPLATABLE_VALUE(uint32_t, send_times); - TEMPLATABLE_VALUE(uint32_t, send_wait); - - protected: RemoteTransmitterBase *parent_{}; }; diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index 50ff02c1aa..0e1666b3b4 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -24,7 +24,9 @@ DECLARE_REMOTE_PROTOCOL(Samsung) template class SamsungAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) - void encode(RemoteTransmitData *dst, Ts... x) override { + + protected: + void encode_(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); SamsungProtocol().encode(dst, data); diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index 9f0bcdf82f..a122d3758c 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -26,7 +26,9 @@ template class SonyAction : public RemoteTransmitterActionBasedata_.value(x...); data.nbits = this->nbits_.value(x...); diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 86713b8a5c..c747be9972 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -68,7 +68,8 @@ template class RFBridgeSendCodeAction : public Action { TEMPLATABLE_VALUE(uint16_t, high) TEMPLATABLE_VALUE(uint32_t, code) - void play(Ts... x) { + protected: + void play_(Ts... x) { RFBridgeData data{}; data.sync = this->sync_.value(x...); data.low = this->low_.value(x...); @@ -77,7 +78,6 @@ template class RFBridgeSendCodeAction : public Action { this->parent_->send_code(data); } - protected: RFBridgeComponent *parent_; }; @@ -85,9 +85,9 @@ template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} - void play(Ts... x) { this->parent_->learn(); } - protected: + void play_(Ts... x) { this->parent_->learn(); } + RFBridgeComponent *parent_; }; diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4220645478..0bbcf21ddb 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -74,9 +74,9 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } protected: + void play_(Ts... x) override { this->encoder_->set_value(this->value_.value(x...)); } RotaryEncoderSensor *encoder_; }; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index a6b208167f..92eba15e41 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -23,9 +23,9 @@ template class ScriptExecuteAction : public Action { public: ScriptExecuteAction(Script *script) : script_(script) {} - void play(Ts... x) override { this->script_->trigger(); } - protected: + void play_(Ts... x) override { this->script_->trigger(); } + Script *script_; }; @@ -33,9 +33,9 @@ template class ScriptStopAction : public Action { public: ScriptStopAction(Script *script) : script_(script) {} - void play(Ts... x) override { this->script_->stop(); } - protected: + void play_(Ts... x) override { this->script_->stop(); } + Script *script_; }; @@ -53,14 +53,11 @@ template class ScriptWaitAction : public Action, public C public: ScriptWaitAction(Script *script) : script_(script) {} - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { - this->play_next(x...); + this->play_next_(x...); return; } this->var_ = std::make_tuple(x...); @@ -74,12 +71,15 @@ template class ScriptWaitAction : public Action, public C if (this->script_->is_running()) return; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } protected: + void play_(Ts... x) override { /* ignore - see play_complex */ + } + Script *script_; std::tuple var_{}; }; diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 079077dba0..e6ddc3b95c 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -25,9 +25,9 @@ template class SensorPublishAction : public Action { public: SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: + void play_(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } Sensor *sensor_; }; diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index a37188740c..b4076e8cd5 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -64,18 +64,18 @@ template class ServoWriteAction : public Action { public: ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) - void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } protected: + void play_(Ts... x) override { this->servo_->write(this->value_.value(x...)); } Servo *servo_; }; template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} - void play(Ts... x) override { this->servo_->detach(); } protected: + void play_(Ts... x) override { this->servo_->detach(); } Servo *servo_; }; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index 696eb8890f..afdb504681 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -78,13 +78,13 @@ template class Sim800LSendSmsAction : public Action { TEMPLATABLE_VALUE(std::string, recipient) TEMPLATABLE_VALUE(std::string, message) - void play(Ts... x) { + protected: + void play_(Ts... x) { auto recipient = this->recipient_.value(x...); auto message = this->message_.value(x...); this->parent_->send_sms(recipient, message); } - protected: Sim800LComponent *parent_; }; diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 33777dce83..31bdf59bf9 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -43,9 +43,9 @@ template class SetTargetAction : public Action { TEMPLATABLE_VALUE(int32_t, target) - void play(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } - protected: + void play_(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } + Stepper *parent_; }; @@ -55,9 +55,9 @@ template class ReportPositionAction : public Action { TEMPLATABLE_VALUE(int32_t, position) - void play(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } - protected: + void play_(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } + Stepper *parent_; }; @@ -67,13 +67,13 @@ template class SetSpeedAction : public Action { TEMPLATABLE_VALUE(float, speed); - void play(Ts... x) override { + protected: + 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_; }; diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 90bdabf0f4..c4824b106b 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -11,9 +11,9 @@ template class TurnOnAction : public Action { public: explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_on(); } - protected: + void play_(Ts... x) override { this->switch_->turn_on(); } + Switch *switch_; }; @@ -21,9 +21,9 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->turn_off(); } - protected: + void play_(Ts... x) override { this->switch_->turn_off(); } + Switch *switch_; }; @@ -31,9 +31,9 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} - void play(Ts... x) override { this->switch_->toggle(); } - protected: + void play_(Ts... x) override { this->switch_->toggle(); } + Switch *switch_; }; @@ -73,9 +73,9 @@ template class SwitchPublishAction : public Action { public: SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) - void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: + void play_(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } Switch *switch_; }; diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 496efb1cc3..9fac17c4cb 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -30,9 +30,9 @@ template class TextSensorPublishAction : public Action { public: TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) - void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: + void play_(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } TextSensor *sensor_; }; diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 6eab24687c..6291cf1ecf 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -43,7 +43,8 @@ template class SetLevelPercentAction : public Action, pub public: TEMPLATABLE_VALUE(uint8_t, level_percent) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto level_percent = this->level_percent_.value(x...); this->parent_->set_level_percent(level_percent); } @@ -53,7 +54,8 @@ template class SetLevelAction : public Action, public Par public: TEMPLATABLE_VALUE(uint8_t, level) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto level = this->level_.value(x...); this->parent_->set_level(level); } @@ -63,20 +65,21 @@ template class SetBrightnessAction : public Action, publi public: TEMPLATABLE_VALUE(uint8_t, brightness) - void play(Ts... x) override { + protected: + void play_(Ts... x) override { auto brightness = this->brightness_.value(x...); this->parent_->set_brightness(brightness); } }; template class TurnOnAction : public Action, public Parented { - public: - void play(Ts... x) override { this->parent_->turn_on(); } + protected: + void play_(Ts... x) override { this->parent_->turn_on(); } }; template class TurnOffAction : public Action, public Parented { - public: - void play(Ts... x) override { this->parent_->turn_off(); } + protected: + void play_(Ts... x) override { this->parent_->turn_off(); } }; } // namespace tm1651 diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 9686f94413..6889bc12e4 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -17,7 +17,8 @@ template class UARTWriteAction : public Action, public Pa this->static_ = true; } - void play(Ts... x) override { + protected: + void play_(Ts... x) override { if (this->static_) { this->parent_->write_array(this->data_static_); } else { @@ -25,8 +26,6 @@ template class UARTWriteAction : public Action, public Pa this->parent_->write_array(val); } } - - protected: bool static_{false}; std::function(Ts...)> data_func_{}; std::vector data_static_{}; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 6e595fc458..cb95364675 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -75,13 +75,25 @@ template class ActionList; template class Action { public: - virtual void play(Ts... x) = 0; virtual void play_complex(Ts... x) { this->num_running_++; - this->play(x...); - this->play_next(x...); + this->play_(x...); + this->play_next_(x...); } - void play_next(Ts... x) { + virtual void stop_complex() { + if (num_running_) { + this->stop_(); + this->num_running_ = 0; + } + this->stop_next_(); + } + virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); } + + protected: + friend ActionList; + + virtual void play_(Ts... x) = 0; + void play_next_(Ts... x) { if (this->num_running_ > 0) { this->num_running_--; if (this->next_ != nullptr) { @@ -89,37 +101,26 @@ template class Action { } } } - virtual void stop() {} - virtual void stop_complex() { - if (num_running_) { - this->stop(); - this->num_running_ = 0; - } - this->stop_next(); + template void play_next_tuple_(const std::tuple &tuple, seq) { + this->play_next_(std::get(tuple)...); } - void stop_next() { + void play_next_tuple_(const std::tuple &tuple) { + this->play_next_tuple_(tuple, typename gens::type()); + } + + virtual void stop_() {} + void stop_next_() { if (this->next_ != nullptr) { this->next_->stop_complex(); } } - virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next(); } - bool is_running_next() { + + bool is_running_next_() { if (this->next_ == nullptr) return false; return this->next_->is_running(); } - void play_next_tuple(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); - } - - protected: - friend ActionList; - - template void play_next_tuple_(const std::tuple &tuple, seq) { - this->play_next(std::get(tuple)...); - } - Action *next_ = nullptr; int num_running_{0}; diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index bd34009790..4edd459424 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -108,27 +108,29 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void stop() override { - this->cancel_timeout(""); - } - - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { - auto f = std::bind(&Action::play_next, this, x...); + auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; this->set_timeout(this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void play_(Ts... x) override { /* ignore - see play_complex */ + } + + void stop_() override { + this->cancel_timeout(""); + } }; template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - void play(Ts... x) override { this->f_(x...); } protected: + void play_(Ts... x) override { this->f_(x...); } + std::function f_; }; @@ -138,15 +140,12 @@ template class IfAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); + this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void add_else(const std::vector *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); - } - - void play(Ts... x) override { /* ignore - see play_complex */ + this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void play_complex(Ts... x) override { @@ -154,25 +153,28 @@ template class IfAction : public Action { bool res = this->condition_->check(x...); if (res) { if (this->then_.empty()) { - this->play_next(x...); + this->play_next_(x...); } else if (this->num_running_ > 0) { this->then_.play(x...); } } else { if (this->else_.empty()) { - this->play_next(x...); + this->play_next_(x...); } else if (this->num_running_ > 0) { this->else_.play(x...); } } } - void stop() override { + protected: + void play_(Ts... x) override { /* ignore - see play_complex */ + } + + void stop_() override { this->then_.stop(); this->else_.stop(); } - protected: Condition *condition_; ActionList then_; ActionList else_; @@ -192,14 +194,11 @@ template class WhileAction : public Action { } } else { // condition false, play next - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } })); } - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { this->num_running_++; // Store loop parameters @@ -208,7 +207,7 @@ template class WhileAction : public Action { if (!this->condition_->check_tuple(this->var_)) { // If new condition check failed, stop loop if running this->then_.stop(); - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); return; } @@ -217,9 +216,12 @@ template class WhileAction : public Action { } } - void stop() override { this->then_.stop(); } - protected: + void play_(Ts... x) override { /* ignore - see play_complex */ + } + + void stop_() override { this->then_.stop(); } + Condition *condition_; ActionList then_; std::tuple var_{}; @@ -229,15 +231,12 @@ template class WaitUntilAction : public Action, public Co public: WaitUntilAction(Condition *condition) : condition_(condition) {} - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { if (this->num_running_ > 0) { - this->play_next(x...); + this->play_next_(x...); } return; } @@ -253,12 +252,15 @@ template class WaitUntilAction : public Action, public Co return; } - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } protected: + void play_(Ts... x) override { /* ignore - see play_complex */ + } + Condition *condition_; std::tuple var_{}; }; @@ -266,9 +268,10 @@ template class WaitUntilAction : public Action, public Co template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { this->component_->update(); } protected: + void play_(Ts... x) override { this->component_->update(); } + PollingComponent *component_; }; From 1bec1faf6db9a7696e96c51152d92b94eda01b29 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 24 May 2020 23:27:28 -0300 Subject: [PATCH 003/200] lint --- .../components/api/homeassistant_service.h | 4 +-- esphome/components/binary_sensor/automation.h | 4 +-- esphome/components/climate/automation.h | 4 +-- esphome/components/cover/automation.h | 20 ++++++------ .../deep_sleep/deep_sleep_component.h | 8 ++--- esphome/components/dfplayer/dfplayer.h | 18 ++++------- esphome/components/display/display_buffer.h | 9 ++---- esphome/components/esp8266_pwm/esp8266_pwm.h | 3 +- esphome/components/fan/automation.h | 9 ++---- .../components/globals/globals_component.h | 4 +-- .../components/http_request/http_request.h | 4 +-- .../integration/integration_sensor.h | 4 +-- esphome/components/ledc/ledc_output.h | 4 +-- esphome/components/light/automation.h | 16 +++++----- esphome/components/mhz19/mhz19.h | 12 +++---- esphome/components/mqtt/mqtt_client.h | 9 +++--- esphome/components/output/automation.h | 12 +++---- esphome/components/pid/pid_climate.h | 4 +-- esphome/components/remote_base/jvc_protocol.h | 3 +- esphome/components/remote_base/lg_protocol.h | 3 +- esphome/components/remote_base/nec_protocol.h | 3 +- .../remote_base/panasonic_protocol.h | 3 +- .../components/remote_base/pioneer_protocol.h | 3 +- esphome/components/remote_base/raw_protocol.h | 4 +-- esphome/components/remote_base/rc5_protocol.h | 3 +- .../remote_base/rc_switch_protocol.h | 15 +++------ esphome/components/remote_base/remote_base.h | 10 +++--- .../components/remote_base/samsung_protocol.h | 3 +- .../components/remote_base/sony_protocol.h | 3 +- esphome/components/rf_bridge/rf_bridge.h | 8 ++--- .../rotary_encoder/rotary_encoder.h | 3 +- esphome/components/script/script.h | 12 +++---- esphome/components/sensor/automation.h | 3 +- esphome/components/servo/servo.h | 6 ++-- esphome/components/sim800l/sim800l.h | 4 +-- esphome/components/stepper/stepper.h | 12 +++---- esphome/components/switch/automation.h | 15 ++++----- esphome/components/text_sensor/automation.h | 3 +- esphome/components/tm1651/tm1651.h | 17 +++++----- esphome/components/uart/automation.h | 4 +-- esphome/core/automation.h | 8 ++--- esphome/core/base_automation.h | 31 +++++++++---------- 42 files changed, 151 insertions(+), 176 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 0877efbf26..8a72765195 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -30,8 +30,7 @@ template class HomeAssistantServiceCallAction : public Actionvariables_.push_back(TemplatableKeyValuePair(key, value)); } - protected: - void play_(Ts... x) override { + void play(Ts... x) override { HomeassistantServiceResponse resp; resp.service = this->service_.value(x...); resp.is_event = this->is_event_; @@ -56,6 +55,7 @@ template class HomeAssistantServiceCallAction : public Actionparent_->send_homeassistant_service_call(resp); } + protected: APIServer *parent_; bool is_event_; std::vector> data_; diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index b76fbdae0c..6b0321628c 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -138,12 +138,12 @@ template class BinarySensorPublishAction : public Action explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); } + protected: BinarySensor *sensor_; }; diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 2fd9d81e6b..0cd52b1036 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -18,8 +18,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto call = this->climate_->make_call(); call.set_mode(this->mode_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...)); @@ -31,6 +30,7 @@ template class ControlAction : public Action { call.perform(); } + protected: Climate *climate_; }; diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index bad641c150..0092f987f2 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -11,9 +11,9 @@ template class OpenAction : public Action { public: explicit OpenAction(Cover *cover) : cover_(cover) {} - protected: - void play_(Ts... x) override { this->cover_->open(); } + void play(Ts... x) override { this->cover_->open(); } + protected: Cover *cover_; }; @@ -21,9 +21,9 @@ template class CloseAction : public Action { public: explicit CloseAction(Cover *cover) : cover_(cover) {} - protected: - void play_(Ts... x) override { this->cover_->close(); } + void play(Ts... x) override { this->cover_->close(); } + protected: Cover *cover_; }; @@ -31,9 +31,9 @@ template class StopAction : public Action { public: explicit StopAction(Cover *cover) : cover_(cover) {} - protected: - void play_(Ts... x) override { this->cover_->stop(); } + void play(Ts... x) override { this->cover_->stop(); } + protected: Cover *cover_; }; @@ -45,8 +45,7 @@ template class ControlAction : public Action { TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, tilt) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) call.set_stop(this->stop_.value(x...)); @@ -57,6 +56,7 @@ template class ControlAction : public Action { call.perform(); } + protected: Cover *cover_; }; @@ -67,8 +67,7 @@ template class CoverPublishAction : public Action { TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(CoverOperation, current_operation) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); if (this->tilt_.has_value()) @@ -78,6 +77,7 @@ template class CoverPublishAction : public Action { this->cover_->publish_state(); } + protected: Cover *cover_; }; diff --git a/esphome/components/deep_sleep/deep_sleep_component.h b/esphome/components/deep_sleep/deep_sleep_component.h index 6287a2259a..4372a3f66c 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.h +++ b/esphome/components/deep_sleep/deep_sleep_component.h @@ -85,9 +85,9 @@ template class EnterDeepSleepAction : public Action { public: EnterDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} - protected: - void play_(Ts... x) override { this->deep_sleep_->begin_sleep(true); } + void play(Ts... x) override { this->deep_sleep_->begin_sleep(true); } + protected: DeepSleepComponent *deep_sleep_; }; @@ -95,9 +95,9 @@ template class PreventDeepSleepAction : public Action { public: PreventDeepSleepAction(DeepSleepComponent *deep_sleep) : deep_sleep_(deep_sleep) {} - protected: - void play_(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } + void play(Ts... x) override { this->deep_sleep_->prevent_deep_sleep(); } + protected: DeepSleepComponent *deep_sleep_; }; diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 89f0fb691c..cb9686bb64 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -104,8 +104,7 @@ class DFPlayer : public uart::UARTDevice, public Component { #define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ template class ACTION_CLASS : public Action, public Parented { \ - protected: \ - void play_(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ }; DFPLAYER_SIMPLE_ACTION(NextAction, next) @@ -116,8 +115,7 @@ template class PlayFileAction : public Action, public Par TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); if (loop) { @@ -134,8 +132,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -151,8 +148,7 @@ template class SetDeviceAction : public Action, public Pa public: TEMPLATABLE_VALUE(Device, device) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); } @@ -162,8 +158,7 @@ template class SetVolumeAction : public Action, public Pa public: TEMPLATABLE_VALUE(uint8_t, volume) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); } @@ -173,8 +168,7 @@ template class SetEqAction : public Action, public Parent public: TEMPLATABLE_VALUE(EqPreset, eq) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 2a6ead3306..85871df16e 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -392,8 +392,7 @@ template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto *page = this->page_.value(x...); if (page != nullptr) { page->show(); @@ -405,8 +404,7 @@ template class DisplayPageShowNextAction : public Action public: DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} - protected: - void play_(Ts... x) override { this->buffer_->show_next_page(); } + void play(Ts... x) override { this->buffer_->show_next_page(); } DisplayBuffer *buffer_; }; @@ -415,8 +413,7 @@ template class DisplayPageShowPrevAction : public Action public: DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} - protected: - void play_(Ts... x) override { this->buffer_->show_prev_page(); } + void play(Ts... x) override { this->buffer_->show_prev_page(); } DisplayBuffer *buffer_; }; diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 5871b0dcbc..51b74f48ba 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -38,8 +38,7 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - protected: - void play_(Ts... x) { + void play(Ts... x) { float freq = this->frequency_.value(x...); this->parent_->update_frequency(freq); } diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index f2435a220f..d96ed994e8 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -14,8 +14,7 @@ template class TurnOnAction : public Action { TEMPLATABLE_VALUE(bool, oscillating) TEMPLATABLE_VALUE(FanSpeed, speed) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto call = this->state_->turn_on(); if (this->oscillating_.has_value()) { call.set_oscillating(this->oscillating_.value(x...)); @@ -33,8 +32,7 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(FanState *state) : state_(state) {} - protected: - void play_(Ts... x) override { this->state_->turn_off().perform(); } + void play(Ts... x) override { this->state_->turn_off().perform(); } FanState *state_; }; @@ -43,8 +41,7 @@ template class ToggleAction : public Action { public: explicit ToggleAction(FanState *state) : state_(state) {} - protected: - void play_(Ts... x) override { this->state_->toggle().perform(); } + void play(Ts... x) override { this->state_->toggle().perform(); } FanState *state_; }; diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index bf839b4256..397c55f6c4 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -59,9 +59,9 @@ template class GlobalVarSetAction : public Actionparent_->value() = this->value_.value(x...); } + void play(Ts... x) override { this->parent_->value() = this->value_.value(x...); } + protected: C *parent_; }; diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index 4f164f3409..e6c0510b32 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -71,8 +71,7 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } - protected: - void play_(Ts... x) override { + void play(Ts... x) override { this->parent_->set_url(this->url_.value(x...)); this->parent_->set_method(this->method_.value(x...)); if (this->body_.has_value()) { @@ -107,6 +106,7 @@ template class HttpRequestSendAction : public Action { this->parent_->close(); } + protected: void encode_json_(Ts... x, JsonObject &root) { for (const auto &item : this->json_) { auto val = item.second; diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 85a89f6e43..2fcec069b2 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -76,9 +76,9 @@ template class ResetAction : public Action { public: explicit ResetAction(IntegrationSensor *parent) : parent_(parent) {} - protected: - void play_(Ts... x) override { this->parent_->reset(); } + void play(Ts... x) override { this->parent_->reset(); } + protected: IntegrationSensor *parent_; }; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index 7ff0987e0e..3f56f502b0 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -43,12 +43,12 @@ template class SetFrequencyAction : public Action { SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} TEMPLATABLE_VALUE(float, frequency); - protected: - void play_(Ts... x) { + void play(Ts... x) { float freq = this->frequency_.value(x...); this->parent_->apply_frequency(freq); } + protected: LEDCOutput *parent_; }; diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 1c641562d4..dfab780658 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -13,13 +13,13 @@ template class ToggleAction : public Action { TEMPLATABLE_VALUE(uint32_t, transition_length) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto call = this->state_->toggle(); call.set_transition_length(this->transition_length_.optional_value(x...)); call.perform(); } + protected: LightState *state_; }; @@ -38,8 +38,7 @@ template class LightControlAction : public Action { TEMPLATABLE_VALUE(float, color_temperature) TEMPLATABLE_VALUE(std::string, effect) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto call = this->parent_->make_call(); call.set_state(this->state_.optional_value(x...)); call.set_brightness(this->brightness_.optional_value(x...)); @@ -54,6 +53,7 @@ template class LightControlAction : public Action { call.perform(); } + protected: LightState *parent_; }; @@ -64,8 +64,7 @@ template class DimRelativeAction : public Action { TEMPLATABLE_VALUE(float, relative_brightness) TEMPLATABLE_VALUE(uint32_t, transition_length) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto call = this->parent_->make_call(); float rel = this->relative_brightness_.value(x...); float cur; @@ -78,6 +77,7 @@ template class DimRelativeAction : public Action { call.perform(); } + protected: LightState *parent_; }; @@ -143,8 +143,7 @@ template class AddressableSet : public Action { TEMPLATABLE_VALUE(uint8_t, blue) TEMPLATABLE_VALUE(uint8_t, white) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto *out = (AddressableLight *) this->parent_->get_output(); int32_t range_from = this->range_from_.value_or(x..., 0); int32_t range_to = this->range_to_.value_or(x..., out->size() - 1) + 1; @@ -160,6 +159,7 @@ template class AddressableSet : public Action { out->schedule_show(); } + protected: LightState *parent_; }; diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index bdb2c50d19..151351be4c 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -38,9 +38,9 @@ template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - protected: - void play_(Ts... x) override { this->mhz19_->calibrate_zero(); } + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } + protected: MHZ19Component *mhz19_; }; @@ -48,9 +48,9 @@ template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - protected: - void play_(Ts... x) override { this->mhz19_->abc_enable(); } + void play(Ts... x) override { this->mhz19_->abc_enable(); } + protected: MHZ19Component *mhz19_; }; @@ -58,9 +58,9 @@ template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} - protected: - void play_(Ts... x) override { this->mhz19_->abc_disable(); } + void play(Ts... x) override { this->mhz19_->abc_disable(); } + protected: MHZ19Component *mhz19_; }; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index fbd2435bfc..2bbebff845 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -299,12 +299,12 @@ template class MQTTPublishAction : public Action { TEMPLATABLE_VALUE(uint8_t, qos) TEMPLATABLE_VALUE(bool, retain) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { this->parent_->publish(this->topic_.value(x...), this->payload_.value(x...), this->qos_.value(x...), this->retain_.value(x...)); } + protected: MQTTClientComponent *parent_; }; @@ -317,14 +317,15 @@ template class MQTTPublishJsonAction : public Action { void set_payload(std::function payload) { this->payload_ = payload; } - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); auto qos = this->qos_.value(x...); auto retain = this->retain_.value(x...); this->parent_->publish_json(topic, f, qos, retain); } + + protected: void encode_(Ts... x, JsonObject &root) { this->payload_(x..., root); } std::function payload_; MQTTClientComponent *parent_; diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index c65b0d3f6e..51c2849702 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -12,9 +12,9 @@ template class TurnOffAction : public Action { public: TurnOffAction(BinaryOutput *output) : output_(output) {} - protected: - void play_(Ts... x) override { this->output_->turn_off(); } + void play(Ts... x) override { this->output_->turn_off(); } + protected: BinaryOutput *output_; }; @@ -22,9 +22,9 @@ template class TurnOnAction : public Action { public: TurnOnAction(BinaryOutput *output) : output_(output) {} - protected: - void play_(Ts... x) override { this->output_->turn_on(); } + void play(Ts... x) override { this->output_->turn_on(); } + protected: BinaryOutput *output_; }; @@ -34,9 +34,9 @@ template class SetLevelAction : public Action { TEMPLATABLE_VALUE(float, level) - protected: - void play_(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } + void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } + protected: FloatOutput *output_; }; diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 0c92cee798..3dae92af5f 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -75,8 +75,7 @@ template class PIDAutotuneAction : public Action { void set_positive_output(float positive_output) { positive_output_ = positive_output; } void set_negative_output(float negative_output) { negative_output_ = negative_output; } - protected: - void play_(Ts... x) { + void play(Ts... x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); tuner->set_output_negative(this->negative_output_); @@ -84,6 +83,7 @@ template class PIDAutotuneAction : public Action { this->parent_->start_autotune(std::move(tuner)); } + protected: float noiseband_; float positive_output_; float negative_output_; diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index af666bf131..fc40a6a874 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -24,8 +24,7 @@ template class JVCAction : public RemoteTransmitterActionBasedata_.value(x...); JVCProtocol().encode(dst, data); diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index a2e9489d09..6267560443 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -27,8 +27,7 @@ template class LGAction : public RemoteTransmitterActionBasedata_.value(x...); data.nbits = this->nbits_.value(x...); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index 8c1db632b6..593a3efe17 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -26,8 +26,7 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index 1a09b2ffd6..eae97a8a14 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -27,8 +27,7 @@ template class PanasonicAction : public RemoteTransmitterActionB TEMPLATABLE_VALUE(uint16_t, address) TEMPLATABLE_VALUE(uint32_t, command) - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { PanasonicData data{}; data.address = this->address_.value(x...); data.command = this->command_.value(x...); diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index 8761762232..4cac4f9f32 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -26,8 +26,7 @@ template class PioneerAction : public RemoteTransmitterActionBas TEMPLATABLE_VALUE(uint16_t, rc_code_1) TEMPLATABLE_VALUE(uint16_t, rc_code_2) - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { PioneerData data{}; data.rc_code_1 = this->rc_code_1_.value(x...); data.rc_code_2 = this->rc_code_2_.value(x...); diff --git a/esphome/components/remote_base/raw_protocol.h b/esphome/components/remote_base/raw_protocol.h index 39da5fa8fb..1d9f1c5acc 100644 --- a/esphome/components/remote_base/raw_protocol.h +++ b/esphome/components/remote_base/raw_protocol.h @@ -46,8 +46,7 @@ template class RawAction : public RemoteTransmitterActionBasecode_static_ != nullptr) { for (size_t i = 0; i < this->code_static_len_; i++) { auto val = this->code_static_[i]; @@ -62,6 +61,7 @@ template class RawAction : public RemoteTransmitterActionBaseset_carrier_frequency(this->carrier_frequency_.value(x...)); } + protected: std::function(Ts...)> code_func_{}; const int32_t *code_static_{nullptr}; int32_t code_static_len_{0}; diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index f90284c23c..589c8d42de 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -27,8 +27,7 @@ template class RC5Action : public RemoteTransmitterActionBaseaddress_.value(x...); data.command = this->command_.value(x...); diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 480f49ea92..8362899cec 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -71,8 +71,7 @@ template class RCSwitchRawAction : public RemoteTransmitterActio TEMPLATABLE_VALUE(RCSwitchBase, protocol); TEMPLATABLE_VALUE(std::string, code); - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { auto code = this->code_.value(x...); uint64_t the_code = decode_binary_string(code); uint8_t nbits = code.size(); @@ -89,8 +88,7 @@ template class RCSwitchTypeAAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(std::string, device); TEMPLATABLE_VALUE(bool, state); - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { auto group = this->group_.value(x...); auto device = this->device_.value(x...); auto state = this->state_.value(x...); @@ -113,8 +111,7 @@ template class RCSwitchTypeBAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(uint8_t, channel); TEMPLATABLE_VALUE(bool, state); - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { auto address = this->address_.value(x...); auto channel = this->channel_.value(x...); auto state = this->state_.value(x...); @@ -136,8 +133,7 @@ template class RCSwitchTypeCAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(uint8_t, device); TEMPLATABLE_VALUE(bool, state); - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { auto family = this->family_.value(x...); auto group = this->group_.value(x...); auto device = this->device_.value(x...); @@ -160,8 +156,7 @@ template class RCSwitchTypeDAction : public RemoteTransmitterAct TEMPLATABLE_VALUE(uint8_t, device); TEMPLATABLE_VALUE(bool, state); - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { auto group = this->group_.value(x...); auto device = this->device_.value(x...); auto state = this->state_.value(x...); diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index c9cae96c7a..916fe29c1f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -326,17 +326,17 @@ template class RemoteTransmitterActionBase : public Actionparent_->transmit(); - this->encode_(call.get_data(), x...); + this->encode(call.get_data(), x...); call.set_send_times(this->send_times_.value_or(x..., 1)); call.set_send_wait(this->send_wait_.value_or(x..., 0)); call.perform(); } + protected: + virtual void encode(RemoteTransmitData *dst, Ts... x) = 0; + RemoteTransmitterBase *parent_{}; }; diff --git a/esphome/components/remote_base/samsung_protocol.h b/esphome/components/remote_base/samsung_protocol.h index 0e1666b3b4..f7a54788e5 100644 --- a/esphome/components/remote_base/samsung_protocol.h +++ b/esphome/components/remote_base/samsung_protocol.h @@ -25,8 +25,7 @@ template class SamsungAction : public RemoteTransmitterActionBas public: TEMPLATABLE_VALUE(uint32_t, data) - protected: - void encode_(RemoteTransmitData *dst, Ts... x) override { + void encode(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); SamsungProtocol().encode(dst, data); diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index a122d3758c..aecc8ab91c 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -27,8 +27,7 @@ template class SonyAction : public RemoteTransmitterActionBasedata_.value(x...); data.nbits = this->nbits_.value(x...); diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index c747be9972..86713b8a5c 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -68,8 +68,7 @@ template class RFBridgeSendCodeAction : public Action { TEMPLATABLE_VALUE(uint16_t, high) TEMPLATABLE_VALUE(uint32_t, code) - protected: - void play_(Ts... x) { + void play(Ts... x) { RFBridgeData data{}; data.sync = this->sync_.value(x...); data.low = this->low_.value(x...); @@ -78,6 +77,7 @@ template class RFBridgeSendCodeAction : public Action { this->parent_->send_code(data); } + protected: RFBridgeComponent *parent_; }; @@ -85,9 +85,9 @@ template class RFBridgeLearnAction : public Action { public: RFBridgeLearnAction(RFBridgeComponent *parent) : parent_(parent) {} - protected: - void play_(Ts... x) { this->parent_->learn(); } + void play(Ts... x) { this->parent_->learn(); } + protected: RFBridgeComponent *parent_; }; diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 0bbcf21ddb..f0e47dfe0a 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -75,8 +75,9 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } + protected: - void play_(Ts... x) override { this->encoder_->set_value(this->value_.value(x...)); } RotaryEncoderSensor *encoder_; }; diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 92eba15e41..8495014f00 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -23,9 +23,9 @@ template class ScriptExecuteAction : public Action { public: ScriptExecuteAction(Script *script) : script_(script) {} - protected: - void play_(Ts... x) override { this->script_->trigger(); } + void play(Ts... x) override { this->script_->trigger(); } + protected: Script *script_; }; @@ -33,9 +33,9 @@ template class ScriptStopAction : public Action { public: ScriptStopAction(Script *script) : script_(script) {} - protected: - void play_(Ts... x) override { this->script_->stop(); } + void play(Ts... x) override { this->script_->stop(); } + protected: Script *script_; }; @@ -76,10 +76,10 @@ template class ScriptWaitAction : public Action, public C float get_setup_priority() const override { return setup_priority::DATA; } - protected: - void play_(Ts... x) override { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } + protected: Script *script_; std::tuple var_{}; }; diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index e6ddc3b95c..c70fb93963 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -26,8 +26,9 @@ template class SensorPublishAction : public Action { SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + protected: - void play_(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } Sensor *sensor_; }; diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index b4076e8cd5..b864efc877 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -65,8 +65,9 @@ template class ServoWriteAction : public Action { ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) + void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } + protected: - void play_(Ts... x) override { this->servo_->write(this->value_.value(x...)); } Servo *servo_; }; @@ -74,8 +75,9 @@ template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} + void play(Ts... x) override { this->servo_->detach(); } + protected: - void play_(Ts... x) override { this->servo_->detach(); } Servo *servo_; }; diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h index afdb504681..696eb8890f 100644 --- a/esphome/components/sim800l/sim800l.h +++ b/esphome/components/sim800l/sim800l.h @@ -78,13 +78,13 @@ template class Sim800LSendSmsAction : public Action { TEMPLATABLE_VALUE(std::string, recipient) TEMPLATABLE_VALUE(std::string, message) - protected: - void play_(Ts... x) { + void play(Ts... x) { auto recipient = this->recipient_.value(x...); auto message = this->message_.value(x...); this->parent_->send_sms(recipient, message); } + protected: Sim800LComponent *parent_; }; diff --git a/esphome/components/stepper/stepper.h b/esphome/components/stepper/stepper.h index 31bdf59bf9..33777dce83 100644 --- a/esphome/components/stepper/stepper.h +++ b/esphome/components/stepper/stepper.h @@ -43,9 +43,9 @@ template class SetTargetAction : public Action { TEMPLATABLE_VALUE(int32_t, target) - protected: - void play_(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } + void play(Ts... x) override { this->parent_->set_target(this->target_.value(x...)); } + protected: Stepper *parent_; }; @@ -55,9 +55,9 @@ template class ReportPositionAction : public Action { TEMPLATABLE_VALUE(int32_t, position) - protected: - void play_(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } + void play(Ts... x) override { this->parent_->report_position(this->position_.value(x...)); } + protected: Stepper *parent_; }; @@ -67,13 +67,13 @@ template class SetSpeedAction : public Action { TEMPLATABLE_VALUE(float, speed); - protected: - void play_(Ts... x) override { + 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_; }; diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index c4824b106b..579daf4d24 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -11,9 +11,9 @@ template class TurnOnAction : public Action { public: explicit TurnOnAction(Switch *a_switch) : switch_(a_switch) {} - protected: - void play_(Ts... x) override { this->switch_->turn_on(); } + void play(Ts... x) override { this->switch_->turn_on(); } + protected: Switch *switch_; }; @@ -21,9 +21,9 @@ template class TurnOffAction : public Action { public: explicit TurnOffAction(Switch *a_switch) : switch_(a_switch) {} - protected: - void play_(Ts... x) override { this->switch_->turn_off(); } + void play(Ts... x) override { this->switch_->turn_off(); } + protected: Switch *switch_; }; @@ -31,9 +31,9 @@ template class ToggleAction : public Action { public: explicit ToggleAction(Switch *a_switch) : switch_(a_switch) {} - protected: - void play_(Ts... x) override { this->switch_->toggle(); } + void play(Ts... x) override { this->switch_->toggle(); } + protected: Switch *switch_; }; @@ -74,8 +74,9 @@ template class SwitchPublishAction : public Action { SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } + protected: - void play_(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } Switch *switch_; }; diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 9fac17c4cb..6810d10b13 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -31,8 +31,9 @@ template class TextSensorPublishAction : public Action { TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } + protected: - void play_(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } TextSensor *sensor_; }; diff --git a/esphome/components/tm1651/tm1651.h b/esphome/components/tm1651/tm1651.h index 6291cf1ecf..6eab24687c 100644 --- a/esphome/components/tm1651/tm1651.h +++ b/esphome/components/tm1651/tm1651.h @@ -43,8 +43,7 @@ template class SetLevelPercentAction : public Action, pub public: TEMPLATABLE_VALUE(uint8_t, level_percent) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto level_percent = this->level_percent_.value(x...); this->parent_->set_level_percent(level_percent); } @@ -54,8 +53,7 @@ template class SetLevelAction : public Action, public Par public: TEMPLATABLE_VALUE(uint8_t, level) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto level = this->level_.value(x...); this->parent_->set_level(level); } @@ -65,21 +63,20 @@ template class SetBrightnessAction : public Action, publi public: TEMPLATABLE_VALUE(uint8_t, brightness) - protected: - void play_(Ts... x) override { + void play(Ts... x) override { auto brightness = this->brightness_.value(x...); this->parent_->set_brightness(brightness); } }; template class TurnOnAction : public Action, public Parented { - protected: - void play_(Ts... x) override { this->parent_->turn_on(); } + public: + void play(Ts... x) override { this->parent_->turn_on(); } }; template class TurnOffAction : public Action, public Parented { - protected: - void play_(Ts... x) override { this->parent_->turn_off(); } + public: + void play(Ts... x) override { this->parent_->turn_off(); } }; } // namespace tm1651 diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 6889bc12e4..2212e58449 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -17,8 +17,7 @@ template class UARTWriteAction : public Action, public Pa this->static_ = true; } - protected: - void play_(Ts... x) override { + void play(Ts... x) override { if (this->static_) { this->parent_->write_array(this->data_static_); } else { @@ -26,6 +25,7 @@ template class UARTWriteAction : public Action, public Pa this->parent_->write_array(val); } } + protected: bool static_{false}; std::function(Ts...)> data_func_{}; std::vector data_static_{}; diff --git a/esphome/core/automation.h b/esphome/core/automation.h index cb95364675..02bd8bb299 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -77,12 +77,12 @@ template class Action { public: virtual void play_complex(Ts... x) { this->num_running_++; - this->play_(x...); + this->play(x...); this->play_next_(x...); } virtual void stop_complex() { if (num_running_) { - this->stop_(); + this->stop(); this->num_running_ = 0; } this->stop_next_(); @@ -92,7 +92,7 @@ template class Action { protected: friend ActionList; - virtual void play_(Ts... x) = 0; + virtual void play(Ts... x) = 0; void play_next_(Ts... x) { if (this->num_running_ > 0) { this->num_running_--; @@ -108,7 +108,7 @@ template class Action { this->play_next_tuple_(tuple, typename gens::type()); } - virtual void stop_() {} + virtual void stop() {} void stop_next_() { if (this->next_ != nullptr) { this->next_->stop_complex(); diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index 4edd459424..d2656290bc 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -115,22 +115,19 @@ template class DelayAction : public Action, public Compon } float get_setup_priority() const override { return setup_priority::HARDWARE; } - protected: - void play_(Ts... x) override { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } - void stop_() override { - this->cancel_timeout(""); - } + void stop() override { this->cancel_timeout(""); } }; template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} - protected: - void play_(Ts... x) override { this->f_(x...); } + void play(Ts... x) override { this->f_(x...); } + protected: std::function f_; }; @@ -166,15 +163,15 @@ template class IfAction : public Action { } } - protected: - void play_(Ts... x) override { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } - void stop_() override { + void stop() override { this->then_.stop(); this->else_.stop(); } + protected: Condition *condition_; ActionList then_; ActionList else_; @@ -216,12 +213,12 @@ template class WhileAction : public Action { } } - protected: - void play_(Ts... x) override { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } - void stop_() override { this->then_.stop(); } + void stop() override { this->then_.stop(); } + protected: Condition *condition_; ActionList then_; std::tuple var_{}; @@ -257,10 +254,10 @@ template class WaitUntilAction : public Action, public Co float get_setup_priority() const override { return setup_priority::DATA; } - protected: - void play_(Ts... x) override { /* ignore - see play_complex */ + void play(Ts... x) override { /* ignore - see play_complex */ } + protected: Condition *condition_; std::tuple var_{}; }; @@ -269,9 +266,9 @@ template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} - protected: - void play_(Ts... x) override { this->component_->update(); } + void play(Ts... x) override { this->component_->update(); } + protected: PollingComponent *component_; }; From 9e7e8ab116b47e784dd480dd870c4d23ad891ee9 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 24 May 2020 23:45:15 -0300 Subject: [PATCH 004/200] format --- esphome/components/uart/automation.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/uart/automation.h b/esphome/components/uart/automation.h index 2212e58449..9686f94413 100644 --- a/esphome/components/uart/automation.h +++ b/esphome/components/uart/automation.h @@ -25,6 +25,7 @@ template class UARTWriteAction : public Action, public Pa this->parent_->write_array(val); } } + protected: bool static_{false}; std::function(Ts...)> data_func_{}; From 3fba3a5e2e812485a49dd7cf38aed7e1ff403c94 Mon Sep 17 00:00:00 2001 From: Alexander Pohl Date: Wed, 27 May 2020 00:33:28 +0200 Subject: [PATCH 005/200] Add support for additional Xiaomi BLE sensors (#1027) (#1027) --- .travis.yml | 1 + esphome/components/xiaomi_ble/xiaomi_ble.cpp | 437 ++++++++++++------ esphome/components/xiaomi_ble/xiaomi_ble.h | 50 +- esphome/components/xiaomi_cgd1/__init__.py | 0 esphome/components/xiaomi_cgd1/sensor.py | 43 ++ .../components/xiaomi_cgd1/xiaomi_cgd1.cpp | 77 +++ esphome/components/xiaomi_cgd1/xiaomi_cgd1.h | 36 ++ .../components/xiaomi_cgg1/xiaomi_cgg1.cpp | 42 ++ esphome/components/xiaomi_cgg1/xiaomi_cgg1.h | 17 +- esphome/components/xiaomi_gcls002/__init__.py | 0 esphome/components/xiaomi_gcls002/sensor.py | 45 ++ .../xiaomi_gcls002/xiaomi_gcls002.cpp | 66 +++ .../xiaomi_gcls002/xiaomi_gcls002.h | 37 ++ esphome/components/xiaomi_hhccjcy01/sensor.py | 8 +- .../xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp | 45 +- .../xiaomi_hhccjcy01/xiaomi_hhccjcy01.h | 23 +- .../components/xiaomi_hhccpot002/__init__.py | 0 .../components/xiaomi_hhccpot002/sensor.py | 35 ++ .../xiaomi_hhccpot002/xiaomi_hhccpot002.cpp | 60 +++ .../xiaomi_hhccpot002/xiaomi_hhccpot002.h | 33 ++ .../components/xiaomi_jqjcy01ym/__init__.py | 0 esphome/components/xiaomi_jqjcy01ym/sensor.py | 44 ++ .../xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp | 66 +++ .../xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h | 37 ++ .../xiaomi_lywsd02/xiaomi_lywsd02.cpp | 40 ++ .../xiaomi_lywsd02/xiaomi_lywsd02.h | 15 +- .../components/xiaomi_lywsd03mmc/__init__.py | 0 .../components/xiaomi_lywsd03mmc/sensor.py | 44 ++ .../xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp | 81 ++++ .../xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h | 36 ++ .../xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp | 42 ++ .../xiaomi_lywsdcgq/xiaomi_lywsdcgq.h | 17 +- .../components/xiaomi_mjyd02yla/__init__.py | 0 .../xiaomi_mjyd02yla/binary_sensor.py | 47 ++ .../xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp | 79 ++++ .../xiaomi_mjyd02yla/xiaomi_mjyd02yla.h | 40 ++ .../components/xiaomi_mue4094rt/__init__.py | 0 .../xiaomi_mue4094rt/binary_sensor.py | 29 ++ .../xiaomi_mue4094rt/xiaomi_mue4094rt.cpp | 59 +++ .../xiaomi_mue4094rt/xiaomi_mue4094rt.h | 33 ++ esphome/components/xiaomi_wx08zm/__init__.py | 0 .../components/xiaomi_wx08zm/binary_sensor.py | 36 ++ .../xiaomi_wx08zm/xiaomi_wx08zm.cpp | 64 +++ .../components/xiaomi_wx08zm/xiaomi_wx08zm.h | 36 ++ esphome/config_validation.py | 17 + esphome/const.py | 10 + tests/test2.yaml | 151 ++++-- tests/test4.yaml | 73 +++ 48 files changed, 1875 insertions(+), 276 deletions(-) create mode 100644 esphome/components/xiaomi_cgd1/__init__.py create mode 100644 esphome/components/xiaomi_cgd1/sensor.py create mode 100644 esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp create mode 100644 esphome/components/xiaomi_cgd1/xiaomi_cgd1.h create mode 100644 esphome/components/xiaomi_gcls002/__init__.py create mode 100644 esphome/components/xiaomi_gcls002/sensor.py create mode 100644 esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp create mode 100644 esphome/components/xiaomi_gcls002/xiaomi_gcls002.h create mode 100644 esphome/components/xiaomi_hhccpot002/__init__.py create mode 100644 esphome/components/xiaomi_hhccpot002/sensor.py create mode 100644 esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp create mode 100644 esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h create mode 100644 esphome/components/xiaomi_jqjcy01ym/__init__.py create mode 100644 esphome/components/xiaomi_jqjcy01ym/sensor.py create mode 100644 esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp create mode 100644 esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h create mode 100644 esphome/components/xiaomi_lywsd03mmc/__init__.py create mode 100644 esphome/components/xiaomi_lywsd03mmc/sensor.py create mode 100644 esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp create mode 100644 esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h create mode 100644 esphome/components/xiaomi_mjyd02yla/__init__.py create mode 100644 esphome/components/xiaomi_mjyd02yla/binary_sensor.py create mode 100644 esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp create mode 100644 esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h create mode 100644 esphome/components/xiaomi_mue4094rt/__init__.py create mode 100644 esphome/components/xiaomi_mue4094rt/binary_sensor.py create mode 100644 esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp create mode 100644 esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h create mode 100644 esphome/components/xiaomi_wx08zm/__init__.py create mode 100644 esphome/components/xiaomi_wx08zm/binary_sensor.py create mode 100644 esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp create mode 100644 esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h create mode 100644 tests/test4.yaml diff --git a/.travis.yml b/.travis.yml index ca0a3082db..3e6c7adccb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ matrix: - esphome tests/test1.yaml compile - esphome tests/test2.yaml compile - esphome tests/test3.yaml compile + - esphome tests/test4.yaml compile - env: TARGET=Cpp-Lint dist: trusty sudo: required diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 030ee73d4b..2ca27bf51a 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -1,187 +1,322 @@ #include "xiaomi_ble.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 +#include +#include "mbedtls/ccm.h" + namespace esphome { namespace xiaomi_ble { static const char *TAG = "xiaomi_ble"; -bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result) { - switch (data_type) { - case 0x0D: { // temperature+humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % - if (data_length != 4) - return false; - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); - result.temperature = temperature / 10.0f; - result.humidity = humidity / 10.0f; - return true; - } - case 0x0A: { // battery, 1 byte, 8-bit unsigned integer, 1 % - if (data_length != 1) - return false; - result.battery_level = data[0]; - return true; - } - case 0x06: { // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % - if (data_length != 2) - return false; - const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.humidity = humidity / 10.0f; - return true; - } - case 0x04: { // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C - if (data_length != 2) - return false; - const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.temperature = temperature / 10.0f; - return true; - } - case 0x09: { // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm - if (data_length != 2) - return false; - const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); - result.conductivity = conductivity; - return true; - } - case 0x07: { // illuminance, 3 bytes, 24-bit unsigned integer (LE), 1 lx - if (data_length != 3) - return false; - const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); - result.illuminance = illuminance; - return true; - } - case 0x08: { // soil moisture, 1 byte, 8-bit unsigned integer, 1 % - if (data_length != 1) - return false; - result.moisture = data[0]; - return true; - } - default: - return false; - } -} -bool parse_xiaomi_service_data(XiaomiParseResult &result, const esp32_ble_tracker::ServiceData &service_data) { - if (!service_data.uuid.contains(0x95, 0xFE)) { - // ESP_LOGVV(TAG, "Xiaomi no service data UUID magic bytes"); +bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result) { + result.has_encryption = (message[0] & 0x08) ? true : false; // update encryption status + if (result.has_encryption) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload is encrypted, stop reading message."); return false; } - const auto raw = service_data.data; - - if (raw.size() < 14) { - // ESP_LOGVV(TAG, "Xiaomi service data too short!"); - return false; - } - - bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; - bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; - bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; - bool is_cgg1 = ((raw[1] & 0x30) == 0x30 || (raw[1] & 0x20) == 0x20) && raw[2] == 0x47 && raw[3] == 0x03; - - if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { - // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); - return false; - } - - result.type = XiaomiParseResult::TYPE_HHCCJCY01; - if (is_lywsdcgq) { - result.type = XiaomiParseResult::TYPE_LYWSDCGQ; - } else if (is_lywsd02) { - result.type = XiaomiParseResult::TYPE_LYWSD02; - } else if (is_cgg1) { - result.type = XiaomiParseResult::TYPE_CGG1; - } - - uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; - // Data point specs // Byte 0: type // Byte 1: fixed 0x10 // Byte 2: length // Byte 3..3+len-1: data point value - const uint8_t *raw_data = &raw[raw_offset]; - uint8_t data_offset = 0; - uint8_t data_length = raw.size() - raw_offset; - bool success = false; + const uint8_t *raw = message.data() + result.raw_offset; + const uint8_t *data = raw + 3; + const uint8_t data_length = raw[2]; - while (true) { - if (data_length < 4) - // at least 4 bytes required - // type, fixed 0x10, length, 1 byte value - break; - - const uint8_t datapoint_type = raw_data[data_offset + 0]; - const uint8_t datapoint_length = raw_data[data_offset + 2]; - - if (data_length < 3 + datapoint_length) - // 3 fixed bytes plus value length - break; - - const uint8_t *datapoint_data = &raw_data[data_offset + 3]; - - if (parse_xiaomi_data_byte(datapoint_type, datapoint_data, datapoint_length, result)) - success = true; - - data_length -= data_offset + 3 + datapoint_length; - data_offset += 3 + datapoint_length; - } - - return success; -} -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device) { - XiaomiParseResult result; - bool success = false; - for (auto &service_data : device.get_service_datas()) { - if (parse_xiaomi_service_data(result, service_data)) - success = true; - } - if (!success) - return {}; - return result; -} - -bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { - auto res = parse_xiaomi(device); - if (!res.has_value()) + if ((data_length < 1) || (data_length > 4)) { + ESP_LOGVV(TAG, "parse_xiaomi_message(): payload has wrong size (%d)!", data_length); return false; - - const char *name = "HHCCJCY01"; - if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) { - name = "LYWSDCGQ"; - } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { - name = "LYWSD02"; - } else if (res->type == XiaomiParseResult::TYPE_CGG1) { - name = "CGG1"; } - ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str()); - - if (res->temperature.has_value()) { - ESP_LOGD(TAG, " Temperature: %.1f°C", *res->temperature); + // motion detection, 1 byte, 8-bit unsigned integer + if ((raw[0] == 0x03) && (data_length == 1)) { + result.has_motion = (data[0]) ? true : false; } - if (res->humidity.has_value()) { - ESP_LOGD(TAG, " Humidity: %.1f%%", *res->humidity); + // temperature, 2 bytes, 16-bit signed integer (LE), 0.1 °C + else if ((raw[0] == 0x04) && (data_length == 2)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.temperature = temperature / 10.0f; } - if (res->battery_level.has_value()) { - ESP_LOGD(TAG, " Battery Level: %.0f%%", *res->battery_level); + // humidity, 2 bytes, 16-bit signed integer (LE), 0.1 % + else if ((raw[0] == 0x06) && (data_length == 2)) { + const int16_t humidity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.humidity = humidity / 10.0f; } - if (res->conductivity.has_value()) { - ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *res->conductivity); + // illuminance (+ motion), 3 bytes, 24-bit unsigned integer (LE), 1 lx + else if (((raw[0] == 0x07) || (raw[0] == 0x0F)) && (data_length == 3)) { + const uint32_t illuminance = uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16); + result.illuminance = illuminance; + result.is_light = (illuminance == 100) ? true : false; + if (raw[0] == 0x0F) + result.has_motion = true; } - if (res->illuminance.has_value()) { - ESP_LOGD(TAG, " Illuminance: %.0flx", *res->illuminance); + // soil moisture, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x08) && (data_length == 1)) { + result.moisture = data[0]; } - if (res->moisture.has_value()) { - ESP_LOGD(TAG, " Moisture: %.0f%%", *res->moisture); + // conductivity, 2 bytes, 16-bit unsigned integer (LE), 1 µS/cm + else if ((raw[0] == 0x09) && (data_length == 2)) { + const uint16_t conductivity = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.conductivity = conductivity; + } + // battery, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x0A) && (data_length == 1)) { + result.battery_level = data[0]; + } + // temperature + humidity, 4 bytes, 16-bit signed integer (LE) each, 0.1 °C, 0.1 % + else if ((raw[0] == 0x0D) && (data_length == 4)) { + const int16_t temperature = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + const int16_t humidity = uint16_t(data[2]) | (uint16_t(data[3]) << 8); + result.temperature = temperature / 10.0f; + result.humidity = humidity / 10.0f; + } + // formaldehyde, 2 bytes, 16-bit unsigned integer (LE), 0.01 mg / m3 + else if ((raw[0] == 0x10) && (data_length == 2)) { + const uint16_t formaldehyde = uint16_t(data[0]) | (uint16_t(data[1]) << 8); + result.formaldehyde = formaldehyde / 100.0f; + } + // on/off state, 1 byte, 8-bit unsigned integer + else if ((raw[0] == 0x12) && (data_length == 1)) { + result.is_active = (data[0]) ? true : false; + } + // mosquito tablet, 1 byte, 8-bit unsigned integer, 1 % + else if ((raw[0] == 0x13) && (data_length == 1)) { + result.tablet = data[0]; + } + // idle time since last motion, 4 byte, 32-bit unsigned integer, 1 min + else if ((raw[0] == 0x17) && (data_length == 4)) { + const uint32_t idle_time = + uint32_t(data[0]) | (uint32_t(data[1]) << 8) | (uint32_t(data[2]) << 16) | (uint32_t(data[2]) << 24); + result.idle_time = idle_time / 60.0f; + result.has_motion = (idle_time) ? false : true; + } else { + return false; } return true; } +optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data) { + XiaomiParseResult result; + if (!service_data.uuid.contains(0x95, 0xFE)) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): no service data UUID magic bytes."); + return {}; + } + + auto raw = service_data.data; + result.has_data = (raw[0] & 0x40) ? true : false; + result.has_capability = (raw[0] & 0x20) ? true : false; + result.has_encryption = (raw[0] & 0x08) ? true : false; + + if (!result.has_data) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): service data has no DATA flag."); + return {}; + } + + static uint8_t last_frame_count = 0; + if (last_frame_count == raw[4]) { + ESP_LOGVV(TAG, "parse_xiaomi_header(): duplicate data packet received (%d).", static_cast(last_frame_count)); + result.is_duplicate = true; + return {}; + } + last_frame_count = raw[4]; + result.is_duplicate = false; + result.raw_offset = result.has_capability ? 12 : 11; + + if ((raw[2] == 0x98) && (raw[3] == 0x00)) { // MiFlora + result.type = XiaomiParseResult::TYPE_HHCCJCY01; + result.name = "HHCCJCY01"; + } else if ((raw[2] == 0xaa) && (raw[3] == 0x01)) { // round body, segment LCD + result.type = XiaomiParseResult::TYPE_LYWSDCGQ; + result.name = "LYWSDCGQ"; + } else if ((raw[2] == 0x5d) && (raw[3] == 0x01)) { // FlowerPot, RoPot + result.type = XiaomiParseResult::TYPE_HHCCPOT002; + result.name = "HHCCPOT002"; + } else if ((raw[2] == 0xdf) && (raw[3] == 0x02)) { // Xiaomi (Honeywell) formaldehyde sensor, OLED display + result.type = XiaomiParseResult::TYPE_JQJCY01YM; + result.name = "JQJCY01YM"; + } else if ((raw[2] == 0xdd) && (raw[3] == 0x03)) { // Philips/Xiaomi BLE nightlight + result.type = XiaomiParseResult::TYPE_MUE4094RT; + result.name = "MUE4094RT"; + result.raw_offset -= 6; + } else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // round body, e-ink display + result.type = XiaomiParseResult::TYPE_CGG1; + result.name = "CGG1"; + } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden + result.type = XiaomiParseResult::TYPE_GCLS002; + result.name = "GCLS002"; + } else if ((raw[2] == 0x5b) && (raw[3] == 0x04)) { // rectangular body, e-ink display + result.type = XiaomiParseResult::TYPE_LYWSD02; + result.name = "LYWSD02"; + } else if ((raw[2] == 0x0a) && (raw[3] == 0x04)) { // Mosquito Repellent Smart Version + result.type = XiaomiParseResult::TYPE_WX08ZM; + result.name = "WX08ZM"; + } else if ((raw[2] == 0x76) && (raw[3] == 0x05)) { // Cleargrass (Qingping) alarm clock, segment LCD + result.type = XiaomiParseResult::TYPE_CGD1; + result.name = "CGD1"; + } else if ((raw[2] == 0x5b) && (raw[3] == 0x05)) { // small square body, segment LCD, encrypted + result.type = XiaomiParseResult::TYPE_LYWSD03MMC; + result.name = "LYWSD03MMC"; + } else if ((raw[2] == 0xf6) && (raw[3] == 0x07)) { // Xiaomi-Yeelight BLE nightlight + result.type = XiaomiParseResult::TYPE_MJYD02YLA; + result.name = "MJYD02YLA"; + if (raw.size() == 19) + result.raw_offset -= 6; + } else { + ESP_LOGVV(TAG, "parse_xiaomi_header(): unknown device, no magic bytes."); + return {}; + } + + return result; +} + +bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address) { + if (!((raw.size() == 19) || ((raw.size() >= 22) && (raw.size() <= 24)))) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): data packet has wrong size (%d)!", raw.size()); + ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + return false; + } + + uint8_t mac_reverse[6] = {0}; + mac_reverse[5] = (uint8_t)(address >> 40); + mac_reverse[4] = (uint8_t)(address >> 32); + mac_reverse[3] = (uint8_t)(address >> 24); + mac_reverse[2] = (uint8_t)(address >> 16); + mac_reverse[1] = (uint8_t)(address >> 8); + mac_reverse[0] = (uint8_t)(address >> 0); + + XiaomiAESVector vector{.key = {0}, + .plaintext = {0}, + .ciphertext = {0}, + .authdata = {0x11}, + .iv = {0}, + .tag = {0}, + .keysize = 16, + .authsize = 1, + .datasize = 0, + .tagsize = 4, + .ivsize = 12}; + + vector.datasize = (raw.size() == 19) ? raw.size() - 12 : raw.size() - 18; + int cipher_pos = (raw.size() == 19) ? 5 : 11; + + const uint8_t *v = raw.data(); + + memcpy(vector.key, bindkey, vector.keysize); + memcpy(vector.ciphertext, v + cipher_pos, vector.datasize); + memcpy(vector.tag, v + raw.size() - vector.tagsize, vector.tagsize); + memcpy(vector.iv, mac_reverse, 6); // MAC address reverse + memcpy(vector.iv + 6, v + 2, 3); // sensor type (2) + packet id (1) + memcpy(vector.iv + 9, v + raw.size() - 7, 3); // payload counter + + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + + int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, vector.key, vector.keysize * 8); + if (ret) { + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): mbedtls_ccm_setkey() failed."); + mbedtls_ccm_free(&ctx); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx, vector.datasize, vector.iv, vector.ivsize, vector.authdata, vector.authsize, + vector.ciphertext, vector.plaintext, vector.tag, vector.tagsize); + if (ret) { + uint8_t mac_address[6] = {0}; + memcpy(mac_address, mac_reverse + 5, 1); + memcpy(mac_address + 1, mac_reverse + 4, 1); + memcpy(mac_address + 2, mac_reverse + 3, 1); + memcpy(mac_address + 3, mac_reverse + 2, 1); + memcpy(mac_address + 4, mac_reverse + 1, 1); + memcpy(mac_address + 5, mac_reverse, 1); + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption failed."); + ESP_LOGVV(TAG, " MAC address : %s", hexencode(mac_address, 6).c_str()); + ESP_LOGVV(TAG, " Packet : %s", hexencode(raw.data(), raw.size()).c_str()); + ESP_LOGVV(TAG, " Key : %s", hexencode(vector.key, vector.keysize).c_str()); + ESP_LOGVV(TAG, " Iv : %s", hexencode(vector.iv, vector.ivsize).c_str()); + ESP_LOGVV(TAG, " Cipher : %s", hexencode(vector.ciphertext, vector.datasize).c_str()); + ESP_LOGVV(TAG, " Tag : %s", hexencode(vector.tag, vector.tagsize).c_str()); + mbedtls_ccm_free(&ctx); + return false; + } + + // replace encrypted payload with plaintext + uint8_t *p = vector.plaintext; + for (std::vector::iterator it = raw.begin() + cipher_pos; it != raw.begin() + cipher_pos + vector.datasize; + ++it) { + *it = *(p++); + } + + // clear encrypted flag + raw[0] &= ~0x08; + + ESP_LOGVV(TAG, "decrypt_xiaomi_payload(): authenticated decryption passed."); + ESP_LOGVV(TAG, " Plaintext : %s, Packet : %d", hexencode(raw.data() + cipher_pos, vector.datasize).c_str(), + static_cast(raw[4])); + + mbedtls_ccm_free(&ctx); + return true; +} + +bool report_xiaomi_results(const optional &result, const std::string &address) { + if (!result.has_value()) { + ESP_LOGVV(TAG, "report_xiaomi_results(): no results available."); + return false; + } + + ESP_LOGD(TAG, "Got Xiaomi %s (%s):", result->name.c_str(), address.c_str()); + + if (result->temperature.has_value()) { + ESP_LOGD(TAG, " Temperature: %.1f°C", *result->temperature); + } + if (result->humidity.has_value()) { + ESP_LOGD(TAG, " Humidity: %.1f%%", *result->humidity); + } + if (result->battery_level.has_value()) { + ESP_LOGD(TAG, " Battery Level: %.0f%%", *result->battery_level); + } + if (result->conductivity.has_value()) { + ESP_LOGD(TAG, " Conductivity: %.0fµS/cm", *result->conductivity); + } + if (result->illuminance.has_value()) { + ESP_LOGD(TAG, " Illuminance: %.0flx", *result->illuminance); + } + if (result->moisture.has_value()) { + ESP_LOGD(TAG, " Moisture: %.0f%%", *result->moisture); + } + if (result->tablet.has_value()) { + ESP_LOGD(TAG, " Mosquito tablet: %.0f%%", *result->tablet); + } + if (result->is_active.has_value()) { + ESP_LOGD(TAG, " Repellent: %s", (*result->is_active) ? "on" : "off"); + } + if (result->has_motion.has_value()) { + ESP_LOGD(TAG, " Motion: %s", (*result->has_motion) ? "yes" : "no"); + } + if (result->is_light.has_value()) { + ESP_LOGD(TAG, " Light: %s", (*result->is_light) ? "on" : "off"); + } + + return true; +} + +bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + // Previously the message was parsed twice per packet, once by XiaomiListener::parse_device() + // and then again by the respective device class's parse_device() function. Parsing the header + // here and then for each device seems to be unneccessary and complicates the duplicate packet filtering. + // Hence I disabled the call to parse_xiaomi_header() here and the message parsing is done entirely + // in the respecive device instance. The XiaomiListener class is defined in __init__.py and I was not + // able to remove it entirely. + + return false; // with true it's not showing device scans +} + } // namespace xiaomi_ble } // namespace esphome diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 824ea80edf..daa71787a5 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -9,18 +9,58 @@ namespace esphome { namespace xiaomi_ble { struct XiaomiParseResult { - enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type; + enum { + TYPE_HHCCJCY01, + TYPE_GCLS002, + TYPE_HHCCPOT002, + TYPE_LYWSDCGQ, + TYPE_LYWSD02, + TYPE_CGG1, + TYPE_LYWSD03MMC, + TYPE_CGD1, + TYPE_JQJCY01YM, + TYPE_MUE4094RT, + TYPE_WX08ZM, + TYPE_MJYD02YLA + } type; + std::string name; optional temperature; optional humidity; - optional battery_level; + optional moisture; optional conductivity; optional illuminance; - optional moisture; + optional formaldehyde; + optional battery_level; + optional tablet; + optional idle_time; + optional is_active; + optional has_motion; + optional is_light; + bool has_data; // 0x40 + bool has_capability; // 0x20 + bool has_encryption; // 0x08 + bool is_duplicate; + int raw_offset; }; -bool parse_xiaomi_data_byte(uint8_t data_type, const uint8_t *data, uint8_t data_length, XiaomiParseResult &result); +struct XiaomiAESVector { + uint8_t key[16]; + uint8_t plaintext[16]; + uint8_t ciphertext[16]; + uint8_t authdata[16]; + uint8_t iv[16]; + uint8_t tag[16]; + size_t keysize; + size_t authsize; + size_t datasize; + size_t tagsize; + size_t ivsize; +}; -optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &device); +bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult &result); +optional parse_xiaomi_header(const esp32_ble_tracker::ServiceData &service_data); +bool decrypt_xiaomi_payload(std::vector &raw, const uint8_t *bindkey, const uint64_t &address); +bool report_xiaomi_results(const optional &result, const std::string &address); class XiaomiListener : public esp32_ble_tracker::ESPBTDeviceListener { public: diff --git a/esphome/components/xiaomi_cgd1/__init__.py b/esphome/components/xiaomi_cgd1/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py new file mode 100644 index 0000000000..343279f8fe --- /dev/null +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -0,0 +1,43 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_cgd1_ns = cg.esphome_ns.namespace('xiaomi_cgd1') +XiaomiCGD1 = xiaomi_cgd1_ns.class_('XiaomiCGD1', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiCGD1), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + + cg.add_library("mbedtls", "cdf462088d") diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp new file mode 100644 index 0000000000..d701e8ee6d --- /dev/null +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.cpp @@ -0,0 +1,77 @@ +#include "xiaomi_cgd1.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgd1 { + +static const char *TAG = "xiaomi_cgd1"; + +void XiaomiCGD1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGD1"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiCGD1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiCGD1::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_cgd1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h new file mode 100644 index 0000000000..b9e05f857c --- /dev/null +++ b/esphome/components/xiaomi_cgd1/xiaomi_cgd1.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cgd1 { + +class XiaomiCGD1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_cgd1 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 6cc14f5a8e..a7c94fafad 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -15,6 +15,48 @@ void XiaomiCGG1::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } +bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_cgg1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 7f73011275..57f883405c 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -14,22 +14,7 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_gcls002/__init__.py b/esphome/components/xiaomi_gcls002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_gcls002/sensor.py b/esphome/components/xiaomi_gcls002/sensor.py new file mode 100644 index 0000000000..1822977c38 --- /dev/null +++ b/esphome/components/xiaomi_gcls002/sensor.py @@ -0,0 +1,45 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_gcls002_ns = cg.esphome_ns.namespace('xiaomi_gcls002') +XiaomiGCLS002 = xiaomi_gcls002_ns.class_('XiaomiGCLS002', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiGCLS002), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp new file mode 100644 index 0000000000..24156f98ac --- /dev/null +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.cpp @@ -0,0 +1,66 @@ +#include "xiaomi_gcls002.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_gcls002 { + +static const char *TAG = "xiaomi_gcls002"; + +void XiaomiGCLS002::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi GCLS002"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); + LOG_SENSOR(" ", "Illuminance", this->illuminance_); +} + +bool XiaomiGCLS002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_gcls002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h new file mode 100644 index 0000000000..d800e2837d --- /dev/null +++ b/esphome/components/xiaomi_gcls002/xiaomi_gcls002.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_gcls002 { + +class XiaomiGCLS002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; + sensor::Sensor *illuminance_{nullptr}; +}; + +} // namespace xiaomi_gcls002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py index 495446ba11..0b0349d7e4 100644 --- a/esphome/components/xiaomi_hhccjcy01/sensor.py +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -1,8 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ +from esphome.const import CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER @@ -21,7 +21,6 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), cv.Optional(CONF_CONDUCTIVITY): sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) @@ -44,6 +43,3 @@ def to_code(config): if CONF_CONDUCTIVITY in config: sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) cg.add(var.set_conductivity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 8c8152c54c..fd099f7aa5 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -14,7 +14,50 @@ void XiaomiHHCCJCY01::dump_config() { LOG_SENSOR(" ", "Moisture", this->moisture_); LOG_SENSOR(" ", "Conductivity", this->conductivity_); LOG_SENSOR(" ", "Illuminance", this->illuminance_); - LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiHHCCJCY01::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + if (res->illuminance.has_value() && this->illuminance_ != nullptr) + this->illuminance_->publish_state(*res->illuminance); + success = true; + } + + if (!success) { + return false; + } + + return true; } } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index c1b8511bb8..e72bf98161 100644 --- a/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -14,26 +14,7 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->moisture.has_value() && this->moisture_ != nullptr) - this->moisture_->publish_state(*res->moisture); - if (res->conductivity.has_value() && this->conductivity_ != nullptr) - this->conductivity_->publish_state(*res->conductivity); - if (res->illuminance.has_value() && this->illuminance_ != nullptr) - this->illuminance_->publish_state(*res->illuminance); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } @@ -41,7 +22,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } - void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } protected: uint64_t address_; @@ -49,7 +29,6 @@ class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceL sensor::Sensor *moisture_{nullptr}; sensor::Sensor *conductivity_{nullptr}; sensor::Sensor *illuminance_{nullptr}; - sensor::Sensor *battery_level_{nullptr}; }; } // namespace xiaomi_hhccjcy01 diff --git a/esphome/components/xiaomi_hhccpot002/__init__.py b/esphome/components/xiaomi_hhccpot002/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_hhccpot002/sensor.py b/esphome/components/xiaomi_hhccpot002/sensor.py new file mode 100644 index 0000000000..33cd96252e --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/sensor.py @@ -0,0 +1,35 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID, \ + CONF_MOISTURE, CONF_CONDUCTIVITY, UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_hhccpot002_ns = cg.esphome_ns.namespace('xiaomi_hhccpot002') +XiaomiHHCCPOT002 = xiaomi_hhccpot002_ns.class_('XiaomiHHCCPOT002', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiHHCCPOT002), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp new file mode 100644 index 0000000000..2b5ad3a826 --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.cpp @@ -0,0 +1,60 @@ +#include "xiaomi_hhccpot002.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_hhccpot002 { + +static const char *TAG = "xiaomi_hhccpot002"; + +void XiaomiHHCCPOT002 ::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi HHCCPOT002"); + LOG_SENSOR(" ", "Moisture", this->moisture_); + LOG_SENSOR(" ", "Conductivity", this->conductivity_); +} + +bool XiaomiHHCCPOT002::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->moisture.has_value() && this->moisture_ != nullptr) + this->moisture_->publish_state(*res->moisture); + if (res->conductivity.has_value() && this->conductivity_ != nullptr) + this->conductivity_->publish_state(*res->conductivity); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_hhccpot002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h new file mode 100644 index 0000000000..1add8e27b1 --- /dev/null +++ b/esphome/components/xiaomi_hhccpot002/xiaomi_hhccpot002.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_hhccpot002 { + +class XiaomiHHCCPOT002 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_moisture(sensor::Sensor *moisture) { moisture_ = moisture; } + void set_conductivity(sensor::Sensor *conductivity) { conductivity_ = conductivity; } + + protected: + uint64_t address_; + sensor::Sensor *moisture_{nullptr}; + sensor::Sensor *conductivity_{nullptr}; +}; + +} // namespace xiaomi_hhccpot002 +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_jqjcy01ym/__init__.py b/esphome/components/xiaomi_jqjcy01ym/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_jqjcy01ym/sensor.py b/esphome/components/xiaomi_jqjcy01ym/sensor.py new file mode 100644 index 0000000000..2bd397e829 --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/sensor.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_HUMIDITY, UNIT_MILLIGRAMS_PER_CUBIC_METER, ICON_FLASK_OUTLINE, CONF_FORMALDEHYDE + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_jqjcy01ym_ns = cg.esphome_ns.namespace('xiaomi_jqjcy01ym') +XiaomiJQJCY01YM = xiaomi_jqjcy01ym_ns.class_('XiaomiJQJCY01YM', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiJQJCY01YM), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_FORMALDEHYDE): + sensor.sensor_schema(UNIT_MILLIGRAMS_PER_CUBIC_METER, ICON_FLASK_OUTLINE, 2), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_FORMALDEHYDE in config: + sens = yield sensor.new_sensor(config[CONF_FORMALDEHYDE]) + cg.add(var.set_formaldehyde(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp new file mode 100644 index 0000000000..3e7090509b --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.cpp @@ -0,0 +1,66 @@ +#include "xiaomi_jqjcy01ym.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_jqjcy01ym { + +static const char *TAG = "xiaomi_jqjcy01ym"; + +void XiaomiJQJCY01YM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi JQJCY01YM"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiJQJCY01YM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->formaldehyde.has_value() && this->formaldehyde_ != nullptr) + this->formaldehyde_->publish_state(*res->formaldehyde); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_jqjcy01ym +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h new file mode 100644 index 0000000000..d750e1e97f --- /dev/null +++ b/esphome/components/xiaomi_jqjcy01ym/xiaomi_jqjcy01ym.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_jqjcy01ym { + +class XiaomiJQJCY01YM : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_formaldehyde(sensor::Sensor *formaldehyde) { formaldehyde_ = formaldehyde; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *formaldehyde_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_jqjcy01ym +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp index cd77c133a5..5ecd99047e 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp @@ -14,6 +14,46 @@ void XiaomiLYWSD02::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_); } +bool XiaomiLYWSD02::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_lywsd02 } // namespace esphome diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h index 9b8aba1bb0..f32506eb44 100644 --- a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h +++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h @@ -14,20 +14,7 @@ class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceLis public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_lywsd03mmc/__init__.py b/esphome/components/xiaomi_lywsd03mmc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py new file mode 100644 index 0000000000..71f7b20752 --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_BINDKEY + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_lywsd03mmc_ns = cg.esphome_ns.namespace('xiaomi_lywsd03mmc') +XiaomiLYWSD03MMC = xiaomi_lywsd03mmc_ns.class_('XiaomiLYWSD03MMC', + esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiLYWSD03MMC), + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + + cg.add_library("mbedtls", "cdf462088d") diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp new file mode 100644 index 0000000000..e9cc99358b --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.cpp @@ -0,0 +1,81 @@ +#include "xiaomi_lywsd03mmc.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd03mmc { + +static const char *TAG = "xiaomi_lywsd03mmc"; + +void XiaomiLYWSD03MMC::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSD03MMC"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiLYWSD03MMC::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (res->humidity.has_value() && this->humidity_ != nullptr) { + // see https://github.com/custom-components/sensor.mitemp_bt/issues/7#issuecomment-595948254 + *res->humidity = trunc(*res->humidity); + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiLYWSD03MMC::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_lywsd03mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h new file mode 100644 index 0000000000..c2828e3cd1 --- /dev/null +++ b/esphome/components/xiaomi_lywsd03mmc/xiaomi_lywsd03mmc.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_lywsd03mmc { + +class XiaomiLYWSD03MMC : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_lywsd03mmc +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index 2dacff2876..035ac8c906 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -15,6 +15,48 @@ void XiaomiLYWSDCGQ::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } +bool XiaomiLYWSDCGQ::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + } // namespace xiaomi_lywsdcgq } // namespace esphome diff --git a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index b6756eec61..553b5965fd 100644 --- a/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -14,22 +14,7 @@ class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceLi public: void set_address(uint64_t address) { address_ = address; } - bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { - if (device.address_uint64() != this->address_) - return false; - - auto res = xiaomi_ble::parse_xiaomi(device); - if (!res.has_value()) - return false; - - if (res->temperature.has_value() && this->temperature_ != nullptr) - this->temperature_->publish_state(*res->temperature); - if (res->humidity.has_value() && this->humidity_ != nullptr) - this->humidity_->publish_state(*res->humidity); - if (res->battery_level.has_value() && this->battery_level_ != nullptr) - this->battery_level_->publish_state(*res->battery_level); - return true; - } + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; void dump_config() override; float get_setup_priority() const override { return setup_priority::DATA; } diff --git a/esphome/components/xiaomi_mjyd02yla/__init__.py b/esphome/components/xiaomi_mjyd02yla/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py new file mode 100644 index 0000000000..e34864c480 --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_ID, CONF_BINDKEY, \ + CONF_DEVICE_CLASS, CONF_LIGHT, CONF_BATTERY_LEVEL, UNIT_PERCENT, ICON_BATTERY, \ + CONF_IDLE_TIME, UNIT_MINUTE, ICON_TIMELAPSE + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mjyd02yla_ns = cg.esphome_ns.namespace('xiaomi_mjyd02yla') +XiaomiMJYD02YLA = xiaomi_mjyd02yla_ns.class_('XiaomiMJYD02YLA', binary_sensor.BinarySensor, + cg.Component, esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiMJYD02YLA), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Required(CONF_BINDKEY): cv.bind_key, + cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, + cv.Optional(CONF_IDLE_TIME): sensor.sensor_schema(UNIT_MINUTE, ICON_TIMELAPSE, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), + cv.Optional(CONF_LIGHT): binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.Optional(CONF_DEVICE_CLASS, default='light'): binary_sensor.device_class, + }), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_bindkey(config[CONF_BINDKEY])) + + if CONF_IDLE_TIME in config: + sens = yield sensor.new_sensor(config[CONF_IDLE_TIME]) + cg.add(var.set_idle_time(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) + if CONF_LIGHT in config: + sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT]) + cg.add(var.set_light(sens)) + + cg.add_library("mbedtls", "cdf462088d") diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp new file mode 100644 index 0000000000..aaea3606ba --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.cpp @@ -0,0 +1,79 @@ +#include "xiaomi_mjyd02yla.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mjyd02yla { + +static const char *TAG = "xiaomi_mjyd02yla"; + +void XiaomiMJYD02YLA::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MJYD02YL-A"); + LOG_BINARY_SENSOR(" ", "Motion", this); + LOG_BINARY_SENSOR(" ", "Light", this->is_light_); + LOG_SENSOR(" ", "Idle Time", this->idle_time_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiMJYD02YLA::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->idle_time.has_value() && this->idle_time_ != nullptr) + this->idle_time_->publish_state(*res->idle_time); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + if (res->is_light.has_value() && this->is_light_ != nullptr) + this->is_light_->publish_state(*res->is_light); + if (res->has_motion.has_value()) + this->publish_state(*res->has_motion); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +void XiaomiMJYD02YLA::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + +} // namespace xiaomi_mjyd02yla +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h new file mode 100644 index 0000000000..d3fde4d6f8 --- /dev/null +++ b/esphome/components/xiaomi_mjyd02yla/xiaomi_mjyd02yla.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mjyd02yla { + +class XiaomiMJYD02YLA : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_idle_time(sensor::Sensor *idle_time) { idle_time_ = idle_time; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + void set_light(binary_sensor::BinarySensor *light) { is_light_ = light; } + + protected: + uint64_t address_; + uint8_t bindkey_[16]; + sensor::Sensor *idle_time_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; + binary_sensor::BinarySensor *is_light_{nullptr}; +}; + +} // namespace xiaomi_mjyd02yla +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mue4094rt/__init__.py b/esphome/components/xiaomi_mue4094rt/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_mue4094rt/binary_sensor.py b/esphome/components/xiaomi_mue4094rt/binary_sensor.py new file mode 100644 index 0000000000..946b1694c4 --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/binary_sensor.py @@ -0,0 +1,29 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor, esp32_ble_tracker +from esphome.const import CONF_MAC_ADDRESS, CONF_DEVICE_CLASS, CONF_TIMEOUT, CONF_ID + + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_mue4094rt_ns = cg.esphome_ns.namespace('xiaomi_mue4094rt') +XiaomiMUE4094RT = xiaomi_mue4094rt_ns.class_('XiaomiMUE4094RT', binary_sensor.BinarySensor, + cg.Component, esp32_ble_tracker.ESPBTDeviceListener) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiMUE4094RT), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_DEVICE_CLASS, default='motion'): binary_sensor.device_class, + cv.Optional(CONF_TIMEOUT, default='5s'): cv.positive_time_period_milliseconds, +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + cg.add(var.set_time(config[CONF_TIMEOUT])) diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp new file mode 100644 index 0000000000..45337f330e --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.cpp @@ -0,0 +1,59 @@ +#include "xiaomi_mue4094rt.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mue4094rt { + +static const char *TAG = "xiaomi_mue4094rt"; + +void XiaomiMUE4094RT::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi MUE4094RT"); + LOG_BINARY_SENSOR(" ", "Motion", this); +} + +bool XiaomiMUE4094RT::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->has_motion.has_value()) { + this->publish_state(*res->has_motion); + this->set_timeout("motion_timeout", timeout_, [this]() { this->publish_state(false); }); + } + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_mue4094rt +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h new file mode 100644 index 0000000000..31f913ec94 --- /dev/null +++ b/esphome/components/xiaomi_mue4094rt/xiaomi_mue4094rt.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_mue4094rt { + +class XiaomiMUE4094RT : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_time(uint16_t timeout) { timeout_ = timeout; } + + protected: + uint64_t address_; + uint16_t timeout_; +}; + +} // namespace xiaomi_mue4094rt +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_wx08zm/__init__.py b/esphome/components/xiaomi_wx08zm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_wx08zm/binary_sensor.py b/esphome/components/xiaomi_wx08zm/binary_sensor.py new file mode 100644 index 0000000000..1d60dbf5e0 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/binary_sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, binary_sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TABLET, \ + UNIT_PERCENT, ICON_BUG, ICON_BATTERY, CONF_ID + + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_wx08zm_ns = cg.esphome_ns.namespace('xiaomi_wx08zm') +XiaomiWX08ZM = xiaomi_wx08zm_ns.class_('XiaomiWX08ZM', binary_sensor.BinarySensor, + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.All(binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(XiaomiWX08ZM), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TABLET): sensor.sensor_schema(UNIT_PERCENT, ICON_BUG, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TABLET in config: + sens = yield sensor.new_sensor(config[CONF_TABLET]) + cg.add(var.set_tablet(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp new file mode 100644 index 0000000000..6ecdf8f4f4 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.cpp @@ -0,0 +1,64 @@ +#include "xiaomi_wx08zm.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_wx08zm { + +static const char *TAG = "xiaomi_wx08zm"; + +void XiaomiWX08ZM::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi WX08ZM"); + LOG_BINARY_SENSOR(" ", "Mosquito Repellent", this); + LOG_SENSOR(" ", "Tablet Resource", this->tablet_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +bool XiaomiWX08ZM::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { + if (device.address_uint64() != this->address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + + bool success = false; + for (auto &service_data : device.get_service_datas()) { + auto res = xiaomi_ble::parse_xiaomi_header(service_data); + if (!res.has_value()) { + continue; + } + if (res->is_duplicate) { + continue; + } + if (res->has_encryption) { + ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + continue; + } + if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { + continue; + } + if (!(xiaomi_ble::report_xiaomi_results(res, device.address_str()))) { + continue; + } + if (res->is_active.has_value()) { + this->publish_state(*res->is_active); + } + if (res->tablet.has_value() && this->tablet_ != nullptr) + this->tablet_->publish_state(*res->tablet); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + success = true; + } + + if (!success) { + return false; + } + + return true; +} + +} // namespace xiaomi_wx08zm +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h new file mode 100644 index 0000000000..f3eba0e159 --- /dev/null +++ b/esphome/components/xiaomi_wx08zm/xiaomi_wx08zm.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_wx08zm { + +class XiaomiWX08ZM : public Component, + public binary_sensor::BinarySensorInitiallyOff, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_tablet(sensor::Sensor *tablet) { tablet_ = tablet; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *tablet_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_wx08zm +} // namespace esphome + +#endif diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 55199e6647..fa2a14dbd6 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -566,6 +566,23 @@ def mac_address(value): return core.MACAddress(*parts_int) +def bind_key(value): + value = string_strict(value) + parts = [value[i:i+2] for i in range(0, len(value), 2)] + if len(parts) != 16: + raise Invalid("Bind key must consist of 16 hexadecimal numbers") + parts_int = [] + if any(len(part) != 2 for part in parts): + raise Invalid("Bind key must be format XX") + for part in parts: + try: + parts_int.append(int(part, 16)) + except ValueError: + raise Invalid("Bind key must be hex values from 00 to FF") + + return ''.join(f'{part:02X}' for part in parts_int) + + def uuid(value): return Coerce(uuid_.UUID)(value) diff --git a/esphome/const.py b/esphome/const.py index 31359e610d..b244d5681a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -55,6 +55,7 @@ CONF_BELOW = 'below' CONF_BINARY = 'binary' CONF_BINARY_SENSOR = 'binary_sensor' CONF_BINARY_SENSORS = 'binary_sensors' +CONF_BINDKEY = 'bindkey' CONF_BIRTH_MESSAGE = 'birth_message' CONF_BIT_DEPTH = 'bit_depth' CONF_BLUE = 'blue' @@ -194,6 +195,7 @@ CONF_ID = 'id' CONF_IDLE = 'idle' CONF_IDLE_ACTION = 'idle_action' CONF_IDLE_LEVEL = 'idle_level' +CONF_IDLE_TIME = 'idle_time' CONF_IF = 'if' CONF_IIR_FILTER = 'iir_filter' CONF_ILLUMINANCE = 'illuminance' @@ -265,6 +267,7 @@ CONF_MODEL = 'model' CONF_MOISTURE = 'moisture' CONF_MONTHS = 'months' CONF_MOSI_PIN = 'mosi_pin' +CONF_MOTION = 'motion' CONF_MOVEMENT_COUNTER = 'movement_counter' CONF_MQTT = 'mqtt' CONF_MQTT_ID = 'mqtt_id' @@ -445,6 +448,7 @@ CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SWING_MODE = 'swing_mode' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' +CONF_TABLET = 'tablet' CONF_TAG = 'tag' CONF_TARGET = 'target' CONF_TARGET_TEMPERATURE = 'target_temperature' @@ -520,17 +524,20 @@ ICON_ARROW_EXPAND_VERTICAL = 'mdi:arrow-expand-vertical' ICON_BATTERY = 'mdi:battery' ICON_BRIEFCASE_DOWNLOAD = 'mdi:briefcase-download' ICON_BRIGHTNESS_5 = 'mdi:brightness-5' +ICON_BUG = 'mdi:bug' ICON_CHECK_CIRCLE_OUTLINE = 'mdi:check-circle-outline' ICON_CHEMICAL_WEAPON = 'mdi:chemical-weapon' ICON_COUNTER = 'mdi:counter' ICON_CURRENT_AC = 'mdi:current-ac' ICON_EMPTY = '' ICON_FLASH = 'mdi:flash' +ICON_FLASK_OUTLINE = 'mdi:flask-outline' ICON_FLOWER = 'mdi:flower' ICON_GAS_CYLINDER = 'mdi:gas-cylinder' ICON_GAUGE = 'mdi:gauge' ICON_LIGHTBULB = 'mdi:lightbulb' ICON_MAGNET = 'mdi:magnet' +ICON_MOTION_SENSOR = 'mdi:motion-sensor' ICON_NEW_BOX = 'mdi:new-box' ICON_PERCENT = 'mdi:percent' ICON_PERIODIC_TABLE_CO2 = 'mdi:periodic-table-co2' @@ -546,6 +553,7 @@ ICON_SIGN_DIRECTION = 'mdi:sign-direction' ICON_SIGNAL = 'mdi:signal-distance-variant' ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' ICON_THERMOMETER = 'mdi:thermometer' +ICON_TIMELAPSE = 'mdi:timelapse' ICON_TIMER = 'mdi:timer' ICON_WATER_PERCENT = 'mdi:water-percent' ICON_WEATHER_SUNSET = 'mdi:weather-sunset' @@ -575,6 +583,8 @@ UNIT_MICROGRAMS_PER_CUBIC_METER = 'µg/m³' UNIT_MICROMETER = 'µm' UNIT_MICROSIEMENS_PER_CENTIMETER = 'µS/cm' UNIT_MICROTESLA = 'µT' +UNIT_MILLIGRAMS_PER_CUBIC_METER = 'mg/m³' +UNIT_MINUTE = 'min' UNIT_OHM = 'Ω' UNIT_PARTS_PER_BILLION = 'ppb' UNIT_PARTS_PER_MILLION = 'ppm' diff --git a/tests/test2.yaml b/tests/test2.yaml index 6bc0ac93c3..34b7ad6b07 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -44,16 +44,14 @@ ota: logger: level: DEBUG -web_server: - auth: - username: admin - password: admin - as3935_i2c: irq_pin: GPIO12 sensor: + - platform: homeassistant + entity_id: sensor.hello_world + id: ha_hello_world - platform: ble_rssi mac_address: AC:37:43:77:5F:4C name: "BLE Google Home Mini RSSI value" @@ -66,40 +64,6 @@ sensor: - platform: ble_rssi service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' name: "BLE Test Service 128" - - platform: xiaomi_hhccjcy01 - mac_address: 94:2B:FF:5C:91:61 - temperature: - name: "Xiaomi HHCCJCY01 Temperature" - moisture: - name: "Xiaomi HHCCJCY01 Moisture" - illuminance: - name: "Xiaomi HHCCJCY01 Illuminance" - conductivity: - name: "Xiaomi HHCCJCY01 Soil Conductivity" - battery_level: - name: "Xiaomi HHCCJCY01 Battery Level" - - platform: xiaomi_lywsdcgq - mac_address: 7A:80:8E:19:36:BA - temperature: - name: "Xiaomi LYWSDCGQ Temperature" - humidity: - name: "Xiaomi LYWSDCGQ Humidity" - battery_level: - name: "Xiaomi LYWSDCGQ Battery Level" - - platform: xiaomi_lywsd02 - mac_address: 3F:5B:7D:82:58:4E - temperature: - name: "Xiaomi LYWSD02 Temperature" - humidity: - name: "Xiaomi LYWSD02 Humidity" - - platform: xiaomi_cgg1 - mac_address: 7A:80:8E:19:36:BA - temperature: - name: "Xiaomi CGG1 Temperature" - humidity: - name: "Xiaomi CGG1 Humidity" - battery_level: - name: "Xiaomi CGG1 Battery Level" - platform: ruuvitag mac_address: FF:56:D3:2F:7D:E8 humidity: @@ -122,14 +86,88 @@ sensor: name: "RuuviTag Movement Counter" measurement_sequence_number: name: "RuuviTag Measurement Sequence Number" - - platform: homeassistant - entity_id: sensor.hello_world - id: ha_hello_world - platform: as3935 lightning_energy: name: "Lightning Energy" distance: name: "Distance Storm" + - platform: xiaomi_hhccjcy01 + mac_address: 94:2B:FF:5C:91:61 + temperature: + name: "Xiaomi HHCCJCY01 Temperature" + moisture: + name: "Xiaomi HHCCJCY01 Moisture" + illuminance: + name: "Xiaomi HHCCJCY01 Illuminance" + conductivity: + name: "Xiaomi HHCCJCY01 Soil Conductivity" + - platform: xiaomi_lywsdcgq + mac_address: 7A:80:8E:19:36:BA + temperature: + name: "Xiaomi LYWSDCGQ Temperature" + humidity: + name: "Xiaomi LYWSDCGQ Humidity" + battery_level: + name: "Xiaomi LYWSDCGQ Battery Level" + - platform: xiaomi_lywsd02 + mac_address: 3F:5B:7D:82:58:4E + temperature: + name: "Xiaomi LYWSD02 Temperature" + humidity: + name: "Xiaomi LYWSD02 Humidity" + - platform: xiaomi_cgg1 + mac_address: 7A:80:8E:19:36:BA + temperature: + name: "Xiaomi CGG1 Temperature" + humidity: + name: "Xiaomi CGG1 Humidity" + battery_level: + name: "Xiaomi CGG1 Battery Level" + - platform: xiaomi_gcls002 + mac_address: "94:2B:FF:5C:91:61" + temperature: + name: "GCLS02 Temperature" + moisture: + name: "GCLS02 Moisture" + conductivity: + name: "GCLS02 Soil Conductivity" + illuminance: + name: "GCLS02 Illuminance" + - platform: xiaomi_hhccpot002 + mac_address: "94:2B:FF:5C:91:61" + moisture: + name: "HHCCPOT002 Moisture" + conductivity: + name: "HHCCPOT002 Soil Conductivity" + - platform: xiaomi_lywsd03mmc + mac_address: "A4:C1:38:4E:16:78" + bindkey: "e9efaa6873f9f9c87a5e75a5f814801c" + temperature: + name: "Xiaomi LYWSD03MMC Temperature" + humidity: + name: "Xiaomi LYWSD03MMC Humidity" + battery_level: + name: "Xiaomi LYWSD03MMC Battery Level" + - platform: xiaomi_cgd1 + mac_address: "A4:C1:38:D1:61:7D" + bindkey: "c99d2313182473b38001086febf781bd" + temperature: + name: "Xiaomi CGD1 Temperature" + humidity: + name: "Xiaomi CGD1 Humidity" + battery_level: + name: "Xiaomi CGD1 Battery Level" + - platform: xiaomi_jqjcy01ym + mac_address: "7A:80:8E:19:36:BA" + temperature: + name: "JQJCY01YM Temperature" + humidity: + name: "JQJCY01YM Humidity" + formaldehyde: + name: "JQJCY01YM Formaldehyde" + battery_level: + name: "JQJCY01YM Battery Level" + time: - platform: homeassistant @@ -142,6 +180,9 @@ esp32_touch: setup_mode: True binary_sensor: + - platform: homeassistant + entity_id: binary_sensor.hello_world + id: ha_hello_world_binary - platform: ble_presence mac_address: AC:37:43:77:5F:4C name: "ESP32 BLE Tracker Google Home Mini" @@ -158,11 +199,30 @@ binary_sensor: name: "ESP32 Touch Pad GPIO27" pin: GPIO27 threshold: 1000 - - platform: homeassistant - entity_id: binary_sensor.hello_world - id: ha_hello_world_binary - platform: as3935 name: "Storm Alert" + - platform: xiaomi_mue4094rt + name: "MUE4094RT Motion" + mac_address: "7A:80:8E:19:36:BA" + timeout: "5s" + - platform: xiaomi_mjyd02yla + name: "MJYD02YL-A Motion" + mac_address: "50:EC:50:CD:32:02" + bindkey: "48403ebe2d385db8d0c187f81e62cb64" + idle_time: + name: "MJYD02YL-A Idle Time" + light: + name: "MJYD02YL-A Light Status" + battery_level: + name: "MJYD02YL-A Battery Level" + - platform: xiaomi_wx08zm + name: "WX08ZM Activation State" + mac_address: "74:a3:4a:b5:07:34" + tablet: + name: "WX08ZM Tablet Resource" + battery_level: + name: "WX08ZM Battery Level" + esp32_ble_tracker: on_ble_advertise: @@ -257,3 +317,4 @@ interval: display: + diff --git a/tests/test4.yaml b/tests/test4.yaml new file mode 100644 index 0000000000..c7aa016839 --- /dev/null +++ b/tests/test4.yaml @@ -0,0 +1,73 @@ +esphome: + name: $devicename + platform: ESP32 + board: nodemcu-32s + build_path: build/test4 + +substitutions: + devicename: test4 + +ethernet: + type: LAN8720 + mdc_pin: GPIO23 + mdio_pin: GPIO25 + clk_mode: GPIO0_IN + phy_addr: 0 + power_pin: GPIO25 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + +api: + +i2c: + sda: 21 + scl: 22 + scan: False + +spi: + clk_pin: GPIO21 + mosi_pin: GPIO22 + miso_pin: GPIO23 + +uart: + tx_pin: GPIO22 + rx_pin: GPIO23 + baud_rate: 115200 + +ota: + safe_mode: True + port: 3286 + +logger: + level: DEBUG + +web_server: + auth: + username: admin + password: admin + +sensor: + - platform: homeassistant + entity_id: sensor.hello_world + id: ha_hello_world +# +# platform sensor.apds9960 requires component apds9960 +# +# - platform: apds9960 +# type: proximity +# name: APDS9960 Proximity +# - platform: apds9960 +# type: clear +# name: APDS9960 Clear +# - platform: apds9960 +# type: red +# name: APDS9960 Red +# - platform: apds9960 +# type: green +# name: APDS9960 Green +# - platform: apds9960 +# type: blue +# name: APDS9960 Blue From fed37b48a107dbf01b4cf53b3906b8a3a5e64c46 Mon Sep 17 00:00:00 2001 From: gitolicious <26963495+gitolicious@users.noreply.github.com> Date: Wed, 27 May 2020 00:47:35 +0200 Subject: [PATCH 006/200] Explicitly set language to English (#1073) --- esphome/components/web_server/web_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 1f6cd10666..48a47080b2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -139,7 +139,7 @@ float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); std::string title = App.get_name() + " Web Server"; - stream->print(F("")); + stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><title>")); stream->print(title.c_str()); stream->print(F("")); #ifdef WEBSERVER_CSS_INCLUDE From ecd65003d436eb73d729a108846283051d1e5b8f Mon Sep 17 00:00:00 2001 From: igg Date: Thu, 28 May 2020 15:11:25 -0700 Subject: [PATCH 007/200] Added support for ssd1327 (#985) Co-authored-by: Ilya Goldberg --- esphome/components/ssd1325_base/__init__.py | 1 + .../components/ssd1325_base/ssd1325_base.cpp | 27 +++++++++++++++---- .../components/ssd1325_base/ssd1325_base.h | 1 + 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 69e11ec0d1..011642c408 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -14,6 +14,7 @@ MODELS = { 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, + 'SSD1327_128X128': SSD1325Model.SSD1327_MODEL_128_128, } SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 3079e19cc8..22dc51e790 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -48,15 +48,24 @@ void SSD1325::setup() { this->command(SSD1325_SETCLOCK); /* set osc division */ this->command(0xF1); /* 145 */ this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ - this->command(0x3f); /* duty = 1/64 */ - this->command(SSD1325_SETOFFSET); /* set display offset --- */ - this->command(0x4C); /* 76 */ + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x7f); // duty = height - 1 + else + this->command(0x3f); // duty = 1/64 + this->command(SSD1325_SETOFFSET); /* set display offset --- */ + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x00); // 0 + else + this->command(0x4C); // 76 this->command(SSD1325_SETSTARTLINE); /*set start line */ this->command(0x00); /* ------ */ this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ this->command(0x02); this->command(SSD1325_SETREMAP); /* set segment remap------ */ - this->command(0x56); + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x55); // 0x56 is flipped horizontally: enable column swap, disable nibble remap + else + this->command(0x56); this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ this->command(SSD1325_SETGRAYTABLE); this->command(0x01); @@ -90,7 +99,10 @@ void SSD1325::display() { this->command(0x3F); /* set column end address */ this->command(SSD1325_SETROWADDR); /* set row address */ this->command(0x00); /* set row start address */ - this->command(0x3F); /* set row end address */ + if (this->model_ == SSD1327_MODEL_128_128) + this->command(0x7F); // 127 is last row + else + this->command(0x3F); // 63 is last row this->write_display_data(); } @@ -108,6 +120,8 @@ int SSD1325::get_height_internal() { return 16; case SSD1325_MODEL_64_48: return 48; + case SSD1327_MODEL_128_128: + return 128; default: return 0; } @@ -116,6 +130,7 @@ int SSD1325::get_width_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: case SSD1325_MODEL_128_64: + case SSD1327_MODEL_128_128: return 128; case SSD1325_MODEL_96_16: return 96; @@ -168,6 +183,8 @@ const char *SSD1325::model_str_() { return "SSD1325 96x16"; case SSD1325_MODEL_64_48: return "SSD1325 64x48"; + case SSD1327_MODEL_128_128: + return "SSD1327 128x128"; default: return "Unknown"; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index e227f68f86..e796b85a33 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -12,6 +12,7 @@ enum SSD1325Model { SSD1325_MODEL_128_64, SSD1325_MODEL_96_16, SSD1325_MODEL_64_48, + SSD1327_MODEL_128_128, }; class SSD1325 : public PollingComponent, public display::DisplayBuffer { From 7e4d12f88011a5f7c44d00b33a95a1d750eea67c Mon Sep 17 00:00:00 2001 From: Adrian Cuzman Date: Fri, 29 May 2020 01:54:00 +0300 Subject: [PATCH 008/200] added energy reading for pzem004 (#1022) --- esphome/components/pzem004t/pzem004t.cpp | 10 +++++++++- esphome/components/pzem004t/pzem004t.h | 3 +++ esphome/components/pzem004t/sensor.py | 8 +++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp index e2d832b019..b9deab1949 100644 --- a/esphome/components/pzem004t/pzem004t.cpp +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -59,11 +59,19 @@ void PZEM004T::loop() { if (this->power_sensor_ != nullptr) this->power_sensor_->publish_state(power); ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(READ_ENERGY); + break; + } + + case 0xA3: { // Energy Response + uint32_t energy = (uint32_t(resp[1]) << 16) | (uint32_t(resp[2]) << 8) | (uint32_t(resp[3])); + if (this->energy_sensor_ != nullptr) + this->energy_sensor_->publish_state(energy); + ESP_LOGD(TAG, "Got Energy %u Wh", energy); this->write_state_(DONE); break; } - case 0xA3: // Energy Response case 0xA5: // Set Power Alarm Response case 0xB0: // Voltage Request case 0xB1: // Current Request diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h index f0208d415a..517b81eb21 100644 --- a/esphome/components/pzem004t/pzem004t.h +++ b/esphome/components/pzem004t/pzem004t.h @@ -12,6 +12,7 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } void loop() override; @@ -23,12 +24,14 @@ class PZEM004T : public PollingComponent, public uart::UARTDevice { sensor::Sensor *voltage_sensor_; sensor::Sensor *current_sensor_; sensor::Sensor *power_sensor_; + sensor::Sensor *energy_sensor_; enum PZEM004TReadState { SET_ADDRESS = 0xB4, READ_VOLTAGE = 0xB0, READ_CURRENT = 0xB1, READ_POWER = 0xB2, + READ_ENERGY = 0xB3, DONE = 0x00, } read_state_{DONE}; diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py index 6e3628c5ec..b54ba4887c 100644 --- a/esphome/components/pzem004t/sensor.py +++ b/esphome/components/pzem004t/sensor.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import sensor, uart from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ - UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + CONF_ENERGY, UNIT_VOLT, ICON_FLASH, ICON_COUNTER, UNIT_AMPERE, UNIT_WATT, \ + UNIT_WATT_HOURS DEPENDENCIES = ['uart'] @@ -15,6 +16,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), + cv.Optional(CONF_ENERGY): sensor.sensor_schema(UNIT_WATT_HOURS, ICON_COUNTER, 0) }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) @@ -35,3 +37,7 @@ def to_code(config): conf = config[CONF_POWER] sens = yield sensor.new_sensor(conf) cg.add(var.set_power_sensor(sens)) + if CONF_ENERGY in config: + conf = config[CONF_ENERGY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_energy_sensor(sens)) From d1f4c2ab57e7fc4c982a091106c82fed0ec09822 Mon Sep 17 00:00:00 2001 From: rradar <34582688+rradar@users.noreply.github.com> Date: Fri, 29 May 2020 03:10:34 +0100 Subject: [PATCH 009/200] BH1750 Measurement time (#997) * BH1750 Measurement time Fixes https://github.com/esphome/feature-requests/issues/148 * lint * add test Co-authored-by: Otto Winter Co-authored-by: Guillermo Ruffino --- esphome/components/bh1750/bh1750.cpp | 11 +++++++++++ esphome/components/bh1750/bh1750.h | 2 ++ esphome/components/bh1750/sensor.py | 3 +++ tests/test1.yaml | 2 +- 4 files changed, 17 insertions(+), 1 deletion(-) diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index 9cd152e1ef..2ec297a6f4 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -7,6 +7,8 @@ namespace bh1750 { static const char *TAG = "bh1750.sensor"; static const uint8_t BH1750_COMMAND_POWER_ON = 0b00000001; +static const uint8_t BH1750_COMMAND_MT_REG_HI = 0b01000000; // last 3 bits +static const uint8_t BH1750_COMMAND_MT_REG_LO = 0b01100000; // last 5 bits void BH1750Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up BH1750 '%s'...", this->name_.c_str()); @@ -14,7 +16,13 @@ void BH1750Sensor::setup() { this->mark_failed(); return; } + + uint8_t mtreg_hi = (this->measurement_time_ >> 5) & 0b111; + uint8_t mtreg_lo = (this->measurement_time_ >> 0) & 0b11111; + this->write_bytes(BH1750_COMMAND_MT_REG_HI | mtreg_hi, nullptr, 0); + this->write_bytes(BH1750_COMMAND_MT_REG_LO | mtreg_lo, nullptr, 0); } + void BH1750Sensor::dump_config() { LOG_SENSOR("", "BH1750", this); LOG_I2C_DEVICE(this); @@ -59,6 +67,7 @@ void BH1750Sensor::update() { this->set_timeout("illuminance", wait, [this]() { this->read_data_(); }); } + float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } void BH1750Sensor::read_data_() { uint16_t raw_value; @@ -68,10 +77,12 @@ void BH1750Sensor::read_data_() { } float lx = float(raw_value) / 1.2f; + lx *= 69.0f / this->measurement_time_; ESP_LOGD(TAG, "'%s': Got illuminance=%.1flx", this->get_name().c_str(), lx); this->publish_state(lx); this->status_clear_warning(); } + void BH1750Sensor::set_resolution(BH1750Resolution resolution) { this->resolution_ = resolution; } } // namespace bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 8df0bda02a..00abd53e92 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -28,6 +28,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: * @param resolution The new resolution of the sensor. */ void set_resolution(BH1750Resolution resolution); + void set_measurement_time(uint8_t measurement_time) { measurement_time_ = measurement_time; } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) @@ -40,6 +41,7 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void read_data_(); BH1750Resolution resolution_{BH1750_RESOLUTION_0P5_LX}; + uint8_t measurement_time_; }; } // namespace bh1750 diff --git a/esphome/components/bh1750/sensor.py b/esphome/components/bh1750/sensor.py index b3ce0eaf88..54735616d5 100644 --- a/esphome/components/bh1750/sensor.py +++ b/esphome/components/bh1750/sensor.py @@ -15,9 +15,11 @@ BH1750_RESOLUTIONS = { BH1750Sensor = bh1750_ns.class_('BH1750Sensor', sensor.Sensor, cg.PollingComponent, i2c.I2CDevice) +CONF_MEASUREMENT_TIME = 'measurement_time' CONFIG_SCHEMA = sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 1).extend({ cv.GenerateID(): cv.declare_id(BH1750Sensor), cv.Optional(CONF_RESOLUTION, default=0.5): cv.enum(BH1750_RESOLUTIONS, float=True), + cv.Optional(CONF_MEASUREMENT_TIME, default=69): cv.int_range(min=31, max=254), }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x23)) @@ -28,3 +30,4 @@ def to_code(config): yield i2c.register_i2c_device(var, config) cg.add(var.set_resolution(config[CONF_RESOLUTION])) + cg.add(var.set_measurement_time(config[CONF_MEASUREMENT_TIME])) diff --git a/tests/test1.yaml b/tests/test1.yaml index b7d6fe7a9d..115dc73464 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -308,6 +308,7 @@ sensor: retain: False availability: state_topic: livingroom/custom_state_topic + measurement_time: 31 - platform: bme280 temperature: name: "Outside Temperature" @@ -1169,7 +1170,6 @@ light: if (initial_run) { it[0] = current_color; } - - automation: name: Custom Effect sequence: From 64bd33a94eb723ea882d4afa36360780fea3cfb8 Mon Sep 17 00:00:00 2001 From: Ivan Shvedunov Date: Tue, 9 Jun 2020 06:10:49 +0300 Subject: [PATCH 010/200] Sort keys in dicts in output yaml for 'config' command (#1049) Having different output each time due to random key order makes some tasks harder (such as CI scripts). --- esphome/yaml_util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 053fba6274..1758e739db 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -338,7 +338,7 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors self.represented_objects[self.alias_key] = node best_style = True if hasattr(mapping, 'items'): - mapping = list(mapping.items()) + mapping = sorted(mapping.items(), key=lambda item: item[0]) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) From cd7af19e7cf32bc9c40480caa6efa2ba306cf34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Wed, 10 Jun 2020 02:22:06 +0200 Subject: [PATCH 011/200] Extend uart: with rx_buffer_size: (#1006) * Extend `uart:` with `rx_buffer_size:` This allows to configure `rx_buffer_size:` to efficiently receive big payloads over UART * lint * remove old default value * add test Co-authored-by: Guillermo Ruffino --- esphome/components/uart/__init__.py | 5 ++++- esphome/components/uart/uart.h | 6 ++++-- esphome/components/uart/uart_esp32.cpp | 2 ++ esphome/components/uart/uart_esp8266.cpp | 10 ++++++++-- esphome/const.py | 1 + tests/test1.yaml | 1 + 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 06208dc621..65a9a3eaf8 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, CONF_DATA +from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CONF_UART_ID, \ + CONF_DATA, CONF_RX_BUFFER_SIZE from esphome.core import CORE, coroutine uart_ns = cg.esphome_ns.namespace('uart') @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, + cv.Optional(CONF_RX_BUFFER_SIZE, default=256): cv.validate_bytes, cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(min=5, max=8), cv.Optional(CONF_PARITY, default="NONE"): cv.enum(UART_PARITY_OPTIONS, upper=True) @@ -61,6 +63,7 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_rx_buffer_size(config[CONF_RX_BUFFER_SIZE])) cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) cg.add(var.set_data_bits(config[CONF_DATA_BITS])) cg.add(var.set_parity(config[CONF_PARITY])) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 5528da1d5f..dedfdd74af 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -19,7 +19,7 @@ const char *parity_to_str(UARTParityOptions parity); class ESP8266SoftwareSerial { public: void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t nr_bits, - UARTParityOptions parity); + UARTParityOptions parity, size_t rx_buffer_size); uint8_t read_byte(); uint8_t peek_byte(); @@ -44,7 +44,7 @@ class ESP8266SoftwareSerial { uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_{512}; + size_t rx_buffer_size_; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; uint8_t stop_bits_; @@ -93,6 +93,7 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } void set_data_bits(uint8_t nr_bits) { this->nr_bits_ = nr_bits; } void set_parity(UARTParityOptions parity) { this->parity_ = parity; } @@ -108,6 +109,7 @@ class UARTComponent : public Component, public Stream { #endif optional tx_pin_; optional rx_pin_; + size_t rx_buffer_size_; uint32_t baud_rate_; uint8_t stop_bits_; uint8_t nr_bits_; diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp index cb6ac843d1..e18f9c5b1f 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_esp32.cpp @@ -81,6 +81,7 @@ void UARTComponent::setup() { int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; this->hw_serial_->begin(this->baud_rate_, get_config(), rx, tx); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } void UARTComponent::dump_config() { @@ -90,6 +91,7 @@ void UARTComponent::dump_config() { } if (this->rx_pin_.has_value()) { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); ESP_LOGCONFIG(TAG, " Bits: %u", this->nr_bits_); diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index 59a08677b5..40975b6e5e 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -52,18 +52,22 @@ void UARTComponent::setup() { if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { this->hw_serial_ = &Serial; this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); this->hw_serial_->swap(); } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); } else { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->nr_bits_, this->parity_); + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->nr_bits_, this->parity_, + this->rx_buffer_size_); } } @@ -74,6 +78,7 @@ void UARTComponent::dump_config() { } if (this->rx_pin_.has_value()) { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); + ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); ESP_LOGCONFIG(TAG, " Bits: %u", this->nr_bits_); @@ -210,8 +215,9 @@ void ESP8266SoftwareSerial::begin() { // this->gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); } void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t nr_bits, - UARTParityOptions parity) { + UARTParityOptions parity, size_t rx_buffer_size) { this->bit_time_ = F_CPU / baud_rate; + this->rx_buffer_size_ = rx_buffer_size; this->stop_bits_ = stop_bits; this->nr_bits_ = nr_bits; this->parity_ = parity; diff --git a/esphome/const.py b/esphome/const.py index b244d5681a..95b2da36d7 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -395,6 +395,7 @@ CONF_RTD_WIRES = 'rtd_wires' CONF_RUN_CYCLES = 'run_cycles' CONF_RUN_DURATION = 'run_duration' CONF_RW_PIN = 'rw_pin' +CONF_RX_BUFFER_SIZE = 'rx_buffer_size' CONF_RX_ONLY = 'rx_only' CONF_RX_PIN = 'rx_pin' CONF_SAFE_MODE = 'safe_mode' diff --git a/tests/test1.yaml b/tests/test1.yaml index 115dc73464..dae8cfdb21 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -135,6 +135,7 @@ uart: parity: NONE data_bits: 8 stop_bits: 1 + rx_buffer_size: 512 ota: safe_mode: True From f63fd9696fe207c2c996e4a8f87b009a0b0174e3 Mon Sep 17 00:00:00 2001 From: Marvin Gaube Date: Wed, 10 Jun 2020 03:00:12 +0200 Subject: [PATCH 012/200] Add Webserver Prometheus support for sensor, binary sensor, fan, light, cover and switch (#1032) --- esphome/components/web_server/__init__.py | 5 +- esphome/components/web_server/web_server.cpp | 12 + esphome/components/web_server/web_server.h | 7 + .../web_server/web_server_prometheus.cpp | 344 ++++++++++++++++++ .../web_server/web_server_prometheus.h | 61 ++++ esphome/const.py | 1 + tests/test1.yaml | 1 + tests/test3.yaml | 1 + 8 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 esphome/components/web_server/web_server_prometheus.cpp create mode 100644 esphome/components/web_server/web_server_prometheus.h diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 2f0d179eba..be0a180a9d 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -4,7 +4,7 @@ from esphome.components import web_server_base from esphome.components.web_server_base import CONF_WEB_SERVER_BASE_ID from esphome.const import ( CONF_CSS_INCLUDE, CONF_CSS_URL, CONF_ID, CONF_JS_INCLUDE, CONF_JS_URL, CONF_PORT, - CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) + CONF_AUTH, CONF_USERNAME, CONF_PASSWORD, CONF_PROMETHEUS) from esphome.core import coroutine_with_priority AUTO_LOAD = ['json', 'web_server_base'] @@ -19,6 +19,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_CSS_INCLUDE): cv.file_, cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, cv.Optional(CONF_JS_INCLUDE): cv.file_, + cv.Optional(CONF_PROMETHEUS, default=False): cv.boolean, cv.Optional(CONF_AUTH): cv.Schema({ cv.Required(CONF_USERNAME): cv.string_strict, cv.Required(CONF_PASSWORD): cv.string_strict, @@ -49,3 +50,5 @@ def to_code(config): cg.add_define('WEBSERVER_JS_INCLUDE') with open(config[CONF_JS_INCLUDE], "r") as myfile: cg.add(var.set_js_include(myfile.read())) + if config[CONF_PROMETHEUS]: + cg.add_define('WEBSERVER_PROMETHEUS') diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 48a47080b2..7f0bb0f973 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -572,6 +572,11 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { if (request->url() == "/") return true; +#ifdef WEBSERVER_PROMETHEUS + if (request->url() == "/metrics") + return true; +#endif + #ifdef WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") return true; @@ -632,6 +637,13 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { return; } +#ifdef WEBSERVER_PROMETHEUS + if (request->url() == "/metrics") { + this->prometheus.handle_request(request); + return; + } +#endif + #ifdef WEBSERVER_CSS_INCLUDE if (request->url() == "/0.css") { this->handle_css_request(request); diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index b3bf2ef7f7..77fcb1e88c 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -3,6 +3,9 @@ #include "esphome/core/component.h" #include "esphome/core/controller.h" #include "esphome/components/web_server_base/web_server_base.h" +#ifdef WEBSERVER_PROMETHEUS +#include "esphome/components/web_server/web_server_prometheus.h" +#endif #include @@ -170,6 +173,10 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { const char *css_include_{nullptr}; const char *js_url_{nullptr}; const char *js_include_{nullptr}; + +#ifdef WEBSERVER_PROMETHEUS + WebServerPrometheus prometheus; +#endif }; } // namespace web_server diff --git a/esphome/components/web_server/web_server_prometheus.cpp b/esphome/components/web_server/web_server_prometheus.cpp new file mode 100644 index 0000000000..4ba75c1673 --- /dev/null +++ b/esphome/components/web_server/web_server_prometheus.cpp @@ -0,0 +1,344 @@ +#include "web_server.h" +#include "web_server_prometheus.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/util.h" +#include "esphome/components/json/json_util.h" + +#include "StreamString.h" + +#include + +#ifdef USE_LOGGER +#include +#endif + +namespace esphome { +namespace web_server { + +void WebServerPrometheus::handle_request(AsyncWebServerRequest *request) { + AsyncResponseStream *stream = request->beginResponseStream("text/plain"); + +#ifdef USE_SENSOR + this->sensor_type_(stream); + for (auto *obj : App.get_sensors()) + this->sensor_row_(stream, obj); +#endif + +#ifdef USE_BINARY_SENSOR + this->binary_sensor_type_(stream); + for (auto *obj : App.get_binary_sensors()) + this->binary_sensor_row_(stream, obj); +#endif + +#ifdef USE_FAN + this->fan_type_(stream); + for (auto *obj : App.get_fans()) + this->fan_row_(stream, obj); +#endif + +#ifdef USE_LIGHT + this->light_type_(stream); + for (auto *obj : App.get_lights()) + this->light_row_(stream, obj); +#endif + +#ifdef USE_COVER + this->cover_type_(stream); + for (auto *obj : App.get_covers()) + this->cover_row_(stream, obj); +#endif + +#ifdef USE_SWITCH + this->switch_type_(stream); + for (auto *obj : App.get_switches()) + this->switch_row_(stream, obj); +#endif + + request->send(stream); +} + +// Type-specific implementation +#ifdef USE_SENSOR +void WebServerPrometheus::sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_sensor_failed GAUGE\n")); +} +void WebServerPrometheus::sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",unit=\"")); + stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(F("\"} ")); + stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +// Type-specific implementation +#ifdef USE_BINARY_SENSOR +void WebServerPrometheus::binary_sensor_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_binary_sensor_value GAUGE\n")); + stream->print(F("#TYPE esphome_binary_sensor_failed GAUGE\n")); +} +void WebServerPrometheus::binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_binary_sensor_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_binary_sensor_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_FAN +void WebServerPrometheus::fan_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_fan_value GAUGE\n")); + stream->print(F("#TYPE esphome_fan_failed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_speed GAUGE\n")); + stream->print(F("#TYPE esphome_fan_oscillation GAUGE\n")); +} +void WebServerPrometheus::fan_row_(AsyncResponseStream *stream, fan::FanState *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_fan_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + // Speed if available + if (obj->get_traits().supports_speed()) { + stream->print(F("esphome_fan_speed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->speed); + stream->print('\n'); + } + // Oscillation if available + if (obj->get_traits().supports_oscillation()) { + stream->print(F("esphome_fan_oscillation{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->oscillating); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_fan_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_LIGHT +void WebServerPrometheus::light_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_light_state GAUGE\n")); + stream->print(F("#TYPE esphome_light_color GAUGE\n")); + stream->print(F("#TYPE esphome_light_effect_active GAUGE\n")); +} +void WebServerPrometheus::light_row_(AsyncResponseStream *stream, light::LightState *obj) { + if (obj->is_internal()) + return; + // State + stream->print(F("esphome_light_state{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->remote_values.is_on()); + stream->print(F("\n")); + // Brightness and RGBW + light::LightColorValues color = obj->current_values; + float brightness, r, g, b, w; + color.as_brightness(&brightness); + color.as_rgbw(&r, &g, &b, &w); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"brightness\"} ")); + stream->print(brightness); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"r\"} ")); + stream->print(r); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"g\"} ")); + stream->print(g); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"b\"} ")); + stream->print(b); + stream->print(F("\n")); + stream->print(F("esphome_light_color{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",channel=\"w\"} ")); + stream->print(w); + stream->print(F("\n")); + // Effect + std::string effect = obj->get_effect_name(); + if (effect == "None") { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"None\"} 0\n")); + } else { + stream->print(F("esphome_light_effect_active{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\",effect=\"")); + stream->print(effect.c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_COVER +void WebServerPrometheus::cover_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_cover_value GAUGE\n")); + stream->print(F("#TYPE esphome_cover_failed GAUGE\n")); +} +void WebServerPrometheus::cover_row_(AsyncResponseStream *stream, cover::Cover *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->position)) { + // We have a valid value, output this value + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_cover_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->position); + stream->print('\n'); + if (obj->get_traits().get_supports_tilt()) { + stream->print(F("esphome_cover_tilt{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->tilt); + stream->print('\n'); + } + } else { + // Invalid state + stream->print(F("esphome_cover_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +#ifdef USE_SWITCH +void WebServerPrometheus::switch_type_(AsyncResponseStream *stream) { + stream->print(F("#TYPE esphome_switch_value GAUGE\n")); + stream->print(F("#TYPE esphome_switch_failed GAUGE\n")); +} +void WebServerPrometheus::switch_row_(AsyncResponseStream *stream, switch_::Switch *obj) { + if (obj->is_internal()) + return; + if (!isnan(obj->state)) { + // We have a valid value, output this value + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 0\n")); + // Data itself + stream->print(F("esphome_switch_value{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} ")); + stream->print(obj->state); + stream->print('\n'); + } else { + // Invalid state + stream->print(F("esphome_switch_failed{id=\"")); + stream->print(obj->get_object_id().c_str()); + stream->print(F("\",name=\"")); + stream->print(obj->get_name().c_str()); + stream->print(F("\"} 1\n")); + } +} +#endif + +} // namespace web_server +} // namespace esphome diff --git a/esphome/components/web_server/web_server_prometheus.h b/esphome/components/web_server/web_server_prometheus.h new file mode 100644 index 0000000000..93682c1ed5 --- /dev/null +++ b/esphome/components/web_server/web_server_prometheus.h @@ -0,0 +1,61 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/controller.h" +#include "esphome/components/web_server_base/web_server_base.h" + +namespace esphome { +namespace web_server { + +class WebServerPrometheus { + public: + WebServerPrometheus(){}; + /// Handle an prometheus metrics request under '/metrics'. + void handle_request(AsyncWebServerRequest *request); + + protected: +#ifdef USE_SENSOR + /// Return the type for prometheus + void sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void sensor_row_(AsyncResponseStream *stream, sensor::Sensor *obj); +#endif + +#ifdef USE_BINARY_SENSOR + /// Return the type for prometheus + void binary_sensor_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void binary_sensor_row_(AsyncResponseStream *stream, binary_sensor::BinarySensor *obj); +#endif + +#ifdef USE_FAN + /// Return the type for prometheus + void fan_type_(AsyncResponseStream *stream); + /// Return the sensor state as prometheus data point + void fan_row_(AsyncResponseStream *stream, fan::FanState *obj); +#endif + +#ifdef USE_LIGHT + /// Return the type for prometheus + void light_type_(AsyncResponseStream *stream); + /// Return the Light Values state as prometheus data point + void light_row_(AsyncResponseStream *stream, light::LightState *obj); +#endif + +#ifdef USE_COVER + /// Return the type for prometheus + void cover_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void cover_row_(AsyncResponseStream *stream, cover::Cover *obj); +#endif + +#ifdef USE_SWITCH + /// Return the type for prometheus + void switch_type_(AsyncResponseStream *stream); + /// Return the switch Values state as prometheus data point + void switch_row_(AsyncResponseStream *stream, switch_::Switch *obj); +#endif +}; + +} // namespace web_server +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 95b2da36d7..5ac2b3c9ca 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -358,6 +358,7 @@ CONF_POWER_SAVE_MODE = 'power_save_mode' CONF_POWER_SUPPLY = 'power_supply' CONF_PRESSURE = 'pressure' CONF_PRIORITY = 'priority' +CONF_PROMETHEUS = 'prometheus' CONF_PROTOCOL = 'protocol' CONF_PULL_MODE = 'pull_mode' CONF_PULSE_LENGTH = 'pulse_length' diff --git a/tests/test1.yaml b/tests/test1.yaml index dae8cfdb21..d9563a470a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -153,6 +153,7 @@ web_server: port: 8080 css_url: https://esphome.io/_static/webserver-v1.min.css js_url: https://esphome.io/_static/webserver-v1.min.js + prometheus: true power_supply: id: 'atx_power_supply' diff --git a/tests/test3.yaml b/tests/test3.yaml index 0d8be590cf..62658c3c4b 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -197,6 +197,7 @@ logger: esp8266_store_log_strings_in_flash: false web_server: + prometheus: true deep_sleep: run_duration: 20s From bab562dc3a511b2e357883bfee1b411bc3dac252 Mon Sep 17 00:00:00 2001 From: Adriaan Peeters Date: Wed, 10 Jun 2020 04:23:25 +0200 Subject: [PATCH 013/200] Turn off PN532 RF field when not expecting a tag (#1046) * Turn off PN532 RF field when not expecting a tag Avoids interference with Wifi connectivity of nearby devices. * Rename turn_off_rf_ method * documented off command bytes Co-authored-by: Guillermo Ruffino --- esphome/components/pn532/pn532.cpp | 18 +++++++++++++++++- esphome/components/pn532/pn532.h | 2 ++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 93000a7421..792d92a6ac 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -86,6 +86,8 @@ void PN532::setup() { this->mark_failed(); return; } + + this->turn_off_rf_(); } void PN532::update() { @@ -114,13 +116,16 @@ void PN532::loop() { if (read.size() <= 2 || read[0] != 0x4B) { // Something failed + this->turn_off_rf_(); return; } uint8_t num_targets = read[1]; - if (num_targets != 1) + if (num_targets != 1) { // no tags found or too many + this->turn_off_rf_(); return; + } // const uint8_t target_number = read[2]; // const uint16_t sens_res = uint16_t(read[3] << 8) | read[4]; @@ -150,6 +155,17 @@ void PN532::loop() { format_uid(buf, nfcid, nfcid_length); ESP_LOGD(TAG, "Found new tag '%s'", buf); } + + this->turn_off_rf_(); +} + +void PN532::turn_off_rf_() { + ESP_LOGVV(TAG, "Turning RF field OFF"); + this->pn532_write_command_check_ack_({ + 0x32, // RFConfiguration + 0x1, // RF Field + 0x0 // Off + }); } float PN532::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 49d5878265..3a734b7ba2 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -55,6 +55,8 @@ class PN532 : public PollingComponent, bool read_ack_(); + void turn_off_rf_(); + bool requested_read_{false}; std::vector binary_sensors_; std::vector triggers_; From 821c1a8bbdb6b2812a36b066b28bac856768ff4b Mon Sep 17 00:00:00 2001 From: igg Date: Wed, 10 Jun 2020 13:03:11 -0700 Subject: [PATCH 014/200] making SPI CS optional (#988) * making SPI CS optional * CS pin should be declared as optional or required in CONFIG_SCHEMA * changed SPI_DEVICE_SCHEMA to a function, like i2c * added spi_device_schema() to pcd8544, lint fixes * updated max31856 with new spi_device_schema() * cleanup imports Co-authored-by: Ilya Goldberg --- esphome/components/as3935_spi/__init__.py | 4 +-- esphome/components/atm90e32/sensor.py | 2 +- esphome/components/max31855/sensor.py | 2 +- esphome/components/max31856/sensor.py | 2 +- esphome/components/max31865/sensor.py | 2 +- esphome/components/max6675/sensor.py | 2 +- esphome/components/max7219/display.py | 2 +- esphome/components/pcd8544/display.py | 2 +- esphome/components/pn532/__init__.py | 2 +- esphome/components/pn532/pn532.cpp | 3 +- esphome/components/spi/__init__.py | 22 ++++++++++---- esphome/components/spi/spi.cpp | 8 +++-- esphome/components/spi/spi.h | 30 +++++++++++-------- esphome/components/ssd1306_spi/display.py | 2 +- esphome/components/ssd1325_spi/display.py | 2 +- .../components/ssd1325_spi/ssd1325_spi.cpp | 21 ++++++++----- .../components/waveshare_epaper/display.py | 2 +- 17 files changed, 67 insertions(+), 43 deletions(-) diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py index fa27c2b0f5..da5582c531 100644 --- a/esphome/components/as3935_spi/__init__.py +++ b/esphome/components/as3935_spi/__init__.py @@ -10,8 +10,8 @@ as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi') SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice) CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ - cv.GenerateID(): cv.declare_id(SPIAS3935) -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA)) + cv.GenerateID(): cv.declare_id(SPIAS3935), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(CS_PIN_required=True))) def to_code(config): diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index fc526dfbc0..f27fd3126a 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -55,7 +55,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True), cv.Optional(CONF_CURRENT_PHASES, default='3'): cv.enum(CURRENT_PHASES, upper=True), cv.Optional(CONF_GAIN_PGA, default='2X'): cv.enum(PGA_GAINS, upper=True), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max31855/sensor.py b/esphome/components/max31855/sensor.py index dce28bd542..d1b5649b3e 100644 --- a/esphome/components/max31855/sensor.py +++ b/esphome/components/max31855/sensor.py @@ -11,7 +11,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31855Sensor), cv.Optional(CONF_REFERENCE_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max31856/sensor.py b/esphome/components/max31856/sensor.py index 523b5301d4..4e1411a2a4 100644 --- a/esphome/components/max31856/sensor.py +++ b/esphome/components/max31856/sensor.py @@ -16,7 +16,7 @@ FILTER = { CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX31856Sensor), cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max31865/sensor.py b/esphome/components/max31865/sensor.py index ff1df9c5c8..7df36dfde4 100644 --- a/esphome/components/max31865/sensor.py +++ b/esphome/components/max31865/sensor.py @@ -20,7 +20,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 2).extend({ cv.Required(CONF_RTD_NOMINAL_RESISTANCE): cv.All(cv.resistance, cv.Range(min=100, max=1000)), cv.Optional(CONF_MAINS_FILTER, default='60HZ'): cv.enum(FILTER, upper=True, space=''), cv.Optional(CONF_RTD_WIRES, default=4): cv.int_range(min=2, max=4), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max6675/sensor.py b/esphome/components/max6675/sensor.py index 59d24a5283..7f0c943399 100644 --- a/esphome/components/max6675/sensor.py +++ b/esphome/components/max6675/sensor.py @@ -9,7 +9,7 @@ MAX6675Sensor = max6675_ns.class_('MAX6675Sensor', sensor.Sensor, cg.PollingComp CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ cv.GenerateID(): cv.declare_id(MAX6675Sensor), -}).extend(cv.polling_component_schema('60s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('60s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index c96454ce8c..0657f3f042 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -14,7 +14,7 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=255), cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/pcd8544/display.py b/esphome/components/pcd8544/display.py index e47937e46a..8cc92065ec 100644 --- a/esphome/components/pcd8544/display.py +++ b/esphome/components/pcd8544/display.py @@ -17,7 +17,7 @@ CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, # CE -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index c82d35b398..931b6ada6b 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -17,7 +17,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ON_TAG): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PN532Trigger), }), -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA) +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()) def to_code(config): diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 792d92a6ac..36677bc729 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,7 +31,8 @@ void PN532::setup() { // (this may time out, but that's ok) // 3. Send SAM config command with normal mode without waiting for ready bit (IRQ not initialized yet) // 4. Probably optional, send SAM config again, this time checking ACK and return value - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); delay(10); // send dummy firmware version command to get synced up diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 69899d1e84..6ced4bc80f 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -34,15 +34,25 @@ def to_code(config): cg.add(var.set_mosi(mosi)) -SPI_DEVICE_SCHEMA = cv.Schema({ - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), - cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, -}) +def spi_device_schema(CS_PIN_required=False): + """Create a schema for an SPI device. + :param CS_PIN_required: If true, make the CS_PIN required in the config. + :return: The SPI device schema, `extend` this in your config schema. + """ + schema = { + cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + } + if CS_PIN_required: + schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema + else: + schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema + return cv.Schema(schema) @coroutine def register_spi_device(var, config): parent = yield cg.get_variable(config[CONF_SPI_ID]) cg.add(var.set_spi_parent(parent)) - pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) - cg.add(var.set_cs_pin(pin)) + if CONF_CS_PIN in config: + pin = yield cg.gpio_pin_expression(config[CONF_CS_PIN]) + cg.add(var.set_cs_pin(pin)) diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index bf2a18955a..ea6f8d68f6 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -12,9 +12,11 @@ void ICACHE_RAM_ATTR HOT SPIComponent::disable() { if (this->hw_spi_ != nullptr) { this->hw_spi_->endTransaction(); } - ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); - this->active_cs_->digital_write(true); - this->active_cs_ = nullptr; + if (this->active_cs_) { + ESP_LOGVV(TAG, "Disabling SPI Chip on pin %u...", this->active_cs_->get_pin()); + this->active_cs_->digital_write(true); + this->active_cs_ = nullptr; + } } void SPIComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index ccef6192f3..ea0595c641 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -127,19 +127,21 @@ class SPIComponent : public Component { template void enable(GPIOPin *cs) { - SPIComponent::debug_enable(cs->get_pin()); + if (cs) { + SPIComponent::debug_enable(cs->get_pin()); - if (this->hw_spi_ != nullptr) { - uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); - SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); - this->hw_spi_->beginTransaction(settings); - } else { - this->clk_->digital_write(CLOCK_POLARITY); - this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; + if (this->hw_spi_ != nullptr) { + uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); + SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); + this->hw_spi_->beginTransaction(settings); + } else { + this->clk_->digital_write(CLOCK_POLARITY); + this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; + } + + this->active_cs_ = cs; + this->active_cs_->digital_write(false); } - - this->active_cs_ = cs; - this->active_cs_->digital_write(false); } void disable(); @@ -174,8 +176,10 @@ class SPIDevice { void set_cs_pin(GPIOPin *cs) { cs_ = cs; } void spi_setup() { - this->cs_->setup(); - this->cs_->digital_write(true); + if (this->cs_) { + this->cs_->setup(); + this->cs_->digital_write(true); + } } void enable() { this->parent_->template enable(this->cs_); } diff --git a/esphome/components/ssd1306_spi/display.py b/esphome/components/ssd1306_spi/display.py index 65f0df1c51..19882af6c4 100644 --- a/esphome/components/ssd1306_spi/display.py +++ b/esphome/components/ssd1306_spi/display.py @@ -13,7 +13,7 @@ SPISSD1306 = ssd1306_spi.class_('SPISSD1306', ssd1306_base.SSD1306, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1306_base.SSD1306_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1306), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index 4615d45393..d7c7733afc 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -13,7 +13,7 @@ SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1325), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index 399700f1dd..cd5f4a2794 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -11,7 +11,8 @@ void SPISSD1325::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); this->spi_setup(); this->dc_pin_->setup(); // OUTPUT - this->cs_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT this->init_reset_(); delay(500); // NOLINT @@ -27,19 +28,24 @@ void SPISSD1325::dump_config() { LOG_UPDATE_INTERVAL(this); } void SPISSD1325::command(uint8_t value) { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(false); delay(1); this->enable(); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); this->write_byte(value); - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } void HOT SPISSD1325::write_display_data() { - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->dc_pin_->digital_write(true); - this->cs_->digital_write(false); + if (this->cs_) + this->cs_->digital_write(false); delay(1); this->enable(); for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { @@ -56,7 +62,8 @@ void HOT SPISSD1325::write_display_data() { } } } - this->cs_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(true); this->disable(); } diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index a654e55981..3e6a972742 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -50,7 +50,7 @@ CONFIG_SCHEMA = cv.All(display.FULL_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_FULL_UPDATE_EVERY): cv.uint32_t, -}).extend(cv.polling_component_schema('1s')).extend(spi.SPI_DEVICE_SCHEMA), +}).extend(cv.polling_component_schema('1s')).extend(spi.spi_device_schema()), validate_full_update_every_only_type_a, cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) From 42007d03d49f94d26591ee3ae7dab549abd8baff Mon Sep 17 00:00:00 2001 From: Evgeny Date: Fri, 12 Jun 2020 04:14:54 +0200 Subject: [PATCH 015/200] AQI calculator for HM3301 (#1011) * HM3301 AQI calculator * remove logs * fixed after lint * fixed after lint * fixed after lint * check NP for AQI sensor * validation for AQI sensor --- .../hm3301/abstract_aqi_calculator.h | 14 +++++ esphome/components/hm3301/aqi_calculator.cpp | 46 ++++++++++++++++ .../hm3301/aqi_calculator_factory.h | 30 +++++++++++ esphome/components/hm3301/caqi_calculator.cpp | 52 +++++++++++++++++++ esphome/components/hm3301/hm3301.cpp | 36 ++++++++++--- esphome/components/hm3301/hm3301.h | 8 +++ esphome/components/hm3301/sensor.py | 31 ++++++++++- tests/test1.yaml | 3 ++ tests/test3.yaml | 3 ++ 9 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 esphome/components/hm3301/abstract_aqi_calculator.h create mode 100644 esphome/components/hm3301/aqi_calculator.cpp create mode 100644 esphome/components/hm3301/aqi_calculator_factory.h create mode 100644 esphome/components/hm3301/caqi_calculator.cpp diff --git a/esphome/components/hm3301/abstract_aqi_calculator.h b/esphome/components/hm3301/abstract_aqi_calculator.h new file mode 100644 index 0000000000..f160a91148 --- /dev/null +++ b/esphome/components/hm3301/abstract_aqi_calculator.h @@ -0,0 +1,14 @@ +#pragma once + +#include "Arduino.h" + +namespace esphome { +namespace hm3301 { + +class AbstractAQICalculator { + public: + virtual uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0; +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator.cpp b/esphome/components/hm3301/aqi_calculator.cpp new file mode 100644 index 0000000000..6b70c5d4fd --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator.cpp @@ -0,0 +1,46 @@ +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class AQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 6; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 51}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 12}, {13, 45}, {36, 55}, {56, 150}, {151, 250}, {251, 500}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254}, + {255, 354}, {355, 424}, {425, 604}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + return ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS - 1; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/aqi_calculator_factory.h b/esphome/components/hm3301/aqi_calculator_factory.h new file mode 100644 index 0000000000..483a158822 --- /dev/null +++ b/esphome/components/hm3301/aqi_calculator_factory.h @@ -0,0 +1,30 @@ +#pragma once + +#include "Arduino.h" +#include "caqi_calculator.cpp" +#include "aqi_calculator.cpp" + +namespace esphome { +namespace hm3301 { + +enum AQICalculatorType { CAQI_TYPE = 0, AQI_TYPE = 1 }; + +class AQICalculatorFactory { + public: + AbstractAQICalculator *get_calculator(AQICalculatorType type) { + if (type == 0) { + return caqi_calculator_; + } else if (type == 1) { + return aqi_calculator_; + } + + return nullptr; + } + + protected: + CAQICalculator *caqi_calculator_ = new CAQICalculator(); + AQICalculator *aqi_calculator_ = new AQICalculator(); +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/caqi_calculator.cpp b/esphome/components/hm3301/caqi_calculator.cpp new file mode 100644 index 0000000000..511179cb71 --- /dev/null +++ b/esphome/components/hm3301/caqi_calculator.cpp @@ -0,0 +1,52 @@ +#include "esphome/core/log.h" +#include "abstract_aqi_calculator.h" + +namespace esphome { +namespace hm3301 { + +class CAQICalculator : public AbstractAQICalculator { + public: + uint8_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override { + int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_); + int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_); + + return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index; + } + + protected: + static const int AMOUNT_OF_LEVELS = 5; + + int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; + + int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}}; + + int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}}; + + int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + int grid_index = get_grid_index_(value, array); + if (grid_index == -1) { + return -1; + } + + int aqi_lo = index_grid_[grid_index][0]; + int aqi_hi = index_grid_[grid_index][1]; + int conc_lo = array[grid_index][0]; + int conc_hi = array[grid_index][1]; + + int aqi = ((aqi_hi - aqi_lo) / (conc_hi - conc_lo)) * (value - conc_lo) + aqi_lo; + + return aqi; + } + + int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) { + for (int i = 0; i < AMOUNT_OF_LEVELS; i++) { + if (value >= array[i][0] && value <= array[i][1]) { + return i; + } + } + return -1; + } +}; + +} // namespace hm3301 +} // namespace esphome diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 6456ee354a..cbce714012 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -1,5 +1,5 @@ -#include "hm3301.h" #include "esphome/core/log.h" +#include "hm3301.h" namespace esphome { namespace hm3301 { @@ -30,6 +30,7 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "PM1.0", this->pm_1_0_sensor_); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } @@ -47,17 +48,38 @@ void HM3301Component::update() { return; } + int16_t pm_1_0_value = -1; if (this->pm_1_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); - this->pm_1_0_sensor_->publish_state(value); + pm_1_0_value = get_sensor_value_(data_buffer_, PM_1_0_VALUE_INDEX); } + + int16_t pm_2_5_value = -1; if (this->pm_2_5_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); - this->pm_2_5_sensor_->publish_state(value); + pm_2_5_value = get_sensor_value_(data_buffer_, PM_2_5_VALUE_INDEX); } + + int16_t pm_10_0_value = -1; if (this->pm_10_0_sensor_ != nullptr) { - uint16_t value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); - this->pm_10_0_sensor_->publish_state(value); + pm_10_0_value = get_sensor_value_(data_buffer_, PM_10_0_VALUE_INDEX); + } + + int8_t aqi_value = -1; + if (this->aqi_sensor_ != nullptr && pm_2_5_value != -1 && pm_10_0_value != -1) { + AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_); + aqi_value = calculator->get_aqi(pm_2_5_value, pm_10_0_value); + } + + if (pm_1_0_value != -1) { + this->pm_1_0_sensor_->publish_state(pm_1_0_value); + } + if (pm_2_5_value != -1) { + this->pm_2_5_sensor_->publish_state(pm_2_5_value); + } + if (pm_10_0_value != -1) { + this->pm_10_0_sensor_->publish_state(pm_10_0_value); + } + if (aqi_value != -1) { + this->aqi_sensor_->publish_state(aqi_value); } this->status_clear_warning(); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index 0fbb32612e..5594f1719c 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/i2c/i2c.h" +#include "aqi_calculator_factory.h" #include @@ -16,6 +17,9 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { pm_1_0_sensor_ = pm_1_0_sensor; } void set_pm_2_5_sensor(sensor::Sensor *pm_2_5_sensor) { pm_2_5_sensor_ = pm_2_5_sensor; } void set_pm_10_0_sensor(sensor::Sensor *pm_10_0_sensor) { pm_10_0_sensor_ = pm_10_0_sensor; } + void set_aqi_sensor(sensor::Sensor *aqi_sensor) { aqi_sensor_ = aqi_sensor; } + + void set_aqi_calculation_type(AQICalculatorType aqi_calc_type) { aqi_calc_type_ = aqi_calc_type; } void setup() override; void dump_config() override; @@ -32,6 +36,10 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { sensor::Sensor *pm_1_0_sensor_{nullptr}; sensor::Sensor *pm_2_5_sensor_{nullptr}; sensor::Sensor *pm_10_0_sensor_{nullptr}; + sensor::Sensor *aqi_sensor_{nullptr}; + + AQICalculatorType aqi_calc_type_; + AQICalculatorFactory aqi_calculator_factory_ = AQICalculatorFactory(); bool read_sensor_value_(uint8_t *); bool validate_checksum_(const uint8_t *); diff --git a/esphome/components/hm3301/sensor.py b/esphome/components/hm3301/sensor.py index 718d0a20bb..ef7669bc03 100644 --- a/esphome/components/hm3301/sensor.py +++ b/esphome/components/hm3301/sensor.py @@ -8,6 +8,25 @@ DEPENDENCIES = ['i2c'] hm3301_ns = cg.esphome_ns.namespace('hm3301') HM3301Component = hm3301_ns.class_('HM3301Component', cg.PollingComponent, i2c.I2CDevice) +AQICalculatorType = hm3301_ns.enum('AQICalculatorType') + +CONF_AQI = 'aqi' +CONF_CALCULATION_TYPE = 'calculation_type' +UNIT_INDEX = 'index' + +AQI_CALCULATION_TYPE = { + 'CAQI': AQICalculatorType.CAQI_TYPE, + 'AQI': AQICalculatorType.AQI_TYPE +} + + +def validate(config): + if CONF_AQI in config and CONF_PM_2_5 not in config: + raise cv.Invalid("AQI sensor requires PM 2.5") + if CONF_AQI in config and CONF_PM_10_0 not in config: + raise cv.Invalid("AQI sensor requires PM 10 sensors") + return config + CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(HM3301Component), @@ -18,8 +37,11 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), cv.Optional(CONF_PM_10_0): sensor.sensor_schema(UNIT_MICROGRAMS_PER_CUBIC_METER, ICON_CHEMICAL_WEAPON, 0), - -}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40))) + cv.Optional(CONF_AQI): + sensor.sensor_schema(UNIT_INDEX, ICON_CHEMICAL_WEAPON, 0).extend({ + cv.Required(CONF_CALCULATION_TYPE): cv.enum(AQI_CALCULATION_TYPE, upper=True), + }) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x40)), validate) def to_code(config): @@ -39,5 +61,10 @@ def to_code(config): sens = yield sensor.new_sensor(config[CONF_PM_10_0]) cg.add(var.set_pm_10_0_sensor(sens)) + if CONF_AQI in config: + sens = yield sensor.new_sensor(config[CONF_AQI]) + cg.add(var.set_aqi_sensor(sens)) + cg.add(var.set_aqi_calculation_type(config[CONF_AQI][CONF_CALCULATION_TYPE])) + # https://platformio.org/lib/show/6306/Grove%20-%20Laser%20PM2.5%20Sensor%20HM3301 cg.add_library('6306', '1.0.3') diff --git a/tests/test1.yaml b/tests/test1.yaml index d9563a470a..5f8bdd1111 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -738,6 +738,9 @@ sensor: name: "PM2.5" pm_10_0: name: "PM10.0" + aqi: + name: "AQI" + calculation_type: "CAQI" esp32_touch: setup_mode: False diff --git a/tests/test3.yaml b/tests/test3.yaml index 62658c3c4b..06bfce97a2 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -361,6 +361,9 @@ sensor: name: "PM2.5" pm_10_0: name: "PM10.0" + aqi: + name: "AQI" + calculation_type: "AQI" - platform: pmsx003 type: PMSX003 pm_1_0: From 8aedac81a5d2ac6f32e3a36be5c982cebd5b1f87 Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Fri, 12 Jun 2020 20:17:46 +0200 Subject: [PATCH 016/200] RGBWW/CCT Lights: Fix gamma_correct when using constant_brightness (#1043) --- esphome/components/light/light_color_values.h | 37 +++++++++++-------- esphome/components/light/light_state.cpp | 25 +++---------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 39a93cbbcd..90f5570b90 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -17,12 +17,13 @@ namespace light { * Not all values have to be populated though, for example a simple monochromatic light only needs * to access the state and brightness attributes. * - * PLease note all float values are automatically clamped. + * Please note all float values are automatically clamped. * * state - Whether the light should be on/off. Represented as a float for transitions. * brightness - The brightness of the light. * red, green, blue - RGB values. * white - The white value for RGBW lights. + * color_temperature - Temperature of the white value, range from 0.0 (cold) to 1.0 (warm) */ class LightColorValues { public: @@ -173,30 +174,33 @@ class LightColorValues { void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } + void as_brightness(float *brightness, float gamma = 0) const { + *brightness = gamma_correct(this->state_ * this->brightness_, gamma); + } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue) const { - *red = this->state_ * this->brightness_ * this->red_; - *green = this->state_ * this->brightness_ * this->green_; - *blue = this->state_ * this->brightness_ * this->blue_; + void as_rgb(float *red, float *green, float *blue, float gamma = 0) const { + *red = gamma_correct(this->state_ * this->brightness_ * this->red_, gamma); + *green = gamma_correct(this->state_ * this->brightness_ * this->green_, gamma); + *blue = gamma_correct(this->state_ * this->brightness_ * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white) const { - this->as_rgb(red, green, blue); - *white = this->state_ * this->brightness_ * this->white_; + void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0) const { + this->as_rgb(red, green, blue, gamma); + *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); } /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white, bool constant_brightness = false) const { - this->as_rgb(red, green, blue); + float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { + this->as_rgb(red, green, blue, gamma); const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - *cold_white = this->state_ * this->brightness_ * this->white_ * cw_fraction; - *warm_white = this->state_ * this->brightness_ * this->white_ * ww_fraction; + const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *cold_white = white_level * cw_fraction; + *warm_white = white_level * ww_fraction; if (!constant_brightness) { const float max_cw_ww = std::max(ww_fraction, cw_fraction); *cold_white /= max_cw_ww; @@ -206,12 +210,13 @@ class LightColorValues { /// Convert these light color values to an CWWW representation with the given parameters. void as_cwww(float color_temperature_cw, float color_temperature_ww, float *cold_white, float *warm_white, - bool constant_brightness = false) const { + float gamma = 0, bool constant_brightness = false) const { const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; - *cold_white = this->state_ * this->brightness_ * cw_fraction; - *warm_white = this->state_ * this->brightness_ * ww_fraction; + const float white_level = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *cold_white = white_level * cw_fraction; + *warm_white = white_level * ww_fraction; if (!constant_brightness) { const float max_cw_ww = std::max(ww_fraction, cw_fraction); *cold_white /= max_cw_ww; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index c6e7df0811..9aa32f6904 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -702,39 +702,24 @@ LightOutput *LightState::get_output() const { return this->output_; } void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; } void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness); - *brightness = gamma_correct(*brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness, this->gamma_correct_); } void LightState::current_values_as_rgb(float *red, float *green, float *blue) { - this->current_values.as_rgb(red, green, blue); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_); } void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) { - this->current_values.as_rgbw(red, green, blue, white); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *white = gamma_correct(*white, this->gamma_correct_); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, constant_brightness); - *red = gamma_correct(*red, this->gamma_correct_); - *green = gamma_correct(*green, this->gamma_correct_); - *blue = gamma_correct(*blue, this->gamma_correct_); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + warm_white, this->gamma_correct_, constant_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); this->current_values.as_cwww(traits.get_min_mireds(), traits.get_max_mireds(), cold_white, warm_white, - constant_brightness); - *cold_white = gamma_correct(*cold_white, this->gamma_correct_); - *warm_white = gamma_correct(*warm_white, this->gamma_correct_); + this->gamma_correct_, constant_brightness); } void LightState::add_new_remote_values_callback(std::function &&send_callback) { this->remote_values_callback_.add(std::move(send_callback)); From 27204aa53cd114a5b2c7a06f383ae5aa6ccb302a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 13 Jun 2020 01:17:13 +0200 Subject: [PATCH 017/200] Add E1.31 support (#950) This adds a `e131` component that allows to register `e131` addressable light effect. This uses an internal implementation that is thread-safe instead of using external libraries. --- esphome/components/e131/__init__.py | 49 +++++++ esphome/components/e131/e131.cpp | 105 ++++++++++++++ esphome/components/e131/e131.h | 57 ++++++++ .../e131/e131_addressable_light_effect.cpp | 90 ++++++++++++ .../e131/e131_addressable_light_effect.h | 48 +++++++ esphome/components/e131/e131_packet.cpp | 136 ++++++++++++++++++ tests/test1.yaml | 4 + tests/test3.yaml | 5 + 8 files changed, 494 insertions(+) create mode 100644 esphome/components/e131/__init__.py create mode 100644 esphome/components/e131/e131.cpp create mode 100644 esphome/components/e131/e131.h create mode 100644 esphome/components/e131/e131_addressable_light_effect.cpp create mode 100644 esphome/components/e131/e131_addressable_light_effect.h create mode 100644 esphome/components/e131/e131_packet.cpp diff --git a/esphome/components/e131/__init__.py b/esphome/components/e131/__init__.py new file mode 100644 index 0000000000..0ba16bc928 --- /dev/null +++ b/esphome/components/e131/__init__.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS + +e131_ns = cg.esphome_ns.namespace('e131') +E131AddressableLightEffect = e131_ns.class_('E131AddressableLightEffect', AddressableLightEffect) +E131Component = e131_ns.class_('E131Component', cg.Component) + +METHODS = { + 'UNICAST': e131_ns.E131_UNICAST, + 'MULTICAST': e131_ns.E131_MULTICAST +} + +CHANNELS = { + 'MONO': e131_ns.E131_MONO, + 'RGB': e131_ns.E131_RGB, + 'RGBW': e131_ns.E131_RGBW +} + +CONF_UNIVERSE = 'universe' +CONF_E131_ID = 'e131_id' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(E131Component), + cv.Optional(CONF_METHOD, default='MULTICAST'): cv.one_of(*METHODS, upper=True), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + cg.add(var.set_method(METHODS[config[CONF_METHOD]])) + + +@register_addressable_effect('e131', E131AddressableLightEffect, "E1.31", { + cv.GenerateID(CONF_E131_ID): cv.use_id(E131Component), + cv.Required(CONF_UNIVERSE): cv.int_range(min=1, max=512), + cv.Optional(CONF_CHANNELS, default='RGB'): cv.one_of(*CHANNELS, upper=True) +}) +def e131_light_effect_to_code(config, effect_id): + parent = yield cg.get_variable(config[CONF_E131_ID]) + + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_first_universe(config[CONF_UNIVERSE])) + cg.add(effect.set_channels(CHANNELS[config[CONF_CHANNELS]])) + cg.add(effect.set_e131(parent)) + yield effect diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp new file mode 100644 index 0000000000..d107d9f9fc --- /dev/null +++ b/esphome/components/e131/e131.cpp @@ -0,0 +1,105 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131"; +static const int PORT = 5568; + +E131Component::E131Component() {} + +E131Component::~E131Component() { + if (udp_) { + udp_->stop(); + } +} + +void E131Component::setup() { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(PORT)) { + ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); + mark_failed(); + } + + join_igmp_groups_(); +} + +void E131Component::loop() { + std::vector payload; + E131Packet packet; + int universe = 0; + + while (uint16_t packet_size = udp_->parsePacket()) { + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!packet_(payload, universe, packet)) { + ESP_LOGV(TAG, "Invalid packet recevied of size %zu.", payload.size()); + continue; + } + + if (!process_(universe, packet)) { + ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); + } + } +} + +void E131Component::add_effect(E131AddressableLightEffect *light_effect) { + if (light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.insert(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + join_(universe); + } +} + +void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { + if (!light_effects_.count(light_effect)) { + return; + } + + ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), + light_effect->get_first_universe(), light_effect->get_last_universe()); + + light_effects_.erase(light_effect); + + for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { + leave_(universe); + } +} + +bool E131Component::process_(int universe, const E131Packet &packet) { + bool handled = false; + + ESP_LOGV(TAG, "Received E1.31 packet for %d universe, with %d bytes", universe, packet.count); + + for (auto light_effect : light_effects_) { + handled = light_effect->process_(universe, packet) || handled; + } + + return handled; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131.h b/esphome/components/e131/e131.h new file mode 100644 index 0000000000..3f647edbf1 --- /dev/null +++ b/esphome/components/e131/e131.h @@ -0,0 +1,57 @@ +#pragma once + +#include "esphome/core/component.h" + +#include +#include +#include + +class UDP; + +namespace esphome { +namespace e131 { + +class E131AddressableLightEffect; + +enum E131ListenMethod { E131_MULTICAST, E131_UNICAST }; + +const int E131_MAX_PROPERTY_VALUES_COUNT = 513; + +struct E131Packet { + uint16_t count; + uint8_t values[E131_MAX_PROPERTY_VALUES_COUNT]; +}; + +class E131Component : public esphome::Component { + public: + E131Component(); + ~E131Component(); + + void setup() override; + void loop() override; + float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } + + public: + void add_effect(E131AddressableLightEffect *light_effect); + void remove_effect(E131AddressableLightEffect *light_effect); + + public: + void set_method(E131ListenMethod listen_method) { this->listen_method_ = listen_method; } + + protected: + bool packet_(const std::vector &data, int &universe, E131Packet &packet); + bool process_(int universe, const E131Packet &packet); + bool join_igmp_groups_(); + void join_(int universe); + void leave_(int universe); + + protected: + E131ListenMethod listen_method_{E131_MULTICAST}; + std::unique_ptr udp_; + std::set light_effects_; + std::map universe_consumers_; + std::map universe_packets_; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.cpp b/esphome/components/e131/e131_addressable_light_effect.cpp new file mode 100644 index 0000000000..8657d828c5 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.cpp @@ -0,0 +1,90 @@ +#include "e131.h" +#include "e131_addressable_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131_addressable_light_effect"; +static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); + +E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } + +int E131AddressableLightEffect::get_lights_per_universe() const { return MAX_DATA_SIZE / channels_; } + +int E131AddressableLightEffect::get_first_universe() const { return first_universe_; } + +int E131AddressableLightEffect::get_last_universe() const { return first_universe_ + get_universe_count() - 1; } + +int E131AddressableLightEffect::get_universe_count() const { + // Round up to lights_per_universe + auto lights = get_lights_per_universe(); + return (get_addressable_()->size() + lights - 1) / lights; +} + +void E131AddressableLightEffect::start() { + AddressableLightEffect::start(); + + if (this->e131_) { + this->e131_->add_effect(this); + } +} + +void E131AddressableLightEffect::stop() { + if (this->e131_) { + this->e131_->remove_effect(this); + } + + AddressableLightEffect::stop(); +} + +void E131AddressableLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // ignore, it is run by `E131Component::update()` +} + +bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet) { + auto it = get_addressable_(); + + // check if this is our universe and data are valid + if (universe < first_universe_ || universe > get_last_universe()) + return false; + + int output_offset = (universe - first_universe_) * get_lights_per_universe(); + // limit amount of lights per universe and received + int output_end = std::min(it->size(), std::min(output_offset + get_lights_per_universe(), packet.count - 1)); + auto input_data = packet.values + 1; + + ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %d-%d.", get_name().c_str(), universe, output_offset, + output_end); + + switch (channels_) { + case E131_MONO: + for (; output_offset < output_end; output_offset++, input_data++) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[0], input_data[0], input_data[0])); + } + break; + + case E131_RGB: + for (; output_offset < output_end; output_offset++, input_data += 3) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], + (input_data[0] + input_data[1] + input_data[2]) / 3)); + } + break; + + case E131_RGBW: + for (; output_offset < output_end; output_offset++, input_data += 4) { + auto output = (*it)[output_offset]; + output.set(light::ESPColor(input_data[0], input_data[1], input_data[2], input_data[3])); + } + break; + } + + return true; +} + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_addressable_light_effect.h b/esphome/components/e131/e131_addressable_light_effect.h new file mode 100644 index 0000000000..85af4fe7a9 --- /dev/null +++ b/esphome/components/e131/e131_addressable_light_effect.h @@ -0,0 +1,48 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +namespace esphome { +namespace e131 { + +class E131Component; +struct E131Packet; + +enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; + +class E131AddressableLightEffect : public light::AddressableLightEffect { + public: + E131AddressableLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + public: + int get_data_per_universe() const; + int get_lights_per_universe() const; + int get_first_universe() const; + int get_last_universe() const; + int get_universe_count() const; + + public: + void set_first_universe(int universe) { this->first_universe_ = universe; } + void set_channels(E131LightChannels channels) { this->channels_ = channels; } + void set_e131(E131Component *e131) { this->e131_ = e131; } + + protected: + bool process_(int universe, const E131Packet &packet); + + protected: + int first_universe_{0}; + int last_universe_{0}; + E131LightChannels channels_{E131_RGB}; + E131Component *e131_{nullptr}; + + friend class E131Component; +}; + +} // namespace e131 +} // namespace esphome diff --git a/esphome/components/e131/e131_packet.cpp b/esphome/components/e131/e131_packet.cpp new file mode 100644 index 0000000000..ca68f5126d --- /dev/null +++ b/esphome/components/e131/e131_packet.cpp @@ -0,0 +1,136 @@ +#include "e131.h" +#include "esphome/core/log.h" +#include "esphome/core/util.h" + +#include +#include + +namespace esphome { +namespace e131 { + +static const char *TAG = "e131"; + +static const uint8_t ACN_ID[12] = {0x41, 0x53, 0x43, 0x2d, 0x45, 0x31, 0x2e, 0x31, 0x37, 0x00, 0x00, 0x00}; +static const uint32_t VECTOR_ROOT = 4; +static const uint32_t VECTOR_FRAME = 2; +static const uint8_t VECTOR_DMP = 2; + +// E1.31 Packet Structure +union E131RawPacket { + struct { + // Root Layer + uint16_t preamble_size; + uint16_t postamble_size; + uint8_t acn_id[12]; + uint16_t root_flength; + uint32_t root_vector; + uint8_t cid[16]; + + // Frame Layer + uint16_t frame_flength; + uint32_t frame_vector; + uint8_t source_name[64]; + uint8_t priority; + uint16_t reserved; + uint8_t sequence_number; + uint8_t options; + uint16_t universe; + + // DMP Layer + uint16_t dmp_flength; + uint8_t dmp_vector; + uint8_t type; + uint16_t first_address; + uint16_t address_increment; + uint16_t property_value_count; + uint8_t property_values[E131_MAX_PROPERTY_VALUES_COUNT]; + } __attribute__((packed)); + + uint8_t raw[638]; +}; + +// We need to have at least one `1` value +// Get the offset of `property_values[1]` +const long E131_MIN_PACKET_SIZE = reinterpret_cast(&((E131RawPacket *) nullptr)->property_values[1]); + +bool E131Component::join_igmp_groups_() { + if (listen_method_ != E131_MULTICAST) + return false; + if (!udp_) + return false; + + for (auto universe : universe_consumers_) { + if (!universe.second) + continue; + + ip4_addr_t multicast_addr = { + static_cast(IPAddress(239, 255, ((universe.first >> 8) & 0xff), ((universe.first >> 0) & 0xff)))}; + + auto err = igmp_joingroup(IP4_ADDR_ANY4, &multicast_addr); + + if (err) { + ESP_LOGW(TAG, "IGMP join for %d universe of E1.31 failed. Multicast might not work.", universe.first); + } + } + + return true; +} + +void E131Component::join_(int universe) { + // store only latest received packet for the given universe + auto consumers = ++universe_consumers_[universe]; + + if (consumers > 1) { + return; // we already joined before + } + + if (join_igmp_groups_()) { + ESP_LOGD(TAG, "Joined %d universe for E1.31.", universe); + } +} + +void E131Component::leave_(int universe) { + auto consumers = --universe_consumers_[universe]; + + if (consumers > 0) { + return; // we have other consumers of the given universe + } + + if (listen_method_ == E131_MULTICAST) { + ip4_addr_t multicast_addr = { + static_cast(IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)))}; + + igmp_leavegroup(IP4_ADDR_ANY4, &multicast_addr); + } + + ESP_LOGD(TAG, "Left %d universe for E1.31.", universe); +} + +bool E131Component::packet_(const std::vector &data, int &universe, E131Packet &packet) { + if (data.size() < E131_MIN_PACKET_SIZE) + return false; + + auto sbuff = reinterpret_cast(&data[0]); + + if (memcmp(sbuff->acn_id, ACN_ID, sizeof(sbuff->acn_id)) != 0) + return false; + if (htonl(sbuff->root_vector) != VECTOR_ROOT) + return false; + if (htonl(sbuff->frame_vector) != VECTOR_FRAME) + return false; + if (sbuff->dmp_vector != VECTOR_DMP) + return false; + if (sbuff->property_values[0] != 0) + return false; + + universe = htons(sbuff->universe); + packet.count = htons(sbuff->property_value_count); + if (packet.count > E131_MAX_PROPERTY_VALUES_COUNT) + return false; + + memcpy(packet.values, sbuff->property_values, packet.count); + return true; +} + +} // namespace e131 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 5f8bdd1111..b6290c0440 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1048,6 +1048,8 @@ output: pin: GPIO25 id: dac_output +e131: + light: - platform: binary name: "Desk Lamp" @@ -1189,6 +1191,8 @@ light: red: 0% green: 100% blue: 0% + - e131: + universe: 1 - platform: fastled_spi id: addr2 chipset: WS2801 diff --git a/tests/test3.yaml b/tests/test3.yaml index 06bfce97a2..fbfc486fe5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -697,6 +697,8 @@ mcp23017: mcp23008: id: mcp23008_hub +e131: + light: - platform: neopixelbus name: Neopixelbus Light @@ -705,6 +707,9 @@ light: variant: SK6812 method: ESP8266_UART0 num_leds: 100 + effects: + - e131: + universe: 1 servo: id: my_servo From d1b051a6bdc3c3de5b9e1ea14e9c3e1aa1c46e2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 13 Jun 2020 01:34:38 +0200 Subject: [PATCH 018/200] Add Adalight support (#956) A component to support [Adalight](https://learn.adafruit.com/adalight-diy-ambient-tv-lighting). This allows to control addressable LEDs over UART, by pushing data right into LEDs. The most useful to use [Prismatik](https://github.com/psieg/Lightpack) to create an immersive effect on PC. Co-authored-by: Guillermo Ruffino --- esphome/components/adalight/__init__.py | 24 +++ .../adalight/adalight_light_effect.cpp | 140 ++++++++++++++++++ .../adalight/adalight_light_effect.h | 41 +++++ tests/test1.yaml | 26 +++- tests/test3.yaml | 14 +- 5 files changed, 234 insertions(+), 11 deletions(-) create mode 100644 esphome/components/adalight/__init__.py create mode 100644 esphome/components/adalight/adalight_light_effect.cpp create mode 100644 esphome/components/adalight/adalight_light_effect.h diff --git a/esphome/components/adalight/__init__.py b/esphome/components/adalight/__init__.py new file mode 100644 index 0000000000..66fae17f1e --- /dev/null +++ b/esphome/components/adalight/__init__.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_UART_ID + +DEPENDENCIES = ['uart'] + +adalight_ns = cg.esphome_ns.namespace('adalight') +AdalightLightEffect = adalight_ns.class_( + 'AdalightLightEffect', uart.UARTDevice, AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('adalight', AdalightLightEffect, "Adalight", { + cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent) +}) +def adalight_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + yield uart.register_uart_device(effect, config) + + yield effect diff --git a/esphome/components/adalight/adalight_light_effect.cpp b/esphome/components/adalight/adalight_light_effect.cpp new file mode 100644 index 0000000000..1bf357e308 --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.cpp @@ -0,0 +1,140 @@ +#include "adalight_light_effect.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace adalight { + +static const char *TAG = "adalight_light_effect"; + +static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; +static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; + +AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void AdalightLightEffect::start() { + AddressableLightEffect::start(); + + last_ack_ = 0; + last_byte_ = 0; + last_reset_ = 0; +} + +void AdalightLightEffect::stop() { + frame_.resize(0); + + AddressableLightEffect::stop(); +} + +int AdalightLightEffect::get_frame_size_(int led_count) const { + // 3 bytes: Ada + // 2 bytes: LED count + // 1 byte: checksum + // 3 bytes per LED + return 3 + 2 + 1 + led_count * 3; +} + +void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { + int buffer_capacity = get_frame_size_(it.size()); + + frame_.clear(); + frame_.reserve(buffer_capacity); +} + +void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void AdalightLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + const uint32_t now = millis(); + + if (now - this->last_ack_ >= ADALIGHT_ACK_INTERVAL) { + ESP_LOGV(TAG, "Sending ACK"); + this->write_str("Ada\n"); + this->last_ack_ = now; + } + + if (!this->last_reset_) { + ESP_LOGW(TAG, "Frame: Reset."); + reset_frame_(it); + blank_all_leds_(it); + this->last_reset_ = now; + } + + if (!this->frame_.empty() && now - this->last_byte_ >= ADALIGHT_RECEIVE_TIMEOUT) { + ESP_LOGW(TAG, "Frame: Receive timeout (size=%zu).", this->frame_.size()); + reset_frame_(it); + blank_all_leds_(it); + } + + if (this->available() > 0) { + ESP_LOGV(TAG, "Frame: Available (size=%d).", this->available()); + } + + while (this->available() != 0) { + uint8_t data; + if (!this->read_byte(&data)) + break; + this->frame_.push_back(data); + this->last_byte_ = now; + + switch (this->parse_frame_(it)) { + case INVALID: + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%d).", this->frame_.size(), this->frame_[0]); + reset_frame_(it); + break; + + case PARTIAL: + break; + + case CONSUMED: + ESP_LOGV(TAG, "Frame: Consumed (size=%zu).", this->frame_.size()); + reset_frame_(it); + break; + } + } +} + +AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableLight &it) { + if (frame_.empty()) + return INVALID; + + // Check header: `Ada` + if (frame_[0] != 'A') + return INVALID; + if (frame_.size() > 1 && frame_[1] != 'd') + return INVALID; + if (frame_.size() > 2 && frame_[2] != 'a') + return INVALID; + + // 3 bytes: Count Hi, Count Lo, Checksum + if (frame_.size() < 6) + return PARTIAL; + + // Check checksum + uint16_t checksum = frame_[3] ^ frame_[4] ^ 0x55; + if (checksum != frame_[5]) + return INVALID; + + // Check if we received the full frame + uint16_t led_count = (frame_[3] << 8) + frame_[4] + 1; + auto buffer_size = get_frame_size_(led_count); + if (frame_.size() < buffer_size) + return PARTIAL; + + // Apply lights + auto accepted_led_count = std::min(led_count, it.size()); + uint8_t *led_data = &frame_[6]; + + for (int led = 0; led < accepted_led_count; led++, led_data += 3) { + auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]); + + it[led].set(light::ESPColor(led_data[0], led_data[1], led_data[2], white)); + } + + return CONSUMED; +} + +} // namespace adalight +} // namespace esphome diff --git a/esphome/components/adalight/adalight_light_effect.h b/esphome/components/adalight/adalight_light_effect.h new file mode 100644 index 0000000000..4f77394ebc --- /dev/null +++ b/esphome/components/adalight/adalight_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" +#include "esphome/components/uart/uart.h" + +#include + +namespace esphome { +namespace adalight { + +class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { + public: + AdalightLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + + protected: + enum Frame { + INVALID, + PARTIAL, + CONSUMED, + }; + + int get_frame_size_(int led_count) const; + void reset_frame_(light::AddressableLight &it); + void blank_all_leds_(light::AddressableLight &it); + Frame parse_frame_(light::AddressableLight &it); + + protected: + uint32_t last_ack_{0}; + uint32_t last_byte_{0}; + uint32_t last_reset_{0}; + std::vector frame_; +}; + +} // namespace adalight +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index b6290c0440..a793d74b5c 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -128,14 +128,20 @@ spi: miso_pin: GPIO23 uart: - tx_pin: GPIO22 - rx_pin: GPIO23 - baud_rate: 115200 - id: uart0 - parity: NONE - data_bits: 8 - stop_bits: 1 - rx_buffer_size: 512 + - tx_pin: GPIO22 + rx_pin: GPIO23 + baud_rate: 115200 + id: uart0 + parity: NONE + data_bits: 8 + stop_bits: 1 + rx_buffer_size: 512 + + - id: adalight_uart + tx_pin: GPIO25 + rx_pin: GPIO26 + baud_rate: 115200 + rx_buffer_size: 1024 ota: safe_mode: True @@ -179,6 +185,8 @@ as3935_spi: cs_pin: GPIO12 irq_pin: GPIO13 +adalight: + sensor: - platform: adc pin: A0 @@ -1177,6 +1185,8 @@ light: if (initial_run) { it[0] = current_color; } + - adalight: + uart_id: adalight_uart - automation: name: Custom Effect sequence: diff --git a/tests/test3.yaml b/tests/test3.yaml index fbfc486fe5..fc61c1ae85 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -183,9 +183,13 @@ spi: miso_pin: GPIO14 uart: - tx_pin: GPIO1 - rx_pin: GPIO3 - baud_rate: 115200 + - tx_pin: GPIO1 + rx_pin: GPIO3 + baud_rate: 115200 + + - id: adalight_uart + rx_pin: GPIO3 + baud_rate: 115200 ota: safe_mode: True @@ -203,6 +207,8 @@ deep_sleep: run_duration: 20s sleep_duration: 50s +adalight: + sensor: - platform: apds9960 type: proximity @@ -708,6 +714,8 @@ light: method: ESP8266_UART0 num_leds: 100 effects: + - adalight: + uart_id: adalight_uart - e131: universe: 1 From 0c0dec2534282aba8b63204ebdf294579e1620de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Sat, 13 Jun 2020 01:50:09 +0200 Subject: [PATCH 019/200] Add WLED support (#1092) A component to support [WLED](https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control). This allows to control addressable LEDs over WiFi/UDP, by pushing data right into LEDs. The most useful to use [Prismatik](https://github.com/psieg/Lightpack) to create an immersive effect on PC. It supports all WLED protocols: - WARLS - DRGB - DRGBW - DNRGB - WLED Notifier Co-authored-by: Guillermo Ruffino --- esphome/components/wled/__init__.py | 20 ++ esphome/components/wled/wled_light_effect.cpp | 237 ++++++++++++++++++ esphome/components/wled/wled_light_effect.h | 41 +++ tests/test1.yaml | 7 + tests/test3.yaml | 3 + 5 files changed, 308 insertions(+) create mode 100644 esphome/components/wled/__init__.py create mode 100644 esphome/components/wled/wled_light_effect.cpp create mode 100644 esphome/components/wled/wled_light_effect.h diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py new file mode 100644 index 0000000000..1a248e530f --- /dev/null +++ b/esphome/components/wled/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components.light.types import AddressableLightEffect +from esphome.components.light.effects import register_addressable_effect +from esphome.const import CONF_NAME, CONF_PORT + +wled_ns = cg.esphome_ns.namespace('wled') +WLEDLightEffect = wled_ns.class_('WLEDLightEffect', AddressableLightEffect) + +CONFIG_SCHEMA = cv.Schema({}) + + +@register_addressable_effect('wled', WLEDLightEffect, "WLED", { + cv.Optional(CONF_PORT, default=21324): cv.port, +}) +def wled_light_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_port(config[CONF_PORT])) + + yield effect diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp new file mode 100644 index 0000000000..f161ea15e8 --- /dev/null +++ b/esphome/components/wled/wled_light_effect.cpp @@ -0,0 +1,237 @@ +#include "wled_light_effect.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include +#endif + +#ifdef ARDUINO_ARCH_ESP8266 +#include +#include +#endif + +namespace esphome { +namespace wled { + +// Description of protocols: +// https://github.com/Aircoookie/WLED/wiki/UDP-Realtime-Control +enum Protocol { WLED_NOTIFIER = 0, WARLS = 1, DRGB = 2, DRGBW = 3, DNRGB = 4 }; + +const int DEFAULT_BLANK_TIME = 1000; + +static const char *TAG = "wled_light_effect"; + +WLEDLightEffect::WLEDLightEffect(const std::string &name) : AddressableLightEffect(name) {} + +void WLEDLightEffect::start() { + AddressableLightEffect::start(); + + blank_at_ = 0; +} + +void WLEDLightEffect::stop() { + AddressableLightEffect::stop(); + + if (udp_) { + udp_->stop(); + udp_.reset(); + } +} + +void WLEDLightEffect::blank_all_leds_(light::AddressableLight &it) { + for (int led = it.size(); led-- > 0;) { + it[led].set(light::ESPColor::BLACK); + } +} + +void WLEDLightEffect::apply(light::AddressableLight &it, const light::ESPColor ¤t_color) { + // Init UDP lazily + if (!udp_) { + udp_.reset(new WiFiUDP()); + + if (!udp_->begin(port_)) { + ESP_LOGE(TAG, "Cannot bind WLEDLightEffect to %d.", port_); + } + } + + while (uint16_t packet_size = udp_->parsePacket()) { + std::vector payload; + payload.resize(packet_size); + + if (!udp_->read(&payload[0], payload.size())) { + continue; + } + + if (!this->parse_frame_(it, &payload[0], payload.size())) { + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%c/%d).", payload.size(), payload[0], payload[0]); + continue; + } + } + + if (blank_at_ < millis()) { + blank_all_leds_(it); + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } +} + +bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // At minimum frame needs to have: + // 1b - protocol + // 1b - timeout + if (size < 2) { + return false; + } + + uint8_t protocol = payload[0]; + uint8_t timeout = payload[1]; + + payload += 2; + size -= 2; + + switch (protocol) { + case WLED_NOTIFIER: + if (!parse_notifier_frame_(it, payload, size)) + return false; + break; + + case WARLS: + if (!parse_warls_frame_(it, payload, size)) + return false; + break; + + case DRGB: + if (!parse_drgb_frame_(it, payload, size)) + return false; + break; + + case DRGBW: + if (!parse_drgbw_frame_(it, payload, size)) + return false; + break; + + case DNRGB: + if (!parse_dnrgb_frame_(it, payload, size)) + return false; + break; + + default: + return false; + } + + if (timeout == UINT8_MAX) { + blank_at_ = UINT32_MAX; + } else if (timeout > 0) { + blank_at_ = millis() + timeout * 1000; + } else { + blank_at_ = millis() + DEFAULT_BLANK_TIME; + } + + return true; +} + +bool WLEDLightEffect::parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // Packet needs to be empty + return size == 0; +} + +bool WLEDLightEffect::parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: index, r, g, b + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 4) { + uint8_t led = payload[0]; + uint8_t r = payload[1]; + uint8_t g = payload[2]; + uint8_t b = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 3) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // packet: r, g, b, w + if ((size % 4) != 0) { + return false; + } + + auto count = size / 4; + auto max_leds = it.size(); + + for (uint16_t led = 0; led < count; ++led, payload += 4) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + uint8_t w = payload[3]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b, w)); + } + } + + return true; +} + +bool WLEDLightEffect::parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size) { + // offset: high, low + if (size < 2) { + return false; + } + + uint16_t led = (uint16_t(payload[0]) << 8) + payload[1]; + payload += 2; + size -= 2; + + // packet: r, g, b + if ((size % 3) != 0) { + return false; + } + + auto count = size / 3; + auto max_leds = it.size(); + + for (; count > 0; count--, payload += 3, led++) { + uint8_t r = payload[0]; + uint8_t g = payload[1]; + uint8_t b = payload[2]; + + if (led < max_leds) { + it[led].set(light::ESPColor(r, g, b)); + } + } + + return true; +} + +} // namespace wled +} // namespace esphome diff --git a/esphome/components/wled/wled_light_effect.h b/esphome/components/wled/wled_light_effect.h new file mode 100644 index 0000000000..f1d27b06c7 --- /dev/null +++ b/esphome/components/wled/wled_light_effect.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/light/addressable_light_effect.h" + +#include +#include + +class UDP; + +namespace esphome { +namespace wled { + +class WLEDLightEffect : public light::AddressableLightEffect { + public: + WLEDLightEffect(const std::string &name); + + public: + void start() override; + void stop() override; + void apply(light::AddressableLight &it, const light::ESPColor ¤t_color) override; + void set_port(uint16_t port) { this->port_ = port; } + + protected: + void blank_all_leds_(light::AddressableLight &it); + bool parse_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_notifier_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_warls_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_drgbw_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + bool parse_dnrgb_frame_(light::AddressableLight &it, const uint8_t *payload, uint16_t size); + + protected: + uint16_t port_{0}; + std::unique_ptr udp_; + uint32_t blank_at_{0}; + uint32_t dropped_{0}; +}; + +} // namespace wled +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index a793d74b5c..a6c0152928 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -185,6 +185,8 @@ as3935_spi: cs_pin: GPIO12 irq_pin: GPIO13 +wled: + adalight: sensor: @@ -1185,8 +1187,13 @@ light: if (initial_run) { it[0] = current_color; } + + - wled: + port: 11111 + - adalight: uart_id: adalight_uart + - automation: name: Custom Effect sequence: diff --git a/tests/test3.yaml b/tests/test3.yaml index fc61c1ae85..0344aed6de 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -207,6 +207,8 @@ deep_sleep: run_duration: 20s sleep_duration: 50s +wled: + adalight: sensor: @@ -714,6 +716,7 @@ light: method: ESP8266_UART0 num_leds: 100 effects: + - wled: - adalight: uart_id: adalight_uart - e131: From b65070487735044b80373a4e1f800c41454e756e Mon Sep 17 00:00:00 2001 From: Niklas Wagner Date: Sun, 14 Jun 2020 00:51:43 +0200 Subject: [PATCH 020/200] Update docker base image (#1093) * Upgrade docker base image to 2.1.0 * Upgrade docker base image to 2.1.1 Co-authored-by: Otto Winter --- .gitlab-ci.yml | 4 ++-- docker/Dockerfile | 2 +- docker/Dockerfile.lint | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3db0b982ae..cdb2693f46 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '2.0.1' + BASE_VERSION: '2.1.1' TZ: UTC stages: @@ -33,7 +33,7 @@ stages: - docker info - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" script: - - docker run --rm --privileged multiarch/qemu-user-static:4.1.0-1 --reset -p yes + - docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - TAG="${CI_COMMIT_TAG#v}" - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - echo "Tag ${TAG}" diff --git a/docker/Dockerfile b/docker/Dockerfile index 11bbeeda2b..bcdc5feaf7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.0.1 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.1.1 FROM ${BUILD_FROM} COPY . . diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 2d77502dc2..f04b448142 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.0.1 +FROM esphome/esphome-base-amd64:2.1.1 RUN \ apt-get update \ From 35a2258f123fa25cb80d70fddfbec873a3d02479 Mon Sep 17 00:00:00 2001 From: Christian Ferbar <5595808+ferbar@users.noreply.github.com> Date: Sun, 14 Jun 2020 01:26:41 +0200 Subject: [PATCH 021/200] SenseAir: flush input buffer on read error (#1017) * SenseAir: flush input buffer on read error * SenseAir: flush input buffer on read error - fix typo * SenseAir: flush input buffer on read error - fix formating * SenseAir: flush input buffer on read error - fix formating2 * SenseAir: flush input buffer on read error - fix formating3 * SenseAir: flush input buffer on read error - fix formating4 * Update esphome/components/senseair/senseair.cpp * Update esphome/components/senseair/senseair.cpp Co-authored-by: Guillermo Ruffino --- esphome/components/senseair/senseair.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 8b41a441ad..19831874d7 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -18,8 +18,19 @@ void SenseAirComponent::update() { } if (response[0] != 0xFE || response[1] != 0x04) { - ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + ESP_LOGW(TAG, "Invalid preamble from SenseAir! %02x%02x%02x%02x %02x%02x%02x%02x %02x%02x%02x%02x %02x", + response[0], response[1], response[2], response[3], response[4], response[5], response[6], response[7], + response[8], response[9], response[10], response[11], response[12]); + this->status_set_warning(); + while (this->available()) { + unsigned char b; + if (this->read_byte(&b)) { + ESP_LOGD(TAG, " ... %02x", b); + } else { + ESP_LOGD(TAG, " ... nothing read"); + } + } return; } From 0bb81e5b2d1461ca08917d7d2cb30270ed24609f Mon Sep 17 00:00:00 2001 From: Jim Persson Date: Sun, 14 Jun 2020 21:54:31 +0200 Subject: [PATCH 022/200] Add support for controlling fan direction (#1051) * Fix fan oscillation trait not being used * Add fan direction support to SpeedFan * Add fan direction to API * Add fan direction support to BinaryFan * Fix CI errors * Fix python format * Change some ordering to trigger CI * Add test for the configuration --- esphome/components/api/api.proto | 8 ++++ esphome/components/api/api_connection.cpp | 5 +++ esphome/components/api/api_pb2.cpp | 46 ++++++++++++++++++++ esphome/components/api/api_pb2.h | 30 ++++++++----- esphome/components/binary/fan/__init__.py | 8 +++- esphome/components/binary/fan/binary_fan.cpp | 15 ++++++- esphome/components/binary/fan/binary_fan.h | 2 + esphome/components/fan/fan_state.cpp | 6 +++ esphome/components/fan/fan_state.h | 14 ++++++ esphome/components/fan/fan_traits.h | 8 +++- esphome/components/speed/fan/__init__.py | 7 ++- esphome/components/speed/fan/speed_fan.cpp | 15 ++++++- esphome/components/speed/fan/speed_fan.h | 2 + esphome/components/tuya/fan/tuya_fan.cpp | 2 +- esphome/const.py | 1 + tests/test1.yaml | 4 ++ 16 files changed, 156 insertions(+), 17 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 4bb7d1b555..403242d236 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -301,12 +301,17 @@ message ListEntitiesFanResponse { bool supports_oscillation = 5; bool supports_speed = 6; + bool supports_direction = 7; } enum FanSpeed { FAN_SPEED_LOW = 0; FAN_SPEED_MEDIUM = 1; FAN_SPEED_HIGH = 2; } +enum FanDirection { + FAN_DIRECTION_FORWARD = 0; + FAN_DIRECTION_REVERSE = 1; +} message FanStateResponse { option (id) = 23; option (source) = SOURCE_SERVER; @@ -317,6 +322,7 @@ message FanStateResponse { bool state = 2; bool oscillating = 3; FanSpeed speed = 4; + FanDirection direction = 5; } message FanCommandRequest { option (id) = 31; @@ -331,6 +337,8 @@ message FanCommandRequest { FanSpeed speed = 5; bool has_oscillating = 6; bool oscillating = 7; + bool has_direction = 8; + FanDirection direction = 9; } // ==================== LIGHT ==================== diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index beccf91d27..1956f3119d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -248,6 +248,8 @@ bool APIConnection::send_fan_state(fan::FanState *fan) { resp.oscillating = fan->oscillating; if (traits.supports_speed()) resp.speed = static_cast(fan->speed); + if (traits.supports_direction()) + resp.direction = static_cast(fan->direction); return this->send_fan_state_response(resp); } bool APIConnection::send_fan_info(fan::FanState *fan) { @@ -259,6 +261,7 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { msg.unique_id = get_default_unique_id("fan", fan); msg.supports_oscillation = traits.supports_oscillation(); msg.supports_speed = traits.supports_speed(); + msg.supports_direction = traits.supports_direction(); return this->send_list_entities_fan_response(msg); } void APIConnection::fan_command(const FanCommandRequest &msg) { @@ -273,6 +276,8 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { call.set_oscillating(msg.oscillating); if (msg.has_speed) call.set_speed(static_cast(msg.speed)); + if (msg.has_direction) + call.set_direction(static_cast(msg.direction)); call.perform(); } #endif diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 6b98f95f53..c659561aa8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -52,6 +52,16 @@ template<> const char *proto_enum_to_string(enums::FanSpeed val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(enums::FanDirection value) { + switch (value) { + case enums::FAN_DIRECTION_FORWARD: + return "FAN_DIRECTION_FORWARD"; + case enums::FAN_DIRECTION_REVERSE: + return "FAN_DIRECTION_REVERSE"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::LogLevel value) { switch (value) { case enums::LOG_LEVEL_NONE: @@ -760,6 +770,10 @@ bool ListEntitiesFanResponse::decode_varint(uint32_t field_id, ProtoVarInt value this->supports_speed = value.as_bool(); return true; } + case 7: { + this->supports_direction = value.as_bool(); + return true; + } default: return false; } @@ -799,6 +813,7 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_string(4, this->unique_id); buffer.encode_bool(5, this->supports_oscillation); buffer.encode_bool(6, this->supports_speed); + buffer.encode_bool(7, this->supports_direction); } void ListEntitiesFanResponse::dump_to(std::string &out) const { char buffer[64]; @@ -827,6 +842,10 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const { out.append(" supports_speed: "); out.append(YESNO(this->supports_speed)); out.append("\n"); + + out.append(" supports_direction: "); + out.append(YESNO(this->supports_direction)); + out.append("\n"); out.append("}"); } bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -843,6 +862,10 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->speed = value.as_enum(); return true; } + case 5: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -862,6 +885,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(2, this->state); buffer.encode_bool(3, this->oscillating); buffer.encode_enum(4, this->speed); + buffer.encode_enum(5, this->direction); } void FanStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -882,6 +906,10 @@ void FanStateResponse::dump_to(std::string &out) const { out.append(" speed: "); out.append(proto_enum_to_string(this->speed)); out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -910,6 +938,14 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { this->oscillating = value.as_bool(); return true; } + case 8: { + this->has_direction = value.as_bool(); + return true; + } + case 9: { + this->direction = value.as_enum(); + return true; + } default: return false; } @@ -932,6 +968,8 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_enum(5, this->speed); buffer.encode_bool(6, this->has_oscillating); buffer.encode_bool(7, this->oscillating); + buffer.encode_bool(8, this->has_direction); + buffer.encode_enum(9, this->direction); } void FanCommandRequest::dump_to(std::string &out) const { char buffer[64]; @@ -964,6 +1002,14 @@ void FanCommandRequest::dump_to(std::string &out) const { out.append(" oscillating: "); out.append(YESNO(this->oscillating)); out.append("\n"); + + out.append(" has_direction: "); + out.append(YESNO(this->has_direction)); + out.append("\n"); + + out.append(" direction: "); + out.append(proto_enum_to_string(this->direction)); + out.append("\n"); out.append("}"); } bool ListEntitiesLightResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 8be89f0365..306bdbf5a9 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -28,6 +28,10 @@ enum FanSpeed : uint32_t { FAN_SPEED_MEDIUM = 1, FAN_SPEED_HIGH = 2, }; +enum FanDirection : uint32_t { + FAN_DIRECTION_FORWARD = 0, + FAN_DIRECTION_REVERSE = 1, +}; enum LogLevel : uint32_t { LOG_LEVEL_NONE = 0, LOG_LEVEL_ERROR = 1, @@ -279,6 +283,7 @@ class ListEntitiesFanResponse : public ProtoMessage { std::string unique_id{}; // NOLINT bool supports_oscillation{false}; // NOLINT bool supports_speed{false}; // NOLINT + bool supports_direction{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -289,10 +294,11 @@ class ListEntitiesFanResponse : public ProtoMessage { }; class FanStateResponse : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool state{false}; // NOLINT - bool oscillating{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT + uint32_t key{0}; // NOLINT + bool state{false}; // NOLINT + bool oscillating{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -302,13 +308,15 @@ class FanStateResponse : public ProtoMessage { }; class FanCommandRequest : public ProtoMessage { public: - uint32_t key{0}; // NOLINT - bool has_state{false}; // NOLINT - bool state{false}; // NOLINT - bool has_speed{false}; // NOLINT - enums::FanSpeed speed{}; // NOLINT - bool has_oscillating{false}; // NOLINT - bool oscillating{false}; // NOLINT + uint32_t key{0}; // NOLINT + bool has_state{false}; // NOLINT + bool state{false}; // NOLINT + bool has_speed{false}; // NOLINT + enums::FanSpeed speed{}; // NOLINT + bool has_oscillating{false}; // NOLINT + bool oscillating{false}; // NOLINT + bool has_direction{false}; // NOLINT + enums::FanDirection direction{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/binary/fan/__init__.py b/esphome/components/binary/fan/__init__.py index dbfe1a8286..6969c1dbbf 100644 --- a/esphome/components/binary/fan/__init__.py +++ b/esphome/components/binary/fan/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_OUTPUT_ID +from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, \ + CONF_OUTPUT, CONF_OUTPUT_ID from .. import binary_ns BinaryFan = binary_ns.class_('BinaryFan', cg.Component) @@ -9,6 +10,7 @@ BinaryFan = binary_ns.class_('BinaryFan', cg.Component) CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), }).extend(cv.COMPONENT_SCHEMA) @@ -25,3 +27,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/binary/fan/binary_fan.cpp b/esphome/components/binary/fan/binary_fan.cpp index 986902efe5..5fd1867e7f 100644 --- a/esphome/components/binary/fan/binary_fan.cpp +++ b/esphome/components/binary/fan/binary_fan.cpp @@ -11,9 +11,12 @@ void binary::BinaryFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void BinaryFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, false); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, false, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -41,6 +44,16 @@ void BinaryFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float BinaryFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/binary/fan/binary_fan.h b/esphome/components/binary/fan/binary_fan.h index 980d2629f6..93294b8dee 100644 --- a/esphome/components/binary/fan/binary_fan.h +++ b/esphome/components/binary/fan/binary_fan.h @@ -16,11 +16,13 @@ class BinaryFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } protected: fan::FanState *fan_; output::BinaryOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; bool next_update_{true}; }; diff --git a/esphome/components/fan/fan_state.cpp b/esphome/components/fan/fan_state.cpp index af170a755c..ae58b04150 100644 --- a/esphome/components/fan/fan_state.cpp +++ b/esphome/components/fan/fan_state.cpp @@ -22,6 +22,7 @@ struct FanStateRTCState { bool state; FanSpeed speed; bool oscillating; + FanDirection direction; }; void FanState::setup() { @@ -34,6 +35,7 @@ void FanState::setup() { call.set_state(recovered.state); call.set_speed(recovered.speed); call.set_oscillating(recovered.oscillating); + call.set_direction(recovered.direction); call.perform(); } float FanState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } @@ -46,6 +48,9 @@ void FanStateCall::perform() const { if (this->oscillating_.has_value()) { this->state_->oscillating = *this->oscillating_; } + if (this->direction_.has_value()) { + this->state_->direction = *this->direction_; + } if (this->speed_.has_value()) { switch (*this->speed_) { case FAN_SPEED_LOW: @@ -63,6 +68,7 @@ void FanStateCall::perform() const { saved.state = this->state_->state; saved.speed = this->state_->speed; saved.oscillating = this->state_->oscillating; + saved.direction = this->state_->direction; this->state_->rtc_.save(&saved); this->state_->state_callback_.call(); diff --git a/esphome/components/fan/fan_state.h b/esphome/components/fan/fan_state.h index 4e937c68bd..7ab8337e94 100644 --- a/esphome/components/fan/fan_state.h +++ b/esphome/components/fan/fan_state.h @@ -15,6 +15,9 @@ enum FanSpeed { FAN_SPEED_HIGH = 2 ///< The fan is running on high/full speed. }; +/// Simple enum to represent the direction of a fan +enum FanDirection { FAN_DIRECTION_FORWARD = 0, FAN_DIRECTION_REVERSE = 1 }; + class FanState; class FanStateCall { @@ -46,6 +49,14 @@ class FanStateCall { return *this; } FanStateCall &set_speed(const char *speed); + FanStateCall &set_direction(FanDirection direction) { + this->direction_ = direction; + return *this; + } + FanStateCall &set_direction(optional direction) { + this->direction_ = direction; + return *this; + } void perform() const; @@ -54,6 +65,7 @@ class FanStateCall { optional binary_state_; optional oscillating_{}; optional speed_{}; + optional direction_{}; }; class FanState : public Nameable, public Component { @@ -76,6 +88,8 @@ class FanState : public Nameable, public Component { bool oscillating{false}; /// The current fan speed. FanSpeed speed{FAN_SPEED_HIGH}; + /// The current direction of the fan + FanDirection direction{FAN_DIRECTION_FORWARD}; FanStateCall turn_on(); FanStateCall turn_off(); diff --git a/esphome/components/fan/fan_traits.h b/esphome/components/fan/fan_traits.h index c46adbf013..75663484c5 100644 --- a/esphome/components/fan/fan_traits.h +++ b/esphome/components/fan/fan_traits.h @@ -6,7 +6,8 @@ namespace fan { class FanTraits { public: FanTraits() = default; - FanTraits(bool oscillation, bool speed) : oscillation_(oscillation), speed_(speed) {} + FanTraits(bool oscillation, bool speed, bool direction) + : oscillation_(oscillation), speed_(speed), direction_(direction) {} /// Return if this fan supports oscillation. bool supports_oscillation() const { return this->oscillation_; } @@ -16,10 +17,15 @@ class FanTraits { bool supports_speed() const { return this->speed_; } /// Set whether this fan supports speed modes. void set_speed(bool speed) { this->speed_ = speed; } + /// Return if this fan supports changing direction + bool supports_direction() const { return this->direction_; } + /// Set whether this fan supports changing direction + void set_direction(bool direction) { this->direction_ = direction; } protected: bool oscillation_{false}; bool speed_{false}; + bool direction_{false}; }; } // namespace fan diff --git a/esphome/components/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index 65ee5960f0..420c957d87 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import fan, output -from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, \ +from esphome.const import CONF_OSCILLATION_OUTPUT, CONF_OUTPUT, CONF_DIRECTION_OUTPUT, \ CONF_OUTPUT_ID, CONF_SPEED, CONF_LOW, CONF_MEDIUM, CONF_HIGH from .. import speed_ns @@ -11,6 +11,7 @@ CONFIG_SCHEMA = fan.FAN_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(SpeedFan), cv.Required(CONF_OUTPUT): cv.use_id(output.FloatOutput), cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), + cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_SPEED, default={}): cv.Schema({ cv.Optional(CONF_LOW, default=0.33): cv.percentage, cv.Optional(CONF_MEDIUM, default=0.66): cv.percentage, @@ -30,3 +31,7 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) cg.add(var.set_oscillating(oscillation_output)) + + if CONF_DIRECTION_OUTPUT in config: + direction_output = yield cg.get_variable(config[CONF_DIRECTION_OUTPUT]) + cg.add(var.set_direction(direction_output)) diff --git a/esphome/components/speed/fan/speed_fan.cpp b/esphome/components/speed/fan/speed_fan.cpp index 3bfbc1fc3c..45117d64c3 100644 --- a/esphome/components/speed/fan/speed_fan.cpp +++ b/esphome/components/speed/fan/speed_fan.cpp @@ -11,9 +11,12 @@ void SpeedFan::dump_config() { if (this->fan_->get_traits().supports_oscillation()) { ESP_LOGCONFIG(TAG, " Oscillation: YES"); } + if (this->fan_->get_traits().supports_direction()) { + ESP_LOGCONFIG(TAG, " Direction: YES"); + } } void SpeedFan::setup() { - auto traits = fan::FanTraits(this->oscillating_ != nullptr, true); + auto traits = fan::FanTraits(this->oscillating_ != nullptr, true, this->direction_ != nullptr); this->fan_->set_traits(traits); this->fan_->add_on_state_callback([this]() { this->next_update_ = true; }); } @@ -46,6 +49,16 @@ void SpeedFan::loop() { } ESP_LOGD(TAG, "Setting oscillation: %s", ONOFF(enable)); } + + if (this->direction_ != nullptr) { + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + if (enable) { + this->direction_->turn_on(); + } else { + this->direction_->turn_off(); + } + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } } float SpeedFan::get_setup_priority() const { return setup_priority::DATA; } diff --git a/esphome/components/speed/fan/speed_fan.h b/esphome/components/speed/fan/speed_fan.h index 74910bda94..cce9d07544 100644 --- a/esphome/components/speed/fan/speed_fan.h +++ b/esphome/components/speed/fan/speed_fan.h @@ -16,6 +16,7 @@ class SpeedFan : public Component { void dump_config() override; float get_setup_priority() const override; void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } + void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } void set_speeds(float low, float medium, float high) { this->low_speed_ = low; this->medium_speed_ = medium; @@ -26,6 +27,7 @@ class SpeedFan : public Component { fan::FanState *fan_; output::FloatOutput *output_; output::BinaryOutput *oscillating_{nullptr}; + output::BinaryOutput *direction_{nullptr}; float low_speed_{}; float medium_speed_{}; float high_speed_{}; diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index b9fe2c0829..9850fa65ed 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -7,7 +7,7 @@ namespace tuya { static const char *TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value()); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { diff --git a/esphome/const.py b/esphome/const.py index 5ac2b3c9ca..4226e25afa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -130,6 +130,7 @@ CONF_DIMENSIONS = 'dimensions' CONF_DIO_PIN = 'dio_pin' CONF_DIR_PIN = 'dir_pin' CONF_DIRECTION = 'direction' +CONF_DIRECTION_OUTPUT = 'direction_output' CONF_DISCOVERY = 'discovery' CONF_DISCOVERY_PREFIX = 'discovery_prefix' CONF_DISCOVERY_RETAIN = 'discovery_retain' diff --git a/tests/test1.yaml b/tests/test1.yaml index a6c0152928..bd44ff6e63 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1467,9 +1467,13 @@ fan: - platform: binary output: gpio_26 name: "Living Room Fan 1" + oscillation_output: gpio_19 + direction_output: gpio_26 - platform: speed output: pca_6 name: "Living Room Fan 2" + oscillation_output: gpio_19 + direction_output: gpio_26 speed: low: 0.45 medium: 0.75 From 35078fd52f752db26cdffcb3c827c56564db7ede Mon Sep 17 00:00:00 2001 From: Jeff Rescignano Date: Sun, 14 Jun 2020 21:41:02 -0400 Subject: [PATCH 023/200] Add API component to logging error message (#1062) * Add API component to logging error message * lint Co-authored-by: Guillermo Ruffino --- esphome/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 73723dfa00..4059024531 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -33,8 +33,8 @@ def get_serial_ports(): def choose_prompt(options): if not options: raise EsphomeError("Found no valid options for upload/logging, please make sure relevant " - "sections (ota, mqtt, ...) are in your configuration and/or the device " - "is plugged in.") + "sections (ota, api, mqtt, ...) are in your configuration and/or the " + "device is plugged in.") if len(options) == 1: return options[0][1] From 9632ac0e029d09399aca2eb425db3a155845ff08 Mon Sep 17 00:00:00 2001 From: SenexCrenshaw <35600301+SenexCrenshaw@users.noreply.github.com> Date: Sun, 14 Jun 2020 21:42:46 -0400 Subject: [PATCH 024/200] Add MCP3008 I/O Expander (#1057) * Add MCP3008 * Fixed Travis build issues * Travis Build Fix * Fix Travis foramt issues * Travis fixes * Code review changes * Fix bit shift. off by one and selecting wrong pin * general fixes * lint Co-authored-by: Guillermo Ruffino --- esphome/components/mcp3008/__init__.py | 23 ++++++++++++ esphome/components/mcp3008/mcp3008.cpp | 52 ++++++++++++++++++++++++++ esphome/components/mcp3008/mcp3008.h | 42 +++++++++++++++++++++ esphome/components/mcp3008/sensor.py | 23 ++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 esphome/components/mcp3008/__init__.py create mode 100644 esphome/components/mcp3008/mcp3008.cpp create mode 100644 esphome/components/mcp3008/mcp3008.h create mode 100644 esphome/components/mcp3008/sensor.py diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py new file mode 100644 index 0000000000..2196b96a92 --- /dev/null +++ b/esphome/components/mcp3008/__init__.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import spi +from esphome.const import CONF_ID + +DEPENDENCIES = ['spi'] +AUTO_LOAD = ['sensor'] +MULTI_CONF = True + +CONF_MCP3008 = 'mcp3008' + +mcp3008_ns = cg.esphome_ns.namespace('mcp3008') +MCP3008 = mcp3008_ns.class_('MCP3008', cg.Component, spi.SPIDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(MCP3008), +}).extend(spi.spi_device_schema(CS_PIN_required=True)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp new file mode 100644 index 0000000000..70484e8818 --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -0,0 +1,52 @@ +#include "mcp3008.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp3008 { + +static const char *TAG = "mcp3008"; + +float MCP3008::get_setup_priority() const { return setup_priority::HARDWARE; } + +void MCP3008::setup() { + ESP_LOGCONFIG(TAG, "Setting up mcp3008"); + this->spi_setup(); +} + +void MCP3008::dump_config() { + ESP_LOGCONFIG(TAG, "MCP3008:"); + LOG_PIN(" CS Pin: ", this->cs_); +} + +float MCP3008::read_data_(uint8_t pin) { + byte data_msb = 0; + byte data_lsb = 0; + + byte command = ((0x01 << 7) | // start bit + ((pin & 0x07) << 4)); // channel number + + this->enable(); + + this->transfer_byte(0x01); + data_msb = this->transfer_byte(command) & 0x03; + data_lsb = this->transfer_byte(0x00); + + this->disable(); + + int data = data_msb << 8 | data_lsb; + + return data / 1024.0f; +} + +MCP3008Sensor::MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin) + : PollingComponent(1000), parent_(parent), pin_(pin) { + this->set_name(name); +} +void MCP3008Sensor::setup() { LOG_SENSOR("", "Setting up MCP3008 Sensor '%s'...", this); } +void MCP3008Sensor::update() { + float value_v = this->parent_->read_data_(pin_); + this->publish_state(value_v); +} + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/mcp3008.h b/esphome/components/mcp3008/mcp3008.h new file mode 100644 index 0000000000..594bdc4204 --- /dev/null +++ b/esphome/components/mcp3008/mcp3008.h @@ -0,0 +1,42 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace mcp3008 { + +class MCP3008Sensor; + +class MCP3008 : public Component, + public spi::SPIDevice { // At 3.3V 2MHz is too fast 1.35MHz is about right + public: + MCP3008() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + + protected: + float read_data_(uint8_t pin); + + friend class MCP3008Sensor; +}; + +class MCP3008Sensor : public PollingComponent, public sensor::Sensor { + public: + MCP3008Sensor(MCP3008 *parent, std::string name, uint8_t pin); + + void setup() override; + void update() override; + + protected: + MCP3008 *parent_; + uint8_t pin_; +}; + +} // namespace mcp3008 +} // namespace esphome diff --git a/esphome/components/mcp3008/sensor.py b/esphome/components/mcp3008/sensor.py new file mode 100644 index 0000000000..ec7df0429a --- /dev/null +++ b/esphome/components/mcp3008/sensor.py @@ -0,0 +1,23 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_NUMBER, CONF_NAME +from . import mcp3008_ns, MCP3008 + +DEPENDENCIES = ['mcp3008'] + +MCP3008Sensor = mcp3008_ns.class_('MCP3008Sensor', sensor.Sensor, cg.PollingComponent) + +CONF_MCP3008_ID = 'mcp3008_id' + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MCP3008Sensor), + cv.GenerateID(CONF_MCP3008_ID): cv.use_id(MCP3008), + cv.Required(CONF_NUMBER): cv.int_, +}).extend(cv.polling_component_schema('1s')) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_MCP3008_ID]) + var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_NAME], config[CONF_NUMBER]) + yield cg.register_component(var, config) From 4fb750de439f306907671c8f70de026b36d3a359 Mon Sep 17 00:00:00 2001 From: rnauber <7414650+rnauber@users.noreply.github.com> Date: Mon, 15 Jun 2020 05:37:15 +0200 Subject: [PATCH 025/200] ADE7953: Fix dereferencing of a null pointer (#1086) * ADE7953: Fix dereferencing of a null pointer The ade7953 driver dereferences a null pointer, when not all of its sensors are used. This gives an exception like: Fatal exception:29 flag:2 (EXCEPTION) epc1:0x4020c241 epc2:0x00000000 epc3:0x00000000 excvaddr:0x00000018 depc:0x00000000 * Fix formatting Co-authored-by: olg --- esphome/components/ade7953/ade7953.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp index 9316d9cad0..c4752abf39 100644 --- a/esphome/components/ade7953/ade7953.cpp +++ b/esphome/components/ade7953/ade7953.cpp @@ -18,7 +18,7 @@ void ADE7953::dump_config() { } #define ADE_PUBLISH_(name, factor) \ - if (name) { \ + if (name && this->name##_sensor_) { \ float value = *name / factor; \ this->name##_sensor_->publish_state(value); \ } From d0004409278cd7e63b5bc0a8e1b9abad0308327d Mon Sep 17 00:00:00 2001 From: korellas Date: Mon, 15 Jun 2020 12:41:17 +0900 Subject: [PATCH 026/200] sgp30 baseline write bug (#1157) (#1078) --- esphome/components/sgp30/sgp30.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp index 8c148b8e83..1ef43c1c73 100644 --- a/esphome/components/sgp30/sgp30.cpp +++ b/esphome/components/sgp30/sgp30.cpp @@ -162,11 +162,11 @@ void SGP30Component::send_env_data_() { void SGP30Component::write_iaq_baseline_(uint16_t eco2_baseline, uint16_t tvoc_baseline) { uint8_t data[7]; data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; - data[1] = eco2_baseline >> 8; - data[2] = eco2_baseline & 0xFF; + data[1] = tvoc_baseline >> 8; + data[2] = tvoc_baseline & 0xFF; data[3] = sht_crc_(data[1], data[2]); - data[4] = tvoc_baseline >> 8; - data[5] = tvoc_baseline & 0xFF; + data[4] = eco2_baseline >> 8; + data[5] = eco2_baseline & 0xFF; data[6] = sht_crc_(data[4], data[5]); if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 7)) { ESP_LOGE(TAG, "Error applying eCO2 baseline: 0x%04X, TVOC baseline: 0x%04X", eco2_baseline, tvoc_baseline); From 3fd130869e81d962c4aebf3b91064d82727bffb5 Mon Sep 17 00:00:00 2001 From: MasterTim17 <52961929+MasterTim17@users.noreply.github.com> Date: Tue, 16 Jun 2020 02:52:47 +0200 Subject: [PATCH 027/200] fix for ESP32 'Association Leave' (#1081) --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 40f12a8adc..d56e75a070 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -98,6 +98,7 @@ void WiFiComponent::loop() { case WIFI_COMPONENT_STATE_STA_CONNECTED: { if (!this->is_connected()) { ESP_LOGW(TAG, "WiFi Connection lost... Reconnecting..."); + this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; this->retry_connect(); } else { this->status_clear_warning(); From 0082c5b4597dbbe75237a14f471beefc6ee86352 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 17 Jun 2020 18:16:42 -0500 Subject: [PATCH 028/200] Climate bang bang enhancements (#1061) * Add fan support to bang bang controller * Fix heat-only and cool-only modes * Added support for all climate actions and modes * Fix up some comments * Fan_only mode behavior tweak * Updated test * Updated test, take 2 * Add accessor methods for easier use in lambadas * Revert "Add accessor methods for easier use in lambadas" This reverts commit f15713129cd5587242a08ad1e2ee37cbf075aa03. Don't code late at night when sleepy. * Fix single/dual setpoint support * Linted * Properly implement single-point, validation improvements * Another quick fix...sigh * Update tests * Update tests, take 2 * Revert "Update tests, take 2" This reverts commit 71bb7fccc53d95a7d221e80ea0a931fb3cfd9a0c. Nope. * Revert "Update tests" This reverts commit 42044291efaaf09858f29ab27bbc3aa9e8b80ee9. Nope 2. * Updated/fixed tests * Revert "Updated/fixed tests" This reverts commit a1a5b1c7db9a240b9da668b6e48abe1dd7910777. Taking a different approach. * Revert "Another quick fix...sigh" This reverts commit 6b1955724a2de3c2d1534ec206dd5794bbb5f568. Taking a different approach. * Revert "Properly implement single-point, validation improvements" This reverts commit f5bfff099e1dc58edd3898da8655c147adde2301. Taking a different approach. * Single/dual set point fixes, full validation * Fix test * Fix restore-on-boot, clean up comments * Decouple two-point/auto operation, more polish * One last away mode tweak * Added set point validation * Clean up over-publishing, always call action after boot * Add fan support to bang bang controller * Fix heat-only and cool-only modes * Added support for all climate actions and modes * Fix up some comments * Fan_only mode behavior tweak * Updated test * Updated test, take 2 * Add accessor methods for easier use in lambadas * Revert "Add accessor methods for easier use in lambadas" This reverts commit f15713129cd5587242a08ad1e2ee37cbf075aa03. Don't code late at night when sleepy. * Fix single/dual setpoint support * Linted * Properly implement single-point, validation improvements * Another quick fix...sigh * Update tests * Update tests, take 2 * Revert "Update tests, take 2" This reverts commit 71bb7fccc53d95a7d221e80ea0a931fb3cfd9a0c. Nope. * Revert "Update tests" This reverts commit 42044291efaaf09858f29ab27bbc3aa9e8b80ee9. Nope 2. * Updated/fixed tests * Revert "Updated/fixed tests" This reverts commit a1a5b1c7db9a240b9da668b6e48abe1dd7910777. Taking a different approach. * Revert "Another quick fix...sigh" This reverts commit 6b1955724a2de3c2d1534ec206dd5794bbb5f568. Taking a different approach. * Revert "Properly implement single-point, validation improvements" This reverts commit f5bfff099e1dc58edd3898da8655c147adde2301. Taking a different approach. * Single/dual set point fixes, full validation * Fix test * Fix restore-on-boot, clean up comments * Decouple two-point/auto operation, more polish * One last away mode tweak * Added set point validation * Clean up over-publishing, always call action after boot * Added refresh() function to allow easy on-device state refreshing --- .../bang_bang/bang_bang_climate.cpp | 520 +++++++++++++++--- .../components/bang_bang/bang_bang_climate.h | 252 +++++++-- esphome/components/bang_bang/climate.py | 235 +++++++- esphome/const.py | 22 + tests/test3.yaml | 43 ++ 5 files changed, 947 insertions(+), 125 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index cf527988fe..29f08fbd89 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -9,136 +9,393 @@ static const char *TAG = "bang_bang.climate"; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; - // control may have changed, recompute - this->compute_state_(); - // current temperature changed, publish state + // required action may have changed, recompute, refresh + this->switch_to_action_(compute_action_()); + // current temperature and possibly action changed, so publish the new state this->publish_state(); }); this->current_temperature = this->sensor_->state; - // restore set points + // restore all climate data, if possible auto restore = this->restore_state_(); if (restore.has_value()) { restore->to_call(this).perform(); } else { - // restore from defaults, change_away handles those for us + // restore from defaults, change_away handles temps for us this->mode = climate::CLIMATE_MODE_AUTO; this->change_away_(false); } + // refresh the climate action based on the restored settings + this->switch_to_action_(compute_action_()); + this->setup_complete_ = true; + this->publish_state(); +} +void BangBangClimate::refresh() { + this->switch_to_mode_(this->mode); + this->switch_to_action_(compute_action_()); + this->switch_to_fan_mode_(this->fan_mode); + this->switch_to_swing_mode_(this->swing_mode); + this->publish_state(); } void BangBangClimate::control(const climate::ClimateCall &call) { if (call.get_mode().has_value()) this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); if (call.get_target_temperature_low().has_value()) this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) - this->change_away_(*call.get_away()); - - this->compute_state_(); - this->publish_state(); + if (call.get_away().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_away()); + } else { + this->away = *call.get_away(); + } + } + // set point validation + if (this->supports_two_points_) { + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + if (this->target_temperature_high < this->target_temperature_low) + this->target_temperature_high = this->target_temperature_low; + } else { + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } + // make any changes happen + refresh(); } climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(true); + traits.set_supports_auto_mode(this->supports_auto_); traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(true); + traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); + traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); + traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); + traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); + traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); + traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); + traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); + traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); + traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); + traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); + traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); + traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); + traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); traits.set_supports_away(this->supports_away_); traits.set_supports_action(true); return traits; } -void BangBangClimate::compute_state_() { - if (this->mode != climate::CLIMATE_MODE_AUTO) { - // in non-auto mode, switch directly to appropriate action - // - HEAT mode -> HEATING action - // - COOL mode -> COOLING action - // - OFF mode -> OFF action (not IDLE!) - this->switch_to_action_(static_cast(this->mode)); - return; - } - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { - // if any control parameters are nan, go to OFF action (not IDLE!) - this->switch_to_action_(climate::CLIMATE_ACTION_OFF); - return; - } - const bool too_cold = this->current_temperature < this->target_temperature_low; - const bool too_hot = this->current_temperature > this->target_temperature_high; +climate::ClimateAction BangBangClimate::compute_action_() { + climate::ClimateAction target_action = this->action; + if (this->supports_two_points_) { + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || + isnan(this->target_temperature_high) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; - climate::ClimateAction target_action; - if (too_cold) { - // too cold -> enable heating if possible, else idle - if (this->supports_heat_) - target_action = climate::CLIMATE_ACTION_HEATING; - else - target_action = climate::CLIMATE_ACTION_IDLE; - } else if (too_hot) { - // too hot -> enable cooling if possible, else idle - if (this->supports_cool_) - target_action = climate::CLIMATE_ACTION_COOLING; - else + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature_low - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } } else { - // neither too hot nor too cold -> in range - if (this->supports_cool_ && this->supports_heat_) { - // if supports both ends, go to idle action + if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { target_action = climate::CLIMATE_ACTION_IDLE; - } else { - // else use current mode and don't change (hysteresis) - target_action = this->action; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_COOL: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + case climate::CLIMATE_MODE_HEAT: + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; } } + // do not switch to an action that isn't enabled per the active climate mode + if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) + target_action = climate::CLIMATE_ACTION_IDLE; + if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) + target_action = climate::CLIMATE_ACTION_IDLE; - this->switch_to_action_(target_action); + return target_action; } void BangBangClimate::switch_to_action_(climate::ClimateAction action) { - if (action == this->action) + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->action) && this->setup_complete_) // already in target mode return; - if ((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || - (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) { + if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && + this->setup_complete_) { // switching from OFF to IDLE or vice-versa // these only have visual difference. OFF means user manually disabled, // IDLE means it's in auto mode but value is in target range. this->action = action; - this->publish_state(); return; } - if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); - this->prev_trigger_ = nullptr; + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop(); + this->prev_action_trigger_ = nullptr; } - Trigger<> *trig; + Trigger<> *trig = this->idle_action_trigger_; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - trig = this->idle_trigger_; + // trig = this->idle_action_trigger_; break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_trigger_; + trig = this->cool_action_trigger_; break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_trigger_; + trig = this->heat_action_trigger_; + break; + case climate::CLIMATE_ACTION_FAN: + trig = this->fan_only_action_trigger_; + break; + case climate::CLIMATE_ACTION_DRYING: + trig = this->dry_action_trigger_; break; default: - trig = nullptr; + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; } assert(trig != nullptr); trig->trigger(); this->action = action; - this->prev_trigger_ = trig; - this->publish_state(); + this->prev_action_trigger_ = trig; +} +void BangBangClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop(); + this->prev_fan_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; +} +void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((mode == this->prev_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_AUTO: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + mode = climate::CLIMATE_MODE_AUTO; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->mode = mode; + this->prev_mode_ = mode; + this->prev_mode_trigger_ = trig; +} +void BangBangClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->swing_mode = swing_mode; + this->prev_swing_mode_ = swing_mode; + this->prev_swing_mode_trigger_ = trig; } void BangBangClimate::change_away_(bool away) { if (!away) { - this->target_temperature_low = this->normal_config_.default_temperature_low; - this->target_temperature_high = this->normal_config_.default_temperature_high; + if (this->supports_two_points_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; } else { - this->target_temperature_low = this->away_config_.default_temperature_low; - this->target_temperature_high = this->away_config_.default_temperature_high; + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; } this->away = away; } @@ -150,23 +407,142 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa this->away_config_ = away_config; } BangBangClimate::BangBangClimate() - : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} + : cool_action_trigger_(new Trigger<>()), + cool_mode_trigger_(new Trigger<>()), + dry_action_trigger_(new Trigger<>()), + dry_mode_trigger_(new Trigger<>()), + heat_action_trigger_(new Trigger<>()), + heat_mode_trigger_(new Trigger<>()), + auto_mode_trigger_(new Trigger<>()), + idle_action_trigger_(new Trigger<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + swing_mode_vertical_trigger_(new Trigger<>()) {} +void BangBangClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } -Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } -Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } +void BangBangClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } -Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } +void BangBangClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } +void BangBangClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void BangBangClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void BangBangClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void BangBangClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void BangBangClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void BangBangClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void BangBangClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void BangBangClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void BangBangClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void BangBangClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void BangBangClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void BangBangClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void BangBangClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void BangBangClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void BangBangClimate::set_supports_two_points(bool supports_two_points) { + this->supports_two_points_ = supports_two_points; +} +Trigger<> *BangBangClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *BangBangClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } +Trigger<> *BangBangClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } +Trigger<> *BangBangClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *BangBangClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } +Trigger<> *BangBangClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } +Trigger<> *BangBangClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } +Trigger<> *BangBangClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *BangBangClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *BangBangClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *BangBangClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *BangBangClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } +Trigger<> *BangBangClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } void BangBangClimate::dump_config() { LOG_CLIMATE("", "Bang Bang Climate", this); - ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + if (this->supports_away_) { + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + } } BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; +BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature) + : default_temperature(default_temperature) {} BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 84bcd51f34..fd2b0ce43a 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -11,10 +11,13 @@ namespace bang_bang { struct BangBangClimateTargetTempConfig { public: BangBangClimateTargetTempConfig(); + BangBangClimateTargetTempConfig(float default_temperature); BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; + float hysteresis{NAN}; }; class BangBangClimate : public climate::Climate, public Component { @@ -23,62 +26,243 @@ class BangBangClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; + void set_hysteresis(float hysteresis); void set_sensor(sensor::Sensor *sensor); - Trigger<> *get_idle_trigger() const; - Trigger<> *get_cool_trigger() const; + void set_supports_auto(bool supports_auto); void set_supports_cool(bool supports_cool); - Trigger<> *get_heat_trigger() const; + void set_supports_dry(bool supports_dry); + void set_supports_fan_only(bool supports_fan_only); void set_supports_heat(bool supports_heat); + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); + Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_dry_action_trigger() const; + Trigger<> *get_fan_only_action_trigger() const; + Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_idle_action_trigger() const; + Trigger<> *get_auto_mode_trigger() const; + Trigger<> *get_cool_mode_trigger() const; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + /// Call triggers based on updated climate states (modes/actions) + void refresh(); + protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; + /// Change the away setting, will reset target temperatures to defaults. void change_away_(bool away); + /// Return the traits of this controller. climate::ClimateTraits traits() override; - /// Re-compute the state of this climate controller. - void compute_state_(); + /// Re-compute the required action of this climate controller. + climate::ClimateAction compute_action_(); + + /// Switch the climate device to the given climate action. + void switch_to_action_(climate::ClimateAction action); + + /// Switch the climate device to the given climate fan mode. + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); /// Switch the climate device to the given climate mode. - void switch_to_action_(climate::ClimateAction action); + void switch_to_mode_(climate::ClimateMode mode); + + /// Switch the climate device to the given climate swing mode. + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; - /** The trigger to call when the controller should switch to idle mode. - * - * In idle mode, the controller is assumed to have both heating and cooling disabled. - */ - Trigger<> *idle_trigger_; - /** The trigger to call when the controller should switch to cooling mode. - */ - Trigger<> *cool_trigger_; - /** Whether the controller supports cooling. - * - * A false value for this attribute means that the controller has no cooling action - * (for example a thermostat, where only heating and not-heating is possible). - */ - bool supports_cool_{false}; - /** The trigger to call when the controller should switch to heating mode. - * - * A null value for this attribute means that the controller has no heating action - * For example window blinds, where only cooling (blinds closed) and not-cooling - * (blinds open) is possible. - */ - Trigger<> *heat_trigger_{nullptr}; - bool supports_heat_{false}; - /** A reference to the trigger that was previously active. - * - * This is so that the previous trigger can be stopped before enabling a new one. - */ - Trigger<> *prev_trigger_{nullptr}; - BangBangClimateTargetTempConfig normal_config_{}; + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// A false value for any given attribute means that the controller has no such action + /// (for example a thermostat, where only heating and not-heating is possible). + bool supports_auto_{false}; + bool supports_cool_{false}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + bool supports_heat_{false}; + + /// Whether the controller supports turning on or off just the fan. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// A false value means that the controller has no such mode. bool supports_away_{false}; + + /// The trigger to call when the controller should switch to cooling action/mode. + /// + /// A null value for this attribute means that the controller has no cooling action + /// For example electric heat, where only heating (power on) and not-heating + /// (power off) is possible. + Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *cool_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to dry (dehumidification) mode. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/mode. + /// + /// A null value for this attribute means that the controller has no heating action + /// For example window blinds, where only cooling (blinds closed) and not-cooling + /// (blinds open) is possible. + Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *heat_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to auto mode. + /// + /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "vertical". + Trigger<> *swing_mode_vertical_trigger_{nullptr}; + + /// A reference to the trigger that was previously active. + /// + /// This is so that the previous trigger can be stopped before enabling a new one + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// Temperature data for normal/home and away modes + BangBangClimateTargetTempConfig normal_config_{}; BangBangClimateTargetTempConfig away_config_{}; + + /// Hysteresis value used for computing climate actions + float hysteresis_{0}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; }; } // namespace bang_bang diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 4ef811c55d..75ec58404b 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -2,27 +2,107 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor -from esphome.const import CONF_AWAY_CONFIG, CONF_COOL_ACTION, \ - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, \ - CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR +from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ + CONF_DRY_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, \ + CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, CONF_FAN_MODE_HIGH_ACTION, \ + CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, \ + CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ + CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ + CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION bang_bang_ns = cg.esphome_ns.namespace('bang_bang') BangBangClimate = bang_bang_ns.class_('BangBangClimate', climate.Climate, cg.Component) BangBangClimateTargetTempConfig = bang_bang_ns.struct('BangBangClimateTargetTempConfig') + +def validate_bangbang(config): + # verify corresponding climate action action exists for any defined climate mode action + if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) + if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) + if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_MODE)) + if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config + or CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config + and CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined in away configuration when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and + CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined in away configuration with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + return config + + CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, }), -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION)) +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, + CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), + validate_bangbang) def to_code(config): @@ -30,28 +110,145 @@ def to_code(config): yield cg.register_component(var, config) yield climate.register_climate(var, config) + auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + two_points_available = CONF_HEAT_ACTION in config and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config) + sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) - normal_config = BangBangClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) + if two_points_available is True: + cg.add(var.set_supports_two_points(True)) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) cg.add(var.set_normal_config(normal_config)) - yield automation.build_automation(var.get_idle_trigger(), [], config[CONF_IDLE_ACTION]) + yield automation.build_automation(var.get_idle_action_trigger(), [], + config[CONF_IDLE_ACTION]) + + if auto_mode_available is True: + cg.add(var.set_supports_auto(True)) + else: + cg.add(var.set_supports_auto(False)) if CONF_COOL_ACTION in config: - yield automation.build_automation(var.get_cool_trigger(), [], config[CONF_COOL_ACTION]) + yield automation.build_automation(var.get_cool_action_trigger(), [], + config[CONF_COOL_ACTION]) cg.add(var.set_supports_cool(True)) + if CONF_DRY_ACTION in config: + yield automation.build_automation(var.get_dry_action_trigger(), [], + config[CONF_DRY_ACTION]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + yield automation.build_automation(var.get_fan_only_action_trigger(), [], + config[CONF_FAN_ONLY_ACTION]) + cg.add(var.set_supports_fan_only(True)) if CONF_HEAT_ACTION in config: - yield automation.build_automation(var.get_heat_trigger(), [], config[CONF_HEAT_ACTION]) + yield automation.build_automation(var.get_heat_action_trigger(), [], + config[CONF_HEAT_ACTION]) cg.add(var.set_supports_heat(True)) + if CONF_AUTO_MODE in config: + yield automation.build_automation(var.get_auto_mode_trigger(), [], + config[CONF_AUTO_MODE]) + if CONF_COOL_MODE in config: + yield automation.build_automation(var.get_cool_mode_trigger(), [], + config[CONF_COOL_MODE]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + yield automation.build_automation(var.get_dry_mode_trigger(), [], + config[CONF_DRY_MODE]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + yield automation.build_automation(var.get_fan_only_mode_trigger(), [], + config[CONF_FAN_ONLY_MODE]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + yield automation.build_automation(var.get_heat_mode_trigger(), [], + config[CONF_HEAT_MODE]) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + yield automation.build_automation(var.get_off_mode_trigger(), [], + config[CONF_OFF_MODE]) + if CONF_FAN_MODE_ON_ACTION in config: + yield automation.build_automation(var.get_fan_mode_on_trigger(), [], + config[CONF_FAN_MODE_ON_ACTION]) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + yield automation.build_automation(var.get_fan_mode_off_trigger(), [], + config[CONF_FAN_MODE_OFF_ACTION]) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], + config[CONF_FAN_MODE_AUTO_ACTION]) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + yield automation.build_automation(var.get_fan_mode_low_trigger(), [], + config[CONF_FAN_MODE_LOW_ACTION]) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], + config[CONF_FAN_MODE_MEDIUM_ACTION]) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + yield automation.build_automation(var.get_fan_mode_high_trigger(), [], + config[CONF_FAN_MODE_HIGH_ACTION]) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], + config[CONF_FAN_MODE_MIDDLE_ACTION]) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], + config[CONF_FAN_MODE_FOCUS_ACTION]) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], + config[CONF_FAN_MODE_DIFFUSE_ACTION]) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + yield automation.build_automation(var.get_swing_mode_both_trigger(), [], + config[CONF_SWING_BOTH_ACTION]) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], + config[CONF_SWING_HORIZONTAL_ACTION]) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + yield automation.build_automation(var.get_swing_mode_off_trigger(), [], + config[CONF_SWING_OFF_ACTION]) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], + config[CONF_SWING_VERTICAL_ACTION]) + cg.add(var.set_supports_swing_mode_vertical(True)) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - away_config = BangBangClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) + + if two_points_available is True: + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) cg.add(var.set_away_config(away_config)) diff --git a/esphome/const.py b/esphome/const.py index 4226e25afa..9626a2e00a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -44,6 +44,7 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' +CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' @@ -101,6 +102,7 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' +CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' @@ -139,6 +141,8 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' +CONF_DRY_ACTION = 'dry_action' +CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' CONF_ECHO_PIN = 'echo_pin' @@ -158,6 +162,17 @@ CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' CONF_FAN_MODE = 'fan_mode' +CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' +CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' +CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' +CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' +CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' +CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' +CONF_FAN_MODE_MIDDLE_ACTION = 'fan_mode_middle_action' +CONF_FAN_MODE_OFF_ACTION = 'fan_mode_off_action' +CONF_FAN_MODE_ON_ACTION = 'fan_mode_on_action' +CONF_FAN_ONLY_ACTION = 'fan_only_action' +CONF_FAN_ONLY_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -182,6 +197,7 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' +CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' @@ -189,6 +205,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' +CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' @@ -284,6 +301,7 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' +CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' @@ -448,7 +466,11 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_BOTH_ACTION = 'swing_both_action' +CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' +CONF_SWING_OFF_ACTION = 'swing_off_action' +CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test3.yaml b/tests/test3.yaml index 0344aed6de..53bbdf023c 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -615,6 +615,49 @@ climate: - switch.turn_on: gpio_switch2 heat_action: - switch.turn_on: gpio_switch1 + dry_action: + - switch.turn_on: gpio_switch2 + fan_only_action: + - switch.turn_on: gpio_switch1 + auto_mode: + - switch.turn_on: gpio_switch2 + off_mode: + - switch.turn_on: gpio_switch1 + heat_mode: + - switch.turn_on: gpio_switch2 + cool_mode: + - switch.turn_on: gpio_switch1 + dry_mode: + - switch.turn_on: gpio_switch2 + fan_only_mode: + - switch.turn_on: gpio_switch1 + fan_mode_auto_action: + - switch.turn_on: gpio_switch2 + fan_mode_on_action: + - switch.turn_on: gpio_switch1 + fan_mode_off_action: + - switch.turn_on: gpio_switch2 + fan_mode_low_action: + - switch.turn_on: gpio_switch1 + fan_mode_medium_action: + - switch.turn_on: gpio_switch2 + fan_mode_high_action: + - switch.turn_on: gpio_switch1 + fan_mode_middle_action: + - switch.turn_on: gpio_switch2 + fan_mode_focus_action: + - switch.turn_on: gpio_switch1 + fan_mode_diffuse_action: + - switch.turn_on: gpio_switch2 + swing_off_action: + - switch.turn_on: gpio_switch1 + swing_horizontal_action: + - switch.turn_on: gpio_switch2 + swing_vertical_action: + - switch.turn_on: gpio_switch1 + swing_both_action: + - switch.turn_on: gpio_switch2 + hysteresis: 0.2 away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C From 04e6f475b4f0d0fd9923b0dc6a1ff60ad2eb2ecd Mon Sep 17 00:00:00 2001 From: Lukas Klass Date: Fri, 19 Jun 2020 00:50:58 +0200 Subject: [PATCH 029/200] Fix decode and encode for RC5-protocol (#1047) * Fix receive and transmit for RC5-protocol Receive did not work as a sequence of two high or low puleses are detected as one long pulse. Transmit was extended to respect the field bit in order to enable codes up to 127. * Fix code formatting --- esphome/components/remote_base/__init__.py | 2 +- .../components/remote_base/rc5_protocol.cpp | 46 +++++++++++++++---- 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 05a3e7e1aa..770394faec 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -420,7 +420,7 @@ def raw_action(var, config, args): RC5Data, RC5BinarySensor, RC5Trigger, RC5Action, RC5Dumper = declare_protocol('RC5') RC5_SCHEMA = cv.Schema({ cv.Required(CONF_ADDRESS): cv.All(cv.hex_int, cv.Range(min=0, max=0x1F)), - cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x3F)), + cv.Required(CONF_COMMAND): cv.All(cv.hex_int, cv.Range(min=0, max=0x7F)), }) diff --git a/esphome/components/remote_base/rc5_protocol.cpp b/esphome/components/remote_base/rc5_protocol.cpp index 35bff588e9..a6165492ac 100644 --- a/esphome/components/remote_base/rc5_protocol.cpp +++ b/esphome/components/remote_base/rc5_protocol.cpp @@ -14,10 +14,16 @@ void RC5Protocol::encode(RemoteTransmitData *dst, const RC5Data &data) { dst->set_carrier_frequency(36000); uint64_t out_data = 0; - out_data |= 0b11 << 12; + uint8_t command = data.command; + if (data.command >= 64) { + out_data |= 0b10 << 12; + command = command - 64; + } else { + out_data |= 0b11 << 12; + } out_data |= TOGGLE << 11; out_data |= data.address << 6; - out_data |= data.command; + out_data |= command; for (uint64_t mask = 1UL << (NBITS - 1); mask != 0; mask >>= 1) { if (out_data & mask) { @@ -35,22 +41,44 @@ optional RC5Protocol::decode(RemoteReceiveData src) { .address = 0, .command = 0, }; - src.expect_space(BIT_TIME_US); - if (!src.expect_mark(BIT_TIME_US) || !src.expect_space(BIT_TIME_US) || !src.expect_mark(BIT_TIME_US)) + int field_bit = 0; + + if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { + field_bit = 1; + } else if (src.expect_space(2 * BIT_TIME_US)) { + field_bit = 0; + } else { return {}; + } + + if (!(((src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US)) || + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) && + (((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) || + ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US)))))) { + return {}; + } uint64_t out_data = 0; - for (int bit = NBITS - 3; bit >= 0; bit--) { - if (src.expect_space(BIT_TIME_US) && src.expect_mark(BIT_TIME_US)) { - out_data |= 1 << bit; - } else if (src.expect_mark(BIT_TIME_US) && src.expect_space(BIT_TIME_US)) { + for (int bit = NBITS - 4; bit >= 1; bit--) { + if ((src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) && + (src.expect_mark(BIT_TIME_US) || src.peek_mark(2 * BIT_TIME_US))) { out_data |= 0 << bit; + } else if ((src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) && + (src.expect_space(BIT_TIME_US) || src.peek_space(2 * BIT_TIME_US))) { + out_data |= 1 << bit; } else { return {}; } } + if (src.expect_space(BIT_TIME_US) || src.expect_space(2 * BIT_TIME_US)) { + out_data |= 0; + } else if (src.expect_mark(BIT_TIME_US) || src.expect_mark(2 * BIT_TIME_US)) { + out_data |= 1; + } - out.command = out_data & 0x3F; + out.command = (out_data & 0x3F) + (1 - field_bit) * 64; out.address = (out_data >> 6) & 0x1F; return out; } From e2b655a6cceb831ada8d3c3ade5497b06221711d Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Fri, 19 Jun 2020 09:02:59 -0700 Subject: [PATCH 030/200] Add huzzah gpio pins (#1096) --- esphome/pins.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/pins.py b/esphome/pins.py index 22192b599e..7f4c869895 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -33,7 +33,8 @@ ESP8266_BOARD_PINS = { 'espresso_lite_v2': {'LED': 2}, 'gen4iod': {}, 'heltec_wifi_kit_8': 'd1_mini', - 'huzzah': {'LED': 0}, + 'huzzah': {'LED': 0, 'LED_RED': 0, 'LED_BLUE': 2, 'D4': 4, 'D5': 5, 'D12': 12, + 'D13': 13, 'D14': 14, 'D15': 15, 'D16': 16}, 'inventone': {}, 'modwifi': {}, 'nodemcu': {'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, From f9a86291570b8a2bb8efc8da7d2fea5d4ca2dfed Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Fri, 19 Jun 2020 16:59:19 -0700 Subject: [PATCH 031/200] fix percentage handling (#1094) * fix percentage handling * add test * fix lint errors Co-authored-by: Samuel Sieb --- esphome/config_validation.py | 13 ++++++++++--- tests/test3.yaml | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index fa2a14dbd6..1319786841 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -838,9 +838,16 @@ def percentage(value): def possibly_negative_percentage(value): - has_percent_sign = isinstance(value, str) and value.endswith('%') - if has_percent_sign: - value = float(value[:-1].rstrip()) / 100.0 + has_percent_sign = False + if isinstance(value, str): + try: + if value.endswith('%'): + has_percent_sign = False + value = float(value[:-1].rstrip()) / 100.0 + else: + value = float(value) + except ValueError: + raise Invalid("invalid number") if value > 1: msg = "Percentage must not be higher than 100%." if not has_percent_sign: diff --git a/tests/test3.yaml b/tests/test3.yaml index 53bbdf023c..dded01fb67 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -14,6 +14,8 @@ esphome: substitutions: devicename: test3 devicecomment: test3 device + min_sub: "0.03" + max_sub: "12.0%" api: port: 8000 @@ -769,6 +771,8 @@ servo: id: my_servo output: out restore: true + min_level: $min_sub + max_level: $max_sub ttp229_lsf: From ecb1c77f8b4e1a8c47c78c984353f2e18c244574 Mon Sep 17 00:00:00 2001 From: Alex Mekkering Date: Sun, 21 Jun 2020 20:33:01 +0200 Subject: [PATCH 032/200] Add support for command-line substitutions (#1014) * Add support for command-line substitutions * Fix flake8 * pylint fixes --- esphome/__main__.py | 4 +++- esphome/components/substitutions/__init__.py | 8 ++++++-- esphome/config.py | 18 +++++++++--------- esphome/vscode.py | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 4059024531..27262355d6 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -428,6 +428,8 @@ def parse_args(argv): parser.add_argument('-q', '--quiet', help="Disable all esphome logs.", action='store_true') parser.add_argument('--dashboard', help=argparse.SUPPRESS, action='store_true') + parser.add_argument('-s', '--substitution', nargs=2, action='append', + help='Add a substitution', metavar=('key', 'value')) parser.add_argument('configuration', help='Your YAML configuration file.', nargs='*') subparsers = parser.add_subparsers(help='Commands', dest='command') @@ -532,7 +534,7 @@ def run_esphome(argv): CORE.config_path = conf_path CORE.dashboard = args.dashboard - config = read_config() + config = read_config(dict(args.substitution) if args.substitution else {}) if config is None: return 1 CORE.config = config diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 292a7bf299..ab99e040d4 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -101,11 +101,15 @@ def _substitute_item(substitutions, item, path): return None -def do_substitution_pass(config): - if CONF_SUBSTITUTIONS not in config: +def do_substitution_pass(config, command_line_substitutions): + if CONF_SUBSTITUTIONS not in config and not command_line_substitutions: return substitutions = config[CONF_SUBSTITUTIONS] + if substitutions is None: + substitutions = command_line_substitutions + elif command_line_substitutions: + substitutions = {**substitutions, **command_line_substitutions} with cv.prepend_path('substitutions'): if not isinstance(substitutions, dict): raise cv.Invalid("Substitutions must be a key to value mapping, got {}" diff --git a/esphome/config.py b/esphome/config.py index 8d7c622a27..741b4fb04a 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -387,15 +387,15 @@ def recursive_check_replaceme(value): return value -def validate_config(config): +def validate_config(config, command_line_substitutions): result = Config() # 1. Load substitutions if CONF_SUBSTITUTIONS in config: - result[CONF_SUBSTITUTIONS] = config[CONF_SUBSTITUTIONS] + result[CONF_SUBSTITUTIONS] = {**config[CONF_SUBSTITUTIONS], **command_line_substitutions} result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: - substitutions.do_substitution_pass(config) + substitutions.do_substitution_pass(config, command_line_substitutions) except vol.Invalid as err: result.add_error(err) return result @@ -656,7 +656,7 @@ class InvalidYAMLError(EsphomeError): self.base_exc = base_exc -def _load_config(): +def _load_config(command_line_substitutions): try: config = yaml_util.load_yaml(CORE.config_path) except EsphomeError as e: @@ -664,7 +664,7 @@ def _load_config(): CORE.raw_config = config try: - result = validate_config(config) + result = validate_config(config, command_line_substitutions) except EsphomeError: raise except Exception: @@ -674,9 +674,9 @@ def _load_config(): return result -def load_config(): +def load_config(command_line_substitutions): try: - return _load_config() + return _load_config(command_line_substitutions) except vol.Invalid as err: raise EsphomeError(f"Error while parsing config: {err}") @@ -813,10 +813,10 @@ def strip_default_ids(config): return config -def read_config(): +def read_config(command_line_substitutions): _LOGGER.info("Reading configuration %s...", CORE.config_path) try: - res = load_config() + res = load_config(command_line_substitutions) except EsphomeError as err: _LOGGER.error("Error while reading config: %s", err) return None diff --git a/esphome/vscode.py b/esphome/vscode.py index e8c0b106f7..8782ed6e5c 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -66,7 +66,7 @@ def read_config(args): CORE.config_path = data['file'] vs = VSCodeResult() try: - res = load_config() + res = load_config(dict(args.substitution) if args.substitution else {}) except Exception as err: # pylint: disable=broad-except vs.add_yaml_error(str(err)) else: From 82d5d50d616f6eed3c0a666caa9344e7694ff7e1 Mon Sep 17 00:00:00 2001 From: square99 Date: Mon, 22 Jun 2020 10:29:43 +0900 Subject: [PATCH 033/200] Add LG Climate IR (#1097) * Add LG Climate ir * Add LG Climate IR * Add LG Climate IR * Add LG Climate IR * Add LG Climate IR * Add LG Climate IR * Add LG Climate IR Co-authored-by: Sangheon Heo --- esphome/components/climate_ir_lg/__init__.py | 0 esphome/components/climate_ir_lg/climate.py | 18 ++ .../climate_ir_lg/climate_ir_lg.cpp | 204 ++++++++++++++++++ .../components/climate_ir_lg/climate_ir_lg.h | 44 ++++ tests/test1.yaml | 2 + 5 files changed, 268 insertions(+) create mode 100644 esphome/components/climate_ir_lg/__init__.py create mode 100644 esphome/components/climate_ir_lg/climate.py create mode 100644 esphome/components/climate_ir_lg/climate_ir_lg.cpp create mode 100644 esphome/components/climate_ir_lg/climate_ir_lg.h diff --git a/esphome/components/climate_ir_lg/__init__.py b/esphome/components/climate_ir_lg/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir_lg/climate.py b/esphome/components/climate_ir_lg/climate.py new file mode 100644 index 0000000000..37bf9e2628 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +climate_ir_lg_ns = cg.esphome_ns.namespace('climate_ir_lg') +LgIrClimate = climate_ir_lg_ns.class_('LgIrClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(LgIrClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp new file mode 100644 index 0000000000..80677e6de3 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -0,0 +1,204 @@ +#include "climate_ir_lg.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace climate_ir_lg { + +static const char *TAG = "climate.climate_ir_lg"; + +const uint32_t COMMAND_ON = 0x00000; +const uint32_t COMMAND_ON_AI = 0x03000; +const uint32_t COMMAND_COOL = 0x08000; +const uint32_t COMMAND_OFF = 0xC0000; +const uint32_t COMMAND_SWING = 0x10000; +// On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. +const uint32_t COMMAND_AUTO = 0x0B000; +const uint32_t COMMAND_DRY_FAN = 0x09000; + +const uint32_t COMMAND_MASK = 0xFF000; + +const uint32_t FAN_MASK = 0xF0; +const uint32_t FAN_AUTO = 0x50; +const uint32_t FAN_MIN = 0x00; +const uint32_t FAN_MED = 0x20; +const uint32_t FAN_MAX = 0x40; + +// Temperature +const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1; +const uint32_t TEMP_MASK = 0XF00; +const uint32_t TEMP_SHIFT = 8; + +// Constants +static const uint32_t HEADER_HIGH_US = 8000; +static const uint32_t HEADER_LOW_US = 4000; +static const uint32_t BIT_HIGH_US = 600; +static const uint32_t BIT_ONE_LOW_US = 1600; +static const uint32_t BIT_ZERO_LOW_US = 550; + +const uint16_t BITS = 28; + +void LgIrClimate::transmit_state() { + uint32_t remote_state = 0x8800000; + + // ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_); + if (send_swing_cmd_) { + send_swing_cmd_ = false; + remote_state |= COMMAND_SWING; + } else { + if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode == climate::CLIMATE_MODE_AUTO) { + remote_state |= COMMAND_ON_AI; + } else if (mode_before_ == climate::CLIMATE_MODE_OFF && this->mode != climate::CLIMATE_MODE_OFF) { + remote_state |= COMMAND_ON; + this->mode = climate::CLIMATE_MODE_COOL; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state |= COMMAND_COOL; + break; + case climate::CLIMATE_MODE_AUTO: + remote_state |= COMMAND_AUTO; + break; + case climate::CLIMATE_MODE_DRY: + remote_state |= COMMAND_DRY_FAN; + break; + case climate::CLIMATE_MODE_OFF: + default: + remote_state |= COMMAND_OFF; + break; + } + } + mode_before_ = this->mode; + + ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode); + + if (this->mode == climate::CLIMATE_MODE_OFF) { + remote_state |= FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY) { + switch (this->fan_mode) { + case climate::CLIMATE_FAN_HIGH: + remote_state |= FAN_MAX; + break; + case climate::CLIMATE_FAN_MEDIUM: + remote_state |= FAN_MED; + break; + case climate::CLIMATE_FAN_LOW: + remote_state |= FAN_MIN; + break; + case climate::CLIMATE_FAN_AUTO: + default: + remote_state |= FAN_AUTO; + break; + } + } + + if (this->mode == climate::CLIMATE_MODE_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + // remote_state |= FAN_MODE_AUTO_DRY; + } + if (this->mode == climate::CLIMATE_MODE_COOL) { + auto temp = (uint8_t) roundf(clamp(this->target_temperature, TEMP_MIN, TEMP_MAX)); + remote_state |= ((temp - 15) << TEMP_SHIFT); + } + } + transmit_(remote_state); + this->publish_state(); +} + +bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t nbits = 0; + uint32_t remote_state = 0; + + if (!data.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) + return false; + + for (nbits = 0; nbits < 32; nbits++) { + if (data.expect_item(BIT_HIGH_US, BIT_ONE_LOW_US)) { + remote_state = (remote_state << 1) | 1; + } else if (data.expect_item(BIT_HIGH_US, BIT_ZERO_LOW_US)) { + remote_state = (remote_state << 1) | 0; + } else if (nbits == BITS) { + break; + } else { + return false; + } + } + + ESP_LOGD(TAG, "Decoded 0x%02X", remote_state); + if ((remote_state & 0xFF00000) != 0x8800000) + return false; + + if ((remote_state & COMMAND_MASK) == COMMAND_ON) { + this->mode = climate::CLIMATE_MODE_COOL; + } else if ((remote_state & COMMAND_MASK) == COMMAND_ON_AI) { + this->mode = climate::CLIMATE_MODE_AUTO; + } + + if ((remote_state & COMMAND_MASK) == COMMAND_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) { + this->swing_mode = + this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF; + } else { + if ((remote_state & COMMAND_MASK) == COMMAND_AUTO) + this->mode = climate::CLIMATE_MODE_AUTO; + else if ((remote_state & COMMAND_MASK) == COMMAND_DRY_FAN) { + this->mode = climate::CLIMATE_MODE_DRY; + } else { + this->mode = climate::CLIMATE_MODE_COOL; + } + } + + // Temperature + if (this->mode == climate::CLIMATE_MODE_COOL) + this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15; + + // Fan Speed + if (this->mode == climate::CLIMATE_MODE_AUTO) { + this->fan_mode = climate::CLIMATE_FAN_AUTO; + } else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY) { + if ((remote_state & FAN_MASK) == FAN_AUTO) + this->fan_mode = climate::CLIMATE_FAN_AUTO; + else if ((remote_state & FAN_MASK) == FAN_MIN) + this->fan_mode = climate::CLIMATE_FAN_LOW; + else if ((remote_state & FAN_MASK) == FAN_MED) + this->fan_mode = climate::CLIMATE_FAN_MEDIUM; + else if ((remote_state & FAN_MASK) == FAN_MAX) + this->fan_mode = climate::CLIMATE_FAN_HIGH; + } + this->publish_state(); + + return true; +} +void LgIrClimate::transmit_(uint32_t value) { + calc_checksum_(value); + ESP_LOGD(TAG, "Sending climate_lg_ir code: 0x%02X", value); + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(38000); + data->reserve(2 + BITS * 2u); + + data->item(HEADER_HIGH_US, HEADER_LOW_US); + + for (uint32_t mask = 1UL << (BITS - 1); mask != 0; mask >>= 1) { + if (value & mask) + data->item(BIT_HIGH_US, BIT_ONE_LOW_US); + else + data->item(BIT_HIGH_US, BIT_ZERO_LOW_US); + } + data->mark(BIT_HIGH_US); + transmit.perform(); +} +void LgIrClimate::calc_checksum_(uint32_t &value) { + uint32_t mask = 0xF; + uint32_t sum = 0; + for (uint8_t i = 1; i < 8; i++) { + sum += (value & (mask << (i * 4))) >> (i * 4); + } + + value |= (sum & mask); +} + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.h b/esphome/components/climate_ir_lg/climate_ir_lg.h new file mode 100644 index 0000000000..204482e7a8 --- /dev/null +++ b/esphome/components/climate_ir_lg/climate_ir_lg.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace climate_ir_lg { + +// Temperature +const uint8_t TEMP_MIN = 18; // Celsius +const uint8_t TEMP_MAX = 30; // Celsius + +class LgIrClimate : public climate_ir::ClimateIR { + public: + LgIrClimate() + : climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, false, + {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, + climate::CLIMATE_FAN_HIGH}, + {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override { + send_swing_cmd_ = call.get_swing_mode().has_value(); + // swing resets after unit powered off + if (call.get_mode().has_value() && *call.get_mode() == climate::CLIMATE_MODE_OFF) + this->swing_mode = climate::CLIMATE_SWING_OFF; + climate_ir::ClimateIR::control(call); + } + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; + + bool send_swing_cmd_{false}; + + void calc_checksum_(uint32_t &value); + void transmit_(uint32_t value); + + climate::ClimateMode mode_before_{climate::CLIMATE_MODE_OFF}; +}; + +} // namespace climate_ir_lg +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index bd44ff6e63..5343d48970 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1277,6 +1277,8 @@ climate: name: Mitsubishi - platform: whirlpool name: Whirlpool Climate + - platform: climate_ir_lg + name: LG Climate switch: - platform: gpio From 148f5d94180473df7e83937b63b46504d71d7db6 Mon Sep 17 00:00:00 2001 From: buxtronix Date: Sat, 27 Jun 2020 08:13:23 +1000 Subject: [PATCH 034/200] ESP32: Conditionally log on services to avoid OOM crashes (#1098) Also removed new line formatting tidy as it requires a bit of memory which might not be available. * Use suitably scoped char for log newline stripping * fix formatting * Better fix for OOM logging crash * Limit to ESP32 only * format changes --- esphome/components/logger/logger.cpp | 10 ++++++++++ esphome/core/log.cpp | 10 ---------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index bc6951c9b9..140b8f26c1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -103,7 +103,17 @@ void HOT Logger::log_message_(int level, const char *tag, int offset) { const char *msg = this->tx_buffer_ + offset; if (this->baud_rate_ > 0) this->hw_serial_->println(msg); +#ifdef ARDUINO_ARCH_ESP32 + // Suppress network-logging if memory constrained, but still log to serial + // ports. In some configurations (eg BLE enabled) there may be some transient + // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping + // here usually allows the stack to recover instead. + // See issue #1234 for analysis. + if (xPortGetFreeHeapSize() > 2048) + this->log_callback_.call(level, tag, msg); +#else this->log_callback_.call(level, tag, msg); +#endif } Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) diff --git a/esphome/core/log.cpp b/esphome/core/log.cpp index 15d49c0038..9b49a4c6ba 100644 --- a/esphome/core/log.cpp +++ b/esphome/core/log.cpp @@ -53,16 +53,6 @@ int HOT esp_idf_log_vprintf_(const char *format, va_list args) { // NOLINT if (log == nullptr) return 0; - size_t len = strlen(format); - if (format[len - 1] == '\n') { - // Remove trailing newline from format - // Use locally stored - static std::string FORMAT_COPY; - FORMAT_COPY.clear(); - FORMAT_COPY.insert(0, format, len - 1); - format = FORMAT_COPY.c_str(); - } - log->log_vprintf_(ESPHOME_LOG_LEVEL, "esp-idf", 0, format, args); #endif return 0; From ca4107d4504ac7dccf1a663733846798d0570274 Mon Sep 17 00:00:00 2001 From: buxtronix Date: Sun, 28 Jun 2020 07:06:13 +1000 Subject: [PATCH 035/200] Release BT controller unused memory in the right place (#1095) --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5109af21fa..a8185a8c67 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -99,14 +99,14 @@ bool ESP32BLETracker::ble_setup() { return false; } + esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); + // Initialize the bluetooth controller with the default configuration if (!btStart()) { ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status()); return false; } - esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT); - err = esp_bluedroid_init(); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_bluedroid_init failed: %d", err); From 3c9712d6838cc7376034a7a202f251ed32757177 Mon Sep 17 00:00:00 2001 From: phjr Date: Sun, 28 Jun 2020 00:08:15 +0200 Subject: [PATCH 036/200] add support for SN74HC595 shift register (#1083) * add support for SN74HC595 shift register * fix linter errors * more linter reported issues fixed * hopefully last linter error cleanup * one more linter fix * looks like the linter is always keeping stuff for later, is this the final fix? * add test Co-authored-by: Guillermo Ruffino --- esphome/components/sn74hc595/__init__.py | 54 +++++++++++++++++ esphome/components/sn74hc595/sn74hc595.cpp | 70 ++++++++++++++++++++++ esphome/components/sn74hc595/sn74hc595.h | 55 +++++++++++++++++ tests/test1.yaml | 22 ++++++- 4 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 esphome/components/sn74hc595/__init__.py create mode 100644 esphome/components/sn74hc595/sn74hc595.cpp create mode 100644 esphome/components/sn74hc595/sn74hc595.h diff --git a/esphome/components/sn74hc595/__init__.py b/esphome/components/sn74hc595/__init__.py new file mode 100644 index 0000000000..de80c38214 --- /dev/null +++ b/esphome/components/sn74hc595/__init__.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import CONF_ID, CONF_NUMBER, CONF_INVERTED, CONF_DATA_PIN, CONF_CLOCK_PIN + +DEPENDENCIES = [] +MULTI_CONF = True + +sn74hc595_ns = cg.esphome_ns.namespace('sn74hc595') + +SN74HC595Component = sn74hc595_ns.class_('SN74HC595Component', cg.Component) +SN74HC595GPIOPin = sn74hc595_ns.class_('SN74HC595GPIOPin', cg.GPIOPin) + +CONF_SN74HC595 = 'sn74hc595' +CONF_LATCH_PIN = 'latch_pin' +CONF_OE_PIN = 'oe_pin' +CONF_SR_COUNT = 'sr_count' +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(SN74HC595Component), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_LATCH_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_OE_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_SR_COUNT, default=1): cv.int_range(1, 4) +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + data_pin = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data_pin)) + clock_pin = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock_pin)) + latch_pin = yield cg.gpio_pin_expression(config[CONF_LATCH_PIN]) + cg.add(var.set_latch_pin(latch_pin)) + oe_pin = yield cg.gpio_pin_expression(config[CONF_OE_PIN]) + cg.add(var.set_oe_pin(oe_pin)) + cg.add(var.set_sr_count(config[CONF_SR_COUNT])) + + +SN74HC595_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +SN74HC595_INPUT_PIN_SCHEMA = cv.Schema({}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SN74HC595, + (SN74HC595_OUTPUT_PIN_SCHEMA, SN74HC595_INPUT_PIN_SCHEMA)) +def sn74hc595_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_SN74HC595]) + yield SN74HC595GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_INVERTED]) diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp new file mode 100644 index 0000000000..edf8989149 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -0,0 +1,70 @@ +#include "sn74hc595.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sn74hc595 { + +static const char *TAG = "sn74hc595"; + +void SN74HC595Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); + + if (this->have_oe_pin_) { // disable output + pinMode(this->oe_pin_->get_pin(), OUTPUT); + digitalWrite(this->oe_pin_->get_pin(), HIGH); + } + + // initialize output pins + pinMode(this->clock_pin_->get_pin(), OUTPUT); + pinMode(this->data_pin_->get_pin(), OUTPUT); + pinMode(this->latch_pin_->get_pin(), OUTPUT); + digitalWrite(this->clock_pin_->get_pin(), LOW); + digitalWrite(this->data_pin_->get_pin(), LOW); + digitalWrite(this->latch_pin_->get_pin(), LOW); + + // send state to shift register + this->write_gpio_(); +} + +void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } + +bool SN74HC595Component::digital_read_(uint8_t pin) { return bitRead(this->output_bits_, pin); } + +void SN74HC595Component::digital_write_(uint8_t pin, bool value) { + bitWrite(this->output_bits_, pin, value); + this->write_gpio_(); +} + +bool SN74HC595Component::write_gpio_() { + for (int i = this->sr_count_ - 1; i >= 0; i--) { + uint8_t data = (uint8_t)(this->output_bits_ >> (8 * i) & 0xff); + shiftOut(this->data_pin_->get_pin(), this->clock_pin_->get_pin(), MSBFIRST, data); + } + + // pulse latch to activate new values + digitalWrite(this->latch_pin_->get_pin(), HIGH); + digitalWrite(this->latch_pin_->get_pin(), LOW); + + // enable output if configured + if (this->have_oe_pin_) { + digitalWrite(this->oe_pin_->get_pin(), LOW); + } + + return true; +} + +float SN74HC595Component::get_setup_priority() const { return setup_priority::IO; } + +void SN74HC595GPIOPin::setup() {} + +bool SN74HC595GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } + +void SN74HC595GPIOPin::digital_write(bool value) { + this->parent_->digital_write_(this->pin_, value != this->inverted_); +} + +SN74HC595GPIOPin::SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted) + : GPIOPin(pin, OUTPUT, inverted), parent_(parent) {} + +} // namespace sn74hc595 +} // namespace esphome diff --git a/esphome/components/sn74hc595/sn74hc595.h b/esphome/components/sn74hc595/sn74hc595.h new file mode 100644 index 0000000000..d6f9a68bc8 --- /dev/null +++ b/esphome/components/sn74hc595/sn74hc595.h @@ -0,0 +1,55 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" + +namespace esphome { +namespace sn74hc595 { + +class SN74HC595Component : public Component { + public: + SN74HC595Component() = default; + + void setup() override; + float get_setup_priority() const override; + void dump_config() override; + + void set_data_pin(GPIOPin *pin) { data_pin_ = pin; } + void set_clock_pin(GPIOPin *pin) { clock_pin_ = pin; } + void set_latch_pin(GPIOPin *pin) { latch_pin_ = pin; } + void set_oe_pin(GPIOPin *pin) { + oe_pin_ = pin; + have_oe_pin_ = true; + } + void set_sr_count(uint8_t count) { sr_count_ = count; } + + protected: + friend class SN74HC595GPIOPin; + bool digital_read_(uint8_t pin); + void digital_write_(uint8_t pin, bool value); + bool write_gpio_(); + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + GPIOPin *latch_pin_; + GPIOPin *oe_pin_; + uint8_t sr_count_; + bool have_oe_pin_{false}; + uint32_t output_bits_{0x00}; +}; + +/// Helper class to expose a SC74HC595 pin as an internal output GPIO pin. +class SN74HC595GPIOPin : public GPIOPin { + public: + SN74HC595GPIOPin(SN74HC595Component *parent, uint8_t pin, bool inverted = false); + + void setup() override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + SN74HC595Component *parent_; +}; + +} // namespace sn74hc595 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 5343d48970..8f06de2e40 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1187,13 +1187,13 @@ light: if (initial_run) { it[0] = current_color; } - + - wled: port: 11111 - + - adalight: uart_id: adalight_uart - + - automation: name: Custom Effect sequence: @@ -1465,6 +1465,14 @@ switch: id: my_stepper position: 0 + - platform: gpio + name: "SN74HC595 Pin #0" + pin: + sn74hc595: sn74hc595_hub + # Use pin number 0 + number: 0 + inverted: False + fan: - platform: binary output: gpio_26 @@ -1715,3 +1723,11 @@ text_sensor: name: "BSSID" mac_address: name: "Mac Address" + +sn74hc595: + - id: 'sn74hc595_hub' + data_pin: GPIO21 + clock_pin: GPIO23 + latch_pin: GPIO22 + oe_pin: GPIO32 + sr_count: 2 From bfb9cb6732ea3d909f8750d75b0bb5bdd722d48b Mon Sep 17 00:00:00 2001 From: rspaargaren Date: Sun, 28 Jun 2020 22:33:06 +0200 Subject: [PATCH 037/200] Max7219 in Dot Matrix configuration (#1053) * push final files * Update max7219digit.cpp reenter small bug fix on uint8 declaration * small debug * set max offset to 100 * remove unwanted file * Update font file to make travis happy * travis update * further progress in keeping Yarvis happy * travis font * travis will never be satisfied but one step ahead * YARVIS TAKE 10000000 * never ending yarvis * Almost there with Yarvis * removed double declaration YARVIS * added namespace to font file (TRAVIS) * almost there last changes for YARVIS * further travis updates * removed files for travis * fix display.py length of line travis remark * further update on display.py * file delete travis requirement * final entry for travis? * Some further debug on max offset * Travis updates * scroll_left_new * 90degreesrotate * added option to config rotate90 * four orientations for the chips * new setup for scroll * replaced small bug missing {} * debug changed int8 to int16 on scroll function * removed small merge failure * travis updates round 1 * travis updates round 2 * travis details round 3 * added options to set scroll parameters in yaml conf * removed ttf and png to satisfy travis * travis update * travis updates * travis * further updates after input from @glmnet * remove deleted comments * Added ENUM TYPE to config file * change in test file * removed test files * updates for pull request * Typing error removed * travis update * PR updates travis * Update test3.yaml * Update test3.yaml * update device schema as per #988 * Delete partitions.csv * repair on image display and invert routine * further update and a bit of cleanup * added writing 0 in draw pixel * small deletion error * travis updates * Update max7219digit.cpp adding intensity value to dynamically set value * Update max7219digit.h adding option for intensity * remove some files from tests --- esphome/components/max7219digit/__init__.py | 0 esphome/components/max7219digit/display.py | 63 ++++ .../components/max7219digit/max7219digit.cpp | 293 ++++++++++++++++++ .../components/max7219digit/max7219digit.h | 107 +++++++ esphome/components/max7219digit/max7219font.h | 268 ++++++++++++++++ tests/test3.yaml | 12 + 6 files changed, 743 insertions(+) create mode 100644 esphome/components/max7219digit/__init__.py create mode 100644 esphome/components/max7219digit/display.py create mode 100644 esphome/components/max7219digit/max7219digit.cpp create mode 100644 esphome/components/max7219digit/max7219digit.h create mode 100644 esphome/components/max7219digit/max7219font.h diff --git a/esphome/components/max7219digit/__init__.py b/esphome/components/max7219digit/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py new file mode 100644 index 0000000000..5bba71148c --- /dev/null +++ b/esphome/components/max7219digit/display.py @@ -0,0 +1,63 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import display, spi +from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS + +DEPENDENCIES = ['spi'] + +CONF_ROTATE_CHIP = 'rotate_chip' +CONF_SCROLL_SPEED = 'scroll_speed' +CONF_SCROLL_DWELL = 'scroll_dwell' +CONF_SCROLL_DELAY = 'scroll_delay' +CONF_SCROLL_ENABLE = 'scroll_enable' +CONF_SCROLL_MODE = 'scroll_mode' + +SCROLL_MODES = { + 'CONTINUOUS': 0, + 'STOP': 1, +} + +CHIP_MODES = { + '0': 0, + '90': 1, + '180': 2, + '270': 3, +} + +max7219_ns = cg.esphome_ns.namespace('max7219digit') +MAX7219Component = max7219_ns.class_('MAX7219Component', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +MAX7219ComponentRef = MAX7219Component.operator('ref') + +CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(MAX7219Component), + cv.Optional(CONF_NUM_CHIPS, default=4): cv.int_range(min=1, max=255), + cv.Optional(CONF_INTENSITY, default=15): cv.int_range(min=0, max=15), + cv.Optional(CONF_ROTATE_CHIP, default='0'): cv.enum(CHIP_MODES, upper=True), + cv.Optional(CONF_SCROLL_MODE, default='CONTINUOUS'): cv.enum(SCROLL_MODES, upper=True), + cv.Optional(CONF_SCROLL_ENABLE, default=True): cv.boolean, + cv.Optional(CONF_SCROLL_SPEED, default='250ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCROLL_DELAY, default='1000ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCROLL_DWELL, default='1000ms'): cv.positive_time_period_milliseconds, +}).extend(cv.polling_component_schema('500ms')).extend(spi.spi_device_schema(CS_PIN_required=True)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + yield display.register_display(var, config) + + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) + cg.add(var.set_intensity(config[CONF_INTENSITY])) + cg.add(var.set_chip_orientation(config[CONF_ROTATE_CHIP])) + cg.add(var.set_scroll_speed(config[CONF_SCROLL_SPEED])) + cg.add(var.set_scroll_dwell(config[CONF_SCROLL_DWELL])) + cg.add(var.set_scroll_delay(config[CONF_SCROLL_DELAY])) + cg.add(var.set_scroll(config[CONF_SCROLL_ENABLE])) + cg.add(var.set_scroll_mode(config[CONF_SCROLL_MODE])) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(MAX7219ComponentRef, 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp new file mode 100644 index 0000000000..b19f05143f --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -0,0 +1,293 @@ +#include "max7219digit.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include "max7219font.h" + +namespace esphome { +namespace max7219digit { + +static const char *TAG = "max7219DIGIT"; + +static const uint8_t MAX7219_REGISTER_NOOP = 0x00; +static const uint8_t MAX7219_REGISTER_DECODE_MODE = 0x09; +static const uint8_t MAX7219_REGISTER_INTENSITY = 0x0A; +static const uint8_t MAX7219_REGISTER_SCAN_LIMIT = 0x0B; +static const uint8_t MAX7219_REGISTER_SHUTDOWN = 0x0C; +static const uint8_t MAX7219_REGISTER_DISPLAY_TEST = 0x0F; +constexpr uint8_t MAX7219_NO_SHUTDOWN = 0x00; +constexpr uint8_t MAX7219_SHUTDOWN = 0x01; +constexpr uint8_t MAX7219_NO_DISPLAY_TEST = 0x00; +constexpr uint8_t MAX7219_DISPLAY_TEST = 0x01; + +float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void MAX7219Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up MAX7219_DIGITS..."); + this->spi_setup(); + this->stepsleft_ = 0; + this->max_displaybuffer_.reserve(500); // Create base space to write buffer + // Initialize buffer with 0 for display so all non written pixels are blank + this->max_displaybuffer_.resize(this->num_chips_ * 8, 0); + // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway + this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); + // let's use our own ASCII -> led pattern encoding + this->send_to_all_(MAX7219_REGISTER_DECODE_MODE, 0); + // No display test with all the pixels on + this->send_to_all_(MAX7219_REGISTER_DISPLAY_TEST, MAX7219_NO_DISPLAY_TEST); + // SET Intsity of display + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); + // this->send_to_all_(MAX7219_REGISTER_INTENSITY, 1); + this->display(); + // power up + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); +} + +void MAX7219Component::dump_config() { + ESP_LOGCONFIG(TAG, "MAX7219DIGIT:"); + ESP_LOGCONFIG(TAG, " Number of Chips: %u", this->num_chips_); + ESP_LOGCONFIG(TAG, " Intensity: %u", this->intensity_); + ESP_LOGCONFIG(TAG, " Scroll Mode: %u", this->scroll_mode_); + ESP_LOGCONFIG(TAG, " Scroll Speed: %u", this->scroll_speed_); + ESP_LOGCONFIG(TAG, " Scroll Dwell: %u", this->scroll_dwell_); + ESP_LOGCONFIG(TAG, " Scroll Delay: %u", this->scroll_delay_); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_UPDATE_INTERVAL(this); +} + +void MAX7219Component::loop() { + unsigned long now = millis(); + + // check if the buffer has shrunk past the current position since last update + if ((this->max_displaybuffer_.size() >= this->old_buffer_size_ + 3) || + (this->max_displaybuffer_.size() <= this->old_buffer_size_ - 3)) { + this->stepsleft_ = 0; + this->display(); + this->old_buffer_size_ = this->max_displaybuffer_.size(); + } + + // Reset the counter back to 0 when full string has been displayed. + if (this->stepsleft_ > this->max_displaybuffer_.size()) + this->stepsleft_ = 0; + + // Return if there is no need to scroll or scroll is off + if (!this->scroll_ || (this->max_displaybuffer_.size() <= this->num_chips_ * 8)) { + this->display(); + return; + } + + if ((this->stepsleft_ == 0) && (now - this->last_scroll_ < this->scroll_delay_)) { + this->display(); + return; + } + + // Dwell time at end of string in case of stop at end + if (this->scroll_mode_ == 1) { + if (this->stepsleft_ >= this->max_displaybuffer_.size() - this->num_chips_ * 8 + 1) { + if (now - this->last_scroll_ >= this->scroll_dwell_) { + this->stepsleft_ = 0; + this->last_scroll_ = now; + this->display(); + } + return; + } + } + + // Actual call to scroll left action + if (now - this->last_scroll_ >= this->scroll_speed_) { + this->last_scroll_ = now; + this->scroll_left(); + this->display(); + } +} + +void MAX7219Component::display() { + byte pixels[8]; + // Run this loop for every MAX CHIP (GRID OF 64 leds) + // Run this routine for the rows of every chip 8x row 0 top to 7 bottom + // Fill the pixel parameter with diplay data + // Send the data to the chip + for (uint8_t i = 0; i < this->num_chips_; i++) { + for (uint8_t j = 0; j < 8; j++) { + pixels[j] = this->max_displaybuffer_[i * 8 + j]; + } + this->send64pixels(i, pixels); + } +} + +int MAX7219Component::get_height_internal() { + return 8; // TO BE DONE -> STACK TWO DISPLAYS ON TOP OF EACH OTHE + // TO BE DONE -> CREATE Virtual size of screen and scroll +} + +int MAX7219Component::get_width_internal() { return this->num_chips_ * 8; } + +size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } + +void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, int color) { + if (x + 1 > this->max_displaybuffer_.size()) { // Extend the display buffer in case required + this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); + } + + if (y >= this->get_height_internal() || y < 0) // If pixel is outside display then dont draw + return; + + uint16_t pos = x; // X is starting at 0 top left + uint8_t subpos = y; // Y is starting at 0 top left + + if (color == 1) { + this->max_displaybuffer_[pos] |= (1 << subpos); + } else { + this->max_displaybuffer_[pos] &= ~(1 << subpos); + } +} + +void MAX7219Component::send_byte_(uint8_t a_register, uint8_t data) { + this->write_byte(a_register); // Write register value to MAX + this->write_byte(data); // Followed by actual data +} +void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { + this->enable(); // Enable SPI + for (uint8_t i = 0; i < this->num_chips_; i++) // Run the loop for every MAX chip in the stack + this->send_byte_(a_register, data); // Send the data to the chips + this->disable(); // Disable SPI +} +void MAX7219Component::update() { + this->update_ = true; + this->max_displaybuffer_.clear(); + this->max_displaybuffer_.resize(this->num_chips_ * 8, this->bckgrnd_); + if (this->writer_local_.has_value()) // insert Labda function if available + (*this->writer_local_)(*this); +} + +void MAX7219Component::invert_on_off(bool on_off) { this->invert_ = on_off; }; +void MAX7219Component::invert_on_off() { this->invert_ = !this->invert_; }; + +void MAX7219Component::turn_on_off(bool on_off) { + if (on_off) { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 1); + } else { + this->send_to_all_(MAX7219_REGISTER_SHUTDOWN, 0); + } +} + +void MAX7219Component::scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); + this->set_scroll_speed(speed); + this->set_scroll_dwell(dwell); + this->set_scroll_delay(delay); +} + +void MAX7219Component::scroll(bool on_off, uint8_t mode) { + this->set_scroll(on_off); + this->set_scroll_mode(mode); +} + +void MAX7219Component::intensity(uint8_t intensity) { + this->intensity_ = intensity; + this->send_to_all_(MAX7219_REGISTER_INTENSITY, this->intensity_); +} + +void MAX7219Component::scroll(bool on_off) { this->set_scroll(on_off); } + +void MAX7219Component::scroll_left() { + if (this->update_) { + this->max_displaybuffer_.push_back(this->bckgrnd_); + for (uint16_t i = 0; i < this->stepsleft_; i++) { + this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); + this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); + this->update_ = false; + } + } else { + this->max_displaybuffer_.push_back(this->max_displaybuffer_.front()); + this->max_displaybuffer_.erase(this->max_displaybuffer_.begin()); + } + this->stepsleft_++; +} + +void MAX7219Component::send_char(byte chip, byte data) { + // get this character from PROGMEM + for (byte i = 0; i < 8; i++) + this->max_displaybuffer_[chip * 8 + i] = pgm_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); +} // end of send_char + +// send one character (data) to position (chip) + +void MAX7219Component::send64pixels(byte chip, const byte pixels[8]) { + for (byte col = 0; col < 8; col++) { // RUN THIS LOOP 8 times until column is 7 + this->enable(); // start sending by enabling SPI + for (byte i = 0; i < chip; i++) // send extra NOPs to push the pixels out to extra displays + this->send_byte_(MAX7219_REGISTER_NOOP, + MAX7219_REGISTER_NOOP); // run this loop unit the matching chip is reached + byte b = 0; // rotate pixels 90 degrees -- set byte to 0 + if (this->orientation_ == 0) { + for (byte i = 0; i < 8; i++) // run this loop 8 times for all the pixels[8] received + b |= bitRead(pixels[i], col) << (7 - i); // change the column bits into row bits + } else if (this->orientation_ == 1) { + b = pixels[col]; + } else if (this->orientation_ == 2) { + for (byte i = 0; i < 8; i++) + b |= bitRead(pixels[i], 7 - col) << (7 - i); + } else { + b = pixels[7 - col]; + } + // send this byte to dispay at selected chip + if (this->invert_) { + this->send_byte_(col + 1, ~b); + } else { + this->send_byte_(col + 1, b); + } + for (int i = 0; i < this->num_chips_ - chip - 1; i++) // end with enough NOPs so later chips don't update + this->send_byte_(MAX7219_REGISTER_NOOP, MAX7219_REGISTER_NOOP); + this->disable(); // all done disable SPI + } // end of for each column +} // end of send64pixels + +uint8_t MAX7219Component::printdigit(const char *str) { return this->printdigit(0, str); } + +uint8_t MAX7219Component::printdigit(uint8_t start_pos, const char *s) { + byte chip; + for (chip = start_pos; chip < this->num_chips_ && *s; chip++) + send_char(chip, *s++); + // space out rest + while (chip < (this->num_chips_)) + send_char(chip++, ' '); + return 0; +} // end of sendString + +uint8_t MAX7219Component::printdigitf(uint8_t pos, const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::printdigitf(const char *format, ...) { + va_list arg; + va_start(arg, format); + char buffer[64]; + int ret = vsnprintf(buffer, sizeof(buffer), format, arg); + va_end(arg); + if (ret > 0) + return this->printdigit(buffer); + return 0; +} + +#ifdef USE_TIME +uint8_t MAX7219Component::strftimedigit(uint8_t pos, const char *format, time::ESPTime time) { + char buffer[64]; + size_t ret = time.strftime(buffer, sizeof(buffer), format); + if (ret > 0) + return this->printdigit(pos, buffer); + return 0; +} +uint8_t MAX7219Component::strftimedigit(const char *format, time::ESPTime time) { + return this->strftimedigit(0, format, time); +} +#endif + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h new file mode 100644 index 0000000000..b032f33b58 --- /dev/null +++ b/esphome/components/max7219digit/max7219digit.h @@ -0,0 +1,107 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/components/display/display_buffer.h" +#include "esphome/components/spi/spi.h" + +#ifdef USE_TIME +#include "esphome/components/time/real_time_clock.h" +#endif + +namespace esphome { +namespace max7219digit { + +class MAX7219Component; + +using max7219_writer_t = std::function; + +class MAX7219Component : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_writer(max7219_writer_t &&writer) { this->writer_local_ = writer; }; + + void setup() override; + + void loop() override; + + void dump_config() override; + + void update() override; + + float get_setup_priority() const override; + + void display(); + + void invert_on_off(bool on_off); + void invert_on_off(); + + void turn_on_off(bool on_off); + + void draw_absolute_pixel_internal(int x, int y, int color) override; + int get_height_internal() override; + int get_width_internal() override; + + void set_intensity(uint8_t intensity) { this->intensity_ = intensity; }; + void set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; }; + void set_chip_orientation(uint8_t rotate) { this->orientation_ = rotate; }; + void set_scroll_speed(uint16_t speed) { this->scroll_speed_ = speed; }; + void set_scroll_dwell(uint16_t dwell) { this->scroll_dwell_ = dwell; }; + void set_scroll_delay(uint16_t delay) { this->scroll_delay_ = delay; }; + void set_scroll(bool on_off) { this->scroll_ = on_off; }; + void set_scroll_mode(uint8_t mode) { this->scroll_mode_ = mode; }; + + void send_char(byte chip, byte data); + void send64pixels(byte chip, const byte pixels[8]); + + void scroll_left(); + void scroll(bool on_off, uint8_t mode, uint16_t speed, uint16_t delay, uint16_t dwell); + void scroll(bool on_off, uint8_t mode); + void scroll(bool on_off); + void intensity(uint8_t intensity); + + /// Evaluate the printf-format and print the result at the given position. + uint8_t printdigitf(uint8_t pos, const char *format, ...) __attribute__((format(printf, 3, 4))); + /// Evaluate the printf-format and print the result at position 0. + uint8_t printdigitf(const char *format, ...) __attribute__((format(printf, 2, 3))); + + /// Print `str` at the given position. + uint8_t printdigit(uint8_t pos, const char *str); + /// Print `str` at position 0. + uint8_t printdigit(const char *str); + +#ifdef USE_TIME + /// Evaluate the strftime-format and print the result at the given position. + uint8_t strftimedigit(uint8_t pos, const char *format, time::ESPTime time) __attribute__((format(strftime, 3, 0))); + + /// Evaluate the strftime-format and print the result at position 0. + uint8_t strftimedigit(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); +#endif + + protected: + void send_byte_(uint8_t a_register, uint8_t data); + void send_to_all_(uint8_t a_register, uint8_t data); + + uint8_t intensity_; /// Intensity of the display from 0 to 15 (most) + uint8_t num_chips_; + bool scroll_; + bool update_{false}; + uint16_t scroll_speed_; + uint16_t scroll_delay_; + uint16_t scroll_dwell_; + uint16_t old_buffer_size_ = 0; + uint8_t scroll_mode_; + bool invert_ = false; + uint8_t orientation_; + uint8_t bckgrnd_ = 0x0; + std::vector max_displaybuffer_; + unsigned long last_scroll_ = 0; + uint16_t stepsleft_; + size_t get_buffer_length_(); + optional writer_local_{}; +}; + +} // namespace max7219digit +} // namespace esphome diff --git a/esphome/components/max7219digit/max7219font.h b/esphome/components/max7219digit/max7219font.h new file mode 100644 index 0000000000..3d42d1cc2d --- /dev/null +++ b/esphome/components/max7219digit/max7219font.h @@ -0,0 +1,268 @@ +#pragma once + +namespace esphome { +namespace max7219digit { + +// bit patterns for the CP437 font + +const byte MAX7219_DOT_MATRIX_FONT[256][8] PROGMEM = { + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x00 + {0x7E, 0x81, 0x95, 0xB1, 0xB1, 0x95, 0x81, 0x7E}, // 0x01 + {0x7E, 0xFF, 0xEB, 0xCF, 0xCF, 0xEB, 0xFF, 0x7E}, // 0x02 + {0x0E, 0x1F, 0x3F, 0x7E, 0x3F, 0x1F, 0x0E, 0x00}, // 0x03 + {0x08, 0x1C, 0x3E, 0x7F, 0x3E, 0x1C, 0x08, 0x00}, // 0x04 + {0x18, 0xBA, 0xFF, 0xFF, 0xFF, 0xBA, 0x18, 0x00}, // 0x05 + {0x10, 0xB8, 0xFC, 0xFF, 0xFC, 0xB8, 0x10, 0x00}, // 0x06 + {0x00, 0x00, 0x18, 0x3C, 0x3C, 0x18, 0x00, 0x00}, // 0x07 + {0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF}, // 0x08 + {0x00, 0x3C, 0x66, 0x42, 0x42, 0x66, 0x3C, 0x00}, // 0x09 + {0xFF, 0xC3, 0x99, 0xBD, 0xBD, 0x99, 0xC3, 0xFF}, // 0x0A + {0x70, 0xF8, 0x88, 0x88, 0xFD, 0x7F, 0x07, 0x0F}, // 0x0B + {0x00, 0x4E, 0x5F, 0xF1, 0xF1, 0x5F, 0x4E, 0x00}, // 0x0C + {0xC0, 0xE0, 0xFF, 0x7F, 0x05, 0x05, 0x07, 0x07}, // 0x0D + {0xC0, 0xFF, 0x7F, 0x05, 0x05, 0x65, 0x7F, 0x3F}, // 0x0E + {0x99, 0x5A, 0x3C, 0xE7, 0xE7, 0x3C, 0x5A, 0x99}, // 0x0F + {0x7F, 0x3E, 0x3E, 0x1C, 0x1C, 0x08, 0x08, 0x00}, // 0x10 + {0x08, 0x08, 0x1C, 0x1C, 0x3E, 0x3E, 0x7F, 0x00}, // 0x11 + {0x00, 0x24, 0x66, 0xFF, 0xFF, 0x66, 0x24, 0x00}, // 0x12 + {0x00, 0x5F, 0x5F, 0x00, 0x00, 0x5F, 0x5F, 0x00}, // 0x13 + {0x06, 0x0F, 0x09, 0x7F, 0x7F, 0x01, 0x7F, 0x7F}, // 0x14 + {0x40, 0xDA, 0xBF, 0xA5, 0xFD, 0x59, 0x03, 0x02}, // 0x15 + {0x00, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x00}, // 0x16 + {0x80, 0x94, 0xB6, 0xFF, 0xFF, 0xB6, 0x94, 0x80}, // 0x17 + {0x00, 0x04, 0x06, 0x7F, 0x7F, 0x06, 0x04, 0x00}, // 0x18 + {0x00, 0x10, 0x30, 0x7F, 0x7F, 0x30, 0x10, 0x00}, // 0x19 + {0x08, 0x08, 0x08, 0x2A, 0x3E, 0x1C, 0x08, 0x00}, // 0x1A + {0x08, 0x1C, 0x3E, 0x2A, 0x08, 0x08, 0x08, 0x00}, // 0x1B + {0x3C, 0x3C, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00}, // 0x1C + {0x08, 0x1C, 0x3E, 0x08, 0x08, 0x3E, 0x1C, 0x08}, // 0x1D + {0x30, 0x38, 0x3C, 0x3E, 0x3E, 0x3C, 0x38, 0x30}, // 0x1E + {0x06, 0x0E, 0x1E, 0x3E, 0x3E, 0x1E, 0x0E, 0x06}, // 0x1F + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // ' ' + {0x00, 0x06, 0x5F, 0x5F, 0x06, 0x00, 0x00, 0x00}, // '!' + {0x00, 0x07, 0x07, 0x00, 0x07, 0x07, 0x00, 0x00}, // '"' + {0x14, 0x7F, 0x7F, 0x14, 0x7F, 0x7F, 0x14, 0x00}, // '#' + {0x24, 0x2E, 0x6B, 0x6B, 0x3A, 0x12, 0x00, 0x00}, // '$' + {0x46, 0x66, 0x30, 0x18, 0x0C, 0x66, 0x62, 0x00}, // '%' + {0x30, 0x7A, 0x4F, 0x5D, 0x37, 0x7A, 0x48, 0x00}, // '&' + {0x04, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00}, // ''' + {0x00, 0x1C, 0x3E, 0x63, 0x41, 0x00, 0x00, 0x00}, // '(' + {0x00, 0x41, 0x63, 0x3E, 0x1C, 0x00, 0x00, 0x00}, // ')' + {0x08, 0x2A, 0x3E, 0x1C, 0x1C, 0x3E, 0x2A, 0x08}, // '*' + {0x08, 0x08, 0x3E, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '+' + {0x00, 0x80, 0xE0, 0x60, 0x00, 0x00, 0x00, 0x00}, // ',' + {0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // '-' + {0x00, 0x00, 0x60, 0x60, 0x00, 0x00, 0x00, 0x00}, // '.' + {0x60, 0x30, 0x18, 0x0C, 0x06, 0x03, 0x01, 0x00}, // '/' + {0x3E, 0x7F, 0x71, 0x59, 0x4D, 0x7F, 0x3E, 0x00}, // '0' + {0x40, 0x42, 0x7F, 0x7F, 0x40, 0x40, 0x00, 0x00}, // '1' + {0x62, 0x73, 0x59, 0x49, 0x6F, 0x66, 0x00, 0x00}, // '2' + {0x22, 0x63, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '3' + {0x18, 0x1C, 0x16, 0x53, 0x7F, 0x7F, 0x50, 0x00}, // '4' + {0x27, 0x67, 0x45, 0x45, 0x7D, 0x39, 0x00, 0x00}, // '5' + {0x3C, 0x7E, 0x4B, 0x49, 0x79, 0x30, 0x00, 0x00}, // '6' + {0x03, 0x03, 0x71, 0x79, 0x0F, 0x07, 0x00, 0x00}, // '7' + {0x36, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00, 0x00}, // '8' + {0x06, 0x4F, 0x49, 0x69, 0x3F, 0x1E, 0x00, 0x00}, // '9' + {0x00, 0x00, 0x66, 0x66, 0x00, 0x00, 0x00, 0x00}, // ':' + {0x00, 0x80, 0xE6, 0x66, 0x00, 0x00, 0x00, 0x00}, // ';' + {0x08, 0x1C, 0x36, 0x63, 0x41, 0x00, 0x00, 0x00}, // '<' + {0x24, 0x24, 0x24, 0x24, 0x24, 0x24, 0x00, 0x00}, // '=' + {0x00, 0x41, 0x63, 0x36, 0x1C, 0x08, 0x00, 0x00}, // '>' + {0x02, 0x03, 0x51, 0x59, 0x0F, 0x06, 0x00, 0x00}, // '?' + {0x3E, 0x7F, 0x41, 0x5D, 0x5D, 0x1F, 0x1E, 0x00}, // '@' + {0x7C, 0x7E, 0x13, 0x13, 0x7E, 0x7C, 0x00, 0x00}, // 'A' + {0x41, 0x7F, 0x7F, 0x49, 0x49, 0x7F, 0x36, 0x00}, // 'B' + {0x1C, 0x3E, 0x63, 0x41, 0x41, 0x63, 0x22, 0x00}, // 'C' + {0x41, 0x7F, 0x7F, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'D' + {0x41, 0x7F, 0x7F, 0x49, 0x5D, 0x41, 0x63, 0x00}, // 'E' + {0x41, 0x7F, 0x7F, 0x49, 0x1D, 0x01, 0x03, 0x00}, // 'F' + {0x1C, 0x3E, 0x63, 0x41, 0x51, 0x73, 0x72, 0x00}, // 'G' + {0x7F, 0x7F, 0x08, 0x08, 0x7F, 0x7F, 0x00, 0x00}, // 'H' + {0x00, 0x41, 0x7F, 0x7F, 0x41, 0x00, 0x00, 0x00}, // 'I' + {0x30, 0x70, 0x40, 0x41, 0x7F, 0x3F, 0x01, 0x00}, // 'J' + {0x41, 0x7F, 0x7F, 0x08, 0x1C, 0x77, 0x63, 0x00}, // 'K' + {0x41, 0x7F, 0x7F, 0x41, 0x40, 0x60, 0x70, 0x00}, // 'L' + {0x7F, 0x7F, 0x0E, 0x1C, 0x0E, 0x7F, 0x7F, 0x00}, // 'M' + {0x7F, 0x7F, 0x06, 0x0C, 0x18, 0x7F, 0x7F, 0x00}, // 'N' + {0x1C, 0x3E, 0x63, 0x41, 0x63, 0x3E, 0x1C, 0x00}, // 'O' + {0x41, 0x7F, 0x7F, 0x49, 0x09, 0x0F, 0x06, 0x00}, // 'P' + {0x1E, 0x3F, 0x21, 0x71, 0x7F, 0x5E, 0x00, 0x00}, // 'Q' + {0x41, 0x7F, 0x7F, 0x09, 0x19, 0x7F, 0x66, 0x00}, // 'R' + {0x26, 0x6F, 0x4D, 0x59, 0x73, 0x32, 0x00, 0x00}, // 'S' + {0x03, 0x41, 0x7F, 0x7F, 0x41, 0x03, 0x00, 0x00}, // 'T' + {0x7F, 0x7F, 0x40, 0x40, 0x7F, 0x7F, 0x00, 0x00}, // 'U' + {0x1F, 0x3F, 0x60, 0x60, 0x3F, 0x1F, 0x00, 0x00}, // 'V' + {0x7F, 0x7F, 0x30, 0x18, 0x30, 0x7F, 0x7F, 0x00}, // 'W' + {0x43, 0x67, 0x3C, 0x18, 0x3C, 0x67, 0x43, 0x00}, // 'X' + {0x07, 0x4F, 0x78, 0x78, 0x4F, 0x07, 0x00, 0x00}, // 'Y' + {0x47, 0x63, 0x71, 0x59, 0x4D, 0x67, 0x73, 0x00}, // 'Z' + {0x00, 0x7F, 0x7F, 0x41, 0x41, 0x00, 0x00, 0x00}, // '[' + {0x01, 0x03, 0x06, 0x0C, 0x18, 0x30, 0x60, 0x00}, // backslash + {0x00, 0x41, 0x41, 0x7F, 0x7F, 0x00, 0x00, 0x00}, // ']' + {0x08, 0x0C, 0x06, 0x03, 0x06, 0x0C, 0x08, 0x00}, // '^' + {0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80}, // '_' + {0x00, 0x00, 0x03, 0x07, 0x04, 0x00, 0x00, 0x00}, // '`' + {0x20, 0x74, 0x54, 0x54, 0x3C, 0x78, 0x40, 0x00}, // 'a' + {0x41, 0x7F, 0x3F, 0x48, 0x48, 0x78, 0x30, 0x00}, // 'b' + {0x38, 0x7C, 0x44, 0x44, 0x6C, 0x28, 0x00, 0x00}, // 'c' + {0x30, 0x78, 0x48, 0x49, 0x3F, 0x7F, 0x40, 0x00}, // 'd' + {0x38, 0x7C, 0x54, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 'e' + {0x48, 0x7E, 0x7F, 0x49, 0x03, 0x02, 0x00, 0x00}, // 'f' + {0x98, 0xBC, 0xA4, 0xA4, 0xF8, 0x7C, 0x04, 0x00}, // 'g' + {0x41, 0x7F, 0x7F, 0x08, 0x04, 0x7C, 0x78, 0x00}, // 'h' + {0x00, 0x44, 0x7D, 0x7D, 0x40, 0x00, 0x00, 0x00}, // 'i' + {0x60, 0xE0, 0x80, 0x80, 0xFD, 0x7D, 0x00, 0x00}, // 'j' + {0x41, 0x7F, 0x7F, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'k' + {0x00, 0x41, 0x7F, 0x7F, 0x40, 0x00, 0x00, 0x00}, // 'l' + {0x7C, 0x7C, 0x18, 0x38, 0x1C, 0x7C, 0x78, 0x00}, // 'm' + {0x7C, 0x7C, 0x04, 0x04, 0x7C, 0x78, 0x00, 0x00}, // 'n' + {0x38, 0x7C, 0x44, 0x44, 0x7C, 0x38, 0x00, 0x00}, // 'o' + {0x84, 0xFC, 0xF8, 0xA4, 0x24, 0x3C, 0x18, 0x00}, // 'p' + {0x18, 0x3C, 0x24, 0xA4, 0xF8, 0xFC, 0x84, 0x00}, // 'q' + {0x44, 0x7C, 0x78, 0x4C, 0x04, 0x1C, 0x18, 0x00}, // 'r' + {0x48, 0x5C, 0x54, 0x54, 0x74, 0x24, 0x00, 0x00}, // 's' + {0x00, 0x04, 0x3E, 0x7F, 0x44, 0x24, 0x00, 0x00}, // 't' + {0x3C, 0x7C, 0x40, 0x40, 0x3C, 0x7C, 0x40, 0x00}, // 'u' + {0x1C, 0x3C, 0x60, 0x60, 0x3C, 0x1C, 0x00, 0x00}, // 'v' + {0x3C, 0x7C, 0x70, 0x38, 0x70, 0x7C, 0x3C, 0x00}, // 'w' + {0x44, 0x6C, 0x38, 0x10, 0x38, 0x6C, 0x44, 0x00}, // 'x' + {0x9C, 0xBC, 0xA0, 0xA0, 0xFC, 0x7C, 0x00, 0x00}, // 'y' + {0x4C, 0x64, 0x74, 0x5C, 0x4C, 0x64, 0x00, 0x00}, // 'z' + {0x08, 0x08, 0x3E, 0x77, 0x41, 0x41, 0x00, 0x00}, // '{' + {0x00, 0x00, 0x00, 0x77, 0x77, 0x00, 0x00, 0x00}, // '|' + {0x41, 0x41, 0x77, 0x3E, 0x08, 0x08, 0x00, 0x00}, // '}' + {0x02, 0x03, 0x01, 0x03, 0x02, 0x03, 0x01, 0x00}, // '~' + {0x70, 0x78, 0x4C, 0x46, 0x4C, 0x78, 0x70, 0x00}, // 0x7F + {0x0E, 0x9F, 0x91, 0xB1, 0xFB, 0x4A, 0x00, 0x00}, // 0x80 + {0x3A, 0x7A, 0x40, 0x40, 0x7A, 0x7A, 0x40, 0x00}, // 0x81 + {0x38, 0x7C, 0x54, 0x55, 0x5D, 0x19, 0x00, 0x00}, // 0x82 + {0x02, 0x23, 0x75, 0x55, 0x55, 0x7D, 0x7B, 0x42}, // 0x83 + {0x21, 0x75, 0x54, 0x54, 0x7D, 0x79, 0x40, 0x00}, // 0x84 + {0x21, 0x75, 0x55, 0x54, 0x7C, 0x78, 0x40, 0x00}, // 0x85 + {0x20, 0x74, 0x57, 0x57, 0x7C, 0x78, 0x40, 0x00}, // 0x86 + {0x18, 0x3C, 0xA4, 0xA4, 0xE4, 0x40, 0x00, 0x00}, // 0x87 + {0x02, 0x3B, 0x7D, 0x55, 0x55, 0x5D, 0x1B, 0x02}, // 0x88 + {0x39, 0x7D, 0x54, 0x54, 0x5D, 0x19, 0x00, 0x00}, // 0x89 + {0x39, 0x7D, 0x55, 0x54, 0x5C, 0x18, 0x00, 0x00}, // 0x8A + {0x01, 0x45, 0x7C, 0x7C, 0x41, 0x01, 0x00, 0x00}, // 0x8B + {0x02, 0x03, 0x45, 0x7D, 0x7D, 0x43, 0x02, 0x00}, // 0x8C + {0x01, 0x45, 0x7D, 0x7C, 0x40, 0x00, 0x00, 0x00}, // 0x8D + {0x79, 0x7D, 0x16, 0x12, 0x16, 0x7D, 0x79, 0x00}, // 0x8E + {0x70, 0x78, 0x2B, 0x2B, 0x78, 0x70, 0x00, 0x00}, // 0x8F + {0x44, 0x7C, 0x7C, 0x55, 0x55, 0x45, 0x00, 0x00}, // 0x90 + {0x20, 0x74, 0x54, 0x54, 0x7C, 0x7C, 0x54, 0x54}, // 0x91 + {0x7C, 0x7E, 0x0B, 0x09, 0x7F, 0x7F, 0x49, 0x00}, // 0x92 + {0x32, 0x7B, 0x49, 0x49, 0x7B, 0x32, 0x00, 0x00}, // 0x93 + {0x32, 0x7A, 0x48, 0x48, 0x7A, 0x32, 0x00, 0x00}, // 0x94 + {0x32, 0x7A, 0x4A, 0x48, 0x78, 0x30, 0x00, 0x00}, // 0x95 + {0x3A, 0x7B, 0x41, 0x41, 0x7B, 0x7A, 0x40, 0x00}, // 0x96 + {0x3A, 0x7A, 0x42, 0x40, 0x78, 0x78, 0x40, 0x00}, // 0x97 + {0x9A, 0xBA, 0xA0, 0xA0, 0xFA, 0x7A, 0x00, 0x00}, // 0x98 + {0x01, 0x19, 0x3C, 0x66, 0x66, 0x3C, 0x19, 0x01}, // 0x99 + {0x3D, 0x7D, 0x40, 0x40, 0x7D, 0x3D, 0x00, 0x00}, // 0x9A + {0x18, 0x3C, 0x24, 0xE7, 0xE7, 0x24, 0x24, 0x00}, // 0x9B + {0x68, 0x7E, 0x7F, 0x49, 0x43, 0x66, 0x20, 0x00}, // 0x9C + {0x2B, 0x2F, 0xFC, 0xFC, 0x2F, 0x2B, 0x00, 0x00}, // 0x9D + {0xFF, 0xFF, 0x09, 0x09, 0x2F, 0xF6, 0xF8, 0xA0}, // 0x9E + {0x40, 0xC0, 0x88, 0xFE, 0x7F, 0x09, 0x03, 0x02}, // 0x9F + {0x20, 0x74, 0x54, 0x55, 0x7D, 0x79, 0x40, 0x00}, // 0xA0 + {0x00, 0x44, 0x7D, 0x7D, 0x41, 0x00, 0x00, 0x00}, // 0xA1 + {0x30, 0x78, 0x48, 0x4A, 0x7A, 0x32, 0x00, 0x00}, // 0xA2 + {0x38, 0x78, 0x40, 0x42, 0x7A, 0x7A, 0x40, 0x00}, // 0xA3 + {0x7A, 0x7A, 0x0A, 0x0A, 0x7A, 0x70, 0x00, 0x00}, // 0xA4 + {0x7D, 0x7D, 0x19, 0x31, 0x7D, 0x7D, 0x00, 0x00}, // 0xA5 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x2F, 0x28, 0x00}, // 0xA6 + {0x00, 0x26, 0x2F, 0x29, 0x2F, 0x26, 0x00, 0x00}, // 0xA7 + {0x30, 0x78, 0x4D, 0x45, 0x60, 0x20, 0x00, 0x00}, // 0xA8 + {0x38, 0x38, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00}, // 0xA9 + {0x08, 0x08, 0x08, 0x08, 0x38, 0x38, 0x00, 0x00}, // 0xAA + {0x4F, 0x6F, 0x30, 0x18, 0xCC, 0xEE, 0xBB, 0x91}, // 0xAB + {0x4F, 0x6F, 0x30, 0x18, 0x6C, 0x76, 0xFB, 0xF9}, // 0xAC + {0x00, 0x00, 0x00, 0x7B, 0x7B, 0x00, 0x00, 0x00}, // 0xAD + {0x08, 0x1C, 0x36, 0x22, 0x08, 0x1C, 0x36, 0x22}, // 0xAE + {0x22, 0x36, 0x1C, 0x08, 0x22, 0x36, 0x1C, 0x08}, // 0xAF + {0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00}, // 0xB0 + {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55}, // 0xB1 + {0xDD, 0xFF, 0xAA, 0x77, 0xDD, 0xAA, 0xFF, 0x77}, // 0xB2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB3 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB4 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x00, 0x00, 0x00}, // 0xB5 + {0x10, 0x10, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xB6 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x00}, // 0xB7 + {0x14, 0x14, 0x14, 0xFC, 0xFC, 0x00, 0x00, 0x00}, // 0xB8 + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xFF, 0xFF, 0x00}, // 0xB9 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x00}, // 0xBA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xFC, 0xFC, 0x00}, // 0xBB + {0x14, 0x14, 0x17, 0x17, 0x10, 0x1F, 0x1F, 0x00}, // 0xBC + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x00}, // 0xBD + {0x14, 0x14, 0x14, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xBE + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x00, 0x00, 0x00}, // 0xBF + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC0 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x10, 0x10, 0x10}, // 0xC1 + {0x10, 0x10, 0x10, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xC2 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC3 + {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}, // 0xC4 + {0x10, 0x10, 0x10, 0xFF, 0xFF, 0x10, 0x10, 0x10}, // 0xC5 + {0x00, 0x00, 0x00, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xC6 + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0x10}, // 0xC7 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x17, 0x17, 0x14}, // 0xC8 + {0x00, 0x00, 0xFC, 0xFC, 0x04, 0xF4, 0xF4, 0x14}, // 0xC9 + {0x14, 0x14, 0x17, 0x17, 0x10, 0x17, 0x17, 0x14}, // 0xCA + {0x14, 0x14, 0xF4, 0xF4, 0x04, 0xF4, 0xF4, 0x14}, // 0xCB + {0x00, 0x00, 0xFF, 0xFF, 0x00, 0xF7, 0xF7, 0x14}, // 0xCC + {0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14, 0x14}, // 0xCD + {0x14, 0x14, 0xF7, 0xF7, 0x00, 0xF7, 0xF7, 0x14}, // 0xCE + {0x14, 0x14, 0x14, 0x17, 0x17, 0x14, 0x14, 0x14}, // 0xCF + {0x10, 0x10, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD0 + {0x14, 0x14, 0x14, 0xF4, 0xF4, 0x14, 0x14, 0x14}, // 0xD1 + {0x10, 0x10, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD2 + {0x00, 0x00, 0x1F, 0x1F, 0x10, 0x1F, 0x1F, 0x10}, // 0xD3 + {0x00, 0x00, 0x00, 0x1F, 0x1F, 0x14, 0x14, 0x14}, // 0xD4 + {0x00, 0x00, 0x00, 0xFC, 0xFC, 0x14, 0x14, 0x14}, // 0xD5 + {0x00, 0x00, 0xF0, 0xF0, 0x10, 0xF0, 0xF0, 0x10}, // 0xD6 + {0x10, 0x10, 0xFF, 0xFF, 0x10, 0xFF, 0xFF, 0x10}, // 0xD7 + {0x14, 0x14, 0x14, 0xFF, 0xFF, 0x14, 0x14, 0x14}, // 0xD8 + {0x10, 0x10, 0x10, 0x1F, 0x1F, 0x00, 0x00, 0x00}, // 0xD9 + {0x00, 0x00, 0x00, 0xF0, 0xF0, 0x10, 0x10, 0x10}, // 0xDA + {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDB + {0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0, 0xF0}, // 0xDC + {0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00}, // 0xDD + {0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}, // 0xDE + {0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F}, // 0xDF + {0x38, 0x7C, 0x44, 0x6C, 0x38, 0x6C, 0x44, 0x00}, // 0xE0 + {0xFC, 0xFE, 0x2A, 0x2A, 0x3E, 0x14, 0x00, 0x00}, // 0xE1 + {0x7E, 0x7E, 0x02, 0x02, 0x06, 0x06, 0x00, 0x00}, // 0xE2 + {0x02, 0x7E, 0x7E, 0x02, 0x7E, 0x7E, 0x02, 0x00}, // 0xE3 + {0x63, 0x77, 0x5D, 0x49, 0x63, 0x63, 0x00, 0x00}, // 0xE4 + {0x38, 0x7C, 0x44, 0x7C, 0x3C, 0x04, 0x04, 0x00}, // 0xE5 + {0x80, 0xFE, 0x7E, 0x20, 0x20, 0x3E, 0x1E, 0x00}, // 0xE6 + {0x04, 0x06, 0x02, 0x7E, 0x7C, 0x06, 0x02, 0x00}, // 0xE7 + {0x99, 0xBD, 0xE7, 0xE7, 0xBD, 0x99, 0x00, 0x00}, // 0xE8 + {0x1C, 0x3E, 0x6B, 0x49, 0x6B, 0x3E, 0x1C, 0x00}, // 0xE9 + {0x4C, 0x7E, 0x73, 0x01, 0x73, 0x7E, 0x4C, 0x00}, // 0xEA + {0x30, 0x78, 0x4A, 0x4F, 0x7D, 0x39, 0x00, 0x00}, // 0xEB + {0x18, 0x3C, 0x24, 0x3C, 0x3C, 0x24, 0x3C, 0x18}, // 0xEC + {0x98, 0xFC, 0x64, 0x3C, 0x3E, 0x27, 0x3D, 0x18}, // 0xED + {0x1C, 0x3E, 0x6B, 0x49, 0x49, 0x00, 0x00, 0x00}, // 0xEE + {0x7E, 0x7F, 0x01, 0x01, 0x7F, 0x7E, 0x00, 0x00}, // 0xEF + {0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x00, 0x00}, // 0xF0 + {0x44, 0x44, 0x5F, 0x5F, 0x44, 0x44, 0x00, 0x00}, // 0xF1 + {0x40, 0x51, 0x5B, 0x4E, 0x44, 0x40, 0x00, 0x00}, // 0xF2 + {0x40, 0x44, 0x4E, 0x5B, 0x51, 0x40, 0x00, 0x00}, // 0xF3 + {0x00, 0x00, 0x00, 0xFE, 0xFF, 0x01, 0x07, 0x06}, // 0xF4 + {0x60, 0xE0, 0x80, 0xFF, 0x7F, 0x00, 0x00, 0x00}, // 0xF5 + {0x08, 0x08, 0x6B, 0x6B, 0x08, 0x08, 0x00, 0x00}, // 0xF6 + {0x24, 0x36, 0x12, 0x36, 0x24, 0x36, 0x12, 0x00}, // 0xF7 + {0x00, 0x06, 0x0F, 0x09, 0x0F, 0x06, 0x00, 0x00}, // 0xF8 + {0x00, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00, 0x00}, // 0xF9 + {0x00, 0x00, 0x00, 0x10, 0x10, 0x00, 0x00, 0x00}, // 0xFA + {0x10, 0x30, 0x70, 0xC0, 0xFF, 0xFF, 0x01, 0x01}, // 0xFB + {0x00, 0x1F, 0x1F, 0x01, 0x1F, 0x1E, 0x00, 0x00}, // 0xFC + {0x00, 0x19, 0x1D, 0x17, 0x12, 0x00, 0x00, 0x00}, // 0xFD + {0x00, 0x00, 0x3C, 0x3C, 0x3C, 0x3C, 0x00, 0x00}, // 0xFE + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0xFF +}; // end of MAX7219_Dot_Matrix_font + +} // namespace max7219digit +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index dded01fb67..edbf472657 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -803,6 +803,7 @@ tm1651: id: tm1651_battery clk_pin: D6 dio_pin: D5 + rf_bridge: on_code_received: - lambda: |- @@ -817,3 +818,14 @@ rf_bridge: high: 0x1234 code: 0x123456 - rf_bridge.learn + +display: + - platform: max7219digit + cs_pin: GPIO15 + num_chips: 4 + rotate_chip: 0 + intensity: 10 + scroll_mode: 'STOP' + id: my_matrix + lambda: |- + it.printdigit("hello"); From 491f7e96f0b3ab26b84b4a6fd65cf4323a40bddb Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 28 Jun 2020 16:37:36 -0500 Subject: [PATCH 038/200] Add support for ST7789V display module (as on TTGO T-Display) (#1050) * TFT-LCD ST7789V of ESP32 TTGO. This patch allows you to use TFT-LCD ST7789V of ESP32 TTGO * Lots of polish and a few tweaks * Add test * Add color to core, take 1 * Where did those tabs come from? * Fix lines too long * Added color component * Linted * Rebase, SPI fix, test * Shuffle bits * One more thing...oops * Image type fix...oops * Make display_buffer use Color * Fix BGR/RGB, remove predefined colors * Fix all the things * renamed colors to color * migrate max7219 Co-authored-by: musk95 Co-authored-by: Guillermo Ruffino --- esphome/components/color/__init__.py | 23 ++ esphome/components/display/display_buffer.cpp | 82 ++++-- esphome/components/display/display_buffer.h | 47 +-- esphome/components/image/__init__.py | 71 +++-- .../components/max7219digit/max7219digit.cpp | 4 +- .../components/max7219digit/max7219digit.h | 2 +- esphome/components/pcd8544/pcd_8544.cpp | 8 +- esphome/components/pcd8544/pcd_8544.h | 4 +- .../components/ssd1306_base/ssd1306_base.cpp | 8 +- .../components/ssd1306_base/ssd1306_base.h | 4 +- .../components/ssd1325_base/ssd1325_base.cpp | 8 +- .../components/ssd1325_base/ssd1325_base.h | 4 +- esphome/components/st7789v/__init__.py | 3 + esphome/components/st7789v/display.py | 44 +++ esphome/components/st7789v/st7789v.cpp | 274 ++++++++++++++++++ esphome/components/st7789v/st7789v.h | 151 ++++++++++ .../waveshare_epaper/waveshare_epaper.cpp | 8 +- .../waveshare_epaper/waveshare_epaper.h | 4 +- esphome/const.py | 1 + esphome/core/color.h | 161 ++++++++++ tests/test1.yaml | 17 ++ 21 files changed, 843 insertions(+), 85 deletions(-) create mode 100644 esphome/components/color/__init__.py create mode 100644 esphome/components/st7789v/__init__.py create mode 100644 esphome/components/st7789v/display.py create mode 100644 esphome/components/st7789v/st7789v.cpp create mode 100644 esphome/components/st7789v/st7789v.h create mode 100644 esphome/core/color.h diff --git a/esphome/components/color/__init__.py b/esphome/components/color/__init__.py new file mode 100644 index 0000000000..3e2e7b2c07 --- /dev/null +++ b/esphome/components/color/__init__.py @@ -0,0 +1,23 @@ +from esphome import config_validation as cv +from esphome import codegen as cg +from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE + +ColorStruct = cg.esphome_ns.struct('Color') + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(ColorStruct), + cv.Optional(CONF_RED, default=0.0): cv.percentage, + cv.Optional(CONF_GREEN, default=0.0): cv.percentage, + cv.Optional(CONF_BLUE, default=0.0): cv.percentage, + cv.Optional(CONF_WHITE, default=0.0): cv.percentage, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + cg.variable(config[CONF_ID], cg.StructInitializer( + ColorStruct, + ('r', config[CONF_RED]), + ('g', config[CONF_GREEN]), + ('b', config[CONF_BLUE]), + ('w', config[CONF_WHITE]))) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 453e114338..cd28e45071 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -7,8 +7,8 @@ namespace display { static const char *TAG = "display"; -const uint8_t COLOR_OFF = 0; -const uint8_t COLOR_ON = 1; +const Color COLOR_OFF = 0; +const Color COLOR_ON = 1; void DisplayBuffer::init_internal_(uint32_t buffer_length) { this->buffer_ = new uint8_t[buffer_length]; @@ -18,7 +18,7 @@ void DisplayBuffer::init_internal_(uint32_t buffer_length) { } this->clear(); } -void DisplayBuffer::fill(int color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } +void DisplayBuffer::fill(Color color) { this->filled_rectangle(0, 0, this->get_width(), this->get_height(), color); } void DisplayBuffer::clear() { this->fill(COLOR_OFF); } int DisplayBuffer::get_width() { switch (this->rotation_) { @@ -43,7 +43,7 @@ int DisplayBuffer::get_height() { } } void DisplayBuffer::set_rotation(DisplayRotation rotation) { this->rotation_ = rotation; } -void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { +void HOT DisplayBuffer::draw_pixel_at(int x, int y, Color color) { switch (this->rotation_) { case DISPLAY_ROTATION_0_DEGREES: break; @@ -63,7 +63,7 @@ void HOT DisplayBuffer::draw_pixel_at(int x, int y, int color) { this->draw_absolute_pixel_internal(x, y, color); App.feed_wdt(); } -void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { +void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, Color color) { const int32_t dx = abs(x2 - x1), sx = x1 < x2 ? 1 : -1; const int32_t dy = -abs(y2 - y1), sy = y1 < y2 ? 1 : -1; int32_t err = dx + dy; @@ -83,29 +83,29 @@ void HOT DisplayBuffer::line(int x1, int y1, int x2, int y2, int color) { } } } -void HOT DisplayBuffer::horizontal_line(int x, int y, int width, int color) { +void HOT DisplayBuffer::horizontal_line(int x, int y, int width, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = x; i < x + width; i++) this->draw_pixel_at(i, y, color); } -void HOT DisplayBuffer::vertical_line(int x, int y, int height, int color) { +void HOT DisplayBuffer::vertical_line(int x, int y, int height, Color color) { // Future: Could be made more efficient by manipulating buffer directly in certain rotations. for (int i = y; i < y + height; i++) this->draw_pixel_at(x, i, color); } -void DisplayBuffer::rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::rectangle(int x1, int y1, int width, int height, Color color) { this->horizontal_line(x1, y1, width, color); this->horizontal_line(x1, y1 + height - 1, width, color); this->vertical_line(x1, y1, height, color); this->vertical_line(x1 + width - 1, y1, height, color); } -void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, int color) { +void DisplayBuffer::filled_rectangle(int x1, int y1, int width, int height, Color color) { // Future: Use vertical_line and horizontal_line methods depending on rotation to reduce memory accesses. for (int i = y1; i < y1 + height; i++) { this->horizontal_line(x1, i, width, color); } } -void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int color) { +void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, Color color) { int dx = -radius; int dy = 0; int err = 2 - 2 * radius; @@ -128,7 +128,7 @@ void HOT DisplayBuffer::circle(int center_x, int center_xy, int radius, int colo } } while (dx <= 0); } -void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int color) { +void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, Color color) { int dx = -int32_t(radius); int dy = 0; int err = 2 - 2 * radius; @@ -155,7 +155,7 @@ void DisplayBuffer::filled_circle(int center_x, int center_y, int radius, int co } while (dx <= 0); } -void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, TextAlign align, const char *text) { int x_start, y_start; int width, height; this->get_text_bounds(x, y, text, font, align, &x_start, &y_start, &width, &height); @@ -197,16 +197,34 @@ void DisplayBuffer::print(int x, int y, Font *font, int color, TextAlign align, i += match_length; } } -void DisplayBuffer::vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg) { +void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg) { char buffer[256]; int ret = vsnprintf(buffer, sizeof(buffer), format, arg); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::image(int x, int y, Image *image) { - for (int img_x = 0; img_x < image->get_width(); img_x++) { - for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? COLOR_ON : COLOR_OFF); +void DisplayBuffer::image(int x, int y, Image *image) { this->image(x, y, COLOR_ON, image); } +void DisplayBuffer::image(int x, int y, Color color, Image *image, bool invert) { + if (image->get_type() == BINARY) { + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + if (invert) + this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? COLOR_OFF : color); + else + this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color : COLOR_OFF); + } + } + } else if (image->get_type() == GRAYSCALE4) { + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale4_pixel(img_x, img_y)); + } + } + } else if (image->get_type() == RGB565) { + for (int img_x = 0; img_x < image->get_width(); img_x++) { + for (int img_y = 0; img_y < image->get_height(); img_y++) { + this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); + } } } } @@ -248,7 +266,7 @@ void DisplayBuffer::get_text_bounds(int x, int y, const char *text, Font *font, break; } } -void DisplayBuffer::print(int x, int y, Font *font, int color, const char *text) { +void DisplayBuffer::print(int x, int y, Font *font, Color color, const char *text) { this->print(x, y, font, color, TextAlign::TOP_LEFT, text); } void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char *text) { @@ -257,13 +275,13 @@ void DisplayBuffer::print(int x, int y, Font *font, TextAlign align, const char void DisplayBuffer::print(int x, int y, Font *font, const char *text) { this->print(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, text); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, align, format, arg); va_end(arg); } -void DisplayBuffer::printf(int x, int y, Font *font, int color, const char *format, ...) { +void DisplayBuffer::printf(int x, int y, Font *font, Color color, const char *format, ...) { va_list arg; va_start(arg, format); this->vprintf_(x, y, font, color, TextAlign::TOP_LEFT, format, arg); @@ -306,14 +324,14 @@ void DisplayBuffer::do_update_() { } } #ifdef USE_TIME -void DisplayBuffer::strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) { char buffer[64]; size_t ret = time.strftime(buffer, sizeof(buffer), format); if (ret > 0) this->print(x, y, font, color, align, buffer); } -void DisplayBuffer::strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) { +void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); } void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { @@ -431,10 +449,30 @@ bool Image::get_pixel(int x, int y) const { const uint32_t pos = x + y * width_8; return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } +int Image::get_color_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + + const uint32_t pos = (x + y * this->width_) * 2; + int color = (pgm_read_byte(this->data_start_ + pos) << 8) + (pgm_read_byte(this->data_start_ + pos + 1)); + return color; +} +int Image::get_grayscale4_pixel(int x, int y) const { + if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) + return 0; + const uint32_t pos = (x + y * this->width_) / 2; + // 2 = number of pixels per byte, 4 = pixel shift + uint8_t shift = (x % 2) * 4; + int color = (pgm_read_byte(this->data_start_ + pos) >> shift) & 0x0f; + return color; +} int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } +ImageType Image::get_type() const { return this->type_; } Image::Image(const uint8_t *data_start, int width, int height) : width_(width), height_(height), data_start_(data_start) {} +Image::Image(const uint8_t *data_start, int width, int height, int type) + : width_(width), height_(height), type_((ImageType) type), data_start_(data_start) {} DisplayPage::DisplayPage(const display_writer_t &writer) : writer_(writer) {} void DisplayPage::show() { this->parent_->show_page(this); } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index b12fad8c8a..969a5d80cd 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -3,6 +3,7 @@ #include "esphome/core/component.h" #include "esphome/core/defines.h" #include "esphome/core/automation.h" +#include "esphome/core/color.h" #ifdef USE_TIME #include "esphome/components/time/real_time_clock.h" @@ -63,9 +64,11 @@ enum class TextAlign { }; /// Turn the pixel OFF. -extern const uint8_t COLOR_OFF; +extern const Color COLOR_OFF; /// Turn the pixel ON. -extern const uint8_t COLOR_ON; +extern const Color COLOR_ON; + +enum ImageType { BINARY = 0, GRAYSCALE4 = 1, RGB565 = 2 }; enum DisplayRotation { DISPLAY_ROTATION_0_DEGREES = 0, @@ -91,7 +94,7 @@ using display_writer_t = std::function; class DisplayBuffer { public: /// Fill the entire screen with the given color. - virtual void fill(int color); + virtual void fill(Color color); /// Clear the entire screen by filling it with OFF pixels. void clear(); @@ -100,29 +103,29 @@ class DisplayBuffer { /// Get the height of the image in pixels with rotation applied. int get_height(); /// Set a single pixel at the specified coordinates to the given color. - void draw_pixel_at(int x, int y, int color = COLOR_ON); + void draw_pixel_at(int x, int y, Color color = COLOR_ON); /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color. - void line(int x1, int y1, int x2, int y2, int color = COLOR_ON); + void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON); /// Draw a horizontal line from the point [x,y] to [x+width,y] with the given color. - void horizontal_line(int x, int y, int width, int color = COLOR_ON); + void horizontal_line(int x, int y, int width, Color color = COLOR_ON); /// Draw a vertical line from the point [x,y] to [x,y+width] with the given color. - void vertical_line(int x, int y, int height, int color = COLOR_ON); + void vertical_line(int x, int y, int height, Color color = COLOR_ON); /// Draw the outline of a rectangle with the top left point at [x1,y1] and the bottom right point at /// [x1+width,y1+height]. - void rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Fill a rectangle with the top left point at [x1,y1] and the bottom right point at [x1+width,y1+height]. - void filled_rectangle(int x1, int y1, int width, int height, int color = COLOR_ON); + void filled_rectangle(int x1, int y1, int width, int height, Color color = COLOR_ON); /// Draw the outline of a circle centered around [center_x,center_y] with the radius radius with the given color. - void circle(int center_x, int center_xy, int radius, int color = COLOR_ON); + void circle(int center_x, int center_xy, int radius, Color color = COLOR_ON); /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color. - void filled_circle(int center_x, int center_y, int radius, int color = COLOR_ON); + void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -133,7 +136,7 @@ class DisplayBuffer { * @param align The alignment of the text. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, TextAlign align, const char *text); + void print(int x, int y, Font *font, Color color, TextAlign align, const char *text); /** Print `text` with the top left at [x,y] with `font`. * @@ -143,7 +146,7 @@ class DisplayBuffer { * @param color The color to draw the text with. * @param text The text to draw. */ - void print(int x, int y, Font *font, int color, const char *text); + void print(int x, int y, Font *font, Color color, const char *text); /** Print `text` with the anchor point at [x,y] with `font`. * @@ -174,7 +177,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, TextAlign align, const char *format, ...) + void printf(int x, int y, Font *font, Color color, TextAlign align, const char *format, ...) __attribute__((format(printf, 7, 8))); /** Evaluate the printf-format `format` and print the result with the top left at [x,y] with `font`. @@ -186,7 +189,7 @@ class DisplayBuffer { * @param format The format to use. * @param ... The arguments to use for the text formatting. */ - void printf(int x, int y, Font *font, int color, const char *format, ...) __attribute__((format(printf, 6, 7))); + void printf(int x, int y, Font *font, Color color, const char *format, ...) __attribute__((format(printf, 6, 7))); /** Evaluate the printf-format `format` and print the result with the anchor point at [x,y] with `font`. * @@ -220,7 +223,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, TextAlign align, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) __attribute__((format(strftime, 7, 0))); /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. @@ -232,7 +235,7 @@ class DisplayBuffer { * @param format The strftime format to use. * @param time The time to format. */ - void strftime(int x, int y, Font *font, int color, const char *format, time::ESPTime time) + void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) __attribute__((format(strftime, 6, 0))); /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. @@ -261,6 +264,7 @@ class DisplayBuffer { /// Draw the `image` with the top-left corner at [x,y] to the screen. void image(int x, int y, Image *image); + void image(int x, int y, Color color, Image *image, bool invert = false); /** Get the text bounds of the given string. * @@ -290,9 +294,9 @@ class DisplayBuffer { void set_rotation(DisplayRotation rotation); protected: - void vprintf_(int x, int y, Font *font, int color, TextAlign align, const char *format, va_list arg); + void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); - virtual void draw_absolute_pixel_internal(int x, int y, int color) = 0; + virtual void draw_absolute_pixel_internal(int x, int y, Color color) = 0; virtual int get_height_internal() = 0; @@ -378,13 +382,18 @@ class Font { class Image { public: Image(const uint8_t *data_start, int width, int height); + Image(const uint8_t *data_start, int width, int height, int type); bool get_pixel(int x, int y) const; + int get_color_pixel(int x, int y) const; + int get_grayscale4_pixel(int x, int y) const; int get_width() const; int get_height() const; + ImageType get_type() const; protected: int width_; int height_; + ImageType type_{BINARY}; const uint8_t *data_start_; }; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index d0a41a7379..62379b11bb 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -4,13 +4,15 @@ from esphome import core from esphome.components import display, font import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_FILE, CONF_ID, CONF_RESIZE +from esphome.const import CONF_FILE, CONF_ID, CONF_RESIZE, CONF_TYPE from esphome.core import CORE, HexInt _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['display'] MULTI_CONF = True +ImageType = {'binary': 0, 'grayscale4': 1, 'rgb565': 2} + Image_ = display.display_ns.class_('Image') CONF_RAW_DATA_ID = 'raw_data_id' @@ -19,6 +21,7 @@ IMAGE_SCHEMA = cv.Schema({ cv.Required(CONF_ID): cv.declare_id(Image_), cv.Required(CONF_FILE): cv.file_, cv.Optional(CONF_RESIZE): cv.dimensions, + cv.Optional(CONF_TYPE): cv.string, cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), }) @@ -37,20 +40,54 @@ def to_code(config): if CONF_RESIZE in config: image.thumbnail(config[CONF_RESIZE]) - image = image.convert('1', dither=Image.NONE) - width, height = image.size - if width > 500 or height > 500: - _LOGGER.warning("The image you requested is very big. Please consider using the resize " - "parameter") - width8 = ((width + 7) // 8) * 8 - data = [0 for _ in range(height * width8 // 8)] - for y in range(height): - for x in range(width): - if image.getpixel((x, y)): - continue - pos = x + y * width8 - data[pos // 8] |= 0x80 >> (pos % 8) + if CONF_TYPE in config: + if config[CONF_TYPE].startswith('GRAYSCALE4'): + width, height = image.size + image = image.convert('L', dither=Image.NONE) + pixels = list(image.getdata()) + data = [0 for _ in range(height * width // 2)] + pos = 0 + for pixnum, pix in enumerate(pixels): + pixshift = (pixnum % 2) * 4 + data[pos] |= (pix >> 4) << pixshift + if pixshift != 0: + pos += 1 + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['grayscale4']) + elif config[CONF_TYPE].startswith('RGB565'): + width, height = image.size + image = image.convert('RGB') + pixels = list(image.getdata()) + data = [0 for _ in range(height * width * 2)] + pos = 0 + for pix in pixels: + r = (pix[0] >> 3) & 0x1F + g = (pix[1] >> 2) & 0x3F + b = (pix[2] >> 3) & 0x1F + p = (r << 11) + (g << 5) + b + data[pos] = (p >> 8) & 0xFF + pos += 1 + data[pos] = p & 0xFF + pos += 1 + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['rgb565']) + else: + image = image.convert('1', dither=Image.NONE) + width, height = image.size + if width > 500 or height > 500: + _LOGGER.warning("The image you requested is very big. Please consider using" + " the resize parameter.") + width8 = ((width + 7) // 8) * 8 + data = [0 for _ in range(height * width8 // 8)] + for y in range(height): + for x in range(width): + if image.getpixel((x, y)): + continue + pos = x + y * width8 + data[pos // 8] |= 0x80 >> (pos % 8) - rhs = [HexInt(x) for x in data] - prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable(config[CONF_ID], prog_arr, width, height) + rhs = [HexInt(x) for x in data] + prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height) diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index b19f05143f..dff1d27370 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -123,7 +123,7 @@ int MAX7219Component::get_width_internal() { return this->num_chips_ * 8; } size_t MAX7219Component::get_buffer_length_() { return this->num_chips_ * 8; } -void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, Color color) { if (x + 1 > this->max_displaybuffer_.size()) { // Extend the display buffer in case required this->max_displaybuffer_.resize(x + 1, this->bckgrnd_); } @@ -134,7 +134,7 @@ void HOT MAX7219Component::draw_absolute_pixel_internal(int x, int y, int color) uint16_t pos = x; // X is starting at 0 top left uint8_t subpos = y; // Y is starting at 0 top left - if (color == 1) { + if (color.is_on()) { this->max_displaybuffer_[pos] |= (1 << subpos); } else { this->max_displaybuffer_[pos] &= ~(1 << subpos); diff --git a/esphome/components/max7219digit/max7219digit.h b/esphome/components/max7219digit/max7219digit.h index b032f33b58..dfd61e84e5 100644 --- a/esphome/components/max7219digit/max7219digit.h +++ b/esphome/components/max7219digit/max7219digit.h @@ -40,7 +40,7 @@ class MAX7219Component : public PollingComponent, void turn_on_off(bool on_off); - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; diff --git a/esphome/components/pcd8544/pcd_8544.cpp b/esphome/components/pcd8544/pcd_8544.cpp index ed9d1bbd43..e47d71e8af 100644 --- a/esphome/components/pcd8544/pcd_8544.cpp +++ b/esphome/components/pcd8544/pcd_8544.cpp @@ -85,14 +85,14 @@ void HOT PCD8544::display() { this->command(this->PCD8544_SETYADDR); } -void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT PCD8544::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) { return; } uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y % 8; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); @@ -117,8 +117,8 @@ void PCD8544::update() { this->display(); } -void PCD8544::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void PCD8544::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/pcd8544/pcd_8544.h b/esphome/components/pcd8544/pcd_8544.h index a1c247bf7b..4c590b402c 100644 --- a/esphome/components/pcd8544/pcd_8544.h +++ b/esphome/components/pcd8544/pcd_8544.h @@ -43,7 +43,7 @@ class PCD8544 : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -51,7 +51,7 @@ class PCD8544 : public PollingComponent, } protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; void setup_pins_(); diff --git a/esphome/components/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index d60f7dc985..2c1e5c6de8 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -179,20 +179,20 @@ size_t SSD1306::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; } -void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y & 0x07; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); } } -void SSD1306::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1306::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 8adf3c1b87..3e46ef9cc7 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -32,7 +32,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; @@ -41,7 +41,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { bool is_sh1106_() const; - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 22dc51e790..044582804f 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -144,20 +144,20 @@ size_t SSD1325::get_buffer_length_() { return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; } -void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; uint16_t pos = x + (y / 8) * this->get_width_internal(); uint8_t subpos = y % 8; - if (color) { + if (color.is_on()) { this->buffer_[pos] |= (1 << subpos); } else { this->buffer_[pos] &= ~(1 << subpos); } } -void SSD1325::fill(int color) { - uint8_t fill = color ? 0xFF : 0x00; +void SSD1325::fill(Color color) { + uint8_t fill = color.is_on() ? 0xFF : 0x00; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index e796b85a33..b5b28dfbae 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -28,14 +28,14 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } float get_setup_priority() const override { return setup_priority::PROCESSOR; } - void fill(int color) override; + void fill(Color color) override; protected: virtual void command(uint8_t value) = 0; virtual void write_display_data() = 0; void init_reset_(); - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; int get_height_internal() override; int get_width_internal() override; diff --git a/esphome/components/st7789v/__init__.py b/esphome/components/st7789v/__init__.py new file mode 100644 index 0000000000..dc85fa6b76 --- /dev/null +++ b/esphome/components/st7789v/__init__.py @@ -0,0 +1,3 @@ +import esphome.codegen as cg + +st7789v_ns = cg.esphome_ns.namespace('st7789v') diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py new file mode 100644 index 0000000000..4723daf0e4 --- /dev/null +++ b/esphome/components/st7789v/display.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display, spi +from esphome.const import CONF_BACKLIGHT_PIN, CONF_BRIGHTNESS, CONF_CS_PIN, CONF_DC_PIN, CONF_ID, \ + CONF_LAMBDA, CONF_RESET_PIN +from . import st7789v_ns + +DEPENDENCIES = ['spi'] + +ST7789V = st7789v_ns.class_('ST7789V', cg.PollingComponent, spi.SPIDevice, + display.DisplayBuffer) +ST7789VRef = ST7789V.operator('ref') + +CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ST7789V), + cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) + + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + + bl = yield cg.gpio_pin_expression(config[CONF_BACKLIGHT_PIN]) + cg.add(var.set_backlight_pin(bl)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) + + yield display.register_display(var, config) diff --git a/esphome/components/st7789v/st7789v.cpp b/esphome/components/st7789v/st7789v.cpp new file mode 100644 index 0000000000..284f2342fc --- /dev/null +++ b/esphome/components/st7789v/st7789v.cpp @@ -0,0 +1,274 @@ +#include "st7789v.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace st7789v { + +static const char *TAG = "st7789v"; + +void ST7789V::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI ST7789V..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + + this->init_reset_(); + + this->write_command_(ST7789_SLPOUT); // Sleep out + delay(120); // NOLINT + + this->write_command_(ST7789_NORON); // Normal display mode on + + // *** display and color format setting *** + this->write_command_(ST7789_MADCTL); + this->write_data_(ST7789_MADCTL_COLOR_ORDER); + + // JLX240 display datasheet + this->write_command_(0xB6); + this->write_data_(0x0A); + this->write_data_(0x82); + + this->write_command_(ST7789_COLMOD); + this->write_data_(0x55); + delay(10); + + // *** ST7789V Frame rate setting *** + this->write_command_(ST7789_PORCTRL); + this->write_data_(0x0c); + this->write_data_(0x0c); + this->write_data_(0x00); + this->write_data_(0x33); + this->write_data_(0x33); + + this->write_command_(ST7789_GCTRL); // Voltages: VGH / VGL + this->write_data_(0x35); + + // *** ST7789V Power setting *** + this->write_command_(ST7789_VCOMS); + this->write_data_(0x28); // JLX240 display datasheet + + this->write_command_(ST7789_LCMCTRL); + this->write_data_(0x0C); + + this->write_command_(ST7789_VDVVRHEN); + this->write_data_(0x01); + this->write_data_(0xFF); + + this->write_command_(ST7789_VRHS); // voltage VRHS + this->write_data_(0x10); + + this->write_command_(ST7789_VDVS); + this->write_data_(0x20); + + this->write_command_(ST7789_FRCTRL2); + this->write_data_(0x0f); + + this->write_command_(ST7789_PWCTRL1); + this->write_data_(0xa4); + this->write_data_(0xa1); + + // *** ST7789V gamma setting *** + this->write_command_(ST7789_PVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x32); + this->write_data_(0x44); + this->write_data_(0x42); + this->write_data_(0x06); + this->write_data_(0x0e); + this->write_data_(0x12); + this->write_data_(0x14); + this->write_data_(0x17); + + this->write_command_(ST7789_NVGAMCTRL); + this->write_data_(0xd0); + this->write_data_(0x00); + this->write_data_(0x02); + this->write_data_(0x07); + this->write_data_(0x0a); + this->write_data_(0x28); + this->write_data_(0x31); + this->write_data_(0x54); + this->write_data_(0x47); + this->write_data_(0x0e); + this->write_data_(0x1c); + this->write_data_(0x17); + this->write_data_(0x1b); + this->write_data_(0x1e); + + this->write_command_(ST7789_INVON); + + // Clear display - ensures we do not see garbage at power-on + this->draw_filled_rect_(0, 0, 239, 319, 0x0000); + + delay(120); // NOLINT + + this->write_command_(ST7789_DISPON); // Display on + delay(120); // NOLINT + + backlight_(true); + + this->init_internal_(this->get_buffer_length_()); + memset(this->buffer_, 0x00, this->get_buffer_length_()); +} + +void ST7789V::dump_config() { + LOG_DISPLAY("", "SPI ST7789V", this); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" B/L Pin: ", this->backlight_pin_); + LOG_UPDATE_INTERVAL(this); +} + +float ST7789V::get_setup_priority() const { return setup_priority::PROCESSOR; } + +void ST7789V::update() { + this->do_update_(); + this->write_display_data(); +} + +void ST7789V::loop() {} + +void ST7789V::write_display_data() { + uint16_t x1 = 52; // _offsetx + uint16_t x2 = 186; // _offsetx + uint16_t y1 = 40; // _offsety + uint16_t y2 = 279; // _offsety + + this->enable(); + + // set column(x) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + // set page(y) address + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + // write display memory + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); + this->dc_pin_->digital_write(true); + + this->write_array(this->buffer_, this->get_buffer_length_()); + + this->disable(); +} + +void ST7789V::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} + +void ST7789V::backlight_(bool onoff) { + if (this->backlight_pin_ != nullptr) { + this->backlight_pin_->setup(); + this->backlight_pin_->digital_write(onoff); + } +} + +void ST7789V::write_command_(uint8_t value) { + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(value); + this->dc_pin_->digital_write(true); + this->disable(); +} + +void ST7789V::write_data_(uint8_t value) { + this->dc_pin_->digital_write(true); + this->enable(); + this->write_byte(value); + this->disable(); +} + +void ST7789V::write_addr_(uint16_t addr1, uint16_t addr2) { + static uint8_t BYTE[4]; + BYTE[0] = (addr1 >> 8) & 0xFF; + BYTE[1] = addr1 & 0xFF; + BYTE[2] = (addr2 >> 8) & 0xFF; + BYTE[3] = addr2 & 0xFF; + + this->dc_pin_->digital_write(true); + this->write_array(BYTE, 4); +} + +void ST7789V::write_color_(uint16_t color, uint16_t size) { + static uint8_t BYTE[1024]; + int index = 0; + for (int i = 0; i < size; i++) { + BYTE[index++] = (color >> 8) & 0xFF; + BYTE[index++] = color & 0xFF; + } + + this->dc_pin_->digital_write(true); + return write_array(BYTE, size * 2); +} + +int ST7789V::get_height_internal() { + return 240; // 320; +} + +int ST7789V::get_width_internal() { + return 135; // 240; +} + +size_t ST7789V::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * 2; +} + +// Draw a filled rectangle +// x1: Start X coordinate +// y1: Start Y coordinate +// x2: End X coordinate +// y2: End Y coordinate +// color: color +void ST7789V::draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color) { + // ESP_LOGD(TAG,"offset(x)=%d offset(y)=%d",dev->_offsetx,dev->_offsety); + this->enable(); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_CASET); // set column(x) address + this->dc_pin_->digital_write(true); + this->write_addr_(x1, x2); + + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RASET); // set Page(y) address + this->dc_pin_->digital_write(true); + this->write_addr_(y1, y2); + this->dc_pin_->digital_write(false); + this->write_byte(ST7789_RAMWR); // begin a write to memory + this->dc_pin_->digital_write(true); + for (int i = x1; i <= x2; i++) { + uint16_t size = y2 - y1 + 1; + this->write_color_(color, size); + } + this->disable(); +} + +void HOT ST7789V::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + auto color565 = color.to_rgb_565(); + + uint16_t pos = (x + y * this->get_width_internal()) * 2; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/st7789v/st7789v.h b/esphome/components/st7789v/st7789v.h new file mode 100644 index 0000000000..0e17e65fd7 --- /dev/null +++ b/esphome/components/st7789v/st7789v.h @@ -0,0 +1,151 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/spi/spi.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace st7789v { + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t ST7789_NOP = 0x00; // No Operation +static const uint8_t ST7789_SWRESET = 0x01; // Software Reset +static const uint8_t ST7789_RDDID = 0x04; // Read Display ID +static const uint8_t ST7789_RDDST = 0x09; // Read Display Status +static const uint8_t ST7789_RDDPM = 0x0A; // Read Display Power Mode +static const uint8_t ST7789_RDDMADCTL = 0x0B; // Read Display MADCTL +static const uint8_t ST7789_RDDCOLMOD = 0x0C; // Read Display Pixel Format +static const uint8_t ST7789_RDDIM = 0x0D; // Read Display Image Mode +static const uint8_t ST7789_RDDSM = 0x0E; // Read Display Signal Mod +static const uint8_t ST7789_RDDSDR = 0x0F; // Read Display Self-Diagnostic Resul +static const uint8_t ST7789_SLPIN = 0x10; // Sleep in +static const uint8_t ST7789_SLPOUT = 0x11; // Sleep Out +static const uint8_t ST7789_PTLON = 0x12; // Partial Display Mode O +static const uint8_t ST7789_NORON = 0x13; // Normal Display Mode O +static const uint8_t ST7789_INVOFF = 0x20; // Display Inversion Off +static const uint8_t ST7789_INVON = 0x21; // Display Inversion O +static const uint8_t ST7789_GAMSET = 0x26; // Gamma Set +static const uint8_t ST7789_DISPOFF = 0x28; // Display Off +static const uint8_t ST7789_DISPON = 0x29; // Display On +static const uint8_t ST7789_CASET = 0x2A; // Column Address Set +static const uint8_t ST7789_RASET = 0x2B; // Row Address Set +static const uint8_t ST7789_RAMWR = 0x2C; // Memory Write +static const uint8_t ST7789_RAMRD = 0x2E; // Memory Read +static const uint8_t ST7789_PTLAR = 0x30; // Partial Area +static const uint8_t ST7789_VSCRDEF = 0x33; // Vertical Scrolling Definitio +static const uint8_t ST7789_TEOFF = 0x34; // Tearing Effect Line OFF +static const uint8_t ST7789_TEON = 0x35; // Tearing Effect Line On +static const uint8_t ST7789_MADCTL = 0x36; // Memory Data Access Control +static const uint8_t ST7789_VSCSAD = 0x37; // Vertical Scroll Start Address of RAM +static const uint8_t ST7789_IDMOFF = 0x38; // Idle Mode Off +static const uint8_t ST7789_IDMON = 0x39; // Idle mode on +static const uint8_t ST7789_COLMOD = 0x3A; // Interface Pixel Format +static const uint8_t ST7789_WRMEMC = 0x3C; // Write Memory Continue +static const uint8_t ST7789_RDMEMC = 0x3E; // Read Memory Continue +static const uint8_t ST7789_STE = 0x44; // Set Tear Scanline +static const uint8_t ST7789_GSCAN = 0x45; // Get Scanlin +static const uint8_t ST7789_WRDISBV = 0x51; // Write Display Brightness +static const uint8_t ST7789_RDDISBV = 0x52; // Read Display Brightness Value +static const uint8_t ST7789_WRCTRLD = 0x53; // Write CTRL Display +static const uint8_t ST7789_RDCTRLD = 0x54; // Read CTRL Value Display +static const uint8_t ST7789_WRCACE = 0x55; // Write Content Adaptive Brightness Control and Color Enhancement +static const uint8_t ST7789_RDCABC = 0x56; // Read Content Adaptive Brightness Control +static const uint8_t ST7789_WRCABCMB = 0x5E; // Write CABC Minimum Brightnes +static const uint8_t ST7789_RDCABCMB = 0x5F; // Read CABC Minimum Brightnes +static const uint8_t ST7789_RDABCSDR = 0x68; // Read Automatic Brightness Control Self-Diagnostic Result +static const uint8_t ST7789_RDID1 = 0xDA; // Read ID1 +static const uint8_t ST7789_RDID2 = 0xDB; // Read ID2 +static const uint8_t ST7789_RDID3 = 0xDC; // Read ID3 +static const uint8_t ST7789_RAMCTRL = 0xB0; // RAM Control +static const uint8_t ST7789_RGBCTRL = 0xB1; // RGB Interface Contro +static const uint8_t ST7789_PORCTRL = 0xB2; // Porch Setting +static const uint8_t ST7789_FRCTRL1 = 0xB3; // Frame Rate Control 1 (In partial mode/ idle colors) +static const uint8_t ST7789_PARCTRL = 0xB5; // Partial mode Contro +static const uint8_t ST7789_GCTRL = 0xB7; // Gate Contro +static const uint8_t ST7789_GTADJ = 0xB8; // Gate On Timing Adjustmen +static const uint8_t ST7789_DGMEN = 0xBA; // Digital Gamma Enable +static const uint8_t ST7789_VCOMS = 0xBB; // VCOMS Setting +static const uint8_t ST7789_LCMCTRL = 0xC0; // LCM Control +static const uint8_t ST7789_IDSET = 0xC1; // ID Code Settin +static const uint8_t ST7789_VDVVRHEN = 0xC2; // VDV and VRH Command Enabl +static const uint8_t ST7789_VRHS = 0xC3; // VRH Set +static const uint8_t ST7789_VDVS = 0xC4; // VDV Set +static const uint8_t ST7789_VCMOFSET = 0xC5; // VCOMS Offset Set +static const uint8_t ST7789_FRCTRL2 = 0xC6; // Frame Rate Control in Normal Mode +static const uint8_t ST7789_CABCCTRL = 0xC7; // CABC Control +static const uint8_t ST7789_REGSEL1 = 0xC8; // Register Value Selection 1 +static const uint8_t ST7789_REGSEL2 = 0xCA; // Register Value Selection +static const uint8_t ST7789_PWMFRSEL = 0xCC; // PWM Frequency Selection +static const uint8_t ST7789_PWCTRL1 = 0xD0; // Power Control 1 +static const uint8_t ST7789_VAPVANEN = 0xD2; // Enable VAP/VAN signal output +static const uint8_t ST7789_CMD2EN = 0xDF; // Command 2 Enable +static const uint8_t ST7789_PVGAMCTRL = 0xE0; // Positive Voltage Gamma Control +static const uint8_t ST7789_NVGAMCTRL = 0xE1; // Negative Voltage Gamma Control +static const uint8_t ST7789_DGMLUTR = 0xE2; // Digital Gamma Look-up Table for Red +static const uint8_t ST7789_DGMLUTB = 0xE3; // Digital Gamma Look-up Table for Blue +static const uint8_t ST7789_GATECTRL = 0xE4; // Gate Control +static const uint8_t ST7789_SPI2EN = 0xE7; // SPI2 Enable +static const uint8_t ST7789_PWCTRL2 = 0xE8; // Power Control 2 +static const uint8_t ST7789_EQCTRL = 0xE9; // Equalize time control +static const uint8_t ST7789_PROMCTRL = 0xEC; // Program Mode Contro +static const uint8_t ST7789_PROMEN = 0xFA; // Program Mode Enabl +static const uint8_t ST7789_NVMSET = 0xFC; // NVM Setting +static const uint8_t ST7789_PROMACT = 0xFE; // Program action + +// Flags for ST7789_MADCTL +static const uint8_t ST7789_MADCTL_MY = 0x80; +static const uint8_t ST7789_MADCTL_MX = 0x40; +static const uint8_t ST7789_MADCTL_MV = 0x20; +static const uint8_t ST7789_MADCTL_ML = 0x10; +static const uint8_t ST7789_MADCTL_RGB = 0x00; +static const uint8_t ST7789_MADCTL_BGR = 0x08; +static const uint8_t ST7789_MADCTL_MH = 0x04; +static const uint8_t ST7789_MADCTL_SS = 0x02; +static const uint8_t ST7789_MADCTL_GS = 0x01; + +static const uint8_t ST7789_MADCTL_COLOR_ORDER = ST7789_MADCTL_BGR; + +class ST7789V : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { this->dc_pin_ = dc_pin; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_backlight_pin(GPIOPin *backlight_pin) { this->backlight_pin_ = backlight_pin; } + + // ========== INTERNAL METHODS ========== + // (In most use cases you won't need these) + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void loop() override; + + void write_display_data(); + + protected: + GPIOPin *dc_pin_; + GPIOPin *reset_pin_{nullptr}; + GPIOPin *backlight_pin_{nullptr}; + + void init_reset_(); + void backlight_(bool onoff); + void write_command_(uint8_t value); + void write_data_(uint8_t value); + void write_addr_(uint16_t addr1, uint16_t addr2); + void write_color_(uint16_t color, uint16_t size); + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + + void draw_filled_rect_(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t color); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; +}; + +} // namespace st7789v +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index c145fb361c..331adffb5e 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -115,20 +115,20 @@ void WaveshareEPaper::update() { this->do_update_(); this->display(); } -void WaveshareEPaper::fill(int color) { +void WaveshareEPaper::fill(Color color) { // flip logic - const uint8_t fill = color ? 0x00 : 0xFF; + const uint8_t fill = color.is_on() ? 0x00 : 0xFF; for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } -void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color) { +void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0) return; const uint32_t pos = (x + y * this->get_width_internal()) / 8u; const uint8_t subpos = x & 0x07; // flip logic - if (!color) + if (!color.is_on()) this->buffer_[pos] |= 0x80 >> subpos; else this->buffer_[pos] &= ~(0x80 >> subpos); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 1ed7475350..18748e05ca 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -26,7 +26,7 @@ class WaveshareEPaper : public PollingComponent, void update() override; - void fill(int color) override; + void fill(Color color) override; void setup() override { this->setup_pins_(); @@ -36,7 +36,7 @@ class WaveshareEPaper : public PollingComponent, void on_safe_shutdown() override; protected: - void draw_absolute_pixel_internal(int x, int y, int color) override; + void draw_absolute_pixel_internal(int x, int y, Color color) override; bool wait_until_idle_(); diff --git a/esphome/const.py b/esphome/const.py index 9626a2e00a..8b727b615c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -49,6 +49,7 @@ CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' CONF_AWAY_CONFIG = 'away_config' +CONF_BACKLIGHT_PIN = 'backlight_pin' CONF_BATTERY_LEVEL = 'battery_level' CONF_BATTERY_VOLTAGE = 'battery_voltage' CONF_BAUD_RATE = 'baud_rate' diff --git a/esphome/core/color.h b/esphome/core/color.h new file mode 100644 index 0000000000..b19340a1d3 --- /dev/null +++ b/esphome/core/color.h @@ -0,0 +1,161 @@ +#pragma once + +#include "component.h" +#include "helpers.h" + +namespace esphome { + +inline static uint8_t esp_scale8(uint8_t i, uint8_t scale) { return (uint16_t(i) * (1 + uint16_t(scale))) / 256; } + +struct Color { + union { + struct { + union { + uint8_t r; + uint8_t red; + }; + union { + uint8_t g; + uint8_t green; + }; + union { + uint8_t b; + uint8_t blue; + }; + union { + uint8_t w; + uint8_t white; + }; + }; + uint8_t raw[4]; + uint32_t raw_32; + }; + inline Color() ALWAYS_INLINE : r(0), g(0), b(0), w(0) {} // NOLINT + inline Color(float red, float green, float blue) ALWAYS_INLINE : r(uint8_t(red * 255)), + g(uint8_t(green * 255)), + b(uint8_t(blue * 255)), + w(0) {} + inline Color(float red, float green, float blue, float white) ALWAYS_INLINE : r(uint8_t(red * 255)), + g(uint8_t(green * 255)), + b(uint8_t(blue * 255)), + w(uint8_t(white * 255)) {} + inline Color(uint32_t colorcode) ALWAYS_INLINE : r((colorcode >> 16) & 0xFF), + g((colorcode >> 8) & 0xFF), + b((colorcode >> 0) & 0xFF), + w((colorcode >> 24) & 0xFF) {} + inline bool is_on() ALWAYS_INLINE { return this->raw_32 != 0; } + inline Color &operator=(const Color &rhs) ALWAYS_INLINE { + this->r = rhs.r; + this->g = rhs.g; + this->b = rhs.b; + this->w = rhs.w; + return *this; + } + inline Color &operator=(uint32_t colorcode) ALWAYS_INLINE { + this->w = (colorcode >> 24) & 0xFF; + this->r = (colorcode >> 16) & 0xFF; + this->g = (colorcode >> 8) & 0xFF; + this->b = (colorcode >> 0) & 0xFF; + return *this; + } + inline uint8_t &operator[](uint8_t x) ALWAYS_INLINE { return this->raw[x]; } + inline Color operator*(uint8_t scale) const ALWAYS_INLINE { + return Color(esp_scale8(this->red, scale), esp_scale8(this->green, scale), esp_scale8(this->blue, scale), + esp_scale8(this->white, scale)); + } + inline Color &operator*=(uint8_t scale) ALWAYS_INLINE { + this->red = esp_scale8(this->red, scale); + this->green = esp_scale8(this->green, scale); + this->blue = esp_scale8(this->blue, scale); + this->white = esp_scale8(this->white, scale); + return *this; + } + inline Color operator*(const Color &scale) const ALWAYS_INLINE { + return Color(esp_scale8(this->red, scale.red), esp_scale8(this->green, scale.green), + esp_scale8(this->blue, scale.blue), esp_scale8(this->white, scale.white)); + } + inline Color &operator*=(const Color &scale) ALWAYS_INLINE { + this->red = esp_scale8(this->red, scale.red); + this->green = esp_scale8(this->green, scale.green); + this->blue = esp_scale8(this->blue, scale.blue); + this->white = esp_scale8(this->white, scale.white); + return *this; + } + inline Color operator+(const Color &add) const ALWAYS_INLINE { + Color ret; + if (uint8_t(add.r + this->r) < this->r) + ret.r = 255; + else + ret.r = this->r + add.r; + if (uint8_t(add.g + this->g) < this->g) + ret.g = 255; + else + ret.g = this->g + add.g; + if (uint8_t(add.b + this->b) < this->b) + ret.b = 255; + else + ret.b = this->b + add.b; + if (uint8_t(add.w + this->w) < this->w) + ret.w = 255; + else + ret.w = this->w + add.w; + return ret; + } + inline Color &operator+=(const Color &add) ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator+(uint8_t add) const ALWAYS_INLINE { return (*this) + Color(add, add, add, add); } + inline Color &operator+=(uint8_t add) ALWAYS_INLINE { return *this = (*this) + add; } + inline Color operator-(const Color &subtract) const ALWAYS_INLINE { + Color ret; + if (subtract.r > this->r) + ret.r = 0; + else + ret.r = this->r - subtract.r; + if (subtract.g > this->g) + ret.g = 0; + else + ret.g = this->g - subtract.g; + if (subtract.b > this->b) + ret.b = 0; + else + ret.b = this->b - subtract.b; + if (subtract.w > this->w) + ret.w = 0; + else + ret.w = this->w - subtract.w; + return ret; + } + inline Color &operator-=(const Color &subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + inline Color operator-(uint8_t subtract) const ALWAYS_INLINE { + return (*this) - Color(subtract, subtract, subtract, subtract); + } + inline Color &operator-=(uint8_t subtract) ALWAYS_INLINE { return *this = (*this) - subtract; } + static Color random_color() { + float r = float(random_uint32()) / float(UINT32_MAX); + float g = float(random_uint32()) / float(UINT32_MAX); + float b = float(random_uint32()) / float(UINT32_MAX); + float w = float(random_uint32()) / float(UINT32_MAX); + return Color(r, g, b, w); + } + Color fade_to_white(uint8_t amnt) { return Color(1, 1, 1, 1) - (*this * amnt); } + Color fade_to_black(uint8_t amnt) { return *this * amnt; } + Color lighten(uint8_t delta) { return *this + delta; } + Color darken(uint8_t delta) { return *this - delta; } + + uint32_t to_rgb_565() const { + uint32_t color565 = + (esp_scale8(this->red, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->blue, 31) << 0); + return color565; + } + uint32_t to_bgr_565() const { + uint32_t color565 = + (esp_scale8(this->blue, 31) << 11) | (esp_scale8(this->green, 63) << 5) | (esp_scale8(this->red, 31) << 0); + return color565; + } + uint32_t to_grayscale4() const { + uint32_t gs4 = esp_scale8(this->white, 15); + return gs4; + } +}; +static const Color COLOR_BLACK(0, 0, 0); +static const Color COLOR_WHITE(1, 1, 1); +}; // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 8f06de2e40..ec09a00208 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1507,6 +1507,16 @@ interval: id(btn_left)->set_threshold(btn_left_state * 0.9); +color: + - id: kbx_red + red: 100% + green: 1% + blue: 2% + - id: kbx_blue + red: 0% + green: 1% + blue: 100% + display: - platform: lcd_gpio dimensions: 18x4 @@ -1591,6 +1601,13 @@ display: full_update_every: 30 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +- platform: st7789v + cs_pin: GPIO5 + dc_pin: GPIO16 + reset_pin: GPIO23 + backlight_pin: GPIO4 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); tm1651: id: tm1651_battery From 83a12d980edafa4511baf963c79b6a05fea87a94 Mon Sep 17 00:00:00 2001 From: rspaargaren Date: Sun, 28 Jun 2020 23:47:43 +0200 Subject: [PATCH 039/200] Vl53 long range (#1055) * Update vl53l0x_sensor.cpp Changed values for long range support * added true / false option for long range detection * debug missing , * further debug option long_range * Travis updates * added travis tests * Travis removed space should be good now * update to trigger travis again * added old test files for PR * added vl5310x sensor for compile testing * fix variable names Co-authored-by: Guillermo Ruffino --- esphome/components/vl53l0x/sensor.py | 7 ++++++- esphome/components/vl53l0x/vl53l0x_sensor.cpp | 21 +++++++++++++++---- esphome/components/vl53l0x/vl53l0x_sensor.h | 2 ++ tests/test3.yaml | 4 ++++ 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py index 6740d53e13..209016fe40 100644 --- a/esphome/components/vl53l0x/sensor.py +++ b/esphome/components/vl53l0x/sensor.py @@ -10,15 +10,20 @@ VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComp i2c.I2CDevice) CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' +CONF_LONG_RANGE = 'long_range' + CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ cv.GenerateID(): cv.declare_id(VL53L0XSensor), cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( - min=0.0, max=512.0, min_included=False, max_included=False) + min=0.0, max=512.0, min_included=False, max_included=False), + cv.Optional(CONF_LONG_RANGE, default=False): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) + cg.add(var.set_signal_rate_limit(config[CONF_SIGNAL_RATE_LIMIT])) + cg.add(var.set_long_range(config[CONF_LONG_RANGE])) yield sensor.register_sensor(var, config) yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp index 231bed99ac..8ce822352f 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.cpp +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -33,7 +33,8 @@ void VL53L0XSensor::setup() { reg(0xFF) = 0x00; reg(0x80) = 0x00; reg(0x60) |= 0x12; - + if (this->long_range_) + this->signal_rate_limit_ = 0.1; auto rate_value = static_cast(signal_rate_limit_ * 128); write_byte_16(0x44, rate_value); @@ -104,7 +105,11 @@ void VL53L0XSensor::setup() { reg(0x48) = 0x00; reg(0x30) = 0x20; reg(0xFF) = 0x00; - reg(0x30) = 0x09; + if (this->long_range_) { + reg(0x30) = 0x07; // WAS 0x09 + } else { + reg(0x30) = 0x09; + } reg(0x54) = 0x00; reg(0x31) = 0x04; reg(0x32) = 0x03; @@ -116,7 +121,11 @@ void VL53L0XSensor::setup() { reg(0x51) = 0x00; reg(0x52) = 0x96; reg(0x56) = 0x08; - reg(0x57) = 0x30; + if (this->long_range_) { + reg(0x57) = 0x50; // was 0x30 + } else { + reg(0x57) = 0x30; + } reg(0x61) = 0x00; reg(0x62) = 0x00; reg(0x64) = 0x00; @@ -153,7 +162,11 @@ void VL53L0XSensor::setup() { reg(0x44) = 0x00; reg(0x45) = 0x20; reg(0x47) = 0x08; - reg(0x48) = 0x28; + if (this->long_range_) { + reg(0x48) = 0x48; // was 0x28 + } else { + reg(0x48) = 0x28; + } reg(0x67) = 0x00; reg(0x70) = 0x04; reg(0x71) = 0x01; diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h index 1825383cee..4939a9806c 100644 --- a/esphome/components/vl53l0x/vl53l0x_sensor.h +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -29,6 +29,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c void loop() override; void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + void set_long_range(bool long_range) { long_range_ = long_range; } protected: uint32_t get_measurement_timing_budget_() { @@ -247,6 +248,7 @@ class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c } float signal_rate_limit_; + bool long_range_; uint32_t measurement_timing_budget_us_; bool initiated_read_{false}; bool waiting_for_interrupt_{false}; diff --git a/tests/test3.yaml b/tests/test3.yaml index edbf472657..9c51894e12 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -217,6 +217,10 @@ sensor: - platform: apds9960 type: proximity name: APDS9960 Proximity + - platform: vl53l0x + name: "VL53L0x Distance" + address: 0x29 + update_interval: 60s - platform: apds9960 type: clear name: APDS9960 Clear From c041cc483c9308011986c7a25e7e0ad593a0c09e Mon Sep 17 00:00:00 2001 From: vxider Date: Mon, 29 Jun 2020 05:50:20 +0800 Subject: [PATCH 040/200] fix shunt voltage / current / power reading in INA3221 (#1101) * fix shunt voltage / current / power reading in INA3221 * support nagetive shunt voltage reading * fix loss of precision --- esphome/components/ina3221/ina3221.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 3bd568f37d..a0334064ff 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -100,7 +100,7 @@ void INA3221Component::update() { this->status_set_warning(); return; } - const float shunt_voltage_v = int16_t(raw) * 40.0f / 1000000.0f; + const float shunt_voltage_v = int16_t(raw) * 40.0f / 8.0f / 1000000.0f; if (channel.shunt_voltage_sensor_ != nullptr) channel.shunt_voltage_sensor_->publish_state(shunt_voltage_v); current_a = shunt_voltage_v / channel.shunt_resistance_; From 78633c57682d5c062d3743b6b55208513e9cbf79 Mon Sep 17 00:00:00 2001 From: vxider Date: Mon, 29 Jun 2020 05:59:22 +0800 Subject: [PATCH 041/200] fix calibration (#1103) --- esphome/components/ina219/ina219.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index 1150f7c661..44d6501e36 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -120,7 +120,7 @@ void INA219Component::setup() { } this->calibration_lsb_ = lsb; - auto calibration = uint32_t(0.04096f / (0.0001 * lsb * this->shunt_resistance_ohm_)); + auto calibration = uint32_t(0.04096f / (0.000001 * lsb * this->shunt_resistance_ohm_)); ESP_LOGV(TAG, " Using LSB=%u calibration=%u", lsb, calibration); if (!this->write_byte_16(INA219_REGISTER_CALIBRATION, calibration)) { this->mark_failed(); From 2f0722598404f5f6935b294f3e4668660d15ef2e Mon Sep 17 00:00:00 2001 From: Andrew Zaborowski Date: Mon, 29 Jun 2020 00:44:15 +0200 Subject: [PATCH 042/200] Fix: Component script not stopped in certain situations (#1004) * Move stop/is_running implementation to Action base class Try to fix issue #1105. Until now if an UpdateComponentAction, a LambdaAction, or any action that can contain those inside their "else" or "then" action lists, resulted in a call to script.stop on the script that contains them, the script would continue running, because they didn't implement a stop() method. Basically only the asynchronous ones did: DelayAction, WaitUntilAction and ScriptWaitAction. With this change num_running_ in Action replaces DelayAction::num_running_ and WaitUntilAction::triggered_ to provide the same is_running logic to other actions. * Make some Action methods protected Apparently play()/stop() etc. are not meant to be called directly by users of the class and if they're called directly that would not give the expected result for the classes that have an empty play(). Make all methods except play_complex, stop_comples and is_running protected. While there also make RemoteTransmitterActionBase::encode protected. * lint * format Co-authored-by: Guillermo Ruffino --- .../components/api/homeassistant_service.h | 1 + esphome/components/binary_sensor/automation.h | 1 + esphome/components/cover/automation.h | 16 ++-- esphome/components/dfplayer/dfplayer.h | 6 +- esphome/components/display/display_buffer.h | 5 +- esphome/components/esp8266_pwm/esp8266_pwm.h | 1 - esphome/components/fan/automation.h | 3 - esphome/components/mhz19/mhz19.h | 3 + esphome/components/mqtt/mqtt_client.h | 1 + esphome/components/output/automation.h | 1 + esphome/components/pid/pid_climate.h | 8 +- esphome/components/remote_base/jvc_protocol.h | 1 + esphome/components/remote_base/lg_protocol.h | 1 + esphome/components/remote_base/nec_protocol.h | 1 + .../remote_base/panasonic_protocol.h | 1 + .../components/remote_base/pioneer_protocol.h | 1 + esphome/components/remote_base/rc5_protocol.h | 1 + esphome/components/remote_base/remote_base.h | 8 +- .../components/remote_base/samsung_protocol.h | 1 + .../components/remote_base/sony_protocol.h | 1 + .../rotary_encoder/rotary_encoder.h | 1 + esphome/components/script/script.h | 19 ++-- esphome/components/sensor/automation.h | 1 + esphome/components/servo/servo.h | 2 + esphome/components/switch/automation.h | 1 + esphome/components/text_sensor/automation.h | 1 + esphome/core/automation.h | 56 +++++++----- esphome/core/base_automation.h | 87 ++++++++----------- 28 files changed, 121 insertions(+), 109 deletions(-) diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index d68dac3b61..8a72765195 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -29,6 +29,7 @@ template class HomeAssistantServiceCallAction : public Action void add_variable(std::string key, T value) { this->variables_.push_back(TemplatableKeyValuePair(key, value)); } + void play(Ts... x) override { HomeassistantServiceResponse resp; resp.service = this->service_.value(x...); diff --git a/esphome/components/binary_sensor/automation.h b/esphome/components/binary_sensor/automation.h index e9ff37446d..6b0321628c 100644 --- a/esphome/components/binary_sensor/automation.h +++ b/esphome/components/binary_sensor/automation.h @@ -137,6 +137,7 @@ template class BinarySensorPublishAction : public Action public: explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { auto val = this->state_.value(x...); this->sensor_->publish_state(val); diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index a8eb0cdf99..0092f987f2 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -41,6 +41,10 @@ template class ControlAction : public Action { public: explicit ControlAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(bool, stop) + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + void play(Ts... x) override { auto call = this->cover_->make_call(); if (this->stop_.has_value()) @@ -52,10 +56,6 @@ template class ControlAction : public Action { call.perform(); } - TEMPLATABLE_VALUE(bool, stop) - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - protected: Cover *cover_; }; @@ -63,6 +63,10 @@ template class ControlAction : public Action { template class CoverPublishAction : public Action { public: CoverPublishAction(Cover *cover) : cover_(cover) {} + TEMPLATABLE_VALUE(float, position) + TEMPLATABLE_VALUE(float, tilt) + TEMPLATABLE_VALUE(CoverOperation, current_operation) + void play(Ts... x) override { if (this->position_.has_value()) this->cover_->position = this->position_.value(x...); @@ -73,10 +77,6 @@ template class CoverPublishAction : public Action { this->cover_->publish_state(); } - TEMPLATABLE_VALUE(float, position) - TEMPLATABLE_VALUE(float, tilt) - TEMPLATABLE_VALUE(CoverOperation, current_operation) - protected: Cover *cover_; }; diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h index 22ca11c3be..cb9686bb64 100644 --- a/esphome/components/dfplayer/dfplayer.h +++ b/esphome/components/dfplayer/dfplayer.h @@ -104,7 +104,6 @@ class DFPlayer : public uart::UARTDevice, public Component { #define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ template class ACTION_CLASS : public Action, public Parented { \ - public: \ void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ }; @@ -115,6 +114,7 @@ template class PlayFileAction : public Action, public Par public: TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { auto file = this->file_.value(x...); auto loop = this->loop_.value(x...); @@ -131,6 +131,7 @@ template class PlayFolderAction : public Action, public P TEMPLATABLE_VALUE(uint16_t, folder) TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { auto folder = this->folder_.value(x...); auto file = this->file_.value(x...); @@ -146,6 +147,7 @@ template class PlayFolderAction : public Action, public P template class SetDeviceAction : public Action, public Parented { public: TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { auto device = this->device_.value(x...); this->parent_->set_device(device); @@ -155,6 +157,7 @@ template class SetDeviceAction : public Action, public Pa template class SetVolumeAction : public Action, public Parented { public: TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { auto volume = this->volume_.value(x...); this->parent_->set_volume(volume); @@ -164,6 +167,7 @@ template class SetVolumeAction : public Action, public Pa template class SetEqAction : public Action, public Parented { public: TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { auto eq = this->eq_.value(x...); this->parent_->set_eq(eq); diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 969a5d80cd..4b84e90a08 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -400,6 +400,7 @@ class Image { template class DisplayPageShowAction : public Action { public: TEMPLATABLE_VALUE(DisplayPage *, page) + void play(Ts... x) override { auto *page = this->page_.value(x...); if (page != nullptr) { @@ -411,18 +412,18 @@ template class DisplayPageShowAction : public Action { template class DisplayPageShowNextAction : public Action { public: DisplayPageShowNextAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_next_page(); } - protected: DisplayBuffer *buffer_; }; template class DisplayPageShowPrevAction : public Action { public: DisplayPageShowPrevAction(DisplayBuffer *buffer) : buffer_(buffer) {} + void play(Ts... x) override { this->buffer_->show_prev_page(); } - protected: DisplayBuffer *buffer_; }; diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index b6839985b0..51b74f48ba 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -43,7 +43,6 @@ template class SetFrequencyAction : public Action { this->parent_->update_frequency(freq); } - protected: ESP8266PWM *parent_; }; diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index dfa72a3ea6..d96ed994e8 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -25,7 +25,6 @@ template class TurnOnAction : public Action { call.perform(); } - protected: FanState *state_; }; @@ -35,7 +34,6 @@ template class TurnOffAction : public Action { void play(Ts... x) override { this->state_->turn_off().perform(); } - protected: FanState *state_; }; @@ -45,7 +43,6 @@ template class ToggleAction : public Action { void play(Ts... x) override { this->state_->toggle().perform(); } - protected: FanState *state_; }; diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 2201fc87f0..151351be4c 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -37,6 +37,7 @@ class MHZ19Component : public PollingComponent, public uart::UARTDevice { template class MHZ19CalibrateZeroAction : public Action { public: MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } protected: @@ -46,6 +47,7 @@ template class MHZ19CalibrateZeroAction : public Action { template class MHZ19ABCEnableAction : public Action { public: MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_enable(); } protected: @@ -55,6 +57,7 @@ template class MHZ19ABCEnableAction : public Action { template class MHZ19ABCDisableAction : public Action { public: MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_disable(); } protected: diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 6f14b0c92c..2bbebff845 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -316,6 +316,7 @@ template class MQTTPublishJsonAction : public Action { TEMPLATABLE_VALUE(bool, retain) void set_payload(std::function payload) { this->payload_ = payload; } + void play(Ts... x) override { auto f = std::bind(&MQTTPublishJsonAction::encode_, this, x..., std::placeholders::_1); auto topic = this->topic_.value(x...); diff --git a/esphome/components/output/automation.h b/esphome/components/output/automation.h index 8c8a5ab61b..51c2849702 100644 --- a/esphome/components/output/automation.h +++ b/esphome/components/output/automation.h @@ -33,6 +33,7 @@ template class SetLevelAction : public Action { SetLevelAction(FloatOutput *output) : output_(output) {} TEMPLATABLE_VALUE(float, level) + void play(Ts... x) override { this->output_->set_level(this->level_.value(x...)); } protected: diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 8f379c47b4..3dae92af5f 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -71,6 +71,10 @@ template class PIDAutotuneAction : public Action { public: PIDAutotuneAction(PIDClimate *parent) : parent_(parent) {} + void set_noiseband(float noiseband) { noiseband_ = noiseband; } + void set_positive_output(float positive_output) { positive_output_ = positive_output; } + void set_negative_output(float negative_output) { negative_output_ = negative_output; } + void play(Ts... x) { auto tuner = make_unique(); tuner->set_noiseband(this->noiseband_); @@ -79,10 +83,6 @@ template class PIDAutotuneAction : public Action { this->parent_->start_autotune(std::move(tuner)); } - void set_noiseband(float noiseband) { noiseband_ = noiseband; } - void set_positive_output(float positive_output) { positive_output_ = positive_output; } - void set_negative_output(float negative_output) { negative_output_ = negative_output; } - protected: float noiseband_; float positive_output_; diff --git a/esphome/components/remote_base/jvc_protocol.h b/esphome/components/remote_base/jvc_protocol.h index 8a216f5348..fc40a6a874 100644 --- a/esphome/components/remote_base/jvc_protocol.h +++ b/esphome/components/remote_base/jvc_protocol.h @@ -23,6 +23,7 @@ DECLARE_REMOTE_PROTOCOL(JVC) template class JVCAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { JVCData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/lg_protocol.h b/esphome/components/remote_base/lg_protocol.h index b810115f58..6267560443 100644 --- a/esphome/components/remote_base/lg_protocol.h +++ b/esphome/components/remote_base/lg_protocol.h @@ -26,6 +26,7 @@ template class LGAction : public RemoteTransmitterActionBasedata_.value(x...); diff --git a/esphome/components/remote_base/nec_protocol.h b/esphome/components/remote_base/nec_protocol.h index c794991eab..593a3efe17 100644 --- a/esphome/components/remote_base/nec_protocol.h +++ b/esphome/components/remote_base/nec_protocol.h @@ -25,6 +25,7 @@ template class NECAction : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/panasonic_protocol.h b/esphome/components/remote_base/panasonic_protocol.h index b13bd3e92d..eae97a8a14 100644 --- a/esphome/components/remote_base/panasonic_protocol.h +++ b/esphome/components/remote_base/panasonic_protocol.h @@ -26,6 +26,7 @@ template class PanasonicAction : public RemoteTransmitterActionB public: TEMPLATABLE_VALUE(uint16_t, address) TEMPLATABLE_VALUE(uint32_t, command) + void encode(RemoteTransmitData *dst, Ts... x) override { PanasonicData data{}; data.address = this->address_.value(x...); diff --git a/esphome/components/remote_base/pioneer_protocol.h b/esphome/components/remote_base/pioneer_protocol.h index f93e51a033..4cac4f9f32 100644 --- a/esphome/components/remote_base/pioneer_protocol.h +++ b/esphome/components/remote_base/pioneer_protocol.h @@ -25,6 +25,7 @@ template class PioneerAction : public RemoteTransmitterActionBas public: TEMPLATABLE_VALUE(uint16_t, rc_code_1) TEMPLATABLE_VALUE(uint16_t, rc_code_2) + void encode(RemoteTransmitData *dst, Ts... x) override { PioneerData data{}; data.rc_code_1 = this->rc_code_1_.value(x...); diff --git a/esphome/components/remote_base/rc5_protocol.h b/esphome/components/remote_base/rc5_protocol.h index 2e1da74d9f..589c8d42de 100644 --- a/esphome/components/remote_base/rc5_protocol.h +++ b/esphome/components/remote_base/rc5_protocol.h @@ -26,6 +26,7 @@ template class RC5Action : public RemoteTransmitterActionBaseaddress_.value(x...); diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index 250b59e55e..916fe29c1f 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -323,6 +323,9 @@ template class RemoteTransmitterActionBase : public Actionparent_ = parent; } + TEMPLATABLE_VALUE(uint32_t, send_times); + TEMPLATABLE_VALUE(uint32_t, send_wait); + void play(Ts... x) override { auto call = this->parent_->transmit(); this->encode(call.get_data(), x...); @@ -331,12 +334,9 @@ template class RemoteTransmitterActionBase : public Action class SamsungAction : public RemoteTransmitterActionBase { public: TEMPLATABLE_VALUE(uint32_t, data) + void encode(RemoteTransmitData *dst, Ts... x) override { SamsungData data{}; data.data = this->data_.value(x...); diff --git a/esphome/components/remote_base/sony_protocol.h b/esphome/components/remote_base/sony_protocol.h index 9f0bcdf82f..aecc8ab91c 100644 --- a/esphome/components/remote_base/sony_protocol.h +++ b/esphome/components/remote_base/sony_protocol.h @@ -26,6 +26,7 @@ template class SonyAction : public RemoteTransmitterActionBasedata_.value(x...); diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 4220645478..f0e47dfe0a 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -74,6 +74,7 @@ template class RotaryEncoderSetValueAction : public Actionencoder_->set_value(this->value_.value(x...)); } protected: diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 3b97327da8..8495014f00 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -53,41 +53,34 @@ template class ScriptWaitAction : public Action, public C public: ScriptWaitAction(Script *script) : script_(script) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (!this->script_->is_running()) { - this->triggered_ = false; - this->play_next(x...); + this->play_next_(x...); return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (this->script_->is_running()) return; - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Script *script_; - bool triggered_{false}; std::tuple var_{}; }; diff --git a/esphome/components/sensor/automation.h b/esphome/components/sensor/automation.h index 079077dba0..c70fb93963 100644 --- a/esphome/components/sensor/automation.h +++ b/esphome/components/sensor/automation.h @@ -25,6 +25,7 @@ template class SensorPublishAction : public Action { public: SensorPublishAction(Sensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(float, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/servo/servo.h b/esphome/components/servo/servo.h index a37188740c..b864efc877 100644 --- a/esphome/components/servo/servo.h +++ b/esphome/components/servo/servo.h @@ -64,6 +64,7 @@ template class ServoWriteAction : public Action { public: ServoWriteAction(Servo *servo) : servo_(servo) {} TEMPLATABLE_VALUE(float, value) + void play(Ts... x) override { this->servo_->write(this->value_.value(x...)); } protected: @@ -73,6 +74,7 @@ template class ServoWriteAction : public Action { template class ServoDetachAction : public Action { public: ServoDetachAction(Servo *servo) : servo_(servo) {} + void play(Ts... x) override { this->servo_->detach(); } protected: diff --git a/esphome/components/switch/automation.h b/esphome/components/switch/automation.h index 90bdabf0f4..579daf4d24 100644 --- a/esphome/components/switch/automation.h +++ b/esphome/components/switch/automation.h @@ -73,6 +73,7 @@ template class SwitchPublishAction : public Action { public: SwitchPublishAction(Switch *a_switch) : switch_(a_switch) {} TEMPLATABLE_VALUE(bool, state) + void play(Ts... x) override { this->switch_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/components/text_sensor/automation.h b/esphome/components/text_sensor/automation.h index 496efb1cc3..6810d10b13 100644 --- a/esphome/components/text_sensor/automation.h +++ b/esphome/components/text_sensor/automation.h @@ -30,6 +30,7 @@ template class TextSensorPublishAction : public Action { public: TextSensorPublishAction(TextSensor *sensor) : sensor_(sensor) {} TEMPLATABLE_VALUE(std::string, state) + void play(Ts... x) override { this->sensor_->publish_state(this->state_.value(x...)); } protected: diff --git a/esphome/core/automation.h b/esphome/core/automation.h index cbe96a749e..02bd8bb299 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -75,45 +75,55 @@ template class ActionList; template class Action { public: - virtual void play(Ts... x) = 0; virtual void play_complex(Ts... x) { + this->num_running_++; this->play(x...); - this->play_next(x...); + this->play_next_(x...); } - void play_next(Ts... x) { - if (this->next_ != nullptr) { - this->next_->play_complex(x...); + virtual void stop_complex() { + if (num_running_) { + this->stop(); + this->num_running_ = 0; + } + this->stop_next_(); + } + virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); } + + protected: + friend ActionList; + + virtual void play(Ts... x) = 0; + void play_next_(Ts... x) { + if (this->num_running_ > 0) { + this->num_running_--; + if (this->next_ != nullptr) { + this->next_->play_complex(x...); + } } } - virtual void stop() {} - virtual void stop_complex() { - this->stop(); - this->stop_next(); + template void play_next_tuple_(const std::tuple &tuple, seq) { + this->play_next_(std::get(tuple)...); } - void stop_next() { + void play_next_tuple_(const std::tuple &tuple) { + this->play_next_tuple_(tuple, typename gens::type()); + } + + virtual void stop() {} + void stop_next_() { if (this->next_ != nullptr) { this->next_->stop_complex(); } } - virtual bool is_running() { return this->is_running_next(); } - bool is_running_next() { + + bool is_running_next_() { if (this->next_ == nullptr) return false; return this->next_->is_running(); } - void play_next_tuple(const std::tuple &tuple) { - this->play_next_tuple_(tuple, typename gens::type()); - } - - protected: - friend ActionList; - - template void play_next_tuple_(const std::tuple &tuple, seq) { - this->play_next(std::get(tuple)...); - } - Action *next_ = nullptr; + + int num_running_{0}; }; template class ActionList { diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index add3df0bb5..d2656290bc 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -108,34 +108,23 @@ template class DelayAction : public Action, public Compon TEMPLATABLE_VALUE(uint32_t, delay) - void stop() override { - this->cancel_timeout(""); - this->num_running_ = 0; - } - - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { - auto f = std::bind(&DelayAction::delay_end_, this, x...); + auto f = std::bind(&DelayAction::play_next_, this, x...); this->num_running_++; this->set_timeout(this->delay_.value(x...), f); } float get_setup_priority() const override { return setup_priority::HARDWARE; } - bool is_running() override { return this->num_running_ > 0 || this->is_running_next(); } - - protected: - void delay_end_(Ts... x) { - this->num_running_--; - this->play_next(x...); + void play(Ts... x) override { /* ignore - see play_complex */ } - int num_running_{0}; + + void stop() override { this->cancel_timeout(""); } }; template class LambdaAction : public Action { public: explicit LambdaAction(std::function &&f) : f_(std::move(f)) {} + void play(Ts... x) override { this->f_(x...); } protected: @@ -148,41 +137,40 @@ template class IfAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); - this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); + this->then_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void add_else(const std::vector *> &actions) { this->else_.add_actions(actions); - this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next(x...); })); - } - - void play(Ts... x) override { /* ignore - see play_complex */ + this->else_.add_action(new LambdaAction([this](Ts... x) { this->play_next_(x...); })); } void play_complex(Ts... x) override { + this->num_running_++; bool res = this->condition_->check(x...); if (res) { if (this->then_.empty()) { - this->play_next(x...); - } else { + this->play_next_(x...); + } else if (this->num_running_ > 0) { this->then_.play(x...); } } else { if (this->else_.empty()) { - this->play_next(x...); - } else { + this->play_next_(x...); + } else if (this->num_running_ > 0) { this->else_.play(x...); } } } + void play(Ts... x) override { /* ignore - see play_complex */ + } + void stop() override { this->then_.stop(); this->else_.stop(); } - bool is_running() override { return this->then_.is_running() || this->else_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -196,37 +184,40 @@ template class WhileAction : public Action { void add_then(const std::vector *> &actions) { this->then_.add_actions(actions); this->then_.add_action(new LambdaAction([this](Ts... x) { - if (this->condition_->check_tuple(this->var_)) { + if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) { // play again - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } } else { // condition false, play next - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } })); } - void play(Ts... x) override { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Store loop parameters this->var_ = std::make_tuple(x...); // Initial condition check if (!this->condition_->check_tuple(this->var_)) { // If new condition check failed, stop loop if running this->then_.stop(); - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); return; } - this->then_.play_tuple(this->var_); + if (this->num_running_ > 0) { + this->then_.play_tuple(this->var_); + } + } + + void play(Ts... x) override { /* ignore - see play_complex */ } void stop() override { this->then_.stop(); } - bool is_running() override { return this->then_.is_running() || this->is_running_next(); } - protected: Condition *condition_; ActionList then_; @@ -237,48 +228,44 @@ template class WaitUntilAction : public Action, public Co public: WaitUntilAction(Condition *condition) : condition_(condition) {} - void play(Ts... x) { /* ignore - see play_complex */ - } - void play_complex(Ts... x) override { + this->num_running_++; // Check if we can continue immediately. if (this->condition_->check(x...)) { - this->triggered_ = false; - this->play_next(x...); + if (this->num_running_ > 0) { + this->play_next_(x...); + } return; } this->var_ = std::make_tuple(x...); - this->triggered_ = true; this->loop(); } - void stop() override { this->triggered_ = false; } - void loop() override { - if (!this->triggered_) + if (this->num_running_ == 0) return; if (!this->condition_->check_tuple(this->var_)) { return; } - this->triggered_ = false; - this->play_next_tuple(this->var_); + this->play_next_tuple_(this->var_); } float get_setup_priority() const override { return setup_priority::DATA; } - bool is_running() override { return this->triggered_ || this->is_running_next(); } + void play(Ts... x) override { /* ignore - see play_complex */ + } protected: Condition *condition_; - bool triggered_{false}; std::tuple var_{}; }; template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} + void play(Ts... x) override { this->component_->update(); } protected: From 7487ffcbcb61740b2b3c830f8080f7dc9c357e32 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 28 Jun 2020 21:17:10 -0300 Subject: [PATCH 043/200] add test4.yaml --- .gitlab-ci.yml | 5 +++++ script/test | 1 + 2 files changed, 6 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cdb2693f46..0ed7094615 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -131,6 +131,11 @@ test3: script: - esphome tests/test3.yaml compile +test4: + <<: *test + script: + - esphome tests/test3.yaml compile + .deploy-pypi: &deploy-pypi <<: *lint stage: deploy diff --git a/script/test b/script/test index 5e91686aae..a6d99c8f62 100755 --- a/script/test +++ b/script/test @@ -9,3 +9,4 @@ set -x esphome tests/test1.yaml compile esphome tests/test2.yaml compile esphome tests/test3.yaml compile +esphome tests/test4.yaml compile From 6505c30c2b463b9f457ebc9dd32cddd399f6e485 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Tue, 30 Jun 2020 22:17:05 -0300 Subject: [PATCH 044/200] fix gitlab test4.yaml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0ed7094615..1d349c4059 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -134,7 +134,7 @@ test3: test4: <<: *test script: - - esphome tests/test3.yaml compile + - esphome tests/test4.yaml compile .deploy-pypi: &deploy-pypi <<: *lint From 31067530a5e393aeac7da3b9ad9f5a441ee35187 Mon Sep 17 00:00:00 2001 From: Jonathan Adams Date: Wed, 1 Jul 2020 18:08:17 +0100 Subject: [PATCH 045/200] Fixes esphome/issues#1192 - Save on upload bug (#1107) --- esphome/dashboard/static/js/esphome.js | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/dashboard/static/js/esphome.js b/esphome/dashboard/static/js/esphome.js index f6e9979eb2..d80e5e4ec9 100644 --- a/esphome/dashboard/static/js/esphome.js +++ b/esphome/dashboard/static/js/esphome.js @@ -735,6 +735,7 @@ document.querySelectorAll("[data-action='edit']").forEach((button) => { const closeButton = document.querySelector("#js-editor-modal [data-action='close']"); saveButton.setAttribute('data-filename', editorActiveFilename); uploadButton.setAttribute('data-filename', editorActiveFilename); + uploadButton.setAttribute('onClick', `saveFile("${editorActiveFilename}")`); if (editorActiveFilename === "secrets.yaml") { uploadButton.classList.add("disabled"); editorActiveSecrets = true; From f479eae714f54e38eae943ea0ed355312600dabb Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 1 Jul 2020 14:08:55 -0300 Subject: [PATCH 046/200] Revert "Climate bang bang enhancements (#1061)" (#1106) This reverts commit 0082c5b4597dbbe75237a14f471beefc6ee86352. --- .../bang_bang/bang_bang_climate.cpp | 520 +++--------------- .../components/bang_bang/bang_bang_climate.h | 248 ++------- esphome/components/bang_bang/climate.py | 235 +------- esphome/const.py | 22 - tests/test3.yaml | 43 -- 5 files changed, 123 insertions(+), 945 deletions(-) diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 29f08fbd89..cf527988fe 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -9,393 +9,136 @@ static const char *TAG = "bang_bang.climate"; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { this->current_temperature = state; - // required action may have changed, recompute, refresh - this->switch_to_action_(compute_action_()); - // current temperature and possibly action changed, so publish the new state + // control may have changed, recompute + this->compute_state_(); + // current temperature changed, publish state this->publish_state(); }); this->current_temperature = this->sensor_->state; - // restore all climate data, if possible + // restore set points auto restore = this->restore_state_(); if (restore.has_value()) { restore->to_call(this).perform(); } else { - // restore from defaults, change_away handles temps for us + // restore from defaults, change_away handles those for us this->mode = climate::CLIMATE_MODE_AUTO; this->change_away_(false); } - // refresh the climate action based on the restored settings - this->switch_to_action_(compute_action_()); - this->setup_complete_ = true; - this->publish_state(); -} -void BangBangClimate::refresh() { - this->switch_to_mode_(this->mode); - this->switch_to_action_(compute_action_()); - this->switch_to_fan_mode_(this->fan_mode); - this->switch_to_swing_mode_(this->swing_mode); - this->publish_state(); } void BangBangClimate::control(const climate::ClimateCall &call) { if (call.get_mode().has_value()) this->mode = *call.get_mode(); - if (call.get_fan_mode().has_value()) - this->fan_mode = *call.get_fan_mode(); - if (call.get_swing_mode().has_value()) - this->swing_mode = *call.get_swing_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); if (call.get_target_temperature_low().has_value()) this->target_temperature_low = *call.get_target_temperature_low(); if (call.get_target_temperature_high().has_value()) this->target_temperature_high = *call.get_target_temperature_high(); - if (call.get_away().has_value()) { - // setup_complete_ blocks modifying/resetting the temps immediately after boot - if (this->setup_complete_) { - this->change_away_(*call.get_away()); - } else { - this->away = *call.get_away(); - } - } - // set point validation - if (this->supports_two_points_) { - if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) - this->target_temperature_low = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) - this->target_temperature_high = this->get_traits().get_visual_max_temperature(); - if (this->target_temperature_high < this->target_temperature_low) - this->target_temperature_high = this->target_temperature_low; - } else { - if (this->target_temperature < this->get_traits().get_visual_min_temperature()) - this->target_temperature = this->get_traits().get_visual_min_temperature(); - if (this->target_temperature > this->get_traits().get_visual_max_temperature()) - this->target_temperature = this->get_traits().get_visual_max_temperature(); - } - // make any changes happen - refresh(); + if (call.get_away().has_value()) + this->change_away_(*call.get_away()); + + this->compute_state_(); + this->publish_state(); } climate::ClimateTraits BangBangClimate::traits() { auto traits = climate::ClimateTraits(); traits.set_supports_current_temperature(true); - traits.set_supports_auto_mode(this->supports_auto_); + traits.set_supports_auto_mode(true); traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_dry_mode(this->supports_dry_); - traits.set_supports_fan_only_mode(this->supports_fan_only_); traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); - traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); - traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); - traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); - traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); - traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); - traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); - traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); - traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); - traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); - traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); - traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); - traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); - traits.set_supports_two_point_target_temperature(this->supports_two_points_); + traits.set_supports_two_point_target_temperature(true); traits.set_supports_away(this->supports_away_); traits.set_supports_action(true); return traits; } -climate::ClimateAction BangBangClimate::compute_action_() { - climate::ClimateAction target_action = this->action; - if (this->supports_two_points_) { - if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || - isnan(this->target_temperature_high) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; +void BangBangClimate::compute_state_() { + if (this->mode != climate::CLIMATE_MODE_AUTO) { + // in non-auto mode, switch directly to appropriate action + // - HEAT mode -> HEATING action + // - COOL mode -> COOLING action + // - OFF mode -> OFF action (not IDLE!) + this->switch_to_action_(static_cast(this->mode)); + return; + } + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { + // if any control parameters are nan, go to OFF action (not IDLE!) + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + const bool too_cold = this->current_temperature < this->target_temperature_low; + const bool too_hot = this->current_temperature > this->target_temperature_high; - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + climate::ClimateAction target_action; + if (too_cold) { + // too cold -> enable heating if possible, else idle + if (this->supports_heat_) + target_action = climate::CLIMATE_ACTION_HEATING; + else + target_action = climate::CLIMATE_ACTION_IDLE; + } else if (too_hot) { + // too hot -> enable cooling if possible, else idle + if (this->supports_cool_) + target_action = climate::CLIMATE_ACTION_COOLING; + else target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; - case climate::CLIMATE_MODE_AUTO: - case climate::CLIMATE_MODE_COOL: - case climate::CLIMATE_MODE_HEAT: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature_high + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature_low - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; - } } else { - if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) - // if any control parameters are nan, go to OFF action (not IDLE!) - return climate::CLIMATE_ACTION_OFF; - - if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || - ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + // neither too hot nor too cold -> in range + if (this->supports_cool_ && this->supports_heat_) { + // if supports both ends, go to idle action target_action = climate::CLIMATE_ACTION_IDLE; - } - - switch (this->mode) { - case climate::CLIMATE_MODE_FAN_ONLY: - if (this->supports_fan_only_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_FAN; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_FAN) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - case climate::CLIMATE_MODE_DRY: - target_action = climate::CLIMATE_ACTION_DRYING; - break; - case climate::CLIMATE_MODE_OFF: - target_action = climate::CLIMATE_ACTION_OFF; - break; - case climate::CLIMATE_MODE_COOL: - if (this->supports_cool_) { - if (this->current_temperature > this->target_temperature + this->hysteresis_) - target_action = climate::CLIMATE_ACTION_COOLING; - else if (this->current_temperature < this->target_temperature - this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_COOLING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - case climate::CLIMATE_MODE_HEAT: - if (this->supports_heat_) { - if (this->current_temperature < this->target_temperature - this->hysteresis_) - target_action = climate::CLIMATE_ACTION_HEATING; - else if (this->current_temperature > this->target_temperature + this->hysteresis_) - if (this->action == climate::CLIMATE_ACTION_HEATING) - target_action = climate::CLIMATE_ACTION_IDLE; - } - break; - default: - break; + } else { + // else use current mode and don't change (hysteresis) + target_action = this->action; } } - // do not switch to an action that isn't enabled per the active climate mode - if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) - target_action = climate::CLIMATE_ACTION_IDLE; - if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) - target_action = climate::CLIMATE_ACTION_IDLE; - return target_action; + this->switch_to_action_(target_action); } void BangBangClimate::switch_to_action_(climate::ClimateAction action) { - // setup_complete_ helps us ensure an action is called immediately after boot - if ((action == this->action) && this->setup_complete_) + if (action == this->action) // already in target mode return; - if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || - (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && - this->setup_complete_) { + if ((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) { // switching from OFF to IDLE or vice-versa // these only have visual difference. OFF means user manually disabled, // IDLE means it's in auto mode but value is in target range. this->action = action; + this->publish_state(); return; } - if (this->prev_action_trigger_ != nullptr) { - this->prev_action_trigger_->stop(); - this->prev_action_trigger_ = nullptr; + if (this->prev_trigger_ != nullptr) { + this->prev_trigger_->stop(); + this->prev_trigger_ = nullptr; } - Trigger<> *trig = this->idle_action_trigger_; + Trigger<> *trig; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - // trig = this->idle_action_trigger_; + trig = this->idle_trigger_; break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_action_trigger_; + trig = this->cool_trigger_; break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_action_trigger_; - break; - case climate::CLIMATE_ACTION_FAN: - trig = this->fan_only_action_trigger_; - break; - case climate::CLIMATE_ACTION_DRYING: - trig = this->dry_action_trigger_; + trig = this->heat_trigger_; break; default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - action = climate::CLIMATE_ACTION_OFF; - // trig = this->idle_action_trigger_; + trig = nullptr; } assert(trig != nullptr); trig->trigger(); this->action = action; - this->prev_action_trigger_ = trig; -} -void BangBangClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { - // setup_complete_ helps us ensure an action is called immediately after boot - if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) - // already in target mode - return; - - if (this->prev_fan_mode_trigger_ != nullptr) { - this->prev_fan_mode_trigger_->stop(); - this->prev_fan_mode_trigger_ = nullptr; - } - Trigger<> *trig = this->fan_mode_auto_trigger_; - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - trig = this->fan_mode_on_trigger_; - break; - case climate::CLIMATE_FAN_OFF: - trig = this->fan_mode_off_trigger_; - break; - case climate::CLIMATE_FAN_AUTO: - // trig = this->fan_mode_auto_trigger_; - break; - case climate::CLIMATE_FAN_LOW: - trig = this->fan_mode_low_trigger_; - break; - case climate::CLIMATE_FAN_MEDIUM: - trig = this->fan_mode_medium_trigger_; - break; - case climate::CLIMATE_FAN_HIGH: - trig = this->fan_mode_high_trigger_; - break; - case climate::CLIMATE_FAN_MIDDLE: - trig = this->fan_mode_middle_trigger_; - break; - case climate::CLIMATE_FAN_FOCUS: - trig = this->fan_mode_focus_trigger_; - break; - case climate::CLIMATE_FAN_DIFFUSE: - trig = this->fan_mode_diffuse_trigger_; - break; - default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - fan_mode = climate::CLIMATE_FAN_AUTO; - // trig = this->fan_mode_auto_trigger_; - } - assert(trig != nullptr); - trig->trigger(); - this->fan_mode = fan_mode; - this->prev_fan_mode_ = fan_mode; - this->prev_fan_mode_trigger_ = trig; -} -void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { - // setup_complete_ helps us ensure an action is called immediately after boot - if ((mode == this->prev_mode_) && this->setup_complete_) - // already in target mode - return; - - if (this->prev_mode_trigger_ != nullptr) { - this->prev_mode_trigger_->stop(); - this->prev_mode_trigger_ = nullptr; - } - Trigger<> *trig = this->auto_mode_trigger_; - switch (mode) { - case climate::CLIMATE_MODE_OFF: - trig = this->off_mode_trigger_; - break; - case climate::CLIMATE_MODE_AUTO: - // trig = this->auto_mode_trigger_; - break; - case climate::CLIMATE_MODE_COOL: - trig = this->cool_mode_trigger_; - break; - case climate::CLIMATE_MODE_HEAT: - trig = this->heat_mode_trigger_; - break; - case climate::CLIMATE_MODE_FAN_ONLY: - trig = this->fan_only_mode_trigger_; - break; - case climate::CLIMATE_MODE_DRY: - trig = this->dry_mode_trigger_; - break; - default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - mode = climate::CLIMATE_MODE_AUTO; - // trig = this->auto_mode_trigger_; - } - assert(trig != nullptr); - trig->trigger(); - this->mode = mode; - this->prev_mode_ = mode; - this->prev_mode_trigger_ = trig; -} -void BangBangClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { - // setup_complete_ helps us ensure an action is called immediately after boot - if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) - // already in target mode - return; - - if (this->prev_swing_mode_trigger_ != nullptr) { - this->prev_swing_mode_trigger_->stop(); - this->prev_swing_mode_trigger_ = nullptr; - } - Trigger<> *trig = this->swing_mode_off_trigger_; - switch (swing_mode) { - case climate::CLIMATE_SWING_BOTH: - trig = this->swing_mode_both_trigger_; - break; - case climate::CLIMATE_SWING_HORIZONTAL: - trig = this->swing_mode_horizontal_trigger_; - break; - case climate::CLIMATE_SWING_OFF: - // trig = this->swing_mode_off_trigger_; - break; - case climate::CLIMATE_SWING_VERTICAL: - trig = this->swing_mode_vertical_trigger_; - break; - default: - // we cannot report an invalid mode back to HA (even if it asked for one) - // and must assume some valid value - swing_mode = climate::CLIMATE_SWING_OFF; - // trig = this->swing_mode_off_trigger_; - } - assert(trig != nullptr); - trig->trigger(); - this->swing_mode = swing_mode; - this->prev_swing_mode_ = swing_mode; - this->prev_swing_mode_trigger_ = trig; + this->prev_trigger_ = trig; + this->publish_state(); } void BangBangClimate::change_away_(bool away) { if (!away) { - if (this->supports_two_points_) { - this->target_temperature_low = this->normal_config_.default_temperature_low; - this->target_temperature_high = this->normal_config_.default_temperature_high; - } else - this->target_temperature = this->normal_config_.default_temperature; + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; } else { - if (this->supports_two_points_) { - this->target_temperature_low = this->away_config_.default_temperature_low; - this->target_temperature_high = this->away_config_.default_temperature_high; - } else - this->target_temperature = this->away_config_.default_temperature; + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; } this->away = away; } @@ -407,142 +150,23 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa this->away_config_ = away_config; } BangBangClimate::BangBangClimate() - : cool_action_trigger_(new Trigger<>()), - cool_mode_trigger_(new Trigger<>()), - dry_action_trigger_(new Trigger<>()), - dry_mode_trigger_(new Trigger<>()), - heat_action_trigger_(new Trigger<>()), - heat_mode_trigger_(new Trigger<>()), - auto_mode_trigger_(new Trigger<>()), - idle_action_trigger_(new Trigger<>()), - off_mode_trigger_(new Trigger<>()), - fan_only_action_trigger_(new Trigger<>()), - fan_only_mode_trigger_(new Trigger<>()), - fan_mode_on_trigger_(new Trigger<>()), - fan_mode_off_trigger_(new Trigger<>()), - fan_mode_auto_trigger_(new Trigger<>()), - fan_mode_low_trigger_(new Trigger<>()), - fan_mode_medium_trigger_(new Trigger<>()), - fan_mode_high_trigger_(new Trigger<>()), - fan_mode_middle_trigger_(new Trigger<>()), - fan_mode_focus_trigger_(new Trigger<>()), - fan_mode_diffuse_trigger_(new Trigger<>()), - swing_mode_both_trigger_(new Trigger<>()), - swing_mode_off_trigger_(new Trigger<>()), - swing_mode_horizontal_trigger_(new Trigger<>()), - swing_mode_vertical_trigger_(new Trigger<>()) {} -void BangBangClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } + : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } -void BangBangClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } +Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } +Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } -void BangBangClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } -void BangBangClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } -void BangBangClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { - this->supports_fan_mode_on_ = supports_fan_mode_on; -} -void BangBangClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { - this->supports_fan_mode_off_ = supports_fan_mode_off; -} -void BangBangClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { - this->supports_fan_mode_auto_ = supports_fan_mode_auto; -} -void BangBangClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { - this->supports_fan_mode_low_ = supports_fan_mode_low; -} -void BangBangClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { - this->supports_fan_mode_medium_ = supports_fan_mode_medium; -} -void BangBangClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { - this->supports_fan_mode_high_ = supports_fan_mode_high; -} -void BangBangClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { - this->supports_fan_mode_middle_ = supports_fan_mode_middle; -} -void BangBangClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { - this->supports_fan_mode_focus_ = supports_fan_mode_focus; -} -void BangBangClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { - this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; -} -void BangBangClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { - this->supports_swing_mode_both_ = supports_swing_mode_both; -} -void BangBangClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { - this->supports_swing_mode_off_ = supports_swing_mode_off; -} -void BangBangClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { - this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; -} -void BangBangClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { - this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; -} -void BangBangClimate::set_supports_two_points(bool supports_two_points) { - this->supports_two_points_ = supports_two_points; -} -Trigger<> *BangBangClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } -Trigger<> *BangBangClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } -Trigger<> *BangBangClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } -Trigger<> *BangBangClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } -Trigger<> *BangBangClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } -Trigger<> *BangBangClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } -Trigger<> *BangBangClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } -Trigger<> *BangBangClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } -Trigger<> *BangBangClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } -Trigger<> *BangBangClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } -Trigger<> *BangBangClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } -Trigger<> *BangBangClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } -Trigger<> *BangBangClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } -Trigger<> *BangBangClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } -Trigger<> *BangBangClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } -Trigger<> *BangBangClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } void BangBangClimate::dump_config() { LOG_CLIMATE("", "Bang Bang Climate", this); - if (this->supports_heat_) - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - if ((this->supports_cool_) || (this->supports_fan_only_)) - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); - ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); - ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); - ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); - ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); - ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); - ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); - ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); - ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); - ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); - ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); - ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); - if (this->supports_away_) { - if (this->supports_heat_) - ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature_low); - if ((this->supports_cool_) || (this->supports_fan_only_)) - ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", - this->away_config_.default_temperature_high); - } + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); } BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig() = default; -BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature) - : default_temperature(default_temperature) {} BangBangClimateTargetTempConfig::BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high) : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index fd2b0ce43a..84bcd51f34 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -11,13 +11,10 @@ namespace bang_bang { struct BangBangClimateTargetTempConfig { public: BangBangClimateTargetTempConfig(); - BangBangClimateTargetTempConfig(float default_temperature); BangBangClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); - float default_temperature{NAN}; float default_temperature_low{NAN}; float default_temperature_high{NAN}; - float hysteresis{NAN}; }; class BangBangClimate : public climate::Climate, public Component { @@ -26,243 +23,62 @@ class BangBangClimate : public climate::Climate, public Component { void setup() override; void dump_config() override; - void set_hysteresis(float hysteresis); void set_sensor(sensor::Sensor *sensor); - void set_supports_auto(bool supports_auto); + Trigger<> *get_idle_trigger() const; + Trigger<> *get_cool_trigger() const; void set_supports_cool(bool supports_cool); - void set_supports_dry(bool supports_dry); - void set_supports_fan_only(bool supports_fan_only); + Trigger<> *get_heat_trigger() const; void set_supports_heat(bool supports_heat); - void set_supports_fan_mode_on(bool supports_fan_mode_on); - void set_supports_fan_mode_off(bool supports_fan_mode_off); - void set_supports_fan_mode_auto(bool supports_fan_mode_auto); - void set_supports_fan_mode_low(bool supports_fan_mode_low); - void set_supports_fan_mode_medium(bool supports_fan_mode_medium); - void set_supports_fan_mode_high(bool supports_fan_mode_high); - void set_supports_fan_mode_middle(bool supports_fan_mode_middle); - void set_supports_fan_mode_focus(bool supports_fan_mode_focus); - void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); - void set_supports_swing_mode_both(bool supports_swing_mode_both); - void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); - void set_supports_swing_mode_off(bool supports_swing_mode_off); - void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); - void set_supports_two_points(bool supports_two_points); - void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); - Trigger<> *get_cool_action_trigger() const; - Trigger<> *get_dry_action_trigger() const; - Trigger<> *get_fan_only_action_trigger() const; - Trigger<> *get_heat_action_trigger() const; - Trigger<> *get_idle_action_trigger() const; - Trigger<> *get_auto_mode_trigger() const; - Trigger<> *get_cool_mode_trigger() const; - Trigger<> *get_dry_mode_trigger() const; - Trigger<> *get_fan_only_mode_trigger() const; - Trigger<> *get_heat_mode_trigger() const; - Trigger<> *get_off_mode_trigger() const; - Trigger<> *get_fan_mode_on_trigger() const; - Trigger<> *get_fan_mode_off_trigger() const; - Trigger<> *get_fan_mode_auto_trigger() const; - Trigger<> *get_fan_mode_low_trigger() const; - Trigger<> *get_fan_mode_medium_trigger() const; - Trigger<> *get_fan_mode_high_trigger() const; - Trigger<> *get_fan_mode_middle_trigger() const; - Trigger<> *get_fan_mode_focus_trigger() const; - Trigger<> *get_fan_mode_diffuse_trigger() const; - Trigger<> *get_swing_mode_both_trigger() const; - Trigger<> *get_swing_mode_horizontal_trigger() const; - Trigger<> *get_swing_mode_off_trigger() const; - Trigger<> *get_swing_mode_vertical_trigger() const; - /// Call triggers based on updated climate states (modes/actions) - void refresh(); - protected: /// Override control to change settings of the climate device. void control(const climate::ClimateCall &call) override; - /// Change the away setting, will reset target temperatures to defaults. void change_away_(bool away); - /// Return the traits of this controller. climate::ClimateTraits traits() override; - /// Re-compute the required action of this climate controller. - climate::ClimateAction compute_action_(); - - /// Switch the climate device to the given climate action. - void switch_to_action_(climate::ClimateAction action); - - /// Switch the climate device to the given climate fan mode. - void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); + /// Re-compute the state of this climate controller. + void compute_state_(); /// Switch the climate device to the given climate mode. - void switch_to_mode_(climate::ClimateMode mode); - - /// Switch the climate device to the given climate swing mode. - void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + void switch_to_action_(climate::ClimateAction action); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; - - /// Whether the controller supports auto/cooling/drying/fanning/heating. - /// - /// A false value for any given attribute means that the controller has no such action - /// (for example a thermostat, where only heating and not-heating is possible). - bool supports_auto_{false}; + /** The trigger to call when the controller should switch to idle mode. + * + * In idle mode, the controller is assumed to have both heating and cooling disabled. + */ + Trigger<> *idle_trigger_; + /** The trigger to call when the controller should switch to cooling mode. + */ + Trigger<> *cool_trigger_; + /** Whether the controller supports cooling. + * + * A false value for this attribute means that the controller has no cooling action + * (for example a thermostat, where only heating and not-heating is possible). + */ bool supports_cool_{false}; - bool supports_dry_{false}; - bool supports_fan_only_{false}; + /** The trigger to call when the controller should switch to heating mode. + * + * A null value for this attribute means that the controller has no heating action + * For example window blinds, where only cooling (blinds closed) and not-cooling + * (blinds open) is possible. + */ + Trigger<> *heat_trigger_{nullptr}; bool supports_heat_{false}; + /** A reference to the trigger that was previously active. + * + * This is so that the previous trigger can be stopped before enabling a new one. + */ + Trigger<> *prev_trigger_{nullptr}; - /// Whether the controller supports turning on or off just the fan. - /// - /// A false value for either attribute means that the controller has no fan on/off action - /// (for example a thermostat, where independent control of the fan is not possible). - bool supports_fan_mode_on_{false}; - bool supports_fan_mode_off_{false}; - - /// Whether the controller supports fan auto mode. - /// - /// A false value for this attribute means that the controller has no fan-auto action - /// (for example a thermostat, where independent control of the fan is not possible). - bool supports_fan_mode_auto_{false}; - - /// Whether the controller supports various fan speeds and/or positions. - /// - /// A false value for any given attribute means that the controller has no such fan action. - bool supports_fan_mode_low_{false}; - bool supports_fan_mode_medium_{false}; - bool supports_fan_mode_high_{false}; - bool supports_fan_mode_middle_{false}; - bool supports_fan_mode_focus_{false}; - bool supports_fan_mode_diffuse_{false}; - - /// Whether the controller supports various swing modes. - /// - /// A false value for any given attribute means that the controller has no such swing mode. - bool supports_swing_mode_both_{false}; - bool supports_swing_mode_off_{false}; - bool supports_swing_mode_horizontal_{false}; - bool supports_swing_mode_vertical_{false}; - - /// Whether the controller supports two set points - /// - /// A false value means that the controller has no such support. - bool supports_two_points_{false}; - - /// Whether the controller supports an "away" mode - /// - /// A false value means that the controller has no such mode. - bool supports_away_{false}; - - /// The trigger to call when the controller should switch to cooling action/mode. - /// - /// A null value for this attribute means that the controller has no cooling action - /// For example electric heat, where only heating (power on) and not-heating - /// (power off) is possible. - Trigger<> *cool_action_trigger_{nullptr}; - Trigger<> *cool_mode_trigger_{nullptr}; - - /// The trigger to call when the controller should switch to dry (dehumidification) mode. - /// - /// In dry mode, the controller is assumed to have both heating and cooling disabled, - /// although the system may use its cooling mechanism to achieve drying. - Trigger<> *dry_action_trigger_{nullptr}; - Trigger<> *dry_mode_trigger_{nullptr}; - - /// The trigger to call when the controller should switch to heating action/mode. - /// - /// A null value for this attribute means that the controller has no heating action - /// For example window blinds, where only cooling (blinds closed) and not-cooling - /// (blinds open) is possible. - Trigger<> *heat_action_trigger_{nullptr}; - Trigger<> *heat_mode_trigger_{nullptr}; - - /// The trigger to call when the controller should switch to auto mode. - /// - /// In auto mode, the controller will enable heating/cooling as necessary and switch - /// to idle when the temperature is within the thresholds/set points. - Trigger<> *auto_mode_trigger_{nullptr}; - - /// The trigger to call when the controller should switch to idle action/off mode. - /// - /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. - Trigger<> *idle_action_trigger_{nullptr}; - Trigger<> *off_mode_trigger_{nullptr}; - - /// The trigger to call when the controller should switch to fan-only action/mode. - /// - /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. - /// The system should activate the fan only. - Trigger<> *fan_only_action_trigger_{nullptr}; - Trigger<> *fan_only_mode_trigger_{nullptr}; - - /// The trigger to call when the controller should switch on the fan. - Trigger<> *fan_mode_on_trigger_{nullptr}; - - /// The trigger to call when the controller should switch off the fan. - Trigger<> *fan_mode_off_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "auto" mode. - Trigger<> *fan_mode_auto_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "low" speed. - Trigger<> *fan_mode_low_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "medium" speed. - Trigger<> *fan_mode_medium_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "high" speed. - Trigger<> *fan_mode_high_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "middle" position. - Trigger<> *fan_mode_middle_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "focus" position. - Trigger<> *fan_mode_focus_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "diffuse" position. - Trigger<> *fan_mode_diffuse_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "both". - Trigger<> *swing_mode_both_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "off". - Trigger<> *swing_mode_off_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "horizontal". - Trigger<> *swing_mode_horizontal_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "vertical". - Trigger<> *swing_mode_vertical_trigger_{nullptr}; - - /// A reference to the trigger that was previously active. - /// - /// This is so that the previous trigger can be stopped before enabling a new one - /// for each climate category (mode, action, fan_mode, swing_mode). - Trigger<> *prev_action_trigger_{nullptr}; - Trigger<> *prev_fan_mode_trigger_{nullptr}; - Trigger<> *prev_mode_trigger_{nullptr}; - Trigger<> *prev_swing_mode_trigger_{nullptr}; - - /// Store previously-known states - /// - /// These are used to determine when a trigger/action needs to be called - climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; - climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; - climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; - - /// Temperature data for normal/home and away modes BangBangClimateTargetTempConfig normal_config_{}; + bool supports_away_{false}; BangBangClimateTargetTempConfig away_config_{}; - - /// Hysteresis value used for computing climate actions - float hysteresis_{0}; - - /// setup_complete_ blocks modifying/resetting the temps immediately after boot - bool setup_complete_{false}; }; } // namespace bang_bang diff --git a/esphome/components/bang_bang/climate.py b/esphome/components/bang_bang/climate.py index 75ec58404b..4ef811c55d 100644 --- a/esphome/components/bang_bang/climate.py +++ b/esphome/components/bang_bang/climate.py @@ -2,107 +2,27 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import climate, sensor -from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ - CONF_DRY_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, \ - CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, CONF_FAN_MODE_HIGH_ACTION, \ - CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, \ - CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ - CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ - CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION +from esphome.const import CONF_AWAY_CONFIG, CONF_COOL_ACTION, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, \ + CONF_ID, CONF_IDLE_ACTION, CONF_SENSOR bang_bang_ns = cg.esphome_ns.namespace('bang_bang') BangBangClimate = bang_bang_ns.class_('BangBangClimate', climate.Climate, cg.Component) BangBangClimateTargetTempConfig = bang_bang_ns.struct('BangBangClimateTargetTempConfig') - -def validate_bangbang(config): - # verify corresponding climate action action exists for any defined climate mode action - if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: - raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) - if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: - raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) - if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: - raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, - CONF_FAN_ONLY_MODE)) - if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config - or CONF_FAN_ONLY_ACTION in config): - raise cv.Invalid("{} must be defined when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: - raise cv.Invalid("{} must be defined when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config - and CONF_FAN_ONLY_ACTION not in config): - raise cv.Invalid("{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: - raise cv.Invalid("{} is defined with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) - - if CONF_AWAY_CONFIG in config: - away = config[CONF_AWAY_CONFIG] - # verify corresponding default target temperature exists when a given climate action exists - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or - CONF_FAN_ONLY_ACTION in config): - raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: - raise cv.Invalid("{} must be defined in away configuration when using {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) - # if a given climate action is NOT defined, it should not have a default target temperature - if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and - CONF_FAN_ONLY_ACTION not in config): - raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) - if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: - raise cv.Invalid("{} is defined in away configuration with no {}".format( - CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) - - return config - - CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(BangBangClimate), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), - cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), - cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(single=True), - cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), - cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), - cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, - cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, - cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ - cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, - cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, }), -}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, - CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), - validate_bangbang) +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION)) def to_code(config): @@ -110,145 +30,28 @@ def to_code(config): yield cg.register_component(var, config) yield climate.register_climate(var, config) - auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config - two_points_available = CONF_HEAT_ACTION in config and (CONF_COOL_ACTION in config or - CONF_FAN_ONLY_ACTION in config) - sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) - cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) - if two_points_available is True: - cg.add(var.set_supports_two_points(True)) - normal_config = BangBangClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: - cg.add(var.set_supports_two_points(False)) - normal_config = BangBangClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: - cg.add(var.set_supports_two_points(False)) - normal_config = BangBangClimateTargetTempConfig( - config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] - ) + normal_config = BangBangClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) cg.add(var.set_normal_config(normal_config)) - yield automation.build_automation(var.get_idle_action_trigger(), [], - config[CONF_IDLE_ACTION]) - - if auto_mode_available is True: - cg.add(var.set_supports_auto(True)) - else: - cg.add(var.set_supports_auto(False)) + yield automation.build_automation(var.get_idle_trigger(), [], config[CONF_IDLE_ACTION]) if CONF_COOL_ACTION in config: - yield automation.build_automation(var.get_cool_action_trigger(), [], - config[CONF_COOL_ACTION]) + yield automation.build_automation(var.get_cool_trigger(), [], config[CONF_COOL_ACTION]) cg.add(var.set_supports_cool(True)) - if CONF_DRY_ACTION in config: - yield automation.build_automation(var.get_dry_action_trigger(), [], - config[CONF_DRY_ACTION]) - cg.add(var.set_supports_dry(True)) - if CONF_FAN_ONLY_ACTION in config: - yield automation.build_automation(var.get_fan_only_action_trigger(), [], - config[CONF_FAN_ONLY_ACTION]) - cg.add(var.set_supports_fan_only(True)) if CONF_HEAT_ACTION in config: - yield automation.build_automation(var.get_heat_action_trigger(), [], - config[CONF_HEAT_ACTION]) + yield automation.build_automation(var.get_heat_trigger(), [], config[CONF_HEAT_ACTION]) cg.add(var.set_supports_heat(True)) - if CONF_AUTO_MODE in config: - yield automation.build_automation(var.get_auto_mode_trigger(), [], - config[CONF_AUTO_MODE]) - if CONF_COOL_MODE in config: - yield automation.build_automation(var.get_cool_mode_trigger(), [], - config[CONF_COOL_MODE]) - cg.add(var.set_supports_cool(True)) - if CONF_DRY_MODE in config: - yield automation.build_automation(var.get_dry_mode_trigger(), [], - config[CONF_DRY_MODE]) - cg.add(var.set_supports_dry(True)) - if CONF_FAN_ONLY_MODE in config: - yield automation.build_automation(var.get_fan_only_mode_trigger(), [], - config[CONF_FAN_ONLY_MODE]) - cg.add(var.set_supports_fan_only(True)) - if CONF_HEAT_MODE in config: - yield automation.build_automation(var.get_heat_mode_trigger(), [], - config[CONF_HEAT_MODE]) - cg.add(var.set_supports_heat(True)) - if CONF_OFF_MODE in config: - yield automation.build_automation(var.get_off_mode_trigger(), [], - config[CONF_OFF_MODE]) - if CONF_FAN_MODE_ON_ACTION in config: - yield automation.build_automation(var.get_fan_mode_on_trigger(), [], - config[CONF_FAN_MODE_ON_ACTION]) - cg.add(var.set_supports_fan_mode_on(True)) - if CONF_FAN_MODE_OFF_ACTION in config: - yield automation.build_automation(var.get_fan_mode_off_trigger(), [], - config[CONF_FAN_MODE_OFF_ACTION]) - cg.add(var.set_supports_fan_mode_off(True)) - if CONF_FAN_MODE_AUTO_ACTION in config: - yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], - config[CONF_FAN_MODE_AUTO_ACTION]) - cg.add(var.set_supports_fan_mode_auto(True)) - if CONF_FAN_MODE_LOW_ACTION in config: - yield automation.build_automation(var.get_fan_mode_low_trigger(), [], - config[CONF_FAN_MODE_LOW_ACTION]) - cg.add(var.set_supports_fan_mode_low(True)) - if CONF_FAN_MODE_MEDIUM_ACTION in config: - yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], - config[CONF_FAN_MODE_MEDIUM_ACTION]) - cg.add(var.set_supports_fan_mode_medium(True)) - if CONF_FAN_MODE_HIGH_ACTION in config: - yield automation.build_automation(var.get_fan_mode_high_trigger(), [], - config[CONF_FAN_MODE_HIGH_ACTION]) - cg.add(var.set_supports_fan_mode_high(True)) - if CONF_FAN_MODE_MIDDLE_ACTION in config: - yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], - config[CONF_FAN_MODE_MIDDLE_ACTION]) - cg.add(var.set_supports_fan_mode_middle(True)) - if CONF_FAN_MODE_FOCUS_ACTION in config: - yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], - config[CONF_FAN_MODE_FOCUS_ACTION]) - cg.add(var.set_supports_fan_mode_focus(True)) - if CONF_FAN_MODE_DIFFUSE_ACTION in config: - yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], - config[CONF_FAN_MODE_DIFFUSE_ACTION]) - cg.add(var.set_supports_fan_mode_diffuse(True)) - if CONF_SWING_BOTH_ACTION in config: - yield automation.build_automation(var.get_swing_mode_both_trigger(), [], - config[CONF_SWING_BOTH_ACTION]) - cg.add(var.set_supports_swing_mode_both(True)) - if CONF_SWING_HORIZONTAL_ACTION in config: - yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], - config[CONF_SWING_HORIZONTAL_ACTION]) - cg.add(var.set_supports_swing_mode_horizontal(True)) - if CONF_SWING_OFF_ACTION in config: - yield automation.build_automation(var.get_swing_mode_off_trigger(), [], - config[CONF_SWING_OFF_ACTION]) - cg.add(var.set_supports_swing_mode_off(True)) - if CONF_SWING_VERTICAL_ACTION in config: - yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], - config[CONF_SWING_VERTICAL_ACTION]) - cg.add(var.set_supports_swing_mode_vertical(True)) if CONF_AWAY_CONFIG in config: away = config[CONF_AWAY_CONFIG] - - if two_points_available is True: - away_config = BangBangClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: - away_config = BangBangClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] - ) - elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: - away_config = BangBangClimateTargetTempConfig( - away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] - ) + away_config = BangBangClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) cg.add(var.set_away_config(away_config)) diff --git a/esphome/const.py b/esphome/const.py index 8b727b615c..4cdf48fc76 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -44,7 +44,6 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' -CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' @@ -103,7 +102,6 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' -CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' @@ -142,8 +140,6 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' -CONF_DRY_ACTION = 'dry_action' -CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' CONF_ECHO_PIN = 'echo_pin' @@ -163,17 +159,6 @@ CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' CONF_FAN_MODE = 'fan_mode' -CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' -CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' -CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' -CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' -CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' -CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' -CONF_FAN_MODE_MIDDLE_ACTION = 'fan_mode_middle_action' -CONF_FAN_MODE_OFF_ACTION = 'fan_mode_off_action' -CONF_FAN_MODE_ON_ACTION = 'fan_mode_on_action' -CONF_FAN_ONLY_ACTION = 'fan_only_action' -CONF_FAN_ONLY_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -198,7 +183,6 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' -CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' @@ -206,7 +190,6 @@ CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' -CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' @@ -302,7 +285,6 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' -CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' @@ -467,11 +449,7 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SWING_BOTH_ACTION = 'swing_both_action' -CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' -CONF_SWING_OFF_ACTION = 'swing_off_action' -CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test3.yaml b/tests/test3.yaml index 9c51894e12..72dde13055 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -621,49 +621,6 @@ climate: - switch.turn_on: gpio_switch2 heat_action: - switch.turn_on: gpio_switch1 - dry_action: - - switch.turn_on: gpio_switch2 - fan_only_action: - - switch.turn_on: gpio_switch1 - auto_mode: - - switch.turn_on: gpio_switch2 - off_mode: - - switch.turn_on: gpio_switch1 - heat_mode: - - switch.turn_on: gpio_switch2 - cool_mode: - - switch.turn_on: gpio_switch1 - dry_mode: - - switch.turn_on: gpio_switch2 - fan_only_mode: - - switch.turn_on: gpio_switch1 - fan_mode_auto_action: - - switch.turn_on: gpio_switch2 - fan_mode_on_action: - - switch.turn_on: gpio_switch1 - fan_mode_off_action: - - switch.turn_on: gpio_switch2 - fan_mode_low_action: - - switch.turn_on: gpio_switch1 - fan_mode_medium_action: - - switch.turn_on: gpio_switch2 - fan_mode_high_action: - - switch.turn_on: gpio_switch1 - fan_mode_middle_action: - - switch.turn_on: gpio_switch2 - fan_mode_focus_action: - - switch.turn_on: gpio_switch1 - fan_mode_diffuse_action: - - switch.turn_on: gpio_switch2 - swing_off_action: - - switch.turn_on: gpio_switch1 - swing_horizontal_action: - - switch.turn_on: gpio_switch2 - swing_vertical_action: - - switch.turn_on: gpio_switch1 - swing_both_action: - - switch.turn_on: gpio_switch2 - hysteresis: 0.2 away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C From 8566dd9100e9c54752f0bf1d7e9d21d3f4de3629 Mon Sep 17 00:00:00 2001 From: vxider Date: Thu, 2 Jul 2020 09:36:36 +0800 Subject: [PATCH 047/200] use default avg mode (#1102) --- esphome/components/ina3221/ina3221.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index a0334064ff..17492433e3 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -42,7 +42,7 @@ void INA3221Component::setup() { config |= 0b0001000000000000; } // 0b0000xxx000000000 << 9 Averaging Mode (0 -> 1 sample, 111 -> 1024 samples) - config |= 0b0000111000000000; + config |= 0b0000000000000000; // 0b0000000xxx000000 << 6 Bus Voltage Conversion time (100 -> 1.1ms, 111 -> 8.244 ms) config |= 0b0000000111000000; // 0b0000000000xxx000 << 3 Shunt Voltage Conversion time (same as above) From d4e76185bdb34f31a1be8efcbf04226b7a06596b Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 1 Jul 2020 20:38:51 -0500 Subject: [PATCH 048/200] Thermostat component (#1105) * Add thermostat component * Add hysteresis accessor * Linted * Fixed up const.py and test * Fix test...again...oops --- esphome/components/thermostat/__init__.py | 0 esphome/components/thermostat/climate.py | 254 ++++++++ .../thermostat/thermostat_climate.cpp | 552 ++++++++++++++++++ .../thermostat/thermostat_climate.h | 271 +++++++++ esphome/const.py | 22 + tests/test3.yaml | 57 ++ 6 files changed, 1156 insertions(+) create mode 100644 esphome/components/thermostat/__init__.py create mode 100644 esphome/components/thermostat/climate.py create mode 100644 esphome/components/thermostat/thermostat_climate.cpp create mode 100644 esphome/components/thermostat/thermostat_climate.h diff --git a/esphome/components/thermostat/__init__.py b/esphome/components/thermostat/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py new file mode 100644 index 0000000000..c9cb81194c --- /dev/null +++ b/esphome/components/thermostat/climate.py @@ -0,0 +1,254 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components import climate, sensor +from esphome.const import CONF_AUTO_MODE, CONF_AWAY_CONFIG, CONF_COOL_ACTION, CONF_COOL_MODE, \ + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DRY_ACTION, \ + CONF_DRY_MODE, CONF_FAN_MODE_ON_ACTION, CONF_FAN_MODE_OFF_ACTION, CONF_FAN_MODE_AUTO_ACTION, \ + CONF_FAN_MODE_LOW_ACTION, CONF_FAN_MODE_MEDIUM_ACTION, CONF_FAN_MODE_HIGH_ACTION, \ + CONF_FAN_MODE_MIDDLE_ACTION, CONF_FAN_MODE_FOCUS_ACTION, CONF_FAN_MODE_DIFFUSE_ACTION, \ + CONF_FAN_ONLY_ACTION, CONF_FAN_ONLY_MODE, CONF_HEAT_ACTION, CONF_HEAT_MODE, CONF_HYSTERESIS, \ + CONF_ID, CONF_IDLE_ACTION, CONF_OFF_MODE, CONF_SENSOR, CONF_SWING_BOTH_ACTION, \ + CONF_SWING_HORIZONTAL_ACTION, CONF_SWING_OFF_ACTION, CONF_SWING_VERTICAL_ACTION + +thermostat_ns = cg.esphome_ns.namespace('thermostat') +ThermostatClimate = thermostat_ns.class_('ThermostatClimate', climate.Climate, cg.Component) +ThermostatClimateTargetTempConfig = thermostat_ns.struct('ThermostatClimateTargetTempConfig') + + +def validate_thermostat(config): + # verify corresponding climate action action exists for any defined climate mode action + if CONF_COOL_MODE in config and CONF_COOL_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_COOL_ACTION, CONF_COOL_MODE)) + if CONF_DRY_MODE in config and CONF_DRY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_DRY_ACTION, CONF_DRY_MODE)) + if CONF_FAN_ONLY_MODE in config and CONF_FAN_ONLY_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_FAN_ONLY_ACTION, + CONF_FAN_ONLY_MODE)) + if CONF_HEAT_MODE in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} must be defined to use {}".format(CONF_HEAT_ACTION, CONF_HEAT_MODE)) + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in config and (CONF_COOL_ACTION in config + or CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in config and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config and (CONF_COOL_ACTION not in config + and CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + # verify corresponding default target temperature exists when a given climate action exists + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH not in away and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config): + raise cv.Invalid("{} must be defined in away configuration when using {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW not in away and CONF_HEAT_ACTION in config: + raise cv.Invalid("{} must be defined in away configuration when using {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + # if a given climate action is NOT defined, it should not have a default target temperature + if CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away and (CONF_COOL_ACTION not in config and + CONF_FAN_ONLY_ACTION not in config): + raise cv.Invalid("{} is defined in away configuration with no {} or {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, CONF_COOL_ACTION, CONF_FAN_ONLY_ACTION)) + if CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away and CONF_HEAT_ACTION not in config: + raise cv.Invalid("{} is defined in away configuration with no {}".format( + CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION)) + + return config + + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ThermostatClimate), + cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), + cv.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_AUTO_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_COOL_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_DRY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_AUTO_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_LOW_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MEDIUM_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_HIGH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_MIDDLE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_FOCUS_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_FAN_MODE_DIFFUSE_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_BOTH_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_HORIZONTAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_OFF_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature, + cv.Optional(CONF_AWAY_CONFIG): cv.Schema({ + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature, + cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, + }), +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_DRY_ACTION, + CONF_FAN_ONLY_ACTION, CONF_HEAT_ACTION), + validate_thermostat) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + auto_mode_available = CONF_HEAT_ACTION in config and CONF_COOL_ACTION in config + two_points_available = CONF_HEAT_ACTION in config and (CONF_COOL_ACTION in config or + CONF_FAN_ONLY_ACTION in config) + + sens = yield cg.get_variable(config[CONF_SENSOR]) + cg.add(var.set_sensor(sens)) + cg.add(var.set_hysteresis(config[CONF_HYSTERESIS])) + + if two_points_available is True: + cg.add(var.set_supports_two_points(True)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in config: + cg.add(var.set_supports_two_points(False)) + normal_config = ThermostatClimateTargetTempConfig( + config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_normal_config(normal_config)) + + yield automation.build_automation(var.get_idle_action_trigger(), [], + config[CONF_IDLE_ACTION]) + + if auto_mode_available is True: + cg.add(var.set_supports_auto(True)) + else: + cg.add(var.set_supports_auto(False)) + + if CONF_COOL_ACTION in config: + yield automation.build_automation(var.get_cool_action_trigger(), [], + config[CONF_COOL_ACTION]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_ACTION in config: + yield automation.build_automation(var.get_dry_action_trigger(), [], + config[CONF_DRY_ACTION]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_ACTION in config: + yield automation.build_automation(var.get_fan_only_action_trigger(), [], + config[CONF_FAN_ONLY_ACTION]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_ACTION in config: + yield automation.build_automation(var.get_heat_action_trigger(), [], + config[CONF_HEAT_ACTION]) + cg.add(var.set_supports_heat(True)) + if CONF_AUTO_MODE in config: + yield automation.build_automation(var.get_auto_mode_trigger(), [], + config[CONF_AUTO_MODE]) + if CONF_COOL_MODE in config: + yield automation.build_automation(var.get_cool_mode_trigger(), [], + config[CONF_COOL_MODE]) + cg.add(var.set_supports_cool(True)) + if CONF_DRY_MODE in config: + yield automation.build_automation(var.get_dry_mode_trigger(), [], + config[CONF_DRY_MODE]) + cg.add(var.set_supports_dry(True)) + if CONF_FAN_ONLY_MODE in config: + yield automation.build_automation(var.get_fan_only_mode_trigger(), [], + config[CONF_FAN_ONLY_MODE]) + cg.add(var.set_supports_fan_only(True)) + if CONF_HEAT_MODE in config: + yield automation.build_automation(var.get_heat_mode_trigger(), [], + config[CONF_HEAT_MODE]) + cg.add(var.set_supports_heat(True)) + if CONF_OFF_MODE in config: + yield automation.build_automation(var.get_off_mode_trigger(), [], + config[CONF_OFF_MODE]) + if CONF_FAN_MODE_ON_ACTION in config: + yield automation.build_automation(var.get_fan_mode_on_trigger(), [], + config[CONF_FAN_MODE_ON_ACTION]) + cg.add(var.set_supports_fan_mode_on(True)) + if CONF_FAN_MODE_OFF_ACTION in config: + yield automation.build_automation(var.get_fan_mode_off_trigger(), [], + config[CONF_FAN_MODE_OFF_ACTION]) + cg.add(var.set_supports_fan_mode_off(True)) + if CONF_FAN_MODE_AUTO_ACTION in config: + yield automation.build_automation(var.get_fan_mode_auto_trigger(), [], + config[CONF_FAN_MODE_AUTO_ACTION]) + cg.add(var.set_supports_fan_mode_auto(True)) + if CONF_FAN_MODE_LOW_ACTION in config: + yield automation.build_automation(var.get_fan_mode_low_trigger(), [], + config[CONF_FAN_MODE_LOW_ACTION]) + cg.add(var.set_supports_fan_mode_low(True)) + if CONF_FAN_MODE_MEDIUM_ACTION in config: + yield automation.build_automation(var.get_fan_mode_medium_trigger(), [], + config[CONF_FAN_MODE_MEDIUM_ACTION]) + cg.add(var.set_supports_fan_mode_medium(True)) + if CONF_FAN_MODE_HIGH_ACTION in config: + yield automation.build_automation(var.get_fan_mode_high_trigger(), [], + config[CONF_FAN_MODE_HIGH_ACTION]) + cg.add(var.set_supports_fan_mode_high(True)) + if CONF_FAN_MODE_MIDDLE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_middle_trigger(), [], + config[CONF_FAN_MODE_MIDDLE_ACTION]) + cg.add(var.set_supports_fan_mode_middle(True)) + if CONF_FAN_MODE_FOCUS_ACTION in config: + yield automation.build_automation(var.get_fan_mode_focus_trigger(), [], + config[CONF_FAN_MODE_FOCUS_ACTION]) + cg.add(var.set_supports_fan_mode_focus(True)) + if CONF_FAN_MODE_DIFFUSE_ACTION in config: + yield automation.build_automation(var.get_fan_mode_diffuse_trigger(), [], + config[CONF_FAN_MODE_DIFFUSE_ACTION]) + cg.add(var.set_supports_fan_mode_diffuse(True)) + if CONF_SWING_BOTH_ACTION in config: + yield automation.build_automation(var.get_swing_mode_both_trigger(), [], + config[CONF_SWING_BOTH_ACTION]) + cg.add(var.set_supports_swing_mode_both(True)) + if CONF_SWING_HORIZONTAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_horizontal_trigger(), [], + config[CONF_SWING_HORIZONTAL_ACTION]) + cg.add(var.set_supports_swing_mode_horizontal(True)) + if CONF_SWING_OFF_ACTION in config: + yield automation.build_automation(var.get_swing_mode_off_trigger(), [], + config[CONF_SWING_OFF_ACTION]) + cg.add(var.set_supports_swing_mode_off(True)) + if CONF_SWING_VERTICAL_ACTION in config: + yield automation.build_automation(var.get_swing_mode_vertical_trigger(), [], + config[CONF_SWING_VERTICAL_ACTION]) + cg.add(var.set_supports_swing_mode_vertical(True)) + + if CONF_AWAY_CONFIG in config: + away = config[CONF_AWAY_CONFIG] + + if two_points_available is True: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW], + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_HIGH in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH] + ) + elif CONF_DEFAULT_TARGET_TEMPERATURE_LOW in away: + away_config = ThermostatClimateTargetTempConfig( + away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW] + ) + cg.add(var.set_away_config(away_config)) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp new file mode 100644 index 0000000000..5d97156e2f --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -0,0 +1,552 @@ +#include "thermostat_climate.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace thermostat { + +static const char *TAG = "thermostat.climate"; + +void ThermostatClimate::setup() { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // required action may have changed, recompute, refresh + this->switch_to_action_(compute_action_()); + // current temperature and possibly action changed, so publish the new state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + // restore all climate data, if possible + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->to_call(this).perform(); + } else { + // restore from defaults, change_away handles temps for us + this->mode = climate::CLIMATE_MODE_AUTO; + this->change_away_(false); + } + // refresh the climate action based on the restored settings + this->switch_to_action_(compute_action_()); + this->setup_complete_ = true; + this->publish_state(); +} +float ThermostatClimate::hysteresis() { return this->hysteresis_; } +void ThermostatClimate::refresh() { + this->switch_to_mode_(this->mode); + this->switch_to_action_(compute_action_()); + this->switch_to_fan_mode_(this->fan_mode); + this->switch_to_swing_mode_(this->swing_mode); + this->publish_state(); +} +void ThermostatClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_fan_mode().has_value()) + this->fan_mode = *call.get_fan_mode(); + if (call.get_swing_mode().has_value()) + this->swing_mode = *call.get_swing_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + if (call.get_target_temperature_low().has_value()) + this->target_temperature_low = *call.get_target_temperature_low(); + if (call.get_target_temperature_high().has_value()) + this->target_temperature_high = *call.get_target_temperature_high(); + if (call.get_away().has_value()) { + // setup_complete_ blocks modifying/resetting the temps immediately after boot + if (this->setup_complete_) { + this->change_away_(*call.get_away()); + } else { + this->away = *call.get_away(); + } + } + // set point validation + if (this->supports_two_points_) { + if (this->target_temperature_low < this->get_traits().get_visual_min_temperature()) + this->target_temperature_low = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature_high > this->get_traits().get_visual_max_temperature()) + this->target_temperature_high = this->get_traits().get_visual_max_temperature(); + if (this->target_temperature_high < this->target_temperature_low) + this->target_temperature_high = this->target_temperature_low; + } else { + if (this->target_temperature < this->get_traits().get_visual_min_temperature()) + this->target_temperature = this->get_traits().get_visual_min_temperature(); + if (this->target_temperature > this->get_traits().get_visual_max_temperature()) + this->target_temperature = this->get_traits().get_visual_max_temperature(); + } + // make any changes happen + refresh(); +} +climate::ClimateTraits ThermostatClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(true); + traits.set_supports_auto_mode(this->supports_auto_); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_dry_mode(this->supports_dry_); + traits.set_supports_fan_only_mode(this->supports_fan_only_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_fan_mode_on(this->supports_fan_mode_on_); + traits.set_supports_fan_mode_off(this->supports_fan_mode_off_); + traits.set_supports_fan_mode_auto(this->supports_fan_mode_auto_); + traits.set_supports_fan_mode_low(this->supports_fan_mode_low_); + traits.set_supports_fan_mode_medium(this->supports_fan_mode_medium_); + traits.set_supports_fan_mode_high(this->supports_fan_mode_high_); + traits.set_supports_fan_mode_middle(this->supports_fan_mode_middle_); + traits.set_supports_fan_mode_focus(this->supports_fan_mode_focus_); + traits.set_supports_fan_mode_diffuse(this->supports_fan_mode_diffuse_); + traits.set_supports_swing_mode_both(this->supports_swing_mode_both_); + traits.set_supports_swing_mode_horizontal(this->supports_swing_mode_horizontal_); + traits.set_supports_swing_mode_off(this->supports_swing_mode_off_); + traits.set_supports_swing_mode_vertical(this->supports_swing_mode_vertical_); + traits.set_supports_two_point_target_temperature(this->supports_two_points_); + traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); + return traits; +} +climate::ClimateAction ThermostatClimate::compute_action_() { + climate::ClimateAction target_action = this->action; + if (this->supports_two_points_) { + if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || + isnan(this->target_temperature_high) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_AUTO: + case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_MODE_HEAT: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature_high + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature_high - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature_low - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature_low + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } else { + if (isnan(this->current_temperature) || isnan(this->target_temperature) || isnan(this->hysteresis_)) + // if any control parameters are nan, go to OFF action (not IDLE!) + return climate::CLIMATE_ACTION_OFF; + + if (((this->action == climate::CLIMATE_ACTION_FAN) && (this->mode != climate::CLIMATE_MODE_FAN_ONLY)) || + ((this->action == climate::CLIMATE_ACTION_DRYING) && (this->mode != climate::CLIMATE_MODE_DRY))) { + target_action = climate::CLIMATE_ACTION_IDLE; + } + + switch (this->mode) { + case climate::CLIMATE_MODE_FAN_ONLY: + if (this->supports_fan_only_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_FAN; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_FAN) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + case climate::CLIMATE_MODE_DRY: + target_action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_OFF: + target_action = climate::CLIMATE_ACTION_OFF; + break; + case climate::CLIMATE_MODE_COOL: + if (this->supports_cool_) { + if (this->current_temperature > this->target_temperature + this->hysteresis_) + target_action = climate::CLIMATE_ACTION_COOLING; + else if (this->current_temperature < this->target_temperature - this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_COOLING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + case climate::CLIMATE_MODE_HEAT: + if (this->supports_heat_) { + if (this->current_temperature < this->target_temperature - this->hysteresis_) + target_action = climate::CLIMATE_ACTION_HEATING; + else if (this->current_temperature > this->target_temperature + this->hysteresis_) + if (this->action == climate::CLIMATE_ACTION_HEATING) + target_action = climate::CLIMATE_ACTION_IDLE; + } + break; + default: + break; + } + } + // do not switch to an action that isn't enabled per the active climate mode + if ((this->mode == climate::CLIMATE_MODE_COOL) && (target_action == climate::CLIMATE_ACTION_HEATING)) + target_action = climate::CLIMATE_ACTION_IDLE; + if ((this->mode == climate::CLIMATE_MODE_HEAT) && (target_action == climate::CLIMATE_ACTION_COOLING)) + target_action = climate::CLIMATE_ACTION_IDLE; + + return target_action; +} +void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((action == this->action) && this->setup_complete_) + // already in target mode + return; + + if (((action == climate::CLIMATE_ACTION_OFF && this->action == climate::CLIMATE_ACTION_IDLE) || + (action == climate::CLIMATE_ACTION_IDLE && this->action == climate::CLIMATE_ACTION_OFF)) && + this->setup_complete_) { + // switching from OFF to IDLE or vice-versa + // these only have visual difference. OFF means user manually disabled, + // IDLE means it's in auto mode but value is in target range. + this->action = action; + return; + } + + if (this->prev_action_trigger_ != nullptr) { + this->prev_action_trigger_->stop(); + this->prev_action_trigger_ = nullptr; + } + Trigger<> *trig = this->idle_action_trigger_; + switch (action) { + case climate::CLIMATE_ACTION_OFF: + case climate::CLIMATE_ACTION_IDLE: + // trig = this->idle_action_trigger_; + break; + case climate::CLIMATE_ACTION_COOLING: + trig = this->cool_action_trigger_; + break; + case climate::CLIMATE_ACTION_HEATING: + trig = this->heat_action_trigger_; + break; + case climate::CLIMATE_ACTION_FAN: + trig = this->fan_only_action_trigger_; + break; + case climate::CLIMATE_ACTION_DRYING: + trig = this->dry_action_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + action = climate::CLIMATE_ACTION_OFF; + // trig = this->idle_action_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->action = action; + this->prev_action_trigger_ = trig; +} +void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((fan_mode == this->prev_fan_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_fan_mode_trigger_ != nullptr) { + this->prev_fan_mode_trigger_->stop(); + this->prev_fan_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->fan_mode_auto_trigger_; + switch (fan_mode) { + case climate::CLIMATE_FAN_ON: + trig = this->fan_mode_on_trigger_; + break; + case climate::CLIMATE_FAN_OFF: + trig = this->fan_mode_off_trigger_; + break; + case climate::CLIMATE_FAN_AUTO: + // trig = this->fan_mode_auto_trigger_; + break; + case climate::CLIMATE_FAN_LOW: + trig = this->fan_mode_low_trigger_; + break; + case climate::CLIMATE_FAN_MEDIUM: + trig = this->fan_mode_medium_trigger_; + break; + case climate::CLIMATE_FAN_HIGH: + trig = this->fan_mode_high_trigger_; + break; + case climate::CLIMATE_FAN_MIDDLE: + trig = this->fan_mode_middle_trigger_; + break; + case climate::CLIMATE_FAN_FOCUS: + trig = this->fan_mode_focus_trigger_; + break; + case climate::CLIMATE_FAN_DIFFUSE: + trig = this->fan_mode_diffuse_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + fan_mode = climate::CLIMATE_FAN_AUTO; + // trig = this->fan_mode_auto_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->fan_mode = fan_mode; + this->prev_fan_mode_ = fan_mode; + this->prev_fan_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((mode == this->prev_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_mode_trigger_ != nullptr) { + this->prev_mode_trigger_->stop(); + this->prev_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->auto_mode_trigger_; + switch (mode) { + case climate::CLIMATE_MODE_OFF: + trig = this->off_mode_trigger_; + break; + case climate::CLIMATE_MODE_AUTO: + // trig = this->auto_mode_trigger_; + break; + case climate::CLIMATE_MODE_COOL: + trig = this->cool_mode_trigger_; + break; + case climate::CLIMATE_MODE_HEAT: + trig = this->heat_mode_trigger_; + break; + case climate::CLIMATE_MODE_FAN_ONLY: + trig = this->fan_only_mode_trigger_; + break; + case climate::CLIMATE_MODE_DRY: + trig = this->dry_mode_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + mode = climate::CLIMATE_MODE_AUTO; + // trig = this->auto_mode_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->mode = mode; + this->prev_mode_ = mode; + this->prev_mode_trigger_ = trig; +} +void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mode) { + // setup_complete_ helps us ensure an action is called immediately after boot + if ((swing_mode == this->prev_swing_mode_) && this->setup_complete_) + // already in target mode + return; + + if (this->prev_swing_mode_trigger_ != nullptr) { + this->prev_swing_mode_trigger_->stop(); + this->prev_swing_mode_trigger_ = nullptr; + } + Trigger<> *trig = this->swing_mode_off_trigger_; + switch (swing_mode) { + case climate::CLIMATE_SWING_BOTH: + trig = this->swing_mode_both_trigger_; + break; + case climate::CLIMATE_SWING_HORIZONTAL: + trig = this->swing_mode_horizontal_trigger_; + break; + case climate::CLIMATE_SWING_OFF: + // trig = this->swing_mode_off_trigger_; + break; + case climate::CLIMATE_SWING_VERTICAL: + trig = this->swing_mode_vertical_trigger_; + break; + default: + // we cannot report an invalid mode back to HA (even if it asked for one) + // and must assume some valid value + swing_mode = climate::CLIMATE_SWING_OFF; + // trig = this->swing_mode_off_trigger_; + } + assert(trig != nullptr); + trig->trigger(); + this->swing_mode = swing_mode; + this->prev_swing_mode_ = swing_mode; + this->prev_swing_mode_trigger_ = trig; +} +void ThermostatClimate::change_away_(bool away) { + if (!away) { + if (this->supports_two_points_) { + this->target_temperature_low = this->normal_config_.default_temperature_low; + this->target_temperature_high = this->normal_config_.default_temperature_high; + } else + this->target_temperature = this->normal_config_.default_temperature; + } else { + if (this->supports_two_points_) { + this->target_temperature_low = this->away_config_.default_temperature_low; + this->target_temperature_high = this->away_config_.default_temperature_high; + } else + this->target_temperature = this->away_config_.default_temperature; + } + this->away = away; +} +void ThermostatClimate::set_normal_config(const ThermostatClimateTargetTempConfig &normal_config) { + this->normal_config_ = normal_config; +} +void ThermostatClimate::set_away_config(const ThermostatClimateTargetTempConfig &away_config) { + this->supports_away_ = true; + this->away_config_ = away_config; +} +ThermostatClimate::ThermostatClimate() + : cool_action_trigger_(new Trigger<>()), + cool_mode_trigger_(new Trigger<>()), + dry_action_trigger_(new Trigger<>()), + dry_mode_trigger_(new Trigger<>()), + heat_action_trigger_(new Trigger<>()), + heat_mode_trigger_(new Trigger<>()), + auto_mode_trigger_(new Trigger<>()), + idle_action_trigger_(new Trigger<>()), + off_mode_trigger_(new Trigger<>()), + fan_only_action_trigger_(new Trigger<>()), + fan_only_mode_trigger_(new Trigger<>()), + fan_mode_on_trigger_(new Trigger<>()), + fan_mode_off_trigger_(new Trigger<>()), + fan_mode_auto_trigger_(new Trigger<>()), + fan_mode_low_trigger_(new Trigger<>()), + fan_mode_medium_trigger_(new Trigger<>()), + fan_mode_high_trigger_(new Trigger<>()), + fan_mode_middle_trigger_(new Trigger<>()), + fan_mode_focus_trigger_(new Trigger<>()), + fan_mode_diffuse_trigger_(new Trigger<>()), + swing_mode_both_trigger_(new Trigger<>()), + swing_mode_off_trigger_(new Trigger<>()), + swing_mode_horizontal_trigger_(new Trigger<>()), + swing_mode_vertical_trigger_(new Trigger<>()) {} +void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; } +void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } +void ThermostatClimate::set_supports_auto(bool supports_auto) { this->supports_auto_ = supports_auto; } +void ThermostatClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } +void ThermostatClimate::set_supports_dry(bool supports_dry) { this->supports_dry_ = supports_dry; } +void ThermostatClimate::set_supports_fan_only(bool supports_fan_only) { this->supports_fan_only_ = supports_fan_only; } +void ThermostatClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } +void ThermostatClimate::set_supports_fan_mode_on(bool supports_fan_mode_on) { + this->supports_fan_mode_on_ = supports_fan_mode_on; +} +void ThermostatClimate::set_supports_fan_mode_off(bool supports_fan_mode_off) { + this->supports_fan_mode_off_ = supports_fan_mode_off; +} +void ThermostatClimate::set_supports_fan_mode_auto(bool supports_fan_mode_auto) { + this->supports_fan_mode_auto_ = supports_fan_mode_auto; +} +void ThermostatClimate::set_supports_fan_mode_low(bool supports_fan_mode_low) { + this->supports_fan_mode_low_ = supports_fan_mode_low; +} +void ThermostatClimate::set_supports_fan_mode_medium(bool supports_fan_mode_medium) { + this->supports_fan_mode_medium_ = supports_fan_mode_medium; +} +void ThermostatClimate::set_supports_fan_mode_high(bool supports_fan_mode_high) { + this->supports_fan_mode_high_ = supports_fan_mode_high; +} +void ThermostatClimate::set_supports_fan_mode_middle(bool supports_fan_mode_middle) { + this->supports_fan_mode_middle_ = supports_fan_mode_middle; +} +void ThermostatClimate::set_supports_fan_mode_focus(bool supports_fan_mode_focus) { + this->supports_fan_mode_focus_ = supports_fan_mode_focus; +} +void ThermostatClimate::set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse) { + this->supports_fan_mode_diffuse_ = supports_fan_mode_diffuse; +} +void ThermostatClimate::set_supports_swing_mode_both(bool supports_swing_mode_both) { + this->supports_swing_mode_both_ = supports_swing_mode_both; +} +void ThermostatClimate::set_supports_swing_mode_off(bool supports_swing_mode_off) { + this->supports_swing_mode_off_ = supports_swing_mode_off; +} +void ThermostatClimate::set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal) { + this->supports_swing_mode_horizontal_ = supports_swing_mode_horizontal; +} +void ThermostatClimate::set_supports_swing_mode_vertical(bool supports_swing_mode_vertical) { + this->supports_swing_mode_vertical_ = supports_swing_mode_vertical; +} +void ThermostatClimate::set_supports_two_points(bool supports_two_points) { + this->supports_two_points_ = supports_two_points; +} +Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } +Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } +Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } +Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } +void ThermostatClimate::dump_config() { + LOG_CLIMATE("", "Thermostat", this); + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); + ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); + ESP_LOGCONFIG(TAG, " Supports DRY: %s", YESNO(this->supports_dry_)); + ESP_LOGCONFIG(TAG, " Supports FAN_ONLY: %s", YESNO(this->supports_fan_only_)); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE ON: %s", YESNO(this->supports_fan_mode_on_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE OFF: %s", YESNO(this->supports_fan_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE AUTO: %s", YESNO(this->supports_fan_mode_auto_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE LOW: %s", YESNO(this->supports_fan_mode_low_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MEDIUM: %s", YESNO(this->supports_fan_mode_medium_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE HIGH: %s", YESNO(this->supports_fan_mode_high_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE MIDDLE: %s", YESNO(this->supports_fan_mode_middle_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE FOCUS: %s", YESNO(this->supports_fan_mode_focus_)); + ESP_LOGCONFIG(TAG, " Supports FAN MODE DIFFUSE: %s", YESNO(this->supports_fan_mode_diffuse_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE BOTH: %s", YESNO(this->supports_swing_mode_both_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE OFF: %s", YESNO(this->supports_swing_mode_off_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE HORIZONTAL: %s", YESNO(this->supports_swing_mode_horizontal_)); + ESP_LOGCONFIG(TAG, " Supports SWING MODE VERTICAL: %s", YESNO(this->supports_swing_mode_vertical_)); + ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); + ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); + if (this->supports_away_) { + if (this->supports_heat_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature_low); + if ((this->supports_cool_) || (this->supports_fan_only_)) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + } +} + +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig() = default; +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature) + : default_temperature(default_temperature) {} +ThermostatClimateTargetTempConfig::ThermostatClimateTargetTempConfig(float default_temperature_low, + float default_temperature_high) + : default_temperature_low(default_temperature_low), default_temperature_high(default_temperature_high) {} + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h new file mode 100644 index 0000000000..86a1007efa --- /dev/null +++ b/esphome/components/thermostat/thermostat_climate.h @@ -0,0 +1,271 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace thermostat { + +struct ThermostatClimateTargetTempConfig { + public: + ThermostatClimateTargetTempConfig(); + ThermostatClimateTargetTempConfig(float default_temperature); + ThermostatClimateTargetTempConfig(float default_temperature_low, float default_temperature_high); + + float default_temperature{NAN}; + float default_temperature_low{NAN}; + float default_temperature_high{NAN}; + float hysteresis{NAN}; +}; + +class ThermostatClimate : public climate::Climate, public Component { + public: + ThermostatClimate(); + void setup() override; + void dump_config() override; + + void set_hysteresis(float hysteresis); + void set_sensor(sensor::Sensor *sensor); + void set_supports_auto(bool supports_auto); + void set_supports_cool(bool supports_cool); + void set_supports_dry(bool supports_dry); + void set_supports_fan_only(bool supports_fan_only); + void set_supports_heat(bool supports_heat); + void set_supports_fan_mode_on(bool supports_fan_mode_on); + void set_supports_fan_mode_off(bool supports_fan_mode_off); + void set_supports_fan_mode_auto(bool supports_fan_mode_auto); + void set_supports_fan_mode_low(bool supports_fan_mode_low); + void set_supports_fan_mode_medium(bool supports_fan_mode_medium); + void set_supports_fan_mode_high(bool supports_fan_mode_high); + void set_supports_fan_mode_middle(bool supports_fan_mode_middle); + void set_supports_fan_mode_focus(bool supports_fan_mode_focus); + void set_supports_fan_mode_diffuse(bool supports_fan_mode_diffuse); + void set_supports_swing_mode_both(bool supports_swing_mode_both); + void set_supports_swing_mode_horizontal(bool supports_swing_mode_horizontal); + void set_supports_swing_mode_off(bool supports_swing_mode_off); + void set_supports_swing_mode_vertical(bool supports_swing_mode_vertical); + void set_supports_two_points(bool supports_two_points); + + void set_normal_config(const ThermostatClimateTargetTempConfig &normal_config); + void set_away_config(const ThermostatClimateTargetTempConfig &away_config); + + Trigger<> *get_cool_action_trigger() const; + Trigger<> *get_dry_action_trigger() const; + Trigger<> *get_fan_only_action_trigger() const; + Trigger<> *get_heat_action_trigger() const; + Trigger<> *get_idle_action_trigger() const; + Trigger<> *get_auto_mode_trigger() const; + Trigger<> *get_cool_mode_trigger() const; + Trigger<> *get_dry_mode_trigger() const; + Trigger<> *get_fan_only_mode_trigger() const; + Trigger<> *get_heat_mode_trigger() const; + Trigger<> *get_off_mode_trigger() const; + Trigger<> *get_fan_mode_on_trigger() const; + Trigger<> *get_fan_mode_off_trigger() const; + Trigger<> *get_fan_mode_auto_trigger() const; + Trigger<> *get_fan_mode_low_trigger() const; + Trigger<> *get_fan_mode_medium_trigger() const; + Trigger<> *get_fan_mode_high_trigger() const; + Trigger<> *get_fan_mode_middle_trigger() const; + Trigger<> *get_fan_mode_focus_trigger() const; + Trigger<> *get_fan_mode_diffuse_trigger() const; + Trigger<> *get_swing_mode_both_trigger() const; + Trigger<> *get_swing_mode_horizontal_trigger() const; + Trigger<> *get_swing_mode_off_trigger() const; + Trigger<> *get_swing_mode_vertical_trigger() const; + /// Get current hysteresis value + float hysteresis(); + /// Call triggers based on updated climate states (modes/actions) + void refresh(); + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + + /// Change the away setting, will reset target temperatures to defaults. + void change_away_(bool away); + + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the required action of this climate controller. + climate::ClimateAction compute_action_(); + + /// Switch the climate device to the given climate action. + void switch_to_action_(climate::ClimateAction action); + + /// Switch the climate device to the given climate fan mode. + void switch_to_fan_mode_(climate::ClimateFanMode fan_mode); + + /// Switch the climate device to the given climate mode. + void switch_to_mode_(climate::ClimateMode mode); + + /// Switch the climate device to the given climate swing mode. + void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode); + + /// The sensor used for getting the current temperature + sensor::Sensor *sensor_{nullptr}; + + /// Whether the controller supports auto/cooling/drying/fanning/heating. + /// + /// A false value for any given attribute means that the controller has no such action + /// (for example a thermostat, where only heating and not-heating is possible). + bool supports_auto_{false}; + bool supports_cool_{false}; + bool supports_dry_{false}; + bool supports_fan_only_{false}; + bool supports_heat_{false}; + + /// Whether the controller supports turning on or off just the fan. + /// + /// A false value for either attribute means that the controller has no fan on/off action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_on_{false}; + bool supports_fan_mode_off_{false}; + + /// Whether the controller supports fan auto mode. + /// + /// A false value for this attribute means that the controller has no fan-auto action + /// (for example a thermostat, where independent control of the fan is not possible). + bool supports_fan_mode_auto_{false}; + + /// Whether the controller supports various fan speeds and/or positions. + /// + /// A false value for any given attribute means that the controller has no such fan action. + bool supports_fan_mode_low_{false}; + bool supports_fan_mode_medium_{false}; + bool supports_fan_mode_high_{false}; + bool supports_fan_mode_middle_{false}; + bool supports_fan_mode_focus_{false}; + bool supports_fan_mode_diffuse_{false}; + + /// Whether the controller supports various swing modes. + /// + /// A false value for any given attribute means that the controller has no such swing mode. + bool supports_swing_mode_both_{false}; + bool supports_swing_mode_off_{false}; + bool supports_swing_mode_horizontal_{false}; + bool supports_swing_mode_vertical_{false}; + + /// Whether the controller supports two set points + /// + /// A false value means that the controller has no such support. + bool supports_two_points_{false}; + + /// Whether the controller supports an "away" mode + /// + /// A false value means that the controller has no such mode. + bool supports_away_{false}; + + /// The trigger to call when the controller should switch to cooling action/mode. + /// + /// A null value for this attribute means that the controller has no cooling action + /// For example electric heat, where only heating (power on) and not-heating + /// (power off) is possible. + Trigger<> *cool_action_trigger_{nullptr}; + Trigger<> *cool_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to dry (dehumidification) mode. + /// + /// In dry mode, the controller is assumed to have both heating and cooling disabled, + /// although the system may use its cooling mechanism to achieve drying. + Trigger<> *dry_action_trigger_{nullptr}; + Trigger<> *dry_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to heating action/mode. + /// + /// A null value for this attribute means that the controller has no heating action + /// For example window blinds, where only cooling (blinds closed) and not-cooling + /// (blinds open) is possible. + Trigger<> *heat_action_trigger_{nullptr}; + Trigger<> *heat_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to auto mode. + /// + /// In auto mode, the controller will enable heating/cooling as necessary and switch + /// to idle when the temperature is within the thresholds/set points. + Trigger<> *auto_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to idle action/off mode. + /// + /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. + Trigger<> *idle_action_trigger_{nullptr}; + Trigger<> *off_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch to fan-only action/mode. + /// + /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. + /// The system should activate the fan only. + Trigger<> *fan_only_action_trigger_{nullptr}; + Trigger<> *fan_only_mode_trigger_{nullptr}; + + /// The trigger to call when the controller should switch on the fan. + Trigger<> *fan_mode_on_trigger_{nullptr}; + + /// The trigger to call when the controller should switch off the fan. + Trigger<> *fan_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "auto" mode. + Trigger<> *fan_mode_auto_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "low" speed. + Trigger<> *fan_mode_low_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "medium" speed. + Trigger<> *fan_mode_medium_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "high" speed. + Trigger<> *fan_mode_high_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "middle" position. + Trigger<> *fan_mode_middle_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "focus" position. + Trigger<> *fan_mode_focus_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the fan to "diffuse" position. + Trigger<> *fan_mode_diffuse_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "both". + Trigger<> *swing_mode_both_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "off". + Trigger<> *swing_mode_off_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "horizontal". + Trigger<> *swing_mode_horizontal_trigger_{nullptr}; + + /// The trigger to call when the controller should switch the swing mode to "vertical". + Trigger<> *swing_mode_vertical_trigger_{nullptr}; + + /// A reference to the trigger that was previously active. + /// + /// This is so that the previous trigger can be stopped before enabling a new one + /// for each climate category (mode, action, fan_mode, swing_mode). + Trigger<> *prev_action_trigger_{nullptr}; + Trigger<> *prev_fan_mode_trigger_{nullptr}; + Trigger<> *prev_mode_trigger_{nullptr}; + Trigger<> *prev_swing_mode_trigger_{nullptr}; + + /// Store previously-known states + /// + /// These are used to determine when a trigger/action needs to be called + climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON}; + climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF}; + climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF}; + + /// Temperature data for normal/home and away modes + ThermostatClimateTargetTempConfig normal_config_{}; + ThermostatClimateTargetTempConfig away_config_{}; + + /// Hysteresis value used for computing climate actions + float hysteresis_{0}; + + /// setup_complete_ blocks modifying/resetting the temps immediately after boot + bool setup_complete_{false}; +}; + +} // namespace thermostat +} // namespace esphome diff --git a/esphome/const.py b/esphome/const.py index 4cdf48fc76..8b727b615c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -44,6 +44,7 @@ CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' CONF_AUTH = 'auth' +CONF_AUTO_MODE = 'auto_mode' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' @@ -102,6 +103,7 @@ CONF_CONDITION = 'condition' CONF_CONDITION_ID = 'condition_id' CONF_CONDUCTIVITY = 'conductivity' CONF_COOL_ACTION = 'cool_action' +CONF_COOL_MODE = 'cool_mode' CONF_COUNT_MODE = 'count_mode' CONF_CRON = 'cron' CONF_CS_PIN = 'cs_pin' @@ -140,6 +142,8 @@ CONF_DIV_RATIO = 'div_ratio' CONF_DNS1 = 'dns1' CONF_DNS2 = 'dns2' CONF_DOMAIN = 'domain' +CONF_DRY_ACTION = 'dry_action' +CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' CONF_ECHO_PIN = 'echo_pin' @@ -159,6 +163,17 @@ CONF_EXTERNAL_VCC = 'external_vcc' CONF_FALLING_EDGE = 'falling_edge' CONF_FAMILY = 'family' CONF_FAN_MODE = 'fan_mode' +CONF_FAN_MODE_AUTO_ACTION = 'fan_mode_auto_action' +CONF_FAN_MODE_DIFFUSE_ACTION = 'fan_mode_diffuse_action' +CONF_FAN_MODE_FOCUS_ACTION = 'fan_mode_focus_action' +CONF_FAN_MODE_HIGH_ACTION = 'fan_mode_high_action' +CONF_FAN_MODE_LOW_ACTION = 'fan_mode_low_action' +CONF_FAN_MODE_MEDIUM_ACTION = 'fan_mode_medium_action' +CONF_FAN_MODE_MIDDLE_ACTION = 'fan_mode_middle_action' +CONF_FAN_MODE_OFF_ACTION = 'fan_mode_off_action' +CONF_FAN_MODE_ON_ACTION = 'fan_mode_on_action' +CONF_FAN_ONLY_ACTION = 'fan_only_action' +CONF_FAN_ONLY_MODE = 'fan_only_mode' CONF_FAST_CONNECT = 'fast_connect' CONF_FILE = 'file' CONF_FILTER = 'filter' @@ -183,6 +198,7 @@ CONF_GROUP = 'group' CONF_HARDWARE_UART = 'hardware_uart' CONF_HEARTBEAT = 'heartbeat' CONF_HEAT_ACTION = 'heat_action' +CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' CONF_HIGH = 'high' @@ -190,6 +206,7 @@ CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' CONF_HOURS = 'hours' CONF_HUMIDITY = 'humidity' +CONF_HYSTERESIS = "hysteresis" CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' @@ -285,6 +302,7 @@ CONF_NUM_CHANNELS = 'num_channels' CONF_NUM_CHIPS = 'num_chips' CONF_NUM_LEDS = 'num_leds' CONF_NUMBER = 'number' +CONF_OFF_MODE = 'off_mode' CONF_OFFSET = 'offset' CONF_ON = 'on' CONF_ON_BLE_ADVERTISE = 'on_ble_advertise' @@ -449,7 +467,11 @@ CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' +CONF_SWING_BOTH_ACTION = 'swing_both_action' +CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' +CONF_SWING_OFF_ACTION = 'swing_off_action' +CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test3.yaml b/tests/test3.yaml index 72dde13055..ef464fd5b7 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -624,6 +624,63 @@ climate: away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C + - platform: thermostat + name: Thermostat Climate + sensor: ha_hello_world + default_target_temperature_low: 18°C + default_target_temperature_high: 24°C + idle_action: + - switch.turn_on: gpio_switch1 + cool_action: + - switch.turn_on: gpio_switch2 + heat_action: + - switch.turn_on: gpio_switch1 + dry_action: + - switch.turn_on: gpio_switch2 + fan_only_action: + - switch.turn_on: gpio_switch1 + auto_mode: + - switch.turn_on: gpio_switch2 + off_mode: + - switch.turn_on: gpio_switch1 + heat_mode: + - switch.turn_on: gpio_switch2 + cool_mode: + - switch.turn_on: gpio_switch1 + dry_mode: + - switch.turn_on: gpio_switch2 + fan_only_mode: + - switch.turn_on: gpio_switch1 + fan_mode_auto_action: + - switch.turn_on: gpio_switch2 + fan_mode_on_action: + - switch.turn_on: gpio_switch1 + fan_mode_off_action: + - switch.turn_on: gpio_switch2 + fan_mode_low_action: + - switch.turn_on: gpio_switch1 + fan_mode_medium_action: + - switch.turn_on: gpio_switch2 + fan_mode_high_action: + - switch.turn_on: gpio_switch1 + fan_mode_middle_action: + - switch.turn_on: gpio_switch2 + fan_mode_focus_action: + - switch.turn_on: gpio_switch1 + fan_mode_diffuse_action: + - switch.turn_on: gpio_switch2 + swing_off_action: + - switch.turn_on: gpio_switch1 + swing_horizontal_action: + - switch.turn_on: gpio_switch2 + swing_vertical_action: + - switch.turn_on: gpio_switch1 + swing_both_action: + - switch.turn_on: gpio_switch2 + hysteresis: 0.2 + away_config: + default_target_temperature_low: 16°C + default_target_temperature_high: 20°C cover: - platform: endstop From 382793de9a56d6fb2cc4bc8e18af61e7ea865c06 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 8 Jul 2020 12:29:16 -0300 Subject: [PATCH 049/200] unpin mbedtls version (#1114) --- esphome/components/xiaomi_cgd1/sensor.py | 2 +- esphome/components/xiaomi_lywsd03mmc/sensor.py | 2 +- esphome/components/xiaomi_mjyd02yla/binary_sensor.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py index 343279f8fe..b7891c8a56 100644 --- a/esphome/components/xiaomi_cgd1/sensor.py +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -40,4 +40,4 @@ def to_code(config): sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) - cg.add_library("mbedtls", "cdf462088d") + cg.add_library("mbedtls", None) diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py index 71f7b20752..1ab59b01c5 100644 --- a/esphome/components/xiaomi_lywsd03mmc/sensor.py +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -41,4 +41,4 @@ def to_code(config): sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) - cg.add_library("mbedtls", "cdf462088d") + cg.add_library("mbedtls", None) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index e34864c480..6d2a674969 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -44,4 +44,4 @@ def to_code(config): sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT]) cg.add(var.set_light(sens)) - cg.add_library("mbedtls", "cdf462088d") + cg.add_library("mbedtls", None) From 6873f09f5722ff800f4e52dac3689e17dffe7927 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 8 Jul 2020 15:25:32 -0300 Subject: [PATCH 050/200] Fix ethernet logging too many warn messages (#1112) --- esphome/components/ethernet/ethernet_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index d5548fc377..0553d66273 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -37,7 +37,7 @@ void EthernetComponent::setup() { } void EthernetComponent::loop() { const uint32_t now = millis(); - if (!this->connected_ && !this->last_connected_ && now - this->last_connected_ > 15000) { + if (!this->connected_ && !this->last_connected_ && now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting via ethernet failed! Re-connecting..."); this->start_connect_(); return; From 7a16f846eb65ece249562f18e2a71bb2ffab3253 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 8 Jul 2020 15:52:51 -0300 Subject: [PATCH 051/200] add click dependency (#1111) * add click dependency * Update requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index f999c4d98b..0c99eaccbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 +click==7.1.2 From 33212d1abf45084c70d0da058684283129c698aa Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento Date: Thu, 9 Jul 2020 18:25:46 -0500 Subject: [PATCH 052/200] Add Integral Reset Action to PIDClimate (#1104) --- esphome/components/pid/climate.py | 13 +++++++++++++ esphome/components/pid/pid_climate.cpp | 2 ++ esphome/components/pid/pid_climate.h | 11 +++++++++++ esphome/components/pid/pid_controller.h | 2 ++ 4 files changed, 28 insertions(+) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index a3e2299296..97a98efc20 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -7,6 +7,7 @@ from esphome.const import CONF_ID, CONF_SENSOR pid_ns = cg.esphome_ns.namespace('pid') PIDClimate = pid_ns.class_('PIDClimate', climate.Climate, cg.Component) PIDAutotuneAction = pid_ns.class_('PIDAutotuneAction', automation.Action) +PIDResetIntegralTermAction = pid_ns.class_('PIDResetIntegralTermAction', automation.Action) CONF_DEFAULT_TARGET_TEMPERATURE = 'default_target_temperature' @@ -64,6 +65,18 @@ def to_code(config): cg.add(var.set_default_target_temperature(config[CONF_DEFAULT_TARGET_TEMPERATURE])) +@automation.register_action( + 'climate.pid.reset_integral_term', + PIDResetIntegralTermAction, + automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + }) +) +def pid_reset_integral_term(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + @automation.register_action('climate.pid.autotune', PIDAutotuneAction, automation.maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(PIDClimate), cv.Optional(CONF_NOISEBAND, default=0.25): cv.float_, diff --git a/esphome/components/pid/pid_climate.cpp b/esphome/components/pid/pid_climate.cpp index 0c777ffd8b..24fb0ec905 100644 --- a/esphome/components/pid/pid_climate.cpp +++ b/esphome/components/pid/pid_climate.cpp @@ -148,5 +148,7 @@ void PIDClimate::start_autotune(std::unique_ptr &&autotune) { }); } +void PIDClimate::reset_integral_term() { this->controller_.reset_accumulated_integral(); } + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 3dae92af5f..12436e225c 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -39,6 +39,7 @@ class PIDClimate : public climate::Climate, public Component { default_target_temperature_ = default_target_temperature; } void start_autotune(std::unique_ptr &&autotune); + void reset_integral_term(); protected: /// Override control to change settings of the climate device. @@ -90,5 +91,15 @@ template class PIDAutotuneAction : public Action { PIDClimate *parent_; }; +template class PIDResetIntegralTermAction : public Action { + public: + PIDResetIntegralTermAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { this->parent_->reset_integral_term(); } + + protected: + PIDClimate *parent_; +}; + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/pid_controller.h b/esphome/components/pid/pid_controller.h index 7ec7724e15..4caad8dd8b 100644 --- a/esphome/components/pid/pid_controller.h +++ b/esphome/components/pid/pid_controller.h @@ -40,6 +40,8 @@ struct PIDController { return proportional_term + integral_term + derivative_term; } + void reset_accumulated_integral() { accumulated_integral_ = 0; } + /// Proportional gain K_p. float kp = 0; /// Integral gain K_i. From e5d4e12457555d20fd8bcd32b1c009da493e3252 Mon Sep 17 00:00:00 2001 From: peq123 Date: Fri, 10 Jul 2020 00:29:44 +0100 Subject: [PATCH 053/200] RGBWW - added channel interlock for RGB vs white (#1042) * Update light_color_values.h * Update light_color_values.h * Update light_color_values.h * Update light_color_values.h * colour relative to white value * Update light_state.cpp * Update light_state.cpp * Update light_state.cpp * Update light_state.cpp * Update light_color_values.h * Update light_state.cpp * linting * Update light_state.cpp * Update light_color_values.h * more lint * more lint * Update light_state.cpp * Update light_state.cpp * Update light_state.cpp * add optional interlock * Update test1.yaml * Update light_state.cpp * Update light_state.h * interlock * optional interlock. * interlock as trait * color interlock as trait * optional color interlock.... * optional color interlock * Update light_color_values.h * Lint checks.... * making travis happy * making sure gamma_correct fix is included * Update light_color_values.h * making travis happy * clang-format * Code tidy * Update light_color_values.h * Update light_color_values.h --- esphome/components/light/light_color_values.h | 22 +++++--- esphome/components/light/light_state.cpp | 56 ++++++++++++++----- esphome/components/light/light_state.h | 6 +- esphome/components/light/light_traits.h | 5 ++ esphome/components/rgb/rgb_light_output.h | 3 +- esphome/components/rgbw/light.py | 3 + esphome/components/rgbw/rgbw_light_output.h | 5 +- esphome/components/rgbww/light.py | 3 + esphome/components/rgbww/rgbww_light_output.h | 6 +- tests/test1.yaml | 2 + 10 files changed, 83 insertions(+), 28 deletions(-) diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index 90f5570b90..cdd05ae7b7 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -179,22 +179,28 @@ class LightColorValues { } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue, float gamma = 0) const { - *red = gamma_correct(this->state_ * this->brightness_ * this->red_, gamma); - *green = gamma_correct(this->state_ * this->brightness_ * this->green_, gamma); - *blue = gamma_correct(this->state_ * this->brightness_ * this->blue_, gamma); + void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + float brightness = this->state_ * this->brightness_; + if (color_interlock) { + brightness = brightness * (1.0f - this->white_); + } + *red = gamma_correct(brightness * this->red_, gamma); + *green = gamma_correct(brightness * this->green_, gamma); + *blue = gamma_correct(brightness * this->blue_, gamma); } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0) const { - this->as_rgb(red, green, blue, gamma); + void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, + bool color_interlock = false) const { + this->as_rgb(red, green, blue, gamma, color_interlock); *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); } /// Convert these light color values to an RGBWW representation with the given parameters. void as_rgbww(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { - this->as_rgb(red, green, blue, gamma); + float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false, + bool color_interlock = false) const { + this->as_rgb(red, green, blue, gamma, color_interlock); const float color_temp = clamp(this->color_temperature_, color_temperature_cw, color_temperature_ww); const float ww_fraction = (color_temp - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); const float cw_fraction = 1.0f - ww_fraction; diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index 9aa32f6904..d34bc88f53 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -400,26 +400,51 @@ LightColorValues LightCall::validate_() { this->green_ = optional(1.0f); this->blue_ = optional(1.0f); } + // make white values binary aka 0.0f or 1.0f...this allows brightness to do its job + if (traits.get_supports_color_interlock()) { + if (*this->white_ > 0.0f) { + this->white_ = optional(1.0f); + } else { + this->white_ = optional(0.0f); + } + } } - // White to 0% if (exclusively) setting any RGB value + // White to 0% if (exclusively) setting any RGB value that isn't 255,255,255 else if (this->red_.has_value() || this->green_.has_value() || this->blue_.has_value()) { - if (!this->white_.has_value()) { + if (*this->red_ == 1.0f && *this->green_ == 1.0f && *this->blue_ == 1.0f && traits.get_supports_rgb_white_value() && + traits.get_supports_color_interlock()) { + this->white_ = optional(1.0f); + } else if (!this->white_.has_value() || !traits.get_supports_rgb_white_value()) { this->white_ = optional(0.0f); } } // if changing Kelvin alone, change to white light else if (this->color_temperature_.has_value()) { - if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { - this->red_ = optional(1.0f); - this->green_ = optional(1.0f); - this->blue_ = optional(1.0f); + if (!traits.get_supports_color_interlock()) { + if (!this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } } // if setting Kelvin from color (i.e. switching to white light), set White to 100% auto cv = this->parent_->remote_values; bool was_color = cv.get_red() != 1.0f || cv.get_blue() != 1.0f || cv.get_green() != 1.0f; bool now_white = *this->red_ == 1.0f && *this->blue_ == 1.0f && *this->green_ == 1.0f; - if (!this->white_.has_value() && was_color && now_white) { - this->white_ = optional(1.0f); + if (traits.get_supports_color_interlock()) { + if (cv.get_white() < 1.0f) { + this->white_ = optional(1.0f); + } + + if (was_color && !this->red_.has_value() && !this->green_.has_value() && !this->blue_.has_value()) { + this->red_ = optional(1.0f); + this->green_ = optional(1.0f); + this->blue_ = optional(1.0f); + } + } else { + if (!this->white_.has_value() && was_color && now_white) { + this->white_ = optional(1.0f); + } } } @@ -704,17 +729,20 @@ void LightState::current_values_as_binary(bool *binary) { this->current_values.a void LightState::current_values_as_brightness(float *brightness) { this->current_values.as_brightness(brightness, this->gamma_correct_); } -void LightState::current_values_as_rgb(float *red, float *green, float *blue) { - this->current_values.as_rgb(red, green, blue, this->gamma_correct_); +void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgb(red, green, blue, this->gamma_correct_, traits.get_supports_color_interlock()); } -void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white) { - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_); +void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { + auto traits = this->get_traits(); + this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, traits.get_supports_color_interlock()); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness) { + bool constant_brightness, bool color_interlock) { auto traits = this->get_traits(); this->current_values.as_rgbww(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, cold_white, - warm_white, this->gamma_correct_, constant_brightness); + warm_white, this->gamma_correct_, constant_brightness, + traits.get_supports_color_interlock()); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { auto traits = this->get_traits(); diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index f399cc2be4..e48cf9f864 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -266,12 +266,12 @@ class LightState : public Nameable, public Component { void current_values_as_brightness(float *brightness); - void current_values_as_rgb(float *red, float *green, float *blue); + void current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock = false); - void current_values_as_rgbw(float *red, float *green, float *blue, float *white); + void current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock = false); void current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, - bool constant_brightness = false); + bool constant_brightness = false, bool color_interlock = false); void current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false); diff --git a/esphome/components/light/light_traits.h b/esphome/components/light/light_traits.h index 2052b55c8e..ed9c0d44ea 100644 --- a/esphome/components/light/light_traits.h +++ b/esphome/components/light/light_traits.h @@ -20,6 +20,10 @@ class LightTraits { void set_supports_color_temperature(bool supports_color_temperature) { this->supports_color_temperature_ = supports_color_temperature; } + bool get_supports_color_interlock() const { return this->supports_color_interlock_; } + void set_supports_color_interlock(bool supports_color_interlock) { + this->supports_color_interlock_ = supports_color_interlock; + } float get_min_mireds() const { return this->min_mireds_; } void set_min_mireds(float min_mireds) { this->min_mireds_ = min_mireds; } float get_max_mireds() const { return this->max_mireds_; } @@ -32,6 +36,7 @@ class LightTraits { bool supports_color_temperature_{false}; float min_mireds_{0}; float max_mireds_{0}; + bool supports_color_interlock_{false}; }; } // namespace light diff --git a/esphome/components/rgb/rgb_light_output.h b/esphome/components/rgb/rgb_light_output.h index e612c80f73..1a3bf9f614 100644 --- a/esphome/components/rgb/rgb_light_output.h +++ b/esphome/components/rgb/rgb_light_output.h @@ -12,6 +12,7 @@ class RGBLightOutput : public light::LightOutput { void set_red(output::FloatOutput *red) { red_ = red; } void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } + light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); @@ -20,7 +21,7 @@ class RGBLightOutput : public light::LightOutput { } void write_state(light::LightState *state) override { float red, green, blue; - state->current_values_as_rgb(&red, &green, &blue); + state->current_values_as_rgb(&red, &green, &blue, false); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); diff --git a/esphome/components/rgbw/light.py b/esphome/components/rgbw/light.py index 75d6082e5a..ca31a8229d 100644 --- a/esphome/components/rgbw/light.py +++ b/esphome/components/rgbw/light.py @@ -5,6 +5,7 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_RED, CONF_OUTPUT_ID, CONF_ rgbw_ns = cg.esphome_ns.namespace('rgbw') RGBWLightOutput = rgbw_ns.class_('RGBWLightOutput', light.LightOutput) +CONF_COLOR_INTERLOCK = 'color_interlock' CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWLightOutput), @@ -12,6 +13,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_GREEN): cv.use_id(output.FloatOutput), cv.Required(CONF_BLUE): cv.use_id(output.FloatOutput), cv.Required(CONF_WHITE): cv.use_id(output.FloatOutput), + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, }) @@ -27,3 +29,4 @@ def to_code(config): cg.add(var.set_blue(blue)) white = yield cg.get_variable(config[CONF_WHITE]) cg.add(var.set_white(white)) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbw/rgbw_light_output.h b/esphome/components/rgbw/rgbw_light_output.h index b58c7f9d54..90a650851b 100644 --- a/esphome/components/rgbw/rgbw_light_output.h +++ b/esphome/components/rgbw/rgbw_light_output.h @@ -13,16 +13,18 @@ class RGBWLightOutput : public light::LightOutput { void set_green(output::FloatOutput *green) { green_ = green; } void set_blue(output::FloatOutput *blue) { blue_ = blue; } void set_white(output::FloatOutput *white) { white_ = white; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); + traits.set_supports_color_interlock(this->color_interlock_); traits.set_supports_rgb(true); traits.set_supports_rgb_white_value(true); return traits; } void write_state(light::LightState *state) override { float red, green, blue, white; - state->current_values_as_rgbw(&red, &green, &blue, &white); + state->current_values_as_rgbw(&red, &green, &blue, &white, this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -34,6 +36,7 @@ class RGBWLightOutput : public light::LightOutput { output::FloatOutput *green_; output::FloatOutput *blue_; output::FloatOutput *white_; + bool color_interlock_{false}; }; } // namespace rgbw diff --git a/esphome/components/rgbww/light.py b/esphome/components/rgbww/light.py index 78f4bee630..1513a684ea 100644 --- a/esphome/components/rgbww/light.py +++ b/esphome/components/rgbww/light.py @@ -9,6 +9,7 @@ rgbww_ns = cg.esphome_ns.namespace('rgbww') RGBWWLightOutput = rgbww_ns.class_('RGBWWLightOutput', light.LightOutput) CONF_CONSTANT_BRIGHTNESS = 'constant_brightness' +CONF_COLOR_INTERLOCK = 'color_interlock' CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(RGBWWLightOutput), @@ -20,6 +21,7 @@ CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend({ cv.Required(CONF_COLD_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Required(CONF_WARM_WHITE_COLOR_TEMPERATURE): cv.color_temperature, cv.Optional(CONF_CONSTANT_BRIGHTNESS, default=False): cv.boolean, + cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, }) @@ -42,3 +44,4 @@ def to_code(config): cg.add(var.set_warm_white(wwhite)) cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) cg.add(var.set_constant_brightness(config[CONF_CONSTANT_BRIGHTNESS])) + cg.add(var.set_color_interlock(config[CONF_COLOR_INTERLOCK])) diff --git a/esphome/components/rgbww/rgbww_light_output.h b/esphome/components/rgbww/rgbww_light_output.h index a975331a37..152766970e 100644 --- a/esphome/components/rgbww/rgbww_light_output.h +++ b/esphome/components/rgbww/rgbww_light_output.h @@ -17,19 +17,22 @@ class RGBWWLightOutput : public light::LightOutput { void set_cold_white_temperature(float cold_white_temperature) { cold_white_temperature_ = cold_white_temperature; } void set_warm_white_temperature(float warm_white_temperature) { warm_white_temperature_ = warm_white_temperature; } void set_constant_brightness(bool constant_brightness) { constant_brightness_ = constant_brightness; } + void set_color_interlock(bool color_interlock) { color_interlock_ = color_interlock; } light::LightTraits get_traits() override { auto traits = light::LightTraits(); traits.set_supports_brightness(true); traits.set_supports_rgb(true); traits.set_supports_rgb_white_value(true); traits.set_supports_color_temperature(true); + traits.set_supports_color_interlock(this->color_interlock_); traits.set_min_mireds(this->cold_white_temperature_); traits.set_max_mireds(this->warm_white_temperature_); return traits; } void write_state(light::LightState *state) override { float red, green, blue, cwhite, wwhite; - state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_); + state->current_values_as_rgbww(&red, &green, &blue, &cwhite, &wwhite, this->constant_brightness_, + this->color_interlock_); this->red_->set_level(red); this->green_->set_level(green); this->blue_->set_level(blue); @@ -46,6 +49,7 @@ class RGBWWLightOutput : public light::LightOutput { float cold_white_temperature_; float warm_white_temperature_; bool constant_brightness_; + bool color_interlock_{false}; }; } // namespace rgbww diff --git a/tests/test1.yaml b/tests/test1.yaml index ec09a00208..83da6da9e3 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1114,6 +1114,7 @@ light: green: pca_4 blue: pca_5 white: pca_6 + color_interlock: true - platform: rgbww name: "Living Room Lights 2" red: pca_3 @@ -1123,6 +1124,7 @@ light: warm_white: pca_6 cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds + color_interlock: true - platform: cwww name: "Living Room Lights 2" cold_white: pca_6 From c693c219f46c345dd64ea8c670d876615ba2077d Mon Sep 17 00:00:00 2001 From: Tom Price Date: Fri, 10 Jul 2020 01:35:35 +0100 Subject: [PATCH 054/200] Add ESP32 support for WPA2-EAP Enterprise WiFi authentication (#1080) * Add config generation and validation to support various WPA2-EAP authentication methods. Config generate validates that all the required parameters are set in valid combinations. In the event of EAP-TLS, the private key is matched to the certificate to check for pasting errors. Add EAPAuth struct to header. Add eap_auth property to WiFiAP with getters/setters. * Add C++ code for setting WPA2-EAP authentication. * Fix config for x509 EAP-TLS authentication. * Fix a few linting issues. * Fix a couple more linting issues that only showed up in CI * Remove stray character. Co-authored-by: Guillermo Ruffino --- esphome/components/wifi/__init__.py | 88 ++++++++++++++++++- esphome/components/wifi/wifi_component.cpp | 2 + esphome/components/wifi/wifi_component.h | 13 +++ .../components/wifi/wifi_component_esp32.cpp | 46 ++++++++++ esphome/config_validation.py | 23 +++++ esphome/const.py | 5 ++ requirements.txt | 1 + requirements_test.txt | 1 + 8 files changed, 178 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d3c7e51603..8b68264b8e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,3 +1,6 @@ +import logging +from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed448, ed25519 + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -5,12 +8,15 @@ from esphome.automation import Condition from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY + CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, \ + CONF_CERTIFICATE, CONF_KEY, CONF_USERNAME, CONF_EAP from esphome.core import CORE, HexInt, coroutine_with_priority +_LOGGER = logging.getLogger(__name__) AUTO_LOAD = ['network'] wifi_ns = cg.esphome_ns.namespace('wifi') +EAPAuth = wifi_ns.struct('EAPAuth') IPAddress = cg.global_ns.class_('IPAddress') ManualIP = wifi_ns.struct('ManualIP') WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) @@ -36,6 +42,55 @@ def validate_password(value): return value +def validate_eap(value): + if CONF_USERNAME in value: + if CONF_IDENTITY not in value: + _LOGGER.info("EAP 'identity:' is not set, assuming username.") + value[CONF_IDENTITY] = value[CONF_USERNAME] + if CONF_PASSWORD not in value: + raise cv.Invalid("You cannot use the EAP 'username:' option without a 'password:'. " + "Please provide the 'password:' key") + if CONF_CERTIFICATE in value or CONF_KEY in value: + if CONF_CERTIFICATE not in value and CONF_KEY not in value: + raise cv.Invalid("You have provided an EAP 'certificate:' or 'key:' without providing " + "the other. Please check you have provided both.") + # Check the key is valid and for this certificate, just to check the user hasn't pasted + # the wrong thing. I write this after I spent a while debugging that exact issue. + # This may require a password to decrypt to key, so we should verify that at the same time. + certPw = None + if CONF_PASSWORD in value: + certPw = value[CONF_PASSWORD] + + cert = cv.load_certificate(value[CONF_CERTIFICATE]) + try: + key = cv.load_key(value[CONF_KEY], certPw) + except ValueError as e: + raise cv.Invalid( + "There was an error with the EAP 'password:' provided for 'key:' :%s" % e + ) + except TypeError as e: + raise cv.Invalid("There was an error with the EAP 'key:' provided :%s" % e) + + if isinstance(key, rsa.RSAPrivateKey): + if key.public_key().public_numbers() != cert.public_key().public_numbers(): + raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") + elif isinstance(key, ec.EllipticCurvePrivateKey): + if key.public_key().public_numbers() != cert.public_key().public_numbers(): + raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") + elif isinstance(key, ed448.Ed448PrivateKey): + if key.public_key() != cert: + raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") + elif isinstance(key, ed25519.Ed25519PrivateKey): + if key.public_key() != cert: + raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") + else: + _LOGGER.warning( + "Unrecognised EAP 'certificate:' 'key:' pair format: %s. Proceed with caution!", + type(key) + ) + return value + + def validate_channel(value): value = cv.positive_int(value) if value < 1: @@ -56,6 +111,15 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, }) +EAP_AUTH_SCHEMA = cv.Schema({ + cv.Optional(CONF_IDENTITY): cv.string_strict, + cv.Optional(CONF_USERNAME): cv.string_strict, + cv.Optional(CONF_PASSWORD): cv.string_strict, + cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.certificate, + cv.Optional(CONF_CERTIFICATE): cv.certificate, + cv.Optional(CONF_KEY): cv.string_strict, +}) + WIFI_NETWORK_BASE = cv.Schema({ cv.GenerateID(): cv.declare_id(WiFiAP), cv.Optional(CONF_SSID): cv.ssid, @@ -73,6 +137,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, }) @@ -93,6 +158,10 @@ def validate(config): raise cv.Invalid("Please specify at least an SSID or an Access Point " "to create.") + for network in config[CONF_NETWORKS]: + if CONF_EAP in network: + network[CONF_EAP] = validate_eap(network[CONF_EAP]) + if config.get(CONF_FAST_CONNECT, False): networks = config.get(CONF_NETWORKS, []) if not networks: @@ -118,6 +187,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, @@ -133,6 +203,20 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ }), validate) +def eap_auth(config): + if config is None: + return None + return cg.StructInitializer( + EAPAuth, + ('identity', config.get(CONF_IDENTITY, "")), + ('username', config.get(CONF_USERNAME, "")), + ('password', config.get(CONF_PASSWORD, "")), + ('ca_cert', config.get(CONF_CERTIFICATE_AUTHORITY, "")), + ('client_cert', config.get(CONF_CERTIFICATE, "")), + ('client_key', config.get(CONF_KEY, "")), + ) + + def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) @@ -158,6 +242,8 @@ def wifi_network(config, static_ip): cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: cg.add(ap.set_password(config[CONF_PASSWORD])) + if CONF_EAP in config: + cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d56e75a070..dc955add25 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -541,12 +541,14 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } +const optional &WiFiAP::get_eap() const { return this->eap_; } const optional &WiFiAP::get_channel() const { return this->channel_; } const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } bool WiFiAP::get_hidden() const { return this->hidden_; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d04e1c2ce0..d58ea6c0fa 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -57,6 +57,16 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +struct EAPAuth { + std::string identity; // required for all auth types + std::string username; + std::string password; + char *ca_cert; // optionally verify authentication server + // used for EAP-TLS + char *client_cert; + char *client_key; +}; + using bssid_t = std::array; class WiFiAP { @@ -65,6 +75,7 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); + void set_eap(optional eap_auth); void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -72,6 +83,7 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; + const optional &get_eap() const; const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -81,6 +93,7 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; + optional eap_; optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index e345ab1671..86c8078171 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "lwip/err.h" #include "lwip/dns.h" @@ -187,6 +188,51 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { return false; } + // setup enterprise authentication if required + if (ap.get_eap().has_value()) { + // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. + EAPAuth eap = ap.get_eap().value(); + err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); + } + int ca_cert_len = strlen(eap.ca_cert); + int client_cert_len = strlen(eap.client_cert); + int client_key_len = strlen(eap.client_key); + if (ca_cert_len) { + err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); + } + } + // workout what type of EAP this is + // validation is not required as the config tool has already validated it + if (client_cert_len && client_key_len) { + // if we have certs, this must be EAP-TLS + err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + } + } else { + // in the absence of certs, assume this is username/password based + err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + } + err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + } + } + esp_wpa2_config_t wpa2Config = WPA2_CONFIG_INIT_DEFAULT(); + err = esp_wifi_sta_wpa2_ent_enable(&wpa2Config); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + } + } + this->wifi_apply_hostname_(); err = esp_wifi_connect(); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1319786841..9f7bdddaf0 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -10,6 +10,10 @@ from string import ascii_letters, digits import voluptuous as vol +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.serialization import load_pem_private_key + from esphome import core from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \ @@ -717,6 +721,25 @@ def domain_name(value): return value +def load_certificate(value): + return x509.load_pem_x509_certificate(value.encode('UTF-8'), default_backend()) + + +def load_key(value, password): + if password: + password = password.encode("UTF-8") + return load_pem_private_key(value.encode('UTF-8'), password, default_backend()) + + +def certificate(value): + value = string_strict(value) + try: + load_certificate(value) # raises ValueError + return value + except ValueError: + return Invalid("Invalid certificate") + + def ssid(value): value = string_strict(value) if not value: diff --git a/esphome/const.py b/esphome/const.py index 8b727b615c..cf2155b83d 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -76,6 +76,8 @@ CONF_CALIBRATION = 'calibration' CONF_CAPACITANCE = 'capacitance' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' +CONF_CERTIFICATE = "certificate" +CONF_CERTIFICATE_AUTHORITY = "certificate_authority" CONF_CHANGE_MODE_EVERY = 'change_mode_every' CONF_CHANNEL = 'channel' CONF_CHANNELS = 'channels' @@ -146,6 +148,7 @@ CONF_DRY_ACTION = 'dry_action' CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' +CONF_EAP = 'eap' CONF_ECHO_PIN = 'echo_pin' CONF_EFFECT = 'effect' CONF_EFFECTS = 'effects' @@ -211,6 +214,7 @@ CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' CONF_ID = 'id' +CONF_IDENTITY = 'identity' CONF_IDLE = 'idle' CONF_IDLE_ACTION = 'idle_action' CONF_IDLE_LEVEL = 'idle_level' @@ -238,6 +242,7 @@ CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' CONF_KEEP_ON_TIME = 'keep_on_time' CONF_KEEPALIVE = 'keepalive' +CONF_KEY = 'key' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' diff --git a/requirements.txt b/requirements.txt index 0c99eaccbe..3efe16cc45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,5 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 +cryptography==2.9.2 click==7.1.2 diff --git a/requirements_test.txt b/requirements_test.txt index b90c4ea969..72078ad6e4 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,6 +10,7 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 +cryptography==2.9.2 pylint==2.5.0 flake8==3.7.9 From 7fa98e288f648318bdfca93c6e4493679a291a42 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 9 Jul 2020 19:53:49 -0500 Subject: [PATCH 055/200] SSD1325 grayscale support (#1064) * SSD1325 grayscale support implemented * Linted * Fix garbage on display at power-up * Rebase * Linted * Add brightness control, CS fixes * Fix up SSD1327 init * Add turn_on() and turn_off() methods * Set brightness later in setup(), logging tweak * Added is_on() method * Added grayscale image support * Updated to use Color, 1327 GS fix --- esphome/components/display/display_buffer.cpp | 17 +-- esphome/components/display/display_buffer.h | 4 +- esphome/components/image/__init__.py | 16 +- esphome/components/ssd1325_base/__init__.py | 8 +- .../components/ssd1325_base/ssd1325_base.cpp | 142 ++++++++++++------ .../components/ssd1325_base/ssd1325_base.h | 7 + esphome/components/ssd1325_spi/display.py | 2 +- .../components/ssd1325_spi/ssd1325_spi.cpp | 19 +-- 8 files changed, 129 insertions(+), 86 deletions(-) diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index cd28e45071..30c22f72ff 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -7,8 +7,8 @@ namespace display { static const char *TAG = "display"; -const Color COLOR_OFF = 0; -const Color COLOR_ON = 1; +const Color COLOR_OFF(0, 0, 0, 0); +const Color COLOR_ON(1, 1, 1, 1); void DisplayBuffer::init_internal_(uint32_t buffer_length) { this->buffer_ = new uint8_t[buffer_length]; @@ -214,10 +214,10 @@ void DisplayBuffer::image(int x, int y, Color color, Image *image, bool invert) this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color : COLOR_OFF); } } - } else if (image->get_type() == GRAYSCALE4) { + } else if (image->get_type() == GRAYSCALE) { for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_y = 0; img_y < image->get_height(); img_y++) { - this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale4_pixel(img_x, img_y)); + this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); } } } else if (image->get_type() == RGB565) { @@ -457,14 +457,11 @@ int Image::get_color_pixel(int x, int y) const { int color = (pgm_read_byte(this->data_start_ + pos) << 8) + (pgm_read_byte(this->data_start_ + pos + 1)); return color; } -int Image::get_grayscale4_pixel(int x, int y) const { +Color Image::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return 0; - const uint32_t pos = (x + y * this->width_) / 2; - // 2 = number of pixels per byte, 4 = pixel shift - uint8_t shift = (x % 2) * 4; - int color = (pgm_read_byte(this->data_start_ + pos) >> shift) & 0x0f; - return color; + const uint32_t pos = (x + y * this->width_); + return Color(pgm_read_byte(this->data_start_ + pos) << 24); } int Image::get_width() const { return this->width_; } int Image::get_height() const { return this->height_; } diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 4b84e90a08..88f77a0362 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -68,7 +68,7 @@ extern const Color COLOR_OFF; /// Turn the pixel ON. extern const Color COLOR_ON; -enum ImageType { BINARY = 0, GRAYSCALE4 = 1, RGB565 = 2 }; +enum ImageType { BINARY = 0, GRAYSCALE = 1, RGB565 = 2 }; enum DisplayRotation { DISPLAY_ROTATION_0_DEGREES = 0, @@ -385,7 +385,7 @@ class Image { Image(const uint8_t *data_start, int width, int height, int type); bool get_pixel(int x, int y) const; int get_color_pixel(int x, int y) const; - int get_grayscale4_pixel(int x, int y) const; + Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; ImageType get_type() const; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 62379b11bb..3649f8a869 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['display'] MULTI_CONF = True -ImageType = {'binary': 0, 'grayscale4': 1, 'rgb565': 2} +ImageType = {'binary': 0, 'grayscale': 1, 'rgb565': 2} Image_ = display.display_ns.class_('Image') @@ -41,20 +41,18 @@ def to_code(config): image.thumbnail(config[CONF_RESIZE]) if CONF_TYPE in config: - if config[CONF_TYPE].startswith('GRAYSCALE4'): + if config[CONF_TYPE].startswith('GRAYSCALE'): width, height = image.size image = image.convert('L', dither=Image.NONE) pixels = list(image.getdata()) - data = [0 for _ in range(height * width // 2)] + data = [0 for _ in range(height * width)] pos = 0 - for pixnum, pix in enumerate(pixels): - pixshift = (pixnum % 2) * 4 - data[pos] |= (pix >> 4) << pixshift - if pixshift != 0: - pos += 1 + for pix in pixels: + data[pos] = pix + pos += 1 rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['grayscale4']) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['grayscale']) elif config[CONF_TYPE].startswith('RGB565'): width, height = image.size image = image.convert('RGB') diff --git a/esphome/components/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py index 011642c408..6cb0dafe54 100644 --- a/esphome/components/ssd1325_base/__init__.py +++ b/esphome/components/ssd1325_base/__init__.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import display -from esphome.const import CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.const import CONF_BRIGHTNESS, CONF_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, \ + CONF_RESET_PIN from esphome.core import coroutine ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') @@ -22,12 +23,13 @@ SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_MODEL): SSD1325_MODEL, cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, }).extend(cv.polling_component_schema('1s')) @coroutine -def setup_ssd1036(var, config): +def setup_ssd1325(var, config): yield cg.register_component(var, config) yield display.register_display(var, config) @@ -35,6 +37,8 @@ def setup_ssd1036(var, config): if CONF_RESET_PIN in config: reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) if CONF_EXTERNAL_VCC in config: cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) if CONF_LAMBDA in config: diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp index 044582804f..dfb1ef00ee 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.cpp +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -8,7 +8,11 @@ namespace ssd1325_base { static const char *TAG = "ssd1325"; static const uint8_t BLACK = 0; -static const uint8_t WHITE = 1; +static const uint8_t WHITE = 15; +static const uint8_t SSD1325_MAX_CONTRAST = 127; +static const uint8_t SSD1325_COLORMASK = 0x0f; +static const uint8_t SSD1325_COLORSHIFT = 4; +static const uint8_t SSD1325_PIXELSPERBYTE = 2; static const uint8_t SSD1325_SETCOLADDR = 0x15; static const uint8_t SSD1325_SETROWADDR = 0x75; @@ -33,6 +37,7 @@ static const uint8_t SSD1325_SETROWPERIOD = 0xB2; static const uint8_t SSD1325_SETCLOCK = 0xB3; static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETDEFAULTGRAYTABLE = 0xB9; static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; static const uint8_t SSD1325_SETVSL = 0xBF; @@ -44,40 +49,57 @@ static const uint8_t SSD1325_COPY = 0x25; void SSD1325::setup() { this->init_internal_(this->get_buffer_length_()); - this->command(SSD1325_DISPLAYOFF); /* display off */ - this->command(SSD1325_SETCLOCK); /* set osc division */ - this->command(0xF1); /* 145 */ - this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ + this->command(SSD1325_DISPLAYOFF); // display off + this->command(SSD1325_SETCLOCK); // set osc division + this->command(0xF1); // 145 + this->command(SSD1325_SETMULTIPLEX); // multiplex ratio if (this->model_ == SSD1327_MODEL_128_128) this->command(0x7f); // duty = height - 1 else - this->command(0x3f); // duty = 1/64 - this->command(SSD1325_SETOFFSET); /* set display offset --- */ + this->command(0x3f); // duty = 1/64 + this->command(SSD1325_SETOFFSET); // set display offset if (this->model_ == SSD1327_MODEL_128_128) this->command(0x00); // 0 else - this->command(0x4C); // 76 - this->command(SSD1325_SETSTARTLINE); /*set start line */ - this->command(0x00); /* ------ */ - this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(0x4C); // 76 + this->command(SSD1325_SETSTARTLINE); // set start line + this->command(0x00); // ... + this->command(SSD1325_MASTERCONFIG); // Set Master Config DC/DC Converter this->command(0x02); - this->command(SSD1325_SETREMAP); /* set segment remap------ */ + this->command(SSD1325_SETREMAP); // set segment remapping if (this->model_ == SSD1327_MODEL_128_128) - this->command(0x55); // 0x56 is flipped horizontally: enable column swap, disable nibble remap + this->command(0x53); // COM bottom-up, split odd/even, enable column and nibble remapping else - this->command(0x56); - this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(0x50); // COM bottom-up, split odd/even + this->command(SSD1325_SETCURRENT + 0x2); // Set Full Current Range this->command(SSD1325_SETGRAYTABLE); - this->command(0x01); - this->command(0x11); - this->command(0x22); - this->command(0x32); - this->command(0x43); - this->command(0x54); - this->command(0x65); - this->command(0x76); - this->command(SSD1325_SETCONTRAST); /* set contrast current */ - this->command(0x7F); // max! + // gamma ~2.2 + if (this->model_ == SSD1327_MODEL_128_128) { + this->command(0); + this->command(1); + this->command(2); + this->command(3); + this->command(6); + this->command(8); + this->command(12); + this->command(16); + this->command(20); + this->command(26); + this->command(32); + this->command(39); + this->command(46); + this->command(54); + this->command(63); + } else { + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + } this->command(SSD1325_SETROWPERIOD); this->command(0x51); this->command(SSD1325_SETPHASELEN); @@ -87,22 +109,25 @@ void SSD1325::setup() { this->command(SSD1325_SETPRECHARGECOMPENABLE); this->command(0x28); this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin - this->command(0x1C); //? - this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x1C); + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin this->command(0x0D | 0x02); - this->command(SSD1325_NORMALDISPLAY); /* set display mode */ - this->command(SSD1325_DISPLAYON); /* display ON */ + this->command(SSD1325_NORMALDISPLAY); // set display mode + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON } void SSD1325::display() { - this->command(SSD1325_SETCOLADDR); /* set column address */ - this->command(0x00); /* set column start address */ - this->command(0x3F); /* set column end address */ - this->command(SSD1325_SETROWADDR); /* set row address */ - this->command(0x00); /* set row start address */ + this->command(SSD1325_SETCOLADDR); // set column address + this->command(0x00); // set column start address + this->command(0x3F); // set column end address + this->command(SSD1325_SETROWADDR); // set row address + this->command(0x00); // set row start address if (this->model_ == SSD1327_MODEL_128_128) - this->command(0x7F); // 127 is last row + this->command(127); // set last row else - this->command(0x3F); // 63 is last row + this->command(63); // set last row this->write_display_data(); } @@ -110,6 +135,27 @@ void SSD1325::update() { this->do_update_(); this->display(); } +void SSD1325::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1325_SETCONTRAST); + this->command(int(SSD1325_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1325::is_on() { return this->is_on_; } +void SSD1325::turn_on() { + this->command(SSD1325_DISPLAYON); + this->is_on_ = true; +} +void SSD1325::turn_off() { + this->command(SSD1325_DISPLAYOFF); + this->is_on_ = false; +} int SSD1325::get_height_internal() { switch (this->model_) { case SSD1325_MODEL_128_32: @@ -141,23 +187,25 @@ int SSD1325::get_width_internal() { } } size_t SSD1325::get_buffer_length_() { - return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / SSD1325_PIXELSPERBYTE; } - void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, Color color) { if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) return; - - uint16_t pos = x + (y / 8) * this->get_width_internal(); - uint8_t subpos = y % 8; - if (color.is_on()) { - this->buffer_[pos] |= (1 << subpos); - } else { - this->buffer_[pos] &= ~(1 << subpos); - } + uint32_t color4 = color.to_grayscale4(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x / SSD1325_PIXELSPERBYTE) + (y * this->get_width_internal() / SSD1325_PIXELSPERBYTE); + uint8_t shift = (x % SSD1325_PIXELSPERBYTE) * SSD1325_COLORSHIFT; + // ensure 'color4' is valid (only 4 bits aka 1 nibble) and shift the bits left when necessary + color4 = (color4 & SSD1325_COLORMASK) << shift; + // first mask off the nibble we must change... + this->buffer_[pos] &= (~SSD1325_COLORMASK >> shift); + // ...then lay the new nibble back on top. done! + this->buffer_[pos] |= color4; } void SSD1325::fill(Color color) { - uint8_t fill = color.is_on() ? 0xFF : 0x00; + const uint32_t color4 = color.to_grayscale4(); + uint8_t fill = (color4 & SSD1325_COLORMASK) | ((color4 & SSD1325_COLORMASK) << SSD1325_COLORSHIFT); for (uint32_t i = 0; i < this->get_buffer_length_(); i++) this->buffer_[i] = fill; } diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h index b5b28dfbae..a06ba69a59 100644 --- a/esphome/components/ssd1325_base/ssd1325_base.h +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -26,6 +26,11 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1325Model model) { this->model_ = model; } void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); float get_setup_priority() const override { return setup_priority::PROCESSOR; } void fill(Color color) override; @@ -45,6 +50,8 @@ class SSD1325 : public PollingComponent, public display::DisplayBuffer { SSD1325Model model_{SSD1325_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + bool is_on_{false}; + float brightness_{1.0}; }; } // namespace ssd1325_base diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index d7c7733afc..ed61d3dec3 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield ssd1325_base.setup_ssd1036(var, config) + yield ssd1325_base.setup_ssd1325(var, config) yield spi.register_spi_device(var, config) dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp index cd5f4a2794..9df05439ca 100644 --- a/esphome/components/ssd1325_spi/ssd1325_spi.cpp +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -21,9 +21,11 @@ void SPISSD1325::setup() { void SPISSD1325::dump_config() { LOG_DISPLAY("", "SPI SSD1325", this); ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); - LOG_PIN(" CS Pin: ", this->cs_); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); LOG_UPDATE_INTERVAL(this); } @@ -48,20 +50,7 @@ void HOT SPISSD1325::write_display_data() { this->cs_->digital_write(false); delay(1); this->enable(); - for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { - for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once - uint8_t left8 = this->buffer_[y * 16 + x]; - uint8_t right8 = this->buffer_[y * 16 + x + 1]; - for (uint8_t p = 0; p < 8; p++) { - uint8_t d = 0; - if (left8 & (1 << p)) - d |= 0xF0; - if (right8 & (1 << p)) - d |= 0x0F; - this->write_byte(d); - } - } - } + this->write_array(this->buffer_, this->get_buffer_length_()); if (this->cs_) this->cs_->digital_write(true); this->disable(); From 417a3cdf51d3fef5628bf0d0ef2f14effa4c9551 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Thu, 9 Jul 2020 21:49:26 -0500 Subject: [PATCH 056/200] Add SSD1351 OLED display support (#1100) * Add SSD1351 display support * Linting round 2 * Updated/rebased for proper Color support * Fix color image processing --- esphome/components/display/display_buffer.cpp | 14 +- esphome/components/display/display_buffer.h | 4 +- esphome/components/image/__init__.py | 18 +- esphome/components/ssd1351_base/__init__.py | 40 ++++ .../components/ssd1351_base/ssd1351_base.cpp | 193 ++++++++++++++++++ .../components/ssd1351_base/ssd1351_base.h | 54 +++++ esphome/components/ssd1351_spi/__init__.py | 0 esphome/components/ssd1351_spi/display.py | 26 +++ .../components/ssd1351_spi/ssd1351_spi.cpp | 72 +++++++ esphome/components/ssd1351_spi/ssd1351_spi.h | 30 +++ tests/test1.yaml | 7 + 11 files changed, 440 insertions(+), 18 deletions(-) create mode 100644 esphome/components/ssd1351_base/__init__.py create mode 100644 esphome/components/ssd1351_base/ssd1351_base.cpp create mode 100644 esphome/components/ssd1351_base/ssd1351_base.h create mode 100644 esphome/components/ssd1351_spi/__init__.py create mode 100644 esphome/components/ssd1351_spi/display.py create mode 100644 esphome/components/ssd1351_spi/ssd1351_spi.cpp create mode 100644 esphome/components/ssd1351_spi/ssd1351_spi.h diff --git a/esphome/components/display/display_buffer.cpp b/esphome/components/display/display_buffer.cpp index 30c22f72ff..4f62a62c6d 100644 --- a/esphome/components/display/display_buffer.cpp +++ b/esphome/components/display/display_buffer.cpp @@ -1,4 +1,5 @@ #include "display_buffer.h" +#include "esphome/core/color.h" #include "esphome/core/log.h" #include "esphome/core/application.h" @@ -220,7 +221,7 @@ void DisplayBuffer::image(int x, int y, Color color, Image *image, bool invert) this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); } } - } else if (image->get_type() == RGB565) { + } else if (image->get_type() == RGB) { for (int img_x = 0; img_x < image->get_width(); img_x++) { for (int img_y = 0; img_y < image->get_height(); img_y++) { this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); @@ -449,13 +450,14 @@ bool Image::get_pixel(int x, int y) const { const uint32_t pos = x + y * width_8; return pgm_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); } -int Image::get_color_pixel(int x, int y) const { +Color Image::get_color_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) return 0; - - const uint32_t pos = (x + y * this->width_) * 2; - int color = (pgm_read_byte(this->data_start_ + pos) << 8) + (pgm_read_byte(this->data_start_ + pos + 1)); - return color; + const uint32_t pos = (x + y * this->width_) * 3; + const uint32_t color32 = (pgm_read_byte(this->data_start_ + pos + 2) << 0) | + (pgm_read_byte(this->data_start_ + pos + 1) << 8) | + (pgm_read_byte(this->data_start_ + pos + 0) << 16); + return Color(color32); } Color Image::get_grayscale_pixel(int x, int y) const { if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 88f77a0362..a8a308538e 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -68,7 +68,7 @@ extern const Color COLOR_OFF; /// Turn the pixel ON. extern const Color COLOR_ON; -enum ImageType { BINARY = 0, GRAYSCALE = 1, RGB565 = 2 }; +enum ImageType { BINARY = 0, GRAYSCALE = 1, RGB = 2 }; enum DisplayRotation { DISPLAY_ROTATION_0_DEGREES = 0, @@ -384,7 +384,7 @@ class Image { Image(const uint8_t *data_start, int width, int height); Image(const uint8_t *data_start, int width, int height, int type); bool get_pixel(int x, int y) const; - int get_color_pixel(int x, int y) const; + Color get_color_pixel(int x, int y) const; Color get_grayscale_pixel(int x, int y) const; int get_width() const; int get_height() const; diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 3649f8a869..b5c9e29f97 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -11,7 +11,7 @@ _LOGGER = logging.getLogger(__name__) DEPENDENCIES = ['display'] MULTI_CONF = True -ImageType = {'binary': 0, 'grayscale': 1, 'rgb565': 2} +ImageType = {'binary': 0, 'grayscale': 1, 'rgb': 2} Image_ = display.display_ns.class_('Image') @@ -53,24 +53,22 @@ def to_code(config): rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['grayscale']) - elif config[CONF_TYPE].startswith('RGB565'): + elif config[CONF_TYPE].startswith('RGB'): width, height = image.size image = image.convert('RGB') pixels = list(image.getdata()) - data = [0 for _ in range(height * width * 2)] + data = [0 for _ in range(height * width * 3)] pos = 0 for pix in pixels: - r = (pix[0] >> 3) & 0x1F - g = (pix[1] >> 2) & 0x3F - b = (pix[2] >> 3) & 0x1F - p = (r << 11) + (g << 5) + b - data[pos] = (p >> 8) & 0xFF + data[pos] = pix[0] pos += 1 - data[pos] = p & 0xFF + data[pos] = pix[1] + pos += 1 + data[pos] = pix[2] pos += 1 rhs = [HexInt(x) for x in data] prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) - cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['rgb565']) + cg.new_Pvariable(config[CONF_ID], prog_arr, width, height, ImageType['rgb']) else: image = image.convert('1', dither=Image.NONE) width, height = image.size diff --git a/esphome/components/ssd1351_base/__init__.py b/esphome/components/ssd1351_base/__init__.py new file mode 100644 index 0000000000..198f81668e --- /dev/null +++ b/esphome/components/ssd1351_base/__init__.py @@ -0,0 +1,40 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import display +from esphome.const import CONF_BRIGHTNESS, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN +from esphome.core import coroutine + +ssd1351_base_ns = cg.esphome_ns.namespace('ssd1351_base') +SSD1351 = ssd1351_base_ns.class_('SSD1351', cg.PollingComponent, display.DisplayBuffer) +SSD1351Model = ssd1351_base_ns.enum('SSD1351Model') + +MODELS = { + 'SSD1351_128X96': SSD1351Model.SSD1351_MODEL_128_96, + 'SSD1351_128X128': SSD1351Model.SSD1351_MODEL_128_128, +} + +SSD1351_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1351_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1351_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1351(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_BRIGHTNESS in config: + cg.add(var.init_brightness(config[CONF_BRIGHTNESS])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1351_base/ssd1351_base.cpp b/esphome/components/ssd1351_base/ssd1351_base.cpp new file mode 100644 index 0000000000..fded8e3482 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.cpp @@ -0,0 +1,193 @@ +#include "ssd1351_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1351_base { + +static const char *TAG = "ssd1351"; + +static const uint16_t BLACK = 0; +static const uint16_t WHITE = 0xffff; +static const uint16_t SSD1351_COLORMASK = 0xffff; +static const uint8_t SSD1351_MAX_CONTRAST = 15; +static const uint8_t SSD1351_BYTESPERPIXEL = 2; +// SSD1351 commands +static const uint8_t SSD1351_SETCOLUMN = 0x15; +static const uint8_t SSD1351_SETROW = 0x75; +static const uint8_t SSD1351_SETREMAP = 0xA0; +static const uint8_t SSD1351_STARTLINE = 0xA1; +static const uint8_t SSD1351_DISPLAYOFFSET = 0xA2; +static const uint8_t SSD1351_DISPLAYOFF = 0xAE; +static const uint8_t SSD1351_DISPLAYON = 0xAF; +static const uint8_t SSD1351_PRECHARGE = 0xB1; +static const uint8_t SSD1351_CLOCKDIV = 0xB3; +static const uint8_t SSD1351_PRECHARGELEVEL = 0xBB; +static const uint8_t SSD1351_VCOMH = 0xBE; +// display controls +static const uint8_t SSD1351_DISPLAYALLOFF = 0xA4; +static const uint8_t SSD1351_DISPLAYALLON = 0xA5; +static const uint8_t SSD1351_NORMALDISPLAY = 0xA6; +static const uint8_t SSD1351_INVERTDISPLAY = 0xA7; +// contrast controls +static const uint8_t SSD1351_CONTRASTABC = 0xC1; +static const uint8_t SSD1351_CONTRASTMASTER = 0xC7; +// memory functions +static const uint8_t SSD1351_WRITERAM = 0x5C; +static const uint8_t SSD1351_READRAM = 0x5D; +// other functions +static const uint8_t SSD1351_FUNCTIONSELECT = 0xAB; +static const uint8_t SSD1351_DISPLAYENHANCE = 0xB2; +static const uint8_t SSD1351_SETVSL = 0xB4; +static const uint8_t SSD1351_SETGPIO = 0xB5; +static const uint8_t SSD1351_PRECHARGE2 = 0xB6; +static const uint8_t SSD1351_SETGRAY = 0xB8; +static const uint8_t SSD1351_USELUT = 0xB9; +static const uint8_t SSD1351_MUXRATIO = 0xCA; +static const uint8_t SSD1351_COMMANDLOCK = 0xFD; +static const uint8_t SSD1351_HORIZSCROLL = 0x96; +static const uint8_t SSD1351_STOPSCROLL = 0x9E; +static const uint8_t SSD1351_STARTSCROLL = 0x9F; + +void SSD1351::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1351_COMMANDLOCK); + this->data(0x12); + this->command(SSD1351_COMMANDLOCK); + this->data(0xB1); + this->command(SSD1351_DISPLAYOFF); + this->command(SSD1351_CLOCKDIV); + this->data(0xF1); // 7:4 = Oscillator Freq, 3:0 = CLK Div Ratio (A[3:0]+1 = 1..16) + this->command(SSD1351_MUXRATIO); + this->data(127); + this->command(SSD1351_DISPLAYOFFSET); + this->data(0x00); + this->command(SSD1351_SETGPIO); + this->data(0x00); + this->command(SSD1351_FUNCTIONSELECT); + this->data(0x01); // internal (diode drop) + this->command(SSD1351_PRECHARGE); + this->data(0x32); + this->command(SSD1351_VCOMH); + this->data(0x05); + this->command(SSD1351_NORMALDISPLAY); + this->command(SSD1351_SETVSL); + this->data(0xA0); + this->data(0xB5); + this->data(0x55); + this->command(SSD1351_PRECHARGE2); + this->data(0x01); + this->command(SSD1351_SETREMAP); + this->data(0x34); + this->command(SSD1351_STARTLINE); + this->data(0x00); + this->command(SSD1351_CONTRASTABC); + this->data(0xC8); + this->data(0x80); + this->data(0xC8); + set_brightness(this->brightness_); + this->fill(BLACK); // clear display - ensures we do not see garbage at power-on + this->display(); // ...write buffer, which actually clears the display's memory + this->turn_on(); // display ON +} +void SSD1351::display() { + this->command(SSD1351_SETCOLUMN); // set column address + this->data(0x00); // set column start address + this->data(0x7F); // set column end address + this->command(SSD1351_SETROW); // set row address + this->data(0x00); // set row start address + this->data(0x7F); // set last row + this->command(SSD1351_WRITERAM); + this->write_display_data(); +} +void SSD1351::update() { + this->do_update_(); + this->display(); +} +void SSD1351::set_brightness(float brightness) { + // validation + if (brightness > 1) + this->brightness_ = 1.0; + else if (brightness < 0) + this->brightness_ = 0; + else + this->brightness_ = brightness; + // now write the new brightness level to the display + this->command(SSD1351_CONTRASTMASTER); + this->data(int(SSD1351_MAX_CONTRAST * (this->brightness_))); +} +bool SSD1351::is_on() { return this->is_on_; } +void SSD1351::turn_on() { + this->command(SSD1351_DISPLAYON); + this->is_on_ = true; +} +void SSD1351::turn_off() { + this->command(SSD1351_DISPLAYOFF); + this->is_on_ = false; +} +int SSD1351::get_height_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return 96; + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +int SSD1351::get_width_internal() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + case SSD1351_MODEL_128_128: + return 128; + default: + return 0; + } +} +size_t SSD1351::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) * size_t(SSD1351_BYTESPERPIXEL); +} +void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + const uint32_t color565 = color.to_rgb_565(); + // where should the bits go in the big buffer array? math... + uint16_t pos = (x + y * this->get_width_internal()) * SSD1351_BYTESPERPIXEL; + this->buffer_[pos++] = (color565 >> 8) & 0xff; + this->buffer_[pos] = color565 & 0xff; +} +void SSD1351::fill(Color color) { + const uint32_t color565 = color.to_rgb_565(); + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + if (i & 1) { + this->buffer_[i] = color565 & 0xff; + } else { + this->buffer_[i] = (color565 >> 8) & 0xff; + } +} +void SSD1351::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1351::model_str_() { + switch (this->model_) { + case SSD1351_MODEL_128_96: + return "SSD1351 128x96"; + case SSD1351_MODEL_128_128: + return "SSD1351 128x128"; + default: + return "Unknown"; + } +} + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_base/ssd1351_base.h b/esphome/components/ssd1351_base/ssd1351_base.h new file mode 100644 index 0000000000..2730f798b5 --- /dev/null +++ b/esphome/components/ssd1351_base/ssd1351_base.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1351_base { + +enum SSD1351Model { + SSD1351_MODEL_128_96 = 0, + SSD1351_MODEL_128_128, +}; + +class SSD1351 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1351Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void init_brightness(float brightness) { this->brightness_ = brightness; } + void set_brightness(float brightness); + bool is_on(); + void turn_on(); + void turn_off(); + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(Color color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void data(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, Color color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1351Model model_{SSD1351_MODEL_128_96}; + GPIOPin *reset_pin_{nullptr}; + bool is_on_{false}; + float brightness_{1.0}; +}; + +} // namespace ssd1351_base +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/__init__.py b/esphome/components/ssd1351_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1351_spi/display.py b/esphome/components/ssd1351_spi/display.py new file mode 100644 index 0000000000..16b0d4387a --- /dev/null +++ b/esphome/components/ssd1351_spi/display.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1351_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +AUTO_LOAD = ['ssd1351_base'] +DEPENDENCIES = ['spi'] + +ssd1351_spi = cg.esphome_ns.namespace('ssd1351_spi') +SPISSD1351 = ssd1351_spi.class_('SPISSD1351', ssd1351_base.SSD1351, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1351_base.SSD1351_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1351), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1351_base.setup_ssd1351(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.cpp b/esphome/components/ssd1351_spi/ssd1351_spi.cpp new file mode 100644 index 0000000000..2839ef7a8e --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.cpp @@ -0,0 +1,72 @@ +#include "ssd1351_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1351_spi { + +static const char *TAG = "ssd1351_spi"; + +void SPISSD1351::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1351..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + if (this->cs_) + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1351::setup(); +} +void SPISSD1351::dump_config() { + LOG_DISPLAY("", "SPI SSD1351", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + if (this->cs_) + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " Initial Brightness: %.2f", this->brightness_); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1351::command(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void SPISSD1351::data(uint8_t value) { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + delay(1); + this->enable(); + if (this->cs_) + this->cs_->digital_write(false); + this->write_byte(value); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1351::write_display_data() { + if (this->cs_) + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + if (this->cs_) + this->cs_->digital_write(false); + delay(1); + this->enable(); + this->write_array(this->buffer_, this->get_buffer_length_()); + if (this->cs_) + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/esphome/components/ssd1351_spi/ssd1351_spi.h b/esphome/components/ssd1351_spi/ssd1351_spi.h new file mode 100644 index 0000000000..b8f3310f5c --- /dev/null +++ b/esphome/components/ssd1351_spi/ssd1351_spi.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1351_base/ssd1351_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1351_spi { + +class SPISSD1351 : public ssd1351_base::SSD1351, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + void data(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1351_spi +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 83da6da9e3..5d8108c4be 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1594,6 +1594,13 @@ display: reset_pin: GPIO23 lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +- platform: ssd1351_spi + model: "SSD1351 128x128" + cs_pin: GPIO23 + dc_pin: GPIO23 + reset_pin: GPIO23 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); - platform: waveshare_epaper cs_pin: GPIO23 dc_pin: GPIO23 From 0e5e5592830b41dd71ddf6e27b7f8fa16b5bd3b1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Fri, 10 Jul 2020 14:50:19 +1200 Subject: [PATCH 057/200] Add cryptography requirement to the setup.py file (#1116) --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0a19dccf95..409c5a872e 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ REQUIRES = [ 'pytz==2020.1', 'pyserial==3.4', 'ifaddr==0.1.6', + 'cryptography==2.9.2', ] # If you have problems importing platformio and esptool as modules you can set From ec026ab3a8bffaaae89e998466bcc06120c06827 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 10 Jul 2020 17:36:29 -0300 Subject: [PATCH 058/200] Revert "Add ESP32 support for WPA2-EAP Enterprise WiFi authentication (#1080)" (#1117) This reverts commit c693c219f46c345dd64ea8c670d876615ba2077d. --- esphome/components/wifi/__init__.py | 88 +------------------ esphome/components/wifi/wifi_component.cpp | 2 - esphome/components/wifi/wifi_component.h | 13 --- .../components/wifi/wifi_component_esp32.cpp | 46 ---------- esphome/config_validation.py | 23 ----- esphome/const.py | 5 -- requirements.txt | 1 - requirements_test.txt | 1 - 8 files changed, 1 insertion(+), 178 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8b68264b8e..d3c7e51603 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -1,6 +1,3 @@ -import logging -from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed448, ed25519 - import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -8,15 +5,12 @@ from esphome.automation import Condition from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, \ - CONF_CERTIFICATE, CONF_KEY, CONF_USERNAME, CONF_EAP + CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY from esphome.core import CORE, HexInt, coroutine_with_priority -_LOGGER = logging.getLogger(__name__) AUTO_LOAD = ['network'] wifi_ns = cg.esphome_ns.namespace('wifi') -EAPAuth = wifi_ns.struct('EAPAuth') IPAddress = cg.global_ns.class_('IPAddress') ManualIP = wifi_ns.struct('ManualIP') WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) @@ -42,55 +36,6 @@ def validate_password(value): return value -def validate_eap(value): - if CONF_USERNAME in value: - if CONF_IDENTITY not in value: - _LOGGER.info("EAP 'identity:' is not set, assuming username.") - value[CONF_IDENTITY] = value[CONF_USERNAME] - if CONF_PASSWORD not in value: - raise cv.Invalid("You cannot use the EAP 'username:' option without a 'password:'. " - "Please provide the 'password:' key") - if CONF_CERTIFICATE in value or CONF_KEY in value: - if CONF_CERTIFICATE not in value and CONF_KEY not in value: - raise cv.Invalid("You have provided an EAP 'certificate:' or 'key:' without providing " - "the other. Please check you have provided both.") - # Check the key is valid and for this certificate, just to check the user hasn't pasted - # the wrong thing. I write this after I spent a while debugging that exact issue. - # This may require a password to decrypt to key, so we should verify that at the same time. - certPw = None - if CONF_PASSWORD in value: - certPw = value[CONF_PASSWORD] - - cert = cv.load_certificate(value[CONF_CERTIFICATE]) - try: - key = cv.load_key(value[CONF_KEY], certPw) - except ValueError as e: - raise cv.Invalid( - "There was an error with the EAP 'password:' provided for 'key:' :%s" % e - ) - except TypeError as e: - raise cv.Invalid("There was an error with the EAP 'key:' provided :%s" % e) - - if isinstance(key, rsa.RSAPrivateKey): - if key.public_key().public_numbers() != cert.public_key().public_numbers(): - raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") - elif isinstance(key, ec.EllipticCurvePrivateKey): - if key.public_key().public_numbers() != cert.public_key().public_numbers(): - raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") - elif isinstance(key, ed448.Ed448PrivateKey): - if key.public_key() != cert: - raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") - elif isinstance(key, ed25519.Ed25519PrivateKey): - if key.public_key() != cert: - raise cv.Invalid("The provided EAP 'key:' does not match the 'certificate:'") - else: - _LOGGER.warning( - "Unrecognised EAP 'certificate:' 'key:' pair format: %s. Proceed with caution!", - type(key) - ) - return value - - def validate_channel(value): value = cv.positive_int(value) if value < 1: @@ -111,15 +56,6 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, }) -EAP_AUTH_SCHEMA = cv.Schema({ - cv.Optional(CONF_IDENTITY): cv.string_strict, - cv.Optional(CONF_USERNAME): cv.string_strict, - cv.Optional(CONF_PASSWORD): cv.string_strict, - cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.certificate, - cv.Optional(CONF_CERTIFICATE): cv.certificate, - cv.Optional(CONF_KEY): cv.string_strict, -}) - WIFI_NETWORK_BASE = cv.Schema({ cv.GenerateID(): cv.declare_id(WiFiAP), cv.Optional(CONF_SSID): cv.ssid, @@ -137,7 +73,6 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, - cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, }) @@ -158,10 +93,6 @@ def validate(config): raise cv.Invalid("Please specify at least an SSID or an Access Point " "to create.") - for network in config[CONF_NETWORKS]: - if CONF_EAP in network: - network[CONF_EAP] = validate_eap(network[CONF_EAP]) - if config.get(CONF_FAST_CONNECT, False): networks = config.get(CONF_NETWORKS, []) if not networks: @@ -187,7 +118,6 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, - cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, @@ -203,20 +133,6 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ }), validate) -def eap_auth(config): - if config is None: - return None - return cg.StructInitializer( - EAPAuth, - ('identity', config.get(CONF_IDENTITY, "")), - ('username', config.get(CONF_USERNAME, "")), - ('password', config.get(CONF_PASSWORD, "")), - ('ca_cert', config.get(CONF_CERTIFICATE_AUTHORITY, "")), - ('client_cert', config.get(CONF_CERTIFICATE, "")), - ('client_key', config.get(CONF_KEY, "")), - ) - - def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) @@ -242,8 +158,6 @@ def wifi_network(config, static_ip): cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: cg.add(ap.set_password(config[CONF_PASSWORD])) - if CONF_EAP in config: - cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index dc955add25..d56e75a070 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -541,14 +541,12 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } -void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } -const optional &WiFiAP::get_eap() const { return this->eap_; } const optional &WiFiAP::get_channel() const { return this->channel_; } const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } bool WiFiAP::get_hidden() const { return this->hidden_; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d58ea6c0fa..d04e1c2ce0 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -57,16 +57,6 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; -struct EAPAuth { - std::string identity; // required for all auth types - std::string username; - std::string password; - char *ca_cert; // optionally verify authentication server - // used for EAP-TLS - char *client_cert; - char *client_key; -}; - using bssid_t = std::array; class WiFiAP { @@ -75,7 +65,6 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); - void set_eap(optional eap_auth); void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -83,7 +72,6 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; - const optional &get_eap() const; const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -93,7 +81,6 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; - optional eap_; optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index 86c8078171..e345ab1671 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,7 +6,6 @@ #include #include -#include #include "lwip/err.h" #include "lwip/dns.h" @@ -188,51 +187,6 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { return false; } - // setup enterprise authentication if required - if (ap.get_eap().has_value()) { - // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. - EAPAuth eap = ap.get_eap().value(); - err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); - } - int ca_cert_len = strlen(eap.ca_cert); - int client_cert_len = strlen(eap.client_cert); - int client_key_len = strlen(eap.client_key); - if (ca_cert_len) { - err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); - } - } - // workout what type of EAP this is - // validation is not required as the config tool has already validated it - if (client_cert_len && client_key_len) { - // if we have certs, this must be EAP-TLS - err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, - (uint8_t *) eap.client_key, client_key_len + 1, - (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); - } - } else { - // in the absence of certs, assume this is username/password based - err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); - } - err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); - } - } - esp_wpa2_config_t wpa2Config = WPA2_CONFIG_INIT_DEFAULT(); - err = esp_wifi_sta_wpa2_ent_enable(&wpa2Config); - if (err != ESP_OK) { - ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); - } - } - this->wifi_apply_hostname_(); err = esp_wifi_connect(); diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 9f7bdddaf0..1319786841 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -10,10 +10,6 @@ from string import ascii_letters, digits import voluptuous as vol -from cryptography import x509 -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.serialization import load_pem_private_key - from esphome import core from esphome.const import CONF_AVAILABILITY, CONF_COMMAND_TOPIC, CONF_DISCOVERY, CONF_ID, \ CONF_INTERNAL, CONF_NAME, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, \ @@ -721,25 +717,6 @@ def domain_name(value): return value -def load_certificate(value): - return x509.load_pem_x509_certificate(value.encode('UTF-8'), default_backend()) - - -def load_key(value, password): - if password: - password = password.encode("UTF-8") - return load_pem_private_key(value.encode('UTF-8'), password, default_backend()) - - -def certificate(value): - value = string_strict(value) - try: - load_certificate(value) # raises ValueError - return value - except ValueError: - return Invalid("Invalid certificate") - - def ssid(value): value = string_strict(value) if not value: diff --git a/esphome/const.py b/esphome/const.py index cf2155b83d..8b727b615c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -76,8 +76,6 @@ CONF_CALIBRATION = 'calibration' CONF_CAPACITANCE = 'capacitance' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' -CONF_CERTIFICATE = "certificate" -CONF_CERTIFICATE_AUTHORITY = "certificate_authority" CONF_CHANGE_MODE_EVERY = 'change_mode_every' CONF_CHANNEL = 'channel' CONF_CHANNELS = 'channels' @@ -148,7 +146,6 @@ CONF_DRY_ACTION = 'dry_action' CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' -CONF_EAP = 'eap' CONF_ECHO_PIN = 'echo_pin' CONF_EFFECT = 'effect' CONF_EFFECTS = 'effects' @@ -214,7 +211,6 @@ CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' CONF_ID = 'id' -CONF_IDENTITY = 'identity' CONF_IDLE = 'idle' CONF_IDLE_ACTION = 'idle_action' CONF_IDLE_LEVEL = 'idle_level' @@ -242,7 +238,6 @@ CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' CONF_KEEP_ON_TIME = 'keep_on_time' CONF_KEEPALIVE = 'keepalive' -CONF_KEY = 'key' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' diff --git a/requirements.txt b/requirements.txt index 3efe16cc45..0c99eaccbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,5 +10,4 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 -cryptography==2.9.2 click==7.1.2 diff --git a/requirements_test.txt b/requirements_test.txt index 72078ad6e4..b90c4ea969 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,7 +10,6 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 -cryptography==2.9.2 pylint==2.5.0 flake8==3.7.9 From 5ca4bac10a7cf797313a4288a73c8071be1628d1 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 10 Jul 2020 17:37:26 -0300 Subject: [PATCH 059/200] Revert "Add cryptography requirement to the setup.py file (#1116)" (#1118) This reverts commit 0e5e5592830b41dd71ddf6e27b7f8fa16b5bd3b1. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index 409c5a872e..0a19dccf95 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,6 @@ REQUIRES = [ 'pytz==2020.1', 'pyserial==3.4', 'ifaddr==0.1.6', - 'cryptography==2.9.2', ] # If you have problems importing platformio and esptool as modules you can set From 45f6926a5ad4fdf41c99df5938d61fa53140b693 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 10 Jul 2020 18:55:16 -0300 Subject: [PATCH 060/200] point dev docs to next url --- esphome/dashboard/dashboard.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 4f2d63d545..c4eb0eb266 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -100,9 +100,15 @@ cookie_authenticated_yes = b'yes' def template_args(): version = const.__version__ + if 'b' in version: + docs_link = 'https://beta.esphome.io/' + elif 'dev' in version: + docs_link = 'https://next.esphome.io/' + else: + docs_link = 'https://www.esphome.io/' return { 'version': version, - 'docs_link': 'https://beta.esphome.io/' if 'b' in version else 'https://esphome.io/', + 'docs_link': docs_link, 'get_static_file_url': get_static_file_url, 'relative_url': settings.relative_url, 'streamer_mode': get_bool_env('ESPHOME_STREAMER_MODE'), From ac4a17970389ea471ded1134ba533e84a2f46cb1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Jul 2020 08:22:03 +1200 Subject: [PATCH 061/200] Install updated git version in lint image (#1122) --- docker/Dockerfile.lint | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index f04b448142..cdd715ef5a 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -6,6 +6,10 @@ RUN \ clang-format-7 \ clang-tidy-7 \ patch \ + software-properties-common \ + && apt-add-repository ppa:git-core/ppa \ + && apt-get install -y --no-install-recommends \ + git \ && rm -rf \ /tmp/* \ /var/{cache,log}/* \ From f3158c8b24d44836a092c10771125a0546d62fa6 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 11 Jul 2020 17:33:25 -0300 Subject: [PATCH 062/200] fixes script wait not waiting (#1123) --- esphome/components/script/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 9590679f83..4437fa8d9e 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -8,7 +8,7 @@ script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) -ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) +ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action, cg.Component) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) CONFIG_SCHEMA = automation.validate_automation({ @@ -48,7 +48,9 @@ def script_stop_action_to_code(config, action_id, template_arg, args): })) def script_wait_action_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) + var = cg.new_Pvariable(action_id, template_arg, paren) + yield cg.register_component(var, {}) + yield var @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ From ef9e6e4d6e37583b4b12a3a3f6fa9739b4f30909 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Jul 2020 08:52:29 +1200 Subject: [PATCH 063/200] Add support for Tuya Switches (#1074) * Add tuya component to test4.yaml * Add support for tuya switches * Better config dump --- esphome/components/tuya/switch/__init__.py | 28 ++++++++++++++++ .../components/tuya/switch/tuya_switch.cpp | 33 +++++++++++++++++++ esphome/components/tuya/switch/tuya_switch.h | 26 +++++++++++++++ tests/test4.yaml | 8 +++++ 4 files changed, 95 insertions(+) create mode 100644 esphome/components/tuya/switch/__init__.py create mode 100644 esphome/components/tuya/switch/tuya_switch.cpp create mode 100644 esphome/components/tuya/switch/tuya_switch.h diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py new file mode 100644 index 0000000000..bbc8122801 --- /dev/null +++ b/esphome/components/tuya/switch/__init__.py @@ -0,0 +1,28 @@ +from esphome.components import switch +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_SWITCH_DATAPOINT = "switch_datapoint" + +TuyaSwitch = tuya_ns.class_('TuyaSwitch', switch.Switch, cg.Component) + +CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaSwitch), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SWITCH_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield switch.register_switch(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) diff --git a/esphome/components/tuya/switch/tuya_switch.cpp b/esphome/components/tuya/switch/tuya_switch.cpp new file mode 100644 index 0000000000..8f7c7f170d --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.cpp @@ -0,0 +1,33 @@ +#include "esphome/core/log.h" +#include "tuya_switch.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.switch"; + +void TuyaSwitch::setup() { + this->parent_->register_listener(this->switch_id_, [this](TuyaDatapoint datapoint) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); +} + +void TuyaSwitch::write_state(bool state) { + TuyaDatapoint datapoint{}; + datapoint.id = this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = state; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(state)); + + this->publish_state(state); +} + +void TuyaSwitch::dump_config() { + LOG_SWITCH("", "Tuya Switch", this); + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", this->switch_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/switch/tuya_switch.h b/esphome/components/tuya/switch/tuya_switch.h new file mode 100644 index 0000000000..89e6264e5c --- /dev/null +++ b/esphome/components/tuya/switch/tuya_switch.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/switch/switch.h" + +namespace esphome { +namespace tuya { + +class TuyaSwitch : public switch_::Switch, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + void write_state(bool state) override; + + Tuya *parent_; + uint8_t switch_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index c7aa016839..852179f9b5 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -49,6 +49,9 @@ web_server: username: admin password: admin + +tuya: + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -71,3 +74,8 @@ sensor: # - platform: apds9960 # type: blue # name: APDS9960 Blue + +switch: + - platform: tuya + id: tuya_switch + switch_datapoint: 1 From c3acf08c024c6d042a0c14731805c0c73364dc19 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Sat, 11 Jul 2020 14:32:10 -0700 Subject: [PATCH 064/200] fix script.wait action (#1120) Co-authored-by: Samuel Sieb Co-authored-by: Guillermo Ruffino --- esphome/components/script/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 4437fa8d9e..5075bb1f56 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -48,7 +48,7 @@ def script_stop_action_to_code(config, action_id, template_arg, args): })) def script_wait_action_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) + var = yield cg.new_Pvariable(action_id, template_arg, paren) yield cg.register_component(var, {}) yield var From 68d29c5af59fd2de209e062bb420d21265d2a49b Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Jul 2020 10:04:34 +1200 Subject: [PATCH 065/200] Add support for Tuya Climate devices (#1076) * Start support for tuya climate devices * Add tuya climate to test4 * Fix lint and cast * Remove blank line * Try to display accurate action based on observed behaviour. * Fix action when in off mode * Improve config dump * merge use of CONF_SWITCH_DATAPOINT Co-authored-by: Guillermo Ruffino --- esphome/components/tuya/climate/__init__.py | 41 +++++ .../components/tuya/climate/tuya_climate.cpp | 154 ++++++++++++++++++ .../components/tuya/climate/tuya_climate.h | 45 +++++ esphome/components/tuya/fan/__init__.py | 3 +- esphome/components/tuya/light/__init__.py | 3 +- esphome/components/tuya/switch/__init__.py | 4 +- esphome/const.py | 1 + tests/test4.yaml | 7 + 8 files changed, 251 insertions(+), 7 deletions(-) create mode 100644 esphome/components/tuya/climate/__init__.py create mode 100644 esphome/components/tuya/climate/tuya_climate.cpp create mode 100644 esphome/components/tuya/climate/tuya_climate.h diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py new file mode 100644 index 0000000000..b938be7b8c --- /dev/null +++ b/esphome/components/tuya/climate/__init__.py @@ -0,0 +1,41 @@ +from esphome.components import climate +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" +CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" +# CONF_ECO_MODE_DATAPOINT = "eco_mode_datapoint" + +TuyaClimate = tuya_ns.class_('TuyaClimate', climate.Climate, cg.Component) + +CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaClimate), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_TARGET_TEMPERATURE_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_CURRENT_TEMPERATURE_DATAPOINT): cv.uint8_t, + # cv.Optional(CONF_ECO_MODE_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key( + CONF_TARGET_TEMPERATURE_DATAPOINT, CONF_SWITCH_DATAPOINT)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_TARGET_TEMPERATURE_DATAPOINT in config: + cg.add(var.set_target_temperature_id(config[CONF_TARGET_TEMPERATURE_DATAPOINT])) + if CONF_CURRENT_TEMPERATURE_DATAPOINT in config: + cg.add(var.set_current_temperature_id(config[CONF_CURRENT_TEMPERATURE_DATAPOINT])) + # if CONF_ECO_MODE_DATAPOINT in config: + # cg.add(var.set_eco_mode_id(config[CONF_ECO_MODE_DATAPOINT])) diff --git a/esphome/components/tuya/climate/tuya_climate.cpp b/esphome/components/tuya/climate/tuya_climate.cpp new file mode 100644 index 0000000000..cfd5acccbd --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.cpp @@ -0,0 +1,154 @@ +#include "esphome/core/log.h" +#include "tuya_climate.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.climate"; + +void TuyaClimate::setup() { + if (this->switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.value_bool) { + this->mode = climate::CLIMATE_MODE_HEAT; + } else { + this->mode = climate::CLIMATE_MODE_OFF; + } + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported switch is: %s", ONOFF(datapoint.value_bool)); + }); + } + if (this->target_temperature_id_.has_value()) { + this->parent_->register_listener(*this->target_temperature_id_, [this](TuyaDatapoint datapoint) { + this->target_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported target temperature is: %d", datapoint.value_int); + }); + } + if (this->current_temperature_id_.has_value()) { + this->parent_->register_listener(*this->current_temperature_id_, [this](TuyaDatapoint datapoint) { + this->current_temperature = datapoint.value_int; + this->compute_state_(); + this->publish_state(); + ESP_LOGD(TAG, "MCU reported current temperature is: %d", datapoint.value_int); + }); + } + // if (this->eco_mode_id_.has_value()) { + // this->parent_->register_listener(*this->eco_mode_id_, [this](TuyaDatapoint datapoint) { + // this->eco_mode = datapoint.value_bool; + // this->compute_state_(); + // this->publish_state(); + // ESP_LOGD(TAG, "MCU reported eco mode of: %s", ONOFF(datapoint.value_bool)); + // }); + // } +} + +void TuyaClimate::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) { + this->mode = *call.get_mode(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = this->mode != climate::CLIMATE_MODE_OFF; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting switch: %s", ONOFF(datapoint.value_bool)); + } + if (call.get_target_temperature_low().has_value()) + this->target_temperature_low = *call.get_target_temperature_low(); + if (call.get_target_temperature_high().has_value()) + this->target_temperature_high = *call.get_target_temperature_high(); + if (call.get_target_temperature().has_value()) { + this->target_temperature = *call.get_target_temperature(); + + TuyaDatapoint datapoint{}; + datapoint.id = *this->target_temperature_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = (int) this->target_temperature; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting target temperature: %d", datapoint.value_int); + } + // if (call.get_eco_mode().has_value()) { + // this->eco_mode = *call.get_eco_mode(); + + // TuyaDatapoint datapoint{}; + // datapoint.id = *this->eco_mode_id_; + // datapoint.type = TuyaDatapointType::BOOLEAN; + // datapoint.value_bool = this->eco_mode; + // this->parent_->set_datapoint_value(datapoint); + // ESP_LOGD(TAG, "Setting eco mode: %s", ONOFF(datapoint.value_bool)); + // } + + this->compute_state_(); + this->publish_state(); +} + +climate::ClimateTraits TuyaClimate::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->current_temperature_id_.has_value()); + traits.set_supports_heat_mode(true); + // traits.set_supports_eco_mode(this->eco_mode_id_.has_value()); + traits.set_supports_action(true); + return traits; +} + +void TuyaClimate::dump_config() { + LOG_CLIMATE("", "Tuya Climate", this); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); + if (this->target_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Target Temperature has datapoint ID %u", *this->target_temperature_id_); + if (this->current_temperature_id_.has_value()) + ESP_LOGCONFIG(TAG, " Current Temperature has datapoint ID %u", *this->current_temperature_id_); + // if (this->eco_mode_id_.has_value()) + // ESP_LOGCONFIG(TAG, " Eco Mode has datapoint ID %u", *this->mode_id_); +} + +void TuyaClimate::compute_state_() { + if (isnan(this->current_temperature) || isnan(this->target_temperature)) { + // if any control parameters are nan, go to OFF action (not IDLE!) + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); + return; + } + + const bool too_cold = this->current_temperature < this->target_temperature - 1; + const bool too_hot = this->current_temperature > this->target_temperature + 1; + const bool on_target = this->current_temperature == this->target_temperature; + + climate::ClimateAction target_action; + if (too_cold) { + // too cold -> show as heating if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_HEAT)) { + target_action = climate::CLIMATE_ACTION_HEATING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (too_hot) { + // too hot -> show as cooling if possible, else idle + if (this->traits().supports_mode(climate::CLIMATE_MODE_COOL)) { + target_action = climate::CLIMATE_ACTION_COOLING; + } else { + target_action = climate::CLIMATE_ACTION_IDLE; + } + } else if (on_target) { + target_action = climate::CLIMATE_ACTION_IDLE; + } else { + target_action = this->action; + } + this->switch_to_action_(target_action); +} + +void TuyaClimate::switch_to_action_(climate::ClimateAction action) { + // For now this just sets the current action but could include triggers later + this->action = action; +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/climate/tuya_climate.h b/esphome/components/tuya/climate/tuya_climate.h new file mode 100644 index 0000000000..a073b0996c --- /dev/null +++ b/esphome/components/tuya/climate/tuya_climate.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/climate/climate.h" + +namespace esphome { +namespace tuya { + +class TuyaClimate : public climate::Climate, public Component { + public: + void setup() override; + void dump_config() override; + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_target_temperature_id(uint8_t target_temperature_id) { + this->target_temperature_id_ = target_temperature_id; + } + void set_current_temperature_id(uint8_t current_temperature_id) { + this->current_temperature_id_ = current_temperature_id; + } + // void set_eco_mode_id(uint8_t eco_mode_id) { this->eco_mode_id_ = eco_mode_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Re-compute the state of this climate controller. + void compute_state_(); + + /// Switch the climate device to the given climate mode. + void switch_to_action_(climate::ClimateAction action); + + Tuya *parent_; + optional switch_id_{}; + optional target_temperature_id_{}; + optional current_temperature_id_{}; + // optional eco_mode_id_{}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index 8b4a0fa25f..e8492fd71b 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,13 +1,12 @@ from esphome.components import fan import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID +from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_SPEED_DATAPOINT = "speed_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" TuyaFan = tuya_ns.class_('TuyaFan', cg.Component) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index adaeb52531..d014f8a763 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -2,13 +2,12 @@ from esphome.components import light import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ - CONF_DEFAULT_TRANSITION_LENGTH + CONF_DEFAULT_TRANSITION_LENGTH, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] CONF_DIMMER_DATAPOINT = "dimmer_datapoint" -CONF_SWITCH_DATAPOINT = "switch_datapoint" TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py index bbc8122801..0eadcf0a22 100644 --- a/esphome/components/tuya/switch/__init__.py +++ b/esphome/components/tuya/switch/__init__.py @@ -1,13 +1,11 @@ from esphome.components import switch import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] -CONF_SWITCH_DATAPOINT = "switch_datapoint" - TuyaSwitch = tuya_ns.class_('TuyaSwitch', switch.Switch, cg.Component) CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ diff --git a/esphome/const.py b/esphome/const.py index 8b727b615c..61ac2563ed 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -472,6 +472,7 @@ CONF_SWING_HORIZONTAL_ACTION = 'swing_horizontal_action' CONF_SWING_MODE = 'swing_mode' CONF_SWING_OFF_ACTION = 'swing_off_action' CONF_SWING_VERTICAL_ACTION = 'swing_vertical_action' +CONF_SWITCH_DATAPOINT = "switch_datapoint" CONF_SWITCHES = 'switches' CONF_SYNC = 'sync' CONF_TABLET = 'tablet' diff --git a/tests/test4.yaml b/tests/test4.yaml index 852179f9b5..a7fbacc99d 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -75,7 +75,14 @@ sensor: # type: blue # name: APDS9960 Blue +climate: + - platform: tuya + id: tuya_climate + switch_datapoint: 1 + target_temperature_datapoint: 3 + switch: - platform: tuya id: tuya_switch switch_datapoint: 1 + From a76b8e745b3bc572c6696da56b6fe2b1a5ccda7f Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Jul 2020 10:46:10 +1200 Subject: [PATCH 066/200] Add support for Tuya Sensors (#1088) * Add Tuya sensor * Add tuya sensor to test4 * Forgot id --- esphome/components/tuya/sensor/__init__.py | 28 ++++++++++++ .../components/tuya/sensor/tuya_sensor.cpp | 44 +++++++++++++++++++ esphome/components/tuya/sensor/tuya_sensor.h | 24 ++++++++++ tests/test4.yaml | 4 +- 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 esphome/components/tuya/sensor/__init__.py create mode 100644 esphome/components/tuya/sensor/tuya_sensor.cpp create mode 100644 esphome/components/tuya/sensor/tuya_sensor.h diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py new file mode 100644 index 0000000000..5f2ff2d674 --- /dev/null +++ b/esphome/components/tuya/sensor/__init__.py @@ -0,0 +1,28 @@ +from esphome.components import sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_SENSOR_DATAPOINT = "sensor_datapoint" + +TuyaSensor = tuya_ns.class_('TuyaSensor', sensor.Sensor, cg.Component) + +CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaSensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, +}).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) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp new file mode 100644 index 0000000000..49a1af2efe --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -0,0 +1,44 @@ +#include "esphome/core/log.h" +#include "tuya_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.sensor"; + +void TuyaSensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) { + if (datapoint.type == TuyaDatapointType::BOOLEAN) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported sensor is: %s", ONOFF(datapoint.value_bool)); + } else if (datapoint.type == TuyaDatapointType::INTEGER) { + this->publish_state(datapoint.value_int); + ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_int); + } else if (datapoint.type == TuyaDatapointType::ENUM) { + this->publish_state(datapoint.value_enum); + ESP_LOGD(TAG, "MCU reported sensor is: %d", datapoint.value_enum); + } else if (datapoint.type == TuyaDatapointType::BITMASK) { + this->publish_state(datapoint.value_bitmask); + ESP_LOGD(TAG, "MCU reported sensor is: %x", datapoint.value_bitmask); + } + }); +} + +// void TuyaSensor::write_state(bool state) { +// TuyaDatapoint datapoint{}; +// datapoint.id = this->sensor_id_; +// datapoint.type = TuyaDatapointType::BOOLEAN; +// datapoint.value_bool = state; +// this->parent_->set_datapoint_value(datapoint); +// ESP_LOGD(TAG, "Setting sensor: %s", ONOFF(state)); + +// this->publish_state(state); +// } + +void TuyaSensor::dump_config() { + LOG_SENSOR("", "Tuya Sensor", this); + ESP_LOGCONFIG(TAG, " Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/sensor/tuya_sensor.h b/esphome/components/tuya/sensor/tuya_sensor.h new file mode 100644 index 0000000000..8fd7cd1770 --- /dev/null +++ b/esphome/components/tuya/sensor/tuya_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaSensor : public sensor::Sensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index a7fbacc99d..2709941b0f 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -49,13 +49,15 @@ web_server: username: admin password: admin - tuya: sensor: - platform: homeassistant entity_id: sensor.hello_world id: ha_hello_world + - platform: tuya + id: tuya_sensor + sensor_datapoint: 1 # # platform sensor.apds9960 requires component apds9960 # From ed5f207ebaf704914f0d648b465b26ddbf2a0f84 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Jul 2020 10:50:41 +1200 Subject: [PATCH 067/200] Add support for Tuya Binary Sensors (#1089) * Add Tuya binary sensor * Add tuya binary sensor to test4 * Fix copy/paste * Forgot id --- .../components/tuya/binary_sensor/__init__.py | 28 +++++++++++++++++++ .../tuya/binary_sensor/tuya_binary_sensor.cpp | 22 +++++++++++++++ .../tuya/binary_sensor/tuya_binary_sensor.h | 24 ++++++++++++++++ tests/test4.yaml | 6 +++- 4 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 esphome/components/tuya/binary_sensor/__init__.py create mode 100644 esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp create mode 100644 esphome/components/tuya/binary_sensor/tuya_binary_sensor.h diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py new file mode 100644 index 0000000000..3ba479bf19 --- /dev/null +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -0,0 +1,28 @@ +from esphome.components import binary_sensor +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_ID +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_SENSOR_DATAPOINT = "sensor_datapoint" + +TuyaBinarySensor = tuya_ns.class_('TuyaBinarySensor', binary_sensor.BinarySensor, cg.Component) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(TuyaBinarySensor), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_SENSOR_DATAPOINT): cv.uint8_t, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield binary_sensor.register_binary_sensor(var, config) + + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) + + cg.add(var.set_sensor_id(config[CONF_SENSOR_DATAPOINT])) diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp new file mode 100644 index 0000000000..ad3d18efae --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.cpp @@ -0,0 +1,22 @@ +#include "esphome/core/log.h" +#include "tuya_binary_sensor.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.binary_sensor"; + +void TuyaBinarySensor::setup() { + this->parent_->register_listener(this->sensor_id_, [this](TuyaDatapoint datapoint) { + this->publish_state(datapoint.value_bool); + ESP_LOGD(TAG, "MCU reported binary sensor is: %s", ONOFF(datapoint.value_bool)); + }); +} + +void TuyaBinarySensor::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Binary Sensor:"); + ESP_LOGCONFIG(TAG, " Binary Sensor has datapoint ID %u", this->sensor_id_); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h new file mode 100644 index 0000000000..1eeeb40477 --- /dev/null +++ b/esphome/components/tuya/binary_sensor/tuya_binary_sensor.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace tuya { + +class TuyaBinarySensor : public binary_sensor::BinarySensor, public Component { + public: + void setup() override; + void dump_config() override; + void set_sensor_id(uint8_t sensor_id) { this->sensor_id_ = sensor_id; } + + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + + protected: + Tuya *parent_; + uint8_t sensor_id_{0}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 2709941b0f..61c1e278c4 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -77,6 +77,11 @@ sensor: # type: blue # name: APDS9960 Blue +binary_sensor: + - platform: tuya + id: tuya_binary_sensor + sensor_datapoint: 1 + climate: - platform: tuya id: tuya_climate @@ -87,4 +92,3 @@ switch: - platform: tuya id: tuya_switch switch_datapoint: 1 - From e41ea4207412276eb99c2685b65b7076cfd74bc5 Mon Sep 17 00:00:00 2001 From: Wauter Date: Sun, 12 Jul 2020 00:55:30 +0200 Subject: [PATCH 068/200] feature request 398 add 'hide timestamp' option for version text sensor (#1085) Co-authored-by: Guillermo Ruffino --- esphome/components/version/text_sensor.py | 6 ++++-- esphome/components/version/version_text_sensor.cpp | 10 ++++++++-- esphome/components/version/version_text_sensor.h | 4 ++++ esphome/const.py | 1 + tests/test1.yaml | 3 +++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/esphome/components/version/text_sensor.py b/esphome/components/version/text_sensor.py index 21044bb89f..01cf8ba30b 100644 --- a/esphome/components/version/text_sensor.py +++ b/esphome/components/version/text_sensor.py @@ -1,14 +1,15 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor -from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX +from esphome.const import CONF_ID, CONF_ICON, ICON_NEW_BOX, CONF_HIDE_TIMESTAMP version_ns = cg.esphome_ns.namespace('version') VersionTextSensor = version_ns.class_('VersionTextSensor', text_sensor.TextSensor, cg.Component) CONFIG_SCHEMA = text_sensor.TEXT_SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(VersionTextSensor), - cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon + cv.Optional(CONF_ICON, default=ICON_NEW_BOX): text_sensor.icon, + cv.Optional(CONF_HIDE_TIMESTAMP, default=False): cv.boolean }).extend(cv.COMPONENT_SCHEMA) @@ -16,3 +17,4 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield text_sensor.register_text_sensor(var, config) yield cg.register_component(var, config) + cg.add(var.set_hide_timestamp(config[CONF_HIDE_TIMESTAMP])) diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 6aedfdedcd..fa8e6a9d01 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -8,9 +8,15 @@ namespace version { static const char *TAG = "version.text_sensor"; -void VersionTextSensor::setup() { this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); } +void VersionTextSensor::setup() { + if (this->hide_timestamp_) { + this->publish_state(ESPHOME_VERSION); + } else { + this->publish_state(ESPHOME_VERSION " " + App.get_compilation_time()); + } +} float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } - +void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } std::string VersionTextSensor::unique_id() { return get_mac_address() + "-version"; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index cc798939ef..9355e78442 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -8,10 +8,14 @@ namespace version { class VersionTextSensor : public text_sensor::TextSensor, public Component { public: + void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; float get_setup_priority() const override; std::string unique_id() override; + + protected: + bool hide_timestamp_{false}; }; } // namespace version diff --git a/esphome/const.py b/esphome/const.py index 61ac2563ed..1760db0dab 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -201,6 +201,7 @@ CONF_HEAT_ACTION = 'heat_action' CONF_HEAT_MODE = 'heat_mode' CONF_HEATER = 'heater' CONF_HIDDEN = 'hidden' +CONF_HIDE_TIMESTAMP = 'hide_timestamp' CONF_HIGH = 'high' CONF_HIGH_VOLTAGE_REFERENCE = 'high_voltage_reference' CONF_HOUR = 'hour' diff --git a/tests/test1.yaml b/tests/test1.yaml index 5d8108c4be..63320dfe29 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1749,6 +1749,9 @@ text_sensor: name: "BSSID" mac_address: name: "Mac Address" +- platform: version + name: "ESPHome Version No Timestamp" + hide_timestamp: True sn74hc595: - id: 'sn74hc595_hub' From c8998941a5fe83ce04072e9251662d4efd5a0bc3 Mon Sep 17 00:00:00 2001 From: Paul Deen Date: Sun, 12 Jul 2020 01:03:13 +0200 Subject: [PATCH 069/200] Add 7.5inch v2 waveshare (#1077) * Added 7.5inchV2 * Added 7.5inV2 * Added 7.5inch_V2 * fixed Display function * Inverted bytecode Added ~() to invert the bytecode and make the screen look black text on white background * Hoping to keep Travis happy with blank lines * more travis hacking * travis happy? * wow * gfhj --- .../components/waveshare_epaper/display.py | 2 + .../waveshare_epaper/waveshare_epaper.cpp | 71 +++++++++++++++---- .../waveshare_epaper/waveshare_epaper.h | 24 +++++++ 3 files changed, 82 insertions(+), 15 deletions(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 3e6a972742..577d832785 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -16,6 +16,7 @@ WaveshareEPaper2P9InB = waveshare_epaper_ns.class_('WaveshareEPaper2P9InB', Wave WaveshareEPaper4P2In = waveshare_epaper_ns.class_('WaveshareEPaper4P2In', WaveshareEPaper) WaveshareEPaper5P8In = waveshare_epaper_ns.class_('WaveshareEPaper5P8In', WaveshareEPaper) WaveshareEPaper7P5In = waveshare_epaper_ns.class_('WaveshareEPaper7P5In', WaveshareEPaper) +WaveshareEPaper7P5InV2 = waveshare_epaper_ns.class_('WaveshareEPaper7P5InV2', WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeAModel') WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeBModel') @@ -31,6 +32,7 @@ MODELS = { '4.20in': ('b', WaveshareEPaper4P2In), '5.83in': ('b', WaveshareEPaper5P8In), '7.50in': ('b', WaveshareEPaper7P5In), + '7.50inV2': ('b', WaveshareEPaper7P5InV2), } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 331adffb5e..d57b814bb2 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -751,63 +751,51 @@ void WaveshareEPaper5P8In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } - void WaveshareEPaper7P5In::initialize() { // COMMAND POWER SETTING this->command(0x01); this->data(0x37); this->data(0x00); - // COMMAND PANEL SETTING this->command(0x00); this->data(0xCF); this->data(0x0B); - // COMMAND BOOSTER SOFT START this->command(0x06); this->data(0xC7); this->data(0xCC); this->data(0x28); - // COMMAND POWER ON this->command(0x04); this->wait_until_idle_(); delay(10); - // COMMAND PLL CONTROL this->command(0x30); this->data(0x3C); - // COMMAND TEMPERATURE SENSOR CALIBRATION this->command(0x41); this->data(0x00); - // COMMAND VCOM AND DATA INTERVAL SETTING this->command(0x50); this->data(0x77); - // COMMAND TCON SETTING this->command(0x60); this->data(0x22); - // COMMAND RESOLUTION SETTING this->command(0x61); this->data(0x02); this->data(0x80); this->data(0x01); this->data(0x80); - // COMMAND VCM DC SETTING REGISTER this->command(0x82); this->data(0x1E); - this->command(0xE5); this->data(0x03); } void HOT WaveshareEPaper7P5In::display() { // COMMAND DATA START TRANSMISSION 1 this->command(0x10); - this->start_data_(); for (size_t i = 0; i < this->get_buffer_length_(); i++) { uint8_t temp1 = this->buffer_[i]; @@ -817,7 +805,6 @@ void HOT WaveshareEPaper7P5In::display() { temp2 = 0x03; else temp2 = 0x00; - temp2 <<= 4; temp1 <<= 1; j++; @@ -828,11 +815,9 @@ void HOT WaveshareEPaper7P5In::display() { temp1 <<= 1; this->write_byte(temp2); } - App.feed_wdt(); } this->end_data_(); - // COMMAND DISPLAY REFRESH this->command(0x12); } @@ -846,6 +831,62 @@ void WaveshareEPaper7P5In::dump_config() { LOG_PIN(" Busy Pin: ", this->busy_pin_); LOG_UPDATE_INTERVAL(this); } +void WaveshareEPaper7P5InV2::initialize() { + // COMMAND POWER SETTING + this->command(0x01); + this->data(0x07); + this->data(0x07); + this->data(0x3f); + this->data(0x3f); + this->command(0x04); + delay(100); // NOLINT + this->wait_until_idle_(); + // COMMAND PANEL SETTING + this->command(0x00); + this->data(0x1F); + + // COMMAND RESOLUTION SETTING + this->command(0x61); + this->data(0x03); + this->data(0x20); + this->data(0x01); + this->data(0xE0); + // COMMAND ...? + this->command(0x15); + this->data(0x00); + // COMMAND VCOM AND DATA INTERVAL SETTING + this->command(0x50); + this->data(0x10); + this->data(0x07); + // COMMAND TCON SETTING + this->command(0x60); + this->data(0x22); +} +void HOT WaveshareEPaper7P5InV2::display() { + uint32_t buf_len = this->get_buffer_length_(); + // COMMAND DATA START TRANSMISSION NEW DATA + this->command(0x13); + delay(2); + for (uint32_t i = 0; i < buf_len; i++) { + this->data(~(this->buffer_[i])); + } + + // COMMAND DISPLAY REFRESH + this->command(0x12); + delay(100); // NOLINT + this->wait_until_idle_(); +} + +int WaveshareEPaper7P5InV2::get_width_internal() { return 800; } +int WaveshareEPaper7P5InV2::get_height_internal() { return 480; } +void WaveshareEPaper7P5InV2::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this); + ESP_LOGCONFIG(TAG, " Model: 7.5inV2"); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Busy Pin: ", this->busy_pin_); + LOG_UPDATE_INTERVAL(this); +} } // namespace waveshare_epaper } // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 18748e05ca..01b162fd35 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -105,6 +105,7 @@ enum WaveshareEPaperTypeBModel { WAVESHARE_EPAPER_2_7_IN = 0, WAVESHARE_EPAPER_4_2_IN, WAVESHARE_EPAPER_7_5_IN, + WAVESHARE_EPAPER_7_5_INV2, }; class WaveshareEPaper2P7In : public WaveshareEPaper { @@ -236,5 +237,28 @@ class WaveshareEPaper7P5In : public WaveshareEPaper { int get_height_internal() override; }; +class WaveshareEPaper7P5InV2 : public WaveshareEPaper { + public: + void initialize() override; + + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER OFF + this->command(0x02); + this->wait_until_idle_(); + // COMMAND DEEP SLEEP + this->command(0x07); + this->data(0xA5); // check byte + } + + protected: + int get_width_internal() override; + + int get_height_internal() override; +}; + } // namespace waveshare_epaper } // namespace esphome From 1923e0312b248f761324a55b9a07f9a92bd5c626 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sun, 12 Jul 2020 14:38:29 +1200 Subject: [PATCH 070/200] Add github actions (#1125) --- .github/workflows/pull.yaml | 59 ++++++++ .github/workflows/release-dev.yaml | 124 +++++++++++++++++ .github/workflows/release-version.yaml | 180 +++++++++++++++++++++++++ script/setup | 4 +- 4 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/pull.yaml create mode 100644 .github/workflows/release-dev.yaml create mode 100644 .github/workflows/release-version.yaml diff --git a/.github/workflows/pull.yaml b/.github/workflows/pull.yaml new file mode 100644 index 0000000000..6b66192150 --- /dev/null +++ b/.github/workflows/pull.yaml @@ -0,0 +1,59 @@ +name: PR testing + +on: [pull_request] + +jobs: + lint-custom: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Custom + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/ci-custom.py + lint-python: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Python + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/lint-python + lint-tidy: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Tidy + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - run: script/setup + - run: pio init --ide atom + - run: script/clang-tidy --all-headers --fix + - run: script/ci-suggest-changes + lint-format: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Format + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - run: script/setup + - run: script/clang-format -i + - run: script/ci-suggest-changes + + test: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: esphome tests/${{ matrix.test }}.yaml compile diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml new file mode 100644 index 0000000000..7f71d79610 --- /dev/null +++ b/.github/workflows/release-dev.yaml @@ -0,0 +1,124 @@ +name: Release dev + +on: + push: + branches: + - dev + +jobs: + lint-custom: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Custom + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/ci-custom.py + lint-python: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Python + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/lint-python + lint-tidy: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Tidy + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: pio init --ide atom + - run: script/clang-tidy --all-headers --fix + - run: script/ci-suggest-changes + lint-format: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Format + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/clang-format -i + - run: script/ci-suggest-changes + + test: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: esphome tests/${{ matrix.test }}.yaml compile + + deploy-docker: + runs-on: ubuntu-latest + needs: [lint-custom, lint-python, lint-tidy, lint-format, test] + strategy: + matrix: + arch: [aarch64, amd64, armv7, i386] + build-type: [hassio, docker] + steps: + - uses: actions/checkout@v2 + - run: docker info + - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - env: + BASE_VERSION: 2.1.1 + run: | + if [[ "${{ matrix.build-type }}" == "hassio" ]]; then + BUILD_FROM=esphome/esphome-hassio-base-${{ matrix.arch }}:${BASE_VERSION} + BUILD_TO=${{ github.repository }}-hassio-${{ matrix.arch }} + DOCKERFILE=docker/Dockerfile.hassio + else + BUILD_FROM=esphome/esphome-base-${{ matrix.arch }}:${BASE_VERSION} + BUILD_TO=${{ github.repository }}-${{ matrix.arch }} + DOCKERFILE=docker/Dockerfile + fi + + TAG=${{ github.sha }} + TAG=${TAG:0:7} + + echo "Building tag: ${TAG}" + + docker build \ + --build-arg BUILD_FROM=${BUILD_FROM} \ + --build-arg BUILD_VERSION=${TAG} \ + --tag ${BUILD_TO}:dev \ + --file ${DOCKERFILE} \ + . + + echo "Pushing to ${BUILD_TO}:dev" + docker push ${BUILD_TO}:dev + + deploy-docker-manifest-version: + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - run: mkdir -p ~/.docker + - run: | + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - run: | + REPO=${{ github.repository }} + + docker manifest create ${REPO}:dev \ + ${REPO}-aarch64:dev \ + ${REPO}-amd64:dev \ + ${REPO}-armv7:dev \ + ${REPO}-i386:dev + + echo "Pushing to ${REPO}:dev" + docker manifest push ${REPO}:dev diff --git a/.github/workflows/release-version.yaml b/.github/workflows/release-version.yaml new file mode 100644 index 0000000000..cb72aae761 --- /dev/null +++ b/.github/workflows/release-version.yaml @@ -0,0 +1,180 @@ +name: Release a version + +on: + push: + tags: + - "*" + +jobs: + lint-custom: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Custom + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/ci-custom.py + lint-python: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Python + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/lint-python + lint-tidy: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Tidy + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: pio init --ide atom + - run: script/clang-tidy --all-headers --fix + - run: script/ci-suggest-changes + lint-format: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + name: Lint Format + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: script/clang-format -i + - run: script/ci-suggest-changes + + test: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - run: script/setup + - run: esphome tests/${{ matrix.test }}.yaml compile + + deploy-pypi: + runs-on: ubuntu-latest + container: jesserockz/esphome-lint + needs: [lint-custom, lint-python, lint-tidy, lint-format, test] + steps: + - run: pip install twine wheel + - run: python setup.py sdist bdist_wheel + - run: twine upload dist/* + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + + deploy-docker: + runs-on: ubuntu-latest + needs: [lint-custom, lint-python, lint-tidy, lint-format, test] + strategy: + matrix: + arch: [aarch64, amd64, armv7, i386] + build-type: [hassio, docker] + steps: + - uses: actions/checkout@v2 + - run: docker info + - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - env: + BASE_VERSION: 2.1.1 + run: | + if [[ "${{ matrix.build-type }}" == "hassio" ]]; then + BUILD_FROM=esphome/esphome-hassio-base-${{ matrix.arch }}:${BASE_VERSION} + BUILD_TO=${{ github.repository }}-hassio-${{ matrix.arch }} + DOCKERFILE=docker/Dockerfile.hassio + else + BUILD_FROM=esphome/esphome-base-${{ matrix.arch }}:${BASE_VERSION} + BUILD_TO=${{ github.repository }}-${{ matrix.arch }} + DOCKERFILE=docker/Dockerfile + fi + + TAG=${{ github.ref }} + TAG=${TAG#refs/tags/v} + + echo "Building tag: ${TAG}" + + docker build \ + --build-arg BUILD_FROM=${BUILD_FROM} \ + --build-arg BUILD_VERSION=${TAG} \ + --tag ${BUILD_TO}:${TAG} \ + --file ${DOCKERFILE} \ + . + + echo "Pushing to ${BUILD_TO}:${TAG}" + docker push ${BUILD_TO}:${TAG} + + beta_tag="^v\d+\.\d+\.\d+b\d+$" + if [[ "${TAG}" ~= "${beta_tag}" ]]; then + echo "Pushing to ${BUILD_TO}:beta" + docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:beta + docker push ${BUILD_TO}:beta + else + echo "Pushing to ${BUILD_TO}:latest" + docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:latest + docker push ${BUILD_TO}:latest + fi + + deploy-docker-manifest-version: + runs-on: ubuntu-latest + steps: + - run: mkdir -p ~/.docker + - run: | + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - run: | + REPO=${{ github.repository }} + + TAG=${{ github.ref }} + TAG=${TAG#refs/tags/v} + + docker manifest create ${REPO}:${TAG} \ + ${REPO}-aarch64:${TAG} \ + ${REPO}-amd64:${TAG} \ + ${REPO}-armv7:${TAG} \ + ${REPO}-i386:${TAG} + + echo "Pushing to ${REPO}:${TAG}" + docker push ${REPO}:${TAG} + + deploy-docker-manifest: + runs-on: ubuntu-latest + steps: + - run: mkdir -p ~/.docker + - run: | + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - run: | + REPO=${{ github.repository }} + + TAG=${{ github.ref }} + TAG=${TAG#refs/tags/v} + + beta_tag="^v\d+\.\d+\.\d+b\d+$" + if [[ "${TAG}" ~= "${beta_tag}" ]]; then + TAG=beta + else + TAG=latest + fi + docker manifest create ${REPO}:${TAG} \ + ${REPO}-aarch64:${TAG} \ + ${REPO}-amd64:${TAG} \ + ${REPO}-armv7:${TAG} \ + ${REPO}-i386:${TAG} + + echo "Pushing to ${REPO}:${TAG}" + docker push ${REPO}:${TAG} diff --git a/script/setup b/script/setup index b6cff39f0c..810fb2a2bf 100755 --- a/script/setup +++ b/script/setup @@ -4,5 +4,5 @@ set -e cd "$(dirname "$0")/.." -pip install -r requirements_test.txt -pip install -e . +pip3 install -r requirements_test.txt +pip3 install -e . From c52cb7bbad9137239653207fb51c81454861bdcb Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 12 Jul 2020 12:57:22 -0300 Subject: [PATCH 071/200] Temporary change docker build repository Should be reverted to esphome's --- .github/workflows/release-dev.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 7f71d79610..42e9723f4f 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -77,11 +77,11 @@ jobs: run: | if [[ "${{ matrix.build-type }}" == "hassio" ]]; then BUILD_FROM=esphome/esphome-hassio-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=${{ github.repository }}-hassio-${{ matrix.arch }} + BUILD_TO=glmnet/esphome-hassio-${{ matrix.arch }} DOCKERFILE=docker/Dockerfile.hassio else BUILD_FROM=esphome/esphome-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=${{ github.repository }}-${{ matrix.arch }} + BUILD_TO=glmnet/esphome-${{ matrix.arch }} DOCKERFILE=docker/Dockerfile fi @@ -112,7 +112,7 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: | - REPO=${{ github.repository }} + REPO=glmnet/esphome docker manifest create ${REPO}:dev \ ${REPO}-aarch64:dev \ From c296b4c34840e2e9f4d959707c69dafddbf7d78d Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Jul 2020 05:08:44 +1200 Subject: [PATCH 072/200] Add PR labels based on files changed (#1127) * Add PR labels based on files changed * Remove platform code --- .github/workflows/pr-labels.yaml | 24 ++++++++++++++++++++++++ script/pr-labels | 29 +++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 .github/workflows/pr-labels.yaml create mode 100755 script/pr-labels diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml new file mode 100644 index 0000000000..268536eeeb --- /dev/null +++ b/.github/workflows/pr-labels.yaml @@ -0,0 +1,24 @@ +name: PR labels + +on: [pull_request] + + +jobs: + pr-label: + runs-on: ubuntu-latest + name: PR label + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - id: pr_labels + run: script/pr-labels ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} + - uses: octokit/request-action@v2.0.7 + if: ${{ steps.pr_labels.outputs.labels != '[""]' }} + with: + route: POST /repos/:repository/issues/:number/labels + number: ${{ github.event.number }} + repository: ${{ github.repository }} + labels: ${{ steps.pr_labels.outputs.labels }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/script/pr-labels b/script/pr-labels new file mode 100755 index 0000000000..ef5eb8e57f --- /dev/null +++ b/script/pr-labels @@ -0,0 +1,29 @@ +#!/bin/bash + +components=() +for c_diff in $(git diff ${1}..${2} --name-only esphome/components); do + base=${c_diff#esphome/components/} + component=`dirname ${base}` + + if [[ "${component}" == "." ]]; then + component=${platform} + fi + + if [[ ! "${components[@]}" =~ "${component}" ]]; then + components+=($component) + fi +done + +labels=() + +echo "Components:" +for component in ${components[@]}; do + echo " ${component}" + labels+=("component: $component") +done + +echo "Labels: ${labels[@]}" + +output=$(printf '%s\n' "${labels[@]}" | jq -R . | jq -c -s .) + +echo "::set-output name=labels::${output}" From 1a47e4b5243bc9405589149d7c7d6844589c0270 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 12 Jul 2020 14:36:07 -0300 Subject: [PATCH 073/200] update test readme --- tests/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/README.md b/tests/README.md index af650a2565..c8962dedc1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -9,3 +9,17 @@ Of course this is all just very high-level and things like unit tests would be much better. So if you have time and know how to set up a unit testing framework for python, please do give it a try. + +When adding entries in test_.yaml files we usually need only +one file updated, unless conflicting code is generated for +different configurations, e.g. `wifi` and `ethernet` cannot +be tested on the same device. + +Current test_.yaml file contents. + +| Test name | Platform | Network | +|-|-|-| +| test1.yaml | ESP32 | wifi | +| test2.yaml | ESP32 | ethernet | +| test3.yaml | ESP8266 | wifi | +| test4.yaml | ESP32 | ethernet | From 5d136a18afb9382a1ef3e3d7ee17c3c7e91591d0 Mon Sep 17 00:00:00 2001 From: vxider Date: Mon, 13 Jul 2020 02:12:28 +0800 Subject: [PATCH 074/200] brightness support for nextion (#1109) --- esphome/components/nextion/display.py | 5 ++++- esphome/components/nextion/nextion.cpp | 1 + esphome/components/nextion/nextion.h | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index 30d7519380..394de69585 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display, uart -from esphome.const import CONF_ID, CONF_LAMBDA +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_BRIGHTNESS from . import nextion_ns DEPENDENCIES = ['uart'] @@ -12,6 +12,7 @@ NextionRef = Nextion.operator('ref') CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Nextion), + cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, }).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA) @@ -20,6 +21,8 @@ def to_code(config): yield cg.register_component(var, config) yield uart.register_uart_device(var, config) + if CONF_BRIGHTNESS in config: + cg.add(var.set_brightness(config[CONF_BRIGHTNESS])) if CONF_LAMBDA in config: lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(NextionRef, 'it')], return_type=cg.void) diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e594e147f4..d18d9f33d8 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -9,6 +9,7 @@ static const char *TAG = "nextion"; void Nextion::setup() { this->send_command_no_ack(""); this->send_command_printf("bkcmd=3"); + this->set_backlight_brightness(static_cast(brightness_ * 100)); this->goto_page("0"); } float Nextion::get_setup_priority() const { return setup_priority::PROCESSOR; } diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index bd37e241e9..a55ff747ee 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -365,6 +365,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { // (In most use cases you won't need these) void register_touch_component(NextionTouchComponent *obj) { this->touch_.push_back(obj); } void setup() override; + void set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override; void update() override; void loop() override; @@ -392,6 +393,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { std::vector touch_; optional writer_; bool wait_for_ack_{true}; + float brightness_{1.0}; }; class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { From 6e30bacae3eb891bd4a9dabb26f3aafcddaf11c1 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:19:02 +1200 Subject: [PATCH 075/200] Revert "Add PR labels based on files changed (#1127)" (#1128) This reverts commit c296b4c34840e2e9f4d959707c69dafddbf7d78d. --- .github/workflows/pr-labels.yaml | 24 ------------------------ script/pr-labels | 29 ----------------------------- 2 files changed, 53 deletions(-) delete mode 100644 .github/workflows/pr-labels.yaml delete mode 100755 script/pr-labels diff --git a/.github/workflows/pr-labels.yaml b/.github/workflows/pr-labels.yaml deleted file mode 100644 index 268536eeeb..0000000000 --- a/.github/workflows/pr-labels.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: PR labels - -on: [pull_request] - - -jobs: - pr-label: - runs-on: ubuntu-latest - name: PR label - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - id: pr_labels - run: script/pr-labels ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} - - uses: octokit/request-action@v2.0.7 - if: ${{ steps.pr_labels.outputs.labels != '[""]' }} - with: - route: POST /repos/:repository/issues/:number/labels - number: ${{ github.event.number }} - repository: ${{ github.repository }} - labels: ${{ steps.pr_labels.outputs.labels }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/script/pr-labels b/script/pr-labels deleted file mode 100755 index ef5eb8e57f..0000000000 --- a/script/pr-labels +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -components=() -for c_diff in $(git diff ${1}..${2} --name-only esphome/components); do - base=${c_diff#esphome/components/} - component=`dirname ${base}` - - if [[ "${component}" == "." ]]; then - component=${platform} - fi - - if [[ ! "${components[@]}" =~ "${component}" ]]; then - components+=($component) - fi -done - -labels=() - -echo "Components:" -for component in ${components[@]}; do - echo " ${component}" - labels+=("component: $component") -done - -echo "Labels: ${labels[@]}" - -output=$(printf '%s\n' "${labels[@]}" | jq -R . | jq -c -s .) - -echo "::set-output name=labels::${output}" From 582ac4ac81bc6cd819a93bc200ab25f59b82d069 Mon Sep 17 00:00:00 2001 From: Joppy Date: Tue, 14 Jul 2020 00:23:53 +1200 Subject: [PATCH 076/200] Add support for Toshiba heat pumps (#1121) The IR signals are based on captures from the WH-H01EE remote controller. Both transmit and receive are supported. --- esphome/components/toshiba/__init__.py | 0 esphome/components/toshiba/climate.py | 18 +++ esphome/components/toshiba/toshiba.cpp | 204 +++++++++++++++++++++++++ esphome/components/toshiba/toshiba.h | 21 +++ tests/test1.yaml | 2 + 5 files changed, 245 insertions(+) create mode 100644 esphome/components/toshiba/__init__.py create mode 100644 esphome/components/toshiba/climate.py create mode 100644 esphome/components/toshiba/toshiba.cpp create mode 100644 esphome/components/toshiba/toshiba.h diff --git a/esphome/components/toshiba/__init__.py b/esphome/components/toshiba/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/toshiba/climate.py b/esphome/components/toshiba/climate.py new file mode 100644 index 0000000000..ea7efbf2f5 --- /dev/null +++ b/esphome/components/toshiba/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +toshiba_ns = cg.esphome_ns.namespace('toshiba') +ToshibaClimate = toshiba_ns.class_('ToshibaClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(ToshibaClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp new file mode 100644 index 0000000000..33e2831dd3 --- /dev/null +++ b/esphome/components/toshiba/toshiba.cpp @@ -0,0 +1,204 @@ +#include "toshiba.h" + +namespace esphome { +namespace toshiba { + +const uint16_t TOSHIBA_HEADER_MARK = 4380; +const uint16_t TOSHIBA_HEADER_SPACE = 4370; +const uint16_t TOSHIBA_GAP_SPACE = 5480; +const uint16_t TOSHIBA_BIT_MARK = 540; +const uint16_t TOSHIBA_ZERO_SPACE = 540; +const uint16_t TOSHIBA_ONE_SPACE = 1620; + +const uint8_t TOSHIBA_COMMAND_DEFAULT = 0x01; +const uint8_t TOSHIBA_COMMAND_TIMER = 0x02; +const uint8_t TOSHIBA_COMMAND_POWER = 0x08; +const uint8_t TOSHIBA_COMMAND_MOTION = 0x02; + +const uint8_t TOSHIBA_MODE_AUTO = 0x00; +const uint8_t TOSHIBA_MODE_COOL = 0x01; +const uint8_t TOSHIBA_MODE_DRY = 0x02; +const uint8_t TOSHIBA_MODE_HEAT = 0x03; +const uint8_t TOSHIBA_MODE_FAN_ONLY = 0x04; +const uint8_t TOSHIBA_MODE_OFF = 0x07; + +const uint8_t TOSHIBA_FAN_SPEED_AUTO = 0x00; +const uint8_t TOSHIBA_FAN_SPEED_QUIET = 0x20; +const uint8_t TOSHIBA_FAN_SPEED_1 = 0x40; +const uint8_t TOSHIBA_FAN_SPEED_2 = 0x60; +const uint8_t TOSHIBA_FAN_SPEED_3 = 0x80; +const uint8_t TOSHIBA_FAN_SPEED_4 = 0xa0; +const uint8_t TOSHIBA_FAN_SPEED_5 = 0xc0; + +const uint8_t TOSHIBA_POWER_HIGH = 0x01; +const uint8_t TOSHIBA_POWER_ECO = 0x03; + +const uint8_t TOSHIBA_MOTION_SWING = 0x04; +const uint8_t TOSHIBA_MOTION_FIX = 0x00; + +static const char *TAG = "toshiba.climate"; + +void ToshibaClimate::transmit_state() { + uint8_t message[16] = {0}; + uint8_t message_length = 9; + + /* Header */ + message[0] = 0xf2; + message[1] = 0x0d; + + /* Message length */ + message[2] = message_length - 6; + + /* First checksum */ + message[3] = message[0] ^ message[1] ^ message[2]; + + /* Command */ + message[4] = TOSHIBA_COMMAND_DEFAULT; + + /* Temperature */ + uint8_t temperature = static_cast(this->target_temperature); + if (temperature < 17) { + temperature = 17; + } + if (temperature > 30) { + temperature = 30; + } + message[5] = (temperature - 17) << 4; + + /* Mode and fan */ + uint8_t mode; + switch (this->mode) { + case climate::CLIMATE_MODE_OFF: + mode = TOSHIBA_MODE_OFF; + break; + + case climate::CLIMATE_MODE_HEAT: + mode = TOSHIBA_MODE_HEAT; + break; + + case climate::CLIMATE_MODE_COOL: + mode = TOSHIBA_MODE_COOL; + break; + + case climate::CLIMATE_MODE_AUTO: + default: + mode = TOSHIBA_MODE_AUTO; + } + + message[6] |= mode | TOSHIBA_FAN_SPEED_AUTO; + + /* Zero */ + message[7] = 0x00; + + /* If timers bit in the command is set, two extra bytes are added here */ + + /* If power bit is set in the command, one extra byte is added here */ + + /* The last byte is the xor of all bytes from [4] */ + for (uint8_t i = 4; i < 8; i++) { + message[8] ^= message[i]; + } + + /* Transmit */ + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + data->set_carrier_frequency(38000); + + for (uint8_t copy = 0; copy < 2; copy++) { + data->mark(TOSHIBA_HEADER_MARK); + data->space(TOSHIBA_HEADER_SPACE); + + for (uint8_t byte = 0; byte < message_length; byte++) { + for (uint8_t bit = 0; bit < 8; bit++) { + data->mark(TOSHIBA_BIT_MARK); + if (message[byte] & (1 << (7 - bit))) { + data->space(TOSHIBA_ONE_SPACE); + } else { + data->space(TOSHIBA_ZERO_SPACE); + } + } + } + + data->mark(TOSHIBA_BIT_MARK); + data->space(TOSHIBA_GAP_SPACE); + } + + transmit.perform(); +} + +bool ToshibaClimate::on_receive(remote_base::RemoteReceiveData data) { + uint8_t message[16] = {0}; + uint8_t message_length = 4; + + /* Validate header */ + if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) { + return false; + } + + /* Decode bytes */ + for (uint8_t byte = 0; byte < message_length; byte++) { + for (uint8_t bit = 0; bit < 8; bit++) { + if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ONE_SPACE)) { + message[byte] |= 1 << (7 - bit); + } else if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_ZERO_SPACE)) { + /* Bit is already clear */ + } else { + return false; + } + } + + /* Update length */ + if (byte == 3) { + /* Validate the first checksum before trusting the length field */ + if ((message[0] ^ message[1] ^ message[2]) != message[3]) { + return false; + } + message_length = message[2] + 6; + } + } + + /* Validate the second checksum before trusting any more of the message */ + uint8_t checksum = 0; + for (uint8_t i = 4; i < message_length - 1; i++) { + checksum ^= message[i]; + } + + if (checksum != message[message_length - 1]) { + return false; + } + + /* Check if this is a short swing/fix message */ + if (message[4] & TOSHIBA_COMMAND_MOTION) { + /* Not supported yet */ + return false; + } + + /* Get the mode. */ + switch (message[6] & 0x0f) { + case TOSHIBA_MODE_OFF: + this->mode = climate::CLIMATE_MODE_OFF; + break; + + case TOSHIBA_MODE_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + + case TOSHIBA_MODE_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + + case TOSHIBA_MODE_AUTO: + default: + /* Note: Dry and Fan-only modes are reported as Auto, as they are not supported yet */ + this->mode = climate::CLIMATE_MODE_AUTO; + } + + /* Get the target temperature */ + this->target_temperature = (message[5] >> 4) + 17; + + this->publish_state(); + return true; +} + +} /* namespace toshiba */ +} /* namespace esphome */ diff --git a/esphome/components/toshiba/toshiba.h b/esphome/components/toshiba/toshiba.h new file mode 100644 index 0000000000..3ab0dcdcdb --- /dev/null +++ b/esphome/components/toshiba/toshiba.h @@ -0,0 +1,21 @@ +#pragma once + +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace toshiba { + +const float TOSHIBA_TEMP_MIN = 17.0; +const float TOSHIBA_TEMP_MAX = 30.0; + +class ToshibaClimate : public climate_ir::ClimateIR { + public: + ToshibaClimate() : climate_ir::ClimateIR(TOSHIBA_TEMP_MIN, TOSHIBA_TEMP_MAX, 1.0f) {} + + protected: + void transmit_state() override; + bool on_receive(remote_base::RemoteReceiveData data) override; +}; + +} /* namespace toshiba */ +} /* namespace esphome */ diff --git a/tests/test1.yaml b/tests/test1.yaml index 63320dfe29..4a47ffd7b9 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1281,6 +1281,8 @@ climate: name: Whirlpool Climate - platform: climate_ir_lg name: LG Climate + - platform: toshiba + name: Toshiba Climate switch: - platform: gpio From e6f42fa6f0934a4fae64b110d1c91e0c950d09a2 Mon Sep 17 00:00:00 2001 From: Dmitry Berezovsky Date: Mon, 13 Jul 2020 17:45:06 +0300 Subject: [PATCH 077/200] Packages feature (#1052) * Started to work on packages feature * Added some more validation to packages config * Fixed some linter warnings * Updated tests * Reordered consts to avoid linter error * Reordered consts to avoid linter error * Refactored test yaml files to integrate into existing test pipeline Co-authored-by: Dmitry Berezovsky --- esphome/components/packages/__init__.py | 51 +++++++++++++++++++ esphome/config.py | 13 ++++- esphome/const.py | 1 + requirements.txt | 1 + requirements_test.txt | 1 + setup.py | 1 + tests/test1.yaml | 7 +++ .../test_packages/test_packages_package1.yaml | 2 + .../test_packages_package_wifi.yaml | 4 ++ tests/test_packages/test_uptime_sensor.yaml | 5 ++ 10 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 esphome/components/packages/__init__.py create mode 100644 tests/test_packages/test_packages_package1.yaml create mode 100644 tests/test_packages/test_packages_package_wifi.yaml create mode 100644 tests/test_packages/test_uptime_sensor.yaml diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py new file mode 100644 index 0000000000..729af4a9a0 --- /dev/null +++ b/esphome/components/packages/__init__.py @@ -0,0 +1,51 @@ +from deepmerge import conservative_merger as package_merger + +import esphome.config_validation as cv + +from esphome.const import CONF_PACKAGES + +VALID_PACKAGE_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' + + +def _merge_package(config, package_name, package_config): + config = config.copy() + package_merger.merge(config, package_config) + return config + + +def _is_valid_package_name(value: str) -> bool: + if not value: + return False + if value[0].isdigit(): + return False + try: + cv.valid_name(value) + except cv.Invalid: + return False + return True + + +def do_packages_pass(config: dict): + if CONF_PACKAGES not in config: + return + packages = config[CONF_PACKAGES] + temp_config = config.copy() + with cv.prepend_path(CONF_PACKAGES): + if packages is not None and not isinstance(packages, dict): + raise cv.Invalid("Packages must be a key to value mapping, got {} instead" + "".format(type(packages))) + for package_name, package_config in packages.items(): + with cv.prepend_path(package_name): + if not isinstance(package_config, dict): + raise cv.Invalid("Package definition must be a dictionary containing valid " + "esphome configuration to be merged with the main " + "config, got {} instead" + .format(type(package_config))) + if not _is_valid_package_name(package_name): + raise cv.Invalid("Package name is invalid. Valid name should consist of " + "letters, numbers and underscores. It shouldn't also " + "start with number") + temp_config = _merge_package(temp_config, package_name, package_config) + del temp_config[CONF_PACKAGES] + config.clear() + config.update(temp_config) diff --git a/esphome/config.py b/esphome/config.py index 741b4fb04a..6f3a498444 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -12,8 +12,9 @@ import voluptuous as vol from esphome import core, core_config, yaml_util from esphome.components import substitutions +from esphome.components.packages import do_packages_pass from esphome.components.substitutions import CONF_SUBSTITUTIONS -from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS +from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS, CONF_PACKAGES from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent from esphome.util import safe_print, OrderedDict @@ -390,6 +391,16 @@ def recursive_check_replaceme(value): def validate_config(config, command_line_substitutions): result = Config() + # 0. Load packages + if CONF_PACKAGES in config: + result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) + try: + do_packages_pass(config) + except vol.Invalid as err: + result.update(config) + result.add_error(err) + return result + # 1. Load substitutions if CONF_SUBSTITUTIONS in config: result[CONF_SUBSTITUTIONS] = {**config[CONF_SUBSTITUTIONS], **command_line_substitutions} diff --git a/esphome/const.py b/esphome/const.py index 1760db0dab..45b5e5e209 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -342,6 +342,7 @@ CONF_OUTPUT = 'output' CONF_OUTPUT_ID = 'output_id' CONF_OUTPUTS = 'outputs' CONF_OVERSAMPLING = 'oversampling' +CONF_PACKAGES = 'packages' CONF_PAGE_ID = 'page_id' CONF_PAGES = 'pages' CONF_PANASONIC = 'panasonic' diff --git a/requirements.txt b/requirements.txt index 0c99eaccbe..0a3573f7b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 click==7.1.2 +deepmerge==0.1.0 diff --git a/requirements_test.txt b/requirements_test.txt index b90c4ea969..da4656b085 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,6 +10,7 @@ pyserial==3.4 ifaddr==0.1.6 platformio==4.3.3 esptool==2.8 +deepmerge==0.1.0 pylint==2.5.0 flake8==3.7.9 diff --git a/setup.py b/setup.py index 0a19dccf95..ede80b34e1 100755 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ REQUIRES = [ 'pytz==2020.1', 'pyserial==3.4', 'ifaddr==0.1.6', + 'deepmerge==0.1.0' ] # If you have problems importing platformio and esptool as modules you can set diff --git a/tests/test1.yaml b/tests/test1.yaml index 4a47ffd7b9..b131b1a6ab 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1,3 +1,6 @@ +substitutions: + devicename: test1 + esphome: name: test1 platform: ESP32 @@ -25,6 +28,10 @@ esphome: white: 100% build_path: build/test1 +packages: + wifi: !include test_packages/test_packages_package_wifi.yaml + pkg_test: !include test_packages/test_packages_package1.yaml + wifi: networks: - ssid: 'MySSID' diff --git a/tests/test_packages/test_packages_package1.yaml b/tests/test_packages/test_packages_package1.yaml new file mode 100644 index 0000000000..0495984d42 --- /dev/null +++ b/tests/test_packages/test_packages_package1.yaml @@ -0,0 +1,2 @@ +sensor: + - <<: !include ./test_uptime_sensor.yaml diff --git a/tests/test_packages/test_packages_package_wifi.yaml b/tests/test_packages/test_packages_package_wifi.yaml new file mode 100644 index 0000000000..7d5d41ddab --- /dev/null +++ b/tests/test_packages/test_packages_package_wifi.yaml @@ -0,0 +1,4 @@ +wifi: + networks: + - ssid: 'WiFiFromPackage' + password: 'password1' diff --git a/tests/test_packages/test_uptime_sensor.yaml b/tests/test_packages/test_uptime_sensor.yaml new file mode 100644 index 0000000000..1bf52a6d0b --- /dev/null +++ b/tests/test_packages/test_uptime_sensor.yaml @@ -0,0 +1,5 @@ +# Uptime sensor. +platform: uptime +id: ${devicename}_uptime_pcg +name: Uptime From Package +update_interval: 5min From 351ecf9bd4f3322c110347fabb80cea672fbd1a3 Mon Sep 17 00:00:00 2001 From: Carlos Gustavo Sarmiento Date: Mon, 13 Jul 2020 15:30:17 -0500 Subject: [PATCH 078/200] Allow updating pid control params (#1115) --- esphome/components/pid/climate.py | 26 +++++++++++++++++++ esphome/components/pid/pid_climate.h | 25 ++++++++++++++++++ esphome/components/pid/sensor/__init__.py | 3 +++ .../pid/sensor/pid_climate_sensor.cpp | 12 +++++++++ .../pid/sensor/pid_climate_sensor.h | 3 +++ tests/test3.yaml | 23 ++++++++++++++++ 6 files changed, 92 insertions(+) diff --git a/esphome/components/pid/climate.py b/esphome/components/pid/climate.py index 97a98efc20..446c614f14 100644 --- a/esphome/components/pid/climate.py +++ b/esphome/components/pid/climate.py @@ -8,6 +8,7 @@ pid_ns = cg.esphome_ns.namespace('pid') PIDClimate = pid_ns.class_('PIDClimate', climate.Climate, cg.Component) PIDAutotuneAction = pid_ns.class_('PIDAutotuneAction', automation.Action) PIDResetIntegralTermAction = pid_ns.class_('PIDResetIntegralTermAction', automation.Action) +PIDSetControlParametersAction = pid_ns.class_('PIDSetControlParametersAction', automation.Action) CONF_DEFAULT_TARGET_TEMPERATURE = 'default_target_temperature' @@ -90,3 +91,28 @@ def esp8266_set_frequency_to_code(config, action_id, template_arg, args): cg.add(var.set_positive_output(config[CONF_POSITIVE_OUTPUT])) cg.add(var.set_negative_output(config[CONF_NEGATIVE_OUTPUT])) yield var + + +@automation.register_action( + 'climate.pid.set_control_parameters', + PIDSetControlParametersAction, + automation.maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(PIDClimate), + cv.Required(CONF_KP): cv.templatable(cv.float_), + cv.Optional(CONF_KI, default=0.0): cv.templatable(cv.float_), + cv.Optional(CONF_KD, default=0.0): cv.templatable(cv.float_), + }) +) +def set_control_parameters(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + + kp_template_ = yield cg.templatable(config[CONF_KP], args, float) + cg.add(var.set_kp(kp_template_)) + + ki_template_ = yield cg.templatable(config[CONF_KI], args, float) + cg.add(var.set_ki(ki_template_)) + + kd_template_ = yield cg.templatable(config[CONF_KD], args, float) + cg.add(var.set_kd(kd_template_)) + yield var diff --git a/esphome/components/pid/pid_climate.h b/esphome/components/pid/pid_climate.h index 12436e225c..f11d768867 100644 --- a/esphome/components/pid/pid_climate.h +++ b/esphome/components/pid/pid_climate.h @@ -29,6 +29,9 @@ class PIDClimate : public climate::Climate, public Component { float get_output_value() const { return output_value_; } float get_error_value() const { return controller_.error; } + float get_kp() { return controller_.kp; } + float get_ki() { return controller_.ki; } + float get_kd() { return controller_.kd; } float get_proportional_term() const { return controller_.proportional_term; } float get_integral_term() const { return controller_.integral_term; } float get_derivative_term() const { return controller_.derivative_term; } @@ -101,5 +104,27 @@ template class PIDResetIntegralTermAction : public Action PIDClimate *parent_; }; +template class PIDSetControlParametersAction : public Action { + public: + PIDSetControlParametersAction(PIDClimate *parent) : parent_(parent) {} + + void play(Ts... x) { + auto kp = this->kp_.value(x...); + auto ki = this->ki_.value(x...); + auto kd = this->kd_.value(x...); + + this->parent_->set_kp(kp); + this->parent_->set_ki(ki); + this->parent_->set_kd(kd); + } + + protected: + TEMPLATABLE_VALUE(float, kp) + TEMPLATABLE_VALUE(float, ki) + TEMPLATABLE_VALUE(float, kd) + + PIDClimate *parent_; +}; + } // namespace pid } // namespace esphome diff --git a/esphome/components/pid/sensor/__init__.py b/esphome/components/pid/sensor/__init__.py index cfab23d204..ff8cf15eb5 100644 --- a/esphome/components/pid/sensor/__init__.py +++ b/esphome/components/pid/sensor/__init__.py @@ -15,6 +15,9 @@ PID_CLIMATE_SENSOR_TYPES = { 'DERIVATIVE': PIDClimateSensorType.PID_SENSOR_TYPE_DERIVATIVE, 'HEAT': PIDClimateSensorType.PID_SENSOR_TYPE_HEAT, 'COOL': PIDClimateSensorType.PID_SENSOR_TYPE_COOL, + 'KP': PIDClimateSensorType.PID_SENSOR_TYPE_KP, + 'KI': PIDClimateSensorType.PID_SENSOR_TYPE_KI, + 'KD': PIDClimateSensorType.PID_SENSOR_TYPE_KD, } CONF_CLIMATE_ID = 'climate_id' diff --git a/esphome/components/pid/sensor/pid_climate_sensor.cpp b/esphome/components/pid/sensor/pid_climate_sensor.cpp index 6241a139f6..f60627b6ac 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.cpp +++ b/esphome/components/pid/sensor/pid_climate_sensor.cpp @@ -35,6 +35,18 @@ void PIDClimateSensor::update_from_parent_() { case PID_SENSOR_TYPE_COOL: value = clamp(-this->parent_->get_output_value(), 0.0f, 1.0f); break; + case PID_SENSOR_TYPE_KP: + value = this->parent_->get_kp(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KI: + value = this->parent_->get_ki(); + this->publish_state(value); + return; + case PID_SENSOR_TYPE_KD: + value = this->parent_->get_kd(); + this->publish_state(value); + return; default: value = NAN; break; diff --git a/esphome/components/pid/sensor/pid_climate_sensor.h b/esphome/components/pid/sensor/pid_climate_sensor.h index 85759f1eaf..f3774610f8 100644 --- a/esphome/components/pid/sensor/pid_climate_sensor.h +++ b/esphome/components/pid/sensor/pid_climate_sensor.h @@ -14,6 +14,9 @@ enum PIDClimateSensorType { PID_SENSOR_TYPE_DERIVATIVE, PID_SENSOR_TYPE_HEAT, PID_SENSOR_TYPE_COOL, + PID_SENSOR_TYPE_KP, + PID_SENSOR_TYPE_KI, + PID_SENSOR_TYPE_KD, }; class PIDClimateSensor : public sensor::Sensor, public Component { diff --git a/tests/test3.yaml b/tests/test3.yaml index ef464fd5b7..3854a528f4 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -169,6 +169,13 @@ api: then: - tm1651.turn_off: id: tm1651_battery + - service: pid_set_control_parameters + then: + - climate.pid.set_control_parameters: + id: pid_climate + kp: 1.0 + kd: 1.0 + ki: 1.0 wifi: ssid: 'MySSID' @@ -681,6 +688,17 @@ climate: away_config: default_target_temperature_low: 16°C default_target_temperature_high: 20°C + - platform: pid + id: pid_climate + name: "PID Climate Controller" + sensor: ha_hello_world + default_target_temperature: 21°C + heat_output: my_slow_pwm + control_parameters: + kp: 0.0 + ki: 0.0 + kd: 0.0 + cover: - platform: endstop @@ -761,6 +779,11 @@ output: id: dimmer1 gate_pin: GPIO5 zero_cross_pin: GPIO12 + - platform: slow_pwm + pin: GPIO5 + id: my_slow_pwm + period: 15s + mcp23017: id: mcp23017_hub From c8a4eb426c289d835e87ef0bcaf12c64ee7982f3 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 14 Jul 2020 10:55:15 +1200 Subject: [PATCH 079/200] Github actions repo (#1130) * Revert dev release to esphome repo * Delete gitlab ci * Delete travis file --- .github/workflows/release-dev.yaml | 6 +- .gitlab-ci.yml | 347 ----------------------------- .travis.yml | 44 ---- 3 files changed, 3 insertions(+), 394 deletions(-) delete mode 100644 .gitlab-ci.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 42e9723f4f..7f71d79610 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -77,11 +77,11 @@ jobs: run: | if [[ "${{ matrix.build-type }}" == "hassio" ]]; then BUILD_FROM=esphome/esphome-hassio-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=glmnet/esphome-hassio-${{ matrix.arch }} + BUILD_TO=${{ github.repository }}-hassio-${{ matrix.arch }} DOCKERFILE=docker/Dockerfile.hassio else BUILD_FROM=esphome/esphome-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=glmnet/esphome-${{ matrix.arch }} + BUILD_TO=${{ github.repository }}-${{ matrix.arch }} DOCKERFILE=docker/Dockerfile fi @@ -112,7 +112,7 @@ jobs: DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: | - REPO=glmnet/esphome + REPO=${{ github.repository }} docker manifest create ${REPO}:dev \ ${REPO}-aarch64:dev \ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 1d349c4059..0000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,347 +0,0 @@ ---- -# Based on https://gitlab.com/hassio-addons/addon-node-red/blob/master/.gitlab-ci.yml -variables: - DOCKER_DRIVER: overlay2 - DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '2.1.1' - TZ: UTC - -stages: - - lint - - test - - deploy - -.lint: &lint - image: esphome/esphome-lint:latest - stage: lint - before_script: - - script/setup - tags: - - docker - -.test: &test - image: esphome/esphome-lint:latest - stage: test - before_script: - - script/setup - tags: - - docker - -.docker-base: &docker-base - image: esphome/esphome-base-builder - before_script: - - docker info - - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" - script: - - docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - - TAG="${CI_COMMIT_TAG#v}" - - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - - echo "Tag ${TAG}" - - - | - if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} - BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} - DOCKERFILE=docker/Dockerfile.hassio - else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} - if [[ "${BUILD_ARCH}" == "amd64" ]]; then - BUILD_TO=esphome/esphome - else - BUILD_TO=esphome/esphome-${BUILD_ARCH} - fi - DOCKERFILE=docker/Dockerfile - fi - - - | - docker build \ - --build-arg "BUILD_FROM=${BUILD_FROM}" \ - --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${BUILD_TO}:${TAG}" \ - --file "${DOCKERFILE}" \ - . - - | - if [[ "${RELEASE}" = "YES" ]]; then - echo "Pushing to ${BUILD_TO}:${TAG}" - docker push "${BUILD_TO}:${TAG}" - fi - - | - if [[ "${LATEST}" = "YES" ]]; then - echo "Pushing to :latest" - docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:latest - docker push ${BUILD_TO}:latest - fi - - | - if [[ "${BETA}" = "YES" ]]; then - echo "Pushing to :beta" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:beta - docker push ${BUILD_TO}:beta - fi - - | - if [[ "${DEV}" = "YES" ]]; then - echo "Pushing to :dev" - docker tag \ - ${BUILD_TO}:${TAG} \ - ${BUILD_TO}:dev - docker push ${BUILD_TO}:dev - fi - services: - - docker:dind - tags: - - docker - stage: deploy - -lint-custom: - <<: *lint - script: - - script/ci-custom.py - -lint-python: - <<: *lint - script: - - script/lint-python - -lint-tidy: - <<: *lint - script: - - pio init --ide atom - - script/clang-tidy --all-headers --fix - - script/ci-suggest-changes - -lint-format: - <<: *lint - script: - - script/clang-format -i - - script/ci-suggest-changes - -test1: - <<: *test - script: - - esphome tests/test1.yaml compile - -test2: - <<: *test - script: - - esphome tests/test2.yaml compile - -test3: - <<: *test - script: - - esphome tests/test3.yaml compile - -test4: - <<: *test - script: - - esphome tests/test4.yaml compile - -.deploy-pypi: &deploy-pypi - <<: *lint - stage: deploy - script: - - pip install twine wheel - - python setup.py sdist bdist_wheel - - twine upload dist/* - -deploy-release:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+$/ - except: - - /^(?!master).+@/ - -deploy-beta:pypi: - <<: *deploy-pypi - only: - - /^v\d+\.\d+\.\d+b\d+$/ - except: - - /^(?!rc).+@/ - -.latest: &latest - <<: *docker-base - only: - - /^v([0-9\.]+)$/ - except: - - branches - -.beta: &beta - <<: *docker-base - only: - - /^v([0-9\.]+b\d+)$/ - except: - - branches - -.dev: &dev - <<: *docker-base - only: - - dev - -aarch64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - RELEASE: "YES" -aarch64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - RELEASE: "YES" -aarch64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "NO" -aarch64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: aarch64 - DEV: "YES" - IS_HASSIO: "YES" -aarch64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -aarch64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: aarch64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -amd64-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - RELEASE: "YES" -amd64-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - RELEASE: "YES" -amd64-dev-docker: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "NO" -amd64-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: amd64 - DEV: "YES" - IS_HASSIO: "YES" -amd64-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -amd64-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: amd64 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -armv7-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - RELEASE: "YES" -armv7-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - RELEASE: "YES" -armv7-dev-docker: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "NO" -armv7-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: armv7 - DEV: "YES" - IS_HASSIO: "YES" -armv7-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -armv7-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: armv7 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" -i386-beta-docker: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - RELEASE: "YES" -i386-beta-hassio: - <<: *beta - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - RELEASE: "YES" -i386-dev-docker: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "NO" -i386-dev-hassio: - <<: *dev - variables: - BUILD_ARCH: i386 - DEV: "YES" - IS_HASSIO: "YES" -i386-latest-docker: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "NO" - LATEST: "YES" - RELEASE: "YES" -i386-latest-hassio: - <<: *latest - variables: - BETA: "YES" - BUILD_ARCH: i386 - IS_HASSIO: "YES" - LATEST: "YES" - RELEASE: "YES" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 3e6c7adccb..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,44 +0,0 @@ -sudo: false -language: python -python: '3.6' -install: script/setup -cache: - directories: - - "~/.platformio" - -matrix: - fast_finish: true - include: - - python: "3.7" - env: TARGET=Lint3.7 - script: - - script/ci-custom.py - - flake8 esphome - - pylint esphome - - python: "3.6" - env: TARGET=Test3.6 - script: - - esphome tests/test1.yaml compile - - esphome tests/test2.yaml compile - - esphome tests/test3.yaml compile - - esphome tests/test4.yaml compile - - env: TARGET=Cpp-Lint - dist: trusty - sudo: required - addons: - apt: - sources: - - ubuntu-toolchain-r-test - - llvm-toolchain-trusty-7 - packages: - - clang-tidy-7 - - clang-format-7 - before_script: - - pio init --ide atom - - clang-tidy-7 -version - - clang-format-7 -version - - clang-apply-replacements-7 -version - script: - - script/clang-tidy --all-headers -j 2 --fix - - script/clang-format -i -j 2 - - script/ci-suggest-changes From 2012c769f6b6d6969c756a6d457f9ec3e4340ab7 Mon Sep 17 00:00:00 2001 From: Peter Kuehne Date: Tue, 14 Jul 2020 12:11:58 +0100 Subject: [PATCH 080/200] Feature/fix unit tests (#1129) --- esphome/config_validation.py | 2 +- esphome/core.py | 11 +-- tests/unit_tests/test_config_validation.py | 15 ++-- tests/unit_tests/test_helpers.py | 80 +++++++++++----------- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 1319786841..0649e15f5f 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -104,7 +104,7 @@ def alphanumeric(value): raise Invalid("string value is None") value = str(value) if not value.isalnum(): - raise Invalid("string value is not alphanumeric") + raise Invalid(f"{value} is not alphanumeric") return value diff --git a/esphome/core.py b/esphome/core.py index 7bbaf9c54c..dfb37555e1 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -24,15 +24,18 @@ class EsphomeError(Exception): class HexInt(int): def __str__(self): - if 0 <= self <= 255: - return f"0x{self:02X}" - return f"0x{self:X}" + value = self + sign = "-" if value < 0 else "" + value = abs(value) + if 0 <= value <= 255: + return f"{sign}0x{value:02X}" + return f"{sign}0x{value:X}" class IPAddress: def __init__(self, *args): if len(args) != 4: - raise ValueError("IPAddress must consist up 4 items") + raise ValueError("IPAddress must consist of 4 items") self.args = args def __str__(self): diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index a19330062e..ced75af105 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -9,22 +9,24 @@ from esphome.config_validation import Invalid from esphome.core import Lambda, HexInt -def test_check_not_tamplatable__invalid(): +def test_check_not_templatable__invalid(): with pytest.raises(Invalid, match="This option is not templatable!"): config_validation.check_not_templatable(Lambda("")) -@given(one_of( - booleans(), - integers(), - text(alphabet=string.ascii_letters + string.digits)), -) +@pytest.mark.parametrize("value", ("foo", 1, "D12", False)) def test_alphanumeric__valid(value): actual = config_validation.alphanumeric(value) assert actual == str(value) +@pytest.mark.parametrize("value", ("£23", "Foo!")) +def test_alphanumeric__invalid(value): + with pytest.raises(Invalid): + actual = config_validation.alphanumeric(value) + + @given(value=text(alphabet=string.ascii_lowercase + string.digits + "_")) def test_valid_name__valid(value): actual = config_validation.valid_name(value) @@ -110,4 +112,3 @@ def hex_int__valid(value): assert isinstance(actual, HexInt) assert actual == value - diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index e48286ae51..3c0a96149d 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -7,12 +7,12 @@ from esphome import helpers @pytest.mark.parametrize("preferred_string, current_strings, expected", ( - ("foo", [], "foo"), - # TODO: Should this actually start at 1? - ("foo", ["foo"], "foo_2"), - ("foo", ("foo",), "foo_2"), - ("foo", ("foo", "foo_2"), "foo_3"), - ("foo", ("foo", "foo_2", "foo_2"), "foo_3"), + ("foo", [], "foo"), + # TODO: Should this actually start at 1? + ("foo", ["foo"], "foo_2"), + ("foo", ("foo",), "foo_2"), + ("foo", ("foo", "foo_2"), "foo_3"), + ("foo", ("foo", "foo_2", "foo_2"), "foo_3"), )) def test_ensure_unique_string(preferred_string, current_strings, expected): actual = helpers.ensure_unique_string(preferred_string, current_strings) @@ -21,9 +21,9 @@ def test_ensure_unique_string(preferred_string, current_strings, expected): @pytest.mark.parametrize("text, expected", ( - ("foo", "foo"), - ("foo\nbar", "foo\nbar"), - ("foo\nbar\neek", "foo\n bar\neek"), + ("foo", "foo"), + ("foo\nbar", "foo\nbar"), + ("foo\nbar\neek", "foo\n bar\neek"), )) def test_indent_all_but_first_and_last(text, expected): actual = helpers.indent_all_but_first_and_last(text) @@ -32,9 +32,9 @@ def test_indent_all_but_first_and_last(text, expected): @pytest.mark.parametrize("text, expected", ( - ("foo", [" foo"]), - ("foo\nbar", [" foo", " bar"]), - ("foo\nbar\neek", [" foo", " bar", " eek"]), + ("foo", [" foo"]), + ("foo\nbar", [" foo", " bar"]), + ("foo\nbar\neek", [" foo", " bar", " eek"]), )) def test_indent_list(text, expected): actual = helpers.indent_list(text) @@ -43,9 +43,9 @@ def test_indent_list(text, expected): @pytest.mark.parametrize("text, expected", ( - ("foo", " foo"), - ("foo\nbar", " foo\n bar"), - ("foo\nbar\neek", " foo\n bar\n eek"), + ("foo", " foo"), + ("foo\nbar", " foo\n bar"), + ("foo\nbar\neek", " foo\n bar\n eek"), )) def test_indent(text, expected): actual = helpers.indent(text) @@ -54,11 +54,11 @@ def test_indent(text, expected): @pytest.mark.parametrize("string, expected", ( - ("foo", '"foo"'), - ("foo\nbar", '"foo\\012bar"'), - ("foo\\bar", '"foo\\134bar"'), - ('foo "bar"', '"foo \\042bar\\042"'), - ('foo 🐍', '"foo \\360\\237\\220\\215"'), + ("foo", '"foo"'), + ("foo\nbar", '"foo\\012bar"'), + ("foo\\bar", '"foo\\134bar"'), + ('foo "bar"', '"foo \\042bar\\042"'), + ('foo 🐍', '"foo \\360\\237\\220\\215"'), )) def test_cpp_string_escape(string, expected): actual = helpers.cpp_string_escape(string) @@ -83,11 +83,11 @@ def test_is_ip_address__valid(value): @pytest.mark.parametrize("var, value, default, expected", ( - ("FOO", None, False, False), - ("FOO", None, True, True), - ("FOO", "", False, False), - ("FOO", "Yes", False, True), - ("FOO", "123", False, True), + ("FOO", None, False, False), + ("FOO", None, True, True), + ("FOO", "", False, False), + ("FOO", "Yes", False, True), + ("FOO", "123", False, True), )) def test_get_bool_env(monkeypatch, var, value, default, expected): if value is None: @@ -101,8 +101,8 @@ def test_get_bool_env(monkeypatch, var, value, default, expected): @pytest.mark.parametrize("value, expected", ( - (None, False), - ("Yes", True) + (None, False), + ("Yes", True) )) def test_is_hassio(monkeypatch, value, expected): if value is None: @@ -121,7 +121,7 @@ def test_walk_files(fixture_path): actual = list(helpers.walk_files(path)) # Ensure paths start with the root - assert all(p.startswith(path.as_posix()) for p in actual) + assert all(p.startswith(str(path)) for p in actual) class Test_write_file_if_changed: @@ -186,18 +186,18 @@ class Test_copy_file_if_changed: @pytest.mark.parametrize("file1, file2, expected", ( - # Same file - ("file-a.txt", "file-a.txt", True), - # Different files, different size - ("file-a.txt", "file-b_1.txt", False), - # Different files, same size - ("file-a.txt", "file-c.txt", False), - # Same files - ("file-b_1.txt", "file-b_2.txt", True), - # Not a file - ("file-a.txt", "", False), - # File doesn't exist - ("file-a.txt", "file-d.txt", False), + # Same file + ("file-a.txt", "file-a.txt", True), + # Different files, different size + ("file-a.txt", "file-b_1.txt", False), + # Different files, same size + ("file-a.txt", "file-c.txt", False), + # Same files + ("file-b_1.txt", "file-b_2.txt", True), + # Not a file + ("file-a.txt", "", False), + # File doesn't exist + ("file-a.txt", "file-d.txt", False), )) def test_file_compare(fixture_path, file1, file2, expected): path1 = fixture_path / "helpers" / file1 From cf703f6ac4b1f6b49e16aaacaa51debd97808969 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 14:34:44 +0200 Subject: [PATCH 081/200] GH Actions Update (#1134) --- .github/workflows/ci.yml | 196 ++++++++++++++ .github/workflows/matchers/ci-custom.json | 16 ++ .github/workflows/matchers/clang-tidy.json | 17 ++ .github/workflows/matchers/gcc.json | 18 ++ .github/workflows/matchers/lint-python.json | 28 ++ .github/workflows/matchers/python.json | 18 ++ .github/workflows/pull.yaml | 59 ----- .github/workflows/release-dev.yaml | 124 --------- .github/workflows/release-dev.yml | 226 ++++++++++++++++ .github/workflows/release-version.yaml | 180 ------------- .github/workflows/release.yml | 276 ++++++++++++++++++++ docker/Dockerfile.lint | 6 +- script/ci-custom.py | 35 ++- script/clang-tidy | 29 +- script/helpers.py | 6 +- 15 files changed, 846 insertions(+), 388 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/matchers/ci-custom.json create mode 100644 .github/workflows/matchers/clang-tidy.json create mode 100644 .github/workflows/matchers/gcc.json create mode 100644 .github/workflows/matchers/lint-python.json create mode 100644 .github/workflows/matchers/python.json delete mode 100644 .github/workflows/pull.yaml delete mode 100644 .github/workflows/release-dev.yaml create mode 100644 .github/workflows/release-dev.yml delete mode 100644 .github/workflows/release-version.yaml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000..9d03705e84 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,196 @@ +# THESE JOBS ARE COPIED IN release.yml and release-dev.yml +# PLEASE ALSO UPDATE THOSE FILES WHEN CHANGING LINES HERE +name: CI + +on: + push: + # On dev branch release-dev already performs CI checks + # On other branches the `pull_request` trigger will be used + branches: [beta, master] + + pull_request: + # Only trigger on certain events (not when comments are added) + types: [opened, reopened, synchronize] + # Only run when PR is against dev branch (all PRs should be against dev branch) + # Helps prevent accidentally merging PRs against master branch + branches: [dev] + +jobs: + # A fast overview job that checks only changed files + overview: + runs-on: ubuntu-latest + container: esphome/esphome-lint:dev + steps: + # Also fetch history and dev branch so that we can check which files changed + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Fetch dev branch + run: git fetch origin dev + + # Cache the .pio directory with (primarily) library dependencies + - name: Cache .pio lib_deps + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + - name: Set up python environment + run: script/setup + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run a quick lint over all changed files + run: script/quicklint + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-format: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:dev + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:dev + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Lint Custom + run: script/ci-custom.py + - name: Lint Python + run: script/lint-python + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + # Cache the intermediary build files + - name: Cache Test Build + uses: actions/cache@v1 + with: + path: tests/build/${{ matrix.test }} + key: test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}-${{ hashFiles('esphome/**') }} + restore-keys: | + test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}- + test-pio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile diff --git a/.github/workflows/matchers/ci-custom.json b/.github/workflows/matchers/ci-custom.json new file mode 100644 index 0000000000..4e1eafff5e --- /dev/null +++ b/.github/workflows/matchers/ci-custom.json @@ -0,0 +1,16 @@ +{ + "problemMatcher": [ + { + "owner": "ci-custom", + "pattern": [ + { + "regexp": "^ERROR (.*):(\\d+):(\\d+) - (.*)$", + "file": 1, + "line": 2, + "column": 3, + "message": 4 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/clang-tidy.json b/.github/workflows/matchers/clang-tidy.json new file mode 100644 index 0000000000..03e77341a5 --- /dev/null +++ b/.github/workflows/matchers/clang-tidy.json @@ -0,0 +1,17 @@ +{ + "problemMatcher": [ + { + "owner": "clang-tidy", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/gcc.json b/.github/workflows/matchers/gcc.json new file mode 100644 index 0000000000..899239f816 --- /dev/null +++ b/.github/workflows/matchers/gcc.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "gcc", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+):(\\d+):\\s+(?:fatal\\s+)?(warning|error):\\s+(.*)$", + "file": 1, + "line": 2, + "column": 3, + "severity": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/lint-python.json b/.github/workflows/matchers/lint-python.json new file mode 100644 index 0000000000..decbe36c4a --- /dev/null +++ b/.github/workflows/matchers/lint-python.json @@ -0,0 +1,28 @@ +{ + "problemMatcher": [ + { + "owner": "flake8", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+) - ([EFCDNW]\\d{3}.*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + }, + { + "owner": "pylint", + "severity": "error", + "pattern": [ + { + "regexp": "^(.*):(\\d+) - (\\[[EFCRW]\\d{4}\\(.*\\),.*\\].*)$", + "file": 1, + "line": 2, + "message": 3 + } + ] + } + ] +} diff --git a/.github/workflows/matchers/python.json b/.github/workflows/matchers/python.json new file mode 100644 index 0000000000..9c3095c0c9 --- /dev/null +++ b/.github/workflows/matchers/python.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "python", + "pattern": [ + { + "regexp": "^\\s*File\\s\\\"(.*)\\\",\\sline\\s(\\d+),\\sin\\s(.*)$", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*raise\\s(.*)\\(\\'(.*)\\'\\)$", + "message": 2 + } + ] + } + ] +} diff --git a/.github/workflows/pull.yaml b/.github/workflows/pull.yaml deleted file mode 100644 index 6b66192150..0000000000 --- a/.github/workflows/pull.yaml +++ /dev/null @@ -1,59 +0,0 @@ -name: PR testing - -on: [pull_request] - -jobs: - lint-custom: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Custom - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/ci-custom.py - lint-python: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Python - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/lint-python - lint-tidy: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Tidy - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - run: script/setup - - run: pio init --ide atom - - run: script/clang-tidy --all-headers --fix - - run: script/ci-suggest-changes - lint-format: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Format - steps: - - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - run: script/setup - - run: script/clang-format -i - - run: script/ci-suggest-changes - - test: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - strategy: - matrix: - test: - - test1 - - test2 - - test3 - - test4 - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: esphome tests/${{ matrix.test }}.yaml compile diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml deleted file mode 100644 index 7f71d79610..0000000000 --- a/.github/workflows/release-dev.yaml +++ /dev/null @@ -1,124 +0,0 @@ -name: Release dev - -on: - push: - branches: - - dev - -jobs: - lint-custom: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Custom - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/ci-custom.py - lint-python: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Python - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/lint-python - lint-tidy: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Tidy - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: pio init --ide atom - - run: script/clang-tidy --all-headers --fix - - run: script/ci-suggest-changes - lint-format: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Format - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/clang-format -i - - run: script/ci-suggest-changes - - test: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - strategy: - matrix: - test: - - test1 - - test2 - - test3 - - test4 - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: esphome tests/${{ matrix.test }}.yaml compile - - deploy-docker: - runs-on: ubuntu-latest - needs: [lint-custom, lint-python, lint-tidy, lint-format, test] - strategy: - matrix: - arch: [aarch64, amd64, armv7, i386] - build-type: [hassio, docker] - steps: - - uses: actions/checkout@v2 - - run: docker info - - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - - env: - BASE_VERSION: 2.1.1 - run: | - if [[ "${{ matrix.build-type }}" == "hassio" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=${{ github.repository }}-hassio-${{ matrix.arch }} - DOCKERFILE=docker/Dockerfile.hassio - else - BUILD_FROM=esphome/esphome-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=${{ github.repository }}-${{ matrix.arch }} - DOCKERFILE=docker/Dockerfile - fi - - TAG=${{ github.sha }} - TAG=${TAG:0:7} - - echo "Building tag: ${TAG}" - - docker build \ - --build-arg BUILD_FROM=${BUILD_FROM} \ - --build-arg BUILD_VERSION=${TAG} \ - --tag ${BUILD_TO}:dev \ - --file ${DOCKERFILE} \ - . - - echo "Pushing to ${BUILD_TO}:dev" - docker push ${BUILD_TO}:dev - - deploy-docker-manifest-version: - runs-on: ubuntu-latest - needs: [deploy-docker] - steps: - - run: mkdir -p ~/.docker - - run: | - echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - run: | - REPO=${{ github.repository }} - - docker manifest create ${REPO}:dev \ - ${REPO}-aarch64:dev \ - ${REPO}-amd64:dev \ - ${REPO}-armv7:dev \ - ${REPO}-i386:dev - - echo "Pushing to ${REPO}:dev" - docker manifest push ${REPO}:dev diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml new file mode 100644 index 0000000000..564d12be88 --- /dev/null +++ b/.github/workflows/release-dev.yml @@ -0,0 +1,226 @@ +name: Publish dev releases to docker hub + +on: + push: + branches: + - dev + +jobs: + # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml + + lint-clang-format: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:dev + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:dev + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Lint Custom + run: script/ci-custom.py + - name: Lint Python + run: script/lint-python + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + # Cache the intermediary build files + - name: Cache Test Build + uses: actions/cache@v1 + with: + path: tests/build/${{ matrix.test }} + key: test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}-${{ hashFiles('esphome/**') }} + restore-keys: | + test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}- + test-pio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + deploy-docker: + name: Build and publish docker containers + runs-on: ubuntu-latest + needs: [lint-clang-format, lint-clang-tidy, lint-python, test] + strategy: + matrix: + arch: [amd64, i386, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set up env variables + run: | + tag="dev" + base_version="2.1.2" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + if [[ "${{ matrix.arch }}" == "amd64" ]]; then + build_to="esphome/esphome" + fi + fi + + # Set env variables so these values don't need to be calculated again + echo "::set-env name=TAG::${tag}" + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=IMAGE::${build_to}:${tag}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=${TAG}" \ + --tag "${IMAGE}" \ + --file "${DOCKERFILE}" \ + . + - name: Log in to docker hub + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" + - run: docker push "${IMAGE}" + + deploy-docker-manifest: + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - name: "Create the manifest" + run: | + REPO=esphome/esphome + TAG="dev" + + docker manifest create ${REPO}:${TAG} \ + ${REPO}-aarch64:${TAG} \ + ${REPO}-amd64:${TAG} \ + ${REPO}-armv7:${TAG} \ + ${REPO}-i386:${TAG} + echo "::set-env name=TAG::${TAG}" + echo "::set-env name=REPO::${REPO}" + - run: docker push ${REPO}:${TAG} diff --git a/.github/workflows/release-version.yaml b/.github/workflows/release-version.yaml deleted file mode 100644 index cb72aae761..0000000000 --- a/.github/workflows/release-version.yaml +++ /dev/null @@ -1,180 +0,0 @@ -name: Release a version - -on: - push: - tags: - - "*" - -jobs: - lint-custom: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Custom - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/ci-custom.py - lint-python: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Python - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/lint-python - lint-tidy: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Tidy - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: pio init --ide atom - - run: script/clang-tidy --all-headers --fix - - run: script/ci-suggest-changes - lint-format: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - name: Lint Format - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: script/clang-format -i - - run: script/ci-suggest-changes - - test: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - strategy: - matrix: - test: - - test1 - - test2 - - test3 - - test4 - steps: - - uses: actions/checkout@v2 - - run: script/setup - - run: esphome tests/${{ matrix.test }}.yaml compile - - deploy-pypi: - runs-on: ubuntu-latest - container: jesserockz/esphome-lint - needs: [lint-custom, lint-python, lint-tidy, lint-format, test] - steps: - - run: pip install twine wheel - - run: python setup.py sdist bdist_wheel - - run: twine upload dist/* - env: - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - - deploy-docker: - runs-on: ubuntu-latest - needs: [lint-custom, lint-python, lint-tidy, lint-format, test] - strategy: - matrix: - arch: [aarch64, amd64, armv7, i386] - build-type: [hassio, docker] - steps: - - uses: actions/checkout@v2 - - run: docker info - - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - - env: - BASE_VERSION: 2.1.1 - run: | - if [[ "${{ matrix.build-type }}" == "hassio" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=${{ github.repository }}-hassio-${{ matrix.arch }} - DOCKERFILE=docker/Dockerfile.hassio - else - BUILD_FROM=esphome/esphome-base-${{ matrix.arch }}:${BASE_VERSION} - BUILD_TO=${{ github.repository }}-${{ matrix.arch }} - DOCKERFILE=docker/Dockerfile - fi - - TAG=${{ github.ref }} - TAG=${TAG#refs/tags/v} - - echo "Building tag: ${TAG}" - - docker build \ - --build-arg BUILD_FROM=${BUILD_FROM} \ - --build-arg BUILD_VERSION=${TAG} \ - --tag ${BUILD_TO}:${TAG} \ - --file ${DOCKERFILE} \ - . - - echo "Pushing to ${BUILD_TO}:${TAG}" - docker push ${BUILD_TO}:${TAG} - - beta_tag="^v\d+\.\d+\.\d+b\d+$" - if [[ "${TAG}" ~= "${beta_tag}" ]]; then - echo "Pushing to ${BUILD_TO}:beta" - docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:beta - docker push ${BUILD_TO}:beta - else - echo "Pushing to ${BUILD_TO}:latest" - docker tag ${BUILD_TO}:${TAG} ${BUILD_TO}:latest - docker push ${BUILD_TO}:latest - fi - - deploy-docker-manifest-version: - runs-on: ubuntu-latest - steps: - - run: mkdir -p ~/.docker - - run: | - echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - run: | - REPO=${{ github.repository }} - - TAG=${{ github.ref }} - TAG=${TAG#refs/tags/v} - - docker manifest create ${REPO}:${TAG} \ - ${REPO}-aarch64:${TAG} \ - ${REPO}-amd64:${TAG} \ - ${REPO}-armv7:${TAG} \ - ${REPO}-i386:${TAG} - - echo "Pushing to ${REPO}:${TAG}" - docker push ${REPO}:${TAG} - - deploy-docker-manifest: - runs-on: ubuntu-latest - steps: - - run: mkdir -p ~/.docker - - run: | - echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json - - run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - - run: | - REPO=${{ github.repository }} - - TAG=${{ github.ref }} - TAG=${TAG#refs/tags/v} - - beta_tag="^v\d+\.\d+\.\d+b\d+$" - if [[ "${TAG}" ~= "${beta_tag}" ]]; then - TAG=beta - else - TAG=latest - fi - docker manifest create ${REPO}:${TAG} \ - ${REPO}-aarch64:${TAG} \ - ${REPO}-amd64:${TAG} \ - ${REPO}-armv7:${TAG} \ - ${REPO}-i386:${TAG} - - echo "Pushing to ${REPO}:${TAG}" - docker push ${REPO}:${TAG} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..6425cd8990 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,276 @@ +name: Publish Release + +on: + release: + types: [published] + +jobs: + # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml + + lint-clang-format: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:dev + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + - name: Run clang-format + run: script/clang-format -i + - name: Suggest changes + run: script/ci-suggest-changes + + lint-clang-tidy: + runs-on: ubuntu-latest + # cpp lint job runs with esphome-lint docker image so that clang-format-* + # doesn't have to be installed + container: esphome/esphome-lint:dev + # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files + strategy: + matrix: + split: [1, 2, 3, 4] + steps: + - uses: actions/checkout@v2 + # Cache platformio intermediary files (like libraries etc) + # Note: platformio platform versions should be cached via the esphome-lint image + - name: Cache Platformio + uses: actions/cache@v1 + with: + path: .pio + key: lint-cpp-pio-${{ hashFiles('platformio.ini') }} + restore-keys: | + lint-cpp-pio- + # Set up the pio project so that the cpp checks know how files are compiled + # (build flags, libraries etc) + - name: Set up platformio environment + run: pio init --ide atom + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" + echo "::add-matcher::.github/workflows/matchers/gcc.json" + - name: Run clang-tidy + run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} + - name: Suggest changes + run: script/ci-suggest-changes + + lint-python: + # Don't use the esphome-lint docker image because it may contain outdated requirements. + # This way, all dependencies are cached via the cache action. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up python environment + run: script/setup + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/ci-custom.json" + echo "::add-matcher::.github/workflows/matchers/lint-python.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Lint Custom + run: script/ci-custom.py + - name: Lint Python + run: script/lint-python + + test: + runs-on: ubuntu-latest + strategy: + matrix: + test: + - test1 + - test2 + - test3 + - test4 + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + # Use per test platformio cache because tests have different platform versions + - name: Cache ~/.platformio + uses: actions/cache@v1 + with: + path: ~/.platformio + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + restore-keys: | + test-home-platformio-${{ matrix.test }}- + # Cache the intermediary build files + - name: Cache Test Build + uses: actions/cache@v1 + with: + path: tests/build/${{ matrix.test }} + key: test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}-${{ hashFiles('esphome/**') }} + restore-keys: | + test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}- + test-pio-${{ matrix.test }}- + - name: Set up environment + run: script/setup + + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/gcc.json" + echo "::add-matcher::.github/workflows/matchers/python.json" + - run: esphome tests/${{ matrix.test }}.yaml compile + + deploy-pypi: + name: Build and publish to PyPi + needs: [lint-clang-format, lint-clang-tidy, lint-python, test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v1 + with: + python-version: '3.x' + - name: Set up python environment + run: | + script/setup + pip install setuptools wheel twine + - name: Build + run: python setup.py sdist bdist_wheel + - name: Upload + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + run: twine upload dist/* + + deploy-docker: + name: Build and publish docker containers + runs-on: ubuntu-latest + needs: [lint-clang-format, lint-clang-tidy, lint-python, test] + strategy: + matrix: + arch: [amd64, i386, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set up env variables + run: | + tag="${GITHUB_REF#v}" + base_version="2.1.2" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + if [[ "${{ matrix.arch }}" == "amd64" ]]; then + build_to="esphome/esphome" + fi + fi + + # Set env variables so these values don't need to be calculated again + echo "::set-env name=TAG::${tag}" + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=IMAGE::${build_to}:${tag}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=${TAG}" \ + --tag "${IMAGE}" \ + --file "${DOCKERFILE}" \ + . + - name: Log in to docker hub + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" + - run: docker push "${IMAGE}" + - if: ${{ github.event.release.prerelease) }} + name: Publish docker beta tag + run: | + docker tag "${IMAGE}" ${BUILD_TO}:beta + docker push "${BUILD_TO}:beta" + - if: ${{ !github.event.release.prerelease) }} + name: Publish docker latest tag + run: | + docker tag "${IMAGE}" ${BUILD_TO}:latest + docker push "${BUILD_TO}:latest" + + deploy-docker-manifest: + runs-on: ubuntu-latest + needs: [deploy-docker] + steps: + - name: Enable experimental manifest support + run: | + mkdir -p ~/.docker + echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - name: "Create the manifest" + run: | + REPO=esphome/esphome + TAG="${GITHUB_REF#v}" + + docker manifest create ${REPO}:${TAG} \ + ${REPO}-aarch64:${TAG} \ + ${REPO}-amd64:${TAG} \ + ${REPO}-armv7:${TAG} \ + ${REPO}-i386:${TAG} + echo "::set-env name=TAG::${TAG}" + echo "::set-env name=REPO::${REPO}" + - run: docker push ${REPO}:${TAG} + + - name: Publish docker beta tag + if: ${{ github.event.release.prerelease) }} + run: | + docker manifest create ${REPO}:beta \ + ${REPO}-aarch64:beta \ + ${REPO}-amd64:beta \ + ${REPO}-armv7:beta \ + ${REPO}-i386:beta + docker push ${REPO}:beta + - name: Publish docker latest tag + if: ${{ !github.event.release.prerelease) }} + run: | + docker manifest create ${REPO}:latest \ + ${REPO}-aarch64:latest \ + ${REPO}-amd64:latest \ + ${REPO}-armv7:latest \ + ${REPO}-i386:latest + docker push ${REPO}:latest diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index cdd715ef5a..32b316075f 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.1.1 +FROM esphome/esphome-base-amd64:2.1.2 RUN \ apt-get update \ @@ -7,6 +7,8 @@ RUN \ clang-tidy-7 \ patch \ software-properties-common \ + # Update to latest git version because of github actions + # https://github.com/actions/checkout/issues/126 && apt-add-repository ppa:git-core/ppa \ && apt-get install -y --no-install-recommends \ git \ @@ -16,7 +18,7 @@ RUN \ /var/lib/apt/lists/* COPY requirements_test.txt /requirements_test.txt -RUN pip3 install --no-cache-dir wheel && pip3 install --no-cache-dir -r /requirements_test.txt +RUN pip3 install --no-cache-dir -r /requirements_test.txt VOLUME ["/esphome"] WORKDIR /esphome diff --git a/script/ci-custom.py b/script/ci-custom.py index b2b838cb5b..67010d77f9 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -101,10 +101,12 @@ def lint_re_check(regex, **kwargs): if 'NOLINT' in match.group(0): continue lineno = content.count("\n", 0, match.start()) + 1 + substr = content[:match.start()] + col = len(substr) - substr.rfind('\n') err = func(fname, match) if err is None: continue - errors.append(f"{err} See line {lineno}.") + errors.append((lineno, col+1, err)) return errors return decor(new_func) return decorator @@ -121,8 +123,7 @@ def lint_content_find_check(find, **kwargs): errors = [] for line, col in find_all(content, find_): err = func(fname) - errors.append("{err} See line {line}:{col}." - "".format(err=err, line=line+1, col=col+1)) + errors.append((line+1, col+1, err)) return errors return decor(new_func) return decorator @@ -215,9 +216,10 @@ def lint_const_ordered(fname, content): continue target = next(i for i, l in ordered if l == ml) target_text = next(l for i, l in matching if target == i) - errors.append("Constant {} is not ordered, please make sure all constants are ordered. " - "See line {} (should go to line {}, {})" - "".format(highlight(ml), mi, target, target_text)) + errors.append((ml, None, + "Constant {} is not ordered, please make sure all constants are ordered. " + "See line {} (should go to line {}, {})" + "".format(highlight(ml), mi, target, target_text))) return errors @@ -354,13 +356,22 @@ errors = collections.defaultdict(list) def add_errors(fname, errs): if not isinstance(errs, list): errs = [errs] - errs = [x for x in errs if x is not None] for err in errs: + if err is None: + continue + try: + lineno, col, msg = err + except ValueError: + lineno = 1 + col = 1 + msg = err if not isinstance(err, str): raise ValueError("Error is not instance of string!") - if not errs: - return - errors[fname].extend(errs) + if not isinstance(lineno, int): + raise ValueError("Line number is not an int!") + if not isinstance(col, int): + raise ValueError("Column number is not an int!") + errors[fname].append((lineno, col, msg)) for fname in files: @@ -380,8 +391,8 @@ run_checks(LINT_POST_CHECKS, 'POST') for f, errs in sorted(errors.items()): print(f"\033[0;32m************* File \033[1;32m{f}\033[0m") - for err in errs: - print(err) + for lineno, col, msg in errs: + print(f"ERROR {f}:{lineno}:{col} - {msg}") print() sys.exit(len(errors)) diff --git a/script/clang-tidy b/script/clang-tidy index 1005d15580..490e63e0d2 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -2,20 +2,19 @@ from __future__ import print_function +import argparse import multiprocessing import os import re - -import pexpect import shutil import subprocess import sys import tempfile - -import argparse -import click import threading +import click +import pexpect + sys.path.append(os.path.dirname(__file__)) from helpers import basepath, shlex_quote, get_output, build_compile_commands, \ build_all_include, temp_header_file, git_ls_files, filter_changed @@ -49,7 +48,7 @@ def run_tidy(args, tmpdir, queue, lock, failed_files): # Use pexpect for a pseudy-TTY with colored output output, rc = pexpect.run(invocation_s, withexitstatus=True, encoding='utf-8', - timeout=15*60) + timeout=15 * 60) with lock: if rc != 0: print() @@ -65,6 +64,11 @@ def progress_bar_show(value): return '' +def split_list(a, n): + k, m = divmod(len(a), n) + return [a[i * k + min(i, m):(i + 1) * k + min(i + 1, m)] for i in range(n)] + + def main(): parser = argparse.ArgumentParser() parser.add_argument('-j', '--jobs', type=int, @@ -77,6 +81,10 @@ def main(): help='Run clang-tidy in quiet mode') parser.add_argument('-c', '--changed', action='store_true', help='Only run on changed files') + parser.add_argument('--split-num', type=int, help='Split the files into X jobs.', + default=None) + parser.add_argument('--split-at', type=int, help='Which split is this? Starts at 1', + default=None) parser.add_argument('--all-headers', action='store_true', help='Create a dummy file that checks all headers') args = parser.parse_args() @@ -114,7 +122,10 @@ def main(): files.sort() - if args.all_headers: + if args.split_num: + files = split_list(files, args.split_num)[args.split_at - 1] + + if args.all_headers and args.split_at in (None, 1): files.insert(0, temp_header_file) tmpdir = None @@ -157,8 +168,8 @@ def main(): print('Error applying fixes.\n', file=sys.stderr) raise - sys.exit(return_code) + return return_code if __name__ == '__main__': - main() + sys.exit(main()) diff --git a/script/helpers.py b/script/helpers.py index c9bf5224b1..e0aaee8711 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -101,8 +101,10 @@ def splitlines_no_ends(string): def changed_files(): - for remote in ('upstream', 'origin'): - command = ['git', 'merge-base', f'{remote}/dev', 'HEAD'] + check_remotes = ['upstream', 'origin'] + check_remotes.extend(splitlines_no_ends(get_output('git', 'remote'))) + for remote in check_remotes: + command = ['git', 'merge-base', f'refs/remotes/{remote}/dev', 'HEAD'] try: merge_base = splitlines_no_ends(get_output(*command))[0] break From 3d78248aaf5a170c6c412a964183354badcda8e4 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 14:47:03 +0200 Subject: [PATCH 082/200] Fix wrong variable name for docker login --- .github/workflows/release-dev.yml | 4 ++-- .github/workflows/release.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 564d12be88..895fe6319d 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -193,9 +193,9 @@ jobs: . - name: Log in to docker hub env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - run: docker push "${IMAGE}" deploy-docker-manifest: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6425cd8990..6809189f5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -214,9 +214,9 @@ jobs: . - name: Log in to docker hub env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} - run: docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - run: docker push "${IMAGE}" - if: ${{ github.event.release.prerelease) }} name: Publish docker beta tag From 01b1b688b1a61adfe357dabbc49f9945fe359f02 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 15:10:26 +0200 Subject: [PATCH 083/200] Simplify and enable caching --- .github/workflows/release-dev.yml | 16 +++++++--------- .github/workflows/release.yml | 15 +++++++-------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 895fe6319d..c1e1dc6fa0 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -165,15 +165,12 @@ jobs: if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" + build_to="${{ github.repository }}-hassio-${{ matrix.arch }}" dockerfile="docker/Dockerfile.hassio" else build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" + build_to="${{ github.repository }}-${{ matrix.arch }}" dockerfile="docker/Dockerfile" - if [[ "${{ matrix.arch }}" == "amd64" ]]; then - build_to="esphome/esphome" - fi fi # Set env variables so these values don't need to be calculated again @@ -190,6 +187,9 @@ jobs: --build-arg "BUILD_VERSION=${TAG}" \ --tag "${IMAGE}" \ --file "${DOCKERFILE}" \ + --cache-from "${{ github.repository }}:latest" + --cache-from "${{ github.repository }}:beta" + --cache-from "${{ github.repository }}:dev" . - name: Log in to docker hub env: @@ -213,7 +213,7 @@ jobs: run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - name: "Create the manifest" run: | - REPO=esphome/esphome + REPO=${{ github.repository }} TAG="dev" docker manifest create ${REPO}:${TAG} \ @@ -221,6 +221,4 @@ jobs: ${REPO}-amd64:${TAG} \ ${REPO}-armv7:${TAG} \ ${REPO}-i386:${TAG} - echo "::set-env name=TAG::${TAG}" - echo "::set-env name=REPO::${REPO}" - - run: docker push ${REPO}:${TAG} + docker push ${REPO}:${TAG} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6809189f5c..24be029442 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -186,15 +186,12 @@ jobs: if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-hassio-${{ matrix.arch }}" + build_to="${{ github.repository }}-hassio-${{ matrix.arch }}" dockerfile="docker/Dockerfile.hassio" else build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="esphome/esphome-${{ matrix.arch }}" + build_to="${{ github.repository }}-${{ matrix.arch }}" dockerfile="docker/Dockerfile" - if [[ "${{ matrix.arch }}" == "amd64" ]]; then - build_to="esphome/esphome" - fi fi # Set env variables so these values don't need to be calculated again @@ -211,6 +208,9 @@ jobs: --build-arg "BUILD_VERSION=${TAG}" \ --tag "${IMAGE}" \ --file "${DOCKERFILE}" \ + --cache-from "${{ github.repository }}:latest" + --cache-from "${{ github.repository }}:beta" + --cache-from "${{ github.repository }}:dev" . - name: Log in to docker hub env: @@ -244,7 +244,7 @@ jobs: run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - name: "Create the manifest" run: | - REPO=esphome/esphome + REPO=${{ github.repository }} TAG="${GITHUB_REF#v}" docker manifest create ${REPO}:${TAG} \ @@ -252,9 +252,8 @@ jobs: ${REPO}-amd64:${TAG} \ ${REPO}-armv7:${TAG} \ ${REPO}-i386:${TAG} - echo "::set-env name=TAG::${TAG}" echo "::set-env name=REPO::${REPO}" - - run: docker push ${REPO}:${TAG} + docker push ${REPO}:${TAG} - name: Publish docker beta tag if: ${{ github.event.release.prerelease) }} From a53481e2dae26239966804b1129c4872f79fdad7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 15:42:29 +0200 Subject: [PATCH 084/200] Fix GH Actions release-dev.yml --- .github/workflows/release-dev.yml | 9 +++++---- .github/workflows/release.yml | 12 ++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index c1e1dc6fa0..9a12754fbf 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -4,6 +4,7 @@ on: push: branches: - dev + - gh-actions-test jobs: # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml @@ -187,9 +188,9 @@ jobs: --build-arg "BUILD_VERSION=${TAG}" \ --tag "${IMAGE}" \ --file "${DOCKERFILE}" \ - --cache-from "${{ github.repository }}:latest" - --cache-from "${{ github.repository }}:beta" - --cache-from "${{ github.repository }}:dev" + --cache-from "${{ github.repository }}:latest" \ + --cache-from "${{ github.repository }}:beta" \ + --cache-from "${{ github.repository }}:dev" \ . - name: Log in to docker hub env: @@ -221,4 +222,4 @@ jobs: ${REPO}-amd64:${TAG} \ ${REPO}-armv7:${TAG} \ ${REPO}-i386:${TAG} - docker push ${REPO}:${TAG} + docker manifest push ${REPO}:${TAG} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 24be029442..a8af0ba8a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -208,9 +208,9 @@ jobs: --build-arg "BUILD_VERSION=${TAG}" \ --tag "${IMAGE}" \ --file "${DOCKERFILE}" \ - --cache-from "${{ github.repository }}:latest" - --cache-from "${{ github.repository }}:beta" - --cache-from "${{ github.repository }}:dev" + --cache-from "${{ github.repository }}:latest" \ + --cache-from "${{ github.repository }}:beta" \ + --cache-from "${{ github.repository }}:dev" \ . - name: Log in to docker hub env: @@ -253,7 +253,7 @@ jobs: ${REPO}-armv7:${TAG} \ ${REPO}-i386:${TAG} echo "::set-env name=REPO::${REPO}" - docker push ${REPO}:${TAG} + docker manifest push ${REPO}:${TAG} - name: Publish docker beta tag if: ${{ github.event.release.prerelease) }} @@ -263,7 +263,7 @@ jobs: ${REPO}-amd64:beta \ ${REPO}-armv7:beta \ ${REPO}-i386:beta - docker push ${REPO}:beta + docker manifest push ${REPO}:beta - name: Publish docker latest tag if: ${{ !github.event.release.prerelease) }} run: | @@ -272,4 +272,4 @@ jobs: ${REPO}-amd64:latest \ ${REPO}-armv7:latest \ ${REPO}-i386:latest - docker push ${REPO}:latest + docker manifest push ${REPO}:latest From 1ccc6e342c0e2fd9b11b941dc3b078beee8dd2b9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 15:42:56 +0200 Subject: [PATCH 085/200] Remove debug line --- .github/workflows/release-dev.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 9a12754fbf..217d943704 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -4,7 +4,6 @@ on: push: branches: - dev - - gh-actions-test jobs: # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml From 5776e70d7c464f271db51cd41823f7833b8dcfc9 Mon Sep 17 00:00:00 2001 From: Peter Kuehne Date: Tue, 14 Jul 2020 16:59:03 +0100 Subject: [PATCH 086/200] Bug/fix internal flag in binary sensor (#1136) --- esphome/__main__.py | 7 ++++++ esphome/components/binary_sensor/__init__.py | 2 +- .../binary_sensor/test_binary_sensor.py | 24 +++++++++++++++++++ .../binary_sensor/test_binary_sensor.yaml | 18 ++++++++++++++ 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 tests/component_tests/binary_sensor/test_binary_sensor.py create mode 100644 tests/component_tests/binary_sensor/test_binary_sensor.yaml diff --git a/esphome/__main__.py b/esphome/__main__.py index 27262355d6..a961df8ada 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -131,6 +131,11 @@ def wrap_to_code(name, comp): def write_cpp(config): + generate_cpp_contents(config) + return write_cpp_file() + + +def generate_cpp_contents(config): _LOGGER.info("Generating C++ source...") for name, component, conf in iter_components(CORE.config): @@ -140,6 +145,8 @@ def write_cpp(config): CORE.flush_tasks() + +def write_cpp_file(): writer.write_platformio_project() code_s = indent(CORE.cpp_main_section) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 7c78c3a369..2b5dd302c4 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -224,7 +224,7 @@ BINARY_SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ def setup_binary_sensor_core_(var, config): cg.add(var.set_name(config[CONF_NAME])) if CONF_INTERNAL in config: - cg.add(var.set_internal(CONF_INTERNAL)) + cg.add(var.set_internal(config[CONF_INTERNAL])) if CONF_DEVICE_CLASS in config: cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) if CONF_INVERTED in config: diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py new file mode 100644 index 0000000000..8f3cd3aeda --- /dev/null +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -0,0 +1,24 @@ +""" Tests for the binary sensor component """ + +from esphome.core import CORE +from esphome.config import read_config +from esphome.__main__ import generate_cpp_contents + + +def test_binary_sensor_config_value_internal_set(): + """ + Test that the "internal" config value is correctly set + """ + # Given + CORE.config_path = "tests/component_tests/binary_sensor/test_binary_sensor.yaml" + CORE.config = read_config({}) + + # When + generate_cpp_contents(CORE.config) + # print(CORE.cpp_main_section) + + # Then + assert "bs_1->set_internal(true);" in CORE.cpp_main_section + assert "bs_2->set_internal(false);" in CORE.cpp_main_section + + CORE.reset() diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.yaml b/tests/component_tests/binary_sensor/test_binary_sensor.yaml new file mode 100644 index 0000000000..912ae115eb --- /dev/null +++ b/tests/component_tests/binary_sensor/test_binary_sensor.yaml @@ -0,0 +1,18 @@ +esphome: + name: test + platform: ESP8266 + board: d1_mini_lite + +binary_sensor: + - platform: gpio + id: bs_1 + name: "test bs1" + internal: true + pin: + number: D0 + - platform: gpio + id: bs_2 + name: "test bs2" + internal: false + pin: + number: D1 From 412351fd1e8dd001f8d84f29315b1d02165962bb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 18:45:42 +0200 Subject: [PATCH 087/200] Use inclusive terminology (#1137) --- esphome/components/dallas/esp_one_wire.cpp | 2 +- esphome/components/mqtt/__init__.py | 4 ++-- esphome/components/mqtt/mqtt_component.cpp | 4 ++-- esphome/core/component.cpp | 2 +- esphome/core/helpers.cpp | 8 ++++---- esphome/core/helpers.h | 8 ++++---- script/ci-custom.py | 23 ++++++++++++++++++++-- 7 files changed, 35 insertions(+), 16 deletions(-) diff --git a/esphome/components/dallas/esp_one_wire.cpp b/esphome/components/dallas/esp_one_wire.cpp index d90b10894d..92b7317f7c 100644 --- a/esphome/components/dallas/esp_one_wire.cpp +++ b/esphome/components/dallas/esp_one_wire.cpp @@ -30,7 +30,7 @@ bool HOT ICACHE_RAM_ATTR ESPOneWire::reset() { // Switch into RX mode, letting the pin float this->pin_->pin_mode(INPUT_PULLUP); - // after 15µs-60µs wait time, slave pulls low for 60µs-240µs + // after 15µs-60µs wait time, responder pulls low for 60µs-240µs // let's have 70µs just in case delayMicroseconds(70); diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index 2f0ed0f8e2..db99334d0b 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -280,8 +280,8 @@ def mqtt_publish_json_action_to_code(config, action_id, template_arg, args): def get_default_topic_for(data, component_type, name, suffix): - whitelist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' - sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in whitelist) + allowlist = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_' + sanitized_name = ''.join(x for x in name.lower().replace(' ', '_') if x in allowlist) return '{}/{}/{}/{}'.format(data.topic_prefix, component_type, sanitized_name, suffix) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 4201d41c44..9b4255060f 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -12,7 +12,7 @@ static const char *TAG = "mqtt.component"; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { - std::string sanitized_name = sanitize_string_whitelist(App.get_name(), HOSTNAME_CHARACTER_WHITELIST); + std::string sanitized_name = sanitize_string_allowlist(App.get_name(), HOSTNAME_CHARACTER_ALLOWLIST); return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + this->get_default_object_id_() + "/config"; } @@ -117,7 +117,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return sanitize_string_whitelist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_WHITELIST); + return sanitize_string_allowlist(to_lowercase_underscore(this->friendly_name()), HOSTNAME_CHARACTER_ALLOWLIST); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index f4151a14fc..d2e9607e28 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -177,7 +177,7 @@ const std::string &Nameable::get_object_id() { return this->object_id_; } bool Nameable::is_internal() const { return this->internal_; } void Nameable::set_internal(bool internal) { this->internal_ = internal; } void Nameable::calc_object_id_() { - this->object_id_ = sanitize_string_whitelist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_WHITELIST); + this->object_id_ = sanitize_string_allowlist(to_lowercase_underscore(this->name_), HOSTNAME_CHARACTER_ALLOWLIST); // FNV-1 hash this->object_id_hash_ = fnv1_hash(this->object_id_); } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 2222a1a664..78a62a5e86 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -85,16 +85,16 @@ std::string to_lowercase_underscore(std::string s) { return s; } -std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist) { +std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist) { std::string out(s); out.erase(std::remove_if(out.begin(), out.end(), - [&whitelist](const char &c) { return whitelist.find(c) == std::string::npos; }), + [&allowlist](const char &c) { return allowlist.find(c) == std::string::npos; }), out.end()); return out; } std::string sanitize_hostname(const std::string &hostname) { - std::string s = sanitize_string_whitelist(hostname, HOSTNAME_CHARACTER_WHITELIST); + std::string s = sanitize_string_allowlist(hostname, HOSTNAME_CHARACTER_ALLOWLIST); return truncate_string(s, 63); } @@ -154,7 +154,7 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { return PARSE_NONE; } -const char *HOSTNAME_CHARACTER_WHITELIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; +const char *HOSTNAME_CHARACTER_ALLOWLIST = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; uint8_t crc8(uint8_t *data, uint8_t len) { uint8_t crc = 0; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index ab3d883e05..0c660bdc8e 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -24,7 +24,7 @@ namespace esphome { /// The characters that are allowed in a hostname. -extern const char *HOSTNAME_CHARACTER_WHITELIST; +extern const char *HOSTNAME_CHARACTER_ALLOWLIST; /// Gets the MAC address as a string, this can be used as way to identify this ESP. std::string get_mac_address(); @@ -43,7 +43,7 @@ std::string to_string(double val); std::string to_string(long double val); optional parse_float(const std::string &str); -/// Sanitize the hostname by removing characters that are not in the whitelist and truncating it to 63 chars. +/// Sanitize the hostname by removing characters that are not in the allowlist and truncating it to 63 chars. std::string sanitize_hostname(const std::string &hostname); /// Truncate a string to a specific length @@ -121,8 +121,8 @@ std::string uint64_to_string(uint64_t num); /// Convert a uint32_t to a hex string std::string uint32_to_string(uint32_t num); -/// Sanitizes the input string with the whitelist. -std::string sanitize_string_whitelist(const std::string &s, const std::string &whitelist); +/// Sanitizes the input string with the allowlist. +std::string sanitize_string_allowlist(const std::string &s, const std::string &allowlist); uint8_t reverse_bits_8(uint8_t x); uint16_t reverse_bits_16(uint16_t x); diff --git a/script/ci-custom.py b/script/ci-custom.py index 67010d77f9..cde026b5d4 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -91,7 +91,8 @@ def lint_post_check(func): def lint_re_check(regex, **kwargs): - prog = re.compile(regex, re.MULTILINE) + flags = kwargs.pop('flags', re.MULTILINE) + prog = re.compile(regex, flags) decor = lint_content_check(**kwargs) def decorator(func): @@ -327,6 +328,24 @@ def lint_pragma_once(fname, content): return None +@lint_re_check(r'(whitelist|blacklist|slave)', exclude=['script/ci-custom.py'], + flags=re.IGNORECASE | re.MULTILINE) +def lint_inclusive_language(fname, match): + # From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb + return ("Avoid the use of whitelist/blacklist/slave.\n" + "Recommended replacements for 'master / slave' are:\n" + " '{primary,main} / {secondary,replica,subordinate}\n" + " '{initiator,requester} / {target,responder}'\n" + " '{controller,host} / {device,worker,proxy}'\n" + " 'leader / follower'\n" + " 'director / performer'\n" + "\n" + "Recommended replacements for 'blacklist/whitelist' are:\n" + " 'denylist / allowlist'\n" + " 'blocklist / passlist'") + + + @lint_content_find_check('ESP_LOG', include=['*.h', '*.tcc'], exclude=[ 'esphome/components/binary_sensor/binary_sensor.h', 'esphome/components/cover/cover.h', @@ -365,7 +384,7 @@ def add_errors(fname, errs): lineno = 1 col = 1 msg = err - if not isinstance(err, str): + if not isinstance(msg, str): raise ValueError("Error is not instance of string!") if not isinstance(lineno, int): raise ValueError("Line number is not an int!") From 465cd3d1f94ee6bd77a28c1a34be69178de7cdcc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 18:47:17 +0200 Subject: [PATCH 088/200] Add exposure notifications (#1135) --- .../esp32_ble_tracker/esp32_ble_tracker.h | 3 ++ .../exposure_notifications/__init__.py | 28 +++++++++++ .../exposure_notifications.cpp | 49 +++++++++++++++++++ .../exposure_notifications.h | 29 +++++++++++ 4 files changed, 109 insertions(+) create mode 100644 esphome/components/exposure_notifications/__init__.py create mode 100644 esphome/components/exposure_notifications/exposure_notifications.cpp create mode 100644 esphome/components/exposure_notifications/exposure_notifications.h diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index 8d011abfe3..eef7930b78 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -28,6 +28,7 @@ class ESPBTUUID { bool contains(uint8_t data1, uint8_t data2) const; bool operator==(const ESPBTUUID &uuid) const; + bool operator!=(const ESPBTUUID &uuid) const { return !(*this == uuid); } esp_bt_uuid_t get_uuid(); @@ -74,6 +75,8 @@ class ESPBTDevice { uint64_t address_uint64() const; + const uint8_t *address() const { return address_; } + esp_ble_addr_type_t get_address_type() const { return this->address_type_; } int get_rssi() const { return rssi_; } const std::string &get_name() const { return this->name_; } diff --git a/esphome/components/exposure_notifications/__init__.py b/esphome/components/exposure_notifications/__init__.py new file mode 100644 index 0000000000..de9c3a58df --- /dev/null +++ b/esphome/components/exposure_notifications/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome import automation +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import CONF_TRIGGER_ID + +DEPENDENCIES = ['esp32_ble_tracker'] + +exposure_notifications_ns = cg.esphome_ns.namespace('exposure_notifications') +ExposureNotification = exposure_notifications_ns.struct('ExposureNotification') +ExposureNotificationTrigger = exposure_notifications_ns.class_( + 'ExposureNotificationTrigger', esp32_ble_tracker.ESPBTDeviceListener, + automation.Trigger.template(ExposureNotification)) + +CONF_ON_EXPOSURE_NOTIFICATION = 'on_exposure_notification' + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ON_EXPOSURE_NOTIFICATION): automation.validate_automation(cv.Schema({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ExposureNotificationTrigger), + }).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)), +}) + + +def to_code(config): + for conf in config.get(CONF_ON_EXPOSURE_NOTIFICATION, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + yield automation.build_automation(trigger, [(ExposureNotification, 'x')], conf) + yield esp32_ble_tracker.register_ble_device(trigger, conf) diff --git a/esphome/components/exposure_notifications/exposure_notifications.cpp b/esphome/components/exposure_notifications/exposure_notifications.cpp new file mode 100644 index 0000000000..1d8cf83a0e --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.cpp @@ -0,0 +1,49 @@ +#include "exposure_notifications.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace exposure_notifications { + +using namespace esp32_ble_tracker; + +static const char *TAG = "exposure_notifications"; + +bool ExposureNotificationTrigger::parse_device(const ESPBTDevice &device) { + // See also https://blog.google/documents/70/Exposure_Notification_-_Bluetooth_Specification_v1.2.2.pdf + if (device.get_service_uuids().size() != 1) + return false; + + // Exposure notifications have Service UUID FD 6F + ESPBTUUID uuid = device.get_service_uuids()[0]; + // constant service identifier + const ESPBTUUID expected_uuid = ESPBTUUID::from_uint16(0xFD6F); + if (uuid != expected_uuid) + return false; + if (device.get_service_datas().size() != 1) + return false; + + // The service data should be 20 bytes + // First 16 bytes are the rolling proximity identifier (RPI) + // Then 4 bytes of encrypted metadata follow which can be used to get the transmit power level. + ServiceData service_data = device.get_service_datas()[0]; + if (service_data.uuid != expected_uuid) + return false; + auto data = service_data.data; + if (data.size() != 20) + return false; + ExposureNotification notification{}; + memcpy(¬ification.address[0], device.address(), 6); + memcpy(¬ification.rolling_proximity_identifier[0], &data[0], 16); + memcpy(¬ification.associated_encrypted_metadata[0], &data[16], 4); + notification.rssi = device.get_rssi(); + this->trigger(notification); + return true; +} + +} // namespace exposure_notifications +} // namespace esphome + +#endif diff --git a/esphome/components/exposure_notifications/exposure_notifications.h b/esphome/components/exposure_notifications/exposure_notifications.h new file mode 100644 index 0000000000..6b9f61b2a0 --- /dev/null +++ b/esphome/components/exposure_notifications/exposure_notifications.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace exposure_notifications { + +struct ExposureNotification { + std::array address; + int rssi; + std::array rolling_proximity_identifier; + std::array associated_encrypted_metadata; +}; + +class ExposureNotificationTrigger : public Trigger, + public esp32_ble_tracker::ESPBTDeviceListener { + public: + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; +}; + +} // namespace exposure_notifications +} // namespace esphome + +#endif From e20c994b82996912194020092730265e4412654c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 18:59:48 +0200 Subject: [PATCH 089/200] Fix adding another mbedtls (#1131) --- esphome/components/xiaomi_cgd1/sensor.py | 2 -- esphome/components/xiaomi_lywsd03mmc/sensor.py | 2 -- esphome/components/xiaomi_mjyd02yla/binary_sensor.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/esphome/components/xiaomi_cgd1/sensor.py b/esphome/components/xiaomi_cgd1/sensor.py index b7891c8a56..401f6de7d2 100644 --- a/esphome/components/xiaomi_cgd1/sensor.py +++ b/esphome/components/xiaomi_cgd1/sensor.py @@ -39,5 +39,3 @@ def to_code(config): if CONF_BATTERY_LEVEL in config: sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) - - cg.add_library("mbedtls", None) diff --git a/esphome/components/xiaomi_lywsd03mmc/sensor.py b/esphome/components/xiaomi_lywsd03mmc/sensor.py index 1ab59b01c5..9ecf3f64a9 100644 --- a/esphome/components/xiaomi_lywsd03mmc/sensor.py +++ b/esphome/components/xiaomi_lywsd03mmc/sensor.py @@ -40,5 +40,3 @@ def to_code(config): if CONF_BATTERY_LEVEL in config: sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) cg.add(var.set_battery_level(sens)) - - cg.add_library("mbedtls", None) diff --git a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py index 6d2a674969..72cf57d22c 100644 --- a/esphome/components/xiaomi_mjyd02yla/binary_sensor.py +++ b/esphome/components/xiaomi_mjyd02yla/binary_sensor.py @@ -43,5 +43,3 @@ def to_code(config): if CONF_LIGHT in config: sens = yield binary_sensor.new_binary_sensor(config[CONF_LIGHT]) cg.add(var.set_light(sens)) - - cg.add_library("mbedtls", None) From 17b55fc23de187fd3eb5b717a65f60bf04fe44ee Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 14 Jul 2020 19:00:01 +0200 Subject: [PATCH 090/200] Tuya Sensor remove commented out code (style guide) (#1132) --- esphome/components/tuya/sensor/tuya_sensor.cpp | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/esphome/components/tuya/sensor/tuya_sensor.cpp b/esphome/components/tuya/sensor/tuya_sensor.cpp index 49a1af2efe..b8e2aa97b7 100644 --- a/esphome/components/tuya/sensor/tuya_sensor.cpp +++ b/esphome/components/tuya/sensor/tuya_sensor.cpp @@ -24,17 +24,6 @@ void TuyaSensor::setup() { }); } -// void TuyaSensor::write_state(bool state) { -// TuyaDatapoint datapoint{}; -// datapoint.id = this->sensor_id_; -// datapoint.type = TuyaDatapointType::BOOLEAN; -// datapoint.value_bool = state; -// this->parent_->set_datapoint_value(datapoint); -// ESP_LOGD(TAG, "Setting sensor: %s", ONOFF(state)); - -// this->publish_state(state); -// } - void TuyaSensor::dump_config() { LOG_SENSOR("", "Tuya Sensor", this); ESP_LOGCONFIG(TAG, " Sensor has datapoint ID %u", this->sensor_id_); From 764eb960c6a9ea819684105842284c67b3d4e3a4 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 01:37:30 +0200 Subject: [PATCH 091/200] Update build scripts --- .github/workflows/ci.yml | 12 ++-- .github/workflows/release-dev.yml | 67 +++++++++++++--------- .github/workflows/release.yml | 93 +++++++++++++++++-------------- docker/Dockerfile | 2 +- docker/Dockerfile.dev | 2 +- docker/Dockerfile.lint | 23 +------- esphome/core_config.py | 5 +- requirements.txt | 2 +- requirements_test.txt | 14 ----- script/setup | 2 +- setup.py | 2 +- 11 files changed, 110 insertions(+), 114 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9d03705e84..6ade0655a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: # A fast overview job that checks only changed files overview: runs-on: ubuntu-latest - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest steps: # Also fetch history and dev branch so that we can check which files changed - uses: actions/checkout@v2 @@ -27,7 +27,7 @@ jobs: fetch-depth: 0 - name: Fetch dev branch run: git fetch origin dev - + # Cache the .pio directory with (primarily) library dependencies - name: Cache .pio lib_deps uses: actions/cache@v1 @@ -59,7 +59,7 @@ jobs: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest steps: - uses: actions/checkout@v2 # Cache platformio intermediary files (like libraries etc) @@ -85,7 +85,7 @@ jobs: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files strategy: matrix: @@ -106,7 +106,7 @@ jobs: - name: Set up platformio environment run: pio init --ide atom - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" @@ -188,7 +188,7 @@ jobs: - name: Set up environment run: script/setup - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 217d943704..962238807e 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -7,12 +7,12 @@ on: jobs: # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml - + lint-clang-format: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest steps: - uses: actions/checkout@v2 # Cache platformio intermediary files (like libraries etc) @@ -38,7 +38,7 @@ jobs: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files strategy: matrix: @@ -59,7 +59,7 @@ jobs: - name: Set up platformio environment run: pio init --ide atom - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" @@ -141,7 +141,7 @@ jobs: - name: Set up environment run: script/setup - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" @@ -158,45 +158,54 @@ jobs: build_type: ["hassio", "docker"] steps: - uses: actions/checkout@v2 + - name: Set TAG + run: | + TAG="${GITHUB_SHA:0:7}" + echo "::set-env name=TAG::${TAG}" - name: Set up env variables run: | - tag="dev" - base_version="2.1.2" + base_version="2.3.1" if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="${{ github.repository }}-hassio-${{ matrix.arch }}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" dockerfile="docker/Dockerfile.hassio" else build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="${{ github.repository }}-${{ matrix.arch }}" + build_to="esphome/esphome-${{ matrix.arch }}" dockerfile="docker/Dockerfile" fi - # Set env variables so these values don't need to be calculated again - echo "::set-env name=TAG::${tag}" echo "::set-env name=BUILD_FROM::${build_from}" echo "::set-env name=BUILD_TO::${build_to}" - echo "::set-env name=IMAGE::${build_to}:${tag}" echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:latest" || true + docker pull "${BUILD_TO}:beta" || true + docker pull "${BUILD_TO}:dev" || true - name: Register QEMU binfmt run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - run: | docker build \ --build-arg "BUILD_FROM=${BUILD_FROM}" \ --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${IMAGE}" \ + --tag "${BUILD_TO}:${TAG}" \ + --tag "${BUILD_TO}:dev" \ + --cache-from "${BUILD_TO}:latest" \ + --cache-from "${BUILD_TO}:beta" \ + --cache-from "${BUILD_TO}:dev" \ --file "${DOCKERFILE}" \ - --cache-from "${{ github.repository }}:latest" \ - --cache-from "${{ github.repository }}:beta" \ - --cache-from "${{ github.repository }}:dev" \ . - name: Log in to docker hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: docker push "${IMAGE}" + - run: | + docker push "${BUILD_TO}:${TAG}" + docker push "${BUILD_TO}:dev" + deploy-docker-manifest: runs-on: ubuntu-latest @@ -206,6 +215,10 @@ jobs: run: | mkdir -p ~/.docker echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Set TAG + run: | + TAG="${GITHUB_SHA:0:7}" + echo "::set-env name=TAG::${TAG}" - name: Log in to docker hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} @@ -213,12 +226,16 @@ jobs: run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - name: "Create the manifest" run: | - REPO=${{ github.repository }} - TAG="dev" + docker manifest create esphome/esphome:${TAG} \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} \ + esphome/esphome-i386:${TAG} + docker manifest push esphome/esphome:${TAG} - docker manifest create ${REPO}:${TAG} \ - ${REPO}-aarch64:${TAG} \ - ${REPO}-amd64:${TAG} \ - ${REPO}-armv7:${TAG} \ - ${REPO}-i386:${TAG} - docker manifest push ${REPO}:${TAG} + docker manifest create esphome/esphome:dev \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} \ + esphome/esphome-i386:${TAG} + docker manifest push esphome/esphome:dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8af0ba8a1..47811cc53c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,12 +6,12 @@ on: jobs: # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml - + lint-clang-format: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest steps: - uses: actions/checkout@v2 # Cache platformio intermediary files (like libraries etc) @@ -37,7 +37,7 @@ jobs: runs-on: ubuntu-latest # cpp lint job runs with esphome-lint docker image so that clang-format-* # doesn't have to be installed - container: esphome/esphome-lint:dev + container: esphome/esphome-lint:latest # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files strategy: matrix: @@ -58,7 +58,7 @@ jobs: - name: Set up platformio environment run: pio init --ide atom - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" @@ -140,7 +140,6 @@ jobs: - name: Set up environment run: script/setup - - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/gcc.json" @@ -179,54 +178,62 @@ jobs: build_type: ["hassio", "docker"] steps: - uses: actions/checkout@v2 + - name: Set TAG + run: | + TAG="${GITHUB_REF#refs/tags/v}" + echo "::set-env name=TAG::${TAG}" - name: Set up env variables run: | - tag="${GITHUB_REF#v}" - base_version="2.1.2" + base_version="2.3.1" if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" - build_to="${{ github.repository }}-hassio-${{ matrix.arch }}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" dockerfile="docker/Dockerfile.hassio" else build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" - build_to="${{ github.repository }}-${{ matrix.arch }}" + build_to="esphome/esphome-${{ matrix.arch }}" dockerfile="docker/Dockerfile" fi # Set env variables so these values don't need to be calculated again - echo "::set-env name=TAG::${tag}" echo "::set-env name=BUILD_FROM::${build_from}" echo "::set-env name=BUILD_TO::${build_to}" - echo "::set-env name=IMAGE::${build_to}:${tag}" echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:latest" || true + docker pull "${BUILD_TO}:beta" || true + docker pull "${BUILD_TO}:dev" || true - name: Register QEMU binfmt run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - run: | docker build \ --build-arg "BUILD_FROM=${BUILD_FROM}" \ --build-arg "BUILD_VERSION=${TAG}" \ - --tag "${IMAGE}" \ + --tag "${BUILD_TO}:${TAG}" \ + --cache-from "${BUILD_TO}:latest" \ + --cache-from "${BUILD_TO}:beta" \ + --cache-from "${BUILD_TO}:dev" \ --file "${DOCKERFILE}" \ - --cache-from "${{ github.repository }}:latest" \ - --cache-from "${{ github.repository }}:beta" \ - --cache-from "${{ github.repository }}:dev" \ . - name: Log in to docker hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - - run: docker push "${IMAGE}" - - if: ${{ github.event.release.prerelease) }} - name: Publish docker beta tag + - run: docker push "${BUILD_TO}:${TAG}" + + # Always publish to beta tag (also full releases) + - name: Publish docker beta tag run: | - docker tag "${IMAGE}" ${BUILD_TO}:beta + docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta" docker push "${BUILD_TO}:beta" + - if: ${{ !github.event.release.prerelease) }} name: Publish docker latest tag run: | - docker tag "${IMAGE}" ${BUILD_TO}:latest + docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest" docker push "${BUILD_TO}:latest" deploy-docker-manifest: @@ -237,6 +244,10 @@ jobs: run: | mkdir -p ~/.docker echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json + - name: Set TAG + run: | + TAG="${GITHUB_REF#refs/tags/v}" + echo "::set-env name=TAG::${TAG}" - name: Log in to docker hub env: DOCKER_USER: ${{ secrets.DOCKER_USER }} @@ -244,32 +255,28 @@ jobs: run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" - name: "Create the manifest" run: | - REPO=${{ github.repository }} - TAG="${GITHUB_REF#v}" - - docker manifest create ${REPO}:${TAG} \ - ${REPO}-aarch64:${TAG} \ - ${REPO}-amd64:${TAG} \ - ${REPO}-armv7:${TAG} \ - ${REPO}-i386:${TAG} - echo "::set-env name=REPO::${REPO}" - docker manifest push ${REPO}:${TAG} + docker manifest create esphome/esphome:${TAG} \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} \ + esphome/esphome-i386:${TAG} + docker manifest push esphome/esphome:${TAG} - name: Publish docker beta tag - if: ${{ github.event.release.prerelease) }} run: | - docker manifest create ${REPO}:beta \ - ${REPO}-aarch64:beta \ - ${REPO}-amd64:beta \ - ${REPO}-armv7:beta \ - ${REPO}-i386:beta - docker manifest push ${REPO}:beta + docker manifest create esphome/esphome:beta \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} \ + esphome/esphome-i386:${TAG} + docker manifest push esphome/esphome:beta + - name: Publish docker latest tag if: ${{ !github.event.release.prerelease) }} run: | - docker manifest create ${REPO}:latest \ - ${REPO}-aarch64:latest \ - ${REPO}-amd64:latest \ - ${REPO}-armv7:latest \ - ${REPO}-i386:latest - docker manifest push ${REPO}:latest + docker manifest create esphome/esphome:latest \ + esphome/esphome-aarch64:${TAG} \ + esphome/esphome-amd64:${TAG} \ + esphome/esphome-armv7:${TAG} \ + esphome/esphome-i386:${TAG} + docker manifest push esphome/esphome:latest diff --git a/docker/Dockerfile b/docker/Dockerfile index bcdc5feaf7..16b546d0dd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.1.1 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.3.1 FROM ${BUILD_FROM} COPY . . diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index a3871e2513..0a72361a28 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.0.1 +FROM esphome/esphome-base-amd64:2.3.1 COPY . . diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index 32b316075f..a1839a82c0 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,24 +1,7 @@ -FROM esphome/esphome-base-amd64:2.1.2 +FROM esphome/esphome-lint-base:2.3.1 -RUN \ - apt-get update \ - && apt-get install -y --no-install-recommends \ - clang-format-7 \ - clang-tidy-7 \ - patch \ - software-properties-common \ - # Update to latest git version because of github actions - # https://github.com/actions/checkout/issues/126 - && apt-add-repository ppa:git-core/ppa \ - && apt-get install -y --no-install-recommends \ - git \ - && rm -rf \ - /tmp/* \ - /var/{cache,log}/* \ - /var/lib/apt/lists/* - -COPY requirements_test.txt /requirements_test.txt -RUN pip3 install --no-cache-dir -r /requirements_test.txt +COPY requirements.txt requirements_test.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt VOLUME ["/esphome"] WORKDIR /esphome diff --git a/esphome/core_config.py b/esphome/core_config.py index d447eaaffb..9b4ea3a2e1 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -46,6 +46,9 @@ def validate_board(value): validate_platform = cv.one_of(*ESP_PLATFORMS, upper=True) PLATFORMIO_ESP8266_LUT = { + '2.7.2': 'espressif8266@2.6.0', + '2.7.1': 'espressif8266@2.5.1', + '2.7.0': 'espressif8266@2.5.0', '2.6.3': 'espressif8266@2.4.0', '2.6.2': 'espressif8266@2.3.1', '2.6.1': 'espressif8266@2.3.0', @@ -66,7 +69,7 @@ PLATFORMIO_ESP32_LUT = { '1.0.1': 'espressif32@1.6.0', '1.0.2': 'espressif32@1.9.0', '1.0.3': 'espressif32@1.10.0', - '1.0.4': 'espressif32@1.12.1', + '1.0.4': 'espressif32@1.12.4', 'RECOMMENDED': 'espressif32@1.12.1', 'LATEST': 'espressif32', 'DEV': ARDUINO_VERSION_ESP32_DEV, diff --git a/requirements.txt b/requirements.txt index 0a3573f7b0..731bd73f39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tzlocal==2.0.0 pytz==2020.1 pyserial==3.4 ifaddr==0.1.6 -platformio==4.3.3 +platformio==4.3.4 esptool==2.8 click==7.1.2 deepmerge==0.1.0 diff --git a/requirements_test.txt b/requirements_test.txt index da4656b085..ae78feab21 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,17 +1,3 @@ -voluptuous==0.11.7 -PyYAML==5.3.1 -paho-mqtt==1.5.0 -colorlog==4.1.0 -tornado==6.0.4 -protobuf==3.11.3 -tzlocal==2.0.0 -pytz==2020.1 -pyserial==3.4 -ifaddr==0.1.6 -platformio==4.3.3 -esptool==2.8 -deepmerge==0.1.0 - pylint==2.5.0 flake8==3.7.9 pillow diff --git a/script/setup b/script/setup index 810fb2a2bf..d70a44ee49 100755 --- a/script/setup +++ b/script/setup @@ -4,5 +4,5 @@ set -e cd "$(dirname "$0")/.." -pip3 install -r requirements_test.txt +pip3 install -r requirements.txt -r requirements_test.txt pip3 install -e . diff --git a/setup.py b/setup.py index ede80b34e1..2e08cb40b8 100755 --- a/setup.py +++ b/setup.py @@ -41,7 +41,7 @@ REQUIRES = [ # This means they have to be in your $PATH. if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: REQUIRES.extend([ - 'platformio==4.3.3', + 'platformio==4.3.4', 'esptool==2.8', ]) From 13696b1ec45846afda4d15deb1c204846a32b814 Mon Sep 17 00:00:00 2001 From: Samuel Sieb Date: Wed, 15 Jul 2020 02:45:54 -0700 Subject: [PATCH 092/200] add mqtt speed topics for fan (#1140) Co-authored-by: Samuel Sieb --- esphome/components/fan/__init__.py | 4 ++++ tests/test1.yaml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 50fdb1c2c9..7b0a79a0d3 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -34,6 +34,10 @@ FAN_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({ cv.publish_topic), cv.Optional(CONF_OSCILLATION_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), cv.subscribe_topic), + cv.Optional(CONF_SPEED_STATE_TOPIC): cv.All(cv.requires_component('mqtt'), + cv.publish_topic), + cv.Optional(CONF_SPEED_COMMAND_TOPIC): cv.All(cv.requires_component('mqtt'), + cv.subscribe_topic), }) diff --git a/tests/test1.yaml b/tests/test1.yaml index b131b1a6ab..84af00e90a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1499,6 +1499,10 @@ fan: low: 0.45 medium: 0.75 high: 1.0 + oscillation_state_topic: oscillation/state/topic + oscillation_command_topic: oscillation/command/topic + speed_state_topic: speed/state/topic + speed_command_topic: speed/command/topic interval: - interval: 10s From 9880a425f4be40fb684eb4713cab11a0bd095cc2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 12:04:22 +0200 Subject: [PATCH 093/200] Only compile docker builds for dev releases --- .github/workflows/release-dev.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 962238807e..e807666de4 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -155,7 +155,8 @@ jobs: strategy: matrix: arch: [amd64, i386, armv7, aarch64] - build_type: ["hassio", "docker"] + # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly + build_type: ["docker"] steps: - uses: actions/checkout@v2 - name: Set TAG From 2a14473e8cb27f1f1be89ac9d42e3c31c632523d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 12:58:15 +0200 Subject: [PATCH 094/200] Create dependabot.yml --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..b38df29f46 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" From eca36cb868468eeb34fefc9ebcc6f46fcd6d94e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:26:13 +0200 Subject: [PATCH 095/200] Bump pytest from 5.4.1 to 5.4.3 (#1144) Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.1 to 5.4.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.4.1...5.4.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index ae78feab21..eba4c87557 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,7 +4,7 @@ pillow pexpect # Unit tests -pytest==5.4.1 +pytest==5.4.3 pytest-cov==2.8.1 pytest-mock==1.13.0 asyncmock==0.4.2 From ba73cb1b753b81f1e35f172b85829cd2a98eea16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:27:47 +0200 Subject: [PATCH 096/200] Bump hypothesis from 5.10.4 to 5.19.3 (#1146) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 5.10.4 to 5.19.3. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-5.10.4...hypothesis-python-5.19.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index eba4c87557..da28ebe0cc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,4 +8,4 @@ pytest==5.4.3 pytest-cov==2.8.1 pytest-mock==1.13.0 asyncmock==0.4.2 -hypothesis==5.10.4 +hypothesis==5.19.3 From 7bbdfc555358840db990d75baec17cc934e2ad7d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:28:06 +0200 Subject: [PATCH 097/200] Bump protobuf from 3.11.3 to 3.12.2 (#1147) Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 3.11.3 to 3.12.2. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.11.3...v3.12.2) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 731bd73f39..93d55a062b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ PyYAML==5.3.1 paho-mqtt==1.5.0 colorlog==4.1.0 tornado==6.0.4 -protobuf==3.11.3 +protobuf==3.12.2 tzlocal==2.0.0 pytz==2020.1 pyserial==3.4 From 850368b5292073a8c54e6c08f94f366403d7f82e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:28:37 +0200 Subject: [PATCH 098/200] Bump ifaddr from 0.1.6 to 0.1.7 (#1148) Bumps [ifaddr](https://github.com/pydron/ifaddr) from 0.1.6 to 0.1.7. - [Release notes](https://github.com/pydron/ifaddr/releases) - [Commits](https://github.com/pydron/ifaddr/compare/0.1.6...0.1.7) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 93d55a062b..059a07fc83 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ protobuf==3.12.2 tzlocal==2.0.0 pytz==2020.1 pyserial==3.4 -ifaddr==0.1.6 +ifaddr==0.1.7 platformio==4.3.4 esptool==2.8 click==7.1.2 From ca144bae906525f0c9f1c230c4a358c83a96c38e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2020 13:44:43 +0200 Subject: [PATCH 099/200] Bump pytest-cov from 2.8.1 to 2.10.0 (#1145) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index da28ebe0cc..81f4953975 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pexpect # Unit tests pytest==5.4.3 -pytest-cov==2.8.1 +pytest-cov==2.10.0 pytest-mock==1.13.0 asyncmock==0.4.2 hypothesis==5.19.3 From 1254ec284916d2c063ab51a373f555972bd5aae6 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 13:50:35 +0200 Subject: [PATCH 100/200] Fix Waveshare 7.50inV2 (#1143) See also https://github.com/esphome/esphome/pull/1077 --- esphome/components/waveshare_epaper/display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 577d832785..da2e30de00 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -32,7 +32,7 @@ MODELS = { '4.20in': ('b', WaveshareEPaper4P2In), '5.83in': ('b', WaveshareEPaper5P8In), '7.50in': ('b', WaveshareEPaper7P5In), - '7.50inV2': ('b', WaveshareEPaper7P5InV2), + '7.50inv2': ('b', WaveshareEPaper7P5InV2), } From a20e71b32abf37026afaf17e993542878248695c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 13:51:48 +0200 Subject: [PATCH 101/200] Load setup.py requirements from requirements.txt (#1149) --- setup.py | 31 ++++++++++++------------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index 2e08cb40b8..06151bf1e5 100755 --- a/setup.py +++ b/setup.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 """esphome setup script.""" -from setuptools import setup, find_packages import os +from setuptools import setup, find_packages + from esphome import const PROJECT_NAME = 'esphome' @@ -22,28 +23,20 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__) -REQUIRES = [ - 'voluptuous==0.11.7', - 'PyYAML==5.3.1', - 'paho-mqtt==1.5.0', - 'colorlog==4.1.0', - 'tornado==6.0.4', - 'protobuf==3.11.3', - 'tzlocal==2.0.0', - 'pytz==2020.1', - 'pyserial==3.4', - 'ifaddr==0.1.6', - 'deepmerge==0.1.0' -] +here = os.path.abspath(os.path.dirname(__file__)) + +with open(os.path.join(here, 'requirements.txt')) as requirements_txt: + REQUIRES = requirements_txt.read().splitlines() # If you have problems importing platformio and esptool as modules you can set # $ESPHOME_USE_SUBPROCESS to make ESPHome call their executables instead. # This means they have to be in your $PATH. -if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: - REQUIRES.extend([ - 'platformio==4.3.4', - 'esptool==2.8', - ]) +if 'ESPHOME_USE_SUBPROCESS' in os.environ: + # Remove platformio and esptool from requirements + REQUIRES = [ + req for req in REQUIRES + if not any(req.startswith(prefix) for prefix in ['platformio', 'esptool']) + ] CLASSIFIERS = [ 'Environment :: Console', From d5c59292c86366bf709840cb4d22942c325e83b0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 14:00:02 +0200 Subject: [PATCH 102/200] Add pytest to CI (#1138) --- .github/workflows/ci.yml | 42 +++++++++++++++++++++-------- .github/workflows/release-dev.yml | 42 ++++++++++++++++++++++------- .github/workflows/release.yml | 44 +++++++++++++++++++++++-------- esphome/core.py | 16 +++++++++-- esphome/pins.py | 10 ++++--- tests/unit_tests/test_core.py | 4 ++- tests/unit_tests/test_pins.py | 1 - 7 files changed, 120 insertions(+), 39 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ade0655a2..6d6798314f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,6 @@ on: branches: [beta, master] pull_request: - # Only trigger on certain events (not when comments are added) - types: [opened, reopened, synchronize] # Only run when PR is against dev branch (all PRs should be against dev branch) # Helps prevent accidentally merging PRs against master branch branches: [dev] @@ -176,15 +174,6 @@ jobs: key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - # Cache the intermediary build files - - name: Cache Test Build - uses: actions/cache@v1 - with: - path: tests/build/${{ matrix.test }} - key: test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}-${{ hashFiles('esphome/**') }} - restore-keys: | - test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}- - test-pio-${{ matrix.test }}- - name: Set up environment run: script/setup @@ -194,3 +183,34 @@ jobs: echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index e807666de4..3eb6884ee0 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -129,15 +129,6 @@ jobs: key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - # Cache the intermediary build files - - name: Cache Test Build - uses: actions/cache@v1 - with: - path: tests/build/${{ matrix.test }} - key: test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}-${{ hashFiles('esphome/**') }} - restore-keys: | - test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}- - test-pio-${{ matrix.test }}- - name: Set up environment run: script/setup @@ -148,10 +139,41 @@ jobs: echo "::add-matcher::.github/workflows/matchers/python.json" - run: esphome tests/${{ matrix.test }}.yaml compile + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests + deploy-docker: name: Build and publish docker containers runs-on: ubuntu-latest - needs: [lint-clang-format, lint-clang-tidy, lint-python, test] + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: matrix: arch: [amd64, i386, armv7, aarch64] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 47811cc53c..43e0d86365 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -128,15 +128,6 @@ jobs: key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - # Cache the intermediary build files - - name: Cache Test Build - uses: actions/cache@v1 - with: - path: tests/build/${{ matrix.test }} - key: test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}-${{ hashFiles('esphome/**') }} - restore-keys: | - test-pio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }}- - test-pio-${{ matrix.test }}- - name: Set up environment run: script/setup @@ -145,10 +136,41 @@ jobs: echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - run: esphome tests/${{ matrix.test }}.yaml compile + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.7' + - name: Cache pip modules + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: esphome-pip-3.7-${{ hashFiles('setup.py') }} + restore-keys: | + esphome-pip-3.7- + - name: Set up environment + run: script/setup + - name: Install Github Actions annotator + run: pip install pytest-github-actions-annotate-failures + + - name: Register problem matchers + run: | + echo "::add-matcher::.github/workflows/matchers/python.json" + - name: Run pytest + run: | + pytest \ + -qq \ + --durations=10 \ + -o console_output_style=count \ + tests deploy-pypi: name: Build and publish to PyPi - needs: [lint-clang-format, lint-clang-tidy, lint-python, test] + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -171,7 +193,7 @@ jobs: deploy-docker: name: Build and publish docker containers runs-on: ubuntu-latest - needs: [lint-clang-format, lint-clang-tidy, lint-python, test] + needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: matrix: arch: [amd64, i386, armv7, aarch64] diff --git a/esphome/core.py b/esphome/core.py index dfb37555e1..52904f0d0b 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -493,9 +493,9 @@ class EsphomeCore: # The board that's used (for example nodemcuv2) self.board: Optional[str] = None # The full raw configuration - self.raw_config: ConfigType = {} + self.raw_config: Optional[ConfigType] = None # The validated configuration, this is None until the config has been validated - self.config: ConfigType = {} + self.config: Optional[ConfigType] = None # The pending tasks in the task queue (mostly for C++ generation) # This is a priority queue (with heapq) # Each item is a tuple of form: (-priority, unique number, task) @@ -547,6 +547,10 @@ class EsphomeCore: @property def address(self) -> Optional[str]: + if self.config is None: + raise ValueError("Config has not been loaded yet") + + # pylint: disable=unsupported-membership-test,unsubscriptable-object if 'wifi' in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] @@ -557,6 +561,10 @@ class EsphomeCore: @property def comment(self) -> Optional[str]: + if self.config is None: + raise ValueError("Config has not been loaded yet") + + # pylint: disable=unsubscriptable-object if CONF_COMMENT in self.config[CONF_ESPHOME]: return self.config[CONF_ESPHOME][CONF_COMMENT] @@ -570,6 +578,10 @@ class EsphomeCore: @property def arduino_version(self) -> str: + if self.config is None: + raise ValueError("Config has not been loaded yet") + + # pylint: disable=unsubscriptable-object return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] @property diff --git a/esphome/pins.py b/esphome/pins.py index 7f4c869895..b6fde85250 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -256,16 +256,20 @@ ESP32_BOARD_PINS = { def _lookup_pin(value): if CORE.is_esp8266: - board_pins = ESP8266_BOARD_PINS.get(CORE.board, {}) + board_pins_dict = ESP8266_BOARD_PINS base_pins = ESP8266_BASE_PINS elif CORE.is_esp32: - board_pins = ESP32_BOARD_PINS.get(CORE.board, {}) + board_pins_dict = ESP32_BOARD_PINS base_pins = ESP32_BASE_PINS else: raise NotImplementedError + board_pins = board_pins_dict.get(CORE.board, {}) + + # Resolved aliased board pins (shorthand when two boards have the same pin configuration) while isinstance(board_pins, str): - board_pins = ESP8266_BOARD_PINS.get(board_pins, {}) + board_pins = board_pins_dict[board_pins] + if value in board_pins: return board_pins[value] if value in base_pins: diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index cd0b0947f3..166a12c40e 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -459,7 +459,6 @@ class TestEsphomeCore: target.config_path = "foo/config" return target - @pytest.mark.xfail(reason="raw_config and config differ, should they?") def test_reset(self, target): """Call reset on target and compare to new instance""" other = core.EsphomeCore() @@ -469,15 +468,18 @@ class TestEsphomeCore: assert target.__dict__ == other.__dict__ def test_address__none(self, target): + target.config = {} assert target.address is None def test_address__wifi(self, target): + target.config = {} target.config[const.CONF_WIFI] = {const.CONF_USE_ADDRESS: "1.2.3.4"} target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} assert target.address == "1.2.3.4" def test_address__ethernet(self, target): + target.config = {} target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} assert target.address == "4.3.2.1" diff --git a/tests/unit_tests/test_pins.py b/tests/unit_tests/test_pins.py index 606c20eea2..7d68181add 100644 --- a/tests/unit_tests/test_pins.py +++ b/tests/unit_tests/test_pins.py @@ -94,7 +94,6 @@ class Test_lookup_pin: assert actual == expected - @pytest.mark.xfail(reason="This may be expected") def test_valid_32_pin_alias(self, core_esp32): core_esp32.board = MOCK_ESP32_BOARD_ALIAS_ID From 3ec9bcaed6f2d96b2c8d93609e55187be695364d Mon Sep 17 00:00:00 2001 From: Peter Kuehne Date: Wed, 15 Jul 2020 13:04:00 +0100 Subject: [PATCH 103/200] Feature/component test fixture (#1142) --- .../binary_sensor/test_binary_sensor.py | 43 +++++++++++++------ tests/component_tests/conftest.py | 23 ++++++++++ 2 files changed, 54 insertions(+), 12 deletions(-) create mode 100644 tests/component_tests/conftest.py diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index 8f3cd3aeda..72c0dc1cde 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -1,24 +1,43 @@ """ Tests for the binary sensor component """ -from esphome.core import CORE -from esphome.config import read_config -from esphome.__main__ import generate_cpp_contents + +def test_binary_sensor_is_setup(generate_main): + """ + When the binary sensor is set in the yaml file, it should be registered in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/binary_sensor/test_binary_sensor.yaml") + + # Then + assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "App.register_binary_sensor" in main_cpp -def test_binary_sensor_config_value_internal_set(): +def test_binary_sensor_sets_mandatory_fields(generate_main): + """ + When the mandatory fields are set in the yaml, they should be set in main + """ + # Given + + # When + main_cpp = generate_main("tests/component_tests/binary_sensor/test_binary_sensor.yaml") + + # Then + assert "bs_1->set_name(\"test bs1\");" in main_cpp + assert "bs_1->set_pin(new GPIOPin" in main_cpp + + +def test_binary_sensor_config_value_internal_set(generate_main): """ Test that the "internal" config value is correctly set """ # Given - CORE.config_path = "tests/component_tests/binary_sensor/test_binary_sensor.yaml" - CORE.config = read_config({}) # When - generate_cpp_contents(CORE.config) - # print(CORE.cpp_main_section) + main_cpp = generate_main("tests/component_tests/binary_sensor/test_binary_sensor.yaml") # Then - assert "bs_1->set_internal(true);" in CORE.cpp_main_section - assert "bs_2->set_internal(false);" in CORE.cpp_main_section - - CORE.reset() + assert "bs_1->set_internal(true);" in main_cpp + assert "bs_2->set_internal(false);" in main_cpp diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py new file mode 100644 index 0000000000..207a6dead9 --- /dev/null +++ b/tests/component_tests/conftest.py @@ -0,0 +1,23 @@ +""" Fixtures for component tests """ + +import pytest + +from esphome.core import CORE +from esphome.config import read_config +from esphome.__main__ import generate_cpp_contents + + +@pytest.fixture +def generate_main(): + """ Generates the C++ main.cpp file and returns it in string form """ + + def generator(path: str) -> str: + CORE.config_path = path + CORE.config = read_config({}) + generate_cpp_contents(CORE.config) + print(CORE.cpp_main_section) + return CORE.cpp_main_section + + yield generator + + CORE.reset() From b6394b7c6d6c210fd1f28ce01bd06f8777465ae9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 14:42:43 +0200 Subject: [PATCH 104/200] Remove unused bots --- .github/ci-reporter.yml | 8 -------- .github/config.yml | 11 ----------- 2 files changed, 19 deletions(-) delete mode 100644 .github/ci-reporter.yml delete mode 100644 .github/config.yml diff --git a/.github/ci-reporter.yml b/.github/ci-reporter.yml deleted file mode 100644 index 243e671532..0000000000 --- a/.github/ci-reporter.yml +++ /dev/null @@ -1,8 +0,0 @@ -# Set to false to create a new comment instead of updating the app's first one -updateComment: true - -# Use a custom string, or set to false to disable -before: "✨ Good work on this PR so far! ✨ Unfortunately, the [ build]() is failing as of . Here's the output:" - -# Use a custom string, or set to false to disable -after: "Thanks for contributing to this project!" diff --git a/.github/config.yml b/.github/config.yml deleted file mode 100644 index f2b357cc95..0000000000 --- a/.github/config.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot - -# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic -# Anything higher than this threshold will be marked as toxic and commented on -sentimentBotToxicityThreshold: .8 - -# *Required* Comment to reply with -sentimentBotReplyComment: > - Please be sure to review the code of conduct and be respectful of other users. - -# Note: the bot will only work if your repository has a Code of Conduct From dcadcdf0567991d8b8ee08f2acd6a393eafc1593 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 15:03:15 +0200 Subject: [PATCH 105/200] Use more layer caching for esphome/esphome Dockerfile (#1150) --- .github/workflows/release-dev.yml | 2 +- .github/workflows/release.yml | 2 +- docker/Dockerfile | 15 ++++++++++++--- docker/Dockerfile.dev | 2 +- docker/Dockerfile.lint | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 3eb6884ee0..59beeea212 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -187,7 +187,7 @@ jobs: echo "::set-env name=TAG::${TAG}" - name: Set up env variables run: | - base_version="2.3.1" + base_version="2.3.3" if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43e0d86365..84610156a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,7 +206,7 @@ jobs: echo "::set-env name=TAG::${TAG}" - name: Set up env variables run: | - base_version="2.3.1" + base_version="2.3.3" if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" diff --git a/docker/Dockerfile b/docker/Dockerfile index 16b546d0dd..d0b4f2ffa1 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,12 +1,21 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.3.1 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.3.3 FROM ${BUILD_FROM} +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt + +# Then copy esphome and install COPY . . RUN pip3 install --no-cache-dir -e . -ENV USERNAME="" -ENV PASSWORD="" +# Settings for dashboard +ENV USERNAME="" PASSWORD="" +# The directory the user should mount their configuration files to WORKDIR /config +# Set entrypoint to esphome so that the user doesn't have to type 'esphome' +# in every docker command twice ENTRYPOINT ["esphome"] +# When no arguments given, start the dashboard in the workdir CMD ["/config", "dashboard"] diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 0a72361a28..b6a0f8f90b 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.3.1 +FROM esphome/esphome-base-amd64:2.3.3 COPY . . diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index a1839a82c0..bc7caa7e97 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,4 @@ -FROM esphome/esphome-lint-base:2.3.1 +FROM esphome/esphome-lint-base:2.3.3 COPY requirements.txt requirements_test.txt / RUN pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt From 47520965208a6731b3fb224f8b88c7409f497667 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 15:27:24 +0200 Subject: [PATCH 106/200] Don't remove location information for packages (#1133) --- esphome/components/packages/__init__.py | 59 +++++++++----------- esphome/components/substitutions/__init__.py | 3 +- esphome/config.py | 10 ++-- esphome/const.py | 1 + requirements.txt | 1 - 5 files changed, 33 insertions(+), 41 deletions(-) diff --git a/esphome/components/packages/__init__.py b/esphome/components/packages/__init__.py index 729af4a9a0..afbeae7f8d 100644 --- a/esphome/components/packages/__init__.py +++ b/esphome/components/packages/__init__.py @@ -1,51 +1,44 @@ -from deepmerge import conservative_merger as package_merger - import esphome.config_validation as cv from esphome.const import CONF_PACKAGES -VALID_PACKAGE_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' +def _merge_package(full_old, full_new): -def _merge_package(config, package_name, package_config): - config = config.copy() - package_merger.merge(config, package_config) - return config + def merge(old, new): + # pylint: disable=no-else-return + if isinstance(new, dict): + if not isinstance(old, dict): + return new + res = old.copy() + for k, v in new.items(): + res[k] = merge(old[k], v) if k in old else v + return res + elif isinstance(new, list): + if not isinstance(old, list): + return new + return old + new + return new -def _is_valid_package_name(value: str) -> bool: - if not value: - return False - if value[0].isdigit(): - return False - try: - cv.valid_name(value) - except cv.Invalid: - return False - return True + return merge(full_old, full_new) def do_packages_pass(config: dict): if CONF_PACKAGES not in config: - return + return config packages = config[CONF_PACKAGES] - temp_config = config.copy() with cv.prepend_path(CONF_PACKAGES): - if packages is not None and not isinstance(packages, dict): + if not isinstance(packages, dict): raise cv.Invalid("Packages must be a key to value mapping, got {} instead" "".format(type(packages))) + for package_name, package_config in packages.items(): with cv.prepend_path(package_name): - if not isinstance(package_config, dict): - raise cv.Invalid("Package definition must be a dictionary containing valid " - "esphome configuration to be merged with the main " - "config, got {} instead" - .format(type(package_config))) - if not _is_valid_package_name(package_name): - raise cv.Invalid("Package name is invalid. Valid name should consist of " - "letters, numbers and underscores. It shouldn't also " - "start with number") - temp_config = _merge_package(temp_config, package_name, package_config) - del temp_config[CONF_PACKAGES] - config.clear() - config.update(temp_config) + recursive_package = package_config + if isinstance(package_config, dict): + recursive_package = do_packages_pass(package_config) + config = _merge_package(config, recursive_package) + + del config[CONF_PACKAGES] + return config diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index ab99e040d4..a6f70fe928 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -3,11 +3,10 @@ import re import esphome.config_validation as cv from esphome import core +from esphome.const import CONF_SUBSTITUTIONS _LOGGER = logging.getLogger(__name__) -CONF_SUBSTITUTIONS = 'substitutions' - VALID_SUBSTITUTIONS_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' \ '0123456789_' diff --git a/esphome/config.py b/esphome/config.py index 6f3a498444..84d3ca3803 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -11,10 +11,8 @@ from contextlib import contextmanager import voluptuous as vol from esphome import core, core_config, yaml_util -from esphome.components import substitutions -from esphome.components.packages import do_packages_pass -from esphome.components.substitutions import CONF_SUBSTITUTIONS -from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS, CONF_PACKAGES +from esphome.const import CONF_ESPHOME, CONF_PLATFORM, ESP_PLATFORMS, CONF_PACKAGES, \ + CONF_SUBSTITUTIONS from esphome.core import CORE, EsphomeError # noqa from esphome.helpers import color, indent from esphome.util import safe_print, OrderedDict @@ -393,9 +391,10 @@ def validate_config(config, command_line_substitutions): # 0. Load packages if CONF_PACKAGES in config: + from esphome.components.packages import do_packages_pass result.add_output_path([CONF_PACKAGES], CONF_PACKAGES) try: - do_packages_pass(config) + config = do_packages_pass(config) except vol.Invalid as err: result.update(config) result.add_error(err) @@ -403,6 +402,7 @@ def validate_config(config, command_line_substitutions): # 1. Load substitutions if CONF_SUBSTITUTIONS in config: + from esphome.components import substitutions result[CONF_SUBSTITUTIONS] = {**config[CONF_SUBSTITUTIONS], **command_line_substitutions} result.add_output_path([CONF_SUBSTITUTIONS], CONF_SUBSTITUTIONS) try: diff --git a/esphome/const.py b/esphome/const.py index 45b5e5e209..202d2991b5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -467,6 +467,7 @@ CONF_STEP_PIN = 'step_pin' CONF_STOP = 'stop' CONF_STOP_ACTION = 'stop_action' CONF_SUBNET = 'subnet' +CONF_SUBSTITUTIONS = 'substitutions' CONF_SUPPORTS_COOL = 'supports_cool' CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SWING_BOTH_ACTION = 'swing_both_action' diff --git a/requirements.txt b/requirements.txt index 059a07fc83..2d03924cec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,3 @@ ifaddr==0.1.7 platformio==4.3.4 esptool==2.8 click==7.1.2 -deepmerge==0.1.0 From 0cc3902ffc438c40c01a99caed29f311b0e03b8e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 15:29:41 +0200 Subject: [PATCH 107/200] Add tasmota magic bits, Tasmota compat check (#1152) --- esphome/core/application.cpp | 4 ++++ esphome/core/esphal.cpp | 16 ++++++++++++++++ esphome/core/esphal.h | 8 ++++++++ 3 files changed, 28 insertions(+) diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 4ecb247ec3..5e23c6250b 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -1,6 +1,7 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "esphome/core/version.h" +#include "esphome/core/esphal.h" #ifdef USE_STATUS_LED #include "esphome/components/status_led/status_led.h" @@ -58,6 +59,9 @@ void Application::setup() { ESP_LOGI(TAG, "setup() finished successfully!"); this->schedule_dump_config(); this->calculate_looping_components_(); + + // Dummy function to link some symbols into the binary. + force_link_symbols(); } void Application::loop() { uint32_t new_app_state = 0; diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index 2389a2a7f6..90707016c3 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -271,6 +271,22 @@ ISRInternalGPIOPin *GPIOPin::to_isr() const { this->gpio_read_, this->gpio_mask_, this->inverted_); } +void force_link_symbols() { +#ifdef ARDUINO_ARCH_ESP8266 + // Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible + // with their settings - ESPHome uses a different settings system (that can also survive + // erases). So set magic bytes indicating all tasmota versions are supported. + // This only adds 12 bytes of binary size, which is an acceptable price to pay for easier support + // for Tasmota. + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/settings.ino#L346-L380 + // https://github.com/arendst/Tasmota/blob/b05301b1497942167a015a6113b7f424e42942cd/tasmota/i18n.h#L652-L654 + const static uint32_t TASMOTA_MAGIC_BYTES[] PROGMEM = {0x5AA55AA5, 0xFFFFFFFF, 0xA55AA55A}; + // Force link symbol by using a volatile integer (GCC attribute used does not work because of LTO) + volatile int x = 0; + x = TASMOTA_MAGIC_BYTES[x]; +#endif +} + } // namespace esphome #ifdef ARDUINO_ESP8266_RELEASE_2_3_0 diff --git a/esphome/core/esphal.h b/esphome/core/esphal.h index 453e094acc..809c7d91b5 100644 --- a/esphome/core/esphal.h +++ b/esphome/core/esphal.h @@ -116,4 +116,12 @@ class GPIOPin { template void GPIOPin::attach_interrupt(void (*func)(T *), T *arg, int mode) const { this->attach_interrupt_(reinterpret_cast(func), arg, mode); } +/** This function can be used by the HAL to force-link specific symbols + * into the generated binary without modifying the linker script. + * + * It is called by the application very early on startup and should not be used for anything + * other than forcing symbols to be linked. + */ +void force_link_symbols(); + } // namespace esphome From d9c33f19e2c66c2205bded238bb98180d266c4f5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 15:30:32 +0200 Subject: [PATCH 108/200] Fix executable bits on some hassio files (#1151) --- docker/rootfs/etc/cont-init.d/30-esphome.sh | 0 docker/rootfs/etc/cont-init.d/40-migrate.sh | 0 docker/rootfs/etc/nginx/nginx.conf | 0 3 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 docker/rootfs/etc/cont-init.d/30-esphome.sh mode change 100644 => 100755 docker/rootfs/etc/cont-init.d/40-migrate.sh mode change 100755 => 100644 docker/rootfs/etc/nginx/nginx.conf diff --git a/docker/rootfs/etc/cont-init.d/30-esphome.sh b/docker/rootfs/etc/cont-init.d/30-esphome.sh old mode 100644 new mode 100755 diff --git a/docker/rootfs/etc/cont-init.d/40-migrate.sh b/docker/rootfs/etc/cont-init.d/40-migrate.sh old mode 100644 new mode 100755 diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf old mode 100755 new mode 100644 From 8166d0de79e89d06190b2f30553696dc875c8f39 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 21:42:12 +0200 Subject: [PATCH 109/200] Remove i386 arch See also https://github.com/esphome/esphome-docker-base/issues/5 --- .github/workflows/release-dev.yml | 10 ++++------ .github/workflows/release.yml | 13 +++++-------- docker/Dockerfile | 2 +- docker/Dockerfile.dev | 2 +- docker/Dockerfile.lint | 2 +- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 59beeea212..57ec71845b 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -176,7 +176,7 @@ jobs: needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: matrix: - arch: [amd64, i386, armv7, aarch64] + arch: [amd64, armv7, aarch64] # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly build_type: ["docker"] steps: @@ -187,7 +187,7 @@ jobs: echo "::set-env name=TAG::${TAG}" - name: Set up env variables run: | - base_version="2.3.3" + base_version="2.3.4" if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" @@ -252,13 +252,11 @@ jobs: docker manifest create esphome/esphome:${TAG} \ esphome/esphome-aarch64:${TAG} \ esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} \ - esphome/esphome-i386:${TAG} + esphome/esphome-armv7:${TAG} docker manifest push esphome/esphome:${TAG} docker manifest create esphome/esphome:dev \ esphome/esphome-aarch64:${TAG} \ esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} \ - esphome/esphome-i386:${TAG} + esphome/esphome-armv7:${TAG} docker manifest push esphome/esphome:dev diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84610156a1..8196d39fbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -196,7 +196,7 @@ jobs: needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: matrix: - arch: [amd64, i386, armv7, aarch64] + arch: [amd64, armv7, aarch64] build_type: ["hassio", "docker"] steps: - uses: actions/checkout@v2 @@ -206,7 +206,7 @@ jobs: echo "::set-env name=TAG::${TAG}" - name: Set up env variables run: | - base_version="2.3.3" + base_version="2.3.4" if [[ "${{ matrix.build_type }}" == "hassio" ]]; then build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" @@ -280,8 +280,7 @@ jobs: docker manifest create esphome/esphome:${TAG} \ esphome/esphome-aarch64:${TAG} \ esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} \ - esphome/esphome-i386:${TAG} + esphome/esphome-armv7:${TAG} docker manifest push esphome/esphome:${TAG} - name: Publish docker beta tag @@ -289,8 +288,7 @@ jobs: docker manifest create esphome/esphome:beta \ esphome/esphome-aarch64:${TAG} \ esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} \ - esphome/esphome-i386:${TAG} + esphome/esphome-armv7:${TAG} docker manifest push esphome/esphome:beta - name: Publish docker latest tag @@ -299,6 +297,5 @@ jobs: docker manifest create esphome/esphome:latest \ esphome/esphome-aarch64:${TAG} \ esphome/esphome-amd64:${TAG} \ - esphome/esphome-armv7:${TAG} \ - esphome/esphome-i386:${TAG} + esphome/esphome-armv7:${TAG} docker manifest push esphome/esphome:latest diff --git a/docker/Dockerfile b/docker/Dockerfile index d0b4f2ffa1..1a8382aa65 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -ARG BUILD_FROM=esphome/esphome-base-amd64:2.3.3 +ARG BUILD_FROM=esphome/esphome-base-amd64:2.3.4 FROM ${BUILD_FROM} # First install requirements to leverage caching when requirements don't change diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index b6a0f8f90b..9e7ddc4b53 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -1,4 +1,4 @@ -FROM esphome/esphome-base-amd64:2.3.3 +FROM esphome/esphome-base-amd64:2.3.4 COPY . . diff --git a/docker/Dockerfile.lint b/docker/Dockerfile.lint index bc7caa7e97..fd1a3c5330 100644 --- a/docker/Dockerfile.lint +++ b/docker/Dockerfile.lint @@ -1,4 +1,4 @@ -FROM esphome/esphome-lint-base:2.3.3 +FROM esphome/esphome-lint-base:2.3.4 COPY requirements.txt requirements_test.txt / RUN pip3 install --no-cache-dir -r /requirements.txt -r /requirements_test.txt From e962762046ec4021546ed6eb14ad641626bb06e3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 21:42:34 +0200 Subject: [PATCH 110/200] Docker --cache-from only uses first image See also https://stackoverflow.com/a/56024061/8924614 --- .github/workflows/release-dev.yml | 6 +----- .github/workflows/release.yml | 8 ++------ 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 57ec71845b..9544b24a5c 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -158,7 +158,7 @@ jobs: run: script/setup - name: Install Github Actions annotator run: pip install pytest-github-actions-annotate-failures - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/python.json" @@ -204,8 +204,6 @@ jobs: echo "::set-env name=DOCKERFILE::${dockerfile}" - name: Pull for cache run: | - docker pull "${BUILD_TO}:latest" || true - docker pull "${BUILD_TO}:beta" || true docker pull "${BUILD_TO}:dev" || true - name: Register QEMU binfmt run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes @@ -215,8 +213,6 @@ jobs: --build-arg "BUILD_VERSION=${TAG}" \ --tag "${BUILD_TO}:${TAG}" \ --tag "${BUILD_TO}:dev" \ - --cache-from "${BUILD_TO}:latest" \ - --cache-from "${BUILD_TO}:beta" \ --cache-from "${BUILD_TO}:dev" \ --file "${DOCKERFILE}" \ . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8196d39fbc..569a3adf57 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -136,7 +136,7 @@ jobs: echo "::add-matcher::.github/workflows/matchers/gcc.json" echo "::add-matcher::.github/workflows/matchers/python.json" - run: esphome tests/${{ matrix.test }}.yaml compile - + pytest: runs-on: ubuntu-latest steps: @@ -156,7 +156,7 @@ jobs: run: script/setup - name: Install Github Actions annotator run: pip install pytest-github-actions-annotate-failures - + - name: Register problem matchers run: | echo "::add-matcher::.github/workflows/matchers/python.json" @@ -225,8 +225,6 @@ jobs: - name: Pull for cache run: | docker pull "${BUILD_TO}:latest" || true - docker pull "${BUILD_TO}:beta" || true - docker pull "${BUILD_TO}:dev" || true - name: Register QEMU binfmt run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes - run: | @@ -235,8 +233,6 @@ jobs: --build-arg "BUILD_VERSION=${TAG}" \ --tag "${BUILD_TO}:${TAG}" \ --cache-from "${BUILD_TO}:latest" \ - --cache-from "${BUILD_TO}:beta" \ - --cache-from "${BUILD_TO}:dev" \ --file "${DOCKERFILE}" \ . - name: Log in to docker hub From dfbf225403dcf08a44a2f45a901c1e0191c981ee Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 16 Jul 2020 07:59:11 +1200 Subject: [PATCH 111/200] Don't run deploy job when repository is not esphome/esphome (#1157) --- .github/workflows/release-dev.yml | 2 ++ .github/workflows/release.yml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 9544b24a5c..7187848217 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -172,6 +172,7 @@ jobs: deploy-docker: name: Build and publish docker containers + if: github.repository == "esphome/esphome" runs-on: ubuntu-latest needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: @@ -227,6 +228,7 @@ jobs: deploy-docker-manifest: + if: github.repository == "esphome/esphome" runs-on: ubuntu-latest needs: [deploy-docker] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 569a3adf57..955af16b14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,6 +170,7 @@ jobs: deploy-pypi: name: Build and publish to PyPi + if: github.repository == "esphome/esphome" needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] runs-on: ubuntu-latest steps: @@ -192,6 +193,7 @@ jobs: deploy-docker: name: Build and publish docker containers + if: github.repository == "esphome/esphome" runs-on: ubuntu-latest needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: @@ -255,6 +257,7 @@ jobs: docker push "${BUILD_TO}:latest" deploy-docker-manifest: + if: github.repository == "esphome/esphome" runs-on: ubuntu-latest needs: [deploy-docker] steps: From d58d0e89c78d44c8445da518d14b16e0d37dd628 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 22:00:53 +0200 Subject: [PATCH 112/200] Add Docker CI workflow that runs when docker files change --- .github/workflows/ci-docker.yml | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/ci-docker.yml diff --git a/.github/workflows/ci-docker.yml b/.github/workflows/ci-docker.yml new file mode 100644 index 0000000000..df8185281e --- /dev/null +++ b/.github/workflows/ci-docker.yml @@ -0,0 +1,54 @@ +name: CI for docker images + +# Only run when docker paths change +on: + push: + branches: [dev, beta, master] + paths: + - 'docker/**' + - '.github/workflows/**' + + pull_request: + paths: + - 'docker/**' + - '.github/workflows/**' + +jobs: + check-docker: + name: Build docker containers + runs-on: ubuntu-latest + strategy: + matrix: + arch: [amd64, armv7, aarch64] + build_type: ["hassio", "docker"] + steps: + - uses: actions/checkout@v2 + - name: Set up env variables + run: | + base_version="2.3.4" + + if [[ "${{ matrix.build_type }}" == "hassio" ]]; then + build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-hassio-${{ matrix.arch }}" + dockerfile="docker/Dockerfile.hassio" + else + build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" + build_to="esphome/esphome-${{ matrix.arch }}" + dockerfile="docker/Dockerfile" + fi + + echo "::set-env name=BUILD_FROM::${build_from}" + echo "::set-env name=BUILD_TO::${build_to}" + echo "::set-env name=DOCKERFILE::${dockerfile}" + - name: Pull for cache + run: | + docker pull "${BUILD_TO}:dev" || true + - name: Register QEMU binfmt + run: docker run --rm --privileged multiarch/qemu-user-static:5.0.0-2 --reset -p yes + - run: | + docker build \ + --build-arg "BUILD_FROM=${BUILD_FROM}" \ + --build-arg "BUILD_VERSION=ci" \ + --cache-from "${BUILD_TO}:dev" \ + --file "${DOCKERFILE}" \ + . From ef0e611e52a75981ffb493ce9a513c3aa63b1f6f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 22:05:00 +0200 Subject: [PATCH 113/200] GitHub Actions Expression Syntax Needs Single Quotes See also https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions\#literals --- .github/workflows/release-dev.yml | 4 ++-- .github/workflows/release.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 7187848217..896b6cddb4 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -172,7 +172,7 @@ jobs: deploy-docker: name: Build and publish docker containers - if: github.repository == "esphome/esphome" + if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: @@ -228,7 +228,7 @@ jobs: deploy-docker-manifest: - if: github.repository == "esphome/esphome" + if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest needs: [deploy-docker] steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 955af16b14..651064047a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -170,7 +170,7 @@ jobs: deploy-pypi: name: Build and publish to PyPi - if: github.repository == "esphome/esphome" + if: github.repository == 'esphome/esphome' needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] runs-on: ubuntu-latest steps: @@ -193,7 +193,7 @@ jobs: deploy-docker: name: Build and publish docker containers - if: github.repository == "esphome/esphome" + if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] strategy: @@ -257,7 +257,7 @@ jobs: docker push "${BUILD_TO}:latest" deploy-docker-manifest: - if: github.repository == "esphome/esphome" + if: github.repository == 'esphome/esphome' runs-on: ubuntu-latest needs: [deploy-docker] steps: From e069687477b370137d7e7cb8ef00121a475e2605 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 15 Jul 2020 22:13:14 +0200 Subject: [PATCH 114/200] Fix Dockerfile.hassio and improve caching Previous one was broken, see also https://github.com/esphome/esphome/runs/874896962 --- docker/Dockerfile.hassio | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile.hassio b/docker/Dockerfile.hassio index e5c9625680..eb7ef23001 100644 --- a/docker/Dockerfile.hassio +++ b/docker/Dockerfile.hassio @@ -1,11 +1,15 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +# First install requirements to leverage caching when requirements don't change +COPY requirements.txt / +RUN pip3 install --no-cache-dir -r /requirements.txt + # Copy root filesystem COPY docker/rootfs/ / -COPY setup.py setup.cfg MANIFEST.in /opt/esphome/ -COPY esphome /opt/esphome/esphome +# Then copy esphome and install +COPY . /opt/esphome/ RUN pip3 install --no-cache-dir -e /opt/esphome # Build arguments From 27e08d37eae4a0e433e208919db67139cc93c92c Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 15 Jul 2020 20:49:23 -0300 Subject: [PATCH 115/200] Add links for the issues --- .github/ISSUE_TEMPLATE/config.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..4add58dfbe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,12 @@ +blank_issues_enabled: false +contact_links: + - name: Issue Tracker + url: https://github.com/esphome/issues + about: Please create bug reports in the dedicated issue tracker. + - name: Feature Request Tracker + url: https://github.com/esphome/feature-requests + about: Please create feature requests in the dedicated feature request tracker. + - name: Frequently Asked Question + url: https://esphome.io/guides/faq.html + about: Please view the FAQ for common questions and what to include in a bug report. + From e88418a01b04719dc59ec1f55205c55ee861dd6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 08:28:41 +0200 Subject: [PATCH 116/200] Bump pytest-mock from 1.13.0 to 3.2.0 (#1159) Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.13.0 to 3.2.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v1.13.0...v3.2.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 81f4953975..2f4dd6f97b 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,6 +6,6 @@ pexpect # Unit tests pytest==5.4.3 pytest-cov==2.10.0 -pytest-mock==1.13.0 +pytest-mock==3.2.0 asyncmock==0.4.2 hypothesis==5.19.3 From 76a3e75bc7f06de40ae48a4ac8ae90cde24a1933 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 08:28:56 +0200 Subject: [PATCH 117/200] Bump pylint from 2.5.0 to 2.5.3 (#1160) Bumps [pylint](https://github.com/PyCQA/pylint) from 2.5.0 to 2.5.3. - [Release notes](https://github.com/PyCQA/pylint/releases) - [Changelog](https://github.com/PyCQA/pylint/blob/master/ChangeLog) - [Commits](https://github.com/PyCQA/pylint/compare/pylint-2.5.0...pylint-2.5.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2f4dd6f97b..1e1b886923 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -pylint==2.5.0 +pylint==2.5.3 flake8==3.7.9 pillow pexpect From ad913625716f80a1ab5025d04ead93cd50897021 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 08:29:11 +0200 Subject: [PATCH 118/200] Bump tzlocal from 2.0.0 to 2.1 (#1162) Bumps [tzlocal](https://github.com/regebro/tzlocal) from 2.0.0 to 2.1. - [Release notes](https://github.com/regebro/tzlocal/releases) - [Changelog](https://github.com/regebro/tzlocal/blob/master/CHANGES.txt) - [Commits](https://github.com/regebro/tzlocal/compare/2.0.0...2.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2d03924cec..a05da7cd01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ paho-mqtt==1.5.0 colorlog==4.1.0 tornado==6.0.4 protobuf==3.12.2 -tzlocal==2.0.0 +tzlocal==2.1 pytz==2020.1 pyserial==3.4 ifaddr==0.1.7 From 67b4dcf8ae58553030d06eb10ea36f91870b4bfe Mon Sep 17 00:00:00 2001 From: Peter Kuehne Date: Thu, 16 Jul 2020 08:50:06 +0100 Subject: [PATCH 119/200] Fix unit test warning for hypothesis deprecation (#1163) --- tests/unit_tests/test_core.py | 6 +++--- tests/unit_tests/test_helpers.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 166a12c40e..27b64ec3d5 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip4_addr_strings +from hypothesis.provisional import ip_addresses from strategies import mac_addr_strings from esphome import core, const @@ -24,7 +24,7 @@ class TestHexInt: class TestIPAddress: - @given(value=ip4_addr_strings()) + @given(value=ip_addresses(v=4).map(str)) def test_init__valid(self, value): core.IPAddress(*value.split(".")) @@ -33,7 +33,7 @@ class TestIPAddress: with pytest.raises(ValueError, match="IPAddress must consist of 4 items"): core.IPAddress(*value.split(".")) - @given(value=ip4_addr_strings()) + @given(value=ip_addresses(v=4).map(str)) def test_str(self, value): target = core.IPAddress(*value.split(".")) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index 3c0a96149d..6e89a05bc2 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip4_addr_strings +from hypothesis.provisional import ip_addresses from esphome import helpers @@ -75,7 +75,7 @@ def test_is_ip_address__invalid(host): assert actual is False -@given(value=ip4_addr_strings()) +@given(value=ip_addresses(v=4).map(str)) def test_is_ip_address__valid(value): actual = helpers.is_ip_address(value) From 2e54d3f98d5efe25bb31c2846c3495f6ff5b540e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 10:03:11 +0200 Subject: [PATCH 120/200] Bump flake8 from 3.7.9 to 3.8.3 (#1161) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- esphome/core.py | 5 ++++- requirements_test.txt | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/core.py b/esphome/core.py index 52904f0d0b..9b0b8cefa3 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -8,13 +8,16 @@ import os import re # pylint: disable=unused-import, wrong-import-order -from typing import Any, Dict, List, Optional, Set # noqa +from typing import Any, Dict, List, Optional, Set, TYPE_CHECKING # noqa from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio from esphome.util import OrderedDict +if TYPE_CHECKING: + from .cpp_generator import MockObj, MockObjClass, Statement + _LOGGER = logging.getLogger(__name__) diff --git a/requirements_test.txt b/requirements_test.txt index 1e1b886923..e8a4f6f1a0 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ pylint==2.5.3 -flake8==3.7.9 +flake8==3.8.3 pillow pexpect From 6f0bfb286a17129397f8a77203d361d61c72e7c0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 17 Jul 2020 14:25:43 +0200 Subject: [PATCH 121/200] Bump FastLED from 3.2.9 to 3.3.3 (#1164) Already updated in fastled_base in #1020, now also update it in platformio.ini --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index b3aec4c276..22fa74f18e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -14,7 +14,7 @@ lib_deps = AsyncMqttClient-esphome@0.8.4 ArduinoJson-esphomelib@5.13.3 ESPAsyncWebServer-esphome@1.2.6 - FastLED@3.2.9 + FastLED@3.3.3 NeoPixelBus-esphome@2.5.2 ESPAsyncTCP-esphome@1.2.2 1655@1.0.2 ; TinyGPSPlus (has name conflict) From 48f3dfe455ebbf10be3ea701a5461150b4dbbabe Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 17 Jul 2020 09:31:05 -0300 Subject: [PATCH 122/200] Arduino dev branch changed (#1139) --- esphome/const.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index 202d2991b5..3a3e8128c6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -11,14 +11,13 @@ ESP_PLATFORM_ESP8266 = 'ESP8266' ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] ALLOWED_NAME_CHARS = 'abcdefghijklmnopqrstuvwxyz0123456789_' -ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git#feature/stage' +ARDUINO_VERSION_ESP32_DEV = 'https://github.com/platformio/platform-espressif32.git' ARDUINO_VERSION_ESP32_1_0_0 = 'espressif32@1.5.0' ARDUINO_VERSION_ESP32_1_0_1 = 'espressif32@1.6.0' ARDUINO_VERSION_ESP32_1_0_2 = 'espressif32@1.9.0' ARDUINO_VERSION_ESP32_1_0_3 = 'espressif32@1.10.0' ARDUINO_VERSION_ESP32_1_0_4 = 'espressif32@1.11.0' -ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git#feature' \ - '/stage' +ARDUINO_VERSION_ESP8266_DEV = 'https://github.com/platformio/platform-espressif8266.git' ARDUINO_VERSION_ESP8266_2_5_0 = 'espressif8266@2.0.1' ARDUINO_VERSION_ESP8266_2_5_1 = 'espressif8266@2.1.0' ARDUINO_VERSION_ESP8266_2_5_2 = 'espressif8266@2.2.3' From 0afa41d08aaf20991bdf66bca0d6d6945ac81e30 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 17 Jul 2020 14:32:23 +0200 Subject: [PATCH 123/200] Add bump version script (#1153) --- script/bump-version.py | 93 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100755 script/bump-version.py diff --git a/script/bump-version.py b/script/bump-version.py new file mode 100755 index 0000000000..7895735844 --- /dev/null +++ b/script/bump-version.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +import argparse +import re +import subprocess +from dataclasses import dataclass +import sys + + +@dataclass +class Version: + major: int + minor: int + patch: int + beta: int = 0 + dev: bool = False + + def __str__(self): + return f'{self.major}.{self.minor}.{self.full_patch}' + + @property + def full_patch(self): + res = f'{self.patch}' + if self.beta > 0: + res += f'b{self.beta}' + if self.dev: + res += '-dev' + return res + + @classmethod + def parse(cls, value): + match = re.match(r'(\d+).(\d+).(\d+)(b\d+)?(-dev)?', value) + assert match is not None + major = int(match[1]) + minor = int(match[2]) + patch = int(match[3]) + beta = int(match[4][1:]) if match[4] else 0 + dev = bool(match[5]) + return Version( + major=major, minor=minor, patch=patch, + beta=beta, dev=dev + ) + + +def sub(path, pattern, repl, expected_count=1): + with open(path) as fh: + content = fh.read() + content, count = re.subn(pattern, repl, content, re.MULTILINE) + if expected_count is not None: + assert count == expected_count + with open(path, "wt") as fh: + fh.write(content) + + +def write_version(version: Version): + sub( + 'esphome/const.py', + r"^MAJOR_VERSION = \d+$", + f"MAJOR_VERSION = {version.major}" + ) + sub( + 'esphome/const.py', + r"^MINOR_VERSION = \d+$", + f"MINOR_VERSION = {version.minor}" + ) + sub( + 'esphome/const.py', + r"^PATCH_VERSION = .*$", + f"PATCH_VERSION = {version.full_patch}" + ) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('new_version', type=str) + parser.add_argument('--commit', action='store_true') + args = parser.parse_args() + + if args.commit and subprocess.call(["git", "diff", "--quiet"]) == 1: + print("Cannot use --commit because git is dirty.") + return 1 + + version = Version.parse(args.new_version) + print(f"Bumping to {version}") + write_version(version) + + if args.commit: + subprocess.check_call(["git", "commit", "-nam", f"Bump version to v{version}"]) + return 1 + + +if __name__ == "__main__": + sys.exit(main() or 0) From 6405799cc2135b658da159b161ee4a50792b775a Mon Sep 17 00:00:00 2001 From: Wilhelm Erasmus Date: Mon, 20 Jul 2020 23:09:04 +0200 Subject: [PATCH 124/200] fix(cover yaml validation): adds gate to coincide with Home Assistant (#1175) Co-authored-by: Wilhelm Erasmus --- esphome/components/cover/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index ed6ebe98fa..85f7f746ba 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -11,7 +11,7 @@ IS_PLATFORM_COMPONENT = True DEVICE_CLASSES = [ '', 'awning', 'blind', 'curtain', 'damper', 'door', 'garage', - 'shade', 'shutter', 'window' + 'gate', 'shade', 'shutter', 'window' ] cover_ns = cg.esphome_ns.namespace('cover') From 10e411f8c1ae989dadc69a30626d9fdbb5ffaa46 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Tue, 21 Jul 2020 23:26:21 +0300 Subject: [PATCH 125/200] http_request fix urls caching (#1174) * Fix cache * CI Fix Co-authored-by: Nikolay Vasilchuk --- esphome/components/http_request/http_request.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/http_request/http_request.cpp b/esphome/components/http_request/http_request.cpp index 390867c948..46b0910b5e 100644 --- a/esphome/components/http_request/http_request.cpp +++ b/esphome/components/http_request/http_request.cpp @@ -15,15 +15,15 @@ void HttpRequestComponent::dump_config() { void HttpRequestComponent::send() { bool begin_status = false; this->client_.setReuse(true); - static const String URL = this->url_.c_str(); + const String url = this->url_.c_str(); #ifdef ARDUINO_ARCH_ESP32 - begin_status = this->client_.begin(URL); + begin_status = this->client_.begin(url); #endif #ifdef ARDUINO_ARCH_ESP8266 #ifndef CLANG_TIDY this->client_.setFollowRedirects(true); this->client_.setRedirectLimit(3); - begin_status = this->client_.begin(*this->get_wifi_client_(), URL); + begin_status = this->client_.begin(*this->get_wifi_client_(), url); #endif #endif From f29622abe1318a33c987060a0e14f6a8e2594369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Ferrer=20Garc=C3=ADa?= Date: Thu, 23 Jul 2020 15:27:22 +0200 Subject: [PATCH 126/200] Fix rf_bridge send and receive (#1180) * Fix rf_bridge send and receive * rf_bridge clang-format changes * rf_bridge pased data bug fix * rf_bridge logvv included for parsed data --- esphome/components/rf_bridge/rf_bridge.cpp | 88 ++++++++++++++-------- esphome/components/rf_bridge/rf_bridge.h | 6 +- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index f1537cdc87..42689ecf82 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -15,59 +15,74 @@ void RFBridgeComponent::ack_() { this->flush(); } -void RFBridgeComponent::decode_() { - uint8_t action = uartbuf_[0]; - RFBridgeData data{}; +bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + const uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: Start + if (at == 0) + return byte == RF_CODE_START; + + // Byte 1: Action + if (at == 1) + return byte >= RF_CODE_ACK && byte <= RF_CODE_RFOUT; + uint8_t action = raw[1]; switch (action) { case RF_CODE_ACK: ESP_LOGD(TAG, "Action OK"); break; case RF_CODE_LEARN_KO: - this->ack_(); - ESP_LOGD(TAG, "Learn timeout"); + ESP_LOGD(TAG, "Learning timeout"); break; case RF_CODE_LEARN_OK: - ESP_LOGD(TAG, "Learn started"); case RF_CODE_RFIN: - this->ack_(); + if (at < RF_MESSAGE_SIZE + 2) + return true; - data.sync = (uartbuf_[1] << 8) | uartbuf_[2]; - data.low = (uartbuf_[3] << 8) | uartbuf_[4]; - data.high = (uartbuf_[5] << 8) | uartbuf_[6]; - data.code = (uartbuf_[7] << 16) | (uartbuf_[8] << 8) | uartbuf_[9]; + RFBridgeData data; + data.sync = (raw[2] << 8) | raw[3]; + data.low = (raw[4] << 8) | raw[5]; + data.high = (raw[6] << 8) | raw[7]; + data.code = (raw[8] << 16) | (raw[9] << 8) | raw[10]; + + if (action == RF_CODE_LEARN_OK) + ESP_LOGD(TAG, "Learning success"); ESP_LOGD(TAG, "Received RFBridge Code: sync=0x%04X low=0x%04X high=0x%04X code=0x%06X", data.sync, data.low, data.high, data.code); this->callback_.call(data); break; default: - ESP_LOGD(TAG, "Unknown action: 0x%02X", action); + ESP_LOGW(TAG, "Unknown action: 0x%02X", action); break; } - this->last_ = millis(); + + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + + if (byte == RF_CODE_STOP && action != RF_CODE_ACK) + this->ack_(); + + // return false to reset buffer + return false; } void RFBridgeComponent::loop() { - bool receiving = false; - if (this->last_ != 0 && millis() - this->last_ > RF_DEBOUNCE) { - this->last_ = 0; + const uint32_t now = millis(); + if (now - this->last_bridge_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_bridge_byte_ = now; } while (this->available()) { - uint8_t c = this->read(); - if (receiving) { - if (c == RF_CODE_STOP && (this->uartpos_ == 1 || this->uartpos_ == RF_MESSAGE_SIZE + 1)) { - this->decode_(); - receiving = false; - } else if (this->uartpos_ <= RF_MESSAGE_SIZE) { - this->uartbuf_[uartpos_++] = c; - } else { - receiving = false; - } - } else if (c == RF_CODE_START) { - this->uartpos_ = 0; - receiving = true; + uint8_t byte; + this->read_byte(&byte); + if (this->parse_bridge_byte_(byte)) { + ESP_LOGVV(TAG, "Parsed: 0x%02X", byte); + this->last_bridge_byte_ = now; + } else { + this->rx_buffer_.clear(); } } } @@ -77,11 +92,17 @@ void RFBridgeComponent::send_code(RFBridgeData data) { data.code); this->write(RF_CODE_START); this->write(RF_CODE_RFOUT); - this->write(data.sync); - this->write(data.low); - this->write(data.high); - this->write(data.code); + this->write((data.sync >> 8) & 0xFF); + this->write(data.sync & 0xFF); + this->write((data.low >> 8) & 0xFF); + this->write(data.low & 0xFF); + this->write((data.high >> 8) & 0xFF); + this->write(data.high & 0xFF); + this->write((data.code >> 16) & 0xFF); + this->write((data.code >> 8) & 0xFF); + this->write(data.code & 0xFF); this->write(RF_CODE_STOP); + this->flush(); } void RFBridgeComponent::learn() { @@ -89,6 +110,7 @@ void RFBridgeComponent::learn() { this->write(RF_CODE_START); this->write(RF_CODE_LEARN); this->write(RF_CODE_STOP); + this->flush(); } void RFBridgeComponent::dump_config() { diff --git a/esphome/components/rf_bridge/rf_bridge.h b/esphome/components/rf_bridge/rf_bridge.h index 86713b8a5c..7ae84a032f 100644 --- a/esphome/components/rf_bridge/rf_bridge.h +++ b/esphome/components/rf_bridge/rf_bridge.h @@ -45,10 +45,10 @@ class RFBridgeComponent : public uart::UARTDevice, public Component { protected: void ack_(); void decode_(); + bool parse_bridge_byte_(uint8_t byte); - unsigned long last_ = 0; - unsigned char uartbuf_[RF_MESSAGE_SIZE + 3] = {0}; - unsigned char uartpos_ = 0; + std::vector rx_buffer_; + uint32_t last_bridge_byte_{0}; CallbackManager callback_; }; From 11c1fe88278cc54ee01f8df7f0acab809c565a26 Mon Sep 17 00:00:00 2001 From: Troon Date: Thu, 23 Jul 2020 14:27:44 +0100 Subject: [PATCH 127/200] mdi:timer icon replaced with mdi:timer-outline (#1181) Aligned to the MDI changelog here: https://dev.materialdesignicons.com/upgrade#4.9.95-to-5.0.45 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 3a3e8128c6..b37d816256 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -583,7 +583,7 @@ ICON_SIGNAL = 'mdi:signal-distance-variant' ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal' ICON_THERMOMETER = 'mdi:thermometer' ICON_TIMELAPSE = 'mdi:timelapse' -ICON_TIMER = 'mdi:timer' +ICON_TIMER = 'mdi:timer-outline' ICON_WATER_PERCENT = 'mdi:water-percent' ICON_WEATHER_SUNSET = 'mdi:weather-sunset' ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down' From df5d03b0bc73798e05aa3f95e57205180c29fd12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2020 23:25:23 +0200 Subject: [PATCH 128/200] Bump hypothesis from 5.19.3 to 5.20.3 (#1176) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 5.19.3 to 5.20.3. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-5.19.3...hypothesis-python-5.20.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index e8a4f6f1a0..2b7c6c5b1d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,4 +8,4 @@ pytest==5.4.3 pytest-cov==2.10.0 pytest-mock==3.2.0 asyncmock==0.4.2 -hypothesis==5.19.3 +hypothesis==5.20.3 From 1b2de953d0d21785f2ea6393635214fdbea4433c Mon Sep 17 00:00:00 2001 From: dr-oblivium <52540467+dr-oblivium@users.noreply.github.com> Date: Thu, 23 Jul 2020 23:40:30 +0200 Subject: [PATCH 129/200] Fix Home Assistant API disconnects when using st7789v display. (#1179) --- esphome/components/st7789v/display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/st7789v/display.py b/esphome/components/st7789v/display.py index 4723daf0e4..36e5acaa72 100644 --- a/esphome/components/st7789v/display.py +++ b/esphome/components/st7789v/display.py @@ -19,7 +19,7 @@ CONFIG_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_CS_PIN): pins.gpio_output_pin_schema, cv.Required(CONF_BACKLIGHT_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_BRIGHTNESS, default=1.0): cv.percentage, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()) +}).extend(cv.polling_component_schema('5s')).extend(spi.spi_device_schema()) def to_code(config): From a5e1f8fe19cf3befca4cd719295d63f95adc556a Mon Sep 17 00:00:00 2001 From: Frank Bakker Date: Thu, 23 Jul 2020 23:40:40 +0200 Subject: [PATCH 130/200] Fixed type mismatch between result_ field and preference of integration sensor (#1178) --- esphome/components/integration/integration_sensor.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index f9b5a43870..806c0ce567 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -10,7 +10,9 @@ 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_); + float preference_value = 0; + this->rtc_.load(&preference_value); + this->result_ = preference_value; } this->last_update_ = millis(); From ebbfab608c3f21feea0f9eb7f6b6a6c84d4357ae Mon Sep 17 00:00:00 2001 From: Peter Kuehne Date: Thu, 23 Jul 2020 22:51:14 +0100 Subject: [PATCH 131/200] Feature/wizard tests (#1167) --- .gitignore | 1 + esphome/wizard.py | 7 +- tests/unit_tests/test_wizard.py | 389 ++++++++++++++++++++++++++++++++ 3 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 tests/unit_tests/test_wizard.py diff --git a/.gitignore b/.gitignore index 2de9dd40e5..11a80a647c 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ htmlcov/ .esphome nosetests.xml coverage.xml +cov.xml *.cover .hypothesis/ .pytest_cache/ diff --git a/esphome/wizard.py b/esphome/wizard.py index b00f6d2b01..b1a0b17072 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -142,7 +142,7 @@ def wizard(path): if os.path.exists(path): safe_print("Uh oh, it seems like {} already exists, please delete that file first " "or chose another configuration file.".format(color('cyan', path))) - return 1 + return 2 safe_print("Hi there!") sleep(1.5) safe_print("I'm the wizard of ESPHome :)") @@ -162,14 +162,15 @@ def wizard(path): safe_print() sleep(1) name = input(color("bold_white", "(name): ")) + while True: try: name = cv.valid_name(name) break except vol.Invalid: safe_print(color("red", "Oh noes, \"{}\" isn't a valid name. Names can only include " - "numbers, letters and underscores.".format(name))) - name = strip_accents(name).replace(' ', '_') + "numbers, lower-case letters and underscores.".format(name))) + name = strip_accents(name).lower().replace(' ', '_') name = ''.join(c for c in name if c in cv.ALLOWED_NAME_CHARS) safe_print("Shall I use \"{}\" as the name instead?".format(color('cyan', name))) sleep(0.5) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py new file mode 100644 index 0000000000..2baff80edd --- /dev/null +++ b/tests/unit_tests/test_wizard.py @@ -0,0 +1,389 @@ +""" Tests for the wizard.py file """ + +import esphome.wizard as wz +import pytest +from esphome.pins import ESP8266_BOARD_PINS +from mock import MagicMock + + +@pytest.fixture +def default_config(): + return { + "name": "test_name", + "platform": "test_platform", + "board": "test_board", + "ssid": "test_ssid", + "psk": "test_psk", + "password": "" + } + + +@pytest.fixture +def wizard_answers(): + return [ + "test_node", # Name of the node + "ESP8266", # platform + "nodemcuv2", # board + "SSID", # ssid + "psk", # wifi password + "ota_pass", # ota password + ] + + +def test_sanitize_quotes_replaces_with_escaped_char(): + """ + The sanitize_quotes function should replace double quotes with their escaped equivalents + """ + # Given + input_str = "\"key\": \"value\"" + + # When + output_str = wz.sanitize_double_quotes(input_str) + + # Then + assert output_str == "\\\"key\\\": \\\"value\\\"" + + +def test_config_file_fallback_ap_includes_descriptive_name(default_config): + """ + The fallback AP should include the node and a descriptive name + """ + # Given + default_config["name"] = "test_node" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert f"ssid: \"Test Node Fallback Hotspot\"" in config + + +def test_config_file_fallback_ap_name_less_than_32_chars(default_config): + """ + The fallback AP name must be less than 32 chars. + Since it is composed of the node name and "Fallback Hotspot" this can be too long and needs truncating + """ + # Given + default_config["name"] = "a_very_long_name_for_this_node" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert f"ssid: \"A Very Long Name For This Node\"" in config + + +def test_config_file_should_include_ota(default_config): + """ + The Over-The-Air update should be enabled by default + """ + # Given + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "ota:" in config + + +def test_config_file_should_include_ota_when_password_set(default_config): + """ + The Over-The-Air update should be enabled when a password is set + """ + # Given + default_config["password"] = "foo" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "ota:" in config + + +def test_wizard_write_sets_platform(default_config, tmp_path, monkeypatch): + """ + If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards + """ + # Given + monkeypatch.setattr(wz, "write_file", MagicMock()) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert f"platform: {default_config['platform']}" in generated_config + + +def test_wizard_write_defaults_platform_from_board_esp8266(default_config, tmp_path, monkeypatch): + """ + If the platform is not explicitly set, use "ESP8266" if the board is one of the ESP8266 boards + """ + # Given + del default_config["platform"] + default_config["board"] = [*ESP8266_BOARD_PINS][0] + + monkeypatch.setattr(wz, "write_file", MagicMock()) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "platform: ESP8266" in generated_config + + +def test_wizard_write_defaults_platform_from_board_esp32(default_config, tmp_path, monkeypatch): + """ + If the platform is not explicitly set, use "ESP32" if the board is not one of the ESP8266 boards + """ + # Given + del default_config["platform"] + default_config["board"] = "foo" + + monkeypatch.setattr(wz, "write_file", MagicMock()) + + # When + wz.wizard_write(tmp_path, **default_config) + + # Then + generated_config = wz.write_file.call_args.args[1] + assert "platform: ESP32" in generated_config + + +def test_safe_print_step_prints_step_number_and_description(monkeypatch): + """ + The safe_print_step function prints the step number and the passed description + """ + # Given + monkeypatch.setattr(wz, "safe_print", MagicMock()) + monkeypatch.setattr(wz, "sleep", lambda time: 0) + + step_num = 22 + step_desc = "foobartest" + + # When + wz.safe_print_step(step_num, step_desc) + + # Then + # Collect arguments to all safe_print() calls (substituting "" for any empty ones) + all_args = [call.args[0] if len(call.args) else "" for call in wz.safe_print.call_args_list] + + assert any(step_desc == arg for arg in all_args) + assert any(f"STEP {step_num}" in arg for arg in all_args) + + +def test_default_input_uses_default_if_no_input_supplied(monkeypatch): + """ + The default_input() function should return the supplied default value if the user doesn't enter anything + """ + + # Given + monkeypatch.setattr("builtins.input", lambda _: "") + default_string = "foobar" + + # When + retval = wz.default_input("", default_string) + + # Then + assert retval == default_string + + +def test_default_input_uses_user_supplied_value(monkeypatch): + """ + The default_input() function should return the value that the user enters + """ + + # Given + user_input = "A value" + monkeypatch.setattr("builtins.input", lambda _: user_input) + default_string = "foobar" + + # When + retval = wz.default_input("", default_string) + + # Then + assert retval == user_input + + +def test_strip_accents_removes_diacritics(): + """ + The strip_accents() function should remove diacritics (umlauts) + """ + + # Given + input_str = u"Kühne" + expected_str = "Kuhne" + + # When + output_str = wz.strip_accents(input_str) + + # Then + assert output_str == expected_str + + +def test_wizard_rejects_path_with_invalid_extension(): + """ + The wizard should reject config files that are not yaml + """ + + # Given + config_file = "test.json" + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 1 + + +def test_wizard_rejects_existing_files(tmpdir): + """ + The wizard should reject any configuration file that already exists + """ + + # Given + config_file = tmpdir.join("test.yaml") + config_file.write("") + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 2 + + +def test_wizard_accepts_default_answers_esp8266(tmpdir, monkeypatch, wizard_answers): + """ + The wizard should accept the given default answers for esp8266 + """ + + # Given + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_accepts_default_answers_esp32(tmpdir, monkeypatch, wizard_answers): + """ + The wizard should accept the given default answers for esp32 + """ + + # Given + wizard_answers[1] = "ESP32" + wizard_answers[2] = "nodemcu-32s" + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): + """ + When the node name does not conform, a better alternative is offered + * Removes special chars + * Replaces spaces with underscores + * Converts all uppercase letters to lowercase + """ + + # Given + wizard_answers[0] = "Küche #2" + expected_name = "kuche_2" + monkeypatch.setattr(wz, "default_input", MagicMock(side_effect=lambda _, default: default)) + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + assert wz.default_input.call_args.args[1] == expected_name + + +def test_wizard_requires_correct_platform(tmpdir, monkeypatch, wizard_answers): + """ + When the platform is not either esp32 or esp8266, the wizard should reject it + """ + + # Given + wizard_answers.insert(1, "foobar") # add invalid entry for platform + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_requires_correct_board(tmpdir, monkeypatch, wizard_answers): + """ + When the board is not a valid esp8266 board, the wizard should reject it + """ + + # Given + wizard_answers.insert(2, "foobar") # add an invalid entry for board + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 + + +def test_wizard_requires_valid_ssid(tmpdir, monkeypatch, wizard_answers): + """ + When the board is not a valid esp8266 board, the wizard should reject it + """ + + # Given + wizard_answers.insert(3, "") # add an invalid entry for ssid + + config_file = tmpdir.join("test.yaml") + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + monkeypatch.setattr(wz, "wizard_write", MagicMock()) + + # When + retval = wz.wizard(str(config_file)) + + # Then + assert retval == 0 From e8272759be5f0ff54529a38421093c52d849867b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:09:43 +0200 Subject: [PATCH 132/200] Try to fix serial ports listing (#1155) --- esphome/__main__.py | 20 ++++---------------- esphome/dashboard/dashboard.py | 10 +++++----- esphome/util.py | 34 +++++++++++++++++++++++++++++++++- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index a961df8ada..79488e9b55 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -12,24 +12,12 @@ from esphome.const import CONF_BAUD_RATE, CONF_BROKER, CONF_LOGGER, CONF_OTA, \ CONF_PASSWORD, CONF_PORT, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority from esphome.helpers import color, indent -from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files +from esphome.util import run_external_command, run_external_process, safe_print, list_yaml_files, \ + get_serial_ports _LOGGER = logging.getLogger(__name__) -def get_serial_ports(): - # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py - from serial.tools.list_ports import comports - result = [] - for port, desc, info in comports(include_links=True): - if not port: - continue - if "VID:PID" in info: - result.append((port, desc)) - result.sort(key=lambda x: x[0]) - return result - - def choose_prompt(options): if not options: raise EsphomeError("Found no valid options for upload/logging, please make sure relevant " @@ -60,8 +48,8 @@ def choose_prompt(options): def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): options = [] - for res, desc in get_serial_ports(): - options.append((f"{res} ({desc})", res)) + for port in get_serial_ports(): + options.append((f"{port.path} ({port.description})", port.path)) if (show_ota and 'ota' in CORE.config) or (show_api and 'api' in CORE.config): options.append((f"Over The Air ({CORE.address})", CORE.address)) if default == 'OTA': diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index c4eb0eb266..1b150e008d 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -26,11 +26,10 @@ import tornado.web import tornado.websocket from esphome import const, util -from esphome.__main__ import get_serial_ports from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path -from esphome.util import shlex_quote +from esphome.util import shlex_quote, get_serial_ports # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa @@ -313,14 +312,15 @@ class SerialPortRequestHandler(BaseHandler): def get(self): ports = get_serial_ports() data = [] - for port, desc in ports: - if port == '/dev/ttyAMA0': + for port in ports: + desc = port.description + if port.path == '/dev/ttyAMA0': desc = 'UART pins on GPIO header' split_desc = desc.split(' - ') if len(split_desc) == 2 and split_desc[0] == split_desc[1]: # Some serial ports repeat their values desc = split_desc[0] - data.append({'port': port, 'desc': desc}) + data.append({'port': port.path, 'desc': desc}) data.append({'port': 'OTA', 'desc': 'Over-The-Air'}) data.sort(key=lambda x: x['port'], reverse=True) self.write(json.dumps(data)) diff --git a/esphome/util.py b/esphome/util.py index de6736096c..3fe7ba8eb8 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, List import collections import io @@ -7,6 +7,7 @@ import os import re import subprocess import sys +from pathlib import Path from esphome import const @@ -256,3 +257,34 @@ def filter_yaml_files(files): files = [f for f in files if os.path.basename(f) != 'secrets.yaml'] files = [f for f in files if not os.path.basename(f).startswith('.')] return files + + +class SerialPort: + def __init__(self, path: str, description: str): + self.path = path + self.description = description + + +# from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py +def get_serial_ports() -> List[SerialPort]: + from serial.tools.list_ports import comports + result = [] + for port, desc, info in comports(include_links=True): + if not port: + continue + if "VID:PID" in info: + result.append(SerialPort(path=port, description=desc)) + # Also add objects in /dev/serial/by-id/ + # ref: https://github.com/esphome/issues/issues/1346 + + by_id_path = Path('/dev/serial/by-id') + if sys.platform.lower().startswith('linux') and by_id_path.exists(): + from serial.tools.list_ports_linux import SysFS + + for path in by_id_path.glob('*'): + device = SysFS(path) + if device.subsystem == 'platform': + result.append(SerialPort(path=str(path), description=info[1])) + + result.sort(key=lambda x: x.path) + return result From 2e1d14b8b140dc44f82eb42ede2a217a795ade72 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:10:40 +0200 Subject: [PATCH 133/200] Bump NeoPixelBus from 2.5.2 to 2.5.7 (#1165) --- esphome/components/neopixelbus/light.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 2b84882e59..32bfd951e9 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -181,4 +181,4 @@ def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) # https://github.com/Makuna/NeoPixelBus/blob/master/library.json - cg.add_library('NeoPixelBus-esphome', '2.5.2') + cg.add_library('NeoPixelBus-esphome', '2.5.7') diff --git a/platformio.ini b/platformio.ini index 22fa74f18e..0d078b7b35 100644 --- a/platformio.ini +++ b/platformio.ini @@ -15,7 +15,7 @@ lib_deps = ArduinoJson-esphomelib@5.13.3 ESPAsyncWebServer-esphome@1.2.6 FastLED@3.3.3 - NeoPixelBus-esphome@2.5.2 + NeoPixelBus-esphome@2.5.7 ESPAsyncTCP-esphome@1.2.2 1655@1.0.2 ; TinyGPSPlus (has name conflict) 6865@1.0.0 ; TM1651 Battery Display From facd4f63eca03406461b8015683277c89f3a2c45 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:13:08 +0200 Subject: [PATCH 134/200] Ignore hpyothesis updates --- .github/dependabot.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index b38df29f46..ec4bf087cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,7 @@ updates: directory: "/" schedule: interval: "daily" + ignored_updates: + # Hypotehsis is only used for testing and is updated quite often + - match: + dependency_name: hypothesis From 725e8c69f51aab2aac0d4439bd65d852410bf1dc Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:16:34 +0200 Subject: [PATCH 135/200] Add long description to setup.py --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 06151bf1e5..8de5bf6381 100755 --- a/setup.py +++ b/setup.py @@ -28,6 +28,9 @@ here = os.path.abspath(os.path.dirname(__file__)) with open(os.path.join(here, 'requirements.txt')) as requirements_txt: REQUIRES = requirements_txt.read().splitlines() +with open(os.path.join(here, 'README.md')) as readme: + LONG_DESCRIPTION = readme.read() + # If you have problems importing platformio and esptool as modules you can set # $ESPHOME_USE_SUBPROCESS to make ESPHome call their executables instead. # This means they have to be in your $PATH. @@ -57,6 +60,8 @@ setup( author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, description="Make creating custom firmwares for ESP32/ESP8266 super easy.", + long_description=LONG_DESCRIPTION, + long_description_content_type='text/markdown', include_package_data=True, zip_safe=False, platforms='any', From 9208227d92e5ad6a0ec51f49e2dd4fb359d95073 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:35:07 +0200 Subject: [PATCH 136/200] Improve setup.py --- setup.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8de5bf6381..20435b86b5 100755 --- a/setup.py +++ b/setup.py @@ -56,6 +56,13 @@ setup( version=const.__version__, license=PROJECT_LICENSE, url=GITHUB_URL, + project_urls={ + "Bug Tracker": "https://github.com/esphome/issues/issues", + "Feature Request Tracker": "https://github.com/esphome/feature-requests/issues", + "Source Code": "https://github.com/esphome/esphome", + "Documentation": "https://esphome.io", + "Twitter": "https://twitter.com/esphome_", + }, download_url=DOWNLOAD_URL, author=PROJECT_AUTHOR, author_email=PROJECT_EMAIL, @@ -74,5 +81,5 @@ setup( 'esphome = esphome.__main__:main' ] }, - packages=find_packages() + packages=find_packages(include="esphome.*") ) From c73f0eee4cabe4c4d4101699f45e89e286565ac1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2020 10:37:08 +0200 Subject: [PATCH 137/200] Bump colorlog from 4.1.0 to 4.2.1 (#1183) Bumps [colorlog](https://github.com/borntyping/python-colorlog) from 4.1.0 to 4.2.1. - [Release notes](https://github.com/borntyping/python-colorlog/releases) - [Commits](https://github.com/borntyping/python-colorlog/compare/v4.1.0...v4.2.1) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a05da7cd01..5f15ec5aa0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ voluptuous==0.11.7 PyYAML==5.3.1 paho-mqtt==1.5.0 -colorlog==4.1.0 +colorlog==4.2.1 tornado==6.0.4 protobuf==3.12.2 tzlocal==2.1 From 27abb1280ae11a9083edab5ceecd44c64b87fd24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 24 Jul 2020 10:37:22 +0200 Subject: [PATCH 138/200] Bump hypothesis from 5.20.3 to 5.21.0 (#1184) Bumps [hypothesis](https://github.com/HypothesisWorks/hypothesis) from 5.20.3 to 5.21.0. - [Release notes](https://github.com/HypothesisWorks/hypothesis/releases) - [Commits](https://github.com/HypothesisWorks/hypothesis/compare/hypothesis-python-5.20.3...hypothesis-python-5.21.0) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 2b7c6c5b1d..296488fc35 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,4 +8,4 @@ pytest==5.4.3 pytest-cov==2.10.0 pytest-mock==3.2.0 asyncmock==0.4.2 -hypothesis==5.20.3 +hypothesis==5.21.0 From 25c62319a3e1fa1809e4e8d1a693e474e1eaa1cb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:41:06 +0200 Subject: [PATCH 139/200] Fix dependabot version 2 --- .github/dependabot.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ec4bf087cf..d73adbfa30 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,6 @@ updates: directory: "/" schedule: interval: "daily" - ignored_updates: + ignore: # Hypotehsis is only used for testing and is updated quite often - - match: - dependency_name: hypothesis + - dependency-name: hypothesis From 50cf57affbe75ff603a3cf2d87146f669b10cd9d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 10:41:34 +0200 Subject: [PATCH 140/200] ESP8266 Disable Pin Initialization on Boot to fix pin toggling (#1185) --- esphome/core/esphal.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index 90707016c3..30a1e9c49f 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -304,3 +304,14 @@ void *memchr(const void *s, int c, size_t n) { } }; #endif + +#ifdef ARDUINO_ARCH_ESP8266 +extern "C" { +extern void resetPins() { // NOLINT + // Added in framework 2.7.0 + // usually this sets up all pins to be in INPUT mode + // however, not strictly needed as we set up the pins properly + // ourselves and this causes pins to toggle during reboot. +} +} +#endif From c030be4d3f2ace3493fe98819eb85ada2c413c20 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 11:10:09 +0200 Subject: [PATCH 141/200] Fix dashboard logout button and py3.8 removed hmac.new digestmod (#1156) * Fix dashboard logout button and py3.8 removed hmac.new digestmod * Just use SHA256 No reason to use HMAC here, as authenticity is not an issue * Wrong branch * Clenaup --- esphome/dashboard/dashboard.py | 14 +++++++++----- esphome/dashboard/templates/index.html | 4 +++- esphome/dashboard/util.py | 9 +++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 esphome/dashboard/util.py diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 1b150e008d..c53c68b3a3 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -30,6 +30,7 @@ from esphome.helpers import mkdir_p, get_bool_env, run_system_command from esphome.storage_json import EsphomeStorageJSON, StorageJSON, \ esphome_storage_path, ext_storage_path, trash_storage_path from esphome.util import shlex_quote, get_serial_ports +from .util import password_hash # pylint: disable=unused-import, wrong-import-order from typing import Optional # noqa @@ -42,7 +43,7 @@ _LOGGER = logging.getLogger(__name__) class DashboardSettings: def __init__(self): self.config_dir = '' - self.password_digest = '' + self.password_hash = '' self.username = '' self.using_password = False self.on_hassio = False @@ -55,7 +56,7 @@ class DashboardSettings: self.username = args.username or os.getenv('USERNAME', '') self.using_password = bool(password) if self.using_password: - self.password_digest = hmac.new(password.encode()).digest() + self.password_hash = password_hash(password) self.config_dir = args.configuration[0] @property @@ -82,8 +83,11 @@ class DashboardSettings: if username != self.username: return False - password = hmac.new(password.encode()).digest() - return username == self.username and hmac.compare_digest(self.password_digest, password) + # Compare password in constant running time (to prevent timing attacks) + return hmac.compare_digest( + self.password_hash, + password_hash(password) + ) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -446,7 +450,7 @@ class MainRequestHandler(BaseHandler): entries = _list_dashboard_entries() self.render("templates/index.html", entries=entries, begin=begin, - **template_args()) + **template_args(), login_enabled=settings.using_auth) def _ping_func(filename, address): diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 1e546b037f..fa742fd2c6 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -52,7 +52,9 @@
  • Update All
  • Secrets Editor
  • -
  • Logout
  • + {% if login_enabled %} +
  • Logout
  • + {% end %} diff --git a/esphome/dashboard/util.py b/esphome/dashboard/util.py new file mode 100644 index 0000000000..3e3864aa17 --- /dev/null +++ b/esphome/dashboard/util.py @@ -0,0 +1,9 @@ +import hashlib + + +def password_hash(password: str) -> bytes: + """Create a hash of a password to transform it to a fixed-length digest. + + Note this is not meant for secure storage, but for securely comparing passwords. + """ + return hashlib.sha256(password.encode()).digest() From f3d5d27c8f4bb115447c43072f408761a8825d03 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 15:40:05 +0200 Subject: [PATCH 142/200] WPA2 Enterprise Attempt 2 (#1158) --- esphome/components/wifi/__init__.py | 49 ++++++- esphome/components/wifi/wifi_component.cpp | 6 + esphome/components/wifi/wifi_component.h | 21 +++ .../components/wifi/wifi_component_esp32.cpp | 50 +++++++ esphome/components/wifi/wpa2_eap.py | 138 ++++++++++++++++++ esphome/const.py | 5 + requirements_test.txt | 5 +- 7 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 esphome/components/wifi/wpa2_eap.py diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index d3c7e51603..7e7ab468ff 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -5,12 +5,16 @@ from esphome.automation import Condition from esphome.const import CONF_AP, CONF_BSSID, CONF_CHANNEL, CONF_DNS1, CONF_DNS2, CONF_DOMAIN, \ CONF_FAST_CONNECT, CONF_GATEWAY, CONF_HIDDEN, CONF_ID, CONF_MANUAL_IP, CONF_NETWORKS, \ CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_REBOOT_TIMEOUT, CONF_SSID, CONF_STATIC_IP, \ - CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY + CONF_SUBNET, CONF_USE_ADDRESS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, \ + CONF_CERTIFICATE, CONF_KEY, CONF_USERNAME, CONF_EAP from esphome.core import CORE, HexInt, coroutine_with_priority +from . import wpa2_eap + AUTO_LOAD = ['network'] wifi_ns = cg.esphome_ns.namespace('wifi') +EAPAuth = wifi_ns.struct('EAPAuth') IPAddress = cg.global_ns.class_('IPAddress') ManualIP = wifi_ns.struct('ManualIP') WiFiComponent = wifi_ns.class_('WiFiComponent', cg.Component) @@ -56,6 +60,17 @@ STA_MANUAL_IP_SCHEMA = AP_MANUAL_IP_SCHEMA.extend({ cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4, }) +EAP_AUTH_SCHEMA = cv.All(cv.only_on_esp32, cv.Schema({ + cv.Optional(CONF_IDENTITY): cv.string_strict, + cv.Optional(CONF_USERNAME): cv.string_strict, + cv.Optional(CONF_PASSWORD): cv.string_strict, + cv.Optional(CONF_CERTIFICATE_AUTHORITY): wpa2_eap.validate_certificate, + cv.Inclusive(CONF_CERTIFICATE, 'certificate_and_key'): wpa2_eap.validate_certificate, + # Only validate as file first because we need the password to load it + # Actual validation happens in validate_eap. + cv.Inclusive(CONF_KEY, 'certificate_and_key'): cv.file_, +}), wpa2_eap.validate_eap, cv.has_at_least_one_key(CONF_IDENTITY, CONF_CERTIFICATE)) + WIFI_NETWORK_BASE = cv.Schema({ cv.GenerateID(): cv.declare_id(WiFiAP), cv.Optional(CONF_SSID): cv.ssid, @@ -73,6 +88,7 @@ WIFI_NETWORK_STA = WIFI_NETWORK_BASE.extend({ cv.Optional(CONF_BSSID): cv.mac_address, cv.Optional(CONF_HIDDEN): cv.boolean, cv.Optional(CONF_PRIORITY, default=0.0): cv.float_, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, }) @@ -81,9 +97,13 @@ def validate(config): raise cv.Invalid("Cannot have WiFi password without SSID!") if CONF_SSID in config: + # Automatically move single network to 'networks' section + config = config.copy() network = {CONF_SSID: config.pop(CONF_SSID)} if CONF_PASSWORD in config: network[CONF_PASSWORD] = config.pop(CONF_PASSWORD) + if CONF_EAP in config: + network[CONF_EAP] = config.pop(CONF_EAP) if CONF_NETWORKS in config: raise cv.Invalid("You cannot use the 'ssid:' option together with 'networks:'. Please " "copy your network into the 'networks:' key") @@ -118,6 +138,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, + cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, cv.Optional(CONF_DOMAIN, default='.local'): cv.domain_name, @@ -133,6 +154,29 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ }), validate) +def eap_auth(config): + if config is None: + return None + ca_cert = "" + if CONF_CERTIFICATE_AUTHORITY in config: + ca_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE_AUTHORITY]) + client_cert = "" + if CONF_CERTIFICATE in config: + client_cert = wpa2_eap.read_relative_config_path(config[CONF_CERTIFICATE]) + key = "" + if CONF_KEY in config: + key = wpa2_eap.read_relative_config_path(config[CONF_KEY]) + return cg.StructInitializer( + EAPAuth, + ('identity', config.get(CONF_IDENTITY, "")), + ('username', config.get(CONF_USERNAME, "")), + ('password', config.get(CONF_PASSWORD, "")), + ('ca_cert', ca_cert), + ('client_cert', client_cert), + ('client_key', key), + ) + + def safe_ip(ip): if ip is None: return IPAddress(0, 0, 0, 0) @@ -158,6 +202,9 @@ def wifi_network(config, static_ip): cg.add(ap.set_ssid(config[CONF_SSID])) if CONF_PASSWORD in config: cg.add(ap.set_password(config[CONF_PASSWORD])) + if CONF_EAP in config: + cg.add(ap.set_eap(eap_auth(config[CONF_EAP]))) + cg.add_define('ESPHOME_WIFI_WPA2_EAP') if CONF_BSSID in config: cg.add(ap.set_bssid([HexInt(i) for i in config[CONF_BSSID].parts])) if CONF_HIDDEN in config: diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d56e75a070..5c1c533c5d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -541,12 +541,18 @@ void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } void WiFiAP::set_password(const std::string &password) { this->password_ = password; } +#ifdef ESPHOME_WIFI_WPA2_EAP +void WiFiAP::set_eap(optional eap_auth) { this->eap_ = eap_auth; } +#endif void WiFiAP::set_channel(optional channel) { this->channel_ = channel; } void WiFiAP::set_manual_ip(optional manual_ip) { this->manual_ip_ = manual_ip; } void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; } const std::string &WiFiAP::get_ssid() const { return this->ssid_; } const optional &WiFiAP::get_bssid() const { return this->bssid_; } const std::string &WiFiAP::get_password() const { return this->password_; } +#ifdef ESPHOME_WIFI_WPA2_EAP +const optional &WiFiAP::get_eap() const { return this->eap_; } +#endif const optional &WiFiAP::get_channel() const { return this->channel_; } const optional &WiFiAP::get_manual_ip() const { return this->manual_ip_; } bool WiFiAP::get_hidden() const { return this->hidden_; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d04e1c2ce0..536d914a36 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -57,6 +57,18 @@ struct ManualIP { IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; +#ifdef ESPHOME_WIFI_WPA2_EAP +struct EAPAuth { + std::string identity; // required for all auth types + std::string username; + std::string password; + const char *ca_cert; // optionally verify authentication server + // used for EAP-TLS + const char *client_cert; + const char *client_key; +}; +#endif // ESPHOME_WIFI_WPA2_EAP + using bssid_t = std::array; class WiFiAP { @@ -65,6 +77,9 @@ class WiFiAP { void set_bssid(bssid_t bssid); void set_bssid(optional bssid); void set_password(const std::string &password); +#ifdef ESPHOME_WIFI_WPA2_EAP + void set_eap(optional eap_auth); +#endif // ESPHOME_WIFI_WPA2_EAP void set_channel(optional channel); void set_priority(float priority) { priority_ = priority; } void set_manual_ip(optional manual_ip); @@ -72,6 +87,9 @@ class WiFiAP { const std::string &get_ssid() const; const optional &get_bssid() const; const std::string &get_password() const; +#ifdef ESPHOME_WIFI_WPA2_EAP + const optional &get_eap() const; +#endif // ESPHOME_WIFI_WPA2_EAP const optional &get_channel() const; float get_priority() const { return priority_; } const optional &get_manual_ip() const; @@ -81,6 +99,9 @@ class WiFiAP { std::string ssid_; optional bssid_; std::string password_; +#ifdef ESPHOME_WIFI_WPA2_EAP + optional eap_; +#endif // ESPHOME_WIFI_WPA2_EAP optional channel_; float priority_{0}; optional manual_ip_; diff --git a/esphome/components/wifi/wifi_component_esp32.cpp b/esphome/components/wifi/wifi_component_esp32.cpp index e345ab1671..a387be78ce 100644 --- a/esphome/components/wifi/wifi_component_esp32.cpp +++ b/esphome/components/wifi/wifi_component_esp32.cpp @@ -6,6 +6,9 @@ #include #include +#ifdef ESPHOME_WIFI_WPA2_EAP +#include +#endif #include "lwip/err.h" #include "lwip/dns.h" @@ -187,6 +190,53 @@ bool WiFiComponent::wifi_sta_connect_(WiFiAP ap) { return false; } + // setup enterprise authentication if required +#ifdef ESPHOME_WIFI_WPA2_EAP + if (ap.get_eap().has_value()) { + // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0. + EAPAuth eap = ap.get_eap().value(); + err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed! %d", err); + } + int ca_cert_len = strlen(eap.ca_cert); + int client_cert_len = strlen(eap.client_cert); + int client_key_len = strlen(eap.client_key); + if (ca_cert_len) { + err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed! %d", err); + } + } + // workout what type of EAP this is + // validation is not required as the config tool has already validated it + if (client_cert_len && client_key_len) { + // if we have certs, this must be EAP-TLS + err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1, + (uint8_t *) eap.client_key, client_key_len + 1, + (uint8_t *) eap.password.c_str(), strlen(eap.password.c_str())); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed! %d", err); + } + } else { + // in the absence of certs, assume this is username/password based + err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed! %d", err); + } + err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length()); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed! %d", err); + } + } + esp_wpa2_config_t wpa2_config = WPA2_CONFIG_INIT_DEFAULT(); + err = esp_wifi_sta_wpa2_ent_enable(&wpa2_config); + if (err != ESP_OK) { + ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed! %d", err); + } + } +#endif // ESPHOME_WIFI_WPA2_EAP + this->wifi_apply_hostname_(); err = esp_wifi_connect(); diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py new file mode 100644 index 0000000000..5b83c59866 --- /dev/null +++ b/esphome/components/wifi/wpa2_eap.py @@ -0,0 +1,138 @@ +"""Module for all WPA2 Utilities. + +The cryptography package is loaded lazily in the functions +so that it doesn't crash if it's not installed. +""" +import logging +from pathlib import Path + +from esphome.core import CORE +import esphome.config_validation as cv +from esphome.const import CONF_USERNAME, CONF_IDENTITY, CONF_PASSWORD, CONF_CERTIFICATE, \ + CONF_KEY + + +_LOGGER = logging.getLogger(__name__) + + +def validate_cryptography_installed(): + try: + import cryptography + except ImportError: + raise cv.Invalid("This settings requires the cryptography python package. " + "Please install it with `pip install cryptography`") + + if cryptography.__version__[0] < '2': + raise cv.Invalid("Please update your python cryptography installation to least 2.x " + "(pip install -U cryptography)") + + +def wrapped_load_pem_x509_certificate(value): + validate_cryptography_installed() + + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + + return x509.load_pem_x509_certificate(value.encode('UTF-8'), default_backend()) + + +def wrapped_load_pem_private_key(value, password): + validate_cryptography_installed() + + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.backends import default_backend + + if password: + password = password.encode("UTF-8") + return load_pem_private_key(value.encode('UTF-8'), password, default_backend()) + + +def read_relative_config_path(value): + return Path(CORE.relative_config_path(value)).read_text() + + +def _validate_load_certificate(value): + value = cv.file_(value) + try: + contents = read_relative_config_path(value) + return wrapped_load_pem_x509_certificate(contents) + except ValueError as err: + raise cv.Invalid(f"Invalid certificate: {err}") + + +def validate_certificate(value): + _validate_load_certificate(value) + # Validation result should be the path, not the loaded certificate + return value + + +def _validate_load_private_key(key, cert_pw): + key = cv.file_(key) + try: + contents = read_relative_config_path(key) + return wrapped_load_pem_private_key(contents, cert_pw) + except ValueError as e: + raise cv.Invalid(f"There was an error with the EAP 'password:' provided for 'key' {e}") + except TypeError as e: + raise cv.Invalid(f"There was an error with the EAP 'key:' provided: {e}") + + +def _check_private_key_cert_match(key, cert): + from cryptography.hazmat.primitives.asymmetric import rsa, ec + + def check_match_a(): + return key.public_key().public_numbers() == cert.public_key().public_numbers() + + def check_match_b(): + return key.public_key() == cert + + private_key_types = { + rsa.RSAPrivateKey: check_match_a, + ec.EllipticCurvePrivateKey: check_match_a, + } + + try: + # pylint: disable=no-name-in-module + from cryptography.hazmat.primitives.asymmetric import ed448, ed25519 + + private_key_types.update({ + ed448.Ed448PrivateKey: check_match_b, + ed25519.Ed25519PrivateKey: check_match_b, + }) + except ImportError: + # ed448, ed25519 not supported + pass + + key_type = next((kt for kt in private_key_types if isinstance(key, kt)), None) + if key_type is None: + _LOGGER.warning( + "Unrecognised EAP 'certificate:' 'key:' pair format: %s. Proceed with caution!", + type(key) + ) + elif not private_key_types[key_type](): + raise cv.Invalid("The provided EAP 'key' is not valid for the 'certificate'.") + + +def validate_eap(value): + validate_cryptography_installed() + + if CONF_USERNAME in value: + if CONF_IDENTITY not in value: + _LOGGER.info("EAP 'identity:' is not set, assuming username.") + value = value.copy() + value[CONF_IDENTITY] = value[CONF_USERNAME] + if CONF_PASSWORD not in value: + raise cv.Invalid("You cannot use the EAP 'username:' option without a 'password:'. " + "Please provide the 'password:' key") + + if CONF_CERTIFICATE in value or CONF_KEY in value: + # Check the key is valid and for this certificate, just to check the user hasn't pasted + # the wrong thing. I write this after I spent a while debugging that exact issue. + # This may require a password to decrypt to key, so we should verify that at the same time. + cert_pw = value.get(CONF_PASSWORD) + key = _validate_load_private_key(value[CONF_KEY], cert_pw) + + cert = _validate_load_certificate(value[CONF_CERTIFICATE]) + _check_private_key_cert_match(key, cert) + + return value diff --git a/esphome/const.py b/esphome/const.py index b37d816256..57694c4088 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -75,6 +75,8 @@ CONF_CALIBRATION = 'calibration' CONF_CAPACITANCE = 'capacitance' CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent' CONF_CARRIER_FREQUENCY = 'carrier_frequency' +CONF_CERTIFICATE = "certificate" +CONF_CERTIFICATE_AUTHORITY = "certificate_authority" CONF_CHANGE_MODE_EVERY = 'change_mode_every' CONF_CHANNEL = 'channel' CONF_CHANNELS = 'channels' @@ -145,6 +147,7 @@ CONF_DRY_ACTION = 'dry_action' CONF_DRY_MODE = 'dry_mode' CONF_DUMP = 'dump' CONF_DURATION = 'duration' +CONF_EAP = 'eap' CONF_ECHO_PIN = 'echo_pin' CONF_EFFECT = 'effect' CONF_EFFECTS = 'effects' @@ -211,6 +214,7 @@ CONF_I2C = 'i2c' CONF_I2C_ID = 'i2c_id' CONF_ICON = 'icon' CONF_ID = 'id' +CONF_IDENTITY = 'identity' CONF_IDLE = 'idle' CONF_IDLE_ACTION = 'idle_action' CONF_IDLE_LEVEL = 'idle_level' @@ -238,6 +242,7 @@ CONF_JS_URL = 'js_url' CONF_JVC = 'jvc' CONF_KEEP_ON_TIME = 'keep_on_time' CONF_KEEPALIVE = 'keepalive' +CONF_KEY = 'key' CONF_LAMBDA = 'lambda' CONF_LEVEL = 'level' CONF_LG = 'lg' diff --git a/requirements_test.txt b/requirements_test.txt index 296488fc35..179f683b32 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,8 @@ pylint==2.5.3 flake8==3.8.3 -pillow -pexpect +pillow>4.0.0 +cryptography>=2.0.0,<3 +pexpect==4.8.0 # Unit tests pytest==5.4.3 From 5759de079b56a8ed1fa31092e537f7f0103cc939 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 22:36:25 +0200 Subject: [PATCH 143/200] Remove symlink_ops.py (#1196) We no longer use symlinks anywhere --- esphome/symlink_ops.py | 164 ----------------------------------------- 1 file changed, 164 deletions(-) delete mode 100644 esphome/symlink_ops.py diff --git a/esphome/symlink_ops.py b/esphome/symlink_ops.py deleted file mode 100644 index accfbe5ff7..0000000000 --- a/esphome/symlink_ops.py +++ /dev/null @@ -1,164 +0,0 @@ -import os - -if hasattr(os, 'symlink'): - def symlink(src, dst): - return os.symlink(src, dst) - - def islink(path): - return os.path.islink(path) - - def readlink(path): - return os.readlink(path) - - def unlink(path): - return os.unlink(path) -else: - import ctypes - from ctypes import wintypes - # Code taken from - # https://stackoverflow.com/questions/27972776/having-trouble-implementing-a-readlink-function - - kernel32 = ctypes.WinDLL('kernel32', use_last_error=True) - - FILE_READ_ATTRIBUTES = 0x0080 - OPEN_EXISTING = 3 - FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000 - FILE_FLAG_BACKUP_SEMANTICS = 0x02000000 - FILE_ATTRIBUTE_REPARSE_POINT = 0x0400 - - IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003 - IO_REPARSE_TAG_SYMLINK = 0xA000000C - FSCTL_GET_REPARSE_POINT = 0x000900A8 - MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 0x4000 - - LPDWORD = ctypes.POINTER(wintypes.DWORD) - LPWIN32_FIND_DATA = ctypes.POINTER(wintypes.WIN32_FIND_DATAW) - INVALID_HANDLE_VALUE = wintypes.HANDLE(-1).value - - def IsReparseTagNameSurrogate(tag): - return bool(tag & 0x20000000) - - def _check_invalid_handle(result, func, args): - if result == INVALID_HANDLE_VALUE: - raise ctypes.WinError(ctypes.get_last_error()) - return args - - def _check_bool(result, func, args): - if not result: - raise ctypes.WinError(ctypes.get_last_error()) - return args - - kernel32.FindFirstFileW.errcheck = _check_invalid_handle - kernel32.FindFirstFileW.restype = wintypes.HANDLE - kernel32.FindFirstFileW.argtypes = ( - wintypes.LPCWSTR, # _In_ lpFileName - LPWIN32_FIND_DATA) # _Out_ lpFindFileData - - kernel32.FindClose.argtypes = ( - wintypes.HANDLE,) # _Inout_ hFindFile - - kernel32.CreateFileW.errcheck = _check_invalid_handle - kernel32.CreateFileW.restype = wintypes.HANDLE - kernel32.CreateFileW.argtypes = ( - wintypes.LPCWSTR, # _In_ lpFileName - wintypes.DWORD, # _In_ dwDesiredAccess - wintypes.DWORD, # _In_ dwShareMode - wintypes.LPVOID, # _In_opt_ lpSecurityAttributes - wintypes.DWORD, # _In_ dwCreationDisposition - wintypes.DWORD, # _In_ dwFlagsAndAttributes - wintypes.HANDLE) # _In_opt_ hTemplateFile - - kernel32.CloseHandle.argtypes = ( - wintypes.HANDLE,) # _In_ hObject - - kernel32.DeviceIoControl.errcheck = _check_bool - kernel32.DeviceIoControl.argtypes = ( - wintypes.HANDLE, # _In_ hDevice - wintypes.DWORD, # _In_ dwIoControlCode - wintypes.LPVOID, # _In_opt_ lpInBuffer - wintypes.DWORD, # _In_ nInBufferSize - wintypes.LPVOID, # _Out_opt_ lpOutBuffer - wintypes.DWORD, # _In_ nOutBufferSize - LPDWORD, # _Out_opt_ lpBytesReturned - wintypes.LPVOID) # _Inout_opt_ lpOverlapped - - class REPARSE_DATA_BUFFER(ctypes.Structure): - class ReparseData(ctypes.Union): - class LinkData(ctypes.Structure): - _fields_ = (('SubstituteNameOffset', wintypes.USHORT), - ('SubstituteNameLength', wintypes.USHORT), - ('PrintNameOffset', wintypes.USHORT), - ('PrintNameLength', wintypes.USHORT)) - - @property - def PrintName(self): - dt = wintypes.WCHAR * (self.PrintNameLength // ctypes.sizeof(wintypes.WCHAR)) - name = dt.from_address(ctypes.addressof(self.PathBuffer) + - self.PrintNameOffset).value - if name.startswith(r'\??'): - name = r'\\?' + name[3:] # NT => Windows - return name - - class SymbolicLinkData(LinkData): - _fields_ = (('Flags', wintypes.ULONG), ('PathBuffer', wintypes.BYTE * 0)) - - class MountPointData(LinkData): - _fields_ = (('PathBuffer', wintypes.BYTE * 0),) - - class GenericData(ctypes.Structure): - _fields_ = (('DataBuffer', wintypes.BYTE * 0),) - _fields_ = (('SymbolicLinkReparseBuffer', SymbolicLinkData), - ('MountPointReparseBuffer', MountPointData), - ('GenericReparseBuffer', GenericData)) - _fields_ = (('ReparseTag', wintypes.ULONG), - ('ReparseDataLength', wintypes.USHORT), - ('Reserved', wintypes.USHORT), - ('ReparseData', ReparseData)) - _anonymous_ = ('ReparseData',) - - def symlink(src, dst): - csl = ctypes.windll.kernel32.CreateSymbolicLinkW - csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) - csl.restype = ctypes.c_ubyte - flags = 1 if os.path.isdir(src) else 0 - if csl(dst, src, flags) == 0: - error = ctypes.WinError() - # pylint: disable=no-member - if error.winerror == 1314 and error.errno == 22: - from esphome.core import EsphomeError - raise EsphomeError("Cannot create symlink from '%s' to '%s'. Try running tool \ -with elevated privileges" % (src, dst)) - raise error - - def islink(path): - if not os.path.isdir(path): - return False - data = wintypes.WIN32_FIND_DATAW() - kernel32.FindClose(kernel32.FindFirstFileW(path, ctypes.byref(data))) - if not data.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT: - return False - return IsReparseTagNameSurrogate(data.dwReserved0) - - def readlink(path): - n = wintypes.DWORD() - buf = (wintypes.BYTE * MAXIMUM_REPARSE_DATA_BUFFER_SIZE)() - flags = FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS - handle = kernel32.CreateFileW(path, FILE_READ_ATTRIBUTES, 0, None, - OPEN_EXISTING, flags, None) - try: - kernel32.DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, None, 0, - buf, ctypes.sizeof(buf), ctypes.byref(n), None) - finally: - kernel32.CloseHandle(handle) - rb = REPARSE_DATA_BUFFER.from_buffer(buf) - tag = rb.ReparseTag - if tag == IO_REPARSE_TAG_SYMLINK: - return rb.SymbolicLinkReparseBuffer.PrintName - if tag == IO_REPARSE_TAG_MOUNT_POINT: - return rb.MountPointReparseBuffer.PrintName - if not IsReparseTagNameSurrogate(tag): - raise ValueError("not a link") - raise ValueError("unsupported reparse tag: %d" % tag) - - def unlink(path): - return os.rmdir(path) From 7524493c3ceefd80745bd65735b5e46f1ee5ca9f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 22:36:59 +0200 Subject: [PATCH 144/200] Fix senseair flush input buffer wrong log level (#1194) See also https://github.com/esphome/esphome/pull/1017/files --- esphome/components/senseair/senseair.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp index 19831874d7..812a4aa42d 100644 --- a/esphome/components/senseair/senseair.cpp +++ b/esphome/components/senseair/senseair.cpp @@ -24,11 +24,11 @@ void SenseAirComponent::update() { this->status_set_warning(); while (this->available()) { - unsigned char b; + uint8_t b; if (this->read_byte(&b)) { - ESP_LOGD(TAG, " ... %02x", b); + ESP_LOGV(TAG, " ... %02x", b); } else { - ESP_LOGD(TAG, " ... nothing read"); + ESP_LOGV(TAG, " ... nothing read"); } } return; From 43d5e7a8ccdba47edfa65f64c34bfd889c918e31 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 24 Jul 2020 22:37:19 +0200 Subject: [PATCH 145/200] Fix WLED minor issues (#1193) --- esphome/components/e131/e131.cpp | 1 + esphome/components/wled/wled_light_effect.cpp | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/esphome/components/e131/e131.cpp b/esphome/components/e131/e131.cpp index d107d9f9fc..8a293e9067 100644 --- a/esphome/components/e131/e131.cpp +++ b/esphome/components/e131/e131.cpp @@ -31,6 +31,7 @@ void E131Component::setup() { if (!udp_->begin(PORT)) { ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); mark_failed(); + return; } join_igmp_groups_(); diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index f161ea15e8..e06a23aacf 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -50,12 +50,13 @@ void WLEDLightEffect::apply(light::AddressableLight &it, const light::ESPColor & udp_.reset(new WiFiUDP()); if (!udp_->begin(port_)) { - ESP_LOGE(TAG, "Cannot bind WLEDLightEffect to %d.", port_); + ESP_LOGW(TAG, "Cannot bind WLEDLightEffect to %d.", port_); + return; } } + std::vector payload; while (uint16_t packet_size = udp_->parsePacket()) { - std::vector payload; payload.resize(packet_size); if (!udp_->read(&payload[0], payload.size())) { @@ -63,11 +64,12 @@ void WLEDLightEffect::apply(light::AddressableLight &it, const light::ESPColor & } if (!this->parse_frame_(it, &payload[0], payload.size())) { - ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=%c/%d).", payload.size(), payload[0], payload[0]); + ESP_LOGD(TAG, "Frame: Invalid (size=%zu, first=0x%02X).", payload.size(), payload[0]); continue; } } + // FIXME: Use roll-over safe arithmetic if (blank_at_ < millis()) { blank_all_leds_(it); blank_at_ = millis() + DEFAULT_BLANK_TIME; From 62468198d617c1e16713617d96edcc45514888ae Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 13:56:08 +0200 Subject: [PATCH 146/200] Clean up UART Improvements code (#1190) --- esphome/components/uart/uart.cpp | 9 ++--- esphome/components/uart/uart.h | 16 +++------ esphome/components/uart/uart_esp32.cpp | 8 ++--- esphome/components/uart/uart_esp8266.cpp | 46 +++++------------------- 4 files changed, 22 insertions(+), 57 deletions(-) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index cf2d00c929..1cfdb38a90 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -43,7 +43,8 @@ void UARTComponent::check_logger_conflict_() { #endif } -void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, uint8_t nr_bits) { +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, + uint8_t data_bits) { if (this->parent_->baud_rate_ != baud_rate) { ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, this->parent_->baud_rate_); @@ -52,9 +53,9 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, this->parent_->stop_bits_); } - if (this->parent_->nr_bits_ != nr_bits) { - ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", nr_bits, - this->parent_->nr_bits_); + if (this->parent_->data_bits_ != data_bits) { + ESP_LOGE(TAG, " Invalid number of data bits: Integration requested %u data bits but you have %u!", data_bits, + this->parent_->data_bits_); } if (this->parent_->parity_ != parity) { ESP_LOGE(TAG, " Invalid parity: Integration requested parity %s but you have %s!", parity_to_str(parity), diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index dedfdd74af..7430a4ee05 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -18,7 +18,7 @@ const char *parity_to_str(UARTParityOptions parity); #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t nr_bits, + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size); uint8_t read_byte(); @@ -30,8 +30,6 @@ class ESP8266SoftwareSerial { int available(); - void begin(); - void end(); GPIOPin *gpio_tx_pin_{nullptr}; GPIOPin *gpio_rx_pin_{nullptr}; @@ -48,7 +46,7 @@ class ESP8266SoftwareSerial { volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; uint8_t stop_bits_; - uint8_t nr_bits_; + uint8_t data_bits_; UARTParityOptions parity_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; @@ -71,8 +69,6 @@ class UARTComponent : public Component, public Stream { void write_array(const std::vector &data) { this->write_array(&data[0], data.size()); } void write_str(const char *str); - void end(); - void begin(); bool peek_byte(uint8_t *data); @@ -95,7 +91,7 @@ class UARTComponent : public Component, public Stream { void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } void set_rx_buffer_size(size_t rx_buffer_size) { this->rx_buffer_size_ = rx_buffer_size; } void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } - void set_data_bits(uint8_t nr_bits) { this->nr_bits_ = nr_bits; } + void set_data_bits(uint8_t data_bits) { this->data_bits_ = data_bits; } void set_parity(UARTParityOptions parity) { this->parity_ = parity; } protected: @@ -112,7 +108,7 @@ class UARTComponent : public Component, public Stream { size_t rx_buffer_size_; uint32_t baud_rate_; uint8_t stop_bits_; - uint8_t nr_bits_; + uint8_t data_bits_; UARTParityOptions parity_; }; @@ -156,12 +152,10 @@ class UARTDevice : public Stream { size_t write(uint8_t data) override { return this->parent_->write(data); } int read() override { return this->parent_->read(); } int peek() override { return this->parent_->peek(); } - void end() { this->parent_->end(); } - void begin() { this->parent_->begin(); } /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1, - UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t nr_bits = 8); + UARTParityOptions parity = UART_CONFIG_PARITY_NONE, uint8_t data_bits = 8); protected: UARTComponent *parent_{nullptr}; diff --git a/esphome/components/uart/uart_esp32.cpp b/esphome/components/uart/uart_esp32.cpp index e18f9c5b1f..f7af85cf7b 100644 --- a/esphome/components/uart/uart_esp32.cpp +++ b/esphome/components/uart/uart_esp32.cpp @@ -43,7 +43,7 @@ uint32_t UARTComponent::get_config() { else if (this->parity_ == UART_CONFIG_PARITY_ODD) config |= UART_PARITY_ODD | UART_PARITY_EN; - switch (this->nr_bits_) { + switch (this->data_bits_) { case 5: config |= UART_NB_BIT_5; break; @@ -94,7 +94,7 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Bits: %u", this->nr_bits_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); this->check_logger_conflict_(); @@ -114,8 +114,6 @@ void UARTComponent::write_str(const char *str) { this->hw_serial_->write(str); ESP_LOGVV(TAG, " Wrote \"%s\"", str); } -void UARTComponent::end() { this->hw_serial_->end(); } -void UARTComponent::begin() { this->hw_serial_->begin(this->baud_rate_, get_config()); } bool UARTComponent::read_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; @@ -161,4 +159,4 @@ void UARTComponent::flush() { } // namespace uart } // namespace esphome -#endif // ESP32 +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/uart/uart_esp8266.cpp b/esphome/components/uart/uart_esp8266.cpp index 40975b6e5e..edb530f537 100644 --- a/esphome/components/uart/uart_esp8266.cpp +++ b/esphome/components/uart/uart_esp8266.cpp @@ -19,7 +19,7 @@ uint32_t UARTComponent::get_config() { else if (this->parity_ == UART_CONFIG_PARITY_ODD) config |= UART_PARITY_ODD; - switch (this->nr_bits_) { + switch (this->data_bits_) { case 5: config |= UART_NB_BIT_5; break; @@ -66,7 +66,7 @@ void UARTComponent::setup() { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->nr_bits_, this->parity_, + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_, this->data_bits_, this->parity_, this->rx_buffer_size_); } } @@ -81,7 +81,7 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); - ESP_LOGCONFIG(TAG, " Bits: %u", this->nr_bits_); + ESP_LOGCONFIG(TAG, " Data Bits: %u", this->data_bits_); ESP_LOGCONFIG(TAG, " Parity: %s", parity_to_str(this->parity_)); ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { @@ -121,18 +121,6 @@ void UARTComponent::write_str(const char *str) { } ESP_LOGVV(TAG, " Wrote \"%s\"", str); } -void UARTComponent::end() { - if (this->hw_serial_ != nullptr) - this->hw_serial_->end(); - else if (this->sw_serial_ != nullptr) - this->sw_serial_->end(); -} -void UARTComponent::begin() { - if (this->hw_serial_ != nullptr) - this->hw_serial_->begin(this->baud_rate_, static_cast(get_config())); - else if (this->sw_serial_ != nullptr) - this->sw_serial_->begin(); -} bool UARTComponent::read_byte(uint8_t *data) { if (!this->check_read_timeout_()) return false; @@ -198,28 +186,12 @@ void UARTComponent::flush() { this->sw_serial_->flush(); } } -void ESP8266SoftwareSerial::end() { - /* Because of this bug: https://github.com/esp8266/Arduino/issues/6049 - * detach_interrupt can't called. - * So simply reset rx_in_pos and rx_out_pos even if it's totally racy with - * the interrupt. - */ - // this->gpio_rx_pin_->detach_interrupt(); - this->rx_in_pos_ = 0; - this->rx_out_pos_ = 0; -} -void ESP8266SoftwareSerial::begin() { - /* attach_interrupt() is also not safe because gpio_intr() may - * endup with arg == nullptr. - */ - // this->gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); -} -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, uint32_t nr_bits, - UARTParityOptions parity, size_t rx_buffer_size) { +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits, + uint32_t data_bits, UARTParityOptions parity, size_t rx_buffer_size) { this->bit_time_ = F_CPU / baud_rate; this->rx_buffer_size_ = rx_buffer_size; this->stop_bits_ = stop_bits; - this->nr_bits_ = nr_bits; + this->data_bits_ = data_bits; this->parity_ = parity; if (tx_pin != -1) { auto pin = GPIOPin(tx_pin, OUTPUT); @@ -242,7 +214,7 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg const uint32_t start = ESP.getCycleCount(); uint8_t rec = 0; // Manually unroll the loop - for (int i = 0; i < arg->nr_bits_; i++) + for (int i = 0; i < arg->data_bits_; i++) rec |= arg->read_bit_(&wait, start) << i; /* If parity is enabled, just read it and ignore it. */ @@ -282,7 +254,7 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { const uint32_t start = ESP.getCycleCount(); // Start bit this->write_bit_(false, &wait, start); - for (int i = 0; i < this->nr_bits_; i++) { + for (int i = 0; i < this->data_bits_; i++) { bool bit = data & (1 << i); this->write_bit_(bit, &wait, start); if (need_parity_bit) @@ -333,4 +305,4 @@ int ESP8266SoftwareSerial::available() { } // namespace uart } // namespace esphome -#endif // ESP8266 +#endif // ARDUINO_ARCH_ESP8266 From 275c12150e980d267670354251b46b71963ce4c3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 14:05:31 +0200 Subject: [PATCH 147/200] Partially revert make SPI CS pin optional (#1187) --- esphome/components/as3935_spi/__init__.py | 2 +- esphome/components/max7219digit/display.py | 2 +- esphome/components/mcp3008/__init__.py | 2 +- esphome/components/pn532/pn532.cpp | 3 +-- esphome/components/spi/__init__.py | 6 +++--- esphome/components/spi/spi.h | 20 +++++++++++--------- esphome/components/ssd1325_spi/display.py | 2 +- 7 files changed, 19 insertions(+), 18 deletions(-) diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py index da5582c531..30c2240c27 100644 --- a/esphome/components/as3935_spi/__init__.py +++ b/esphome/components/as3935_spi/__init__.py @@ -11,7 +11,7 @@ SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDev CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPIAS3935), -}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(CS_PIN_required=True))) +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=True))) def to_code(config): diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index 5bba71148c..eb6d112a64 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = display.BASIC_DISPLAY_SCHEMA.extend({ cv.Optional(CONF_SCROLL_SPEED, default='250ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SCROLL_DELAY, default='1000ms'): cv.positive_time_period_milliseconds, cv.Optional(CONF_SCROLL_DWELL, default='1000ms'): cv.positive_time_period_milliseconds, -}).extend(cv.polling_component_schema('500ms')).extend(spi.spi_device_schema(CS_PIN_required=True)) +}).extend(cv.polling_component_schema('500ms')).extend(spi.spi_device_schema(cs_pin_required=True)) def to_code(config): diff --git a/esphome/components/mcp3008/__init__.py b/esphome/components/mcp3008/__init__.py index 2196b96a92..acacc96159 100644 --- a/esphome/components/mcp3008/__init__.py +++ b/esphome/components/mcp3008/__init__.py @@ -14,7 +14,7 @@ MCP3008 = mcp3008_ns.class_('MCP3008', cg.Component, spi.SPIDevice) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MCP3008), -}).extend(spi.spi_device_schema(CS_PIN_required=True)) +}).extend(spi.spi_device_schema(cs_pin_required=True)) def to_code(config): diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 36677bc729..792d92a6ac 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -31,8 +31,7 @@ void PN532::setup() { // (this may time out, but that's ok) // 3. Send SAM config command with normal mode without waiting for ready bit (IRQ not initialized yet) // 4. Probably optional, send SAM config again, this time checking ACK and return value - if (this->cs_) - this->cs_->digital_write(false); + this->cs_->digital_write(false); delay(10); // send dummy firmware version command to get synced up diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 6ced4bc80f..71dac0f8c9 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -34,15 +34,15 @@ def to_code(config): cg.add(var.set_mosi(mosi)) -def spi_device_schema(CS_PIN_required=False): +def spi_device_schema(cs_pin_required=True): """Create a schema for an SPI device. - :param CS_PIN_required: If true, make the CS_PIN required in the config. + :param cs_pin_required: If true, make the CS_PIN required in the config. :return: The SPI device schema, `extend` this in your config schema. """ schema = { cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), } - if CS_PIN_required: + if cs_pin_required: schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema else: schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index ea0595c641..ba726f8052 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -127,18 +127,20 @@ class SPIComponent : public Component { template void enable(GPIOPin *cs) { - if (cs) { + if (cs != nullptr) { SPIComponent::debug_enable(cs->get_pin()); + } - if (this->hw_spi_ != nullptr) { - uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); - SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); - this->hw_spi_->beginTransaction(settings); - } else { - this->clk_->digital_write(CLOCK_POLARITY); - this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; - } + if (this->hw_spi_ != nullptr) { + uint8_t data_mode = (uint8_t(CLOCK_POLARITY) << 1) | uint8_t(CLOCK_PHASE); + SPISettings settings(DATA_RATE, BIT_ORDER, data_mode); + this->hw_spi_->beginTransaction(settings); + } else { + this->clk_->digital_write(CLOCK_POLARITY); + this->wait_cycle_ = uint32_t(F_CPU) / DATA_RATE / 2ULL; + } + if (cs != nullptr) { this->active_cs_ = cs; this->active_cs_->digital_write(false); } diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py index ed61d3dec3..2d8e91c3df 100644 --- a/esphome/components/ssd1325_spi/display.py +++ b/esphome/components/ssd1325_spi/display.py @@ -13,7 +13,7 @@ SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevic CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(SPISSD1325), cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, -}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema()), +}).extend(cv.COMPONENT_SCHEMA).extend(spi.spi_device_schema(cs_pin_required=False)), cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) From 32efa5d83ee03b1c2bad7fee7267f1253007c31f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 14:20:51 +0200 Subject: [PATCH 148/200] New script modes POC (#1168) Co-authored-by: Guillermo Ruffino --- .../bang_bang/bang_bang_climate.cpp | 2 +- esphome/components/endstop/endstop_cover.cpp | 2 +- esphome/components/light/base_light_effects.h | 4 +- esphome/components/script/__init__.py | 52 ++++++++++++- esphome/components/script/script.cpp | 67 ++++++++++++++++ esphome/components/script/script.h | 77 ++++++++++++++++--- .../template/cover/template_cover.cpp | 2 +- .../template/switch/template_switch.cpp | 2 +- .../thermostat/thermostat_climate.cpp | 8 +- .../time_based/time_based_cover.cpp | 2 +- esphome/config_validation.py | 4 +- esphome/core/automation.h | 31 +++++++- 12 files changed, 226 insertions(+), 27 deletions(-) create mode 100644 esphome/components/script/script.cpp diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index cf527988fe..45d5174390 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -108,7 +108,7 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { } if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); this->prev_trigger_ = nullptr; } Trigger<> *trig; diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 1c239226c1..8e20cb6a29 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -94,7 +94,7 @@ void EndstopCover::dump_config() { float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index dcef60397d..d6d930e9d4 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -67,9 +67,9 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: AutomationLightEffect(const std::string &name) : LightEffect(name) {} - void stop() override { this->trig_->stop(); } + void stop() override { this->trig_->stop_action(); } void apply() override { - if (!this->trig_->is_running()) { + if (!this->trig_->is_action_running()) { this->trig_->trigger(); } } diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 5075bb1f56..eb337d7681 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_MODE script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) @@ -10,10 +10,47 @@ ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action, cg.Component) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) +SingleScript = script_ns.class_('SingleScript', Script) +RestartScript = script_ns.class_('RestartScript', Script) +QueueingScript = script_ns.class_('QueueingScript', Script, cg.Component) +ParallelScript = script_ns.class_('ParallelScript', Script) + +CONF_SINGLE = 'single' +CONF_RESTART = 'restart' +CONF_QUEUE = 'queue' +CONF_PARALLEL = 'parallel' +CONF_MAX_RUNS = 'max_runs' + +SCRIPT_MODES = { + CONF_SINGLE: SingleScript, + CONF_RESTART: RestartScript, + CONF_QUEUE: QueueingScript, + CONF_PARALLEL: ParallelScript, +} + + +def check_max_runs(value): + if CONF_MAX_RUNS not in value: + return value + if value[CONF_MODE] not in [CONF_QUEUE, CONF_PARALLEL]: + raise cv.Invalid("The option 'max_runs' is only valid in 'queue' and 'parallel' mode.", + path=[CONF_MAX_RUNS]) + return value + + +def assign_declare_id(value): + value = value.copy() + value[CONF_ID] = cv.declare_id(SCRIPT_MODES[value[CONF_MODE]])(value[CONF_ID]) + return value + CONFIG_SCHEMA = automation.validate_automation({ - cv.Required(CONF_ID): cv.declare_id(Script), -}) + # Don't declare id as cv.declare_id yet, because the ID type + # dpeends on the mode. Will be checked later with assign_declare_id + cv.Required(CONF_ID): cv.string_strict, + cv.Optional(CONF_MODE, default=CONF_SINGLE): cv.one_of(*SCRIPT_MODES, lower=True), + cv.Optional(CONF_MAX_RUNS): cv.positive_int, +}, extra_validators=cv.All(check_max_runs, assign_declare_id)) def to_code(config): @@ -21,6 +58,15 @@ def to_code(config): triggers = [] for conf in config: trigger = cg.new_Pvariable(conf[CONF_ID]) + # Add a human-readable name to the script + cg.add(trigger.set_name(conf[CONF_ID].id)) + + if CONF_MAX_RUNS in conf: + cg.add(trigger.set_max_runs(conf[CONF_MAX_RUNS])) + + if conf[CONF_MODE] == CONF_QUEUE: + yield cg.register_component(trigger, conf) + triggers.append((trigger, conf)) for trigger, conf in triggers: diff --git a/esphome/components/script/script.cpp b/esphome/components/script/script.cpp new file mode 100644 index 0000000000..f4441b7dcd --- /dev/null +++ b/esphome/components/script/script.cpp @@ -0,0 +1,67 @@ +#include "script.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace script { + +static const char *TAG = "script"; + +void SingleScript::execute() { + if (this->is_action_running()) { + ESP_LOGW(TAG, "Script '%s' is already running! (mode: single)", this->name_.c_str()); + return; + } + + this->trigger(); +} + +void RestartScript::execute() { + if (this->is_action_running()) { + ESP_LOGD(TAG, "Script '%s' restarting (mode: restart)", this->name_.c_str()); + this->stop_action(); + } + + this->trigger(); +} + +void QueueingScript::execute() { + if (this->is_action_running()) { + // num_runs_ is the number of *queued* instances, so total number of instances is + // num_runs_ + 1 + if (this->max_runs_ != 0 && this->num_runs_ + 1 >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of queued runs exceeded!", this->name_.c_str()); + return; + } + + ESP_LOGD(TAG, "Script '%s' queueing new instance (mode: queue)", this->name_.c_str()); + this->num_runs_++; + return; + } + + this->trigger(); + // Check if the trigger was immediate and we can continue right away. + this->loop(); +} + +void QueueingScript::stop() { + this->num_runs_ = 0; + Script::stop(); +} + +void QueueingScript::loop() { + if (this->num_runs_ != 0 && !this->is_action_running()) { + this->num_runs_--; + this->trigger(); + } +} + +void ParallelScript::execute() { + if (this->max_runs_ != 0 && this->automation_parent_->num_running() >= this->max_runs_) { + ESP_LOGW(TAG, "Script '%s' maximum number of parallel runs exceeded!", this->name_.c_str()); + return; + } + this->trigger(); +} + +} // namespace script +} // namespace esphome diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index 8495014f00..64db6b80e7 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -1,29 +1,86 @@ #pragma once #include "esphome/core/automation.h" +#include "esphome/core/component.h" namespace esphome { namespace script { +/// The abstract base class for all script types. class Script : public Trigger<> { public: - void execute() { - bool prev = this->in_stack_; - this->in_stack_ = true; - this->trigger(); - this->in_stack_ = prev; - } - bool script_is_running() { return this->in_stack_ || this->is_running(); } + /** Execute a new instance of this script. + * + * The behavior of this function when a script is already running is defined by the subtypes + */ + virtual void execute() = 0; + /// Check if any instance of this script is currently running. + virtual bool is_running() { return this->is_action_running(); } + /// Stop all instances of this script. + virtual void stop() { this->stop_action(); } + + // Internal function to give scripts readable names. + void set_name(const std::string &name) { name_ = name; } protected: - bool in_stack_{false}; + std::string name_; +}; + +/** A script type for which only a single instance at a time is allowed. + * + * If a new instance is executed while the previous one hasn't finished yet, + * a warning is printed and the new instance is discarded. + */ +class SingleScript : public Script { + public: + void execute() override; +}; + +/** A script type that restarts scripts from the beginning when a new instance is started. + * + * If a new instance is started but another one is already running, the existing + * script is stopped and the new instance starts from the beginning. + */ +class RestartScript : public Script { + public: + void execute() override; +}; + +/** A script type that queues new instances that are created. + * + * Only one instance of the script can be active at a time. + */ +class QueueingScript : public Script, public Component { + public: + void execute() override; + void stop() override; + void loop() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int num_runs_ = 0; + int max_runs_ = 0; +}; + +/** A script type that executes new instances in parallel. + * + * If a new instance is started while previous ones haven't finished yet, + * the new one is exeucted in parallel to the other instances. + */ +class ParallelScript : public Script { + public: + void execute() override; + void set_max_runs(int max_runs) { max_runs_ = max_runs; } + + protected: + int max_runs_ = 0; }; template class ScriptExecuteAction : public Action { public: ScriptExecuteAction(Script *script) : script_(script) {} - void play(Ts... x) override { this->script_->trigger(); } + void play(Ts... x) override { this->script_->execute(); } protected: Script *script_; @@ -43,7 +100,7 @@ template class IsRunningCondition : public Condition { public: explicit IsRunningCondition(Script *parent) : parent_(parent) {} - bool check(Ts... x) override { return this->parent_->script_is_running(); } + bool check(Ts... x) override { return this->parent_->is_running(); } protected: Script *parent_; diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 887f282007..147f76af7d 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -120,7 +120,7 @@ void TemplateCover::set_has_position(bool has_position) { this->has_position_ = void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } void TemplateCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index 5868b30996..d9f95e203c 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -19,7 +19,7 @@ void TemplateSwitch::loop() { } void TemplateSwitch::write_state(bool state) { if (this->prev_trigger_ != nullptr) { - this->prev_trigger_->stop(); + this->prev_trigger_->stop_action(); } if (state) { diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 5d97156e2f..1ffbd3c169 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -223,7 +223,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action) { } if (this->prev_action_trigger_ != nullptr) { - this->prev_action_trigger_->stop(); + this->prev_action_trigger_->stop_action(); this->prev_action_trigger_ = nullptr; } Trigger<> *trig = this->idle_action_trigger_; @@ -262,7 +262,7 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode) { return; if (this->prev_fan_mode_trigger_ != nullptr) { - this->prev_fan_mode_trigger_->stop(); + this->prev_fan_mode_trigger_->stop_action(); this->prev_fan_mode_trigger_ = nullptr; } Trigger<> *trig = this->fan_mode_auto_trigger_; @@ -313,7 +313,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode) { return; if (this->prev_mode_trigger_ != nullptr) { - this->prev_mode_trigger_->stop(); + this->prev_mode_trigger_->stop_action(); this->prev_mode_trigger_ = nullptr; } Trigger<> *trig = this->auto_mode_trigger_; @@ -355,7 +355,7 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo return; if (this->prev_swing_mode_trigger_ != nullptr) { - this->prev_swing_mode_trigger_->stop(); + this->prev_swing_mode_trigger_->stop_action(); this->prev_swing_mode_trigger_ = nullptr; } Trigger<> *trig = this->swing_mode_off_trigger_; diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 6d1de144f5..1aa3c2471a 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -78,7 +78,7 @@ void TimeBasedCover::control(const CoverCall &call) { } void TimeBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { - this->prev_command_trigger_->stop(); + this->prev_command_trigger_->stop_action(); this->prev_command_trigger_ = nullptr; } } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 0649e15f5f..c6ce33dd23 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -812,6 +812,7 @@ def mqtt_qos(value): def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" def validator(value): + # pylint: disable=unsupported-membership-test if comp not in CORE.raw_config: raise Invalid(f"This option requires component {comp}") return value @@ -1125,7 +1126,7 @@ def typed_schema(schemas, **kwargs): def validator(value): if not isinstance(value, dict): raise Invalid("Value must be dict") - if CONF_TYPE not in value: + if key not in value: raise Invalid("type not specified!") value = value.copy() key_v = key_validator(value.pop(key)) @@ -1175,6 +1176,7 @@ class OnlyWith(Optional): @property def default(self): + # pylint: disable=unsupported-membership-test if self._component not in CORE.raw_config: return vol.UNDEFINED return self._default diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 02bd8bb299..6d79480f0f 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -50,18 +50,22 @@ template class Automation; template class Trigger { public: + /// Inform the parent automation that the event has triggered. void trigger(Ts... x) { if (this->automation_parent_ == nullptr) return; this->automation_parent_->trigger(x...); } void set_automation_parent(Automation *automation_parent) { this->automation_parent_ = automation_parent; } - void stop() { + + /// Stop any action connected to this trigger. + void stop_action() { if (this->automation_parent_ == nullptr) return; this->automation_parent_->stop(); } - bool is_running() { + /// Returns true if any action connected to this trigger is running. + bool is_action_running() { if (this->automation_parent_ == nullptr) return false; return this->automation_parent_->is_running(); @@ -87,8 +91,18 @@ template class Action { } this->stop_next_(); } + /// Check if this or any of the following actions are currently running. virtual bool is_running() { return this->num_running_ > 0 || this->is_running_next_(); } + /// The total number of actions that are currently running in this plus any of + /// the following actions in the chain. + int num_running_total() { + int total = this->num_running_; + if (this->next_ != nullptr) + total += this->next_->num_running_total(); + return total; + } + protected: friend ActionList; @@ -123,6 +137,8 @@ template class Action { Action *next_ = nullptr; + /// The number of instances of this sequence in the list of actions + /// that is currently being executed. int num_running_{0}; }; @@ -151,11 +167,19 @@ template class ActionList { this->actions_begin_->stop_complex(); } bool empty() const { return this->actions_begin_ == nullptr; } + + /// Check if any action in this action list is currently running. bool is_running() { if (this->actions_begin_ == nullptr) return false; return this->actions_begin_->is_running(); } + /// Return the number of actions in this action list that are currently running. + int num_running() { + if (this->actions_begin_ == nullptr) + return false; + return this->actions_begin_->num_running_total(); + } protected: template void play_tuple_(const std::tuple &tuple, seq) { this->play(std::get(tuple)...); } @@ -177,6 +201,9 @@ template class Automation { bool is_running() { return this->actions_.is_running(); } + /// Return the number of actions in the action part of this automation that are currently running. + int num_running() { return this->actions_.num_running(); } + protected: Trigger *trigger_; ActionList actions_; From 55388724af7dfc71ebafb49472673e91fe606cd5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 14:21:56 +0200 Subject: [PATCH 149/200] Revert "Sort keys in dicts in output yaml for 'config' command (#1049)" (#1191) --- esphome/config_validation.py | 16 +++------------- esphome/core.py | 3 --- esphome/voluptuous_schema.py | 3 ++- esphome/yaml_util.py | 2 +- pylintrc | 3 +++ 5 files changed, 9 insertions(+), 18 deletions(-) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index c6ce33dd23..1e54c04fe5 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -186,6 +186,7 @@ def ensure_list(*validators): None and empty dictionaries are converted to empty lists. """ user = All(*validators) + list_schema = Schema([user]) def validator(value): check_not_templatable(value) @@ -193,19 +194,7 @@ def ensure_list(*validators): return [] if not isinstance(value, list): return [user(value)] - ret = [] - errs = [] - for i, val in enumerate(value): - try: - with prepend_path([i]): - ret.append(user(val)) - except MultipleInvalid as err: - errs.extend(err.errors) - except Invalid as err: - errs.append(err) - if errs: - raise MultipleInvalid(errs) - return ret + return list_schema(value) return validator @@ -811,6 +800,7 @@ def mqtt_qos(value): def requires_component(comp): """Validate that this option can only be specified when the component `comp` is loaded.""" + # pylint: disable=unsupported-membership-test def validator(value): # pylint: disable=unsupported-membership-test if comp not in CORE.raw_config: diff --git a/esphome/core.py b/esphome/core.py index 9b0b8cefa3..0065b750c4 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -553,7 +553,6 @@ class EsphomeCore: if self.config is None: raise ValueError("Config has not been loaded yet") - # pylint: disable=unsupported-membership-test,unsubscriptable-object if 'wifi' in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] @@ -567,7 +566,6 @@ class EsphomeCore: if self.config is None: raise ValueError("Config has not been loaded yet") - # pylint: disable=unsubscriptable-object if CONF_COMMENT in self.config[CONF_ESPHOME]: return self.config[CONF_ESPHOME][CONF_COMMENT] @@ -584,7 +582,6 @@ class EsphomeCore: if self.config is None: raise ValueError("Config has not been loaded yet") - # pylint: disable=unsubscriptable-object return self.config[CONF_ESPHOME][CONF_ARDUINO_VERSION] @property diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index c30447a427..ce13f0ceb0 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -52,7 +52,8 @@ class _Schema(vol.Schema): all_required_keys = {key for key in schema if isinstance(key, vol.Required)} # Keys that may have defaults - all_default_keys = {key for key in schema if isinstance(key, vol.Optional)} + # This is a list because sets do not guarantee insertion order + all_default_keys = [key for key in schema if isinstance(key, vol.Optional)] # Recursively compile schema _compiled_schema = {} diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 1758e739db..053fba6274 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -338,7 +338,7 @@ class ESPHomeDumper(yaml.SafeDumper): # pylint: disable=too-many-ancestors self.represented_objects[self.alias_key] = node best_style = True if hasattr(mapping, 'items'): - mapping = sorted(mapping.items(), key=lambda item: item[0]) + mapping = list(mapping.items()) for item_key, item_value in mapping: node_key = self.represent_data(item_key) node_value = self.represent_data(item_value) diff --git a/pylintrc b/pylintrc index c65a9a7cd9..00ffdc9f9a 100644 --- a/pylintrc +++ b/pylintrc @@ -25,3 +25,6 @@ disable= stop-iteration-return, no-self-use, import-outside-toplevel, + # Broken + unsupported-membership-test, + unsubscriptable-object, From 0fc8e8d483bb4df1c0e1ca3b77d318476b5a6b99 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 14:22:56 +0200 Subject: [PATCH 150/200] Fix SN74HC595 doesn't use ESPHome HAL and add lint checks for it (#1188) --- esphome/components/adc/adc_sensor.cpp | 4 +- .../components/max7219digit/max7219digit.cpp | 31 +++++----- esphome/components/mcp3008/mcp3008.cpp | 8 +-- esphome/components/sn74hc595/sn74hc595.cpp | 35 ++++++----- esphome/components/sx1509/sx1509.cpp | 2 +- .../ultrasonic/ultrasonic_sensor.cpp | 3 +- script/ci-custom.py | 59 ++++++++++++++++++- 7 files changed, 105 insertions(+), 37 deletions(-) diff --git a/esphome/components/adc/adc_sensor.cpp b/esphome/components/adc/adc_sensor.cpp index 2c448d0392..9b1f452131 100644 --- a/esphome/components/adc/adc_sensor.cpp +++ b/esphome/components/adc/adc_sensor.cpp @@ -58,7 +58,7 @@ void ADCSensor::update() { } float ADCSensor::sample() { #ifdef ARDUINO_ARCH_ESP32 - float value_v = analogRead(this->pin_) / 4095.0f; + float value_v = analogRead(this->pin_) / 4095.0f; // NOLINT switch (this->attenuation_) { case ADC_0db: value_v *= 1.1; @@ -80,7 +80,7 @@ float ADCSensor::sample() { #ifdef USE_ADC_SENSOR_VCC return ESP.getVcc() / 1024.0f; #else - return analogRead(this->pin_) / 1024.0f; + return analogRead(this->pin_) / 1024.0f; // NOLINT #endif #endif } diff --git a/esphome/components/max7219digit/max7219digit.cpp b/esphome/components/max7219digit/max7219digit.cpp index dff1d27370..ab9be4ad36 100644 --- a/esphome/components/max7219digit/max7219digit.cpp +++ b/esphome/components/max7219digit/max7219digit.cpp @@ -101,7 +101,7 @@ void MAX7219Component::loop() { } void MAX7219Component::display() { - byte pixels[8]; + uint8_t pixels[8]; // Run this loop for every MAX CHIP (GRID OF 64 leds) // Run this routine for the rows of every chip 8x row 0 top to 7 bottom // Fill the pixel parameter with diplay data @@ -205,29 +205,32 @@ void MAX7219Component::scroll_left() { this->stepsleft_++; } -void MAX7219Component::send_char(byte chip, byte data) { +void MAX7219Component::send_char(uint8_t chip, uint8_t data) { // get this character from PROGMEM - for (byte i = 0; i < 8; i++) + for (uint8_t i = 0; i < 8; i++) this->max_displaybuffer_[chip * 8 + i] = pgm_read_byte(&MAX7219_DOT_MATRIX_FONT[data][i]); } // end of send_char // send one character (data) to position (chip) -void MAX7219Component::send64pixels(byte chip, const byte pixels[8]) { - for (byte col = 0; col < 8; col++) { // RUN THIS LOOP 8 times until column is 7 - this->enable(); // start sending by enabling SPI - for (byte i = 0; i < chip; i++) // send extra NOPs to push the pixels out to extra displays +void MAX7219Component::send64pixels(uint8_t chip, const uint8_t pixels[8]) { + for (uint8_t col = 0; col < 8; col++) { // RUN THIS LOOP 8 times until column is 7 + this->enable(); // start sending by enabling SPI + for (uint8_t i = 0; i < chip; i++) // send extra NOPs to push the pixels out to extra displays this->send_byte_(MAX7219_REGISTER_NOOP, MAX7219_REGISTER_NOOP); // run this loop unit the matching chip is reached - byte b = 0; // rotate pixels 90 degrees -- set byte to 0 + uint8_t b = 0; // rotate pixels 90 degrees -- set byte to 0 if (this->orientation_ == 0) { - for (byte i = 0; i < 8; i++) // run this loop 8 times for all the pixels[8] received - b |= bitRead(pixels[i], col) << (7 - i); // change the column bits into row bits + for (uint8_t i = 0; i < 8; i++) { + // run this loop 8 times for all the pixels[8] received + b |= ((pixels[i] >> col) & 1) << (7 - i); // change the column bits into row bits + } } else if (this->orientation_ == 1) { b = pixels[col]; } else if (this->orientation_ == 2) { - for (byte i = 0; i < 8; i++) - b |= bitRead(pixels[i], 7 - col) << (7 - i); + for (uint8_t i = 0; i < 8; i++) { + b |= ((pixels[i] >> (7 - col)) << (7 - i)); + } } else { b = pixels[7 - col]; } @@ -246,8 +249,8 @@ void MAX7219Component::send64pixels(byte chip, const byte pixels[8]) { uint8_t MAX7219Component::printdigit(const char *str) { return this->printdigit(0, str); } uint8_t MAX7219Component::printdigit(uint8_t start_pos, const char *s) { - byte chip; - for (chip = start_pos; chip < this->num_chips_ && *s; chip++) + uint8_t chip = start_pos; + for (; chip < this->num_chips_ && *s; chip++) send_char(chip, *s++); // space out rest while (chip < (this->num_chips_)) diff --git a/esphome/components/mcp3008/mcp3008.cpp b/esphome/components/mcp3008/mcp3008.cpp index 70484e8818..a4d019ab8f 100644 --- a/esphome/components/mcp3008/mcp3008.cpp +++ b/esphome/components/mcp3008/mcp3008.cpp @@ -19,11 +19,11 @@ void MCP3008::dump_config() { } float MCP3008::read_data_(uint8_t pin) { - byte data_msb = 0; - byte data_lsb = 0; + uint8_t data_msb = 0; + uint8_t data_lsb = 0; - byte command = ((0x01 << 7) | // start bit - ((pin & 0x07) << 4)); // channel number + uint8_t command = ((0x01 << 7) | // start bit + ((pin & 0x07) << 4)); // channel number this->enable(); diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index edf8989149..d82c5d5036 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -10,17 +10,17 @@ void SN74HC595Component::setup() { ESP_LOGCONFIG(TAG, "Setting up SN74HC595..."); if (this->have_oe_pin_) { // disable output - pinMode(this->oe_pin_->get_pin(), OUTPUT); - digitalWrite(this->oe_pin_->get_pin(), HIGH); + this->oe_pin_->pin_mode(OUTPUT); + this->oe_pin_->digital_write(true); } // initialize output pins - pinMode(this->clock_pin_->get_pin(), OUTPUT); - pinMode(this->data_pin_->get_pin(), OUTPUT); - pinMode(this->latch_pin_->get_pin(), OUTPUT); - digitalWrite(this->clock_pin_->get_pin(), LOW); - digitalWrite(this->data_pin_->get_pin(), LOW); - digitalWrite(this->latch_pin_->get_pin(), LOW); + this->clock_pin_->pin_mode(OUTPUT); + this->data_pin_->pin_mode(OUTPUT); + this->latch_pin_->pin_mode(OUTPUT); + this->clock_pin_->digital_write(LOW); + this->data_pin_->digital_write(LOW); + this->latch_pin_->digital_write(LOW); // send state to shift register this->write_gpio_(); @@ -28,26 +28,33 @@ void SN74HC595Component::setup() { void SN74HC595Component::dump_config() { ESP_LOGCONFIG(TAG, "SN74HC595:"); } -bool SN74HC595Component::digital_read_(uint8_t pin) { return bitRead(this->output_bits_, pin); } +bool SN74HC595Component::digital_read_(uint8_t pin) { return this->output_bits_ >> pin; } void SN74HC595Component::digital_write_(uint8_t pin, bool value) { - bitWrite(this->output_bits_, pin, value); + uint32_t mask = 1UL << pin; + this->output_bits_ &= ~mask; + if (value) + this->output_bits_ |= mask; this->write_gpio_(); } bool SN74HC595Component::write_gpio_() { for (int i = this->sr_count_ - 1; i >= 0; i--) { uint8_t data = (uint8_t)(this->output_bits_ >> (8 * i) & 0xff); - shiftOut(this->data_pin_->get_pin(), this->clock_pin_->get_pin(), MSBFIRST, data); + for (int j = 0; j < 8; j++) { + this->data_pin_->digital_write(data & (1 << (7 - j))); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } } // pulse latch to activate new values - digitalWrite(this->latch_pin_->get_pin(), HIGH); - digitalWrite(this->latch_pin_->get_pin(), LOW); + this->latch_pin_->digital_write(true); + this->latch_pin_->digital_write(false); // enable output if configured if (this->have_oe_pin_) { - digitalWrite(this->oe_pin_->get_pin(), LOW); + this->oe_pin_->digital_write(false); } return true; diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp index 2806a1cac2..0d6ffbb9b8 100644 --- a/esphome/components/sx1509/sx1509.cpp +++ b/esphome/components/sx1509/sx1509.cpp @@ -144,7 +144,7 @@ void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_fr osc_source = (osc_source & 0b11) << 5; // 2-bit value, bits 6:5 osc_pin_function = (osc_pin_function & 1) << 4; // 1-bit value bit 4 osc_freq_out = (osc_freq_out & 0b1111); // 4-bit value, bits 3:0 - byte reg_clock = osc_source | osc_pin_function | osc_freq_out; + uint8_t reg_clock = osc_source | osc_pin_function | osc_freq_out; this->write_byte(REG_CLOCK, reg_clock); osc_divider = constrain(osc_divider, 1, 7); diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index f8130f7d1f..c573c21863 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -17,7 +17,8 @@ void UltrasonicSensorComponent::update() { delayMicroseconds(this->pulse_time_us_); this->trigger_pin_->digital_write(false); - uint32_t time = pulseIn(this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); + uint32_t time = pulseIn( // NOLINT + this->echo_pin_->get_pin(), uint8_t(!this->echo_pin_->is_inverted()), this->timeout_us_); ESP_LOGV(TAG, "Echo took %uµs", time); diff --git a/script/ci-custom.py b/script/ci-custom.py index cde026b5d4..3954eea0de 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -255,6 +255,63 @@ def lint_conf_from_const_py(fname, match): "const.py directly.".format(highlight(name))) +RAW_PIN_ACCESS_RE = r'^\s(pinMode|digitalWrite|digitalRead)\((.*)->get_pin\(\),\s*([^)]+).*\)' + + +@lint_re_check(RAW_PIN_ACCESS_RE, include=cpp_include) +def lint_no_raw_pin_access(fname, match): + func = match.group(1) + pin = match.group(2) + mode = match.group(3) + new_func = { + 'pinMode': 'pin_mode', + 'digitalWrite': 'digital_write', + 'digitalRead': 'digital_read', + }[func] + new_code = highlight(f'{pin}->{new_func}({mode})') + return (f"Don't use raw {func} calls. Instead, use the `->{new_func}` function: {new_code}") + + +# Functions from Arduino framework that are forbidden to use directly +ARDUINO_FORBIDDEN = [ + 'digitalWrite', 'digitalRead', 'pinMode', + 'shiftOut', 'shiftIn', + 'radians', 'degrees', + 'interrupts', 'noInterrupts', + 'lowByte', 'highByte', + 'bitRead', 'bitSet', 'bitClear', 'bitWrite', + 'bit', 'analogRead', 'analogWrite', + 'pulseIn', 'pulseInLong', + 'tone', +] +ARDUINO_FORBIDDEN_RE = r'[^\w\d](' + r'|'.join(ARDUINO_FORBIDDEN) + r')\(.*' + + +@lint_re_check(ARDUINO_FORBIDDEN_RE, include=cpp_include, exclude=[ + 'esphome/components/mqtt/custom_mqtt_device.h', + 'esphome/core/esphal.*', +]) +def lint_no_arduino_framework_functions(fname, match): + nolint = highlight("// NOLINT") + return ( + f"The function {highlight(match.group(1))} from the Arduino framework is forbidden to be " + f"used directly in the ESPHome codebase. Please use ESPHome's abstractions and equivalent " + f"C++ instead.\n" + f"\n" + f"(If the function is strictly necessary, please add `{nolint}` to the end of the line)" + ) + + +@lint_re_check(r'[^\w\d]byte\s+[\w\d]+\s*=.*', include=cpp_include, exclude={ + 'esphome/components/tuya/tuya.h', +}) +def lint_no_byte_datatype(fname, match): + return ( + f"The datatype {highlight('byte')} is not allowed to be used in ESPHome. " + f"Please use {highlight('uint8_t')} instead." + ) + + @lint_post_check def lint_constants_usage(): errors = [] @@ -328,7 +385,7 @@ def lint_pragma_once(fname, content): return None -@lint_re_check(r'(whitelist|blacklist|slave)', exclude=['script/ci-custom.py'], +@lint_re_check(r'(whitelist|blacklist|slave)', exclude=['script/ci-custom.py'], flags=re.IGNORECASE | re.MULTILINE) def lint_inclusive_language(fname, match): # From https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=49decddd39e5f6132ccd7d9fdc3d7c470b0061bb From 5887fe8302bbd160f18b081be8a6dfa378f65190 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 14:40:47 +0200 Subject: [PATCH 151/200] Enlarge ESP32 app partitions (#1197) --- esphome/writer.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/esphome/writer.py b/esphome/writer.py index 67b1332e8f..041d1c1127 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -180,6 +180,16 @@ def gather_build_flags(): return list(sorted(list(build_flags))) +ESP32_LARGE_PARTITIONS_CSV = """\ +nvs, data, nvs, 0x009000, 0x005000, +otadata, data, ota, 0x00e000, 0x002000, +app0, app, ota_0, 0x010000, 0x1C0000, +app1, app, ota_1, 0x1D0000, 0x1C0000, +eeprom, data, 0x99, 0x390000, 0x001000, +spiffs, data, spiffs, 0x391000, 0x00F000 +""" + + def get_ini_content(): lib_deps = gather_lib_deps() build_flags = gather_build_flags() @@ -196,15 +206,9 @@ def get_ini_content(): if CORE.is_esp32: data['board_build.partitions'] = "partitions.csv" partitions_csv = CORE.relative_build_path('partitions.csv') - if not os.path.isfile(partitions_csv): - with open(partitions_csv, "w") as f: - f.write("nvs, data, nvs, 0x009000, 0x005000,\n") - f.write("otadata, data, ota, 0x00e000, 0x002000,\n") - f.write("app0, app, ota_0, 0x010000, 0x190000,\n") - f.write("app1, app, ota_1, 0x200000, 0x190000,\n") - f.write("eeprom, data, 0x99, 0x390000, 0x001000,\n") - f.write("spiffs, data, spiffs, 0x391000, 0x00F000\n") + write_file_if_changed(partitions_csv, ESP32_LARGE_PARTITIONS_CSV) + # pylint: disable=unsubscriptable-object if CONF_BOARD_FLASH_MODE in CORE.config[CONF_ESPHOME]: flash_mode = CORE.config[CONF_ESPHOME][CONF_BOARD_FLASH_MODE] data['board_build.flash_mode'] = flash_mode From 4996967c79ad20778184c7869d35174110095ca0 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 15:57:18 +0200 Subject: [PATCH 152/200] Add CODEOWNERS mechanism (#1199) --- .github/workflows/ci.yml | 2 + .github/workflows/release-dev.yml | 2 + .github/workflows/release.yml | 2 + CODEOWNERS | 54 +++++++++++++++ esphome/components/adc/__init__.py | 1 + esphome/components/api/__init__.py | 1 + esphome/components/async_tcp/__init__.py | 2 + esphome/components/bang_bang/__init__.py | 1 + esphome/components/binary_sensor/__init__.py | 1 + esphome/components/captive_portal/__init__.py | 1 + esphome/components/climate/__init__.py | 1 + esphome/components/cover/__init__.py | 1 + esphome/components/debug/__init__.py | 1 + esphome/components/dht/__init__.py | 1 + .../exposure_notifications/__init__.py | 1 + esphome/components/fastled_base/__init__.py | 1 + esphome/components/globals/__init__.py | 1 + esphome/components/gpio/__init__.py | 1 + esphome/components/homeassistant/__init__.py | 1 + esphome/components/i2c/__init__.py | 1 + esphome/components/integration/__init__.py | 1 + esphome/components/interval/__init__.py | 1 + esphome/components/json/__init__.py | 1 + esphome/components/ledc/__init__.py | 1 + esphome/components/light/__init__.py | 1 + esphome/components/logger/__init__.py | 1 + esphome/components/network/__init__.py | 1 + esphome/components/ota/__init__.py | 1 + esphome/components/output/__init__.py | 2 + esphome/components/pid/__init__.py | 1 + esphome/components/pn532/__init__.py | 1 + esphome/components/power_supply/__init__.py | 1 + esphome/components/restart/__init__.py | 1 + esphome/components/script/__init__.py | 1 + esphome/components/sensor/__init__.py | 1 + esphome/components/shutdown/__init__.py | 1 + esphome/components/spi/__init__.py | 1 + esphome/components/substitutions/__init__.py | 1 + esphome/components/sun/__init__.py | 1 + esphome/components/switch/__init__.py | 1 + esphome/components/time/__init__.py | 1 + esphome/components/uart/__init__.py | 1 + esphome/components/ultrasonic/__init__.py | 1 + esphome/components/version/__init__.py | 1 + .../components/web_server_base/__init__.py | 1 + esphome/config.py | 4 ++ esphome/helpers.py | 28 +++++--- script/build_codeowners.py | 68 +++++++++++++++++++ 48 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 CODEOWNERS create mode 100755 script/build_codeowners.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d6798314f..ce6a120f7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,8 @@ jobs: run: script/ci-custom.py - name: Lint Python run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check test: runs-on: ubuntu-latest diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index 896b6cddb4..c289c44afc 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -98,6 +98,8 @@ jobs: run: script/ci-custom.py - name: Lint Python run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check test: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 651064047a..d13a6fd55d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,6 +97,8 @@ jobs: run: script/ci-custom.py - name: Lint Python run: script/lint-python + - name: Lint CODEOWNERS + run: script/build_codeowners.py --check test: runs-on: ubuntu-latest diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..79b2d0503c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,54 @@ +# This file is generated by script/build_codeowners.py +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# +# Every time an issue is created with a label corresponding to an integration, +# the integration's code owner is automatically notified. + +# Core Code +setup.py @esphome/core +esphome/*.py @esphome/core +esphome/core/* @esphome/core + +# Integrations +esphome/components/adc/* @esphome/core +esphome/components/api/* @OttoWinter +esphome/components/async_tcp/* @OttoWinter +esphome/components/bang_bang/* @OttoWinter +esphome/components/binary_sensor/* @esphome/core +esphome/components/captive_portal/* @OttoWinter +esphome/components/climate/* @esphome/core +esphome/components/cover/* @esphome/core +esphome/components/debug/* @OttoWinter +esphome/components/dht/* @OttoWinter +esphome/components/exposure_notifications/* @OttoWinter +esphome/components/fastled_base/* @OttoWinter +esphome/components/globals/* @esphome/core +esphome/components/gpio/* @esphome/core +esphome/components/homeassistant/* @OttoWinter +esphome/components/i2c/* @esphome/core +esphome/components/integration/* @OttoWinter +esphome/components/interval/* @esphome/core +esphome/components/json/* @OttoWinter +esphome/components/ledc/* @OttoWinter +esphome/components/light/* @esphome/core +esphome/components/logger/* @esphome/core +esphome/components/network/* @esphome/core +esphome/components/ota/* @esphome/core +esphome/components/output/* @esphome/core +esphome/components/pid/* @OttoWinter +esphome/components/pn532/* @OttoWinter +esphome/components/power_supply/* @esphome/core +esphome/components/restart/* @esphome/core +esphome/components/script/* @esphome/core +esphome/components/sensor/* @esphome/core +esphome/components/shutdown/* @esphome/core +esphome/components/spi/* @esphome/core +esphome/components/substitutions/* @esphome/core +esphome/components/sun/* @OttoWinter +esphome/components/switch/* @esphome/core +esphome/components/time/* @OttoWinter +esphome/components/uart/* @esphome/core +esphome/components/ultrasonic/* @OttoWinter +esphome/components/version/* @esphome/core +esphome/components/web_server_base/* @OttoWinter diff --git a/esphome/components/adc/__init__.py b/esphome/components/adc/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/adc/__init__.py +++ b/esphome/components/adc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index eef60602ba..23cfa51d2b 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -8,6 +8,7 @@ from esphome.core import coroutine_with_priority DEPENDENCIES = ['network'] AUTO_LOAD = ['async_tcp'] +CODEOWNERS = ['@OttoWinter'] api_ns = cg.esphome_ns.namespace('api') APIServer = api_ns.class_('APIServer', cg.Component, cg.Controller) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index cf9d2f1585..8af33bc4ef 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -2,6 +2,8 @@ import esphome.codegen as cg from esphome.core import CORE, coroutine_with_priority +CODEOWNERS = ['@OttoWinter'] + @coroutine_with_priority(200.0) def to_code(config): diff --git a/esphome/components/bang_bang/__init__.py b/esphome/components/bang_bang/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/bang_bang/__init__.py +++ b/esphome/components/bang_bang/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 2b5dd302c4..8361ee8004 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -11,6 +11,7 @@ from esphome.const import CONF_DEVICE_CLASS, CONF_FILTERS, \ from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry +CODEOWNERS = ['@esphome/core'] DEVICE_CLASSES = [ '', 'battery', 'cold', 'connectivity', 'door', 'garage_door', 'gas', 'heat', 'light', 'lock', 'moisture', 'motion', 'moving', 'occupancy', diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 52885ae449..bb8b2198a1 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -7,6 +7,7 @@ from esphome.core import coroutine_with_priority AUTO_LOAD = ['web_server_base'] DEPENDENCIES = ['wifi'] +CODEOWNERS = ['@OttoWinter'] captive_portal_ns = cg.esphome_ns.namespace('captive_portal') CaptivePortal = captive_portal_ns.class_('CaptivePortal', cg.Component) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 843b888218..38e254bb9d 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -10,6 +10,7 @@ from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True +CODEOWNERS = ['@esphome/core'] climate_ns = cg.esphome_ns.namespace('climate') Climate = climate_ns.class_('Climate', cg.Nameable) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 85f7f746ba..f39e7ba540 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -9,6 +9,7 @@ from esphome.core import CORE, coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True +CODEOWNERS = ['@esphome/core'] DEVICE_CLASSES = [ '', 'awning', 'blind', 'curtain', 'damper', 'door', 'garage', 'gate', 'shade', 'shutter', 'window' diff --git a/esphome/components/debug/__init__.py b/esphome/components/debug/__init__.py index a40dadb5c2..d43b0de06a 100644 --- a/esphome/components/debug/__init__.py +++ b/esphome/components/debug/__init__.py @@ -2,6 +2,7 @@ import esphome.config_validation as cv import esphome.codegen as cg from esphome.const import CONF_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['logger'] debug_ns = cg.esphome_ns.namespace('debug') diff --git a/esphome/components/dht/__init__.py b/esphome/components/dht/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/dht/__init__.py +++ b/esphome/components/dht/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/exposure_notifications/__init__.py b/esphome/components/exposure_notifications/__init__.py index de9c3a58df..8175a2d3aa 100644 --- a/esphome/components/exposure_notifications/__init__.py +++ b/esphome/components/exposure_notifications/__init__.py @@ -4,6 +4,7 @@ import esphome.config_validation as cv from esphome.components import esp32_ble_tracker from esphome.const import CONF_TRIGGER_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['esp32_ble_tracker'] exposure_notifications_ns = cg.esphome_ns.namespace('exposure_notifications') diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index ffa49f43c2..e438876d33 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -4,6 +4,7 @@ from esphome.components import light from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MAX_REFRESH_RATE from esphome.core import coroutine +CODEOWNERS = ['@OttoWinter'] fastled_base_ns = cg.esphome_ns.namespace('fastled_base') FastLEDLightOutput = fastled_base_ns.class_('FastLEDLightOutput', light.AddressableLight) diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index e59a7e6acb..f1c4f4faf2 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -5,6 +5,7 @@ from esphome import codegen as cg from esphome.const import CONF_ID, CONF_INITIAL_VALUE, CONF_RESTORE_VALUE, CONF_TYPE, CONF_VALUE from esphome.core import coroutine_with_priority +CODEOWNERS = ['@esphome/core'] globals_ns = cg.esphome_ns.namespace('globals') GlobalsComponent = globals_ns.class_('GlobalsComponent', cg.Component) GlobalVarSetAction = globals_ns.class_('GlobalVarSetAction', automation.Action) diff --git a/esphome/components/gpio/__init__.py b/esphome/components/gpio/__init__.py index ccb920e654..c36ba8f433 100644 --- a/esphome/components/gpio/__init__.py +++ b/esphome/components/gpio/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg +CODEOWNERS = ['@esphome/core'] gpio_ns = cg.esphome_ns.namespace('gpio') diff --git a/esphome/components/homeassistant/__init__.py b/esphome/components/homeassistant/__init__.py index 9fb3836d49..69d759b977 100644 --- a/esphome/components/homeassistant/__init__.py +++ b/esphome/components/homeassistant/__init__.py @@ -1,3 +1,4 @@ import esphome.codegen as cg +CODEOWNERS = ['@OttoWinter'] homeassistant_ns = cg.esphome_ns.namespace('homeassistant') diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 0c71f18019..91c6a97190 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_SCAN, CONF_SCL, CONF_SDA CONF_I2C_ID from esphome.core import coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] i2c_ns = cg.esphome_ns.namespace('i2c') I2CComponent = i2c_ns.class_('I2CComponent', cg.Component) I2CDevice = i2c_ns.class_('I2CDevice') diff --git a/esphome/components/integration/__init__.py b/esphome/components/integration/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/integration/__init__.py +++ b/esphome/components/integration/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/interval/__init__.py b/esphome/components/interval/__init__.py index e0816b7407..be37526ad1 100644 --- a/esphome/components/interval/__init__.py +++ b/esphome/components/interval/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.const import CONF_ID, CONF_INTERVAL +CODEOWNERS = ['@esphome/core'] interval_ns = cg.esphome_ns.namespace('interval') IntervalTrigger = interval_ns.class_('IntervalTrigger', automation.Trigger.template(), cg.PollingComponent) diff --git a/esphome/components/json/__init__.py b/esphome/components/json/__init__.py index f719b05340..63bc16dfa2 100644 --- a/esphome/components/json/__init__.py +++ b/esphome/components/json/__init__.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.core import coroutine_with_priority +CODEOWNERS = ['@OttoWinter'] json_ns = cg.esphome_ns.namespace('json') diff --git a/esphome/components/ledc/__init__.py b/esphome/components/ledc/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/ledc/__init__.py +++ b/esphome/components/ledc/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index 2a44b044b9..9d58fa47bf 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -14,6 +14,7 @@ from .types import ( # noqa LightState, AddressableLightState, light_ns, LightOutput, AddressableLight, \ LightTurnOnTrigger, LightTurnOffTrigger) +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True LightRestoreMode = light_ns.enum('LightRestoreMode') diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 329c515aee..c447b0eee1 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -8,6 +8,7 @@ from esphome.const import CONF_ARGS, CONF_BAUD_RATE, CONF_FORMAT, CONF_HARDWARE_ CONF_LEVEL, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, CONF_TRIGGER_ID, CONF_TX_BUFFER_SIZE from esphome.core import CORE, EsphomeError, Lambda, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] logger_ns = cg.esphome_ns.namespace('logger') LOG_LEVELS = { 'NONE': cg.global_ns.ESPHOME_LOG_LEVEL_NONE, diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 4486d62e1d..a380e32bfe 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1 +1,2 @@ # Dummy package to allow components to depend on network +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 869de777d6..e6bcff045f 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_PASSWORD, CONF_PORT, CONF_SAFE_MODE from esphome.core import CORE, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] DEPENDENCIES = ['network'] ota_ns = cg.esphome_ns.namespace('ota') diff --git a/esphome/components/output/__init__.py b/esphome/components/output/__init__.py index b406f62ee1..34cb7c3f7a 100644 --- a/esphome/components/output/__init__.py +++ b/esphome/components/output/__init__.py @@ -7,6 +7,8 @@ from esphome.const import CONF_ID, CONF_INVERTED, CONF_LEVEL, CONF_MAX_POWER, \ CONF_MIN_POWER, CONF_POWER_SUPPLY from esphome.core import CORE, coroutine + +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True BINARY_OUTPUT_SCHEMA = cv.Schema({ diff --git a/esphome/components/pid/__init__.py b/esphome/components/pid/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/pid/__init__.py +++ b/esphome/components/pid/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/pn532/__init__.py b/esphome/components/pn532/__init__.py index 931b6ada6b..cbd41d11cc 100644 --- a/esphome/components/pn532/__init__.py +++ b/esphome/components/pn532/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.components import spi from esphome.const import CONF_ID, CONF_ON_TAG, CONF_TRIGGER_ID +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['spi'] AUTO_LOAD = ['binary_sensor'] MULTI_CONF = True diff --git a/esphome/components/power_supply/__init__.py b/esphome/components/power_supply/__init__.py index 5646ffdc0b..d502788637 100644 --- a/esphome/components/power_supply/__init__.py +++ b/esphome/components/power_supply/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_ENABLE_TIME, CONF_ID, CONF_KEEP_ON_TIME, CONF_PIN +CODEOWNERS = ['@esphome/core'] power_supply_ns = cg.esphome_ns.namespace('power_supply') PowerSupply = power_supply_ns.class_('PowerSupply', cg.Component) MULTI_CONF = True diff --git a/esphome/components/restart/__init__.py b/esphome/components/restart/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/restart/__init__.py +++ b/esphome/components/restart/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index eb337d7681..d33f64a23b 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.automation import maybe_simple_id from esphome.const import CONF_ID, CONF_MODE +CODEOWNERS = ['@esphome/core'] script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 605f72a103..671bbe2b09 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -12,6 +12,7 @@ from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_B from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True diff --git a/esphome/components/shutdown/__init__.py b/esphome/components/shutdown/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/shutdown/__init__.py +++ b/esphome/components/shutdown/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index 71dac0f8c9..e7f8bc378d 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_CLK_PIN, CONF_ID, CONF_MISO_PIN, CONF_MOSI_PIN, C CONF_CS_PIN from esphome.core import coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] spi_ns = cg.esphome_ns.namespace('spi') SPIComponent = spi_ns.class_('SPIComponent', cg.Component) SPIDevice = spi_ns.class_('SPIDevice') diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index a6f70fe928..9c5d444b2c 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -5,6 +5,7 @@ import esphome.config_validation as cv from esphome import core from esphome.const import CONF_SUBSTITUTIONS +CODEOWNERS = ['@esphome/core'] _LOGGER = logging.getLogger(__name__) VALID_SUBSTITUTIONS_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' \ diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index e4d2023a8e..a92442ea56 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +CODEOWNERS = ['@OttoWinter'] sun_ns = cg.esphome_ns.namespace('sun') Sun = sun_ns.class_('Sun') diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index 3870631e13..7378b2a140 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -7,6 +7,7 @@ from esphome.const import CONF_ICON, CONF_ID, CONF_INTERNAL, CONF_INVERTED, CONF CONF_ON_TURN_ON, CONF_TRIGGER_ID, CONF_MQTT_ID, CONF_NAME from esphome.core import CORE, coroutine, coroutine_with_priority +CODEOWNERS = ['@esphome/core'] IS_PLATFORM_COMPONENT = True switch_ns = cg.esphome_ns.namespace('switch_') diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 6283392103..49ed53c47e 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -17,6 +17,7 @@ from esphome.core import coroutine, coroutine_with_priority _LOGGER = logging.getLogger(__name__) +CODEOWNERS = ['@OttoWinter'] IS_PLATFORM_COMPONENT = True time_ns = cg.esphome_ns.namespace('time') diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 65a9a3eaf8..b83aedad9f 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_RX_PIN, CONF_TX_PIN, CON CONF_DATA, CONF_RX_BUFFER_SIZE from esphome.core import CORE, coroutine +CODEOWNERS = ['@esphome/core'] uart_ns = cg.esphome_ns.namespace('uart') UARTComponent = uart_ns.class_('UARTComponent', cg.Component) UARTDevice = uart_ns.class_('UARTDevice') diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index e69de29bb2..6f14e10033 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@OttoWinter'] diff --git a/esphome/components/version/__init__.py b/esphome/components/version/__init__.py index e69de29bb2..63db7aee2e 100644 --- a/esphome/components/version/__init__.py +++ b/esphome/components/version/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ['@esphome/core'] diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index d2faaf7162..f7b2238552 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.const import CONF_ID from esphome.core import coroutine_with_priority, CORE +CODEOWNERS = ['@OttoWinter'] DEPENDENCIES = ['network'] AUTO_LOAD = ['async_tcp'] diff --git a/esphome/config.py b/esphome/config.py index 84d3ca3803..85f48c64b7 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -66,6 +66,10 @@ class ComponentManifest: def auto_load(self): return getattr(self.module, 'AUTO_LOAD', []) + @property + def codeowners(self) -> List[str]: + return getattr(self.module, 'CODEOWNERS', []) + def _get_flags_set(self, name, config): if not hasattr(self.module, name): return set() diff --git a/esphome/helpers.py b/esphome/helpers.py index 6413a25e01..b30ace89d2 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -2,6 +2,9 @@ import codecs import logging import os +from pathlib import Path +from typing import Union +import tempfile _LOGGER = logging.getLogger(__name__) @@ -168,15 +171,21 @@ def read_file(path): raise EsphomeError(f"Error reading file {path}: {err}") -def _write_file(path, text): - import tempfile - directory = os.path.dirname(path) - mkdir_p(directory) +def _write_file(path: Union[Path, str], text: Union[str, bytes]): + """Atomically writes `text` to the given path. - tmp_path = None + Automatically creates all parent directories. + """ + if not isinstance(path, Path): + path = Path(path) data = text if isinstance(text, str): data = text.encode() + + directory = path.parent + directory.mkdir(exist_ok=True, parents=True) + + tmp_path = None try: with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: tmp_path = f_handle.name @@ -193,7 +202,7 @@ def _write_file(path, text): _LOGGER.error("Write file cleanup failed: %s", err) -def write_file(path, text): +def write_file(path: Union[Path, str], text: str): try: _write_file(path, text) except OSError: @@ -201,9 +210,12 @@ def write_file(path, text): raise EsphomeError(f"Could not write file at {path}") -def write_file_if_changed(path, text): +def write_file_if_changed(path: Union[Path, str], text: str): + if not isinstance(path, Path): + path = Path(path) + src_content = None - if os.path.isfile(path): + if path.is_file(): src_content = read_file(path) if src_content != text: write_file(path, text) diff --git a/script/build_codeowners.py b/script/build_codeowners.py new file mode 100755 index 0000000000..3f2bf5d8c3 --- /dev/null +++ b/script/build_codeowners.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys +import argparse + +from esphome.helpers import write_file_if_changed +from esphome.config import get_component +from esphome.core import CORE + +parser = argparse.ArgumentParser() +parser.add_argument('--check', help="Check if the CODEOWNERS file is up to date.", + action='store_true') +args = parser.parse_args() + +# The root directory of the repo +root = Path(__file__).parent.parent +components_dir = root / 'esphome' / 'components' + +BASE = """ +# This file is generated by script/build_codeowners.py +# People marked here will be automatically requested for a review +# when the code that they own is touched. +# +# Every time an issue is created with a label corresponding to an integration, +# the integration's code owner is automatically notified. + +# Core Code +setup.py @esphome/core +esphome/*.py @esphome/core +esphome/core/* @esphome/core + +# Integrations +""".strip() + +parts = [BASE] + +# Fake some diretory so that get_component works +CORE.config_path = str(root) + +for path in sorted(components_dir.iterdir()): + if not path.is_dir(): + continue + if not (path / '__init__.py').is_file(): + continue + name = path.name + comp = get_component(name) + if comp.codeowners: + for owner in comp.codeowners: + if not owner.startswith('@'): + print(f"Codeowner {owner} for integration {name} must start with an '@' symbol!") + sys.exit(1) + + parts.append(f"esphome/components/{name}/* {' '.join(comp.codeowners)}") + +# End newline +parts.append('') +content = '\n'.join(parts) +codeowners_file = root / 'CODEOWNERS' + +if args.check: + if codeowners_file.read_text() != content: + print("CODEOWNERS file is not up to date.") + print("Please run `script/build_codeowners.py`") + sys.exit(1) + print("CODEOWNERS file is up to date") +else: + write_file_if_changed(codeowners_file, content) + print("Wrote CODEOWNERS") From f6e3070dd8bb92636db7b30593874f078bc79940 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 25 Jul 2020 12:57:11 -0300 Subject: [PATCH 153/200] rtttl player (#1171) * rtttl player * fixes * Cleanup, add action, condition, etc. * add test * updates * fixes * Add better error messages * lint --- esphome/components/esp8266_pwm/esp8266_pwm.h | 2 +- esphome/components/ledc/ledc_output.cpp | 4 +- esphome/components/ledc/ledc_output.h | 4 +- esphome/components/output/float_output.h | 13 +- esphome/components/rtttl/__init__.py | 69 +++++++ esphome/components/rtttl/rtttl.cpp | 186 +++++++++++++++++++ esphome/components/rtttl/rtttl.h | 81 ++++++++ tests/test1.yaml | 3 + 8 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 esphome/components/rtttl/__init__.py create mode 100644 esphome/components/rtttl/rtttl.cpp create mode 100644 esphome/components/rtttl/rtttl.h diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.h b/esphome/components/esp8266_pwm/esp8266_pwm.h index 51b74f48ba..661db6611f 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.h +++ b/esphome/components/esp8266_pwm/esp8266_pwm.h @@ -13,7 +13,7 @@ class ESP8266PWM : public output::FloatOutput, public Component { void set_pin(GPIOPin *pin) { pin_ = pin; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically update frequency - void update_frequency(float frequency) { + void update_frequency(float frequency) override { this->set_frequency(frequency); this->write_state(this->last_output_); } diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 2b1c181a62..d4e3327bb1 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -22,7 +22,7 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - this->apply_frequency(this->frequency_); + this->update_frequency(this->frequency_); this->turn_off(); // Attach pin after setting default value ledcAttachPin(this->pin_->get_pin(), this->channel_); @@ -50,7 +50,7 @@ optional ledc_bit_depth_for_frequency(float frequency) { return {}; } -void LEDCOutput::apply_frequency(float frequency) { +void LEDCOutput::update_frequency(float frequency) { auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); if (!bit_depth_opt.has_value()) { ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index 3f56f502b0..b3b14fe855 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -19,7 +19,7 @@ class LEDCOutput : public output::FloatOutput, public Component { void set_channel(uint8_t channel) { this->channel_ = channel; } void set_frequency(float frequency) { this->frequency_ = frequency; } /// Dynamically change frequency at runtime - void apply_frequency(float frequency); + void update_frequency(float frequency) override; /// Setup LEDC. void setup() override; @@ -45,7 +45,7 @@ template class SetFrequencyAction : public Action { void play(Ts... x) { float freq = this->frequency_.value(x...); - this->parent_->apply_frequency(freq); + this->parent_->update_frequency(freq); } protected: diff --git a/esphome/components/output/float_output.h b/esphome/components/output/float_output.h index e3f852b3f6..1b969c9225 100644 --- a/esphome/components/output/float_output.h +++ b/esphome/components/output/float_output.h @@ -46,9 +46,20 @@ class FloatOutput : public BinaryOutput { */ void set_min_power(float min_power); - /// Set the level of this float output, this is called from the front-end. + /** Set the level of this float output, this is called from the front-end. + * + * @param state The new state. + */ void set_level(float state); + /** Set the frequency of the output for PWM outputs. + * + * Implemented only by components which can set the output PWM frequency. + * + * @param frequence The new frequency. + */ + virtual void update_frequency(float frequency) {} + // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py new file mode 100644 index 0000000000..df227ab20f --- /dev/null +++ b/esphome/components/rtttl/__init__.py @@ -0,0 +1,69 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.components.output import FloatOutput +from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID + +CONF_RTTTL = 'rtttl' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +rtttl_ns = cg.esphome_ns.namespace('rtttl') + +Rtttl = rtttl_ns .class_('Rtttl', cg.Component) +PlayAction = rtttl_ns.class_('PlayAction', automation.Action) +StopAction = rtttl_ns.class_('StopAction', automation.Action) +FinishedPlaybackTrigger = rtttl_ns.class_('FinishedPlaybackTrigger', + automation.Trigger.template()) +IsPlayingCondition = rtttl_ns.class_('IsPlayingCondition', automation.Condition) + +MULTI_CONF = True + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(CONF_ID): cv.declare_id(Rtttl), + cv.Required(CONF_OUTPUT): cv.use_id(FloatOutput), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(FinishedPlaybackTrigger), + }), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + out = yield cg.get_variable(config[CONF_OUTPUT]) + cg.add(var.set_output(out)) + + for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('rtttl.play', PlayAction, cv.maybe_simple_value({ + cv.GenerateID(CONF_ID): cv.use_id(Rtttl), + cv.Required(CONF_RTTTL): cv.templatable(cv.string) +}, key=CONF_RTTTL)) +def rtttl_play_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_RTTTL], args, cg.std_string) + cg.add(var.set_value(template_)) + yield var + + +@automation.register_action('rtttl.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(Rtttl), +})) +def rtttl_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('rtttl.is_playing', IsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(Rtttl), +})) +def rtttl_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp new file mode 100644 index 0000000000..da50e1cbe9 --- /dev/null +++ b/esphome/components/rtttl/rtttl.cpp @@ -0,0 +1,186 @@ +#include "rtttl.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace rtttl { + +static const char* TAG = "rtttl"; + +static const uint32_t DOUBLE_NOTE_GAP_MS = 10; + +// These values can also be found as constants in the Tone library (Tone.h) +static const uint16_t NOTES[] = {0, 262, 277, 294, 311, 330, 349, 370, 392, 415, 440, 466, 494, + 523, 554, 587, 622, 659, 698, 740, 784, 831, 880, 932, 988, 1047, + 1109, 1175, 1245, 1319, 1397, 1480, 1568, 1661, 1760, 1865, 1976, 2093, 2217, + 2349, 2489, 2637, 2794, 2960, 3136, 3322, 3520, 3729, 3951}; + +void Rtttl::dump_config() { ESP_LOGCONFIG(TAG, "Rtttl"); } + +void Rtttl::play(std::string rtttl) { + rtttl_ = std::move(rtttl); + + default_duration_ = 4; + default_octave_ = 6; + int bpm = 63; + uint8_t num; + + // Get name + position_ = rtttl_.find(':'); + + // it's somewhat documented to be up to 10 characters but let's be a bit flexible here + if (position_ == std::string::npos || position_ > 15) { + ESP_LOGE(TAG, "Missing ':' when looking for name."); + return; + } + + auto name = this->rtttl_.substr(0, position_); + ESP_LOGD(TAG, "Playing song %s", name.c_str()); + + // get default duration + position_ = this->rtttl_.find("d=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'd='"); + return; + } + position_ += 2; + num = this->get_integer_(); + if (num > 0) + default_duration_ = num; + + // get default octave + position_ = rtttl_.find("o=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing 'o="); + return; + } + position_ += 2; + num = get_integer_(); + if (num >= 3 && num <= 7) + default_octave_ = num; + + // get BPM + position_ = rtttl_.find("b=", position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing b="); + return; + } + position_ += 2; + num = get_integer_(); + if (num != 0) + bpm = num; + + position_ = rtttl_.find(':', position_); + if (position_ == std::string::npos) { + ESP_LOGE(TAG, "Missing second ':'"); + return; + } + position_++; + + // BPM usually expresses the number of quarter notes per minute + wholenote_ = 60 * 1000L * 4 / bpm; // this is the time for whole note (in milliseconds) + + output_freq_ = 0; + last_note_ = millis(); + note_duration_ = 1; +} + +void Rtttl::loop() { + if (note_duration_ == 0 || millis() - last_note_ < note_duration_) + return; + + if (!rtttl_[position_]) { + output_->set_level(0.0); + ESP_LOGD(TAG, "Playback finished"); + this->on_finished_playback_callback_.call(); + note_duration_ = 0; + return; + } + + // align to note: most rtttl's out there does not add and space after the ',' separator but just in case... + while (rtttl_[position_] == ',' || rtttl_[position_] == ' ') + position_++; + + // first, get note duration, if available + uint8_t num = this->get_integer_(); + + if (num) + note_duration_ = wholenote_ / num; + else + note_duration_ = wholenote_ / default_duration_; // we will need to check if we are a dotted note after + + uint8_t note; + + switch (rtttl_[position_]) { + case 'c': + note = 1; + break; + case 'd': + note = 3; + break; + case 'e': + note = 5; + break; + case 'f': + note = 6; + break; + case 'g': + note = 8; + break; + case 'a': + note = 10; + break; + case 'b': + note = 12; + break; + case 'p': + default: + note = 0; + } + position_++; + + // now, get optional '#' sharp + if (rtttl_[position_] == '#') { + note++; + position_++; + } + + // now, get optional '.' dotted note + if (rtttl_[position_] == '.') { + note_duration_ += note_duration_ / 2; + position_++; + } + + // now, get scale + uint8_t scale = get_integer_(); + if (scale == 0) + scale = default_octave_; + + // Now play the note + if (note) { + auto note_index = (scale - 4) * 12 + note; + if (note_index < 0 || note_index >= sizeof(NOTES)) { + ESP_LOGE(TAG, "Note out of valid range"); + return; + } + auto freq = NOTES[note_index]; + + if (freq == output_freq_) { + // Add small silence gap between same note + output_->set_level(0.0); + delay(DOUBLE_NOTE_GAP_MS); + note_duration_ -= DOUBLE_NOTE_GAP_MS; + } + output_freq_ = freq; + + ESP_LOGVV(TAG, "playing note: %d for %dms", note, note_duration_); + output_->update_frequency(freq); + output_->set_level(0.5); + } else { + ESP_LOGVV(TAG, "waiting: %dms", note_duration_); + output_->set_level(0.0); + } + + last_note_ = millis(); +} +} // namespace rtttl +} // namespace esphome diff --git a/esphome/components/rtttl/rtttl.h b/esphome/components/rtttl/rtttl.h new file mode 100644 index 0000000000..76d8241ddf --- /dev/null +++ b/esphome/components/rtttl/rtttl.h @@ -0,0 +1,81 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace rtttl { + +extern uint32_t global_rtttl_id; + +class Rtttl : public Component { + public: + void set_output(output::FloatOutput *output) { output_ = output; } + void play(std::string rtttl); + void stop() { + note_duration_ = 0; + output_->set_level(0.0); + } + void dump_config() override; + + bool is_playing() { return note_duration_ != 0; } + void loop() override; + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + inline uint8_t get_integer_() { + uint8_t ret = 0; + while (isdigit(rtttl_[position_])) { + ret = (ret * 10) + (rtttl_[position_++] - '0'); + } + return ret; + } + + std::string rtttl_; + size_t position_; + uint16_t wholenote_; + uint16_t default_duration_; + uint16_t default_octave_; + uint32_t last_note_; + uint16_t note_duration_; + + uint32_t output_freq_; + output::FloatOutput *output_; + + CallbackManager on_finished_playback_callback_; +}; + +template class PlayAction : public Action { + public: + PlayAction(Rtttl *rtttl) : rtttl_(rtttl) {} + TEMPLATABLE_VALUE(std::string, value) + + void play(Ts... x) override { this->rtttl_->play(this->value_.value(x...)); } + + protected: + Rtttl *rtttl_; +}; + +template class StopAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->stop(); } +}; + +template class IsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class FinishedPlaybackTrigger : public Trigger<> { + public: + explicit FinishedPlaybackTrigger(Rtttl *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace rtttl +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 84af00e90a..3738edd1de 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1773,3 +1773,6 @@ sn74hc595: latch_pin: GPIO22 oe_pin: GPIO32 sr_count: 2 + +rtttl: + output: gpio_19 From bf3b6787275d76ec62bd7aed0d8a63a36fe18914 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 19:26:30 +0200 Subject: [PATCH 154/200] Update bump-version script --- script/bump-version.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/script/bump-version.py b/script/bump-version.py index 7895735844..b59f066db4 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -73,20 +73,12 @@ def write_version(version: Version): def main(): parser = argparse.ArgumentParser() parser.add_argument('new_version', type=str) - parser.add_argument('--commit', action='store_true') args = parser.parse_args() - if args.commit and subprocess.call(["git", "diff", "--quiet"]) == 1: - print("Cannot use --commit because git is dirty.") - return 1 - version = Version.parse(args.new_version) print(f"Bumping to {version}") write_version(version) - - if args.commit: - subprocess.check_call(["git", "commit", "-nam", f"Bump version to v{version}"]) - return 1 + return 0 if __name__ == "__main__": From 8215a018e97c8506ffe08c913580514b8faf89a8 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sat, 25 Jul 2020 17:19:13 -0300 Subject: [PATCH 155/200] Add @glmnet components (#1200) --- CODEOWNERS | 4 ++++ esphome/components/ac_dimmer/output.py | 2 ++ esphome/components/climate_ir/__init__.py | 1 + esphome/components/coolix/climate.py | 1 + esphome/components/dfplayer/__init__.py | 1 + esphome/components/rtttl/__init__.py | 1 + esphome/components/sim800l/__init__.py | 4 ++-- esphome/components/tcl112/climate.py | 1 + esphome/components/tm1637/display.py | 2 ++ esphome/components/whirlpool/climate.py | 1 + 10 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 79b2d0503c..2254ff2caa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,8 +18,10 @@ esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core +esphome/components/climate_ir/* @glmnet esphome/components/cover/* @esphome/core esphome/components/debug/* @OttoWinter +esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter esphome/components/fastled_base/* @OttoWinter @@ -40,9 +42,11 @@ esphome/components/pid/* @OttoWinter esphome/components/pn532/* @OttoWinter esphome/components/power_supply/* @esphome/core esphome/components/restart/* @esphome/core +esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/shutdown/* @esphome/core +esphome/components/sim800l/* @glmnet esphome/components/spi/* @esphome/core esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter diff --git a/esphome/components/ac_dimmer/output.py b/esphome/components/ac_dimmer/output.py index 16f04ac984..17dcd8ac26 100644 --- a/esphome/components/ac_dimmer/output.py +++ b/esphome/components/ac_dimmer/output.py @@ -4,6 +4,8 @@ from esphome import pins from esphome.components import output from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD +CODEOWNERS = ['@glmnet'] + ac_dimmer_ns = cg.esphome_ns.namespace('ac_dimmer') AcDimmer = ac_dimmer_ns.class_('AcDimmer', output.FloatOutput, cg.Component) diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py index 1163705faa..40ab3f22e8 100644 --- a/esphome/components/climate_ir/__init__.py +++ b/esphome/components/climate_ir/__init__.py @@ -6,6 +6,7 @@ from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR from esphome.core import coroutine AUTO_LOAD = ['sensor', 'remote_base'] +CODEOWNERS = ['@glmnet'] climate_ir_ns = cg.esphome_ns.namespace('climate_ir') ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 81412bb586..075fad5a4b 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) diff --git a/esphome/components/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py index 890c2bede4..680a6b89ec 100644 --- a/esphome/components/dfplayer/__init__.py +++ b/esphome/components/dfplayer/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@glmnet'] dfplayer_ns = cg.esphome_ns.namespace('dfplayer') DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) diff --git a/esphome/components/rtttl/__init__.py b/esphome/components/rtttl/__init__.py index df227ab20f..a276f7cb86 100644 --- a/esphome/components/rtttl/__init__.py +++ b/esphome/components/rtttl/__init__.py @@ -4,6 +4,7 @@ from esphome import automation from esphome.components.output import FloatOutput from esphome.const import CONF_ID, CONF_OUTPUT, CONF_TRIGGER_ID +CODEOWNERS = ['@glmnet'] CONF_RTTTL = 'rtttl' CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py index c64112570a..762e045598 100644 --- a/esphome/components/sim800l/__init__.py +++ b/esphome/components/sim800l/__init__.py @@ -5,6 +5,8 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@glmnet'] +MULTI_CONF = True sim800l_ns = cg.esphome_ns.namespace('sim800l') Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) @@ -16,8 +18,6 @@ Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger # Actions Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) -MULTI_CONF = True - CONF_ON_SMS_RECEIVED = 'on_sms_received' CONF_RECIPIENT = 'recipient' CONF_MESSAGE = 'message' diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 3c94f4a243..11ebdc7be8 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) diff --git a/esphome/components/tm1637/display.py b/esphome/components/tm1637/display.py index dcc4b82719..c2692e30de 100644 --- a/esphome/components/tm1637/display.py +++ b/esphome/components/tm1637/display.py @@ -4,6 +4,8 @@ from esphome import pins from esphome.components import display from esphome.const import CONF_CLK_PIN, CONF_DIO_PIN, CONF_ID, CONF_LAMBDA, CONF_INTENSITY +CODEOWNERS = ['@glmnet'] + tm1637_ns = cg.esphome_ns.namespace('tm1637') TM1637Display = tm1637_ns.class_('TM1637Display', cg.PollingComponent) TM1637DisplayRef = TM1637Display.operator('ref') diff --git a/esphome/components/whirlpool/climate.py b/esphome/components/whirlpool/climate.py index 1083b86618..1fd62b411a 100644 --- a/esphome/components/whirlpool/climate.py +++ b/esphome/components/whirlpool/climate.py @@ -4,6 +4,7 @@ from esphome.components import climate_ir from esphome.const import CONF_ID, CONF_MODEL AUTO_LOAD = ['climate_ir'] +CODEOWNERS = ['@glmnet'] whirlpool_ns = cg.esphome_ns.namespace('whirlpool') WhirlpoolClimate = whirlpool_ns.class_('WhirlpoolClimate', climate_ir.ClimateIR) From 1c3ed71d3625d909eb664ed1551e1a7efe716eb3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sat, 25 Jul 2020 23:19:10 +0200 Subject: [PATCH 156/200] Fix bump-version script --- script/bump-version.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/bump-version.py b/script/bump-version.py index b59f066db4..ebc73c5700 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -45,9 +45,9 @@ class Version: def sub(path, pattern, repl, expected_count=1): with open(path) as fh: content = fh.read() - content, count = re.subn(pattern, repl, content, re.MULTILINE) + content, count = re.subn(pattern, repl, content, flags=re.MULTILINE) if expected_count is not None: - assert count == expected_count + assert count == expected_count, f"Pattern {pattern} replacement failed!" with open(path, "wt") as fh: fh.write(content) From f61a82a568ccb0a8129e34fdcfead4215631ae88 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 26 Jul 2020 10:16:54 +0200 Subject: [PATCH 157/200] Fix bump-version string format --- script/bump-version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bump-version.py b/script/bump-version.py index ebc73c5700..2e03ef6f62 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -66,7 +66,7 @@ def write_version(version: Version): sub( 'esphome/const.py', r"^PATCH_VERSION = .*$", - f"PATCH_VERSION = {version.full_patch}" + f"PATCH_VERSION = '{version.full_patch}''" ) From abc83f6cb0de99b649fde39a110463bfde1e39fe Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 26 Jul 2020 13:50:45 +0200 Subject: [PATCH 158/200] Fix typo in bump-version script once again --- script/bump-version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/bump-version.py b/script/bump-version.py index 2e03ef6f62..56062ac5cf 100755 --- a/script/bump-version.py +++ b/script/bump-version.py @@ -66,7 +66,7 @@ def write_version(version: Version): sub( 'esphome/const.py', r"^PATCH_VERSION = .*$", - f"PATCH_VERSION = '{version.full_patch}''" + f"PATCH_VERSION = '{version.full_patch}'" ) From 25ad33a3775080b8bb7a672c64afc3ea29a5cd28 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 27 Jul 2020 00:33:16 +1200 Subject: [PATCH 159/200] Add @jesserockz to codeowners (#1202) Co-authored-by: Otto Winter --- CODEOWNERS | 11 +++++ esphome/components/ct_clamp/sensor.py | 1 + esphome/components/rf_bridge/__init__.py | 1 + .../components/tuya/binary_sensor/__init__.py | 1 + esphome/components/tuya/climate/__init__.py | 1 + esphome/components/tuya/sensor/__init__.py | 1 + esphome/components/tuya/switch/__init__.py | 1 + script/build_codeowners.py | 44 +++++++++++++++---- 8 files changed, 53 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 2254ff2caa..1722f482ac 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -11,6 +11,7 @@ esphome/*.py @esphome/core esphome/core/* @esphome/core # Integrations +esphome/components/ac_dimmer/* @glmnet esphome/components/adc/* @esphome/core esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter @@ -19,7 +20,9 @@ esphome/components/binary_sensor/* @esphome/core esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet +esphome/components/coolix/* @glmnet esphome/components/cover/* @esphome/core +esphome/components/ct_clamp/* @jesserockz esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter @@ -42,6 +45,7 @@ esphome/components/pid/* @OttoWinter esphome/components/pn532/* @OttoWinter esphome/components/power_supply/* @esphome/core esphome/components/restart/* @esphome/core +esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sensor/* @esphome/core @@ -51,8 +55,15 @@ esphome/components/spi/* @esphome/core esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core +esphome/components/tcl112/* @glmnet esphome/components/time/* @OttoWinter +esphome/components/tm1637/* @glmnet +esphome/components/tuya/binary_sensor/* @jesserockz +esphome/components/tuya/climate/* @jesserockz +esphome/components/tuya/sensor/* @jesserockz +esphome/components/tuya/switch/* @jesserockz esphome/components/uart/* @esphome/core esphome/components/ultrasonic/* @OttoWinter esphome/components/version/* @esphome/core esphome/components/web_server_base/* @OttoWinter +esphome/components/whirlpool/* @glmnet diff --git a/esphome/components/ct_clamp/sensor.py b/esphome/components/ct_clamp/sensor.py index 9f41f8c614..42a3b66497 100644 --- a/esphome/components/ct_clamp/sensor.py +++ b/esphome/components/ct_clamp/sensor.py @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler from esphome.const import CONF_SENSOR, CONF_ID, ICON_FLASH, UNIT_AMPERE AUTO_LOAD = ['voltage_sampler'] +CODEOWNERS = ['@jesserockz'] CONF_SAMPLE_DURATION = 'sample_duration' diff --git a/esphome/components/rf_bridge/__init__.py b/esphome/components/rf_bridge/__init__.py index 1fd4fbc7bd..885e5765dd 100644 --- a/esphome/components/rf_bridge/__init__.py +++ b/esphome/components/rf_bridge/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_CODE, CONF_LOW, CONF_SY from esphome.components import uart DEPENDENCIES = ['uart'] +CODEOWNERS = ['@jesserockz'] rf_bridge_ns = cg.esphome_ns.namespace('rf_bridge') RFBridgeComponent = rf_bridge_ns.class_('RFBridgeComponent', cg.Component, uart.UARTDevice) diff --git a/esphome/components/tuya/binary_sensor/__init__.py b/esphome/components/tuya/binary_sensor/__init__.py index 3ba479bf19..b63638b4cc 100644 --- a/esphome/components/tuya/binary_sensor/__init__.py +++ b/esphome/components/tuya/binary_sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] CONF_SENSOR_DATAPOINT = "sensor_datapoint" diff --git a/esphome/components/tuya/climate/__init__.py b/esphome/components/tuya/climate/__init__.py index b938be7b8c..878c5aefe8 100644 --- a/esphome/components/tuya/climate/__init__.py +++ b/esphome/components/tuya/climate/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] CONF_TARGET_TEMPERATURE_DATAPOINT = "target_temperature_datapoint" CONF_CURRENT_TEMPERATURE_DATAPOINT = "current_temperature_datapoint" diff --git a/esphome/components/tuya/sensor/__init__.py b/esphome/components/tuya/sensor/__init__.py index 5f2ff2d674..b3260bfe0b 100644 --- a/esphome/components/tuya/sensor/__init__.py +++ b/esphome/components/tuya/sensor/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] CONF_SENSOR_DATAPOINT = "sensor_datapoint" diff --git a/esphome/components/tuya/switch/__init__.py b/esphome/components/tuya/switch/__init__.py index 0eadcf0a22..f68bbbcdb6 100644 --- a/esphome/components/tuya/switch/__init__.py +++ b/esphome/components/tuya/switch/__init__.py @@ -5,6 +5,7 @@ from esphome.const import CONF_ID, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ['tuya'] +CODEOWNERS = ['@jesserockz'] TuyaSwitch = tuya_ns.class_('TuyaSwitch', switch.Switch, cg.Component) diff --git a/script/build_codeowners.py b/script/build_codeowners.py index 3f2bf5d8c3..fce59b3b95 100755 --- a/script/build_codeowners.py +++ b/script/build_codeowners.py @@ -2,9 +2,10 @@ from pathlib import Path import sys import argparse +from collections import defaultdict from esphome.helpers import write_file_if_changed -from esphome.config import get_component +from esphome.config import get_component, get_platform from esphome.core import CORE parser = argparse.ArgumentParser() @@ -37,20 +38,47 @@ parts = [BASE] # Fake some diretory so that get_component works CORE.config_path = str(root) -for path in sorted(components_dir.iterdir()): +codeowners = defaultdict(list) + +for path in components_dir.iterdir(): if not path.is_dir(): continue if not (path / '__init__.py').is_file(): continue + name = path.name comp = get_component(name) - if comp.codeowners: - for owner in comp.codeowners: - if not owner.startswith('@'): - print(f"Codeowner {owner} for integration {name} must start with an '@' symbol!") - sys.exit(1) + codeowners[f'esphome/components/{name}/*'].extend(comp.codeowners) + + for platform_path in path.iterdir(): + platform_name = platform_path.stem + platform = get_platform(platform_name, name) + if platform is None: + continue + + if platform_path.is_dir(): + # Sub foldered platforms get their own line + if not (platform_path / '__init__.py').is_file(): + continue + codeowners[f'esphome/components/{name}/{platform_name}/*'].extend(platform.codeowners) + continue + + # Non-subfoldered platforms add to codeowners at component level + if not platform_path.is_file() or platform_path.name == '__init__.py': + continue + codeowners[f'esphome/components/{name}/*'].extend(platform.codeowners) + + +for path, owners in sorted(codeowners.items()): + owners = sorted(set(owners)) + if not owners: + continue + for owner in owners: + if not owner.startswith('@'): + print(f"Codeowner {owner} for integration {path} must start with an '@' symbol!") + sys.exit(1) + parts.append(f"{path} {' '.join(owners)}") - parts.append(f"esphome/components/{name}/* {' '.join(comp.codeowners)}") # End newline parts.append('') From bd392e2efc2ea0c4df12ac91071ba3840a77c239 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 26 Jul 2020 07:34:11 -0500 Subject: [PATCH 160/200] Fix set point logging issue (#1201) --- .../thermostat/thermostat_climate.cpp | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 1ffbd3c169..64a7c1b05d 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -507,10 +507,18 @@ Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } void ThermostatClimate::dump_config() { LOG_CLIMATE("", "Thermostat", this); - if (this->supports_heat_) - ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); - if ((this->supports_cool_) || (this->supports_fan_only_)) - ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature Low: %.1f°C", this->normal_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Default Target Temperature High: %.1f°C", this->normal_config_.default_temperature); + } ESP_LOGCONFIG(TAG, " Hysteresis: %.1f°C", this->hysteresis_); ESP_LOGCONFIG(TAG, " Supports AUTO: %s", YESNO(this->supports_auto_)); ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); @@ -533,11 +541,20 @@ void ThermostatClimate::dump_config() { ESP_LOGCONFIG(TAG, " Supports TWO SET POINTS: %s", YESNO(this->supports_two_points_)); ESP_LOGCONFIG(TAG, " Supports AWAY mode: %s", YESNO(this->supports_away_)); if (this->supports_away_) { - if (this->supports_heat_) - ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature_low); - if ((this->supports_cool_) || (this->supports_fan_only_)) - ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", - this->away_config_.default_temperature_high); + if (this->supports_heat_) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", + this->away_config_.default_temperature_low); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature Low: %.1f°C", this->away_config_.default_temperature); + } + if ((this->supports_cool_) || (this->supports_fan_only_)) { + if (this->supports_two_points_) + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", + this->away_config_.default_temperature_high); + else + ESP_LOGCONFIG(TAG, " Away Default Target Temperature High: %.1f°C", this->away_config_.default_temperature); + } } } From 65a489ebddb963c903a193f54ccba5db7807f701 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 26 Jul 2020 15:43:10 +0200 Subject: [PATCH 161/200] Revert "Added auto discovery and setup to Dallas Platform (#1028)" (#1189) This reverts commit 021055f0b8c8f450fdfed5a57d34f796cae68b46. --- esphome/components/dallas/__init__.py | 26 +------------ .../components/dallas/dallas_component.cpp | 37 ------------------- esphome/components/dallas/dallas_component.h | 20 ---------- 3 files changed, 1 insertion(+), 82 deletions(-) diff --git a/esphome/components/dallas/__init__.py b/esphome/components/dallas/__init__.py index 28fc9785ad..85ab4300ee 100644 --- a/esphome/components/dallas/__init__.py +++ b/esphome/components/dallas/__init__.py @@ -1,18 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins -from esphome.const import CONF_ID, CONF_PIN, \ - CONF_RESOLUTION, CONF_UNIT_OF_MEASUREMENT, UNIT_CELSIUS, \ - CONF_ICON, ICON_THERMOMETER, CONF_ACCURACY_DECIMALS +from esphome.const import CONF_ID, CONF_PIN MULTI_CONF = True AUTO_LOAD = ['sensor'] CONF_ONE_WIRE_ID = 'one_wire_id' -CONF_AUTO_SETUP_SENSORS = 'auto_setup_sensors' -CONF_SENSOR_NAME_TEMPLATE = 'sensor_name_template' -SENSOR_NAME_TEMPLATE_DEFAULT = '%s.%s' - dallas_ns = cg.esphome_ns.namespace('dallas') DallasComponent = dallas_ns.class_('DallasComponent', cg.PollingComponent) ESPOneWire = dallas_ns.class_('ESPOneWire') @@ -21,12 +15,6 @@ CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(DallasComponent), cv.GenerateID(CONF_ONE_WIRE_ID): cv.declare_id(ESPOneWire), cv.Required(CONF_PIN): pins.gpio_input_pin_schema, - cv.Optional(CONF_AUTO_SETUP_SENSORS, default=False): cv.boolean, - cv.Optional(CONF_SENSOR_NAME_TEMPLATE, default=SENSOR_NAME_TEMPLATE_DEFAULT): cv.string_strict, - cv.Optional(CONF_RESOLUTION, default=12): cv.int_range(min=9, max=12), - cv.Optional(CONF_UNIT_OF_MEASUREMENT, default=UNIT_CELSIUS): cv.string_strict, - cv.Optional(CONF_ICON, default=ICON_THERMOMETER): cv.icon, - cv.Optional(CONF_ACCURACY_DECIMALS, default=1): cv.int_, }).extend(cv.polling_component_schema('60s')) @@ -34,16 +22,4 @@ def to_code(config): pin = yield cg.gpio_pin_expression(config[CONF_PIN]) one_wire = cg.new_Pvariable(config[CONF_ONE_WIRE_ID], pin) var = cg.new_Pvariable(config[CONF_ID], one_wire) - if CONF_AUTO_SETUP_SENSORS in config: - cg.add(var.set_auto_setup_sensors(config[CONF_AUTO_SETUP_SENSORS])) - if CONF_SENSOR_NAME_TEMPLATE in config: - cg.add(var.set_sensor_name_template(config[CONF_SENSOR_NAME_TEMPLATE])) - if CONF_RESOLUTION in config: - cg.add(var.set_resolution(config[CONF_RESOLUTION])) - if CONF_UNIT_OF_MEASUREMENT in config: - cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) - if CONF_ICON in config: - cg.add(var.set_icon(config[CONF_ICON])) - if CONF_ACCURACY_DECIMALS in config: - cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) yield cg.register_component(var, config) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index d657f6c498..aa839e7331 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -1,6 +1,5 @@ #include "dallas_component.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" namespace esphome { namespace dallas { @@ -53,29 +52,6 @@ void DallasComponent::setup() { continue; } this->found_sensors_.push_back(address); - - if (this->auto_setup_sensors_) { - // avoid re-generating pre-configured sensors - bool skip = false; - for (auto sensor : this->sensors_) { - if (sensor->get_address() == address) { - skip = true; - break; - } - } - if (!skip) { - auto dallastemperaturesensor = this->get_sensor_by_address(address, this->resolution_); - char sensor_name[64]; - snprintf(sensor_name, sizeof(sensor_name), this->sensor_name_template_.c_str(), App.get_name().c_str(), - s.c_str()); - dallastemperaturesensor->set_name(sensor_name); - dallastemperaturesensor->set_unit_of_measurement(this->unit_of_measurement_); - dallastemperaturesensor->set_icon(this->icon_); - dallastemperaturesensor->set_accuracy_decimals(this->accuracy_decimals_); - dallastemperaturesensor->set_force_update(false); - App.register_sensor(dallastemperaturesensor); - } - } } for (auto sensor : this->sensors_) { @@ -180,25 +156,12 @@ void DallasComponent::update() { } } DallasComponent::DallasComponent(ESPOneWire *one_wire) : one_wire_(one_wire) {} -void DallasComponent::set_auto_setup_sensors(bool auto_setup_sensors) { - this->auto_setup_sensors_ = auto_setup_sensors; -} -void DallasComponent::set_sensor_name_template(const std::string &sensor_name_template) { - this->sensor_name_template_ = sensor_name_template; -} -void DallasComponent::set_resolution(uint8_t resolution) { this->resolution_ = resolution; } -void DallasComponent::set_unit_of_measurement(const std::string &unit_of_measurement) { - this->unit_of_measurement_ = unit_of_measurement; -} -void DallasComponent::set_icon(const std::string &icon) { this->icon_ = icon; } -void DallasComponent::set_accuracy_decimals(int8_t accuracy_decimals) { this->accuracy_decimals_ = accuracy_decimals; } DallasTemperatureSensor::DallasTemperatureSensor(uint64_t address, uint8_t resolution, DallasComponent *parent) : parent_(parent) { this->set_address(address); this->set_resolution(resolution); } -const uint64_t &DallasTemperatureSensor::get_address() const { return this->address_; } void DallasTemperatureSensor::set_address(uint64_t address) { this->address_ = address; } uint8_t DallasTemperatureSensor::get_resolution() const { return this->resolution_; } void DallasTemperatureSensor::set_resolution(uint8_t resolution) { this->resolution_ = resolution; } diff --git a/esphome/components/dallas/dallas_component.h b/esphome/components/dallas/dallas_component.h index f2acf09d75..d32aec1758 100644 --- a/esphome/components/dallas/dallas_component.h +++ b/esphome/components/dallas/dallas_component.h @@ -22,30 +22,12 @@ class DallasComponent : public PollingComponent { void update() override; - /// Automatic sensors instantiation - bool get_auto_setup_sensors() const; - void set_auto_setup_sensors(bool auto_setup_sensors); - - /// Get/Set properties for automatically generated sensors. - void set_sensor_name_template(const std::string &sensor_name_template); - void set_resolution(uint8_t resolution); - void set_unit_of_measurement(const std::string &unit_of_measurement); - void set_icon(const std::string &icon); - void set_accuracy_decimals(int8_t accuracy_decimals); - protected: friend DallasTemperatureSensor; ESPOneWire *one_wire_; std::vector sensors_; std::vector found_sensors_; - - bool auto_setup_sensors_; - std::string sensor_name_template_; - uint8_t resolution_; - std::string unit_of_measurement_; - std::string icon_; - int8_t accuracy_decimals_; }; /// Internal class that helps us create multiple sensors for one Dallas hub. @@ -58,8 +40,6 @@ class DallasTemperatureSensor : public sensor::Sensor { /// Helper to create (and cache) the name for this sensor. For example "0xfe0000031f1eaf29". const std::string &get_address_name(); - /// Get the 64-bit unsigned address of this sensor. - const uint64_t &get_address() const; /// Set the 64-bit unsigned address for this sensor. void set_address(uint64_t address); /// Get the index of this sensor. (0 if using address.) From 4c55b9c58c6d76580630424a621aed5b2ff6e677 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 26 Jul 2020 22:12:58 +0200 Subject: [PATCH 162/200] Bump version to v1.16.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 57694c4088..b65d0977fd 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,7 +1,7 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 15 +MINOR_VERSION = 16 PATCH_VERSION = '0-dev' __short_version__ = f'{MAJOR_VERSION}.{MINOR_VERSION}' __version__ = f'{__short_version__}.{PATCH_VERSION}' From b9d55fd1edeec443466771aa873d5b3e5996b587 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 26 Jul 2020 22:42:59 +0200 Subject: [PATCH 163/200] Also run CI checks when merging against beta/master --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce6a120f7a..b26f7e2f43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,6 @@ on: branches: [beta, master] pull_request: - # Only run when PR is against dev branch (all PRs should be against dev branch) - # Helps prevent accidentally merging PRs against master branch - branches: [dev] jobs: # A fast overview job that checks only changed files From 27d4b3b8ad51359eeaa00ed9f37d1c88f72cd84d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2020 11:43:10 +0200 Subject: [PATCH 164/200] Update cryptography requirement from <3,>=2.0.0 to >=2.0.0,<4 (#1206) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- esphome/components/wifi/wpa2_eap.py | 2 -- requirements_test.txt | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/wifi/wpa2_eap.py b/esphome/components/wifi/wpa2_eap.py index 5b83c59866..8bf50598b0 100644 --- a/esphome/components/wifi/wpa2_eap.py +++ b/esphome/components/wifi/wpa2_eap.py @@ -114,8 +114,6 @@ def _check_private_key_cert_match(key, cert): def validate_eap(value): - validate_cryptography_installed() - if CONF_USERNAME in value: if CONF_IDENTITY not in value: _LOGGER.info("EAP 'identity:' is not set, assuming username.") diff --git a/requirements_test.txt b/requirements_test.txt index 179f683b32..e9846fb29c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,7 +1,7 @@ pylint==2.5.3 flake8==3.8.3 pillow>4.0.0 -cryptography>=2.0.0,<3 +cryptography>=2.0.0,<4 pexpect==4.8.0 # Unit tests From a1e10f384e41ebc415b99b533306d55e65c34994 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 27 Jul 2020 06:43:51 -0300 Subject: [PATCH 165/200] fix dashboard select drop down (#1205) --- esphome/dashboard/static/css/esphome.css | 5 +++++ esphome/dashboard/static/js/esphome.js | 13 ++++++++++++- esphome/dashboard/templates/index.html | 6 +++--- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/esphome/dashboard/static/css/esphome.css b/esphome/dashboard/static/css/esphome.css index db0ac55985..116170f95d 100644 --- a/esphome/dashboard/static/css/esphome.css +++ b/esphome/dashboard/static/css/esphome.css @@ -163,6 +163,11 @@ main { color: black; } +nav { + height: auto; + line-height: normal; +} + .select-port-container { margin-top: 8px; margin-right: 10px; diff --git a/esphome/dashboard/static/js/esphome.js b/esphome/dashboard/static/js/esphome.js index d80e5e4ec9..52dda446ce 100644 --- a/esphome/dashboard/static/js/esphome.js +++ b/esphome/dashboard/static/js/esphome.js @@ -5,6 +5,7 @@ $(document).ready(function () { M.AutoInit(document.body); nodeGrid(); startAceWebsocket(); + fixNavbarHeight(); }); // WebSocket URL Helper @@ -16,6 +17,17 @@ if (loc.protocol === "https:") { } const wsUrl = wsLoc.href; +/** + * Fix NavBar height + */ +const fixNavbarHeight = () => { + const fixFunc = () => { + const sel = $(".select-wrapper"); + $(".navbar-fixed").css("height", (sel.position().top + sel.outerHeight()) + "px"); + } + $(window).resize(fixFunc); + fixFunc(); +} /** * Dashboard Dynamic Grid @@ -1003,4 +1015,3 @@ jQuery.validator.addMethod("nospaces", (value, element) => { jQuery.validator.addMethod("lowercase", (value, element) => { return value === value.toLowerCase(); }, "Name must be all lower case!"); - diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index fa742fd2c6..d7ba9373be 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -31,8 +31,8 @@