diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000..52ac3648b0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,8 @@ +# These are supported funding model platforms + +github: +patreon: ottowinter +open_collective: +ko_fi: +tidelift: +custom: https://esphome.io/guides/supporters.html diff --git a/.gitignore b/.gitignore index 6002612c13..b004947390 100644 --- a/.gitignore +++ b/.gitignore @@ -75,6 +75,7 @@ venv.bak/ .pioenvs .piolibdeps +.pio .vscode CMakeListsPrivate.txt CMakeLists.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 969a53b311..c3808b7381 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ - BASE_VERSION: '1.5.1' + BASE_VERSION: '2.0.0' TZ: UTC stages: @@ -33,7 +33,7 @@ stages: - docker info - docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" script: - - docker run --rm --privileged hassioaddons/qemu-user-static:latest + - docker run --rm --privileged multiarch/qemu-user-static:4.1.0-1 --reset -p yes - TAG="${CI_COMMIT_TAG#v}" - TAG="${TAG:-${CI_COMMIT_SHA:0:7}}" - echo "Tag ${TAG}" @@ -107,10 +107,6 @@ lint-tidy: <<: *lint script: - pio init --ide atom - - | - if ! patch -R -p0 -s -f --dry-run ")); + stream->print(F("")); + stream->print(F("

WiFi Networks

")); + + if (request->hasArg("save")) { + stream->print(F("
The ESP will now try to connect to the network...
Please give it some " + "time to connect.
Note: Copy the changed network to your YAML file - the next OTA update will " + "overwrite these settings.
")); + } + + for (auto &scan : wifi::global_wifi_component->get_scan_result()) { + if (scan.get_is_hidden()) + continue; + + stream->print(F("
")); + + if (scan.get_rssi() >= -50) { + stream->print(F("")); + } else if (scan.get_rssi() >= -65) { + stream->print(F("")); + } else if (scan.get_rssi() >= -85) { + stream->print(F("")); + } else { + stream->print(F("")); + } + + stream->print(F("")); + stream->print(scan.get_ssid().c_str()); + stream->print(F("")); + if (scan.get_with_auth()) { + stream->print(F("")); + } + stream->print(F("
")); + } + + stream->print(F("

WiFi Settings







")); + stream->print(F("

OTA Update

")); + stream->print(F("
")); + request->send(stream); +} +void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { + std::string ssid = request->arg("ssid").c_str(); + std::string psk = request->arg("psk").c_str(); + ESP_LOGI(TAG, "Captive Portal Requested WiFi Settings Change:"); + ESP_LOGI(TAG, " SSID='%s'", ssid.c_str()); + ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str()); + this->override_sta_(ssid, psk); + request->redirect("/?save=true"); +} +void CaptivePortal::override_sta_(const std::string &ssid, const std::string &password) { + CaptivePortalSettings save{}; + strcpy(save.ssid, ssid.c_str()); + strcpy(save.password, password.c_str()); + this->pref_.save(&save); + + wifi::WiFiAP sta{}; + sta.set_ssid(ssid); + sta.set_password(password); + wifi::global_wifi_component->set_sta(sta); +} + +void CaptivePortal::setup() { + // Hash with compilation time + // This ensures the AP override is not applied for OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences.make_preference(hash, true); + + CaptivePortalSettings save{}; + if (this->pref_.load(&save)) { + this->override_sta_(save.ssid, save.password); + } +} +void CaptivePortal::start() { + this->base_->init(); + if (!this->initialized_) { + this->base_->add_handler(this); + this->base_->add_ota_handler(); + } + + this->dns_server_ = new DNSServer(); + this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); + IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); + this->dns_server_->start(53, "*", ip); + + this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { + bool not_found = false; + if (!this->active_) { + not_found = true; + } else if (req->host() == wifi::global_wifi_component->wifi_soft_ap_ip().toString()) { + not_found = true; + } + + if (not_found) { + req->send(404, "text/html", "File not found"); + return; + } + + auto url = "http://" + wifi::global_wifi_component->wifi_soft_ap_ip().toString(); + req->redirect(url); + }); + + this->initialized_ = true; + this->active_ = true; +} + +const char STYLESHEET_CSS[] PROGMEM = + R"(*{box-sizing:inherit}div,input{padding:5px;font-size:1em}input{width:95%}body{text-align:center;font-family:sans-serif}button{border:0;border-radius:.3rem;background-color:#1fa3ec;color:#fff;line-height:2.4rem;font-size:1.2rem;width:100%;padding:0}.main{text-align:left;display:inline-block;min-width:260px}.network{display:flex;justify-content:space-between;align-items:center}.network-left{display:flex;align-items:center}.network-ssid{margin-bottom:-7px;margin-left:10px}.info{border:1px solid;margin:10px 0;padding:15px 10px;color:#4f8a10;background-color:#dff2bf})"; +const char LOCK_SVG[] PROGMEM = + R"()"; + +void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { + if (req->url() == "/") { + this->handle_index(req); + return; + } else if (req->url() == "/wifisave") { + this->handle_wifisave(req); + return; + } else if (req->url() == "/stylesheet.css") { + req->send_P(200, "text/css", STYLESHEET_CSS); + return; + } else if (req->url() == "/lock.svg") { + req->send_P(200, "image/svg+xml", LOCK_SVG); + return; + } + + AsyncResponseStream *stream = req->beginResponseStream("image/svg+xml"); + stream->print(F("url() == "/wifi-strength-4.svg") { + stream->print(F("3z")); + } else { + if (req->url() == "/wifi-strength-1.svg") { + stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-5.1 6.36a8.43 8.43 0 0 0-7.22-.01L3.27 7.4")); + } else if (req->url() == "/wifi-strength-2.svg") { + stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-3.21 3.98a11.32 11.32 0 0 0-11 0L3.27 7.4")); + } else if (req->url() == "/wifi-strength-3.svg") { + stream->print(F("3m0 2c3.07 0 6.09.86 8.71 2.45l-1.94 2.43A13.6 13.6 0 0 0 12 8C9 8 6.68 9 5.21 9.84l-1.94-2.")); + } + stream->print(F("4A16.94 16.94 0 0 1 12 5z")); + } + stream->print(F("\"/>")); + req->send(stream); +} +CaptivePortal::CaptivePortal(web_server_base::WebServerBase *base) : base_(base) { global_captive_portal = this; } +float CaptivePortal::get_setup_priority() const { + // Before WiFi + return setup_priority::WIFI + 1.0f; +} + +CaptivePortal *global_captive_portal = nullptr; + +} // namespace captive_portal +} // namespace esphome diff --git a/esphome/components/captive_portal/captive_portal.h b/esphome/components/captive_portal/captive_portal.h new file mode 100644 index 0000000000..4b1717d157 --- /dev/null +++ b/esphome/components/captive_portal/captive_portal.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#include "esphome/core/component.h" +#include "esphome/core/preferences.h" +#include "esphome/components/web_server_base/web_server_base.h" + +namespace esphome { + +namespace captive_portal { + +struct CaptivePortalSettings { + char ssid[33]; + char password[65]; +} PACKED; // NOLINT + +class CaptivePortal : public AsyncWebHandler, public Component { + public: + CaptivePortal(web_server_base::WebServerBase *base); + void setup() override; + void loop() override { + if (this->dns_server_ != nullptr) + this->dns_server_->processNextRequest(); + } + float get_setup_priority() const override; + void start(); + bool is_active() const { return this->active_; } + void end() { + this->active_ = false; + this->base_->deinit(); + this->dns_server_->stop(); + delete this->dns_server_; + } + + bool canHandle(AsyncWebServerRequest *request) override { + if (!this->active_) + return false; + + if (request->method() == HTTP_GET) { + if (request->url() == "/") + return true; + if (request->url() == "/stylesheet.css") + return true; + if (request->url() == "/wifi-strength-1.svg") + return true; + if (request->url() == "/wifi-strength-2.svg") + return true; + if (request->url() == "/wifi-strength-3.svg") + return true; + if (request->url() == "/wifi-strength-4.svg") + return true; + if (request->url() == "/lock.svg") + return true; + if (request->url() == "/wifisave") + return true; + } + + return false; + } + + void handle_index(AsyncWebServerRequest *request); + + void handle_wifisave(AsyncWebServerRequest *request); + + void handleRequest(AsyncWebServerRequest *req) override; + + protected: + void override_sta_(const std::string &ssid, const std::string &password); + + web_server_base::WebServerBase *base_; + bool initialized_{false}; + bool active_{false}; + ESPPreferenceObject pref_; + DNSServer *dns_server_{nullptr}; +}; + +extern CaptivePortal *global_captive_portal; + +} // namespace captive_portal +} // namespace esphome diff --git a/esphome/components/captive_portal/index.html b/esphome/components/captive_portal/index.html new file mode 100644 index 0000000000..627bf81215 --- /dev/null +++ b/esphome/components/captive_portal/index.html @@ -0,0 +1,55 @@ + + + + + + + {{ App.get_name() }} + + + + +
+

WiFi Networks

+
+ The ESP will now try to connect to the network...
+ Please give it some time to connect.
+ Note: Copy the changed network to your YAML file - the next OTA update will overwrite these settings. +
+ + + +

WiFi Settings

+
+
+
+
+ +
+

+
+ +

OTA Update

+
+ + +
+
+ + diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg new file mode 100644 index 0000000000..743a1cc55a --- /dev/null +++ b/esphome/components/captive_portal/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css new file mode 100644 index 0000000000..73f82f05f1 --- /dev/null +++ b/esphome/components/captive_portal/stylesheet.css @@ -0,0 +1,58 @@ +* { + box-sizing: inherit; +} + +div, input { + padding: 5px; + font-size: 1em; +} + +input { + width: 95%; +} + +body { + text-align: center; + font-family: sans-serif; +} + +button { + border: 0; + border-radius: 0.3rem; + background-color: #1fa3ec; + color: #fff; + line-height: 2.4rem; + font-size: 1.2rem; + width: 100%; + padding: 0; +} + +.main { + text-align: left; + display: inline-block; + min-width: 260px; +} + +.network { + display: flex; + justify-content: space-between; + align-items: center; +} + +.network-left { + display: flex; + align-items: center; +} + +.network-ssid { + margin-bottom: -7px; + margin-left: 10px; +} + +.info { + border: 1px solid; + margin: 10px 0px; + padding: 15px 10px; + color: #4f8a10; + background-color: #dff2bf; +} diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg new file mode 100644 index 0000000000..189a38193c --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg new file mode 100644 index 0000000000..9b4b2d2396 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg new file mode 100644 index 0000000000..44b7532bb7 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg new file mode 100644 index 0000000000..a22b0b8281 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 2b40f932f9..7c7da6bb0c 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -173,6 +173,9 @@ void Climate::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", climate_mode_to_string(this->mode)); + if (traits.get_supports_action()) { + ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c58eed1a7c..70c6bef13b 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -121,6 +121,8 @@ class Climate : public Nameable { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. + ClimateAction action{CLIMATE_ACTION_OFF}; /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 07b97f4f33..34aa564fb0 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -17,6 +17,18 @@ const char *climate_mode_to_string(ClimateMode mode) { return "UNKNOWN"; } } +const char *climate_action_to_string(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return "OFF"; + case CLIMATE_ACTION_COOLING: + return "COOLING"; + case CLIMATE_ACTION_HEATING: + return "HEATING"; + default: + return "UNKNOWN"; + } +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 28608b7cd8..e5786286d8 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -17,8 +17,19 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_HEAT = 3, }; +/// Enum for the current action of the climate device. Values match those of ClimateMode. +enum ClimateAction : uint8_t { + /// The climate device is off (inactive or no power) + CLIMATE_ACTION_OFF = 0, + /// The climate device is actively cooling (usually in cool or auto mode) + CLIMATE_ACTION_COOLING = 2, + /// The climate device is actively heating (usually in heat or auto mode) + CLIMATE_ACTION_HEATING = 3, +}; + /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); +const char *climate_action_to_string(ClimateAction action); } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 712186aa80..a1db2bc696 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -30,6 +30,7 @@ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_a void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } +void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; @@ -52,6 +53,7 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const { } void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } bool ClimateTraits::get_supports_away() const { return supports_away_; } +bool ClimateTraits::get_supports_action() const { return supports_action_; } } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 34e03455b1..2d6f44eea6 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -23,6 +23,8 @@ namespace climate { * - heat mode (increases current temperature) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. + * - supports action - if the climate device supports reporting the active + * current action of the device with the action property. * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,6 +43,8 @@ class ClimateTraits { void set_supports_heat_mode(bool supports_heat_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; + void set_supports_action(bool supports_action); + bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; float get_visual_min_temperature() const; @@ -58,6 +62,7 @@ class ClimateTraits { bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; bool supports_away_{false}; + bool supports_action_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp new file mode 100644 index 0000000000..3d8b97e131 --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -0,0 +1,57 @@ +#include "climate_ir.h" + +namespace esphome { +namespace climate { + +climate::ClimateTraits ClimateIR::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_auto_mode(true); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_away(false); + traits.set_visual_min_temperature(this->minimum_temperature_); + traits.set_visual_max_temperature(this->maximum_temperature_); + traits.set_visual_temperature_step(this->temperature_step_); + return traits; +} + +void ClimateIR::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + // initialize target temperature to some value so that it's not NAN + this->target_temperature = + roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + } + // Never send nan to HA + if (isnan(this->target_temperature)) + this->target_temperature = 24; +} + +void ClimateIR::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + this->transmit_state(); + this->publish_state(); +} + +} // namespace climate +} // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h new file mode 100644 index 0000000000..85f56c6b6b --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace climate { + +/* A base for climate which works by sending (and receiving) IR codes + + To send IR codes implement + void ClimateIR::transmit_state_() + + Likewise to decode a IR into the AC state, implement + bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true +*/ +class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { + public: + ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) { + this->minimum_temperature_ = minimum_temperature; + this->maximum_temperature_ = maximum_temperature; + this->temperature_step_ = temperature_step; + } + + void setup() override; + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + + protected: + float minimum_temperature_, maximum_temperature_, temperature_step_; + + /// 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; + + /// Transmit via IR the state of this climate controller. + virtual void transmit_state() {} + + bool supports_cool_{true}; + bool supports_heat_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; +} // namespace climate +} // namespace esphome diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 750a97d087..fe74798689 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,20 +1,22 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor +from esphome.components import climate, remote_transmitter, remote_receiver, sensor from esphome.const import CONF_ID, CONF_SENSOR -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['sensor', 'climate_ir'] coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_RECEIVER_ID = 'receiver_id' CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SUPPORTS_COOL = 'supports_cool' CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(CoolixClimate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), @@ -31,6 +33,9 @@ def to_code(config): if CONF_SENSOR in config: sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_RECEIVER_ID in config: + receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + cg.add(receiver.register_listener(var)) transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index ffc67adeb3..c08571c2e9 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -7,19 +7,26 @@ namespace coolix { static const char *TAG = "coolix.climate"; const uint32_t COOLIX_OFF = 0xB27BE0; +const uint32_t COOLIX_SWING = 0xB26BE0; +const uint32_t COOLIX_LED = 0xB5F5A5; +const uint32_t COOLIX_SILENCE_FP = 0xB5F5B6; + // On, 25C, Mode: Auto, Fan: Auto, Zone Follow: Off, Sensor Temp: Ignore. const uint32_t COOLIX_DEFAULT_STATE = 0xB2BFC8; const uint32_t COOLIX_DEFAULT_STATE_AUTO_24_FAN = 0xB21F48; -const uint8_t COOLIX_COOL = 0b00; -const uint8_t COOLIX_DRY = 0b01; -const uint8_t COOLIX_AUTO = 0b10; -const uint8_t COOLIX_HEAT = 0b11; -const uint8_t COOLIX_FAN = 4; // Synthetic. -const uint32_t COOLIX_MODE_MASK = 0b000000000000000000001100; // 0xC +const uint8_t COOLIX_COOL = 0b0000; +const uint8_t COOLIX_DRY_FAN = 0b0100; +const uint8_t COOLIX_AUTO = 0b1000; +const uint8_t COOLIX_HEAT = 0b1100; +const uint32_t COOLIX_MODE_MASK = 0b1100; +const uint32_t COOLIX_FAN_MASK = 0xF000; +const uint32_t COOLIX_FAN_DRY = 0x1000; +const uint32_t COOLIX_FAN_AUTO = 0xB000; +const uint32_t COOLIX_FAN_MIN = 0x9000; +const uint32_t COOLIX_FAN_MED = 0x5000; +const uint32_t COOLIX_FAN_MAX = 0x3000; // Temperature -const uint8_t COOLIX_TEMP_MIN = 17; // Celsius -const uint8_t COOLIX_TEMP_MAX = 30; // Celsius const uint8_t COOLIX_TEMP_RANGE = COOLIX_TEMP_MAX - COOLIX_TEMP_MIN + 1; const uint8_t COOLIX_FAN_TEMP_CODE = 0b1110; // Part of Fan Mode. const uint32_t COOLIX_TEMP_MASK = 0b11110000; @@ -41,80 +48,25 @@ const uint8_t COOLIX_TEMP_MAP[COOLIX_TEMP_RANGE] = { }; // Constants -// Pulse parms are *50-100 for the Mark and *50+100 for the space -// First MARK is the one after the long gap -// pulse parameters in usec -const uint16_t COOLIX_TICK = 560; // Approximately 21 cycles at 38kHz -const uint16_t COOLIX_BIT_MARK_TICKS = 1; -const uint16_t COOLIX_BIT_MARK = COOLIX_BIT_MARK_TICKS * COOLIX_TICK; -const uint16_t COOLIX_ONE_SPACE_TICKS = 3; -const uint16_t COOLIX_ONE_SPACE = COOLIX_ONE_SPACE_TICKS * COOLIX_TICK; -const uint16_t COOLIX_ZERO_SPACE_TICKS = 1; -const uint16_t COOLIX_ZERO_SPACE = COOLIX_ZERO_SPACE_TICKS * COOLIX_TICK; -const uint16_t COOLIX_HEADER_MARK_TICKS = 8; -const uint16_t COOLIX_HEADER_MARK = COOLIX_HEADER_MARK_TICKS * COOLIX_TICK; -const uint16_t COOLIX_HEADER_SPACE_TICKS = 8; -const uint16_t COOLIX_HEADER_SPACE = COOLIX_HEADER_SPACE_TICKS * COOLIX_TICK; -const uint16_t COOLIX_MIN_GAP_TICKS = COOLIX_HEADER_MARK_TICKS + COOLIX_ZERO_SPACE_TICKS; -const uint16_t COOLIX_MIN_GAP = COOLIX_MIN_GAP_TICKS * COOLIX_TICK; +static const uint32_t BIT_MARK_US = 660; +static const uint32_t HEADER_MARK_US = 560 * 8; +static const uint32_t HEADER_SPACE_US = 560 * 8; +static const uint32_t BIT_ONE_SPACE_US = 1500; +static const uint32_t BIT_ZERO_SPACE_US = 450; +static const uint32_t FOOTER_MARK_US = BIT_MARK_US; +static const uint32_t FOOTER_SPACE_US = HEADER_SPACE_US; const uint16_t COOLIX_BITS = 24; -climate::ClimateTraits CoolixClimate::traits() { - auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); - traits.set_visual_min_temperature(17); - traits.set_visual_max_temperature(30); - traits.set_visual_temperature_step(1); - return traits; -} - -void CoolixClimate::setup() { - if (this->sensor_) { - this->sensor_->add_on_state_callback([this](float state) { - this->current_temperature = state; - // current temperature changed, publish state - this->publish_state(); - }); - this->current_temperature = this->sensor_->state; - } else - this->current_temperature = NAN; - // restore set points - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->apply(this); - } else { - // restore from defaults - this->mode = climate::CLIMATE_MODE_AUTO; - // initialize target temperature to some value so that it's not NAN - this->target_temperature = roundf(this->current_temperature); - } -} - -void CoolixClimate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - - this->transmit_state_(); - this->publish_state(); -} - -void CoolixClimate::transmit_state_() { +void CoolixClimate::transmit_state() { uint32_t remote_state; switch (this->mode) { case climate::CLIMATE_MODE_COOL: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_COOL << 2); + remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_COOL; break; case climate::CLIMATE_MODE_HEAT: - remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | (COOLIX_HEAT << 2); + remote_state = (COOLIX_DEFAULT_STATE & ~COOLIX_MODE_MASK) | COOLIX_HEAT; break; case climate::CLIMATE_MODE_AUTO: remote_state = COOLIX_DEFAULT_STATE_AUTO_24_FAN; @@ -127,10 +79,10 @@ void CoolixClimate::transmit_state_() { if (this->mode != climate::CLIMATE_MODE_OFF) { auto temp = (uint8_t) roundf(clamp(this->target_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); remote_state &= ~COOLIX_TEMP_MASK; // Clear the old temp. - remote_state |= (COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4); + remote_state |= COOLIX_TEMP_MAP[temp - COOLIX_TEMP_MIN] << 4; } - ESP_LOGV(TAG, "Sending coolix code: %u", remote_state); + ESP_LOGV(TAG, "Sending coolix code: 0x%02X", remote_state); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -139,32 +91,113 @@ void CoolixClimate::transmit_state_() { uint16_t repeat = 1; for (uint16_t r = 0; r <= repeat; r++) { // Header - data->mark(COOLIX_HEADER_MARK); - data->space(COOLIX_HEADER_SPACE); + data->mark(HEADER_MARK_US); + data->space(HEADER_SPACE_US); // Data - // Break data into byte segments, starting at the Most Significant + // Break data into bytes, starting at the Most Significant // Byte. Each byte then being sent normal, then followed inverted. for (uint16_t i = 8; i <= COOLIX_BITS; i += 8) { // Grab a bytes worth of data. - uint8_t segment = (remote_state >> (COOLIX_BITS - i)) & 0xFF; + uint8_t byte = (remote_state >> (COOLIX_BITS - i)) & 0xFF; // Normal for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(COOLIX_BIT_MARK); - data->space((segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE); + data->mark(BIT_MARK_US); + data->space((byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); } // Inverted for (uint64_t mask = 1ULL << 7; mask; mask >>= 1) { - data->mark(COOLIX_BIT_MARK); - data->space(!(segment & mask) ? COOLIX_ONE_SPACE : COOLIX_ZERO_SPACE); + data->mark(BIT_MARK_US); + data->space(!(byte & mask) ? BIT_ONE_SPACE_US : BIT_ZERO_SPACE_US); } } // Footer - data->mark(COOLIX_BIT_MARK); - data->space(COOLIX_MIN_GAP); // Pause before repeating + data->mark(BIT_MARK_US); + data->space(FOOTER_SPACE_US); // Pause before repeating } transmit.perform(); } +bool CoolixClimate::on_receive(remote_base::RemoteReceiveData data) { + // Decoded remote state y 3 bytes long code. + uint32_t remote_state = 0; + // The protocol sends the data twice, read here + uint32_t loop_read; + for (uint16_t loop = 1; loop <= 2; loop++) { + if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) + return false; + loop_read = 0; + for (uint8_t a_byte = 0; a_byte < 3; a_byte++) { + uint8_t byte = 0; + for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { + if (data.expect_item(BIT_MARK_US, BIT_ONE_SPACE_US)) + byte |= 1 << a_bit; + else if (!data.expect_item(BIT_MARK_US, BIT_ZERO_SPACE_US)) + return false; + } + // Need to see this segment inverted + for (int8_t a_bit = 7; a_bit >= 0; a_bit--) { + bool bit = byte & (1 << a_bit); + if (!data.expect_item(BIT_MARK_US, bit ? BIT_ZERO_SPACE_US : BIT_ONE_SPACE_US)) + return false; + } + // Receiving MSB first: reorder bytes + loop_read |= byte << ((2 - a_byte) * 8); + } + // Footer Mark + if (!data.expect_mark(BIT_MARK_US)) + return false; + if (loop == 1) { + // Back up state on first loop + remote_state = loop_read; + if (!data.expect_space(FOOTER_SPACE_US)) + return false; + } + } + + ESP_LOGV(TAG, "Decoded 0x%02X", remote_state); + if (remote_state != loop_read || (remote_state & 0xFF0000) != 0xB20000) + return false; + + if (remote_state == COOLIX_OFF) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + if ((remote_state & COOLIX_MODE_MASK) == COOLIX_HEAT) + this->mode = climate::CLIMATE_MODE_HEAT; + else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_AUTO) + this->mode = climate::CLIMATE_MODE_AUTO; + else if ((remote_state & COOLIX_MODE_MASK) == COOLIX_DRY_FAN) { + // climate::CLIMATE_MODE_DRY; + if ((remote_state & COOLIX_FAN_MASK) == COOLIX_FAN_DRY) + ESP_LOGV(TAG, "Not supported DRY mode. Reporting AUTO"); + else + ESP_LOGV(TAG, "Not supported FAN Auto mode. Reporting AUTO"); + this->mode = climate::CLIMATE_MODE_AUTO; + } else + this->mode = climate::CLIMATE_MODE_COOL; + + // Fan Speed + // When climate::CLIMATE_MODE_DRY is implemented replace following line with this: + // if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO || this->mode == climate::CLIMATE_MODE_DRY) + if ((remote_state & COOLIX_FAN_AUTO) == COOLIX_FAN_AUTO) + ESP_LOGV(TAG, "Not supported FAN speed AUTO"); + else if ((remote_state & COOLIX_FAN_MIN) == COOLIX_FAN_MIN) + ESP_LOGV(TAG, "Not supported FAN speed MIN"); + else if ((remote_state & COOLIX_FAN_MED) == COOLIX_FAN_MED) + ESP_LOGV(TAG, "Not supported FAN speed MED"); + else if ((remote_state & COOLIX_FAN_MAX) == COOLIX_FAN_MAX) + ESP_LOGV(TAG, "Not supported FAN speed MAX"); + + // Temperature + uint8_t temperature_code = (remote_state & COOLIX_TEMP_MASK) >> 4; + for (uint8_t i = 0; i < COOLIX_TEMP_RANGE; i++) + if (COOLIX_TEMP_MAP[i] == temperature_code) + this->target_temperature = i + COOLIX_TEMP_MIN; + } + this->publish_state(); + + return true; +} + } // namespace coolix } // namespace esphome diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 0d52018d2a..125d8ffd37 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -1,39 +1,23 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#include "esphome/components/sensor/sensor.h" +#include "esphome/components/climate_ir/climate_ir.h" namespace esphome { namespace coolix { -class CoolixClimate : public climate::Climate, public Component { +// Temperature +const uint8_t COOLIX_TEMP_MIN = 17; // Celsius +const uint8_t COOLIX_TEMP_MAX = 30; // Celsius + +class CoolixClimate : public climate::ClimateIR { public: - void setup() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } - void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + CoolixClimate() : climate::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} 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; - /// Transmit via IR the state of this climate controller. - void transmit_state_(); - - bool supports_cool_{true}; - bool supports_heat_{true}; - - remote_transmitter::RemoteTransmitterComponent *transmitter_; - sensor::Sensor *sensor_{nullptr}; + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace coolix diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 1a7bac2844..674cc0ae98 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -8,6 +8,18 @@ namespace ct_clamp { static const char *TAG = "ct_clamp"; +void CTClampSensor::setup() { + this->is_calibrating_offset_ = true; + this->high_freq_.start(); + this->set_timeout("calibrate_offset", this->sample_duration_, [this]() { + this->high_freq_.stop(); + this->is_calibrating_offset_ = false; + if (this->num_samples_ != 0) { + this->offset_ = this->sample_sum_ / this->num_samples_; + } + }); +} + void CTClampSensor::dump_config() { LOG_SENSOR("", "CT Clamp Sensor", this); ESP_LOGCONFIG(TAG, " Sample Duration: %.2fs", this->sample_duration_ / 1e3f); @@ -15,6 +27,9 @@ void CTClampSensor::dump_config() { } void CTClampSensor::update() { + if (this->is_calibrating_offset_) + return; + // Update only starts the sampling phase, in loop() the actual sampling is happening. // Request a high loop() execution interval during sampling phase. @@ -44,12 +59,18 @@ void CTClampSensor::update() { } void CTClampSensor::loop() { - if (!this->is_sampling_) + if (!this->is_sampling_ && !this->is_calibrating_offset_) return; // Perform a single sample float value = this->source_->sample(); + if (this->is_calibrating_offset_) { + this->sample_sum_ += value; + this->num_samples_++; + return; + } + // Adjust DC offset via low pass filter (exponential moving average) const float alpha = 0.001f; this->offset_ = this->offset_ * (1 - alpha) + value * alpha; diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.h b/esphome/components/ct_clamp/ct_clamp_sensor.h index d816ac781a..c709f6718b 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.h +++ b/esphome/components/ct_clamp/ct_clamp_sensor.h @@ -10,10 +10,14 @@ namespace ct_clamp { class CTClampSensor : public sensor::Sensor, public PollingComponent { public: + void setup() override; void update() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } + float get_setup_priority() const override { + // After the base sensor has been initialized + return setup_priority::DATA - 1.0f; + } void set_sample_duration(uint32_t sample_duration) { sample_duration_ = sample_duration; } void set_source(voltage_sampler::VoltageSampler *source) { source_ = source; } @@ -40,6 +44,8 @@ class CTClampSensor : public sensor::Sensor, public PollingComponent { float sample_sum_ = 0.0f; uint32_t num_samples_ = 0; bool is_sampling_ = false; + /// Calibrate offset value once at boot + bool is_calibrating_offset_ = false; }; } // namespace ct_clamp diff --git a/esphome/components/cwww/light.py b/esphome/components/cwww/light.py index 5d2b4ab2c9..f86f1eace0 100644 --- a/esphome/components/cwww/light.py +++ b/esphome/components/cwww/light.py @@ -25,4 +25,4 @@ def to_code(config): wwhite = yield cg.get_variable(config[CONF_WARM_WHITE]) cg.add(var.set_warm_white(wwhite)) - cg.add(var.set_warm_white_temperature(config[CONF_COLD_WHITE_COLOR_TEMPERATURE])) + cg.add(var.set_warm_white_temperature(config[CONF_WARM_WHITE_COLOR_TEMPERATURE])) diff --git a/esphome/components/dallas/dallas_component.cpp b/esphome/components/dallas/dallas_component.cpp index 1d3e693ff9..6eeddb1b56 100644 --- a/esphome/components/dallas/dallas_component.cpp +++ b/esphome/components/dallas/dallas_component.cpp @@ -132,10 +132,14 @@ void DallasComponent::update() { enable_interrupts(); if (!res) { + ESP_LOGW(TAG, "'%s' - Reseting bus for read failed!", sensor->get_name().c_str()); + sensor->publish_state(NAN); this->status_set_warning(); return; } if (!sensor->check_scratch_pad()) { + ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str()); + sensor->publish_state(NAN); this->status_set_warning(); return; } @@ -244,11 +248,7 @@ bool DallasTemperatureSensor::check_scratch_pad() { this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], crc8(this->scratch_pad_, 8)); #endif - if (crc8(this->scratch_pad_, 8) != this->scratch_pad_[8]) { - ESP_LOGE(TAG, "Reading scratchpad from Dallas Sensor failed"); - return false; - } - return true; + return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]; } float DallasTemperatureSensor::get_temp_c() { int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3); diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index 9cb3ec9cef..4ffc034d50 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/defines.h" +#include "esphome/core/version.h" #ifdef ARDUINO_ARCH_ESP32 #include @@ -19,7 +20,7 @@ void DebugComponent::dump_config() { return; #endif - ESP_LOGD(TAG, "ESPHome Core version %s", ESPHOME_VERSION); + ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); this->free_heap_ = ESP.getFreeHeap(); ESP_LOGD(TAG, "Free Heap Size: %u bytes", this->free_heap_); diff --git a/esphome/components/deep_sleep/__init__.py b/esphome/components/deep_sleep/__init__.py index 93ef04d195..5babf422bd 100644 --- a/esphome/components/deep_sleep/__init__.py +++ b/esphome/components/deep_sleep/__init__.py @@ -1,7 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins, automation -from esphome.automation import maybe_simple_id from esphome.const import CONF_ID, CONF_MODE, CONF_NUMBER, CONF_PINS, CONF_RUN_CYCLES, \ CONF_RUN_DURATION, CONF_SLEEP_DURATION, CONF_WAKEUP_PIN @@ -85,7 +84,7 @@ def to_code(config): cg.add_define('USE_DEEP_SLEEP') -DEEP_SLEEP_ACTION_SCHEMA = maybe_simple_id({ +DEEP_SLEEP_ACTION_SCHEMA = automation.maybe_simple_id({ cv.GenerateID(): cv.use_id(DeepSleepComponent), }) diff --git a/esphome/components/deep_sleep/deep_sleep_component.cpp b/esphome/components/deep_sleep/deep_sleep_component.cpp index 217c0cbf0d..684d7e12bf 100644 --- a/esphome/components/deep_sleep/deep_sleep_component.cpp +++ b/esphome/components/deep_sleep/deep_sleep_component.cpp @@ -87,7 +87,7 @@ void DeepSleepComponent::begin_sleep(bool manual) { ESP.deepSleep(*this->sleep_duration_); #endif } -float DeepSleepComponent::get_setup_priority() const { return -100.0f; } +float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; } void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; } } // namespace deep_sleep diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 24a017e478..1e28246bee 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -56,6 +56,8 @@ void DHT::update() { str = " and consider manually specifying the DHT model using the model option"; } ESP_LOGW(TAG, "Invalid readings! Please check your wiring (pull-up resistor, pin number)%s.", str); + this->temperature_sensor_->publish_state(NAN); + this->humidity_sensor_->publish_state(NAN); this->status_set_warning(); } } @@ -153,28 +155,39 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro if (checksum_a != data[4] && checksum_b != data[4]) { if (report_errors) { - ESP_LOGE(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]); + ESP_LOGW(TAG, "Checksum invalid: %u!=%u", checksum_a, data[4]); } return false; } if (this->model_ == DHT_MODEL_DHT11) { *humidity = data[0]; + if (*humidity > 100) + *humidity = NAN; *temperature = data[2]; } else { uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF); uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF); - *humidity = raw_humidity * 0.1f; if ((raw_temperature & 0x8000) != 0) raw_temperature = ~(raw_temperature & 0x7FFF); + if (raw_temperature == 1 && raw_humidity == 10) { + if (report_errors) { + ESP_LOGW(TAG, "Invalid temperature+humidity! Sensor reported 1°C and 1%% Hum"); + } + return false; + } + + *humidity = raw_humidity * 0.1f; + if (*humidity > 100) + *humidity = NAN; *temperature = int16_t(raw_temperature) * 0.1f; } if (*temperature == 0.0f && (*humidity == 1.0f || *humidity == 2.0f)) { if (report_errors) { - ESP_LOGE(TAG, "DHT reports invalid data. Is the update interval too high or the sensor damaged?"); + ESP_LOGW(TAG, "DHT reports invalid data. Is the update interval too high or the sensor damaged?"); } return false; } diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index e1e18bb7f9..8455f74fb4 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_MODEL, CONF_PIN, CONF_TEMPERATURE, \ - CONF_UPDATE_INTERVAL, ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT + ICON_THERMOMETER, UNIT_CELSIUS, ICON_WATER_PERCENT, UNIT_PERCENT from esphome.cpp_helpers import gpio_pin_expression dht_ns = cg.esphome_ns.namespace('dht') @@ -24,7 +24,6 @@ CONFIG_SCHEMA = cv.Schema({ 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_MODEL, default='auto detect'): cv.enum(DHT_MODELS, upper=True, space='_'), - cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval, }).extend(cv.polling_component_schema('60s')) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 5c204fc7a4..38d19d832e 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -3,7 +3,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import core, automation from esphome.automation import maybe_simple_id -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION, CONF_UPDATE_INTERVAL +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_ROTATION from esphome.core import coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -33,7 +33,6 @@ def validate_rotation(value): BASIC_DISPLAY_SCHEMA = cv.Schema({ - cv.Optional(CONF_UPDATE_INTERVAL): cv.update_interval, cv.Optional(CONF_LAMBDA): cv.lambda_, }) @@ -48,8 +47,6 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend({ @coroutine def setup_display_core_(var, config): - if CONF_UPDATE_INTERVAL in config: - cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) if CONF_ROTATION in config: cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]])) if CONF_PAGES in config: diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 5a4862f733..7e998e77b1 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,19 +1,48 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_SCAN_INTERVAL, ESP_PLATFORM_ESP32 +from esphome.const import CONF_ID, ESP_PLATFORM_ESP32, CONF_INTERVAL, \ + CONF_DURATION from esphome.core import coroutine ESP_PLATFORMS = [ESP_PLATFORM_ESP32] AUTO_LOAD = ['xiaomi_ble'] CONF_ESP32_BLE_ID = 'esp32_ble_id' +CONF_SCAN_PARAMETERS = 'scan_parameters' +CONF_WINDOW = 'window' +CONF_ACTIVE = 'active' esp32_ble_tracker_ns = cg.esphome_ns.namespace('esp32_ble_tracker') ESP32BLETracker = esp32_ble_tracker_ns.class_('ESP32BLETracker', cg.Component) ESPBTDeviceListener = esp32_ble_tracker_ns.class_('ESPBTDeviceListener') + +def validate_scan_parameters(config): + duration = config[CONF_DURATION] + interval = config[CONF_INTERVAL] + window = config[CONF_WINDOW] + + if window > interval: + raise cv.Invalid("Scan window ({}) needs to be smaller than scan interval ({})" + "".format(window, interval)) + + if interval.total_milliseconds * 3 > duration.total_milliseconds: + raise cv.Invalid("Scan duration needs to be at least three times the scan interval to" + "cover all BLE channels.") + + return config + + CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(ESP32BLETracker), - cv.Optional(CONF_SCAN_INTERVAL, default='300s'): cv.positive_time_period_seconds, + cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(cv.Schema({ + cv.Optional(CONF_DURATION, default='5min'): cv.positive_time_period_seconds, + cv.Optional(CONF_INTERVAL, default='320ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_WINDOW, default='200ms'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_ACTIVE, default=True): cv.boolean, + }), validate_scan_parameters), + + cv.Optional('scan_interval'): cv.invalid("This option has been removed in 1.14 (Reason: " + "it never had an effect)"), }).extend(cv.COMPONENT_SCHEMA) ESP_BLE_DEVICE_SCHEMA = cv.Schema({ @@ -24,7 +53,11 @@ ESP_BLE_DEVICE_SCHEMA = cv.Schema({ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) - cg.add(var.set_scan_interval(config[CONF_SCAN_INTERVAL])) + params = config[CONF_SCAN_PARAMETERS] + cg.add(var.set_scan_duration(params[CONF_DURATION])) + cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) + cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) + cg.add(var.set_scan_active(params[CONF_ACTIVE])) @coroutine diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index 5ef814c2ba..ff1fe59668 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,5 +1,6 @@ #include "esp32_ble_tracker.h" #include "esphome/core/log.h" +#include "esphome/core/application.h" #ifdef ARDUINO_ARCH_ESP32 @@ -139,7 +140,7 @@ bool ESP32BLETracker::ble_setup() { void ESP32BLETracker::start_scan(bool first) { if (!xSemaphoreTake(this->scan_end_lock_, 0L)) { - ESP_LOGW("Cannot start scan!"); + ESP_LOGW(TAG, "Cannot start scan!"); return; } @@ -149,19 +150,19 @@ void ESP32BLETracker::start_scan(bool first) { listener->on_scan_end(); } this->already_discovered_.clear(); - this->scan_params_.scan_type = BLE_SCAN_TYPE_ACTIVE; + this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; - // Values determined empirically, higher scan intervals and lower scan windows make the ESP more stable - // Ideally, these values should both be quite low, especially scan window. 0x10/0x10 is the esp-idf - // default and works quite well. 0x100/0x50 discovers a few less BLE broadcast packets but is a lot - // more stable (order of several hours). The old ESPHome default (1600/1600) was terrible with - // crashes every few minutes - this->scan_params_.scan_interval = 0x200; - this->scan_params_.scan_window = 0x30; + this->scan_params_.scan_interval = this->scan_interval_; + this->scan_params_.scan_window = this->scan_window_; esp_ble_gap_set_scan_params(&this->scan_params_); - esp_ble_gap_start_scanning(this->scan_interval_); + esp_ble_gap_start_scanning(this->scan_duration_); + + this->set_timeout("scan", this->scan_duration_ * 2000, []() { + ESP_LOGW(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); + App.reboot(); + }); } void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { @@ -448,10 +449,12 @@ const std::string &ESPBTDevice::get_manufacturer_data() const { return this->man const std::string &ESPBTDevice::get_service_data() const { return this->service_data_; } const optional &ESPBTDevice::get_service_data_uuid() const { return this->service_data_uuid_; } -void ESP32BLETracker::set_scan_interval(uint32_t scan_interval) { this->scan_interval_ = scan_interval; } void ESP32BLETracker::dump_config() { ESP_LOGCONFIG(TAG, "BLE Tracker:"); - ESP_LOGCONFIG(TAG, " Scan Interval: %u s", this->scan_interval_); + ESP_LOGCONFIG(TAG, " Scan Duration: %u s", this->scan_duration_); + ESP_LOGCONFIG(TAG, " Scan Interval: %u ms", this->scan_interval_); + ESP_LOGCONFIG(TAG, " Scan Window: %u ms", this->scan_window_); + ESP_LOGCONFIG(TAG, " Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); } void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { const uint64_t address = device.address_uint64(); diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index f1bcada621..82e8e553fc 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -107,7 +107,10 @@ class ESPBTDeviceListener { class ESP32BLETracker : public Component { public: - void set_scan_interval(uint32_t scan_interval); + void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } + void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } + void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } + void set_scan_active(bool scan_active) { scan_active_ = scan_active; } /// Setup the FreeRTOS task and the Bluetooth stack. void setup() override; @@ -142,7 +145,10 @@ class ESP32BLETracker : public Component { /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; /// The interval in seconds to perform scans. - uint32_t scan_interval_{300}; + uint32_t scan_duration_; + uint32_t scan_interval_; + uint32_t scan_window_; + bool scan_active_; SemaphoreHandle_t scan_result_lock_; SemaphoreHandle_t scan_end_lock_; size_t scan_result_index_{0}; diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e85d0ef5c2..56bc407e84 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -116,6 +116,7 @@ void ESP32TouchComponent::loop() { touch_pad_read(child->get_touch_pad(), &value); } + child->value_ = value; child->publish_state(value < child->get_threshold()); if (this->setup_mode_) { @@ -128,23 +129,7 @@ void ESP32TouchComponent::loop() { delay(250); } } -void ESP32TouchComponent::register_touch_pad(ESP32TouchBinarySensor *pad) { this->children_.push_back(pad); } -void ESP32TouchComponent::set_setup_mode(bool setup_mode) { this->setup_mode_ = setup_mode; } -bool ESP32TouchComponent::iir_filter_enabled_() const { return this->iir_filter_ > 0; } -void ESP32TouchComponent::set_iir_filter(uint32_t iir_filter) { this->iir_filter_ = iir_filter; } -float ESP32TouchComponent::get_setup_priority() const { return setup_priority::DATA; } -void ESP32TouchComponent::set_sleep_duration(uint16_t sleep_duration) { this->sleep_cycle_ = sleep_duration; } -void ESP32TouchComponent::set_measurement_duration(uint16_t meas_cycle) { this->meas_cycle_ = meas_cycle; } -void ESP32TouchComponent::set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { - this->low_voltage_reference_ = low_voltage_reference; -} -void ESP32TouchComponent::set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { - this->high_voltage_reference_ = high_voltage_reference; -} -void ESP32TouchComponent::set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { - this->voltage_attenuation_ = voltage_attenuation; -} void ESP32TouchComponent::on_shutdown() { if (this->iir_filter_enabled_()) { touch_pad_filter_stop(); @@ -155,8 +140,6 @@ void ESP32TouchComponent::on_shutdown() { ESP32TouchBinarySensor::ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold) : BinarySensor(name), touch_pad_(touch_pad), threshold_(threshold) {} -touch_pad_t ESP32TouchBinarySensor::get_touch_pad() const { return this->touch_pad_; } -uint16_t ESP32TouchBinarySensor::get_threshold() const { return this->threshold_; } } // namespace esp32_touch } // namespace esphome diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index b68876c33e..7adee23971 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -12,32 +12,36 @@ class ESP32TouchBinarySensor; class ESP32TouchComponent : public Component { public: - void register_touch_pad(ESP32TouchBinarySensor *pad); + void register_touch_pad(ESP32TouchBinarySensor *pad) { children_.push_back(pad); } - void set_setup_mode(bool setup_mode); + void set_setup_mode(bool setup_mode) { setup_mode_ = setup_mode; } - void set_iir_filter(uint32_t iir_filter); + void set_iir_filter(uint32_t iir_filter) { iir_filter_ = iir_filter; } - void set_sleep_duration(uint16_t sleep_duration); + void set_sleep_duration(uint16_t sleep_duration) { sleep_cycle_ = sleep_duration; } - void set_measurement_duration(uint16_t meas_cycle); + void set_measurement_duration(uint16_t meas_cycle) { meas_cycle_ = meas_cycle; } - void set_low_voltage_reference(touch_low_volt_t low_voltage_reference); + void set_low_voltage_reference(touch_low_volt_t low_voltage_reference) { + low_voltage_reference_ = low_voltage_reference; + } - void set_high_voltage_reference(touch_high_volt_t high_voltage_reference); + void set_high_voltage_reference(touch_high_volt_t high_voltage_reference) { + high_voltage_reference_ = high_voltage_reference; + } - void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation); + void set_voltage_attenuation(touch_volt_atten_t voltage_attenuation) { voltage_attenuation_ = voltage_attenuation; } void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override; + float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; protected: /// Is the IIR filter enabled? - bool iir_filter_enabled_() const; + bool iir_filter_enabled_() const { return iir_filter_ > 0; } uint16_t sleep_cycle_{}; uint16_t meas_cycle_{65535}; @@ -54,14 +58,17 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor { public: ESP32TouchBinarySensor(const std::string &name, touch_pad_t touch_pad, uint16_t threshold); - touch_pad_t get_touch_pad() const; - uint16_t get_threshold() const; + touch_pad_t get_touch_pad() const { return touch_pad_; } + uint16_t get_threshold() const { return threshold_; } + void set_threshold(uint16_t threshold) { threshold_ = threshold; } + uint16_t get_value() const { return value_; } protected: friend ESP32TouchComponent; touch_pad_t touch_pad_; uint16_t threshold_; + uint16_t value_; }; } // namespace esp32_touch diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index f3bc91440e..50fdb1c2c9 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -23,7 +23,7 @@ FanSpeed = fan_ns.enum('FanSpeed') FAN_SPEEDS = { 'OFF': FanSpeed.FAN_SPEED_OFF, 'LOW': FanSpeed.FAN_SPEED_LOW, - 'MEDIuM': FanSpeed.FAN_SPEED_MEDIUM, + 'MEDIUM': FanSpeed.FAN_SPEED_MEDIUM, 'HIGH': FanSpeed.FAN_SPEED_HIGH, } diff --git a/esphome/components/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index aad30198af..b552c917c0 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -5,8 +5,7 @@ from esphome.const import CONF_OUTPUT_ID, CONF_NUM_LEDS, CONF_RGB_ORDER, CONF_MA from esphome.core import coroutine fastled_base_ns = cg.esphome_ns.namespace('fastled_base') -FastLEDLightOutput = fastled_base_ns.class_('FastLEDLightOutput', cg.Component, - light.AddressableLight) +FastLEDLightOutput = fastled_base_ns.class_('FastLEDLightOutput', light.AddressableLight) RGB_ORDERS = [ 'RGB', @@ -35,5 +34,6 @@ def new_fastled_light(config): cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE])) yield light.register_light(var, config) - cg.add_library('FastLED', '3.2.0') + # https://github.com/FastLED/FastLED/blob/master/library.json + cg.add_library('FastLED', '3.2.9') yield var diff --git a/esphome/components/fastled_base/fastled_light.h b/esphome/components/fastled_base/fastled_light.h index f9ae2f58d5..0729941e31 100644 --- a/esphome/components/fastled_base/fastled_light.h +++ b/esphome/components/fastled_base/fastled_light.h @@ -16,7 +16,7 @@ namespace esphome { namespace fastled_base { -class FastLEDLightOutput : public Component, public light::AddressableLight { +class FastLEDLightOutput : public light::AddressableLight { public: /// Only for custom effects: Get the internal controller. CLEDController *get_controller() const { return this->controller_; } diff --git a/esphome/components/globals/__init__.py b/esphome/components/globals/__init__.py index 4dce6e7583..e7030978ed 100644 --- a/esphome/components/globals/__init__.py +++ b/esphome/components/globals/__init__.py @@ -50,6 +50,7 @@ def globals_set_to_code(config, action_id, template_arg, args): full_id, paren = yield cg.get_variable_with_full_id(config[CONF_ID]) template_arg = cg.TemplateArguments(full_id.type, *template_arg) var = cg.new_Pvariable(action_id, template_arg, paren) - templ = yield cg.templatable(config[CONF_VALUE], args, None) + templ = yield cg.templatable(config[CONF_VALUE], args, None, + to_exp=cg.RawExpression) cg.add(var.set_value(templ)) yield var diff --git a/esphome/components/globals/globals_component.h b/esphome/components/globals/globals_component.h index c7d2a18d84..397c55f6c4 100644 --- a/esphome/components/globals/globals_component.h +++ b/esphome/components/globals/globals_component.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace globals { @@ -64,5 +65,7 @@ template class GlobalVarSetAction : public Action T &id(GlobalsComponent *value) { return value->value(); } + } // namespace globals } // namespace esphome diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 3ecbc89f73..ddbd29d5f8 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -20,4 +20,6 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) yield uart.register_uart_device(var, config) - cg.add_library('TinyGPSPlus', '1.0.2') + + # https://platformio.org/lib/show/1655/TinyGPSPlus + cg.add_library('1655', '1.0.2') # TinyGPSPlus, has name conflict diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 0391a9a955..26371565f3 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -8,5 +8,41 @@ static const char *TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::loop() { + while (this->available() && !this->has_time_) { + if (this->tiny_gps_.encode(this->read())) { + if (tiny_gps_.location.isUpdated()) { + ESP_LOGD(TAG, "Location:"); + ESP_LOGD(TAG, " Lat: %f", tiny_gps_.location.lat()); + ESP_LOGD(TAG, " Lon: %f", tiny_gps_.location.lng()); + } + + if (tiny_gps_.speed.isUpdated()) { + ESP_LOGD(TAG, "Speed:"); + ESP_LOGD(TAG, " %f km/h", tiny_gps_.speed.kmph()); + } + if (tiny_gps_.course.isUpdated()) { + ESP_LOGD(TAG, "Course:"); + ESP_LOGD(TAG, " %f °", tiny_gps_.course.deg()); + } + if (tiny_gps_.altitude.isUpdated()) { + ESP_LOGD(TAG, "Altitude:"); + ESP_LOGD(TAG, " %f m", tiny_gps_.altitude.meters()); + } + if (tiny_gps_.satellites.isUpdated()) { + ESP_LOGD(TAG, "Satellites:"); + ESP_LOGD(TAG, " %d", tiny_gps_.satellites.value()); + } + if (tiny_gps_.satellites.isUpdated()) { + ESP_LOGD(TAG, "HDOP:"); + ESP_LOGD(TAG, " %.2f", tiny_gps_.hdop.hdop()); + } + + for (auto *listener : this->listeners_) + listener->on_update(this->tiny_gps_); + } + } +} + } // namespace gps } // namespace esphome diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 7d845d1bed..84a9248bc6 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -27,14 +27,7 @@ class GPS : public Component, public uart::UARTDevice { this->listeners_.push_back(listener); } float get_setup_priority() const override { return setup_priority::HARDWARE; } - void loop() override { - while (this->available() && !this->has_time_) { - if (this->tiny_gps_.encode(this->read())) { - for (auto *listener : this->listeners_) - listener->on_update(this->tiny_gps_); - } - } - } + void loop() override; TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: diff --git a/esphome/components/gps/time/gps_time.cpp b/esphome/components/gps/time/gps_time.cpp index c6aa8adc67..468ad09bac 100644 --- a/esphome/components/gps/time/gps_time.cpp +++ b/esphome/components/gps/time/gps_time.cpp @@ -6,5 +6,29 @@ namespace gps { static const char *TAG = "gps.time"; +void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { + if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) + return; + if (!tiny_gps.time.isUpdated() || !tiny_gps.date.isUpdated()) + return; + if (tiny_gps.date.year() < 2019) + return; + + time::ESPTime val{}; + val.year = tiny_gps.date.year(); + val.month = tiny_gps.date.month(); + val.day_of_month = tiny_gps.date.day(); + // Set these to valid value for recalc_timestamp_utc - it's not used for calculation + val.day_of_week = 1; + val.day_of_year = 1; + + val.hour = tiny_gps.time.hour(); + val.minute = tiny_gps.time.minute(); + val.second = tiny_gps.time.second(); + val.recalc_timestamp_utc(false); + this->synchronize_epoch_(val.timestamp); + this->has_time_ = true; +} + } // namespace gps } // namespace esphome diff --git a/esphome/components/gps/time/gps_time.h b/esphome/components/gps/time/gps_time.h index b09aee364f..f6462be3e0 100644 --- a/esphome/components/gps/time/gps_time.h +++ b/esphome/components/gps/time/gps_time.h @@ -18,20 +18,7 @@ class GPSTime : public time::RealTimeClock, public GPSListener { } protected: - void from_tiny_gps_(TinyGPSPlus &tiny_gps) { - if (!tiny_gps.time.isValid() || !tiny_gps.date.isValid()) - return; - time::ESPTime val{}; - val.year = tiny_gps.date.year(); - val.month = tiny_gps.date.month(); - val.day_of_month = tiny_gps.date.day(); - val.hour = tiny_gps.time.hour(); - val.minute = tiny_gps.time.minute(); - val.second = tiny_gps.time.second(); - val.recalc_timestamp_utc(false); - this->synchronize_epoch_(val.timestamp); - this->has_time_ = true; - } + void from_tiny_gps_(TinyGPSPlus &tiny_gps); bool has_time_{false}; }; diff --git a/esphome/components/hdc1080/hdc1080.cpp b/esphome/components/hdc1080/hdc1080.cpp index 81637039ca..4041c0c464 100644 --- a/esphome/components/hdc1080/hdc1080.cpp +++ b/esphome/components/hdc1080/hdc1080.cpp @@ -19,7 +19,7 @@ void HDC1080Component::setup() { 0b00000000 // reserved }; - if (this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { + if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) { this->mark_failed(); return; } diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index b9321b51c6..4e5dc0f67f 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -8,6 +8,8 @@ namespace esphome { namespace hlw8012 { +enum HLW8012InitialMode { HLW8012_INITIAL_MODE_CURRENT = 0, HLW8012_INITIAL_MODE_VOLTAGE }; + class HLW8012Component : public PollingComponent { public: void setup() override; @@ -15,6 +17,9 @@ class HLW8012Component : public PollingComponent { float get_setup_priority() const override; void update() override; + void set_initial_mode(HLW8012InitialMode initial_mode) { + current_mode_ = initial_mode == HLW8012_INITIAL_MODE_CURRENT; + } void set_change_mode_every(uint32_t change_mode_every) { change_mode_every_ = change_mode_every; } void set_current_resistor(float current_resistor) { current_resistor_ = current_resistor; } void set_voltage_divider(float voltage_divider) { voltage_divider_ = voltage_divider; } diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 697c34f9d2..e1f02b8fd2 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import sensor -from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_CURRENT, \ +from esphome.const import CONF_CHANGE_MODE_EVERY, CONF_INITIAL_MODE, CONF_CURRENT, \ CONF_CURRENT_RESISTOR, CONF_ID, CONF_POWER, CONF_SEL_PIN, CONF_VOLTAGE, CONF_VOLTAGE_DIVIDER, \ ICON_FLASH, UNIT_VOLT, UNIT_AMPERE, UNIT_WATT @@ -10,6 +10,11 @@ AUTO_LOAD = ['pulse_counter'] hlw8012_ns = cg.esphome_ns.namespace('hlw8012') HLW8012Component = hlw8012_ns.class_('HLW8012Component', cg.PollingComponent) +HLW8012InitialMode = hlw8012_ns.enum('HLW8012InitialMode') +INITIAL_MODES = { + CONF_CURRENT: HLW8012InitialMode.HLW8012_INITIAL_MODE_CURRENT, + CONF_VOLTAGE: HLW8012InitialMode.HLW8012_INITIAL_MODE_VOLTAGE, +} CONF_CF1_PIN = 'cf1_pin' CONF_CF_PIN = 'cf_pin' @@ -28,6 +33,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance, cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float, cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(cv.uint32_t, cv.Range(min=1)), + cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(*INITIAL_MODES, lower=True), }).extend(cv.polling_component_schema('60s')) @@ -54,3 +60,4 @@ def to_code(config): cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR])) cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER])) cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY])) + cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]])) diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp index 61c73d272b..203f6d8a24 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.cpp @@ -16,14 +16,16 @@ void HomeassistantBinarySensor::setup() { ESP_LOGW(TAG, "Can't convert '%s' to binary state!", state.c_str()); break; case PARSE_ON: - ESP_LOGD(TAG, "'%s': Got state ON", this->entity_id_.c_str()); - this->publish_state(true); - break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Got state OFF", this->entity_id_.c_str()); - this->publish_state(false); + bool new_state = val == PARSE_ON; + ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state)); + if (this->initial_) + this->publish_initial_state(new_state); + else + this->publish_state(new_state); break; } + this->initial_ = false; }); } void HomeassistantBinarySensor::dump_config() { diff --git a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h index c2c7ec4480..e468fd00eb 100644 --- a/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h +++ b/esphome/components/homeassistant/binary_sensor/homeassistant_binary_sensor.h @@ -15,6 +15,7 @@ class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Com protected: std::string entity_id_; + bool initial_{true}; }; } // namespace homeassistant diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index b21fd4c0ce..e9d97690fb 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -22,19 +22,5 @@ void HomeassistantTime::setup() { HomeassistantTime *global_homeassistant_time = nullptr; -bool GetTimeResponse::decode_32bit(uint32_t field_id, uint32_t value) { - switch (field_id) { - case 1: - // fixed32 epoch_seconds = 1; - if (global_homeassistant_time != nullptr) { - global_homeassistant_time->set_epoch_time(value); - } - return true; - default: - return false; - } -} -api::APIMessageType GetTimeResponse::message_type() const { return api::APIMessageType::GET_TIME_RESPONSE; } - } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 43937c6f13..8ab09d1185 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -17,11 +17,5 @@ class HomeassistantTime : public time::RealTimeClock { extern HomeassistantTime *global_homeassistant_time; -class GetTimeResponse : public api::APIMessage { - public: - bool decode_32bit(uint32_t field_id, uint32_t value) override; - api::APIMessageType message_type() const override; -}; - } // namespace homeassistant } // namespace esphome diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index efa9e7b264..1c808a2501 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -1,5 +1,6 @@ #include "hx711.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace hx711 { @@ -10,6 +11,7 @@ void HX711Sensor::setup() { ESP_LOGCONFIG(TAG, "Setting up HX711 '%s'...", this->name_.c_str()); this->sck_pin_->setup(); this->dout_pin_->setup(); + this->sck_pin_->digital_write(false); // Read sensor once without publishing to set the gain this->read_sensor_(nullptr); @@ -25,8 +27,9 @@ float HX711Sensor::get_setup_priority() const { return setup_priority::DATA; } void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { - ESP_LOGD(TAG, "'%s': Got value %u", this->name_.c_str(), result); - this->publish_state(result); + int32_t value = static_cast(result); + ESP_LOGD(TAG, "'%s': Got value %d", this->name_.c_str(), value); + this->publish_state(value); } } bool HX711Sensor::read_sensor_(uint32_t *result) { @@ -39,10 +42,11 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { this->status_clear_warning(); uint32_t data = 0; + disable_interrupts(); for (uint8_t i = 0; i < 24; i++) { this->sck_pin_->digital_write(true); delayMicroseconds(1); - data |= uint32_t(this->dout_pin_->digital_read()) << (24 - i); + data |= uint32_t(this->dout_pin_->digital_read()) << (23 - i); this->sck_pin_->digital_write(false); delayMicroseconds(1); } @@ -50,10 +54,16 @@ bool HX711Sensor::read_sensor_(uint32_t *result) { // Cycle clock pin for gain setting for (uint8_t i = 0; i < this->gain_; i++) { this->sck_pin_->digital_write(true); + delayMicroseconds(1); this->sck_pin_->digital_write(false); + delayMicroseconds(1); + } + enable_interrupts(); + + if (data & 0x800000ULL) { + data |= 0xFF000000ULL; } - data ^= 0x800000; if (result != nullptr) *result = data; return true; diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 834fb1334a..840944748c 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -32,7 +32,7 @@ void I2CComponent::dump_config() { if (this->scan_) { ESP_LOGI(TAG, "Scanning i2c bus for active devices..."); uint8_t found = 0; - for (uint8_t address = 8; address < 120; address++) { + for (uint8_t address = 1; address < 120; address++) { this->wire_->beginTransmission(address); uint8_t error = this->wire_->endTransmission(); @@ -50,7 +50,7 @@ void I2CComponent::dump_config() { } } } -float I2CComponent::get_setup_priority() const { return setup_priority::HARDWARE; } +float I2CComponent::get_setup_priority() const { return setup_priority::BUS; } void I2CComponent::raw_begin_transmission(uint8_t address) { ESP_LOGVV(TAG, "Beginning Transmission to 0x%02X:", address); @@ -135,6 +135,9 @@ bool I2CComponent::read_bytes(uint8_t address, uint8_t a_register, uint8_t *data delay(conversion); return this->raw_receive(address, data, len); } +bool I2CComponent::read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len) { + return this->raw_receive(address, data, len); +} bool I2CComponent::read_bytes_16(uint8_t address, uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(address, a_register, nullptr, 0)) @@ -156,6 +159,11 @@ bool I2CComponent::write_bytes(uint8_t address, uint8_t a_register, const uint8_ this->raw_write(address, data, len); return this->raw_end_transmission(address); } +bool I2CComponent::write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len) { + this->raw_begin_transmission(address); + this->raw_write(address, data, len); + return this->raw_end_transmission(address); +} bool I2CComponent::write_bytes_16(uint8_t address, uint8_t a_register, const uint16_t *data, uint8_t len) { this->raw_begin_transmission(address); this->raw_write(address, &a_register, 1); diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index e41bd6c5e8..67cd0373d3 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -42,6 +42,7 @@ class I2CComponent : public Component { * @return If the operation was successful. */ bool read_bytes(uint8_t address, uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); + bool read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len); /** Read len amount of 16-bit words (MSB first) from a register into data. * @@ -69,6 +70,7 @@ class I2CComponent : public Component { * @return If the operation was successful. */ bool write_bytes(uint8_t address, uint8_t a_register, const uint8_t *data, uint8_t len); + bool write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len); /** Write len amount of 16-bit words (MSB first) to the specified register for address. * @@ -151,7 +153,6 @@ class I2CDevice { /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); - protected: /** Read len amount of bytes from a register into data. Optionally with a conversion time after * writing the register value to the bus. * @@ -161,15 +162,23 @@ class I2CDevice { * @param conversion The time in ms between writing the register value and reading out the value. * @return If the operation was successful. */ - bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); + bool read_bytes_raw(uint8_t *data, uint8_t len) { return this->parent_->read_bytes_raw(this->address_, data, len); } - template optional> read_bytes(uint8_t a_register) { // NOLINT + template optional> read_bytes(uint8_t a_register) { std::array res; if (!this->read_bytes(a_register, res.data(), N)) { return {}; } return res; } + template optional> read_bytes_raw() { + std::array res; + if (!this->read_bytes_raw(res.data(), N)) { + return {}; + } + return res; + } /** Read len amount of 16-bit words (MSB first) from a register into data. * @@ -179,12 +188,12 @@ class I2CDevice { * @param conversion The time in ms between writing the register value and reading out the value. * @return If the operation was successful. */ - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); /// Read a single byte from a register into the data variable. Return true if successful. - bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); // NOLINT + bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); - optional read_byte(uint8_t a_register) { // NOLINT + optional read_byte(uint8_t a_register) { uint8_t data; if (!this->read_byte(a_register, &data)) return {}; @@ -192,7 +201,7 @@ class I2CDevice { } /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful. - bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); // NOLINT + bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); /** Write len amount of 8-bit bytes to the specified register. * @@ -201,7 +210,10 @@ class I2CDevice { * @param len The amount of bytes to write to the bus. * @return If the operation was successful. */ - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); // NOLINT + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); + bool write_bytes_raw(const uint8_t *data, uint8_t len) { + return this->parent_->write_bytes_raw(this->address_, data, len); + } /** Write a vector of data to a register. * @@ -209,13 +221,17 @@ class I2CDevice { * @param data The data to write. * @return If the operation was successful. */ - bool write_bytes(uint8_t a_register, const std::vector &data) { // NOLINT + bool write_bytes(uint8_t a_register, const std::vector &data) { return this->write_bytes(a_register, data.data(), data.size()); } + bool write_bytes_raw(const std::vector &data) { return this->write_bytes_raw(data.data(), data.size()); } - template bool write_bytes(uint8_t a_register, const std::array &data) { // NOLINT + template bool write_bytes(uint8_t a_register, const std::array &data) { return this->write_bytes(a_register, data.data(), data.size()); } + template bool write_bytes_raw(const std::array &data) { + return this->write_bytes_raw(data.data(), data.size()); + } /** Write len amount of 16-bit words (MSB first) to the specified register. * @@ -224,14 +240,15 @@ class I2CDevice { * @param len The amount of bytes to write to the bus. * @return If the operation was successful. */ - bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); // NOLINT + bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); /// Write a single byte of data into the specified register. Return true if successful. - bool write_byte(uint8_t a_register, uint8_t data); // NOLINT + bool write_byte(uint8_t a_register, uint8_t data); /// Write a single 16-bit word of data into the specified register. Return true if successful. - bool write_byte_16(uint8_t a_register, uint16_t data); // NOLINT + bool write_byte_16(uint8_t a_register, uint16_t data); + protected: uint8_t address_{0x00}; I2CComponent *parent_{nullptr}; }; diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 9ddfd2ad0b..f9b5a43870 100644 --- a/esphome/components/integration/integration_sensor.cpp +++ b/esphome/components/integration/integration_sensor.cpp @@ -45,14 +45,14 @@ std::string IntegrationSensor::unit_of_measurement() { } void IntegrationSensor::process_sensor_value_(float value) { const uint32_t now = millis(); - const float old_value = this->last_value_; - const float new_value = value; + const double old_value = this->last_value_; + const double new_value = value; const uint32_t dt_ms = now - this->last_update_; - const float dt = dt_ms * this->get_time_factor_(); - float area = 0.0f; + const double dt = dt_ms * this->get_time_factor_(); + double area = 0.0f; switch (this->method_) { case INTEGRATION_METHOD_TRAPEZOID: - area = dt * (old_value + new_value) / 2.0f; + area = dt * (old_value + new_value) / 2.0; break; case INTEGRATION_METHOD_LEFT: area = dt * old_value; @@ -61,7 +61,9 @@ void IntegrationSensor::process_sensor_value_(float value) { area = dt * new_value; break; } - this->publish_and_save_(this->last_value_ + area); + this->last_value_ = new_value; + this->last_update_ = now; + this->publish_and_save_(this->result_ + area); } } // namespace integration diff --git a/esphome/components/integration/integration_sensor.h b/esphome/components/integration/integration_sensor.h index 6b1f4ccf1b..2fcec069b2 100644 --- a/esphome/components/integration/integration_sensor.h +++ b/esphome/components/integration/integration_sensor.h @@ -51,10 +51,11 @@ class IntegrationSensor : public sensor::Sensor, public Component { return 0.0f; } } - void publish_and_save_(float result) { + void publish_and_save_(double result) { this->result_ = result; this->publish_state(result); - this->rtc_.save(&result); + float result_f = result; + this->rtc_.save(&result_f); } std::string unit_of_measurement() override; std::string icon() override { return this->sensor_->get_icon(); } @@ -67,7 +68,7 @@ class IntegrationSensor : public sensor::Sensor, public Component { ESPPreferenceObject rtc_; uint32_t last_update_; - float result_{0.0f}; + double result_{0.0f}; float last_value_{0.0f}; }; diff --git a/esphome/components/integration/sensor.py b/esphome/components/integration/sensor.py index 9e4dc0847f..a354ab7433 100644 --- a/esphome/components/integration/sensor.py +++ b/esphome/components/integration/sensor.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import sensor -from esphome.const import CONF_ID, CONF_TIME_ID, CONF_SENSOR, CONF_RESTORE +from esphome.const import CONF_ID, CONF_SENSOR, CONF_RESTORE integration_ns = cg.esphome_ns.namespace('integration') IntegrationSensor = integration_ns.class_('IntegrationSensor', sensor.Sensor, cg.Component) @@ -26,7 +26,6 @@ INTEGRATION_METHODS = { CONF_TIME_UNIT = 'time_unit' CONF_INTEGRATION_METHOD = 'integration_method' - CONFIG_SCHEMA = sensor.SENSOR_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(IntegrationSensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), @@ -45,7 +44,7 @@ def to_code(config): sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) - cg.add(var.set_time(config[CONF_TIME_ID])) + cg.add(var.set_time(config[CONF_TIME_UNIT])) cg.add(var.set_method(config[CONF_INTEGRATION_METHOD])) cg.add(var.set_restore(config[CONF_RESTORE])) diff --git a/esphome/components/lcd_base/__init__.py b/esphome/components/lcd_base/__init__.py index 27f65f9336..bff194578c 100644 --- a/esphome/components/lcd_base/__init__.py +++ b/esphome/components/lcd_base/__init__.py @@ -1,12 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import display -from esphome.const import CONF_DIMENSIONS, CONF_LAMBDA +from esphome.const import CONF_DIMENSIONS from esphome.core import coroutine lcd_base_ns = cg.esphome_ns.namespace('lcd_base') LCDDisplay = lcd_base_ns.class_('LCDDisplay', cg.PollingComponent) -LCDDisplayRef = LCDDisplay.operator('ref') def validate_lcd_dimensions(value): @@ -28,8 +27,3 @@ def setup_lcd_display(var, config): yield cg.register_component(var, config) yield display.register_display(var, config) cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1])) - - if CONF_LAMBDA in config: - lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], [(LCDDisplayRef, 'it')], - return_type=cg.void) - cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_base/lcd_display.cpp b/esphome/components/lcd_base/lcd_display.cpp index af6b8304eb..51541049b1 100644 --- a/esphome/components/lcd_base/lcd_display.cpp +++ b/esphome/components/lcd_base/lcd_display.cpp @@ -107,7 +107,7 @@ void LCDDisplay::update() { for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) this->buffer_[i] = ' '; - this->writer_(*this); + this->call_writer(); this->display(); } void LCDDisplay::command_(uint8_t value) { this->send(value, false); } diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index 200600eb9c..791f31ace3 100644 --- a/esphome/components/lcd_base/lcd_display.h +++ b/esphome/components/lcd_base/lcd_display.h @@ -12,11 +12,8 @@ namespace lcd_base { class LCDDisplay; -using lcd_writer_t = std::function; - class LCDDisplay : public PollingComponent { public: - void set_writer(lcd_writer_t &&writer) { this->writer_ = std::move(writer); } void set_dimensions(uint8_t columns, uint8_t rows) { this->columns_ = columns; this->rows_ = rows; @@ -54,11 +51,11 @@ class LCDDisplay : public PollingComponent { virtual void send(uint8_t value, bool rs) = 0; void command_(uint8_t value); + virtual void call_writer() = 0; uint8_t columns_; uint8_t rows_; uint8_t *buffer_{nullptr}; - lcd_writer_t writer_; }; } // namespace lcd_base diff --git a/esphome/components/lcd_gpio/display.py b/esphome/components/lcd_gpio/display.py index 1f98955ece..91498d59c9 100644 --- a/esphome/components/lcd_gpio/display.py +++ b/esphome/components/lcd_gpio/display.py @@ -2,7 +2,8 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.components import lcd_base -from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID +from esphome.const import CONF_DATA_PINS, CONF_ENABLE_PIN, CONF_RS_PIN, CONF_RW_PIN, CONF_ID, \ + CONF_LAMBDA AUTO_LOAD = ['lcd_base'] @@ -42,3 +43,9 @@ def to_code(config): if CONF_RW_PIN in config: rw = yield cg.gpio_pin_expression(config[CONF_RW_PIN]) cg.add(var.set_rw_pin(rw)) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], + [(GPIOLCDDisplay.operator('ref'), 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_gpio/gpio_lcd_display.h b/esphome/components/lcd_gpio/gpio_lcd_display.h index ed3b0c1137..01f6f95d9a 100644 --- a/esphome/components/lcd_gpio/gpio_lcd_display.h +++ b/esphome/components/lcd_gpio/gpio_lcd_display.h @@ -8,6 +8,7 @@ namespace lcd_gpio { class GPIOLCDDisplay : public lcd_base::LCDDisplay { public: + void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } void setup() override; void set_data_pins(GPIOPin *d0, GPIOPin *d1, GPIOPin *d2, GPIOPin *d3) { this->data_pins_[0] = d0; @@ -36,10 +37,13 @@ class GPIOLCDDisplay : public lcd_base::LCDDisplay { void write_n_bits(uint8_t value, uint8_t n) override; void send(uint8_t value, bool rs) override; + void call_writer() override { this->writer_(*this); } + GPIOPin *rs_pin_{nullptr}; GPIOPin *rw_pin_{nullptr}; GPIOPin *enable_pin_{nullptr}; GPIOPin *data_pins_[8]{nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + std::function writer_; }; } // namespace lcd_gpio diff --git a/esphome/components/lcd_pcf8574/display.py b/esphome/components/lcd_pcf8574/display.py index 2bc04a283f..2bbb3a2f7b 100644 --- a/esphome/components/lcd_pcf8574/display.py +++ b/esphome/components/lcd_pcf8574/display.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import lcd_base, i2c -from esphome.const import CONF_ID +from esphome.const import CONF_ID, CONF_LAMBDA DEPENDENCIES = ['i2c'] AUTO_LOAD = ['lcd_base'] @@ -18,3 +18,9 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield lcd_base.setup_lcd_display(var, config) yield i2c.register_i2c_device(var, config) + + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], + [(PCF8574LCDDisplay.operator('ref'), 'it')], + return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.cpp b/esphome/components/lcd_pcf8574/pcf8574_display.cpp index 59491c7d5a..e3002da25d 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.cpp +++ b/esphome/components/lcd_pcf8574/pcf8574_display.cpp @@ -6,9 +6,13 @@ namespace lcd_pcf8574 { static const char *TAG = "lcd_pcf8574"; +static const uint8_t LCD_DISPLAY_BACKLIGHT_ON = 0x08; +static const uint8_t LCD_DISPLAY_BACKLIGHT_OFF = 0x00; + void PCF8574LCDDisplay::setup() { ESP_LOGCONFIG(TAG, "Setting up PCF8574 LCD Display..."); - if (!this->write_bytes(0x08, nullptr, 0)) { + this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON; + if (!this->write_bytes(this->backlight_value_, nullptr, 0)) { this->mark_failed(); return; } @@ -29,7 +33,7 @@ void PCF8574LCDDisplay::write_n_bits(uint8_t value, uint8_t n) { // Ugly fix: in the super setup() with n == 4 value needs to be shifted left value <<= 4; } - uint8_t data = value | 0x08; // Enable backlight + uint8_t data = value | this->backlight_value_; // Set backlight state this->write_bytes(data, nullptr, 0); // Pulse ENABLE this->write_bytes(data | 0x04, nullptr, 0); @@ -41,6 +45,14 @@ void PCF8574LCDDisplay::send(uint8_t value, bool rs) { this->write_n_bits((value & 0xF0) | rs, 0); this->write_n_bits(((value << 4) & 0xF0) | rs, 0); } +void PCF8574LCDDisplay::backlight() { + this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_ON; + this->write_bytes(this->backlight_value_, nullptr, 0); +} +void PCF8574LCDDisplay::no_backlight() { + this->backlight_value_ = LCD_DISPLAY_BACKLIGHT_OFF; + this->write_bytes(this->backlight_value_, nullptr, 0); +} } // namespace lcd_pcf8574 } // namespace esphome diff --git a/esphome/components/lcd_pcf8574/pcf8574_display.h b/esphome/components/lcd_pcf8574/pcf8574_display.h index 133679c501..4db3afb9b0 100644 --- a/esphome/components/lcd_pcf8574/pcf8574_display.h +++ b/esphome/components/lcd_pcf8574/pcf8574_display.h @@ -9,13 +9,22 @@ namespace lcd_pcf8574 { class PCF8574LCDDisplay : public lcd_base::LCDDisplay, public i2c::I2CDevice { public: + void set_writer(std::function &&writer) { this->writer_ = std::move(writer); } void setup() override; void dump_config() override; + void backlight(); + void no_backlight(); protected: bool is_four_bit_mode() override { return true; } void write_n_bits(uint8_t value, uint8_t n) override; void send(uint8_t value, bool rs) override; + + void call_writer() override { this->writer_(*this); } + + // Stores the current state of the backlight. + uint8_t backlight_value_; + std::function writer_; }; } // namespace lcd_pcf8574 diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 64094478c0..b3652c84d6 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -11,10 +11,10 @@ namespace ledc { static const char *TAG = "ledc.output"; void LEDCOutput::write_state(float state) { - if (this->pin_->is_inverted()) { + if (this->pin_->is_inverted()) state = 1.0f - state; - } + this->duty_ = state; const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); @@ -22,18 +22,45 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - ledcSetup(this->channel_, this->frequency_, this->bit_depth_); - ledcAttachPin(this->pin_->get_pin(), this->channel_); - + this->apply_frequency(this->frequency_); this->turn_off(); + // Attach pin after setting default value + ledcAttachPin(this->pin_->get_pin(), this->channel_); } void LEDCOutput::dump_config() { ESP_LOGCONFIG(TAG, "LEDC Output:"); - LOG_PIN(" Pin", this->pin_); + LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); - ESP_LOGCONFIG(TAG, " Bit Depth: %u", this->bit_depth_); +} + +float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); } +float ledc_min_frequency_for_bit_depth(uint8_t bit_depth) { + const float max_div_num = ((1 << 20) - 1) / 256.0f; + return 80e6f / (max_div_num * float(1 << bit_depth)); +} +optional ledc_bit_depth_for_frequency(float frequency) { + for (int i = 20; i >= 1; i--) { + const float min_frequency = ledc_min_frequency_for_bit_depth(frequency); + const float max_frequency = ledc_max_frequency_for_bit_depth(frequency); + if (min_frequency <= frequency && frequency <= max_frequency) + return i; + } + return {}; +} + +void LEDCOutput::apply_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); + this->status_set_warning(); + } + this->bit_depth_ = *bit_depth_opt; + this->frequency_ = frequency; + ledcSetup(this->channel_, frequency, this->bit_depth_); + // re-apply duty + this->write_state(this->duty_); } uint8_t next_ledc_channel = 0; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index d1b9b099ee..3f56f502b0 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" #ifdef ARDUINO_ARCH_ESP32 @@ -16,11 +17,10 @@ class LEDCOutput : public output::FloatOutput, public Component { explicit LEDCOutput(GPIOPin *pin) : pin_(pin) { this->channel_ = next_ledc_channel++; } void set_channel(uint8_t channel) { this->channel_ = channel; } - void set_bit_depth(uint8_t bit_depth) { this->bit_depth_ = bit_depth; } void set_frequency(float frequency) { this->frequency_ = frequency; } + /// Dynamically change frequency at runtime + void apply_frequency(float frequency); - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) /// Setup LEDC. void setup() override; void dump_config() override; @@ -28,13 +28,28 @@ class LEDCOutput : public output::FloatOutput, public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } /// Override FloatOutput's write_state. - void write_state(float adjusted_value) override; + void write_state(float state) override; protected: GPIOPin *pin_; uint8_t channel_{}; uint8_t bit_depth_{}; float frequency_{}; + float duty_{0.0f}; +}; + +template class SetFrequencyAction : public Action { + public: + SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, frequency); + + void play(Ts... x) { + float freq = this->frequency_.value(x...); + this->parent_->apply_frequency(freq); + } + + protected: + LEDCOutput *parent_; }; } // namespace ledc diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index c507465ff9..b608e9bbf7 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -1,6 +1,4 @@ -import math - -from esphome import pins +from esphome import pins, automation from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg @@ -15,53 +13,36 @@ def calc_max_frequency(bit_depth): def calc_min_frequency(bit_depth): - # LEDC_DIV_NUM_HSTIMER is 15-bit unsigned integer - # lower 8 bits represent fractional part - max_div_num = ((1 << 16) - 1) / 256.0 + max_div_num = ((2**20) - 1) / 256.0 return 80e6 / (max_div_num * (2**bit_depth)) -def validate_frequency_bit_depth(obj): - frequency = obj[CONF_FREQUENCY] - if CONF_BIT_DEPTH not in obj: - obj = obj.copy() - for bit_depth in range(15, 0, -1): - if calc_min_frequency(bit_depth) <= frequency <= calc_max_frequency(bit_depth): - obj[CONF_BIT_DEPTH] = bit_depth - break - else: - min_freq = min(calc_min_frequency(x) for x in range(1, 16)) - max_freq = max(calc_max_frequency(x) for x in range(1, 16)) - if frequency < min_freq: - raise cv.Invalid("This frequency setting is not possible, please choose a higher " - "frequency (at least {}Hz)".format(int(min_freq))) - if frequency > max_freq: - raise cv.Invalid("This frequency setting is not possible, please choose a lower " - "frequency (at most {}Hz)".format(int(max_freq))) - raise cv.Invalid("Invalid frequency!") - - bit_depth = obj[CONF_BIT_DEPTH] - min_freq = calc_min_frequency(bit_depth) - max_freq = calc_max_frequency(bit_depth) - if frequency > max_freq: - raise cv.Invalid('Maximum frequency for bit depth {} is {}Hz. Please decrease the ' - 'bit_depth.'.format(bit_depth, int(math.floor(max_freq)))) - if frequency < calc_min_frequency(bit_depth): - raise cv.Invalid('Minimum frequency for bit depth {} is {}Hz. Please increase the ' - 'bit_depth.'.format(bit_depth, int(math.ceil(min_freq)))) - return obj +def validate_frequency(value): + value = cv.frequency(value) + min_freq = calc_min_frequency(20) + max_freq = calc_max_frequency(1) + if value < min_freq: + raise cv.Invalid("This frequency setting is not possible, please choose a higher " + "frequency (at least {}Hz)".format(int(min_freq))) + if value > max_freq: + raise cv.Invalid("This frequency setting is not possible, please choose a lower " + "frequency (at most {}Hz)".format(int(max_freq))) + return value ledc_ns = cg.esphome_ns.namespace('ledc') LEDCOutput = ledc_ns.class_('LEDCOutput', output.FloatOutput, cg.Component) +SetFrequencyAction = ledc_ns.class_('SetFrequencyAction', automation.Action) -CONFIG_SCHEMA = cv.All(output.FLOAT_OUTPUT_SCHEMA.extend({ +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_FREQUENCY, default='1kHz'): cv.frequency, - cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=1, max=15), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), -}).extend(cv.COMPONENT_SCHEMA), validate_frequency_bit_depth) + + cv.Optional(CONF_BIT_DEPTH): cv.invalid("The bit_depth option has been removed in v1.14, the " + "best bit depth is now automatically calculated."), +}).extend(cv.COMPONENT_SCHEMA) def to_code(config): @@ -72,4 +53,15 @@ def to_code(config): if CONF_CHANNEL in config: cg.add(var.set_channel(config[CONF_CHANNEL])) cg.add(var.set_frequency(config[CONF_FREQUENCY])) - cg.add(var.set_bit_depth(config[CONF_BIT_DEPTH])) + + +@automation.register_action('output.ledc.set_frequency', SetFrequencyAction, cv.Schema({ + cv.Required(CONF_ID): cv.use_id(LEDCOutput), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), +})) +def ledc_set_frequency_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_FREQUENCY], args, float) + cg.add(var.set_frequency(template_)) + yield var diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 640d8112b1..b5dc70a083 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -4,6 +4,8 @@ namespace esphome { namespace light { +static const char *TAG = "light.addressable"; + const ESPColor ESPColor::BLACK = ESPColor(0, 0, 0, 0); const ESPColor ESPColor::WHITE = ESPColor(255, 255, 255, 255); @@ -80,11 +82,78 @@ void ESPRangeView::set(const ESPColor &color) { } } ESPColorView ESPRangeView::operator[](int32_t index) const { - index = interpret_index(index, this->size()); + index = interpret_index(index, this->size()) + this->begin_; return (*this->parent_)[index]; } +ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; } +ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; } +void ESPRangeView::set_red(uint8_t red) { + for (auto c : *this) + c.set_red(red); +} +void ESPRangeView::set_green(uint8_t green) { + for (auto c : *this) + c.set_green(green); +} +void ESPRangeView::set_blue(uint8_t blue) { + for (auto c : *this) + c.set_blue(blue); +} +void ESPRangeView::set_white(uint8_t white) { + for (auto c : *this) + c.set_white(white); +} +void ESPRangeView::set_effect_data(uint8_t effect_data) { + for (auto c : *this) + c.set_effect_data(effect_data); +} +void ESPRangeView::fade_to_white(uint8_t amnt) { + for (auto c : *this) + c.fade_to_white(amnt); +} +void ESPRangeView::fade_to_black(uint8_t amnt) { + for (auto c : *this) + c.fade_to_black(amnt); +} +void ESPRangeView::lighten(uint8_t delta) { + for (auto c : *this) + c.lighten(delta); +} +void ESPRangeView::darken(uint8_t delta) { + for (auto c : *this) + c.darken(delta); +} +ESPRangeView &ESPRangeView::operator=(const ESPRangeView &rhs) { + // If size doesn't match, error (todo warning) + if (rhs.size() != this->size()) + return *this; -ESPColorView ESPRangeView::Iterator::operator*() const { return (*this->range_->parent_)[this->i_]; } + if (this->parent_ != rhs.parent_) { + for (int32_t i = 0; i < this->size(); i++) + (*this)[i].set(rhs[i].get()); + return *this; + } + + // If both equal, already done + if (rhs.begin_ == this->begin_) + return *this; + + if (rhs.begin_ > this->begin_) { + // Copy from left + for (int32_t i = 0; i < this->size(); i++) { + (*this)[i].set(rhs[i].get()); + } + } else { + // Copy from right + for (int32_t i = this->size() - 1; i >= 0; i--) { + (*this)[i].set(rhs[i].get()); + } + } + + return *this; +} + +ESPColorView ESPRangeIterator::operator*() const { return this->range_.parent_->get(this->i_); } int32_t HOT interpret_index(int32_t index, int32_t size) { if (index < 0) @@ -92,5 +161,119 @@ int32_t HOT interpret_index(int32_t index, int32_t size) { return index; } +void AddressableLight::call_setup() { + this->setup(); + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + this->set_interval(5000, [this]() { + const char *name = this->state_parent_ == nullptr ? "" : this->state_parent_->get_name().c_str(); + ESP_LOGVV(TAG, "Addressable Light '%s' (effect_active=%s next_show=%s)", name, YESNO(this->effect_active_), + YESNO(this->next_show_)); + for (int i = 0; i < this->size(); i++) { + auto color = this->get(i); + ESP_LOGVV(TAG, " [%2d] Color: R=%3u G=%3u B=%3u W=%3u", i, color.get_red_raw(), color.get_green_raw(), + color.get_blue_raw(), color.get_white_raw()); + } + ESP_LOGVV(TAG, ""); + }); +#endif +} + +ESPColor esp_color_from_light_color_values(LightColorValues val) { + auto r = static_cast(roundf(val.get_red() * 255.0f)); + auto g = static_cast(roundf(val.get_green() * 255.0f)); + auto b = static_cast(roundf(val.get_blue() * 255.0f)); + auto w = static_cast(roundf(val.get_white() * val.get_state() * 255.0f)); + return ESPColor(r, g, b, w); +} + +void AddressableLight::write_state(LightState *state) { + auto val = state->current_values; + auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); + this->correction_.set_local_brightness(max_brightness); + + this->last_transition_progress_ = 0.0f; + this->accumulated_alpha_ = 0.0f; + + if (this->is_effect_active()) + return; + + // don't use LightState helper, gamma correction+brightness is handled by ESPColorView + + if (state->transformer_ == nullptr || !state->transformer_->is_transition()) { + // no transformer active or non-transition one + this->all() = esp_color_from_light_color_values(val); + } else { + // transition transformer active, activate specialized transition for addressable effects + // instead of using a unified transition for all LEDs, we use the current state each LED as the + // start. Warning: ugly + + // We can't use a direct lerp smoothing here though - that would require creating a copy of the original + // state of each LED at the start of the transition + // Instead, we "fake" the look of the LERP by using an exponential average over time and using + // dynamically-calculated alpha values to match the look of the + + float new_progress = state->transformer_->get_progress(); + float prev_smoothed = LightTransitionTransformer::smoothed_progress(last_transition_progress_); + float new_smoothed = LightTransitionTransformer::smoothed_progress(new_progress); + this->last_transition_progress_ = new_progress; + + auto end_values = state->transformer_->get_end_values(); + ESPColor target_color = esp_color_from_light_color_values(end_values); + + // our transition will handle brightness, disable brightness in correction. + this->correction_.set_local_brightness(255); + uint8_t orig_w = target_color.w; + target_color *= static_cast(roundf(end_values.get_brightness() * end_values.get_state() * 255.0f)); + // w is not scaled by brightness + target_color.w = orig_w; + + float denom = (1.0f - new_smoothed); + float alpha = denom == 0.0f ? 0.0f : (new_smoothed - prev_smoothed) / denom; + + // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length + // We solve this by accumulating the fractional part of the alpha over time. + float alpha255 = alpha * 255.0f; + float alpha255int = floorf(alpha255); + float alpha255remainder = alpha255 - alpha255int; + + this->accumulated_alpha_ += alpha255remainder; + float alpha_add = floorf(this->accumulated_alpha_); + this->accumulated_alpha_ -= alpha_add; + + alpha255 += alpha_add; + alpha255 = clamp(alpha255, 0.0f, 255.0f); + auto alpha8 = static_cast(alpha255); + + if (alpha8 != 0) { + uint8_t inv_alpha8 = 255 - alpha8; + ESPColor add = target_color * alpha8; + + for (auto led : *this) + led = add + led.get() * inv_alpha8; + } + } + + this->schedule_show(); +} + +void ESPColorCorrection::calculate_gamma_table(float gamma) { + for (uint16_t i = 0; i < 256; i++) { + // corrected = val ^ gamma + auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); + this->gamma_table_[i] = corrected; + } + if (gamma == 0.0f) { + for (uint16_t i = 0; i < 256; i++) + this->gamma_reverse_table_[i] = i; + return; + } + for (uint16_t i = 0; i < 256; i++) { + // val = corrected ^ (1/gamma) + auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); + this->gamma_reverse_table_[i] = uncorrected; + } +} + } // namespace light } // namespace esphome diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index b4249097db..a95d70f274 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -189,23 +189,7 @@ class ESPColorCorrection { ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} void set_max_brightness(const ESPColor &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = static_cast(roundf(255.0f * gamma_correct(i / 255.0f, gamma))); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = static_cast(roundf(255.0f * powf(i / 255.0f, 1.0f / gamma))); - this->gamma_reverse_table_[i] = uncorrected; - } - } + void calculate_gamma_table(float gamma); inline ESPColor color_correct(ESPColor color) const ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return ESPColor(this->color_correct_red(color.red), this->color_correct_green(color.green), @@ -335,13 +319,21 @@ class ESPColorView : public ESPColorSettable { void darken(uint8_t delta) override { this->set(this->get().darken(delta)); } ESPColor get() const { return ESPColor(this->get_red(), this->get_green(), this->get_blue(), this->get_white()); } uint8_t get_red() const { return this->color_correction_->color_uncorrect_red(*this->red_); } + uint8_t get_red_raw() const { return *this->red_; } uint8_t get_green() const { return this->color_correction_->color_uncorrect_green(*this->green_); } + uint8_t get_green_raw() const { return *this->green_; } uint8_t get_blue() const { return this->color_correction_->color_uncorrect_blue(*this->blue_); } + uint8_t get_blue_raw() const { return *this->blue_; } uint8_t get_white() const { if (this->white_ == nullptr) return 0; return this->color_correction_->color_uncorrect_white(*this->white_); } + uint8_t get_white_raw() const { + if (this->white_ == nullptr) + return 0; + return *this->white_; + } uint8_t get_effect_data() const { if (this->effect_data_ == nullptr) return 0; @@ -364,23 +356,10 @@ class AddressableLight; int32_t interpret_index(int32_t index, int32_t size); +class ESPRangeIterator; + class ESPRangeView : public ESPColorSettable { public: - class Iterator { - public: - Iterator(ESPRangeView *range, int32_t i) : range_(range), i_(i) {} - Iterator operator++() { - this->i_++; - return *this; - } - bool operator!=(const Iterator &other) const { return this->i_ != other.i_; } - ESPColorView operator*() const; - - protected: - ESPRangeView *range_; - int32_t i_; - }; - ESPRangeView(AddressableLight *parent, int32_t begin, int32_t an_end) : parent_(parent), begin_(begin), end_(an_end) { if (this->end_ < this->begin_) { this->end_ = this->begin_; @@ -388,8 +367,8 @@ class ESPRangeView : public ESPColorSettable { } ESPColorView operator[](int32_t index) const; - Iterator begin() { return {this, this->begin_}; } - Iterator end() { return {this, this->end_}; } + ESPRangeIterator begin(); + ESPRangeIterator end(); void set(const ESPColor &color) override; ESPRangeView &operator=(const ESPColor &rhs) { @@ -404,78 +383,42 @@ class ESPRangeView : public ESPColorSettable { this->set_hsv(rhs); return *this; } - ESPRangeView &operator=(const ESPRangeView &rhs) { - // If size doesn't match, error (todo warning) - if (rhs.size() != this->size()) - return *this; - - if (this->parent_ != rhs.parent_) { - for (int32_t i = 0; i < this->size(); i++) - (*this)[i].set(rhs[i].get()); - return *this; - } - - // If both equal, already done - if (rhs.begin_ == this->begin_) - return *this; - - if (rhs.begin_ < this->begin_) { - // Copy into rhs - for (int32_t i = 0; i < this->size(); i++) - rhs[i].set((*this)[i].get()); - } else { - // Copy into this - for (int32_t i = 0; i < this->size(); i++) - (*this)[i].set(rhs[i].get()); - } - - return *this; - } - void set_red(uint8_t red) override { - for (auto c : *this) - c.set_red(red); - } - void set_green(uint8_t green) override { - for (auto c : *this) - c.set_green(green); - } - void set_blue(uint8_t blue) override { - for (auto c : *this) - c.set_blue(blue); - } - void set_white(uint8_t white) override { - for (auto c : *this) - c.set_white(white); - } - void set_effect_data(uint8_t effect_data) override { - for (auto c : *this) - c.set_effect_data(effect_data); - } - void fade_to_white(uint8_t amnt) override { - for (auto c : *this) - c.fade_to_white(amnt); - } - void fade_to_black(uint8_t amnt) override { - for (auto c : *this) - c.fade_to_white(amnt); - } - void lighten(uint8_t delta) override { - for (auto c : *this) - c.lighten(delta); - } - void darken(uint8_t delta) override { - for (auto c : *this) - c.darken(delta); - } + ESPRangeView &operator=(const ESPRangeView &rhs); + void set_red(uint8_t red) override; + void set_green(uint8_t green) override; + void set_blue(uint8_t blue) override; + void set_white(uint8_t white) override; + void set_effect_data(uint8_t effect_data) override; + void fade_to_white(uint8_t amnt) override; + void fade_to_black(uint8_t amnt) override; + void lighten(uint8_t delta) override; + void darken(uint8_t delta) override; int32_t size() const { return this->end_ - this->begin_; } protected: + friend ESPRangeIterator; + AddressableLight *parent_; int32_t begin_; int32_t end_; }; -class AddressableLight : public LightOutput { +class ESPRangeIterator { + public: + ESPRangeIterator(const ESPRangeView &range, int32_t i) : range_(range), i_(i) {} + ESPRangeIterator operator++() { + this->i_++; + return *this; + } + bool operator!=(const ESPRangeIterator &other) const { return this->i_ != other.i_; } + ESPColorView operator*() const; + + protected: + ESPRangeView range_; + int32_t i_; +}; + +class AddressableLight : public LightOutput, public Component { public: virtual int32_t size() const = 0; ESPColorView operator[](int32_t index) const { return this->get_view_internal(interpret_index(index, this->size())); } @@ -487,8 +430,8 @@ class AddressableLight : public LightOutput { return ESPRangeView(this, from, to); } ESPRangeView all() { return ESPRangeView(this, 0, this->size()); } - ESPRangeView::Iterator begin() { return this->all().begin(); } - ESPRangeView::Iterator end() { return this->all().end(); } + ESPRangeIterator begin() { return this->all().begin(); } + ESPRangeIterator end() { return this->all().end(); } void shift_left(int32_t amnt) { if (amnt < 0) { this->shift_right(-amnt); @@ -509,34 +452,23 @@ class AddressableLight : public LightOutput { } bool is_effect_active() const { return this->effect_active_; } void set_effect_active(bool effect_active) { this->effect_active_ = effect_active; } - void write_state(LightState *state) override { - auto val = state->current_values; - auto max_brightness = static_cast(roundf(val.get_brightness() * val.get_state() * 255.0f)); - this->correction_.set_local_brightness(max_brightness); - - if (this->is_effect_active()) - return; - - // don't use LightState helper, gamma correction+brightness is handled by ESPColorView - ESPColor color = ESPColor(uint8_t(roundf(val.get_red() * 255.0f)), uint8_t(roundf(val.get_green() * 255.0f)), - uint8_t(roundf(val.get_blue() * 255.0f)), - // white is not affected by brightness; so manually scale by state - uint8_t(roundf(val.get_white() * val.get_state() * 255.0f))); - - this->all() = color; - this->schedule_show(); - } + void write_state(LightState *state) override; void set_correction(float red, float green, float blue, float white = 1.0f) { this->correction_.set_max_brightness(ESPColor(uint8_t(roundf(red * 255.0f)), uint8_t(roundf(green * 255.0f)), uint8_t(roundf(blue * 255.0f)), uint8_t(roundf(white * 255.0f)))); } - void setup_state(LightState *state) override { this->correction_.calculate_gamma_table(state->get_gamma_correct()); } + void setup_state(LightState *state) override { + this->correction_.calculate_gamma_table(state->get_gamma_correct()); + this->state_parent_ = state; + } void schedule_show() { this->next_show_ = true; } #ifdef USE_POWER_SUPPLY void set_power_supply(power_supply::PowerSupply *power_supply) { this->power_.set_parent(power_supply); } #endif + void call_setup() override; + protected: bool should_show_() const { return this->effect_active_ || this->next_show_; } void mark_shown_() { @@ -559,6 +491,9 @@ class AddressableLight : public LightOutput { #ifdef USE_POWER_SUPPLY power_supply::PowerSupplyRequester power_; #endif + LightState *state_parent_{nullptr}; + float last_transition_progress_{0.0f}; + float accumulated_alpha_{0.0f}; }; } // namespace light diff --git a/esphome/components/light/addressable_light_effect.h b/esphome/components/light/addressable_light_effect.h index 545af6a0f2..78ae41baad 100644 --- a/esphome/components/light/addressable_light_effect.h +++ b/esphome/components/light/addressable_light_effect.h @@ -50,19 +50,19 @@ class AddressableLightEffect : public LightEffect { class AddressableLambdaLightEffect : public AddressableLightEffect { public: - AddressableLambdaLightEffect(const std::string &name, const std::function &f, + AddressableLambdaLightEffect(const std::string &name, const std::function &f, uint32_t update_interval) : AddressableLightEffect(name), f_(f), update_interval_(update_interval) {} void apply(AddressableLight &it, const ESPColor ¤t_color) override { const uint32_t now = millis(); if (now - this->last_run_ >= this->update_interval_) { this->last_run_ = now; - this->f_(it); + this->f_(it, current_color); } } protected: - std::function f_; + std::function f_; uint32_t update_interval_; uint32_t last_run_{0}; }; @@ -113,7 +113,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { it.shift_right(1); const AddressableColorWipeEffectColor color = this->colors_[this->at_color_]; const ESPColor esp_color = ESPColor(color.r, color.g, color.b, color.w); - if (!this->reverse_) + if (this->reverse_) it[-1] = esp_color; else it[0] = esp_color; @@ -143,14 +143,19 @@ class AddressableScanEffect : public AddressableLightEffect { public: explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } + void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } void apply(AddressableLight &it, const ESPColor ¤t_color) override { it.all() = ESPColor::BLACK; - it[this->at_led_] = current_color; + + for (auto i = 0; i < this->scan_width_; i++) { + it[this->at_led_ + i] = current_color; + } + const uint32_t now = millis(); if (now - this->last_move_ > this->move_interval_) { if (direction_) { this->at_led_++; - if (this->at_led_ == it.size() - 1) + if (this->at_led_ == it.size() - this->scan_width_) this->direction_ = false; } else { this->at_led_--; @@ -163,6 +168,7 @@ class AddressableScanEffect : public AddressableLightEffect { protected: uint32_t move_interval_{}; + uint32_t scan_width_{1}; uint32_t last_move_{0}; int at_led_{0}; bool direction_{true}; @@ -308,14 +314,20 @@ class AddressableFlickerEffect : public AddressableLightEffect { explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} void apply(AddressableLight &it, const ESPColor ¤t_color) override { const uint32_t now = millis(); - const uint8_t delta_intensity = 255 - this->intensity_; + const uint8_t intensity = this->intensity_; + const uint8_t inv_intensity = 255 - intensity; if (now - this->last_update_ < this->update_interval_) return; + this->last_update_ = now; fast_random_set_seed(random_uint32()); for (auto var : it) { - const uint8_t flicker = fast_random_8() % this->intensity_; - var = (var.get() * delta_intensity) + (current_color * flicker); + const uint8_t flicker = fast_random_8() % intensity; + // scale down by random factor + var = var.get() * (255 - flicker); + + // slowly fade back to "real" value + var = (var.get() * inv_intensity) + (current_color * intensity); } } void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } diff --git a/esphome/components/light/automation.h b/esphome/components/light/automation.h index 70c423af0a..2cd55ab6f6 100644 --- a/esphome/components/light/automation.h +++ b/esphome/components/light/automation.h @@ -122,6 +122,7 @@ template class AddressableSet : public Action { range.set_blue(this->blue_.value(x...)); if (this->white_.has_value()) range.set_white(this->white_.value(x...)); + out->schedule_show(); } protected: diff --git a/esphome/components/light/automation.py b/esphome/components/light/automation.py index 41074c098d..9e14246c0f 100644 --- a/esphome/components/light/automation.py +++ b/esphome/components/light/automation.py @@ -155,6 +155,6 @@ def light_addressable_set_to_code(config, action_id, template_arg, args): automation.maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(LightState), })) -def binary_sensor_is_on_to_code(config, condition_id, template_arg, args): +def light_is_on_off_to_code(config, condition_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) yield cg.new_Pvariable(condition_id, template_arg, paren) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 4b33b77073..dcef60397d 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -102,6 +102,7 @@ class StrobeLightEffect : public LightEffect { if (!color.is_on()) { // Don't turn the light off, otherwise the light effect will be stopped call.set_brightness_if_supported(0.0f); + call.set_white_if_supported(0.0f); call.set_state(true); } call.set_publish(false); @@ -129,7 +130,7 @@ class FlickerLightEffect : public LightEffect { LightColorValues out; const float alpha = this->alpha_; const float beta = 1.0f - alpha; - out.set_state(remote.get_state()); + out.set_state(true); out.set_brightness(remote.get_brightness() * beta + current.get_brightness() * alpha + (random_cubic_float() * this->intensity_)); out.set_red(remote.get_red() * beta + current.get_red() * alpha + (random_cubic_float() * this->intensity_)); @@ -144,6 +145,7 @@ class FlickerLightEffect : public LightEffect { if (traits.get_supports_brightness()) call.set_transition_length(0); call.from_light_color_values(out); + call.set_state(true); call.perform(); } diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index a78165fb8a..c2250e7e0c 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -11,11 +11,12 @@ from .types import LambdaLightEffect, RandomLightEffect, StrobeLightEffect, \ FlickerLightEffect, AddressableRainbowLightEffect, AddressableColorWipeEffect, \ AddressableColorWipeEffectColor, AddressableScanEffect, AddressableTwinkleEffect, \ AddressableRandomTwinkleEffect, AddressableFireworksEffect, AddressableFlickerEffect, \ - AutomationLightEffect + AutomationLightEffect, ESPColor CONF_ADD_LED_INTERVAL = 'add_led_interval' CONF_REVERSE = 'reverse' CONF_MOVE_INTERVAL = 'move_interval' +CONF_SCAN_WIDTH = 'scan_width' CONF_TWINKLE_PROBABILITY = 'twinkle_probability' CONF_PROGRESS_INTERVAL = 'progress_interval' CONF_SPARK_PROBABILITY = 'spark_probability' @@ -128,7 +129,7 @@ def flicker_effect_to_code(config, effect_id): cv.Optional(CONF_UPDATE_INTERVAL, default='0ms'): cv.positive_time_period_milliseconds, }) def addressable_lambda_effect_to_code(config, effect_id): - args = [(AddressableLightRef, 'it')] + args = [(AddressableLightRef, 'it'), (ESPColor, 'current_color')] lambda_ = yield cg.process_lambda(config[CONF_LAMBDA], args, return_type=cg.void) var = cg.new_Pvariable(effect_id, config[CONF_NAME], lambda_, config[CONF_UPDATE_INTERVAL]) @@ -179,10 +180,12 @@ def addressable_color_wipe_effect_to_code(config, effect_id): @register_effect('addressable_scan', AddressableScanEffect, "Scan", { cv.Optional(CONF_MOVE_INTERVAL, default='0.1s'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_SCAN_WIDTH, default=1): cv.int_range(min=1), }) def addressable_scan_effect_to_code(config, effect_id): var = cg.new_Pvariable(effect_id, config[CONF_NAME]) cg.add(var.set_move_interval(config[CONF_MOVE_INTERVAL])) + cg.add(var.set_scan_width(config[CONF_SCAN_WIDTH])) yield var diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index cafced27fc..e96d64ad1f 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -24,9 +24,12 @@ void LightState::start_flash_(const LightColorValues &target, uint32_t length) { LightState::LightState(const std::string &name, LightOutput *output) : Nameable(name), output_(output) {} -void LightState::set_immediately_(const LightColorValues &target) { +void LightState::set_immediately_(const LightColorValues &target, bool set_remote_values) { this->transformer_ = nullptr; - this->current_values = this->remote_values = target; + this->current_values = target; + if (set_remote_values) { + this->remote_values = target; + } this->next_write_ = true; } @@ -327,10 +330,10 @@ void LightCall::perform() { // Also set light color values when starting an effect // For example to turn off the light - this->parent_->set_immediately_(v); + this->parent_->set_immediately_(v, true); } else { // INSTANT CHANGE - this->parent_->set_immediately_(v); + this->parent_->set_immediately_(v, this->publish_); } if (this->publish_) { @@ -460,7 +463,8 @@ LightColorValues LightCall::validate_() { this->transition_length_.reset(); } - if (!this->has_transition_() && !this->has_flash_() && !this->has_effect_() && supports_transition) { + if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || *this->effect_ == 0) && + supports_transition) { // nothing specified and light supports transitions, set default transition length this->transition_length_ = this->parent_->default_transition_length_; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index d67aa2c53d..07a0e3147b 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -277,6 +277,7 @@ class LightState : public Nameable, public Component { protected: friend LightOutput; friend LightCall; + friend class AddressableLight; uint32_t hash_base() override; @@ -291,7 +292,7 @@ class LightState : public Nameable, public Component { void start_flash_(const LightColorValues &target, uint32_t length); /// Internal method to set the color values to target immediately (with no transition). - void set_immediately_(const LightColorValues &target); + void set_immediately_(const LightColorValues &target, bool set_remote_values); /// Internal method to start a transformer. void set_transformer_(std::unique_ptr transformer); diff --git a/esphome/components/light/light_transformer.h b/esphome/components/light/light_transformer.h index 91a406f425..222be7802c 100644 --- a/esphome/components/light/light_transformer.h +++ b/esphome/components/light/light_transformer.h @@ -17,7 +17,7 @@ class LightTransformer { LightTransformer() = delete; /// Whether this transformation is finished - virtual bool is_finished() { return this->get_progress_() >= 1.0f; } + virtual bool is_finished() { return this->get_progress() >= 1.0f; } /// This will be called to get the current values for output. virtual LightColorValues get_values() = 0; @@ -29,11 +29,11 @@ class LightTransformer { virtual LightColorValues get_end_values() { return this->get_target_values_(); } virtual bool publish_at_end() = 0; + virtual bool is_transition() = 0; + + float get_progress() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } protected: - /// Get the completion of this transformer, 0 to 1. - float get_progress_() { return clamp((millis() - this->start_time_) / float(this->length_), 0.0f, 1.0f); } - const LightColorValues &get_start_values_() const { return this->start_values_; } const LightColorValues &get_target_values_() const { return this->target_values_; } @@ -61,12 +61,14 @@ class LightTransitionTransformer : public LightTransformer { } LightColorValues get_values() override { - float x = this->get_progress_(); - float v = x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); + float v = LightTransitionTransformer::smoothed_progress(this->get_progress()); return LightColorValues::lerp(this->get_start_values_(), this->get_target_values_(), v); } bool publish_at_end() override { return false; } + bool is_transition() override { return true; } + + static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); } }; class LightFlashTransformer : public LightTransformer { @@ -80,6 +82,7 @@ class LightFlashTransformer : public LightTransformer { LightColorValues get_end_values() override { return this->get_start_values_(); } bool publish_at_end() override { return true; } + bool is_transition() override { return false; } }; } // namespace light diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 9be6cc8319..fb88d021f2 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -7,8 +7,10 @@ LightState = light_ns.class_('LightState', cg.Nameable, cg.Component) # Fake class for addressable lights AddressableLightState = light_ns.class_('LightState', LightState) LightOutput = light_ns.class_('LightOutput') -AddressableLight = light_ns.class_('AddressableLight') +AddressableLight = light_ns.class_('AddressableLight', cg.Component) AddressableLightRef = AddressableLight.operator('ref') + +ESPColor = light_ns.class_('ESPColor') LightColorValues = light_ns.class_('LightColorValues') # Actions diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 94b33ae18e..3e07334313 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -29,7 +29,7 @@ LOG_LEVEL_TO_ESP_LOG = { 'VERY_VERBOSE': cg.global_ns.ESP_LOGVV, } -LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE'] +LOG_LEVEL_SEVERITY = ['NONE', 'ERROR', 'WARN', 'INFO', 'CONFIG', 'DEBUG', 'VERBOSE', 'VERY_VERBOSE'] UART_SELECTION_ESP32 = ['UART0', 'UART1', 'UART2'] @@ -123,6 +123,8 @@ def to_code(config): 'TLS_MEM', 'UPDATER', 'WIFI', + # Spams logs too much: + # 'MDNS_RESPONDER', } for comp in DEBUG_COMPONENTS: cg.add_build_flag("-DDEBUG_ESP_{}".format(comp)) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 78f09989e4..bc6951c9b9 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -10,38 +10,74 @@ namespace logger { static const char *TAG = "logger"; -int HOT Logger::log_vprintf_(int level, const char *tag, const char *format, va_list args) { // NOLINT - if (level > this->level_for(tag)) - return 0; +static const char *LOG_LEVEL_COLORS[] = { + "", // NONE + ESPHOME_LOG_BOLD(ESPHOME_LOG_COLOR_RED), // ERROR + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_YELLOW), // WARNING + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GREEN), // INFO + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_MAGENTA), // CONFIG + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_CYAN), // DEBUG + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_GRAY), // VERBOSE + ESPHOME_LOG_COLOR(ESPHOME_LOG_COLOR_WHITE), // VERY_VERBOSE +}; +static const char *LOG_LEVEL_LETTERS[] = { + "", // NONE + "E", // ERROR + "W", // WARNING + "I", // INFO + "C", // CONFIG + "D", // DEBUG + "V", // VERBOSE + "VV", // VERY_VERBOSE +}; - int ret = vsnprintf(this->tx_buffer_.data(), this->tx_buffer_.capacity(), format, args); - this->log_message_(level, tag, this->tx_buffer_.data(), ret); - return ret; +void Logger::write_header_(int level, const char *tag, int line) { + if (level < 0) + level = 0; + if (level > 7) + level = 7; + + const char *color = LOG_LEVEL_COLORS[level]; + const char *letter = LOG_LEVEL_LETTERS[level]; + this->printf_to_buffer_("%s[%s][%s:%03u]: ", color, letter, tag, line); +} + +void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT + if (level > this->level_for(tag)) + return; + + this->reset_buffer_(); + this->write_header_(level, tag, line); + this->vprintf_to_buffer_(format, args); + this->write_footer_(); + this->log_message_(level, tag); } #ifdef USE_STORE_LOG_STR_IN_FLASH -int Logger::log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args) { // NOLINT +void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, + va_list args) { // NOLINT if (level > this->level_for(tag)) - return 0; + return; + this->reset_buffer_(); // copy format string const char *format_pgm_p = (PGM_P) format; size_t len = 0; - char *write = this->tx_buffer_.data(); char ch = '.'; - while (len < this->tx_buffer_.capacity() && ch != '\0') { - *write++ = ch = pgm_read_byte(format_pgm_p++); - len++; + while (!this->is_buffer_full_() && ch != '\0') { + this->tx_buffer_[this->tx_buffer_at_++] = ch = pgm_read_byte(format_pgm_p++); } - if (len == this->tx_buffer_.capacity()) - return -1; + // Buffer full form copying format + if (this->is_buffer_full_()) + return; + + // length of format string, includes null terminator + uint32_t offset = this->tx_buffer_at_; // now apply vsnprintf - size_t offset = len + 1; - size_t remaining = this->tx_buffer_.capacity() - offset; - char *msg = this->tx_buffer_.data() + offset; - int ret = vsnprintf(msg, remaining, this->tx_buffer_.data(), args); - this->log_message_(level, tag, msg, ret); - return ret; + this->write_header_(level, tag, line); + this->vprintf_to_buffer_(this->tx_buffer_, args); + this->write_footer_(); + this->log_message_(level, tag, offset); } #endif @@ -54,22 +90,26 @@ int HOT Logger::level_for(const char *tag) { return it.level; } } - return this->global_log_level_; + return ESPHOME_LOG_LEVEL; } -void HOT Logger::log_message_(int level, const char *tag, char *msg, int ret) { - if (ret <= 0) - return; +void HOT Logger::log_message_(int level, const char *tag, int offset) { // remove trailing newline - if (msg[ret - 1] == '\n') { - msg[ret - 1] = '\0'; + if (this->tx_buffer_[this->tx_buffer_at_ - 1] == '\n') { + this->tx_buffer_at_--; } + // make sure null terminator is present + this->set_null_terminator_(); + + const char *msg = this->tx_buffer_ + offset; if (this->baud_rate_ > 0) this->hw_serial_->println(msg); this->log_callback_.call(level, tag, msg); } -Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) : baud_rate_(baud_rate), uart_(uart) { - this->set_tx_buffer_size(tx_buffer_size); +Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size, UARTSelection uart) + : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size), uart_(uart) { + // add 1 to buffer size for null terminator + this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; } void Logger::pre_setup() { @@ -96,7 +136,7 @@ void Logger::pre_setup() { if (this->uart_ == UART_SELECTION_UART0_SWAP) { this->hw_serial_->swap(); } - this->hw_serial_->setDebugOutput(this->global_log_level_ >= ESPHOME_LOG_LEVEL_VERBOSE); + this->hw_serial_->setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE); #endif } #ifdef ARDUINO_ARCH_ESP8266 @@ -108,7 +148,7 @@ void Logger::pre_setup() { global_logger = this; #ifdef ARDUINO_ARCH_ESP32 esp_log_set_vprintf(esp_idf_log_vprintf_); - if (this->global_log_level_ >= ESPHOME_LOG_LEVEL_VERBOSE) { + if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { esp_log_level_set("*", ESP_LOG_VERBOSE); } #endif @@ -116,17 +156,15 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; } -void Logger::set_global_log_level(int log_level) { this->global_log_level_ = log_level; } void Logger::set_log_level(const std::string &tag, int log_level) { this->log_levels_.push_back(LogLevelOverride{tag, log_level}); } -void Logger::set_tx_buffer_size(size_t tx_buffer_size) { this->tx_buffer_.reserve(tx_buffer_size); } UARTSelection Logger::get_uart() const { return this->uart_; } void Logger::add_on_log_callback(std::function &&callback) { this->log_callback_.add(std::move(callback)); } float Logger::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; } -const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; +const char *LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; #ifdef ARDUINO_ARCH_ESP32 const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART2"}; #endif @@ -135,13 +173,14 @@ const char *UART_SELECTIONS[] = {"UART0", "UART1", "UART0_SWAP"}; #endif void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:"); - ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[this->global_log_level_]); + ESP_LOGCONFIG(TAG, " Level: %s", LOG_LEVELS[ESPHOME_LOG_LEVEL]); ESP_LOGCONFIG(TAG, " Log Baud Rate: %u", this->baud_rate_); ESP_LOGCONFIG(TAG, " Hardware UART: %s", UART_SELECTIONS[this->uart_]); for (auto &it : this->log_levels_) { ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.tag.c_str(), LOG_LEVELS[it.level]); } } +void Logger::write_footer_() { this->write_to_buffer_(ESPHOME_LOG_RESET_COLOR, strlen(ESPHOME_LOG_RESET_COLOR)); } Logger *global_logger = nullptr; diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 6f06c63595..18196b68d2 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -31,16 +31,9 @@ class Logger : public Component { /// Manually set the baud rate for serial, set to 0 to disable. void set_baud_rate(uint32_t baud_rate); - /// Set the buffer size that's used for constructing log messages. Log messages longer than this will be truncated. - void set_tx_buffer_size(size_t tx_buffer_size); - /// Get the UART used by the logger. UARTSelection get_uart() const; - /// Set the global log level. Note: Use the ESPHOME_LOG_LEVEL define to also remove the logs from the build. - void set_global_log_level(int log_level); - int get_global_log_level() const { return this->global_log_level_; } - /// Set the log level of the specified tag. void set_log_level(const std::string &tag, int log_level); @@ -57,17 +50,58 @@ class Logger : public Component { float get_setup_priority() const override; - int log_vprintf_(int level, const char *tag, const char *format, va_list args); // NOLINT + void log_vprintf_(int level, const char *tag, int line, const char *format, va_list args); // NOLINT #ifdef USE_STORE_LOG_STR_IN_FLASH - int log_vprintf_(int level, const char *tag, const __FlashStringHelper *format, va_list args); // NOLINT + void log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, va_list args); // NOLINT #endif protected: - void log_message_(int level, const char *tag, char *msg, int ret); + void write_header_(int level, const char *tag, int line); + void write_footer_(); + void log_message_(int level, const char *tag, int offset = 0); + + inline bool is_buffer_full_() const { return this->tx_buffer_at_ >= this->tx_buffer_size_; } + inline int buffer_remaining_capacity_() const { return this->tx_buffer_size_ - this->tx_buffer_at_; } + inline void reset_buffer_() { this->tx_buffer_at_ = 0; } + inline void set_null_terminator_() { + // does not increment buffer_at + this->tx_buffer_[this->tx_buffer_at_] = '\0'; + } + inline void write_to_buffer_(char value) { + if (!this->is_buffer_full_()) + this->tx_buffer_[this->tx_buffer_at_++] = value; + } + inline void write_to_buffer_(const char *value, int length) { + for (int i = 0; i < length && !this->is_buffer_full_(); i++) { + this->tx_buffer_[this->tx_buffer_at_++] = value[i]; + } + } + inline void vprintf_to_buffer_(const char *format, va_list args) { + if (this->is_buffer_full_()) + return; + int remaining = this->buffer_remaining_capacity_(); + int ret = vsnprintf(this->tx_buffer_ + this->tx_buffer_at_, remaining, format, args); + if (ret < 0) { + // Encoding error, do not increment buffer_at + return; + } + if (ret >= remaining) { + // output was too long, truncated + ret = remaining; + } + this->tx_buffer_at_ += ret; + } + inline void printf_to_buffer_(const char *format, ...) { + va_list arg; + va_start(arg, format); + this->vprintf_to_buffer_(format, arg); + va_end(arg); + } uint32_t baud_rate_; - std::vector tx_buffer_; - int global_log_level_{ESPHOME_LOG_LEVEL}; + char *tx_buffer_{nullptr}; + int tx_buffer_at_{0}; + int tx_buffer_size_{0}; UARTSelection uart_{UART_SELECTION_UART0}; HardwareSerial *hw_serial_{nullptr}; struct LogLevelOverride { diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index 18a00b10d7..0462ed4342 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -82,7 +82,5 @@ void MAX31855Sensor::read_data_() { this->status_clear_warning(); } -bool MAX31855Sensor::is_device_msb_first() { return true; } - } // namespace max31855 } // namespace esphome diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index f9cdf335f1..1d0fc79ac0 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -7,7 +7,10 @@ namespace esphome { namespace max31855 { -class MAX31855Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice { +class MAX31855Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { public: void setup() override; void dump_config() override; @@ -16,8 +19,6 @@ class MAX31855Sensor : public sensor::Sensor, public PollingComponent, public sp void update() override; protected: - bool is_device_msb_first() override; - void read_data_(); }; diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index 8ea7feb963..53442b9cb1 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -48,7 +48,5 @@ void MAX6675Sensor::read_data_() { this->status_clear_warning(); } -bool MAX6675Sensor::is_device_msb_first() { return true; } - } // namespace max6675 } // namespace esphome diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index 48f51fbe11..09bd9df3b8 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -7,7 +7,10 @@ namespace esphome { namespace max6675 { -class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi::SPIDevice { +class MAX6675Sensor : public sensor::Sensor, + public PollingComponent, + public spi::SPIDevice { public: void setup() override; void dump_config() override; @@ -16,8 +19,6 @@ class MAX6675Sensor : public sensor::Sensor, public PollingComponent, public spi void update() override; protected: - bool is_device_msb_first() override; - void read_data_(); }; diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index bc3c3ae0c9..db43ff19f6 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -155,7 +155,6 @@ void MAX7219Component::send_to_all_(uint8_t a_register, uint8_t data) { this->send_byte_(a_register, data); this->disable(); } -bool MAX7219Component::is_device_msb_first() { return true; } void MAX7219Component::update() { for (uint8_t i = 0; i < this->num_chips_ * 8; i++) this->buffer_[i] = 0; diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index e2379fa69b..1920268ba4 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -16,7 +16,9 @@ class MAX7219Component; using max7219_writer_t = std::function; -class MAX7219Component : public PollingComponent, public spi::SPIDevice { +class MAX7219Component : public PollingComponent, + public spi::SPIDevice { public: void set_writer(max7219_writer_t &&writer); @@ -54,7 +56,6 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice { protected: void send_byte_(uint8_t a_register, uint8_t data); void send_to_all_(uint8_t a_register, uint8_t data); - bool is_device_msb_first() override; uint8_t intensity_{15}; /// Intensity of the display from 0 to 15 (most) uint8_t num_chips_{1}; diff --git a/esphome/components/mcp23008/__init__.py b/esphome/components/mcp23008/__init__.py new file mode 100644 index 0000000000..4241b6ba48 --- /dev/null +++ b/esphome/components/mcp23008/__init__.py @@ -0,0 +1,51 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_NUMBER, CONF_MODE, CONF_INVERTED + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +mcp23008_ns = cg.esphome_ns.namespace('mcp23008') +MCP23008GPIOMode = mcp23008_ns.enum('MCP23008GPIOMode') +MCP23008_GPIO_MODES = { + 'INPUT': MCP23008GPIOMode.MCP23008_INPUT, + 'INPUT_PULLUP': MCP23008GPIOMode.MCP23008_INPUT_PULLUP, + 'OUTPUT': MCP23008GPIOMode.MCP23008_OUTPUT, +} + +MCP23008 = mcp23008_ns.class_('MCP23008', cg.Component, i2c.I2CDevice) +MCP23008GPIOPin = mcp23008_ns.class_('MCP23008GPIOPin', cg.GPIOPin) + +CONFIG_SCHEMA = cv.Schema({ + cv.Required(CONF_ID): cv.declare_id(MCP23008), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + +CONF_MCP23008 = 'mcp23008' +MCP23008_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23008): cv.use_id(MCP23008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +MCP23008_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_MCP23008): cv.use_id(MCP23008), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(MCP23008_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_MCP23008, + (MCP23008_OUTPUT_PIN_SCHEMA, MCP23008_INPUT_PIN_SCHEMA)) +def mcp23008_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_MCP23008]) + yield MCP23008GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], config[CONF_INVERTED]) diff --git a/esphome/components/mcp23008/mcp23008.cpp b/esphome/components/mcp23008/mcp23008.cpp new file mode 100644 index 0000000000..bf5bb55f2e --- /dev/null +++ b/esphome/components/mcp23008/mcp23008.cpp @@ -0,0 +1,91 @@ +#include "mcp23008.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mcp23008 { + +static const char *TAG = "mcp23008"; + +void MCP23008::setup() { + ESP_LOGCONFIG(TAG, "Setting up MCP23008..."); + uint8_t iocon; + if (!this->read_reg_(MCP23008_IOCON, &iocon)) { + this->mark_failed(); + return; + } + + // all pins input + this->write_reg_(MCP23008_IODIR, 0xFF); +} +bool MCP23008::digital_read(uint8_t pin) { + uint8_t bit = pin % 8; + uint8_t reg_addr = MCP23008_GPIO; + uint8_t value = 0; + this->read_reg_(reg_addr, &value); + return value & (1 << bit); +} +void MCP23008::digital_write(uint8_t pin, bool value) { + uint8_t reg_addr = MCP23008_OLAT; + this->update_reg_(pin, value, reg_addr); +} +void MCP23008::pin_mode(uint8_t pin, uint8_t mode) { + uint8_t iodir = MCP23008_IODIR; + uint8_t gppu = MCP23008_GPPU; + switch (mode) { + case MCP23008_INPUT: + this->update_reg_(pin, true, iodir); + break; + case MCP23008_INPUT_PULLUP: + this->update_reg_(pin, true, iodir); + this->update_reg_(pin, true, gppu); + break; + case MCP23008_OUTPUT: + this->update_reg_(pin, false, iodir); + break; + default: + break; + } +} +float MCP23008::get_setup_priority() const { return setup_priority::HARDWARE; } +bool MCP23008::read_reg_(uint8_t reg, uint8_t *value) { + if (this->is_failed()) + return false; + + return this->read_byte(reg, value); +} +bool MCP23008::write_reg_(uint8_t reg, uint8_t value) { + if (this->is_failed()) + return false; + + return this->write_byte(reg, value); +} +void MCP23008::update_reg_(uint8_t pin, bool pin_value, uint8_t reg_addr) { + uint8_t bit = pin % 8; + uint8_t reg_value = 0; + if (reg_addr == MCP23008_OLAT) { + reg_value = this->olat_; + } else { + this->read_reg_(reg_addr, ®_value); + } + + if (pin_value) + reg_value |= 1 << bit; + else + reg_value &= ~(1 << bit); + + this->write_reg_(reg_addr, reg_value); + + if (reg_addr == MCP23008_OLAT) { + this->olat_ = reg_value; + } +} + +MCP23008GPIOPin::MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted) + : GPIOPin(pin, mode, inverted), parent_(parent) {} +void MCP23008GPIOPin::setup() { this->pin_mode(this->mode_); } +void MCP23008GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool MCP23008GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void MCP23008GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace mcp23008 +} // namespace esphome diff --git a/esphome/components/mcp23008/mcp23008.h b/esphome/components/mcp23008/mcp23008.h new file mode 100644 index 0000000000..b4e5d75fd4 --- /dev/null +++ b/esphome/components/mcp23008/mcp23008.h @@ -0,0 +1,69 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace mcp23008 { + +/// Modes for MCP23008 pins +enum MCP23008GPIOMode : uint8_t { + MCP23008_INPUT = INPUT, // 0x00 + MCP23008_INPUT_PULLUP = INPUT_PULLUP, // 0x02 + MCP23008_OUTPUT = OUTPUT // 0x01 +}; + +enum MCP23008GPIORegisters { + // A side + MCP23008_IODIR = 0x00, + MCP23008_IPOL = 0x01, + MCP23008_GPINTEN = 0x02, + MCP23008_DEFVAL = 0x03, + MCP23008_INTCON = 0x04, + MCP23008_IOCON = 0x05, + MCP23008_GPPU = 0x06, + MCP23008_INTF = 0x07, + MCP23008_INTCAP = 0x08, + MCP23008_GPIO = 0x09, + MCP23008_OLAT = 0x0A, +}; + +class MCP23008 : public Component, public i2c::I2CDevice { + public: + MCP23008() = default; + + void setup() override; + + bool digital_read(uint8_t pin); + void digital_write(uint8_t pin, bool value); + void pin_mode(uint8_t pin, uint8_t mode); + + float get_setup_priority() const override; + + protected: + // read a given register + bool read_reg_(uint8_t reg, uint8_t *value); + // write a value to a given register + bool write_reg_(uint8_t reg, uint8_t value); + // update registers with given pin value. + void update_reg_(uint8_t pin, bool pin_value, uint8_t reg_a); + + uint8_t olat_{0x00}; +}; + +class MCP23008GPIOPin : public GPIOPin { + public: + MCP23008GPIOPin(MCP23008 *parent, uint8_t pin, uint8_t mode, bool inverted = false); + + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + MCP23008 *parent_; +}; + +} // namespace mcp23008 +} // namespace esphome diff --git a/esphome/components/mcp23017/mcp23017.cpp b/esphome/components/mcp23017/mcp23017.cpp index 687c816179..9653aa680d 100644 --- a/esphome/components/mcp23017/mcp23017.cpp +++ b/esphome/components/mcp23017/mcp23017.cpp @@ -47,7 +47,7 @@ void MCP23017::pin_mode(uint8_t pin, uint8_t mode) { break; } } -float MCP23017::get_setup_priority() const { return setup_priority::HARDWARE; } +float MCP23017::get_setup_priority() const { return setup_priority::IO; } bool MCP23017::read_reg_(uint8_t reg, uint8_t *value) { if (this->is_failed()) return false; diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index 8f46e288b6..36ccf70d84 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -8,6 +8,9 @@ static const char *TAG = "mhz19"; static const uint8_t MHZ19_REQUEST_LENGTH = 8; static const uint8_t MHZ19_RESPONSE_LENGTH = 9; static const uint8_t MHZ19_COMMAND_GET_PPM[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_ABC_ENABLE[] = {0xFF, 0x01, 0x79, 0xA0, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_ABC_DISABLE[] = {0xFF, 0x01, 0x79, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t MHZ19_COMMAND_CALIBRATE_ZERO[] = {0xFF, 0x01, 0x87, 0x00, 0x00, 0x00, 0x00, 0x00}; uint8_t mhz19_checksum(const uint8_t *command) { uint8_t sum = 0; @@ -17,6 +20,14 @@ uint8_t mhz19_checksum(const uint8_t *command) { return 0xFF - sum + 0x01; } +void MHZ19Component::setup() { + if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { + this->abc_enable(); + } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { + this->abc_disable(); + } +} + void MHZ19Component::update() { uint8_t response[MHZ19_RESPONSE_LENGTH]; if (!this->mhz19_write_command_(MHZ19_COMMAND_GET_PPM, response)) { @@ -50,23 +61,45 @@ void MHZ19Component::update() { this->temperature_sensor_->publish_state(temp); } +void MHZ19Component::calibrate_zero() { + ESP_LOGD(TAG, "MHZ19 Calibrating zero point"); + this->mhz19_write_command_(MHZ19_COMMAND_CALIBRATE_ZERO, nullptr); +} + +void MHZ19Component::abc_enable() { + ESP_LOGD(TAG, "MHZ19 Enabling automatic baseline calibration"); + this->mhz19_write_command_(MHZ19_COMMAND_ABC_ENABLE, nullptr); +} + +void MHZ19Component::abc_disable() { + ESP_LOGD(TAG, "MHZ19 Disabling automatic baseline calibration"); + this->mhz19_write_command_(MHZ19_COMMAND_ABC_DISABLE, nullptr); +} + bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *response) { - this->flush(); + // Empty RX Buffer + while (this->available()) + this->read(); this->write_array(command, MHZ19_REQUEST_LENGTH); this->write_byte(mhz19_checksum(command)); + this->flush(); if (response == nullptr) return true; - bool ret = this->read_array(response, MHZ19_RESPONSE_LENGTH); - this->flush(); - return ret; + return this->read_array(response, MHZ19_RESPONSE_LENGTH); } float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + + if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) { + ESP_LOGCONFIG(TAG, " Automatic baseline calibration enabled on boot"); + } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) { + ESP_LOGCONFIG(TAG, " Automatic baseline calibration disabled on boot"); + } } } // namespace mhz19 diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index 3604628afc..2201fc87f0 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -1,27 +1,64 @@ #pragma once #include "esphome/core/component.h" +#include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" namespace esphome { namespace mhz19 { +enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, MHZ19_ABC_ENABLED, MHZ19_ABC_DISABLED }; + class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: float get_setup_priority() const override; + void setup() override; void update() override; void dump_config() override; + void calibrate_zero(); + void abc_enable(); + void abc_disable(); + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; } protected: bool mhz19_write_command_(const uint8_t *command, uint8_t *response); sensor::Sensor *temperature_sensor_{nullptr}; sensor::Sensor *co2_sensor_{nullptr}; + MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE}; +}; + +template class MHZ19CalibrateZeroAction : public Action { + public: + MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->calibrate_zero(); } + + protected: + MHZ19Component *mhz19_; +}; + +template class MHZ19ABCEnableAction : public Action { + public: + MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_enable(); } + + protected: + MHZ19Component *mhz19_; +}; + +template class MHZ19ABCDisableAction : public Action { + public: + MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {} + void play(Ts... x) override { this->mhz19_->abc_disable(); } + + protected: + MHZ19Component *mhz19_; }; } // namespace mhz19 diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py index 368426e6f7..bdcecf12cb 100644 --- a/esphome/components/mhz19/sensor.py +++ b/esphome/components/mhz19/sensor.py @@ -1,18 +1,26 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import automation +from esphome.automation import maybe_simple_id from esphome.components import sensor, uart from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER DEPENDENCIES = ['uart'] +CONF_AUTOMATIC_BASELINE_CALIBRATION = 'automatic_baseline_calibration' + mhz19_ns = cg.esphome_ns.namespace('mhz19') MHZ19Component = mhz19_ns.class_('MHZ19Component', cg.PollingComponent, uart.UARTDevice) +MHZ19CalibrateZeroAction = mhz19_ns.class_('MHZ19CalibrateZeroAction', automation.Action) +MHZ19ABCEnableAction = mhz19_ns.class_('MHZ19ABCEnableAction', automation.Action) +MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Action) CONFIG_SCHEMA = cv.Schema({ cv.GenerateID(): cv.declare_id(MHZ19Component), cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0), + cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean, }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) @@ -28,3 +36,22 @@ def to_code(config): if CONF_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) cg.add(var.set_temperature_sensor(sens)) + + if CONF_AUTOMATIC_BASELINE_CALIBRATION in config: + cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION])) + + +CALIBRATION_ACTION_SCHEMA = maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(MHZ19Component), +}) + + +@automation.register_action('mhz19.calibrate_zero', MHZ19CalibrateZeroAction, + CALIBRATION_ACTION_SCHEMA) +@automation.register_action('mhz19.abc_enable', MHZ19ABCEnableAction, + CALIBRATION_ACTION_SCHEMA) +@automation.register_action('mhz19.abc_disable', MHZ19ABCDisableAction, + CALIBRATION_ACTION_SCHEMA) +def mhz19_calibration_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index c363f93d06..5a9bbee377 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -15,7 +15,7 @@ from esphome.const import CONF_AVAILABILITY, CONF_BIRTH_MESSAGE, CONF_BROKER, CO from esphome.core import coroutine_with_priority, coroutine, CORE DEPENDENCIES = ['network'] -AUTO_LOAD = ['json'] +AUTO_LOAD = ['json', 'async_tcp'] def validate_message_just_topic(value): @@ -41,7 +41,8 @@ MQTTClientComponent = mqtt_ns.class_('MQTTClientComponent', cg.Component) MQTTPublishAction = mqtt_ns.class_('MQTTPublishAction', automation.Action) MQTTPublishJsonAction = mqtt_ns.class_('MQTTPublishJsonAction', automation.Action) MQTTMessageTrigger = mqtt_ns.class_('MQTTMessageTrigger', - automation.Trigger.template(cg.std_string)) + automation.Trigger.template(cg.std_string), + cg.Component) MQTTJsonMessageTrigger = mqtt_ns.class_('MQTTJsonMessageTrigger', automation.Trigger.template(cg.JsonObjectConstRef)) MQTTComponent = mqtt_ns.class_('MQTTComponent', cg.Component) @@ -104,7 +105,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_PORT, default=1883): cv.port, cv.Optional(CONF_USERNAME, default=''): cv.string, cv.Optional(CONF_PASSWORD, default=''): cv.string, - cv.Optional(CONF_CLIENT_ID, default=lambda: CORE.name): cv.string, + cv.Optional(CONF_CLIENT_ID): cv.string, cv.Optional(CONF_DISCOVERY, default=True): cv.Any(cv.boolean, cv.one_of("CLEAN", upper=True)), cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.publish_topic, @@ -120,7 +121,7 @@ CONFIG_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_SSL_FINGERPRINTS): cv.All(cv.only_on_esp8266, cv.ensure_list(validate_fingerprint)), cv.Optional(CONF_KEEPALIVE, default='15s'): cv.positive_time_period_seconds, - cv.Optional(CONF_REBOOT_TIMEOUT, default='5min'): cv.positive_time_period_milliseconds, + cv.Optional(CONF_REBOOT_TIMEOUT, default='15min'): cv.positive_time_period_milliseconds, cv.Optional(CONF_ON_MESSAGE): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MQTTMessageTrigger), cv.Required(CONF_TOPIC): cv.subscribe_topic, @@ -153,7 +154,8 @@ def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) - cg.add_library('AsyncMqttClient', '0.8.2') + # https://github.com/marvinroger/async-mqtt-client/blob/master/library.json + cg.add_library('AsyncMqttClient-esphome', '0.8.2') cg.add_define('USE_MQTT') cg.add_global(mqtt_ns.using) @@ -161,7 +163,8 @@ def to_code(config): cg.add(var.set_broker_port(config[CONF_PORT])) cg.add(var.set_username(config[CONF_USERNAME])) cg.add(var.set_password(config[CONF_PASSWORD])) - cg.add(var.set_client_id(config[CONF_CLIENT_ID])) + if CONF_CLIENT_ID in config: + cg.add(var.set_client_id(config[CONF_CLIENT_ID])) discovery = config[CONF_DISCOVERY] discovery_retain = config[CONF_DISCOVERY_RETAIN] @@ -216,6 +219,7 @@ def to_code(config): cg.add(trig.set_qos(conf[CONF_QOS])) if CONF_PAYLOAD in conf: cg.add(trig.set_payload(conf[CONF_PAYLOAD])) + yield cg.register_component(trig, conf) yield automation.build_automation(trig, [(cg.std_string, 'x')], conf) for conf in config.get(CONF_ON_JSON_MESSAGE, []): diff --git a/esphome/components/mqtt/custom_mqtt_device.cpp b/esphome/components/mqtt/custom_mqtt_device.cpp new file mode 100644 index 0000000000..8b17c5f17f --- /dev/null +++ b/esphome/components/mqtt/custom_mqtt_device.cpp @@ -0,0 +1,30 @@ +#include "custom_mqtt_device.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace mqtt { + +static const char *TAG = "mqtt.custom"; + +bool CustomMQTTDevice::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) { + return global_mqtt_client->publish(topic, payload, qos, retain); +} +bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t number_decimals) { + auto str = value_accuracy_to_string(value, number_decimals); + return this->publish(topic, str); +} +bool CustomMQTTDevice::publish(const std::string &topic, int value) { + char buffer[24]; + sprintf(buffer, "%d", value); + return this->publish(topic, buffer); +} +bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) { + return global_mqtt_client->publish_json(topic, f, qos, retain); +} +bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f) { + return this->publish_json(topic, f, 0, false); +} +bool CustomMQTTDevice::is_connected() { return global_mqtt_client != nullptr && global_mqtt_client->is_connected(); } + +} // namespace mqtt +} // namespace esphome diff --git a/esphome/components/mqtt/custom_mqtt_device.h b/esphome/components/mqtt/custom_mqtt_device.h new file mode 100644 index 0000000000..1c8b2e916e --- /dev/null +++ b/esphome/components/mqtt/custom_mqtt_device.h @@ -0,0 +1,217 @@ +#pragma once + +#include "esphome/core/component.h" +#include "mqtt_client.h" + +namespace esphome { +namespace mqtt { + +/** This class is a helper class for custom components that communicate using + * MQTT. It has 5 helper functions that you can use (square brackets indicate optional): + * + * - `subscribe(topic, function_pointer, [qos])` + * - `subscribe_json(topic, function_pointer, [qos])` + * - `publish(topic, payload, [qos], [retain])` + * - `publish_json(topic, payload_builder, [qos], [retain])` + * - `is_connected()` + */ +class CustomMQTTDevice { + public: + /** Subscribe to an MQTT topic with the given Quality of Service. + * + * Example: + * + * ```cpp + * class MyCustomMQTTDevice : public Component, public mqtt:CustomMQTTDevice { + * public: + * void setup() override { + * subscribe("the/topic", &MyCustomMQTTDevice::on_message); + * pinMode(5, OUTPUT); + * } + * + * // topic and payload parameters can be removed if not needed + * // e.g: void on_message() { + * + * void on_message(const std::string &topic, const std::string &payload) { + * // do something with topic and payload + * if (payload == "ON") { + * digitalWrite(5, HIGH); + * } else { + * digitalWrite(5, LOW); + * } + * } + * }; + * ``` + * + * @tparam T A C++ template argument for determining the type of the callback. + * @param topic The topic to subscribe to. Re-subscription on re-connects is automatically handled. + * @param callback The callback (must be a class member) to subscribe with. + * @param qos The Quality of Service to subscribe with. Defaults to 0. + */ + template + void subscribe(const std::string &topic, void (T::*callback)(const std::string &, const std::string &), + uint8_t qos = 0); + + template + void subscribe(const std::string &topic, void (T::*callback)(const std::string &), uint8_t qos = 0); + + template void subscribe(const std::string &topic, void (T::*callback)(), uint8_t qos = 0); + + /** Subscribe to an MQTT topic and call the callback if the payload can be decoded + * as JSON with the given Quality of Service. + * + * Example: + * + * ```cpp + * class MyCustomMQTTDevice : public Component, public mqtt:CustomMQTTDevice { + * public: + * void setup() override { + * subscribe_json("the/topic", &MyCustomMQTTDevice::on_json_message); + * pinMode(5, OUTPUT); + * } + * + * // topic parameter can be remove if not needed: + * // e.g.: void on_json_message(JsonObject &payload) { + * + * void on_json_message(const std::string &topic, JsonObject &payload) { + * // do something with topic and payload + * if (payload["number"] == 1) { + * digitalWrite(5, HIGH); + * } else { + * digitalWrite(5, LOW); + * } + * } + * }; + * ``` + * + * @tparam T A C++ template argument for determining the type of the callback. + * @param topic The topic to subscribe to. Re-subscription on re-connects is automatically handled. + * @param callback The callback (must be a class member) to subscribe with. + * @param qos The Quality of Service to subscribe with. Defaults to 0. + */ + template + void subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject &), + uint8_t qos = 0); + + template + void subscribe_json(const std::string &topic, void (T::*callback)(JsonObject &), uint8_t qos = 0); + + /** Publish an MQTT message with the given payload and QoS and retain settings. + * + * Example: + * + * ```cpp + * void in_some_method() { + * publish("the/topic", "The Payload", 0, true); + * } + * ``` + * + * @param topic The topic to publish to. + * @param payload The payload to publish. + * @param qos The Quality of Service to publish with. Defaults to 0 + * @param retain Whether to retain the message. Defaults to false. + */ + bool publish(const std::string &topic, const std::string &payload, uint8_t qos = 0, bool retain = false); + + /** Publish an MQTT message with the given floating point number and number of decimals. + * + * Example: + * + * ```cpp + * void in_some_method() { + * publish("the/topic", 1.0); + * // with two digits after the decimal point + * publish("the/topic", 1.0, 2); + * } + * ``` + * + * @param topic The topic to publish to. + * @param payload The payload to publish. + * @param number_decimals The number of digits after the decimal point to round to, defaults to 3 digits. + */ + bool publish(const std::string &topic, float value, int8_t number_decimals = 3); + + /** Publish an MQTT message with the given integer as payload. + * + * Example: + * + * ```cpp + * void in_some_method() { + * publish("the/topic", 42); + * } + * ``` + * + * @param topic The topic to publish to. + * @param payload The payload to publish. + */ + bool publish(const std::string &topic, int value); + + /** Publish a JSON-encoded MQTT message with the given Quality of Service and retain settings. + * + * Example: + * + * ```cpp + * void in_some_method() { + * publish("the/topic", [=](JsonObject &root) { + * root["the_key"] = "Hello World!"; + * }, 0, false); + * } + * ``` + * + * @param topic The topic to publish to. + * @param payload The payload to publish. + * @param qos The Quality of Service to publish with. + * @param retain Whether to retain the message. + */ + bool publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain); + + /** Publish a JSON-encoded MQTT message. + * + * Example: + * + * ```cpp + * void in_some_method() { + * publish("the/topic", [=](JsonObject &root) { + * root["the_key"] = "Hello World!"; + * }); + * } + * ``` + * + * @param topic The topic to publish to. + * @param payload The payload to publish. + */ + bool publish_json(const std::string &topic, const json::json_build_t &f); + + /// Check whether the MQTT client is currently connected and messages can be published. + bool is_connected(); +}; + +template +void CustomMQTTDevice::subscribe(const std::string &topic, + void (T::*callback)(const std::string &, const std::string &), uint8_t qos) { + auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); + global_mqtt_client->subscribe(topic, f, qos); +} +template +void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(const std::string &), uint8_t qos) { + auto f = std::bind(callback, (T *) this, std::placeholders::_2); + global_mqtt_client->subscribe(topic, f, qos); +} +template void CustomMQTTDevice::subscribe(const std::string &topic, void (T::*callback)(), uint8_t qos) { + auto f = std::bind(callback, (T *) this); + global_mqtt_client->subscribe(topic, f, qos); +} +template +void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(const std::string &, JsonObject &), + uint8_t qos) { + auto f = std::bind(callback, (T *) this, std::placeholders::_1, std::placeholders::_2); + global_mqtt_client->subscribe_json(topic, f, qos); +} +template +void CustomMQTTDevice::subscribe_json(const std::string &topic, void (T::*callback)(JsonObject &), uint8_t qos) { + auto f = std::bind(callback, (T *) this, std::placeholders::_2); + global_mqtt_client->subscribe_json(topic, f, qos); +} + +} // namespace mqtt +} // namespace esphome diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index e63d6649b3..edabcb398c 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -19,15 +19,19 @@ void MQTTBinarySensorComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor) - : MQTTComponent(), binary_sensor_(binary_sensor) {} + : MQTTComponent(), binary_sensor_(binary_sensor) { + if (this->binary_sensor_->is_status_binary_sensor()) { + this->set_custom_state_topic(mqtt::global_mqtt_client->get_availability().topic); + } +} std::string MQTTBinarySensorComponent::friendly_name() const { return this->binary_sensor_->get_name(); } void MQTTBinarySensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig &config) { if (!this->binary_sensor_->get_device_class().empty()) root["device_class"] = this->binary_sensor_->get_device_class(); - if (this->is_status_) + if (this->binary_sensor_->is_status_binary_sensor()) root["payload_on"] = mqtt::global_mqtt_client->get_availability().payload_available; - if (this->is_status_) + if (this->binary_sensor_->is_status_binary_sensor()) root["payload_off"] = mqtt::global_mqtt_client->get_availability().payload_not_available; config.command_topic = false; } @@ -40,13 +44,12 @@ bool MQTTBinarySensorComponent::send_initial_state() { } bool MQTTBinarySensorComponent::is_internal() { return this->binary_sensor_->is_internal(); } bool MQTTBinarySensorComponent::publish_state(bool state) { - if (this->is_status_) + if (this->binary_sensor_->is_status_binary_sensor()) return true; const char *state_s = state ? "ON" : "OFF"; return this->publish(this->get_state_topic_(), state_s); } -void MQTTBinarySensorComponent::set_is_status(bool status) { this->is_status_ = status; } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 7793caec08..1ca82a947e 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -35,7 +35,6 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { std::string component_type() const override; binary_sensor::BinarySensor *binary_sensor_; - bool is_status_{false}; }; } // namespace mqtt diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 819dbd6119..e07204d559 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -15,7 +15,10 @@ namespace mqtt { static const char *TAG = "mqtt"; -MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; } +MQTTClientComponent::MQTTClientComponent() { + global_mqtt_client = this; + this->credentials_.client_id = App.get_name() + "-" + get_mac_address(); +} // Connection void MQTTClientComponent::setup() { @@ -357,18 +360,19 @@ bool MQTTClientComponent::publish(const std::string &topic, const char *payload, } bool logging_topic = topic == this->log_message_.topic; uint16_t ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); - yield(); + delay(0); if (ret == 0 && !logging_topic && this->is_connected()) { - delay(5); + delay(0); ret = this->mqtt_client_.publish(topic.c_str(), qos, retain, payload, payload_length); - yield(); + delay(0); } if (!logging_topic) { if (ret != 0) { ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d)", topic.c_str(), payload, retain); } else { - ESP_LOGW(TAG, "Publish failed for topic='%s' will retry later..", topic.c_str()); + ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). will retry later..", topic.c_str(), + payload_length); // NOLINT this->status_momentary_warning("publish", 1000); } } diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 8085fbf0f2..48b470cfb2 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -141,7 +141,21 @@ std::string MQTTClimateComponent::friendly_name() const { return this->device_-> bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); // mode - const char *mode_s = climate_mode_to_string(this->device_->mode); + const char *mode_s = ""; + switch (this->device_->mode) { + case CLIMATE_MODE_OFF: + mode_s = "off"; + break; + case CLIMATE_MODE_AUTO: + mode_s = "auto"; + break; + case CLIMATE_MODE_COOL: + mode_s = "cool"; + break; + case CLIMATE_MODE_HEAT: + mode_s = "heat"; + break; + } bool success = true; if (!this->publish(this->get_mode_state_topic(), mode_s)) success = false; diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 042e362f75..4201d41c44 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/helpers.h" +#include "esphome/core/version.h" namespace esphome { namespace mqtt { @@ -148,9 +149,6 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai } void MQTTComponent::disable_availability() { this->set_availability("", "", ""); } void MQTTComponent::call_setup() { - // Call component internal setup. - this->setup_internal_(); - if (this->is_internal()) return; @@ -172,8 +170,6 @@ void MQTTComponent::call_setup() { } void MQTTComponent::call_loop() { - this->loop_internal_(); - if (this->is_internal()) return; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 56d18a3d22..a414c261f0 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -73,6 +73,9 @@ void MQTTCoverComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCon root["tilt_status_topic"] = this->get_tilt_state_topic(); root["tilt_command_topic"] = this->get_tilt_command_topic(); } + if (traits.get_supports_tilt() && !traits.get_supports_position()) { + config.command_topic = false; + } } std::string MQTTCoverComponent::component_type() const { return "cover"; } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index e4c08c8e4e..37d475d25d 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,9 +15,6 @@ void MQTTTextSensor::send_discovery(JsonObject &root, mqtt::SendDiscoveryConfig if (!this->sensor_->get_icon().empty()) root["icon"] = this->sensor_->get_icon(); - if (!this->sensor_->unique_id().empty()) - root["unique_id"] = this->sensor_->unique_id(); - config.command_topic = false; } void MQTTTextSensor::setup() { @@ -40,6 +37,7 @@ bool MQTTTextSensor::send_initial_state() { bool MQTTTextSensor::is_internal() { return this->sensor_->is_internal(); } std::string MQTTTextSensor::component_type() const { return "sensor"; } std::string MQTTTextSensor::friendly_name() const { return this->sensor_->get_name(); } +std::string MQTTTextSensor::unique_id() { return this->sensor_->unique_id(); } } // namespace mqtt } // namespace esphome diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index 94afe30381..a5ce0658c7 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -31,6 +31,8 @@ class MQTTTextSensor : public mqtt::MQTTComponent { std::string friendly_name() const override; + std::string unique_id() override; + text_sensor::TextSensor *sensor_; }; diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index f07bdc80d7..e5106d4bd6 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -7,7 +7,7 @@ from esphome.const import CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_METHOD, CONF_NUM_L from esphome.core import CORE neopixelbus_ns = cg.esphome_ns.namespace('neopixelbus') -NeoPixelBusLightOutputBase = neopixelbus_ns.class_('NeoPixelBusLightOutputBase', cg.Component, +NeoPixelBusLightOutputBase = neopixelbus_ns.class_('NeoPixelBusLightOutputBase', light.AddressableLight) NeoPixelRGBLightOutput = neopixelbus_ns.class_('NeoPixelRGBLightOutput', NeoPixelBusLightOutputBase) NeoPixelRGBWLightOutput = neopixelbus_ns.class_('NeoPixelRGBWLightOutput', @@ -101,6 +101,14 @@ ESP8266_METHODS = { ESP32_METHODS = { 'ESP32_I2S_0': 'NeoEsp32I2s0{}Method', 'ESP32_I2S_1': 'NeoEsp32I2s1{}Method', + 'ESP32_RMT_0': 'NeoEsp32Rmt0{}Method', + 'ESP32_RMT_1': 'NeoEsp32Rmt1{}Method', + 'ESP32_RMT_2': 'NeoEsp32Rmt2{}Method', + 'ESP32_RMT_3': 'NeoEsp32Rmt3{}Method', + 'ESP32_RMT_4': 'NeoEsp32Rmt4{}Method', + 'ESP32_RMT_5': 'NeoEsp32Rmt5{}Method', + 'ESP32_RMT_6': 'NeoEsp32Rmt6{}Method', + 'ESP32_RMT_7': 'NeoEsp32Rmt7{}Method', 'BIT_BANG': 'NeoEsp32BitBang{}Method', } @@ -160,4 +168,5 @@ def to_code(config): cg.add(var.set_pixel_order(getattr(ESPNeoPixelOrder, config[CONF_TYPE]))) - cg.add_library('NeoPixelBus', '2.4.1') + # https://github.com/Makuna/NeoPixelBus/blob/master/library.json + cg.add_library('NeoPixelBus-esphome', '2.5.2') diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 25e10d9bbd..5e8097187e 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -48,7 +48,7 @@ enum class ESPNeoPixelOrder { }; template -class NeoPixelBusLightOutputBase : public Component, public light::AddressableLight { +class NeoPixelBusLightOutputBase : public light::AddressableLight { public: NeoPixelBus *get_controller() const { return this->controller_; } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index f41a97ce7e..e594e147f4 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -46,7 +46,7 @@ void Nextion::set_component_value(const char *component, int value) { this->send_command_printf("%s.val=%d", component, value); } void Nextion::display_picture(int picture_id, int x_start, int y_start) { - this->send_command_printf("pic %d %d %d", picture_id, x_start, y_start); + this->send_command_printf("pic %d %d %d", x_start, y_start, picture_id); } void Nextion::set_component_background_color(const char *component, const char *color) { this->send_command_printf("%s.bco=\"%s\"", component, color); diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index e290e57baf..869de777d6 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -22,6 +22,8 @@ def to_code(config): cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_auth_password(config[CONF_PASSWORD])) + yield cg.register_component(var, config) + if config[CONF_SAFE_MODE]: cg.add(var.start_safe_mode()) @@ -29,6 +31,3 @@ def to_code(config): cg.add_library('Update', None) elif CORE.is_esp32: cg.add_library('Hash', None) - - # Register at end for safe mode - yield cg.register_component(var, config) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index d37a7a0c6a..6d2a0dd7c3 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -182,11 +182,11 @@ void OTAComponent::handle_() { error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING; goto error; } - if (ss.indexOf("new Flash config wrong") != -1) { + if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) { error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG; goto error; } - if (ss.indexOf("Flash config wrong real") != -1) { + if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) { error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG; goto error; } @@ -358,7 +358,7 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) { this->safe_mode_start_time_ = millis(); this->safe_mode_enable_time_ = enable_time; this->safe_mode_num_attempts_ = num_attempts; - this->rtc_ = global_preferences.make_preference(233825507UL); + this->rtc_ = global_preferences.make_preference(233825507UL, false); this->safe_mode_rtc_value_ = this->read_rtc_(); ESP_LOGCONFIG(TAG, "There have been %u suspected unsuccessful boot attempts.", this->safe_mode_rtc_value_); @@ -369,19 +369,18 @@ void OTAComponent::start_safe_mode(uint8_t num_attempts, uint32_t enable_time) { ESP_LOGE(TAG, "Boot loop detected. Proceeding to safe mode."); this->status_set_error(); - network_setup(); - this->call_setup(); + this->set_timeout(enable_time, []() { + ESP_LOGE(TAG, "No OTA attempt made, restarting."); + App.reboot(); + }); + + App.setup(); ESP_LOGI(TAG, "Waiting for OTA attempt."); - uint32_t begin = millis(); - while ((millis() - begin) < enable_time) { - this->call_loop(); - network_tick(); - App.feed_wdt(); - yield(); + + while (true) { + App.loop(); } - ESP_LOGE(TAG, "No OTA attempt made, restarting."); - App.reboot(); } else { // increment counter this->write_rtc_(this->safe_mode_rtc_value_ + 1); diff --git a/esphome/components/partition/light.py b/esphome/components/partition/light.py index cfc709854f..62434e7a19 100644 --- a/esphome/components/partition/light.py +++ b/esphome/components/partition/light.py @@ -33,4 +33,5 @@ def to_code(config): conf[CONF_TO] - conf[CONF_FROM] + 1)) var = cg.new_Pvariable(config[CONF_OUTPUT_ID], segments) + yield cg.register_component(var, config) yield light.register_light(var, config) diff --git a/esphome/components/partition/light_partition.h b/esphome/components/partition/light_partition.h index b68e3404f1..8085c43720 100644 --- a/esphome/components/partition/light_partition.h +++ b/esphome/components/partition/light_partition.h @@ -24,7 +24,7 @@ class AddressableSegment { int32_t dst_offset_; }; -class PartitionLightOutput : public light::AddressableLight, public Component { +class PartitionLightOutput : public light::AddressableLight { public: explicit PartitionLightOutput(std::vector segments) : segments_(segments) { int32_t off = 0; diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index d469cf835f..50922e2f48 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -55,8 +55,6 @@ void PCF8574Component::pin_mode(uint8_t pin, uint8_t mode) { default: break; } - - this->write_gpio_(); } bool PCF8574Component::read_gpio_() { if (this->is_failed()) diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 07a41444ce..93000a7421 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -335,7 +335,6 @@ bool PN532::wait_ready_() { return true; } -bool PN532::is_device_msb_first() { return false; } void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index d349c7a150..49d5878265 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -11,7 +11,9 @@ namespace pn532 { class PN532BinarySensor; class PN532Trigger; -class PN532 : public PollingComponent, public spi::SPIDevice { +class PN532 : public PollingComponent, + public spi::SPIDevice { public: void setup() override; @@ -26,8 +28,6 @@ class PN532 : public PollingComponent, public spi::SPIDevice { void register_trigger(PN532Trigger *trig) { this->triggers_.push_back(trig); } protected: - bool is_device_msb_first() override; - /// Write the full command given in data to the PN532 void pn532_write_command_(const std::vector &data); bool pn532_write_command_check_ack_(const std::vector &data); diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index 636147c138..e73bc36036 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import sensor from esphome.const import CONF_COUNT_MODE, CONF_FALLING_EDGE, CONF_ID, CONF_INTERNAL_FILTER, \ - CONF_PIN, CONF_RISING_EDGE, CONF_UPDATE_INTERVAL, CONF_NUMBER, \ + CONF_PIN, CONF_RISING_EDGE, CONF_NUMBER, \ ICON_PULSE, UNIT_PULSES_PER_MINUTE from esphome.core import CORE @@ -49,7 +49,6 @@ CONFIG_SCHEMA = sensor.sensor_schema(UNIT_PULSES_PER_MINUTE, ICON_PULSE, 2).exte cv.Required(CONF_FALLING_EDGE): COUNT_MODE_SCHEMA, }), cv.Optional(CONF_INTERNAL_FILTER, default='13us'): validate_internal_filter, - cv.Optional(CONF_UPDATE_INTERVAL, default='60s'): cv.update_interval, }).extend(cv.polling_component_schema('60s')) diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 8b6f5b1623..a62304c87d 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import binary_sensor -from esphome.const import CONF_DATA, CONF_ID, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, \ +from esphome.const import CONF_DATA, CONF_TRIGGER_ID, CONF_NBITS, CONF_ADDRESS, \ CONF_COMMAND, CONF_CODE, CONF_PULSE_LENGTH, CONF_SYNC, CONF_ZERO, CONF_ONE, CONF_INVERTED, \ CONF_PROTOCOL, CONF_GROUP, CONF_DEVICE, CONF_STATE, CONF_CHANNEL, CONF_FAMILY, CONF_REPEAT, \ CONF_WAIT_TIME, CONF_TIMES, CONF_TYPE_ID, CONF_CARRIER_FREQUENCY @@ -83,14 +83,20 @@ def register_dumper(name, type): return decorator +def validate_repeat(value): + if isinstance(value, dict): + return cv.Schema({ + cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), + cv.Optional(CONF_WAIT_TIME, default='10ms'): + cv.templatable(cv.positive_time_period_microseconds), + })(value) + return validate_repeat({CONF_TIMES: value}) + + def register_action(name, type_, schema): validator = templatize(schema).extend({ cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterBase), - cv.Optional(CONF_REPEAT): cv.Schema({ - cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), - cv.Optional(CONF_WAIT_TIME, default='10ms'): - cv.templatable(cv.positive_time_period_milliseconds), - }), + cv.Optional(CONF_REPEAT): validate_repeat, }) registerer = automation.register_action('remote_transmitter.transmit_{}'.format(name), type_, validator) @@ -244,7 +250,7 @@ def lg_dumper(var, config): def lg_action(var, config, args): template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint8) + template_ = yield cg.templatable(config[CONF_NBITS], args, cg.uint8) cg.add(var.set_nbits(template_)) @@ -344,7 +350,7 @@ RAW_SCHEMA = cv.Schema({ @register_binary_sensor('raw', RawBinarySensor, RAW_SCHEMA) def raw_binary_sensor(var, config): code_ = config[CONF_CODE] - arr = cg.progmem_array(config[CONF_ID], code_) + arr = cg.progmem_array(config[CONF_CODE_STORAGE_ID], code_) cg.add(var.set_data(arr)) cg.add(var.set_len(len(code_))) @@ -414,7 +420,7 @@ def rc5_action(var, config, args): RC_SWITCH_TIMING_SCHEMA = cv.All([cv.uint8_t], cv.Length(min=2, max=2)) RC_SWITCH_PROTOCOL_SCHEMA = cv.Any( - cv.int_range(min=1, max=7), + cv.int_range(min=1, max=8), cv.Schema({ cv.Required(CONF_PULSE_LENGTH): cv.uint32_t, cv.Optional(CONF_SYNC, default=[1, 31]): RC_SWITCH_TIMING_SCHEMA, @@ -432,14 +438,30 @@ def validate_rc_switch_code(value): if c not in ('0', '1'): raise cv.Invalid(u"Invalid RCSwitch code character '{}'. Only '0' and '1' are allowed" u"".format(c)) - if len(value) > 32: - raise cv.Invalid("Maximum length for RCSwitch codes is 32, code '{}' has length {}" + if len(value) > 64: + raise cv.Invalid("Maximum length for RCSwitch codes is 64, code '{}' has length {}" "".format(value, len(value))) if not value: raise cv.Invalid("RCSwitch code must not be empty") return value +def validate_rc_switch_raw_code(value): + if not isinstance(value, (str, text_type)): + raise cv.Invalid("All RCSwitch raw codes must be in quotes ('')") + for c in value: + if c not in ('0', '1', 'x'): + raise cv.Invalid( + "Invalid RCSwitch raw code character '{}'.Only '0', '1' and 'x' are allowed" + .format(c)) + if len(value) > 64: + raise cv.Invalid("Maximum length for RCSwitch raw codes is 64, code '{}' has length {}" + "".format(value, len(value))) + if not value: + raise cv.Invalid("RCSwitch raw code must not be empty") + return value + + def build_rc_switch_protocol(config): if isinstance(config, int): return rc_switch_protocols[config] @@ -451,7 +473,7 @@ def build_rc_switch_protocol(config): RC_SWITCH_RAW_SCHEMA = cv.Schema({ - cv.Required(CONF_CODE): validate_rc_switch_code, + cv.Required(CONF_CODE): validate_rc_switch_raw_code, cv.Optional(CONF_PROTOCOL, default=1): RC_SWITCH_PROTOCOL_SCHEMA, }) RC_SWITCH_TYPE_A_SCHEMA = cv.Schema({ @@ -484,7 +506,7 @@ RC_SWITCH_TRANSMITTER = cv.Schema({ cv.Optional(CONF_REPEAT, default={CONF_TIMES: 5}): cv.Schema({ cv.Required(CONF_TIMES): cv.templatable(cv.positive_int), cv.Optional(CONF_WAIT_TIME, default='10ms'): - cv.templatable(cv.positive_time_period_milliseconds), + cv.templatable(cv.positive_time_period_microseconds), }), }) @@ -618,7 +640,7 @@ def samsung_dumper(var, config): @register_action('samsung', SamsungAction, SAMSUNG_SCHEMA) def samsung_action(var, config, args): - template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint16) + template_ = yield cg.templatable(config[CONF_DATA], args, cg.uint32) cg.add(var.set_data(template_)) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index d983ffeb23..754b2fae49 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -6,14 +6,15 @@ namespace remote_base { static const char *TAG = "remote.rc_switch"; -RCSwitchBase rc_switch_protocols[8] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), +RCSwitchBase rc_switch_protocols[9] = {RCSwitchBase(0, 0, 0, 0, 0, 0, false), RCSwitchBase(350, 10850, 350, 1050, 1050, 350, false), RCSwitchBase(650, 6500, 650, 1300, 1300, 650, false), RCSwitchBase(3000, 7100, 400, 1100, 900, 600, false), RCSwitchBase(380, 2280, 380, 1140, 1140, 380, false), RCSwitchBase(3000, 7000, 500, 1000, 1000, 500, false), RCSwitchBase(10350, 450, 450, 900, 900, 450, true), - RCSwitchBase(300, 9300, 150, 900, 900, 150, false)}; + RCSwitchBase(300, 9300, 150, 900, 900, 150, false), + RCSwitchBase(250, 2500, 250, 1250, 250, 250, false)}; RCSwitchBase::RCSwitchBase(uint32_t sync_high, uint32_t sync_low, uint32_t zero_high, uint32_t zero_low, uint32_t one_high, uint32_t one_low, bool inverted) @@ -52,7 +53,7 @@ void RCSwitchBase::sync(RemoteTransmitData *dst) const { dst->mark(this->sync_low_); } } -void RCSwitchBase::transmit(RemoteTransmitData *dst, uint32_t code, uint8_t len) const { +void RCSwitchBase::transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const { dst->set_carrier_frequency(0); for (int16_t i = len - 1; i >= 0; i--) { if (code & (1 << i)) @@ -108,12 +109,12 @@ bool RCSwitchBase::expect_sync(RemoteReceiveData &src) const { src.advance(2); return true; } -bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *out_nbits) const { +bool RCSwitchBase::decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const { // ignore if sync doesn't exist this->expect_sync(src); *out_data = 0; - for (*out_nbits = 1; *out_nbits < 32; *out_nbits += 1) { + for (*out_nbits = 0; *out_nbits < 64; *out_nbits += 1) { if (this->expect_zero(src)) { *out_data <<= 1; *out_data |= 0; @@ -121,14 +122,13 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *o *out_data <<= 1; *out_data |= 1; } else { - *out_nbits -= 1; return *out_nbits >= 8; } } return true; } -void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_t *out_code) { +void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code) { *out_code = 0; for (int8_t i = nbits - 1; i >= 0; i--) { *out_code <<= 2; @@ -138,24 +138,18 @@ void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_ *out_code |= 0b00; } } -void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint32_t *out_code, +void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code, uint8_t *out_nbits) { uint16_t code = 0; - code |= (switch_group & 0b0001) ? 0 : 0b1000; - code |= (switch_group & 0b0010) ? 0 : 0b0100; - code |= (switch_group & 0b0100) ? 0 : 0b0010; - code |= (switch_group & 0b1000) ? 0 : 0b0001; - code <<= 4; - code |= (switch_device & 0b0001) ? 0 : 0b1000; - code |= (switch_device & 0b0010) ? 0 : 0b0100; - code |= (switch_device & 0b0100) ? 0 : 0b0010; - code |= (switch_device & 0b1000) ? 0 : 0b0001; + code = switch_group ^ 0b11111; + code <<= 5; + code |= switch_device ^ 0b11111; code <<= 2; code |= state ? 0b01 : 0b10; - simple_code_to_tristate(code, 10, out_code); - *out_nbits = 20; + simple_code_to_tristate(code, 12, out_code); + *out_nbits = 24; } -void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint32_t *out_code, +void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint64_t *out_code, uint8_t *out_nbits) { uint16_t code = 0; code |= (address_code == 1) ? 0 : 0b1000; @@ -173,7 +167,7 @@ void RCSwitchBase::type_b_code(uint8_t address_code, uint8_t channel_code, bool simple_code_to_tristate(code, 12, out_code); *out_nbits = 24; } -void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint32_t *out_code, +void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits) { uint16_t code = 0; code |= (family & 0b0001) ? 0b1000 : 0; @@ -191,7 +185,7 @@ void RCSwitchBase::type_c_code(uint8_t family, uint8_t group, uint8_t device, bo simple_code_to_tristate(code, 12, out_code); *out_nbits = 24; } -void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint32_t *out_code, uint8_t *out_nbits) { +void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits) { *out_code = 0; *out_code |= (group == 0) ? 0b11000000 : 0b01000000; *out_code |= (group == 1) ? 0b00110000 : 0b00010000; @@ -208,8 +202,8 @@ void RCSwitchBase::type_d_code(uint8_t group, uint8_t device, bool state, uint32 *out_nbits = 24; } -uint32_t decode_binary_string(const std::string &data) { - uint32_t ret = 0; +uint64_t decode_binary_string(const std::string &data) { + uint64_t ret = 0; for (char c : data) { ret <<= 1UL; ret |= (c != '0'); @@ -217,22 +211,31 @@ uint32_t decode_binary_string(const std::string &data) { return ret; } +uint64_t decode_binary_string_mask(const std::string &data) { + uint64_t ret = 0; + for (char c : data) { + ret <<= 1UL; + ret |= (c != 'x'); + } + return ret; +} + bool RCSwitchRawReceiver::matches(RemoteReceiveData src) { - uint32_t decoded_code; + uint64_t decoded_code; uint8_t decoded_nbits; if (!this->protocol_.decode(src, &decoded_code, &decoded_nbits)) return false; - return decoded_nbits == this->nbits_ && decoded_code == this->code_; + return decoded_nbits == this->nbits_ && (decoded_code & this->mask_) == (this->code_ & this->mask_); } bool RCSwitchDumper::dump(RemoteReceiveData src) { - for (uint8_t i = 1; i <= 7; i++) { + for (uint8_t i = 1; i <= 8; i++) { src.reset(); - uint32_t out_data; + uint64_t out_data; uint8_t out_nbits; RCSwitchBase *protocol = &rc_switch_protocols[i]; if (protocol->decode(src, &out_data, &out_nbits) && out_nbits >= 3) { - char buffer[32]; + char buffer[65]; for (uint8_t j = 0; j < out_nbits; j++) buffer[j] = (out_data & (1 << (out_nbits - j - 1))) ? '1' : '0'; diff --git a/esphome/components/remote_base/rc_switch_protocol.h b/esphome/components/remote_base/rc_switch_protocol.h index 728561c140..0983da27ea 100644 --- a/esphome/components/remote_base/rc_switch_protocol.h +++ b/esphome/components/remote_base/rc_switch_protocol.h @@ -18,7 +18,7 @@ class RCSwitchBase { void sync(RemoteTransmitData *dst) const; - void transmit(RemoteTransmitData *dst, uint32_t code, uint8_t len) const; + void transmit(RemoteTransmitData *dst, uint64_t code, uint8_t len) const; bool expect_one(RemoteReceiveData &src) const; @@ -26,20 +26,20 @@ class RCSwitchBase { bool expect_sync(RemoteReceiveData &src) const; - bool decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *out_nbits) const; + bool decode(RemoteReceiveData &src, uint64_t *out_data, uint8_t *out_nbits) const; - static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint32_t *out_code); + static void simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_t *out_code); - static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint32_t *out_code, + static void type_a_code(uint8_t switch_group, uint8_t switch_device, bool state, uint64_t *out_code, uint8_t *out_nbits); - static void type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint32_t *out_code, + static void type_b_code(uint8_t address_code, uint8_t channel_code, bool state, uint64_t *out_code, uint8_t *out_nbits); - static void type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint32_t *out_code, + static void type_c_code(uint8_t family, uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits); - static void type_d_code(uint8_t group, uint8_t device, bool state, uint32_t *out_code, uint8_t *out_nbits); + static void type_d_code(uint8_t group, uint8_t device, bool state, uint64_t *out_code, uint8_t *out_nbits); protected: uint32_t sync_high_{}; @@ -51,9 +51,11 @@ class RCSwitchBase { bool inverted_{}; }; -extern RCSwitchBase rc_switch_protocols[8]; +extern RCSwitchBase rc_switch_protocols[9]; -uint32_t decode_binary_string(const std::string &data); +uint64_t decode_binary_string(const std::string &data); + +uint64_t decode_binary_string_mask(const std::string &data); template class RCSwitchRawAction : public RemoteTransmitterActionBase { public: @@ -62,7 +64,7 @@ template class RCSwitchRawAction : public RemoteTransmitterActio void encode(RemoteTransmitData *dst, Ts... x) override { auto code = this->code_.value(x...); - uint32_t the_code = decode_binary_string(code); + uint64_t the_code = decode_binary_string(code); uint8_t nbits = code.size(); auto proto = this->protocol_.value(x...); @@ -84,7 +86,7 @@ template class RCSwitchTypeAAction : public RemoteTransmitterAct uint8_t u_group = decode_binary_string(group); uint8_t u_device = decode_binary_string(device); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_a_code(u_group, u_device, state, &code, &nbits); @@ -105,7 +107,7 @@ template class RCSwitchTypeBAction : public RemoteTransmitterAct auto channel = this->channel_.value(x...); auto state = this->state_.value(x...); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_b_code(address, channel, state, &code, &nbits); @@ -130,7 +132,7 @@ template class RCSwitchTypeCAction : public RemoteTransmitterAct auto u_family = static_cast(tolower(family[0]) - 'a'); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_c_code(u_family, group, device, state, &code, &nbits); @@ -152,7 +154,7 @@ template class RCSwitchTypeDAction : public RemoteTransmitterAct auto u_group = static_cast(tolower(group[0]) - 'a'); - uint32_t code; + uint64_t code; uint8_t nbits; RCSwitchBase::type_d_code(u_group, device, state, &code, &nbits); @@ -164,9 +166,10 @@ template class RCSwitchTypeDAction : public RemoteTransmitterAct class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase { public: void set_protocol(const RCSwitchBase &a_protocol) { this->protocol_ = a_protocol; } - void set_code(uint32_t code) { this->code_ = code; } + void set_code(uint64_t code) { this->code_ = code; } void set_code(const std::string &code) { this->code_ = decode_binary_string(code); + this->mask_ = decode_binary_string_mask(code); this->nbits_ = code.size(); } void set_nbits(uint8_t nbits) { this->nbits_ = nbits; } @@ -191,7 +194,8 @@ class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase { bool matches(RemoteReceiveData src) override; RCSwitchBase protocol_; - uint32_t code_; + uint64_t code_; + uint64_t mask_{0xFFFFFFFFFFFFFFFF}; uint8_t nbits_; }; diff --git a/esphome/components/remote_base/remote_base.h b/esphome/components/remote_base/remote_base.h index d8eb1be356..6035e2fd57 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -293,7 +293,7 @@ template class RemoteReceiverBinarySensor : public Remot bool matches(RemoteReceiveData src) override { auto proto = T(); auto res = proto.decode(src); - return res.has_value(); + return res.has_value() && *res == this->data_; } public: diff --git a/esphome/components/remote_base/sony_protocol.cpp b/esphome/components/remote_base/sony_protocol.cpp index 2b50845f32..97318f6608 100644 --- a/esphome/components/remote_base/sony_protocol.cpp +++ b/esphome/components/remote_base/sony_protocol.cpp @@ -31,7 +31,7 @@ optional SonyProtocol::decode(RemoteReceiveData src) { .nbits = 0, }; if (!src.expect_item(HEADER_HIGH_US, HEADER_LOW_US)) - return out; + return {}; for (; out.nbits < 20; out.nbits++) { uint32_t bit; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index d88329d064..949a301661 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -101,15 +101,15 @@ void RotaryEncoderSensor::setup() { ESP_LOGCONFIG(TAG, "Setting up Rotary Encoder '%s'...", this->name_.c_str()); this->pin_a_->setup(); this->store_.pin_a = this->pin_a_->to_isr(); - this->pin_a_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, CHANGE); - this->pin_b_->setup(); this->store_.pin_b = this->pin_b_->to_isr(); - this->pin_b_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, CHANGE); if (this->pin_i_ != nullptr) { this->pin_i_->setup(); } + + this->pin_a_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, CHANGE); + this->pin_b_->attach_interrupt(RotaryEncoderSensorStore::gpio_intr, &this->store_, CHANGE); } void RotaryEncoderSensor::dump_config() { LOG_SENSOR("", "Rotary Encoder", this); diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index b627a4e57f..4220645478 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include "esphome/core/automation.h" #include "esphome/components/sensor/sensor.h" namespace esphome { @@ -43,6 +44,12 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { */ void set_resolution(RotaryEncoderResolution mode); + /// Manually set the value of the counter. + void set_value(int value) { + this->store_.counter = value; + this->loop(); + } + void set_reset_pin(GPIOPin *pin_i) { this->pin_i_ = pin_i; } void set_min_value(int32_t min_value); void set_max_value(int32_t max_value); @@ -63,5 +70,15 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { RotaryEncoderSensorStore store_{}; }; +template class RotaryEncoderSetValueAction : public Action { + public: + RotaryEncoderSetValueAction(RotaryEncoderSensor *encoder) : encoder_(encoder) {} + TEMPLATABLE_VALUE(int, value) + void play(Ts... x) override { this->encoder_->set_value(this->value_.value(x...)); } + + protected: + RotaryEncoderSensor *encoder_; +}; + } // namespace rotary_encoder } // namespace esphome diff --git a/esphome/components/rotary_encoder/sensor.py b/esphome/components/rotary_encoder/sensor.py index 3eb2a5ca45..742096cd5a 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -1,9 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import pins, automation from esphome.components import sensor from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \ - ICON_ROTATE_RIGHT + ICON_ROTATE_RIGHT, CONF_VALUE rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') @@ -18,6 +18,8 @@ CONF_PIN_B = 'pin_b' CONF_PIN_RESET = 'pin_reset' RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component) +RotaryEncoderSetValueAction = rotary_encoder_ns.class_('RotaryEncoderSetValueAction', + automation.Action) def validate_min_max_value(config): @@ -36,7 +38,7 @@ CONFIG_SCHEMA = cv.All(sensor.sensor_schema(UNIT_STEPS, ICON_ROTATE_RIGHT, 0).ex pins.validate_has_interrupt), cv.Required(CONF_PIN_B): cv.All(pins.internal_gpio_input_pin_schema, pins.validate_has_interrupt), - cv.Optional(CONF_PIN_RESET): pins.internal_gpio_input_pin_schema, + cv.Optional(CONF_PIN_RESET): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_RESOLUTION, default=1): cv.enum(RESOLUTIONS, int=True), cv.Optional(CONF_MIN_VALUE): cv.int_, cv.Optional(CONF_MAX_VALUE): cv.int_, @@ -50,7 +52,7 @@ def to_code(config): pin_a = yield cg.gpio_pin_expression(config[CONF_PIN_A]) cg.add(var.set_pin_a(pin_a)) pin_b = yield cg.gpio_pin_expression(config[CONF_PIN_B]) - cg.add(var.set_pin_a(pin_b)) + cg.add(var.set_pin_b(pin_b)) if CONF_PIN_RESET in config: pin_i = yield cg.gpio_pin_expression(config[CONF_PIN_RESET]) @@ -60,3 +62,16 @@ def to_code(config): cg.add(var.set_min_value(config[CONF_MIN_VALUE])) if CONF_MAX_VALUE in config: cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + + +@automation.register_action('sensor.rotary_encoder.set_value', RotaryEncoderSetValueAction, + cv.Schema({ + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_VALUE): cv.templatable(cv.int_), + })) +def sensor_template_publish_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_VALUE], args, int) + cg.add(var.set_value(template_)) + yield var diff --git a/esphome/components/scd30/__init__.py b/esphome/components/scd30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp new file mode 100644 index 0000000000..0b0e08387d --- /dev/null +++ b/esphome/components/scd30/scd30.cpp @@ -0,0 +1,182 @@ +#include "scd30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace scd30 { + +static const char *TAG = "scd30"; + +static const uint16_t SCD30_CMD_GET_FIRMWARE_VERSION = 0xd100; +static const uint16_t SCD30_CMD_START_CONTINUOUS_MEASUREMENTS = 0x0010; +static const uint16_t SCD30_CMD_GET_DATA_READY_STATUS = 0x0202; +static const uint16_t SCD30_CMD_READ_MEASUREMENT = 0x0300; + +/// Commands for future use +static const uint16_t SCD30_CMD_STOP_MEASUREMENTS = 0x0104; +static const uint16_t SCD30_CMD_MEASUREMENT_INTERVAL = 0x4600; +static const uint16_t SCD30_CMD_AUTOMATIC_SELF_CALIBRATION = 0x5306; +static const uint16_t SCD30_CMD_FORCED_CALIBRATION = 0x5204; +static const uint16_t SCD30_CMD_TEMPERATURE_OFFSET = 0x5403; +static const uint16_t SCD30_CMD_ALTITUDE_COMPENSATION = 0x5102; +static const uint16_t SCD30_CMD_SOFT_RESET = 0xD304; + +void SCD30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up scd30..."); + + /// Firmware version identification + if (!this->write_command_(SCD30_CMD_GET_FIRMWARE_VERSION)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + uint16_t raw_firmware_version[3]; + + if (!this->read_data_(raw_firmware_version, 3)) { + this->error_code_ = FIRMWARE_IDENTIFICATION_FAILED; + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "SCD30 Firmware v%0d.%02d", (uint16_t(raw_firmware_version[0]) >> 8), + uint16_t(raw_firmware_version[0] & 0xFF)); + + /// Sensor initialization + if (!this->write_command_(SCD30_CMD_START_CONTINUOUS_MEASUREMENTS)) { + ESP_LOGE(TAG, "Sensor SCD30 error starting continuous measurements."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } +} + +void SCD30Component::dump_config() { + ESP_LOGCONFIG(TAG, "scd30:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + switch (this->error_code_) { + case COMMUNICATION_FAILED: + ESP_LOGW(TAG, "Communication failed! Is the sensor connected?"); + break; + case MEASUREMENT_INIT_FAILED: + ESP_LOGW(TAG, "Measurement Initialization failed!"); + break; + case FIRMWARE_IDENTIFICATION_FAILED: + ESP_LOGW(TAG, "Unable to read sensor firmware version"); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} + +void SCD30Component::update() { + /// Check if measurement is ready before reading the value + if (!this->write_command_(SCD30_CMD_GET_DATA_READY_STATUS)) { + this->status_set_warning(); + return; + } + + uint16_t raw_read_status[1]; + if (!this->read_data_(raw_read_status, 1) || raw_read_status[0] == 0x00) { + this->status_set_warning(); + ESP_LOGW(TAG, "Data not ready yet!"); + return; + } + + if (!this->write_command_(SCD30_CMD_READ_MEASUREMENT)) { + ESP_LOGW(TAG, "Error reading measurement!"); + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[6]; + if (!this->read_data_(raw_data, 6)) { + this->status_set_warning(); + return; + } + + union uint32_float_t { + uint32_t uint32; + float value; + }; + uint32_t temp_c_o2_u32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1]))); + uint32_float_t co2{.uint32 = temp_c_o2_u32}; + + uint32_t temp_temp_u32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3]))); + uint32_float_t temperature{.uint32 = temp_temp_u32}; + + uint32_t temp_hum_u32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5]))); + uint32_float_t humidity{.uint32 = temp_hum_u32}; + + ESP_LOGD(TAG, "Got CO2=%.2fppm temperature=%.2f°C humidity=%.2f%%", + co2.value, temperature.value, humidity.value); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(co2.value); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature.value); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity.value); + + this->status_clear_warning(); + }); +} + +bool SCD30Component::write_command_(uint16_t command) { + // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit. + return this->write_byte(command >> 8, command & 0xFF); +} + +uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) { + uint8_t bit; + uint8_t crc = 0xFF; + + crc ^= data1; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + crc ^= data2; + for (bit = 8; bit > 0; --bit) { + if (crc & 0x80) + crc = (crc << 1) ^ 0x131; + else + crc = (crc << 1); + } + + return crc; +} + +bool SCD30Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + auto *buf = new uint8_t[num_bytes]; + + if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) { + delete[](buf); + return false; + } + + for (uint8_t i = 0; i < len; i++) { + const uint8_t j = 3 * i; + uint8_t crc = sht_crc_(buf[j], buf[j + 1]); + if (crc != buf[j + 2]) { + ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc); + delete[](buf); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + delete[](buf); + return true; +} + +} // namespace scd30 +} // namespace esphome diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h new file mode 100644 index 0000000000..999e66414d --- /dev/null +++ b/esphome/components/scd30/scd30.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace scd30 { + +/// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors. +class SCD30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; } + void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } + void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } + + void setup() override; + void update() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + + enum ErrorCode { + COMMUNICATION_FAILED, + FIRMWARE_IDENTIFICATION_FAILED, + MEASUREMENT_INIT_FAILED, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *co2_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; +}; + +} // namespace scd30 +} // namespace esphome diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py new file mode 100644 index 0000000000..aa863b904a --- /dev/null +++ b/esphome/components/scd30/sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \ + CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \ + UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT + +DEPENDENCIES = ['i2c'] + +scd30_ns = cg.esphome_ns.namespace('scd30') +SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) + +CONF_CO2 = 'co2' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SCD30Component), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, + ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/script/__init__.py b/esphome/components/script/__init__.py index 962e2d56ca..e1983689a6 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -16,8 +16,13 @@ CONFIG_SCHEMA = automation.validate_automation({ def to_code(config): + # Register all variables first, so that scripts can use other scripts + triggers = [] for conf in config: trigger = cg.new_Pvariable(conf[CONF_ID]) + triggers.append((trigger, conf)) + + for trigger, conf in triggers: yield automation.build_automation(trigger, [], conf) diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 1abb6210ce..6ca414c55d 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -94,7 +94,6 @@ float SDS011Component::get_setup_priority() const { return setup_priority::DATA; void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void SDS011Component::sds011_write_command_(const uint8_t *command_data) { - this->flush(); this->write_byte(SDS011_MSG_HEAD); this->write_byte(SDS011_COMMAND_ID_REQUEST); this->write_array(command_data, SDS011_DATA_REQUEST_LENGTH); diff --git a/esphome/components/senseair/__init__.py b/esphome/components/senseair/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp new file mode 100644 index 0000000000..96f456282f --- /dev/null +++ b/esphome/components/senseair/senseair.cpp @@ -0,0 +1,79 @@ +#include "senseair.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace senseair { + +static const char *TAG = "senseair"; +static const uint8_t SENSEAIR_REQUEST_LENGTH = 8; +static const uint8_t SENSEAIR_RESPONSE_LENGTH = 13; +static const uint8_t SENSEAIR_COMMAND_GET_PPM[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6}; + +void SenseAirComponent::update() { + uint8_t response[SENSEAIR_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM, response)) { + ESP_LOGW(TAG, "Reading data from SenseAir failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0xFE || response[1] != 0x04) { + ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + this->status_set_warning(); + return; + } + + uint16_t calc_checksum = this->senseair_checksum_(response, 11); + uint16_t resp_checksum = (uint16_t(response[12]) << 8) | response[11]; + if (resp_checksum != calc_checksum) { + ESP_LOGW(TAG, "SenseAir checksum doesn't match: 0x%02X!=0x%02X", resp_checksum, calc_checksum); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + const uint8_t length = response[2]; + const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; + const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2]; + + ESP_LOGD(TAG, "SenseAir Received COâ‚‚=%uppm Status=0x%02X", ppm, status); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); +} + +uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { + uint16_t crc = 0xFFFF; + uint8_t i; + while (length--) { + crc ^= *ptr++; + for (i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response) { + this->flush(); + this->write_array(command, SENSEAIR_REQUEST_LENGTH); + + if (response == nullptr) + return true; + + bool ret = this->read_array(response, SENSEAIR_RESPONSE_LENGTH); + this->flush(); + return ret; +} + +void SenseAirComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SenseAir:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); +} + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h new file mode 100644 index 0000000000..23bcf40b5a --- /dev/null +++ b/esphome/components/senseair/senseair.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace senseair { + +class SenseAirComponent : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + + void update() override; + void dump_config() override; + + protected: + uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length); + bool senseair_write_command_(const uint8_t *command, uint8_t *response); + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py new file mode 100644 index 0000000000..393bfd5182 --- /dev/null +++ b/esphome/components/senseair/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION + +DEPENDENCIES = ['uart'] + +senseair_ns = cg.esphome_ns.namespace('senseair') +SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SenseAirComponent), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 43f0cefd56..a9b00b7c08 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -61,6 +61,7 @@ SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action) # Filters Filter = sensor_ns.class_('Filter') +MedianFilter = sensor_ns.class_('MedianFilter', Filter) SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter) ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter) LambdaFilter = sensor_ns.class_('LambdaFilter', Filter) @@ -73,6 +74,7 @@ HeartbeatFilter = sensor_ns.class_('HeartbeatFilter', Filter, cg.Component) DeltaFilter = sensor_ns.class_('DeltaFilter', Filter) OrFilter = sensor_ns.class_('OrFilter', Filter) CalibrateLinearFilter = sensor_ns.class_('CalibrateLinearFilter', Filter) +CalibratePolynomialFilter = sensor_ns.class_('CalibratePolynomialFilter', Filter) SensorInRangeCondition = sensor_ns.class_('SensorInRangeCondition', Filter) unit_of_measurement = cv.string_strict @@ -126,6 +128,19 @@ def filter_out_filter_to_code(config, filter_id): yield cg.new_Pvariable(filter_id, config) +MEDIAN_SCHEMA = cv.All(cv.Schema({ + cv.Optional(CONF_WINDOW_SIZE, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_EVERY, default=5): cv.positive_not_null_int, + cv.Optional(CONF_SEND_FIRST_AT, default=1): cv.positive_not_null_int, +}), validate_send_first_at) + + +@FILTER_REGISTRY.register('median', MedianFilter, MEDIAN_SCHEMA) +def median_filter_to_code(config, filter_id): + yield cg.new_Pvariable(filter_id, config[CONF_WINDOW_SIZE], config[CONF_SEND_EVERY], + config[CONF_SEND_FIRST_AT]) + + SLIDING_AVERAGE_SCHEMA = cv.All(cv.Schema({ cv.Optional(CONF_WINDOW_SIZE, default=15): cv.positive_not_null_int, cv.Optional(CONF_SEND_EVERY, default=15): cv.positive_not_null_int, @@ -185,8 +200,15 @@ def debounce_filter_to_code(config, filter_id): yield var +def validate_not_all_from_same(config): + if all(conf[CONF_FROM] == config[0][CONF_FROM] for conf in config): + raise cv.Invalid("The 'from' values of the calibrate_linear filter cannot all point " + "to the same value! Please add more values to the filter.") + return config + + @FILTER_REGISTRY.register('calibrate_linear', CalibrateLinearFilter, cv.All( - cv.ensure_list(validate_datapoint), cv.Length(min=2))) + cv.ensure_list(validate_datapoint), cv.Length(min=2), validate_not_all_from_same)) def calibrate_linear_filter_to_code(config, filter_id): x = [conf[CONF_FROM] for conf in config] y = [conf[CONF_TO] for conf in config] @@ -194,6 +216,32 @@ def calibrate_linear_filter_to_code(config, filter_id): yield cg.new_Pvariable(filter_id, k, b) +CONF_DATAPOINTS = 'datapoints' +CONF_DEGREE = 'degree' + + +def validate_calibrate_polynomial(config): + if config[CONF_DEGREE] >= len(config[CONF_DATAPOINTS]): + raise cv.Invalid("Degree is too high! Maximum possible degree with given datapoints is " + "{}".format(len(config[CONF_DATAPOINTS]) - 1), [CONF_DEGREE]) + return config + + +@FILTER_REGISTRY.register('calibrate_polynomial', CalibratePolynomialFilter, cv.All(cv.Schema({ + cv.Required(CONF_DATAPOINTS): cv.All(cv.ensure_list(validate_datapoint), cv.Length(min=1)), + cv.Required(CONF_DEGREE): cv.positive_int, +}), validate_calibrate_polynomial)) +def calibrate_polynomial_filter_to_code(config, filter_id): + x = [conf[CONF_FROM] for conf in config[CONF_DATAPOINTS]] + y = [conf[CONF_TO] for conf in config[CONF_DATAPOINTS]] + degree = config[CONF_DEGREE] + a = [[1] + [x_**(i+1) for i in range(degree)] for x_ in x] + # Column vector + b = [[v] for v in y] + res = [v[0] for v in _lstsq(a, b)] + yield cg.new_Pvariable(filter_id, res) + + @coroutine def build_filters(config): yield cg.build_registry_list(FILTER_REGISTRY, config) @@ -303,6 +351,66 @@ def fit_linear(x, y): return k, b +def _mat_copy(m): + return [list(row) for row in m] + + +def _mat_transpose(m): + return _mat_copy(zip(*m)) + + +def _mat_identity(n): + return [[int(i == j) for j in range(n)] for i in range(n)] + + +def _mat_dot(a, b): + b_t = _mat_transpose(b) + return [[sum(x*y for x, y in zip(row_a, col_b)) for col_b in b_t] for row_a in a] + + +def _mat_inverse(m): + n = len(m) + m = _mat_copy(m) + id = _mat_identity(n) + + for diag in range(n): + # If diag element is 0, swap rows + if m[diag][diag] == 0: + for i in range(diag+1, n): + if m[i][diag] != 0: + break + else: + raise ValueError("Singular matrix, inverse cannot be calculated!") + + # Swap rows + m[diag], m[i] = m[i], m[diag] + id[diag], id[i] = id[i], id[diag] + + # Scale row to 1 in diagonal + scaler = 1.0 / m[diag][diag] + for j in range(n): + m[diag][j] *= scaler + id[diag][j] *= scaler + + # Subtract diag row + for i in range(n): + if i == diag: + continue + scaler = m[i][diag] + for j in range(n): + m[i][j] -= scaler * m[diag][j] + id[i][j] -= scaler * id[diag][j] + + return id + + +def _lstsq(a, b): + # min_x ||b - ax||^2_2 => x = (a^T a)^{-1} a^T b + a_t = _mat_transpose(a) + x = _mat_inverse(_mat_dot(a_t, a)) + return _mat_dot(_mat_dot(x, a_t), b) + + @coroutine_with_priority(40.0) def to_code(config): cg.add_define('USE_SENSOR') diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 306607dfda..f7a5b5d7ad 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -39,6 +39,44 @@ uint32_t Filter::calculate_remaining_interval(uint32_t input) { } } +// MedianFilter +MedianFilter::MedianFilter(size_t window_size, size_t send_every, size_t send_first_at) + : send_every_(send_every), send_at_(send_every - send_first_at), window_size_(window_size) {} +void MedianFilter::set_send_every(size_t send_every) { this->send_every_ = send_every; } +void MedianFilter::set_window_size(size_t window_size) { this->window_size_ = window_size; } +optional MedianFilter::new_value(float value) { + if (!isnan(value)) { + while (this->queue_.size() >= this->window_size_) { + this->queue_.pop_front(); + } + this->queue_.push_back(value); + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f)", this, value); + } + + if (++this->send_at_ >= this->send_every_) { + this->send_at_ = 0; + + float median = 0.0f; + if (!this->queue_.empty()) { + std::deque median_queue = this->queue_; + sort(median_queue.begin(), median_queue.end()); + + size_t queue_size = median_queue.size(); + if (queue_size % 2) { + median = median_queue[queue_size / 2]; + } else { + median = (median_queue[queue_size / 2] + median_queue[(queue_size / 2) - 1]) / 2.0f; + } + } + + ESP_LOGVV(TAG, "MedianFilter(%p)::new_value(%f) SENDING", this, median); + return median; + } + return {}; +} + +uint32_t MedianFilter::expected_interval(uint32_t input) { return input * this->send_every_; } + // SlidingWindowMovingAverageFilter SlidingWindowMovingAverageFilter::SlidingWindowMovingAverageFilter(size_t window_size, size_t send_every, size_t send_first_at) @@ -128,7 +166,11 @@ optional FilterOutValueFilter::new_value(float value) { else return value; } else { - if (value == this->value_to_filter_out_) + int8_t accuracy = this->parent_->get_accuracy_decimals(); + float accuracy_mult = pow10f(accuracy); + float rounded_filter_out = roundf(accuracy_mult * this->value_to_filter_out_); + float rounded_value = roundf(accuracy_mult * value); + if (rounded_filter_out == rounded_value) return {}; else return value; @@ -228,5 +270,15 @@ float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDW optional CalibrateLinearFilter::new_value(float value) { return value * this->slope_ + this->bias_; } CalibrateLinearFilter::CalibrateLinearFilter(float slope, float bias) : slope_(slope), bias_(bias) {} +optional CalibratePolynomialFilter::new_value(float value) { + float res = 0.0f; + float x = 1.0f; + for (float coefficient : this->coefficients_) { + res += x * coefficient; + x *= value; + } + return res; +} + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 6bd22be230..4c61d4c0a2 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -46,6 +46,36 @@ class Filter { Sensor *parent_{nullptr}; }; +/** Simple median filter. + * + * Takes the median of the last values and pushes it out every . + */ +class MedianFilter : public Filter { + public: + /** Construct a MedianFilter. + * + * @param window_size The number of values that should be used in median calculation. + * @param send_every After how many sensor values should a new one be pushed out. + * @param send_first_at After how many values to forward the very first value. Defaults to the first value + * on startup being published on the first *raw* value, so with no filter applied. Must be less than or equal to + * send_every. + */ + explicit MedianFilter(size_t window_size, size_t send_every, size_t send_first_at); + + optional new_value(float value) override; + + void set_send_every(size_t send_every); + void set_window_size(size_t window_size); + + uint32_t expected_interval(uint32_t input) override; + + protected: + std::deque queue_; + size_t send_every_; + size_t send_at_; + size_t window_size_; +}; + /** Simple sliding window moving average filter. * * Essentially just takes takes the average of the last window_size values and pushes them out @@ -243,5 +273,14 @@ class CalibrateLinearFilter : public Filter { float bias_; }; +class CalibratePolynomialFilter : public Filter { + public: + CalibratePolynomialFilter(const std::vector &coefficients) : coefficients_(coefficients) {} + optional new_value(float value) override; + + protected: + std::vector coefficients_; +}; + } // namespace sensor } // namespace esphome diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index f23c0d59b4..559fdc21ab 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -43,8 +43,14 @@ void SHT3XDComponent::dump_config() { } float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; } void SHT3XDComponent::update() { - if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) + if (this->status_has_warning()) { + ESP_LOGD(TAG, "Retrying to reconnect the sensor."); + this->write_command_(SHT3XD_COMMAND_SOFT_RESET); + } + if (!this->write_command_(SHT3XD_COMMAND_POLLING_H)) { + this->status_set_warning(); return; + } this->set_timeout(50, [this]() { uint16_t raw_data[2]; diff --git a/esphome/components/sim800l/__init__.py b/esphome/components/sim800l/__init__.py new file mode 100644 index 0000000000..c64112570a --- /dev/null +++ b/esphome/components/sim800l/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +sim800l_ns = cg.esphome_ns.namespace('sim800l') +Sim800LComponent = sim800l_ns.class_('Sim800LComponent', cg.Component) + +Sim800LReceivedMessageTrigger = sim800l_ns.class_('Sim800LReceivedMessageTrigger', + automation.Trigger.template(cg.std_string, + cg.std_string)) + +# Actions +Sim800LSendSmsAction = sim800l_ns.class_('Sim800LSendSmsAction', automation.Action) + +MULTI_CONF = True + +CONF_ON_SMS_RECEIVED = 'on_sms_received' +CONF_RECIPIENT = 'recipient' +CONF_MESSAGE = 'message' + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(Sim800LComponent), + cv.Optional(CONF_ON_SMS_RECEIVED): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Sim800LReceivedMessageTrigger), + }), +}).extend(cv.polling_component_schema('5s')).extend(uart.UART_DEVICE_SCHEMA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + for conf in config.get(CONF_ON_SMS_RECEIVED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.std_string, 'message'), + (cg.std_string, 'sender')], conf) + + +SIM800L_SEND_SMS_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.use_id(Sim800LComponent), + cv.Required(CONF_RECIPIENT): cv.templatable(cv.string_strict), + cv.Required(CONF_MESSAGE): cv.templatable(cv.string), +}) + + +@automation.register_action('sim800l.send_sms', Sim800LSendSmsAction, SIM800L_SEND_SMS_SCHEMA) +def sim800l_send_sms_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_RECIPIENT], args, cg.std_string) + cg.add(var.set_recipient(template_)) + template_ = yield cg.templatable(config[CONF_MESSAGE], args, cg.std_string) + cg.add(var.set_message(template_)) + yield var diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp new file mode 100644 index 0000000000..b2d58c5043 --- /dev/null +++ b/esphome/components/sim800l/sim800l.cpp @@ -0,0 +1,260 @@ +#include "sim800l.h" +#include "esphome/core/log.h" +#include + +namespace esphome { +namespace sim800l { + +static const char* TAG = "sim800l"; + +const char ASCII_CR = 0x0D; +const char ASCII_LF = 0x0A; + +void Sim800LComponent::update() { + if (this->watch_dog_++ == 2) { + this->state_ = STATE_INIT; + this->write(26); + } + + if (state_ == STATE_INIT) { + if (this->registered_ && this->send_pending_) { + this->send_cmd_("AT+CSCS=\"GSM\""); + this->state_ = STATE_SENDINGSMS1; + } else { + this->send_cmd_("AT"); + this->state_ = STATE_CHECK_AT; + } + this->expect_ack_ = true; + } + if (state_ == STATE_RECEIVEDSMS) { + // Serial Buffer should have flushed. + // Send cmd to delete received sms + char delete_cmd[20]; + sprintf(delete_cmd, "AT+CMGD=%d", this->parse_index_); + this->send_cmd_(delete_cmd); + this->state_ = STATE_CHECK_SMS; + this->expect_ack_ = true; + } +} + +void Sim800LComponent::send_cmd_(std::string message) { + ESP_LOGV(TAG, "S: %s - %d", message.c_str(), this->state_); + this->watch_dog_ = 0; + this->write_str(message.c_str()); + this->write_byte(ASCII_LF); +} + +void Sim800LComponent::parse_cmd_(std::string message) { + ESP_LOGV(TAG, "R: %s - %d", message.c_str(), this->state_); + + if (message.empty()) + return; + + if (this->expect_ack_) { + bool ok = message == "OK"; + this->expect_ack_ = false; + if (!ok) { + if (this->state_ == STATE_CHECK_AT && message == "AT") { + // Expected ack but AT echo received + this->state_ = STATE_DISABLE_ECHO; + this->expect_ack_ = true; + } else { + ESP_LOGW(TAG, "Not ack. %d %s", this->state_, message.c_str()); + this->state_ = STATE_IDLE; // Let it timeout + return; + } + } + } + + switch (this->state_) { + case STATE_INIT: { + // While we were waiting for update to check for messages, this notifies a message + // is available. + bool message_available = message.compare(0, 6, "+CMTI:") == 0; + if (!message_available) + break; + // Else fall thru ... + } + case STATE_CHECK_SMS: + send_cmd_("AT+CMGL=\"ALL\""); + this->state_ = STATE_PARSE_SMS; + this->parse_index_ = 0; + break; + case STATE_DISABLE_ECHO: + send_cmd_("ATE0"); + this->state_ = STATE_CHECK_AT; + this->expect_ack_ = true; + break; + case STATE_CHECK_AT: + send_cmd_("AT+CMGF=1"); + this->state_ = STATE_CREG; + this->expect_ack_ = true; + break; + case STATE_CREG: + send_cmd_("AT+CREG?"); + this->state_ = STATE_CREGWAIT; + break; + case STATE_CREGWAIT: { + // Response: "+CREG: 0,1" -- the one there means registered ok + // "+CREG: -,-" means not registered ok + bool registered = message.compare(0, 6, "+CREG:") == 0 && message[9] == '1'; + if (registered) { + if (!this->registered_) + ESP_LOGD(TAG, "Registered OK"); + this->state_ = STATE_CSQ; + this->expect_ack_ = true; + } else { + ESP_LOGW(TAG, "Registration Fail"); + if (message[7] == '0') { // Network registration is disable, enable it + send_cmd_("AT+CREG=1"); + this->expect_ack_ = true; + this->state_ = STATE_CHECK_AT; + } else { + // Keep waiting registration + this->state_ = STATE_CREG; + } + } + this->registered_ = registered; + break; + } + case STATE_CSQ: + send_cmd_("AT+CSQ"); + this->state_ = STATE_CSQ_RESPONSE; + break; + case STATE_CSQ_RESPONSE: + if (message.compare(0, 5, "+CSQ:") == 0) { + size_t comma = message.find(',', 6); + if (comma != 6) { + this->rssi_ = strtol(message.substr(6, comma - 6).c_str(), nullptr, 10); + ESP_LOGD(TAG, "RSSI: %d", this->rssi_); + } + } + this->expect_ack_ = true; + this->state_ = STATE_CHECK_SMS; + break; + case STATE_PARSE_SMS: + this->state_ = STATE_PARSE_SMS_RESPONSE; + break; + case STATE_PARSE_SMS_RESPONSE: + if (message.compare(0, 6, "+CMGL:") == 0 && this->parse_index_ == 0) { + size_t start = 7; + size_t end = message.find(',', start); + uint8_t item = 0; + while (end != start) { + item++; + if (item == 1) { // Slot Index + this->parse_index_ = strtol(message.substr(start, end - start).c_str(), nullptr, 10); + } + // item 2 = STATUS, usually "REC UNERAD" + if (item == 3) { // recipient + // Add 1 and remove 2 from substring to get rid of "quotes" + this->sender_ = message.substr(start + 1, end - start - 2); + break; + } + // item 4 = "" + // item 5 = Received timestamp + start = end + 1; + end = message.find(',', start); + } + + if (item < 2) { + ESP_LOGD(TAG, "Invalid message %d %s", this->state_, message.c_str()); + return; + } + this->state_ = STATE_RECEIVESMS; + } + // Otherwise we receive another OK, we do nothing just wait polling to continuously check for SMS + if (message == "OK") + this->state_ = STATE_INIT; + break; + case STATE_RECEIVESMS: + /* Our recipient is set and the message body is in message + kick ESPHome callback now + */ + ESP_LOGD(TAG, "Received SMS from: %s", this->sender_.c_str()); + ESP_LOGD(TAG, "%s", message.c_str()); + this->callback_.call(message, this->sender_); + /* If the message is multiline, next lines will contain message data. + If there were other messages in the list, next line will be +CMGL: ... + At the end of the list the new line and the OK should be received. + To keep this simple just first line of message if considered, then + the next state will swallow all received data and in next poll event + this message index is marked for deletion. + */ + this->state_ = STATE_RECEIVEDSMS; + break; + case STATE_RECEIVEDSMS: + // Let the buffer flush. Next poll will request to delete the parsed index message. + break; + case STATE_SENDINGSMS1: + this->send_cmd_("AT+CMGS=\"" + this->recipient_ + "\""); + this->state_ = STATE_SENDINGSMS2; + break; + case STATE_SENDINGSMS2: + if (message == ">") { + // Send sms body + ESP_LOGD(TAG, "Sending message: '%s'", this->outgoing_message_.c_str()); + this->write_str(this->outgoing_message_.c_str()); + this->write(26); + this->state_ = STATE_SENDINGSMS3; + } else { + this->registered_ = false; + this->state_ = STATE_INIT; + this->send_cmd_("AT+CMEE=2"); + this->write(26); + } + break; + case STATE_SENDINGSMS3: + if (message.compare(0, 6, "+CMGS:") == 0) { + ESP_LOGD(TAG, "SMS Sent OK: %s", message.c_str()); + this->send_pending_ = false; + this->state_ = STATE_CHECK_SMS; + this->expect_ack_ = true; + } + break; + default: + ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_); + break; + } +} + +void Sim800LComponent::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == SIM800L_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + ESP_LOGVV(TAG, "Buffer pos: %u %d", this->read_pos_, byte); // NOLINT + + if (byte == ASCII_CR) + continue; + if (byte >= 0x7F) + byte = '?'; // need to be valid utf8 string for log functions. + this->read_buffer_[this->read_pos_] = byte; + + if (this->state_ == STATE_SENDINGSMS2 && this->read_pos_ == 0 && byte == '>') + this->read_buffer_[++this->read_pos_] = ASCII_LF; + + if (this->read_buffer_[this->read_pos_] == ASCII_LF) { + this->read_buffer_[this->read_pos_] = 0; + this->read_pos_ = 0; + this->parse_cmd_(this->read_buffer_); + } else { + this->read_pos_++; + } + } +} + +void Sim800LComponent::send_sms(std::string recipient, std::string message) { + ESP_LOGD(TAG, "Sending to %s: %s", recipient.c_str(), message.c_str()); + this->recipient_ = recipient; + this->outgoing_message_ = message; + this->send_pending_ = true; + this->update(); +} + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h new file mode 100644 index 0000000000..17cd0111fe --- /dev/null +++ b/esphome/components/sim800l/sim800l.h @@ -0,0 +1,91 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +#define SIM800L_READ_BUFFER_LENGTH 255 + +namespace esphome { +namespace sim800l { + +enum State { + STATE_IDLE = 0, + STATE_INIT, + STATE_CHECK_AT, + STATE_CREG, + STATE_CREGWAIT, + STATE_CSQ, + STATE_CSQ_RESPONSE, + STATE_IDLEWAIT, + STATE_SENDINGSMS1, + STATE_SENDINGSMS2, + STATE_SENDINGSMS3, + STATE_CHECK_SMS, + STATE_PARSE_SMS, + STATE_PARSE_SMS_RESPONSE, + STATE_RECEIVESMS, + STATE_READSMS, + STATE_RECEIVEDSMS, + STATE_DELETEDSMS, + STATE_DISABLE_ECHO, + STATE_PARSE_SMS_OK +}; + +class Sim800LComponent : public uart::UARTDevice, public PollingComponent { + public: + /// Retrieve the latest sensor values. This operation takes approximately 16ms. + void update() override; + void loop() override; + void add_on_sms_received_callback(std::function callback) { + this->callback_.add(std::move(callback)); + } + void send_sms(std::string recipient, std::string message); + + protected: + void send_cmd_(std::string); + void parse_cmd_(std::string); + + std::string sender_; + char read_buffer_[SIM800L_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + uint8_t parse_index_{0}; + uint8_t watch_dog_{0}; + bool expect_ack_{false}; + sim800l::State state_{STATE_IDLE}; + bool registered_{false}; + int rssi_{0}; + + std::string recipient_; + std::string outgoing_message_; + bool send_pending_; + + CallbackManager callback_; +}; + +class Sim800LReceivedMessageTrigger : public Trigger { + public: + explicit Sim800LReceivedMessageTrigger(Sim800LComponent *parent) { + parent->add_on_sms_received_callback( + [this](std::string message, std::string sender) { this->trigger(message, sender); }); + } +}; + +template class Sim800LSendSmsAction : public Action { + public: + Sim800LSendSmsAction(Sim800LComponent *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(std::string, recipient) + TEMPLATABLE_VALUE(std::string, message) + + 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_; +}; + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/sm16716/__init__.py b/esphome/components/sm16716/__init__.py new file mode 100644 index 0000000000..4e342588f9 --- /dev/null +++ b/esphome/components/sm16716/__init__.py @@ -0,0 +1,31 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.const import (CONF_CLOCK_PIN, CONF_DATA_PIN, CONF_ID, + CONF_NUM_CHANNELS, CONF_NUM_CHIPS) + +AUTO_LOAD = ['output'] +sm16716_ns = cg.esphome_ns.namespace('sm16716') +SM16716 = sm16716_ns.class_('SM16716', cg.Component) + +MULTI_CONF = True +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SM16716), + cv.Required(CONF_DATA_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLOCK_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_NUM_CHANNELS, default=3): cv.int_range(min=3, max=255), + cv.Optional(CONF_NUM_CHIPS, default=1): cv.int_range(min=1, max=85), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + data = yield cg.gpio_pin_expression(config[CONF_DATA_PIN]) + cg.add(var.set_data_pin(data)) + clock = yield cg.gpio_pin_expression(config[CONF_CLOCK_PIN]) + cg.add(var.set_clock_pin(clock)) + + cg.add(var.set_num_channels(config[CONF_NUM_CHANNELS])) + cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) diff --git a/esphome/components/sm16716/output.py b/esphome/components/sm16716/output.py new file mode 100644 index 0000000000..93c9ed4ce1 --- /dev/null +++ b/esphome/components/sm16716/output.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import SM16716 + +DEPENDENCIES = ['sm16716'] + +Channel = SM16716.class_('Channel', output.FloatOutput) + +CONF_SM16716_ID = 'sm16716_id' +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.GenerateID(CONF_SM16716_ID): cv.use_id(SM16716), + cv.Required(CONF_ID): cv.declare_id(Channel), + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield output.register_output(var, config) + + parent = yield cg.get_variable(config[CONF_SM16716_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_channel(config[CONF_CHANNEL])) diff --git a/esphome/components/sm16716/sm16716.cpp b/esphome/components/sm16716/sm16716.cpp new file mode 100644 index 0000000000..bc8e4fc1f4 --- /dev/null +++ b/esphome/components/sm16716/sm16716.cpp @@ -0,0 +1,52 @@ +#include "sm16716.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sm16716 { + +static const char *TAG = "sm16716"; + +void SM16716::setup() { + ESP_LOGCONFIG(TAG, "Setting up SM16716OutputComponent..."); + this->data_pin_->setup(); + this->data_pin_->digital_write(false); + this->clock_pin_->setup(); + this->clock_pin_->digital_write(false); + this->pwm_amounts_.resize(this->num_channels_, 0); +} +void SM16716::dump_config() { + ESP_LOGCONFIG(TAG, "SM16716:"); + LOG_PIN(" Data Pin: ", this->data_pin_); + LOG_PIN(" Clock Pin: ", this->clock_pin_); + ESP_LOGCONFIG(TAG, " Total number of channels: %u", this->num_channels_); + ESP_LOGCONFIG(TAG, " Number of chips: %u", this->num_chips_); +} +void SM16716::loop() { + if (!this->update_) + return; + + for (uint8_t i = 0; i < 50; i++) { + this->write_bit_(false); + } + + // send 25 bits (1 start bit plus 24 data bits) for each chip + for (uint8_t index = 0; index < this->num_channels_; index++) { + // send a start bit initially and after every 3 channels + if (index % 3 == 0) { + this->write_bit_(true); + } + + this->write_byte_(this->pwm_amounts_[index]); + } + + // send a blank 25 bits to signal the end + this->write_bit_(false); + this->write_byte_(0); + this->write_byte_(0); + this->write_byte_(0); + + this->update_ = false; +} + +} // namespace sm16716 +} // namespace esphome diff --git a/esphome/components/sm16716/sm16716.h b/esphome/components/sm16716/sm16716.h new file mode 100644 index 0000000000..fe534d93fe --- /dev/null +++ b/esphome/components/sm16716/sm16716.h @@ -0,0 +1,70 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sm16716 { + +class SM16716 : public Component { + public: + class Channel; + + void set_data_pin(GPIOPin *data_pin) { data_pin_ = data_pin; } + void set_clock_pin(GPIOPin *clock_pin) { clock_pin_ = clock_pin; } + void set_num_channels(uint8_t num_channels) { num_channels_ = num_channels; } + void set_num_chips(uint8_t num_chips) { num_chips_ = num_chips; } + + void setup() override; + + void dump_config() override; + + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + /// Send new values if they were updated. + void loop() override; + + class Channel : public output::FloatOutput { + public: + void set_parent(SM16716 *parent) { parent_ = parent; } + void set_channel(uint8_t channel) { channel_ = channel; } + + protected: + void write_state(float state) override { + auto amount = uint8_t(state * 0xFF); + this->parent_->set_channel_value_(this->channel_, amount); + } + + SM16716 *parent_; + uint8_t channel_; + }; + + protected: + void set_channel_value_(uint8_t channel, uint8_t value) { + uint8_t index = this->num_channels_ - channel - 1; + if (this->pwm_amounts_[index] != value) { + this->update_ = true; + } + this->pwm_amounts_[index] = value; + } + void write_bit_(bool value) { + this->data_pin_->digital_write(value); + this->clock_pin_->digital_write(true); + this->clock_pin_->digital_write(false); + } + void write_byte_(uint8_t data) { + for (uint8_t mask = 0x80; mask; mask >>= 1) { + this->write_bit_(data & mask); + } + } + + GPIOPin *data_pin_; + GPIOPin *clock_pin_; + uint8_t num_channels_; + uint8_t num_chips_; + std::vector pwm_amounts_; + bool update_{true}; +}; + +} // namespace sm16716 +} // namespace esphome diff --git a/esphome/components/sntp/sntp_component.cpp b/esphome/components/sntp/sntp_component.cpp index 4f931203fb..c10a3e5ac3 100644 --- a/esphome/components/sntp/sntp_component.cpp +++ b/esphome/components/sntp/sntp_component.cpp @@ -2,7 +2,7 @@ #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 -#include "apps/sntp/sntp.h" +#include "lwip/apps/sntp.h" #endif #ifdef ARDUINO_ARCH_ESP8266 #include "sntp.h" diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index db4b71c29a..bf2a18955a 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -8,74 +8,10 @@ namespace spi { static const char *TAG = "spi"; -void ICACHE_RAM_ATTR HOT SPIComponent::write_byte(uint8_t data) { - uint8_t send_bits = data; - if (this->msb_first_) - send_bits = reverse_bits_8(data); - - this->clk_->digital_write(true); - if (!this->high_speed_) - delayMicroseconds(5); - - for (size_t i = 0; i < 8; i++) { - if (!this->high_speed_) - delayMicroseconds(5); - this->clk_->digital_write(false); - - // sampling on leading edge - this->mosi_->digital_write(send_bits & (1 << i)); - if (!this->high_speed_) - delayMicroseconds(5); - this->clk_->digital_write(true); - } - - ESP_LOGVV(TAG, " Wrote 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); -} - -uint8_t ICACHE_RAM_ATTR HOT SPIComponent::read_byte() { - this->clk_->digital_write(true); - - uint8_t data = 0; - for (size_t i = 0; i < 8; i++) { - if (!this->high_speed_) - delayMicroseconds(5); - data |= uint8_t(this->miso_->digital_read()) << i; - this->clk_->digital_write(false); - if (!this->high_speed_) - delayMicroseconds(5); - this->clk_->digital_write(true); - } - - if (this->msb_first_) { - data = reverse_bits_8(data); - } - - ESP_LOGVV(TAG, " Received 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(data), data); - - return data; -} -void ICACHE_RAM_ATTR HOT SPIComponent::read_array(uint8_t *data, size_t length) { - for (size_t i = 0; i < length; i++) - data[i] = this->read_byte(); -} - -void ICACHE_RAM_ATTR HOT SPIComponent::write_array(uint8_t *data, size_t length) { - for (size_t i = 0; i < length; i++) { - App.feed_wdt(); - this->write_byte(data[i]); - } -} - -void ICACHE_RAM_ATTR HOT SPIComponent::enable(GPIOPin *cs, bool msb_first, bool high_speed) { - ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", cs->get_pin()); - cs->digital_write(false); - - this->active_cs_ = cs; - this->msb_first_ = msb_first; - this->high_speed_ = high_speed; -} - 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; @@ -84,6 +20,53 @@ void SPIComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI bus..."); this->clk_->setup(); this->clk_->digital_write(true); + + bool use_hw_spi = true; + if (this->clk_->is_inverted()) + use_hw_spi = false; + const bool has_miso = this->miso_ != nullptr; + const bool has_mosi = this->mosi_ != nullptr; + if (has_miso && this->miso_->is_inverted()) + use_hw_spi = false; + if (has_mosi && this->mosi_->is_inverted()) + use_hw_spi = false; + int8_t clk_pin = this->clk_->get_pin(); + int8_t miso_pin = has_miso ? this->miso_->get_pin() : -1; + int8_t mosi_pin = has_mosi ? this->mosi_->get_pin() : -1; +#ifdef ARDUINO_ARCH_ESP8266 + if (clk_pin == 6 && miso_pin == 7 && mosi_pin == 8) { + // pass + } else if (clk_pin == 14 && miso_pin == 12 && mosi_pin == 13) { + // pass + } else { + use_hw_spi = false; + } + + if (use_hw_spi) { + this->hw_spi_ = &SPI; + this->hw_spi_->pins(clk_pin, miso_pin, mosi_pin, 0); + this->hw_spi_->begin(); + return; + } +#endif +#ifdef ARDUINO_ARCH_ESP32 + static uint8_t spi_bus_num = 0; + if (spi_bus_num >= 2) { + use_hw_spi = false; + } + + if (use_hw_spi) { + if (spi_bus_num == 0) { + this->hw_spi_ = &SPI; + } else { + this->hw_spi_ = new SPIClass(VSPI); + } + spi_bus_num++; + this->hw_spi_->begin(clk_pin, miso_pin, mosi_pin); + return; + } +#endif + if (this->miso_ != nullptr) { this->miso_->setup(); } @@ -97,8 +80,154 @@ void SPIComponent::dump_config() { LOG_PIN(" CLK Pin: ", this->clk_); LOG_PIN(" MISO Pin: ", this->miso_); LOG_PIN(" MOSI Pin: ", this->mosi_); + ESP_LOGCONFIG(TAG, " Using HW SPI: %s", YESNO(this->hw_spi_ != nullptr)); } float SPIComponent::get_setup_priority() const { return setup_priority::BUS; } +void SPIComponent::debug_tx(uint8_t value) { + ESP_LOGVV(TAG, " TX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value); +} +void SPIComponent::debug_rx(uint8_t value) { + ESP_LOGVV(TAG, " RX 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(value), value); +} +void SPIComponent::debug_enable(uint8_t pin) { ESP_LOGVV(TAG, "Enabling SPI Chip on pin %u...", pin); } + +void SPIComponent::cycle_clock_(bool value) { + uint32_t start = ESP.getCycleCount(); + while (start - ESP.getCycleCount() < this->wait_cycle_) + ; + this->clk_->digital_write(value); + start += this->wait_cycle_; + while (start - ESP.getCycleCount() < this->wait_cycle_) + ; +} + +// NOLINTNEXTLINE +#pragma GCC optimize("unroll-loops") +// NOLINTNEXTLINE +#pragma GCC optimize("O2") + +template +uint8_t HOT SPIComponent::transfer_(uint8_t data) { + // Clock starts out at idle level + this->clk_->digital_write(CLOCK_POLARITY); + uint8_t out_data = 0; + + for (uint8_t i = 0; i < 8; i++) { + uint8_t shift; + if (BIT_ORDER == BIT_ORDER_MSB_FIRST) + shift = 7 - i; + else + shift = i; + + if (CLOCK_PHASE == CLOCK_PHASE_LEADING) { + // sampling on leading edge + if (WRITE) { + this->mosi_->digital_write(data & (1 << shift)); + } + + // SAMPLE! + this->cycle_clock_(!CLOCK_POLARITY); + + if (READ) { + out_data |= uint8_t(this->miso_->digital_read()) << shift; + } + + this->cycle_clock_(CLOCK_POLARITY); + } else { + // sampling on trailing edge + this->cycle_clock_(!CLOCK_POLARITY); + + if (WRITE) { + this->mosi_->digital_write(data & (1 << shift)); + } + + // SAMPLE! + this->cycle_clock_(CLOCK_POLARITY); + + if (READ) { + out_data |= uint8_t(this->miso_->digital_read()) << shift; + } + } + } + +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE + if (WRITE) { + SPIComponent::debug_tx(data); + } + if (READ) { + SPIComponent::debug_rx(out_data); + } +#endif + + App.feed_wdt(); + + return out_data; +} + +// Generate with (py3): +// +// from itertools import product +// bit_orders = ['BIT_ORDER_LSB_FIRST', 'BIT_ORDER_MSB_FIRST'] +// clock_pols = ['CLOCK_POLARITY_LOW', 'CLOCK_POLARITY_HIGH'] +// clock_phases = ['CLOCK_PHASE_LEADING', 'CLOCK_PHASE_TRAILING'] +// reads = [False, True] +// writes = [False, True] +// cpp_bool = {False: 'false', True: 'true'} +// for b, cpol, cph, r, w in product(bit_orders, clock_pols, clock_phases, reads, writes): +// if not r and not w: +// continue +// print(f"template uint8_t SPIComponent::transfer_<{b}, {cpol}, {cph}, {cpp_bool[r]}, {cpp_bool[w]}>(uint8_t +// data);") + +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); +template uint8_t SPIComponent::transfer_( + uint8_t data); + } // namespace spi } // namespace esphome diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 600e1a0cd2..ccef6192f3 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -2,10 +2,61 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include namespace esphome { namespace spi { +/// The bit-order for SPI devices. This defines how the data read from and written to the device is interpreted. +enum SPIBitOrder { + /// The least significant bit is transmitted/received first. + BIT_ORDER_LSB_FIRST, + /// The most significant bit is transmitted/received first. + BIT_ORDER_MSB_FIRST, +}; +/** The SPI clock signal polarity, + * + * This defines how the clock signal is used. Flipping this effectively inverts the clock signal. + */ +enum SPIClockPolarity { + /** The clock signal idles on LOW. (CPOL=0) + * + * A rising edge means a leading edge for the clock. + */ + CLOCK_POLARITY_LOW = false, + /** The clock signal idles on HIGH. (CPOL=1) + * + * A falling edge means a trailing edge for the clock. + */ + CLOCK_POLARITY_HIGH = true, +}; +/** The SPI clock signal phase. + * + * This defines when the data signals are sampled. Most SPI devices use the LEADING clock phase. + */ +enum SPIClockPhase { + /// The data is sampled on a leading clock edge. (CPHA=0) + CLOCK_PHASE_LEADING, + /// The data is sampled on a trailing clock edge. (CPHA=1) + CLOCK_PHASE_TRAILING, +}; +/** The SPI clock signal data rate. This defines for what duration the clock signal is HIGH/LOW. + * So effectively the rate of bytes can be calculated using + * + * effective_byte_rate = spi_data_rate / 16 + * + * Implementations can use the pre-defined constants here, or use an integer in the template definition + * to manually use a specific data rate. + */ +enum SPIDataRate : uint32_t { + DATA_RATE_1KHZ = 1000, + DATA_RATE_200KHZ = 200000, + DATA_RATE_1MHZ = 1000000, + DATA_RATE_2MHZ = 2000000, + DATA_RATE_4MHZ = 4000000, + DATA_RATE_8MHZ = 8000000, +}; + class SPIComponent : public Component { public: void set_clk(GPIOPin *clk) { clk_ = clk; } @@ -16,59 +67,156 @@ class SPIComponent : public Component { void dump_config() override; - uint8_t read_byte(); + template uint8_t read_byte() { + if (this->hw_spi_ != nullptr) { + return this->hw_spi_->transfer(0x00); + } + return this->transfer_(0x00); + } - void read_array(uint8_t *data, size_t length); + template + void read_array(uint8_t *data, size_t length) { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->transfer(data, length); + return; + } + for (size_t i = 0; i < length; i++) { + data[i] = this->read_byte(); + } + } - void write_byte(uint8_t data); + template + void write_byte(uint8_t data) { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->write(data); + return; + } + this->transfer_(data); + } - void write_array(uint8_t *data, size_t length); + template + void write_array(const uint8_t *data, size_t length) { + if (this->hw_spi_ != nullptr) { + auto *data_c = const_cast(data); + this->hw_spi_->writeBytes(data_c, length); + return; + } + for (size_t i = 0; i < length; i++) { + this->write_byte(data[i]); + } + } - void enable(GPIOPin *cs, bool msb_first, bool high_speed); + template + uint8_t transfer_byte(uint8_t data) { + if (this->hw_spi_ != nullptr) { + return this->hw_spi_->transfer(data); + } + return this->transfer_(data); + } + + template + void transfer_array(uint8_t *data, size_t length) { + if (this->hw_spi_ != nullptr) { + this->hw_spi_->transfer(data, length); + return; + } + for (size_t i = 0; i < length; i++) { + data[i] = this->transfer_byte(data[i]); + } + } + + template + void enable(GPIOPin *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; + } + + this->active_cs_ = cs; + this->active_cs_->digital_write(false); + } void disable(); float get_setup_priority() const override; protected: + inline void cycle_clock_(bool value); + + static void debug_enable(uint8_t pin); + static void debug_tx(uint8_t value); + static void debug_rx(uint8_t value); + + template + uint8_t transfer_(uint8_t data); + GPIOPin *clk_; GPIOPin *miso_{nullptr}; GPIOPin *mosi_{nullptr}; GPIOPin *active_cs_{nullptr}; - bool msb_first_{true}; - bool high_speed_{false}; + SPIClass *hw_spi_{nullptr}; + uint32_t wait_cycle_; }; +template class SPIDevice { public: SPIDevice() = default; SPIDevice(SPIComponent *parent, GPIOPin *cs) : parent_(parent), cs_(cs) {} - void set_spi_parent(SPIComponent *parent) { this->parent_ = parent; } - void set_cs_pin(GPIOPin *cs) { this->cs_ = cs; } + void set_spi_parent(SPIComponent *parent) { parent_ = parent; } + void set_cs_pin(GPIOPin *cs) { cs_ = cs; } void spi_setup() { this->cs_->setup(); this->cs_->digital_write(true); } - void enable() { this->parent_->enable(this->cs_, this->is_device_msb_first(), this->is_device_high_speed()); } + void enable() { this->parent_->template enable(this->cs_); } void disable() { this->parent_->disable(); } - uint8_t read_byte() { return this->parent_->read_byte(); } + uint8_t read_byte() { return this->parent_->template read_byte(); } - void read_array(uint8_t *data, size_t length) { return this->parent_->read_array(data, length); } + void read_array(uint8_t *data, size_t length) { + return this->parent_->template read_array(data, length); + } - void write_byte(uint8_t data) { return this->parent_->write_byte(data); } + template std::array read_array() { + std::array data; + this->read_array(data.data(), N); + return data; + } - void write_array(uint8_t *data, size_t length) { this->parent_->write_array(data, length); } + void write_byte(uint8_t data) { + return this->parent_->template write_byte(data); + } + + void write_array(const uint8_t *data, size_t length) { + this->parent_->template write_array(data, length); + } + + template void write_array(const std::array &data) { this->write_array(data.data(), N); } + + void write_array(const std::vector &data) { this->write_array(data.data(), data.size()); } + + uint8_t transfer_byte(uint8_t data) { + return this->parent_->template transfer_byte(data); + } + + void transfer_array(uint8_t *data, size_t length) { + this->parent_->template transfer_array(data, length); + } + + template void transfer_array(std::array &data) { this->transfer_array(data.data(), N); } protected: - virtual bool is_device_msb_first() = 0; - - virtual bool is_device_high_speed() { return false; } - SPIComponent *parent_{nullptr}; GPIOPin *cs_{nullptr}; }; diff --git a/esphome/components/ssd1306_base/__init__.py b/esphome/components/ssd1306_base/__init__.py index 0a678452b2..047ddddcac 100644 --- a/esphome/components/ssd1306_base/__init__.py +++ b/esphome/components/ssd1306_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_EXTERNAL_VCC, CONF_LAMBDA, CONF_MODEL, CONF_RESET_PIN, \ + CONF_BRIGHTNESS from esphome.core import coroutine ssd1306_base_ns = cg.esphome_ns.namespace('ssd1306_base') @@ -25,6 +26,7 @@ SSD1306_MODEL = cv.enum(MODELS, upper=True, space="_") SSD1306_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ cv.Required(CONF_MODEL): SSD1306_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')) @@ -38,6 +40,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.set_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/ssd1306_base/ssd1306_base.cpp b/esphome/components/ssd1306_base/ssd1306_base.cpp index b6f2d94eac..d60f7dc985 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.cpp +++ b/esphome/components/ssd1306_base/ssd1306_base.cpp @@ -79,10 +79,7 @@ void SSD1306::setup() { case SH1106_MODEL_128_64: case SSD1306_MODEL_64_48: case SH1106_MODEL_64_48: - if (this->external_vcc_) - this->command(0x9F); - else - this->command(0xCF); + this->command(int(255 * (this->brightness_))); break; case SSD1306_MODEL_96_16: case SH1106_MODEL_96_16: @@ -100,7 +97,7 @@ void SSD1306::setup() { this->command(0xF1); this->command(SSD1306_COMMAND_SET_VCOM_DETECT); - this->command(0x40); + this->command(0x00); this->command(SSD1306_COMMAND_DISPLAY_ALL_ON_RESUME); this->command(SSD1306_NORMAL_DISPLAY); diff --git a/esphome/components/ssd1306_base/ssd1306_base.h b/esphome/components/ssd1306_base/ssd1306_base.h index 66c12ec938..8adf3c1b87 100644 --- a/esphome/components/ssd1306_base/ssd1306_base.h +++ b/esphome/components/ssd1306_base/ssd1306_base.h @@ -29,6 +29,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { void set_model(SSD1306Model 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 set_brightness(float brightness) { this->brightness_ = brightness; } float get_setup_priority() const override { return setup_priority::PROCESSOR; } void fill(int color) override; @@ -50,6 +51,7 @@ class SSD1306 : public PollingComponent, public display::DisplayBuffer { SSD1306Model model_{SSD1306_MODEL_128_64}; GPIOPin *reset_pin_{nullptr}; bool external_vcc_{false}; + float brightness_{1.0}; }; } // namespace ssd1306_base diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.cpp b/esphome/components/ssd1306_spi/ssd1306_spi.cpp index aeead612ff..d87f412f70 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.cpp +++ b/esphome/components/ssd1306_spi/ssd1306_spi.cpp @@ -7,7 +7,6 @@ namespace ssd1306_spi { static const char *TAG = "ssd1306_spi"; -bool SPISSD1306::is_device_msb_first() { return true; } void SPISSD1306::setup() { ESP_LOGCONFIG(TAG, "Setting up SPI SSD1306..."); this->spi_setup(); @@ -52,7 +51,6 @@ void HOT SPISSD1306::write_display_data() { this->disable(); } } -bool SPISSD1306::is_device_high_speed() { return true; } } // namespace ssd1306_spi } // namespace esphome diff --git a/esphome/components/ssd1306_spi/ssd1306_spi.h b/esphome/components/ssd1306_spi/ssd1306_spi.h index 5d0640bd84..c58ebc800a 100644 --- a/esphome/components/ssd1306_spi/ssd1306_spi.h +++ b/esphome/components/ssd1306_spi/ssd1306_spi.h @@ -7,7 +7,9 @@ namespace esphome { namespace ssd1306_spi { -class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice { +class SPISSD1306 : public ssd1306_base::SSD1306, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } @@ -19,8 +21,6 @@ class SPISSD1306 : public ssd1306_base::SSD1306, public spi::SPIDevice { void command(uint8_t value) override; void write_display_data() override; - bool is_device_msb_first() override; - bool is_device_high_speed() override; GPIOPin *dc_pin_; }; diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 625e64dcc2..fef0902181 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome import automation from esphome.components import time from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +from esphome.py_compat import string_types sun_ns = cg.esphome_ns.namespace('sun') @@ -17,6 +18,10 @@ CONF_ELEVATION = 'elevation' CONF_ON_SUNRISE = 'on_sunrise' CONF_ON_SUNSET = 'on_sunset' +# Default sun elevation is a bit below horizon because sunset +# means time when the entire sun disk is below the horizon +DEFAULT_ELEVATION = -0.883 + ELEVATION_MAP = { 'sunrise': 0.0, 'sunset': 0.0, @@ -27,9 +32,9 @@ ELEVATION_MAP = { def elevation(value): - if isinstance(value, str): + if isinstance(value, string_types): try: - value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')] + value = ELEVATION_MAP[cv.one_of(*ELEVATION_MAP, lower=True, space='_')(value)] except cv.Invalid: pass value = cv.angle(value) @@ -44,11 +49,11 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_ON_SUNRISE): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), - cv.Optional(CONF_ELEVATION, default=0.0): elevation, + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, }), cv.Optional(CONF_ON_SUNSET): automation.validate_automation({ cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), - cv.Optional(CONF_ELEVATION, default=0.0): elevation, + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): elevation, }), }) @@ -79,7 +84,7 @@ def to_code(config): @automation.register_condition('sun.is_above_horizon', SunCondition, cv.Schema({ cv.GenerateID(): cv.use_id(Sun), - cv.Optional(CONF_ELEVATION, default=0): cv.templatable(elevation), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable(elevation), })) def sun_above_horizon_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) @@ -92,7 +97,7 @@ def sun_above_horizon_to_code(config, condition_id, template_arg, args): @automation.register_condition('sun.is_below_horizon', SunCondition, cv.Schema({ cv.GenerateID(): cv.use_id(Sun), - cv.Optional(CONF_ELEVATION, default=0): cv.templatable(elevation), + cv.Optional(CONF_ELEVATION, default=DEFAULT_ELEVATION): cv.templatable(elevation), })) def sun_below_horizon_to_code(config, condition_id, template_arg, args): var = cg.new_Pvariable(condition_id, template_arg) diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 2592c75c62..501d122da0 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -103,7 +103,7 @@ class SunTrigger : public Trigger<>, public PollingComponent, public Parentedlast_elevation_ >= this->elevation_ && this->elevation_ > current; } - if (crossed) { + if (crossed && !isnan(this->last_elevation_)) { this->trigger(); } this->last_elevation_ = current; @@ -111,7 +111,7 @@ class SunTrigger : public Trigger<>, public PollingComponent, public Parentedkey_ = (1 << (col + 8)) | (1 << row); } + void process(uint16_t data) override { this->publish_state(static_cast(data == key_)); } + + protected: + uint16_t key_{0}; +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/output/__init__.py b/esphome/components/sx1509/output/__init__.py new file mode 100644 index 0000000000..80aec0afd4 --- /dev/null +++ b/esphome/components/sx1509/output/__init__.py @@ -0,0 +1,25 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_PIN, CONF_ID +from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID + +DEPENDENCIES = ['sx1509'] + +SX1509FloatOutputChannel = sx1509_ns.class_('SX1509FloatOutputChannel', + output.FloatOutput, cg.Component) + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(SX1509FloatOutputChannel), + cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), + cv.Required(CONF_PIN): cv.int_range(min=0, max=15), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + parent = yield cg.get_variable(config[CONF_SX1509_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield output.register_output(var, config) + cg.add(var.set_pin(config[CONF_PIN])) + cg.add(var.set_parent(parent)) diff --git a/esphome/components/sx1509/output/sx1509_float_output.cpp b/esphome/components/sx1509/output/sx1509_float_output.cpp new file mode 100644 index 0000000000..7ff1bbb61b --- /dev/null +++ b/esphome/components/sx1509/output/sx1509_float_output.cpp @@ -0,0 +1,30 @@ +#include "sx1509_float_output.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx1509 { + +static const char *TAG = "sx1509_float_channel"; + +void SX1509FloatOutputChannel::write_state(float state) { + const uint16_t max_duty = 255; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + this->parent_->set_pin_value(this->pin_, duty); +} + +void SX1509FloatOutputChannel::setup() { + ESP_LOGD(TAG, "setup pin %d", this->pin_); + this->parent_->pin_mode(this->pin_, SX1509_ANALOG_OUTPUT); + this->turn_off(); +} + +void SX1509FloatOutputChannel::dump_config() { + ESP_LOGCONFIG(TAG, "SX1509 PWM:"); + ESP_LOGCONFIG(TAG, " sx1509 pin: %d", this->pin_); + LOG_FLOAT_OUTPUT(this); +} + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/output/sx1509_float_output.h b/esphome/components/sx1509/output/sx1509_float_output.h new file mode 100644 index 0000000000..39e51839ea --- /dev/null +++ b/esphome/components/sx1509/output/sx1509_float_output.h @@ -0,0 +1,27 @@ +#pragma once + +#include "esphome/components/sx1509/sx1509.h" +#include "esphome/components/output/float_output.h" + +namespace esphome { +namespace sx1509 { + +class SX1509Component; + +class SX1509FloatOutputChannel : public output::FloatOutput, public Component { + public: + void set_parent(SX1509Component *parent) { this->parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + + protected: + void write_state(float state) override; + + SX1509Component *parent_; + uint8_t pin_; +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509.cpp b/esphome/components/sx1509/sx1509.cpp new file mode 100644 index 0000000000..2806a1cac2 --- /dev/null +++ b/esphome/components/sx1509/sx1509.cpp @@ -0,0 +1,253 @@ +#include "sx1509.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sx1509 { + +static const char *TAG = "sx1509"; + +void SX1509Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SX1509Component..."); + + ESP_LOGV(TAG, " Resetting devices..."); + if (!this->write_byte(REG_RESET, 0x12)) { + this->mark_failed(); + return; + } + this->write_byte(REG_RESET, 0x34); + + uint16_t data; + this->read_byte_16(REG_INTERRUPT_MASK_A, &data); + if (data == 0xFF00) { + clock_(INTERNAL_CLOCK_2MHZ); + } else { + this->mark_failed(); + return; + } + delayMicroseconds(500); + if (this->has_keypad_) + this->setup_keypad_(); +} + +void SX1509Component::dump_config() { + ESP_LOGCONFIG(TAG, "SX1509:"); + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up SX1509 failed!"); + } + LOG_I2C_DEVICE(this); +} + +void SX1509Component::loop() { + if (this->has_keypad_) { + uint16_t key_data = this->read_key_data(); + for (auto *binary_sensor : this->keypad_binary_sensors_) + binary_sensor->process(key_data); + } +} + +bool SX1509Component::digital_read(uint8_t pin) { + if (this->ddr_mask_ & (1 << pin)) { + uint16_t temp_reg_data; + this->read_byte_16(REG_DATA_B, &temp_reg_data); + if (temp_reg_data & (1 << pin)) + return true; + } + return false; +} + +void SX1509Component::digital_write(uint8_t pin, bool bit_value) { + if ((~this->ddr_mask_) & (1 << pin)) { + // If the pin is an output, write high/low + uint16_t temp_reg_data = 0; + this->read_byte_16(REG_DATA_B, &temp_reg_data); + if (bit_value) + temp_reg_data |= (1 << pin); + else + temp_reg_data &= ~(1 << pin); + this->write_byte_16(REG_DATA_B, temp_reg_data); + } else { + // Otherwise the pin is an input, pull-up/down + uint16_t temp_pullup; + this->read_byte_16(REG_PULL_UP_B, &temp_pullup); + uint16_t temp_pull_down; + this->read_byte_16(REG_PULL_DOWN_B, &temp_pull_down); + + if (bit_value) { + // if HIGH, do pull-up, disable pull-down + temp_pullup |= (1 << pin); + temp_pull_down &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_pullup); + this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down); + } else { + // If LOW do pull-down, disable pull-up + temp_pull_down |= (1 << pin); + temp_pullup &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_pullup); + this->write_byte_16(REG_PULL_DOWN_B, temp_pull_down); + } + } +} + +void SX1509Component::pin_mode(uint8_t pin, uint8_t mode) { + this->read_byte_16(REG_DIR_B, &this->ddr_mask_); + if ((mode == SX1509_OUTPUT) || (mode == SX1509_ANALOG_OUTPUT)) + this->ddr_mask_ &= ~(1 << pin); + else + this->ddr_mask_ |= (1 << pin); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); + + if (mode == INPUT_PULLUP) + digital_write(pin, HIGH); + + if (mode == SX1509_ANALOG_OUTPUT) { + setup_led_driver_(pin); + } +} + +void SX1509Component::setup_led_driver_(uint8_t pin) { + uint16_t temp_word; + uint8_t temp_byte; + + this->read_byte_16(REG_INPUT_DISABLE_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_INPUT_DISABLE_B, temp_word); + + this->read_byte_16(REG_PULL_UP_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_PULL_UP_B, temp_word); + + this->ddr_mask_ &= ~(1 << pin); // 0=output + this->write_byte_16(REG_DIR_B, this->ddr_mask_); + + this->read_byte(REG_CLOCK, &temp_byte); + temp_byte |= (1 << 6); // Internal 2MHz oscillator part 1 (set bit 6) + temp_byte &= ~(1 << 5); // Internal 2MHz oscillator part 2 (clear bit 5) + this->write_byte(REG_CLOCK, temp_byte); + + this->read_byte(REG_MISC, &temp_byte); + temp_byte &= ~(1 << 7); // set linear mode bank B + temp_byte &= ~(1 << 3); // set linear mode bank A + temp_byte |= 0x70; // Frequency of the LED Driver clock ClkX of all IOs: + this->write_byte(REG_MISC, temp_byte); + + this->read_byte_16(REG_LED_DRIVER_ENABLE_B, &temp_word); + temp_word |= (1 << pin); + this->write_byte_16(REG_LED_DRIVER_ENABLE_B, temp_word); + + this->read_byte_16(REG_DATA_B, &temp_word); + temp_word &= ~(1 << pin); + this->write_byte_16(REG_DATA_B, temp_word); +} + +void SX1509Component::clock_(byte osc_source, byte osc_pin_function, byte osc_freq_out, byte osc_divider) { + 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; + this->write_byte(REG_CLOCK, reg_clock); + + osc_divider = constrain(osc_divider, 1, 7); + this->clk_x_ = 2000000; + osc_divider = (osc_divider & 0b111) << 4; // 3-bit value, bits 6:4 + + uint8_t reg_misc; + this->read_byte(REG_MISC, ®_misc); + reg_misc &= ~(0b111 << 4); + reg_misc |= osc_divider; + this->write_byte(REG_MISC, reg_misc); +} + +void SX1509Component::setup_keypad_() { + uint8_t temp_byte; + + // setup row/col pins for INPUT OUTPUT + this->read_byte_16(REG_DIR_B, &this->ddr_mask_); + for (int i = 0; i < this->rows_; i++) + this->ddr_mask_ &= ~(1 << i); + for (int i = 8; i < (this->cols_ * 2); i++) + this->ddr_mask_ |= (1 << i); + this->write_byte_16(REG_DIR_B, this->ddr_mask_); + + this->read_byte(REG_OPEN_DRAIN_A, &temp_byte); + for (int i = 0; i < this->rows_; i++) + temp_byte |= (1 << i); + this->write_byte(REG_OPEN_DRAIN_A, temp_byte); + + this->read_byte(REG_PULL_UP_B, &temp_byte); + for (int i = 0; i < this->cols_; i++) + temp_byte |= (1 << i); + this->write_byte(REG_PULL_UP_B, temp_byte); + + if (debounce_time_ >= scan_time_) { + debounce_time_ = scan_time_ >> 1; // Force debounce_time to be less than scan_time + } + set_debounce_keypad_(debounce_time_, rows_, cols_); + uint8_t scan_time_bits = 0; + for (uint8_t i = 7; i > 0; i--) { + if (scan_time_ & (1 << i)) { + scan_time_bits = i; + break; + } + } + scan_time_bits &= 0b111; // Scan time is bits 2:0 + temp_byte = sleep_time_ | scan_time_bits; + this->write_byte(REG_KEY_CONFIG_1, temp_byte); + rows_ = (rows_ - 1) & 0b111; // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc. + cols_ = (cols_ - 1) & 0b111; // 0b000 = 1 column, ob111 = 8 columns, etc. + this->write_byte(REG_KEY_CONFIG_2, (rows_ << 3) | cols_); +} + +uint16_t SX1509Component::read_key_data() { + uint16_t key_data; + this->read_byte_16(REG_KEY_DATA_1, &key_data); + return (0xFFFF ^ key_data); +} + +void SX1509Component::set_debounce_config_(uint8_t config_value) { + // First make sure clock is configured + uint8_t temp_byte; + this->read_byte(REG_MISC, &temp_byte); + temp_byte |= (1 << 4); // Just default to no divider if not set + this->write_byte(REG_MISC, temp_byte); + this->read_byte(REG_CLOCK, &temp_byte); + temp_byte |= (1 << 6); // default to internal osc. + this->write_byte(REG_CLOCK, temp_byte); + + config_value &= 0b111; // 3-bit value + this->write_byte(REG_DEBOUNCE_CONFIG, config_value); +} + +void SX1509Component::set_debounce_time_(uint8_t time) { + uint8_t config_value = 0; + + for (int i = 7; i >= 0; i--) { + if (time & (1 << i)) { + config_value = i + 1; + break; + } + } + config_value = constrain(config_value, 0, 7); + + set_debounce_config_(config_value); +} + +void SX1509Component::set_debounce_enable_(uint8_t pin) { + uint16_t debounce_enable; + this->read_byte_16(REG_DEBOUNCE_ENABLE_B, &debounce_enable); + debounce_enable |= (1 << pin); + this->write_byte_16(REG_DEBOUNCE_ENABLE_B, debounce_enable); +} + +void SX1509Component::set_debounce_pin_(uint8_t pin) { set_debounce_enable_(pin); } + +void SX1509Component::set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols) { + set_debounce_time_(time); + for (uint16_t i = 0; i < num_rows; i++) + set_debounce_pin_(i); + for (uint16_t i = 0; i < (8 + num_cols); i++) + set_debounce_pin_(i); +} + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509.h b/esphome/components/sx1509/sx1509.h new file mode 100644 index 0000000000..55d5e54091 --- /dev/null +++ b/esphome/components/sx1509/sx1509.h @@ -0,0 +1,89 @@ +#pragma once + +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/component.h" +#include "sx1509_gpio_pin.h" +#include "sx1509_registers.h" + +namespace esphome { +namespace sx1509 { + +// These are used for clock config: +const uint8_t INTERNAL_CLOCK_2MHZ = 2; +const uint8_t EXTERNAL_CLOCK = 1; +const uint8_t SOFTWARE_RESET = 0; +const uint8_t HARDWARE_RESET = 1; + +const uint8_t ANALOG_OUTPUT = 0x03; // To set a pin mode for PWM output + +// PinModes for SX1509 pins +enum SX1509GPIOMode : uint8_t { + SX1509_INPUT = INPUT, // 0x00 + SX1509_INPUT_PULLUP = INPUT_PULLUP, // 0x02 + SX1509_ANALOG_OUTPUT = ANALOG_OUTPUT, // 0x03 + SX1509_OUTPUT = OUTPUT, // 0x01 +}; + +const uint8_t REG_I_ON[16] = {REG_I_ON_0, REG_I_ON_1, REG_I_ON_2, REG_I_ON_3, REG_I_ON_4, REG_I_ON_5, + REG_I_ON_6, REG_I_ON_7, REG_I_ON_8, REG_I_ON_9, REG_I_ON_10, REG_I_ON_11, + REG_I_ON_12, REG_I_ON_13, REG_I_ON_14, REG_I_ON_15}; + +// for all components that implement the process(uint16_t data ) +class SX1509Processor { + public: + virtual void process(uint16_t data){}; +}; + +class SX1509Component : public Component, public i2c::I2CDevice { + public: + SX1509Component() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + bool digital_read(uint8_t pin); + uint16_t read_key_data(); + void set_pin_value(uint8_t pin, uint8_t i_on) { this->write_byte(REG_I_ON[pin], i_on); }; + void pin_mode(uint8_t pin, uint8_t mode); + void digital_write(uint8_t pin, bool bit_value); + u_long get_clock() { return this->clk_x_; }; + void set_rows_cols(uint8_t rows, uint8_t cols) { + this->rows_ = rows; + this->cols_ = cols; + this->has_keypad_ = true; + }; + void set_sleep_time(uint16_t sleep_time) { this->sleep_time_ = sleep_time; }; + void set_scan_time(uint8_t scan_time) { this->scan_time_ = scan_time; }; + void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; }; + void register_keypad_binary_sensor(SX1509Processor *binary_sensor) { + this->keypad_binary_sensors_.push_back(binary_sensor); + }; + + protected: + u_long clk_x_ = 2000000; + uint8_t frequency_ = 0; + uint16_t ddr_mask_ = 0x00; + uint16_t input_mask_ = 0x00; + uint16_t port_mask_ = 0x00; + bool has_keypad_ = false; + uint8_t rows_ = 0; + uint8_t cols_ = 0; + uint16_t sleep_time_ = 128; + uint8_t scan_time_ = 1; + uint8_t debounce_time_ = 1; + std::vector keypad_binary_sensors_; + + void setup_keypad_(); + void set_debounce_config_(uint8_t config_value); + void set_debounce_time_(uint8_t time); + void set_debounce_pin_(uint8_t pin); + void set_debounce_enable_(uint8_t pin); + void set_debounce_keypad_(uint8_t time, uint8_t num_rows, uint8_t num_cols); + void setup_led_driver_(uint8_t pin); + void clock_(uint8_t osc_source = 2, uint8_t osc_pin_function = 1, uint8_t osc_freq_out = 0, uint8_t osc_divider = 0); +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp new file mode 100644 index 0000000000..1d1c87b4e6 --- /dev/null +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -0,0 +1,20 @@ +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" +#include "sx1509_gpio_pin.h" + +namespace esphome { +namespace sx1509 { + +static const char *TAG = "sx1509_gpio_pin"; + +void SX1509GPIOPin::setup() { + ESP_LOGD(TAG, "setup pin %d", this->pin_); + this->parent_->pin_mode(this->pin_, this->mode_); +} + +void SX1509GPIOPin::pin_mode(uint8_t mode) { this->parent_->pin_mode(this->pin_, mode); } +bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } +void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509_gpio_pin.h b/esphome/components/sx1509/sx1509_gpio_pin.h new file mode 100644 index 0000000000..39f841a2a4 --- /dev/null +++ b/esphome/components/sx1509/sx1509_gpio_pin.h @@ -0,0 +1,24 @@ +#pragma once + +#include "sx1509.h" + +namespace esphome { +namespace sx1509 { + +class SX1509Component; + +class SX1509GPIOPin : public GPIOPin { + public: + SX1509GPIOPin(SX1509Component *parent, uint8_t pin, uint8_t mode, bool inverted = false) + : GPIOPin(pin, mode, inverted), parent_(parent){}; + void setup() override; + void pin_mode(uint8_t mode) override; + bool digital_read() override; + void digital_write(bool value) override; + + protected: + SX1509Component *parent_; +}; + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/sx1509/sx1509_registers.h b/esphome/components/sx1509/sx1509_registers.h new file mode 100644 index 0000000000..d73f397f16 --- /dev/null +++ b/esphome/components/sx1509/sx1509_registers.h @@ -0,0 +1,109 @@ +/****************************************************************************** +sx1509_registers.h +Register definitions for SX1509. +Jim Lindblom @ SparkFun Electronics +Original Creation Date: September 21, 2015 +https://github.com/sparkfun/SparkFun_SX1509_Arduino_Library + +Here you'll find the Arduino code used to interface with the SX1509 I2C +16 I/O expander. There are functions to take advantage of everything the +SX1509 provides - input/output setting, writing pins high/low, reading +the input value of pins, LED driver utilities (blink, breath, pwm), and +keypad engine utilites. + +Development environment specifics: + IDE: Arduino 1.6.5 + Hardware Platform: Arduino Uno + SX1509 Breakout Version: v2.0 + +This code is beerware; if you see me (or any other SparkFun employee) at the +local, and you've found our code helpful, please buy us a round! + +Distributed as-is; no warranty is given. +******************************************************************************/ +#pragma once + +namespace esphome { +namespace sx1509 { + +const uint8_t REG_INPUT_DISABLE_B = + 0x00; // RegInputDisableB Input buffer disable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_INPUT_DISABLE_A = + 0x01; // RegInputDisableA Input buffer disable register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_LONG_SLEW_B = + 0x02; // RegLongSlewB Output buffer long slew register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_LONG_SLEW_A = 0x03; // RegLongSlewA Output buffer long slew register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_LOW_DRIVE_B = + 0x04; // RegLowDriveB Output buffer low drive register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_LOW_DRIVE_A = 0x05; // RegLowDriveA Output buffer low drive register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_PULL_UP_B = 0x06; // RegPullUpB Pull_up register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_PULL_UP_A = 0x07; // RegPullUpA Pull_up register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_PULL_DOWN_B = 0x08; // RegPullDownB Pull_down register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_PULL_DOWN_A = 0x09; // RegPullDownA Pull_down register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_OPEN_DRAIN_B = 0x0A; // RegOpenDrainB Open drain register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_OPEN_DRAIN_A = 0x0B; // RegOpenDrainA Open drain register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_POLARITY_B = 0x0C; // RegPolarityB Polarity register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_POLARITY_A = 0x0D; // RegPolarityA Polarity register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_DIR_B = 0x0E; // RegDirB Direction register _ I/O[15_8] (Bank B) 1111 1111 +const uint8_t REG_DIR_A = 0x0F; // RegDirA Direction register _ I/O[7_0] (Bank A) 1111 1111 +const uint8_t REG_DATA_B = 0x10; // RegDataB Data register _ I/O[15_8] (Bank B) 1111 1111* +const uint8_t REG_DATA_A = 0x11; // RegDataA Data register _ I/O[7_0] (Bank A) 1111 1111* +const uint8_t REG_INTERRUPT_MASK_B = + 0x12; // RegInterruptMaskB Interrupt mask register _ I/O[15_8] (Bank B) 1111 1111 +const uint8_t REG_INTERRUPT_MASK_A = + 0x13; // RegInterruptMaskA Interrupt mask register _ I/O[7_0] (Bank A) 1111 1111 +const uint8_t REG_SENSE_HIGH_B = 0x14; // RegSenseHighB Sense register for I/O[15:12] 0000 0000 +const uint8_t REG_SENSE_LOW_B = 0x15; // RegSenseLowB Sense register for I/O[11:8] 0000 0000 +const uint8_t REG_SENSE_HIGH_A = 0x16; // RegSenseHighA Sense register for I/O[7:4] 0000 0000 +const uint8_t REG_SENSE_LOW_A = 0x17; // RegSenseLowA Sense register for I/O[3:0] 0000 0000 +const uint8_t REG_INTERRUPT_SOURCE_B = + 0x18; // RegInterruptSourceB Interrupt source register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_INTERRUPT_SOURCE_A = + 0x19; // RegInterruptSourceA Interrupt source register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_EVENT_STATUS_B = 0x1A; // RegEventStatusB Event status register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_EVENT_STATUS_A = 0x1B; // RegEventStatusA Event status register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_LEVEL_SHIFTER_1 = 0x1C; // RegLevelShifter1 Level shifter register 0000 0000 +const uint8_t REG_LEVEL_SHIFTER_2 = 0x1D; // RegLevelShifter2 Level shifter register 0000 0000 +const uint8_t REG_CLOCK = 0x1E; // RegClock Clock management register 0000 0000 +const uint8_t REG_MISC = 0x1F; // RegMisc Miscellaneous device settings register 0000 0000 +const uint8_t REG_LED_DRIVER_ENABLE_B = + 0x20; // RegLEDDriverEnableB LED driver enable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_LED_DRIVER_ENABLE_A = + 0x21; // RegLEDDriverEnableA LED driver enable register _ I/O[7_0] (Bank A) 0000 0000 +// Debounce and Keypad Engine +const uint8_t REG_DEBOUNCE_CONFIG = 0x22; // RegDebounceConfig Debounce configuration register 0000 0000 +const uint8_t REG_DEBOUNCE_ENABLE_B = + 0x23; // RegDebounceEnableB Debounce enable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_DEBOUNCE_ENABLE_A = + 0x24; // RegDebounceEnableA Debounce enable register _ I/O[7_0] (Bank A) 0000 0000 +const uint8_t REG_KEY_CONFIG_1 = 0x25; // RegKeyConfig1 Key scan configuration register 0000 0000 +const uint8_t REG_KEY_CONFIG_2 = 0x26; // RegKeyConfig2 Key scan configuration register 0000 0000 +const uint8_t REG_KEY_DATA_1 = 0x27; // RegKeyData1 Key value (column) 1111 1111 +const uint8_t REG_KEY_DATA_2 = 0x28; // RegKeyData2 Key value (row) 1111 1111 +// LED Driver (PWM, blinking, breathing) +const uint8_t REG_I_ON_0 = 0x2A; // RegIOn0 ON intensity register for I/O[0] 1111 1111 +const uint8_t REG_I_ON_1 = 0x2D; // RegIOn1 ON intensity register for I/O[1] 1111 1111 +const uint8_t REG_I_ON_2 = 0x30; // RegIOn2 ON intensity register for I/O[2] 1111 1111 +const uint8_t REG_I_ON_3 = 0x33; // RegIOn3 ON intensity register for I/O[3] 1111 1111 +const uint8_t REG_I_ON_4 = 0x36; // RegIOn4 ON intensity register for I/O[4] 1111 1111 +const uint8_t REG_I_ON_5 = 0x3B; // RegIOn5 ON intensity register for I/O[5] 1111 1111 +const uint8_t REG_I_ON_6 = 0x40; // RegIOn6 ON intensity register for I/O[6] 1111 1111 +const uint8_t REG_I_ON_7 = 0x45; // RegIOn7 ON intensity register for I/O[7] 1111 1111 +const uint8_t REG_I_ON_8 = 0x4A; // RegIOn8 ON intensity register for I/O[8] 1111 1111 +const uint8_t REG_I_ON_9 = 0x4D; // RegIOn9 ON intensity register for I/O[9] 1111 1111 +const uint8_t REG_I_ON_10 = 0x50; // RegIOn10 ON intensity register for I/O[10] 1111 1111 +const uint8_t REG_I_ON_11 = 0x53; // RegIOn11 ON intensity register for I/O[11] 1111 1111 +const uint8_t REG_I_ON_12 = 0x56; // RegIOn12 ON intensity register for I/O[12] 1111 1111 +const uint8_t REG_I_ON_13 = 0x5B; // RegIOn13 ON intensity register for I/O[13] 1111 1111 +const uint8_t REG_I_ON_14 = 0x60; // RegIOn14 ON intensity register for I/O[14] 1111 1111 +const uint8_t REG_I_ON_15 = 0x65; // RegIOn15 ON intensity register for I/O[15] 1111 1111 +// Miscellaneous +const uint8_t REG_HIGH_INPUT_B = 0x69; // RegHighInputB High input enable register _ I/O[15_8] (Bank B) 0000 0000 +const uint8_t REG_HIGH_INPUT_A = 0x6A; // RegHighInputA High input enable register _ I/O[7_0] (Bank A) 0000 0000 +// Software Reset +const uint8_t REG_RESET = 0x7D; // RegReset Software reset register 0000 0000 +const uint8_t REG_TEST_1 = 0x7E; // RegTest1 Test register 0000 0000 +const uint8_t REG_TEST_2 = 0x7F; // RegTest2 Test register 0000 0000 + +} // namespace sx1509 +} // namespace esphome diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 50fef7b125..d21300d946 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,20 +1,22 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor +from esphome.components import climate, remote_transmitter, remote_receiver, sensor from esphome.const import CONF_ID, CONF_SENSOR -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['sensor', 'climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_RECEIVER_ID = 'receiver_id' CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SUPPORTS_COOL = 'supports_cool' CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Tcl112Climate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), @@ -31,6 +33,9 @@ def to_code(config): if CONF_SENSOR in config: sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_RECEIVER_ID in config: + receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + cg.add(receiver.register_listener(var)) transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index cbe2f53402..2907ae1743 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -18,62 +18,15 @@ const uint8_t TCL112_AUTO = 8; const uint8_t TCL112_POWER_MASK = 0x04; const uint8_t TCL112_HALF_DEGREE = 0b00100000; -const float TCL112_TEMP_MAX = 31.0; -const float TCL112_TEMP_MIN = 16.0; -const uint16_t TCL112_HEADER_MARK = 3000; +const uint16_t TCL112_HEADER_MARK = 3100; const uint16_t TCL112_HEADER_SPACE = 1650; const uint16_t TCL112_BIT_MARK = 500; -const uint16_t TCL112_ONE_SPACE = 1050; -const uint16_t TCL112_ZERO_SPACE = 325; +const uint16_t TCL112_ONE_SPACE = 1100; +const uint16_t TCL112_ZERO_SPACE = 350; const uint32_t TCL112_GAP = TCL112_HEADER_SPACE; -climate::ClimateTraits Tcl112Climate::traits() { - auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); - traits.set_visual_min_temperature(TCL112_TEMP_MIN); - traits.set_visual_max_temperature(TCL112_TEMP_MAX); - traits.set_visual_temperature_step(.5f); - return traits; -} - -void Tcl112Climate::setup() { - if (this->sensor_) { - this->sensor_->add_on_state_callback([this](float state) { - this->current_temperature = state; - // current temperature changed, publish state - this->publish_state(); - }); - this->current_temperature = this->sensor_->state; - } else - this->current_temperature = NAN; - // restore set points - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->apply(this); - } else { - // restore from defaults - this->mode = climate::CLIMATE_MODE_OFF; - this->target_temperature = 24; - } -} - -void Tcl112Climate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - - this->transmit_state_(); - this->publish_state(); -} - -void Tcl112Climate::transmit_state_() { +void Tcl112Climate::transmit_state() { uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; // A known good state. (On, Cool, 24C) @@ -124,7 +77,10 @@ void Tcl112Climate::transmit_state_() { for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) remote_state[TCL112_STATE_LENGTH - 1] += remote_state[checksum_byte]; - ESP_LOGV(TAG, "Sending tcl code: %u", remote_state[7]); + ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0], + remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], + remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12], + remote_state[13]); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -148,5 +104,76 @@ void Tcl112Climate::transmit_state_() { transmit.perform(); } +bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < TCL112_STATE_LENGTH; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE)) + remote_state[i] |= 1 << j; + else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + } + // Validate footer + if (!data.expect_mark(TCL112_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum = 0; + // Calculate & set the checksum for the current internal state of the remote. + // Stored the checksum value in the last byte. + for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) + checksum += remote_state[checksum_byte]; + if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) { + ESP_LOGV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13]); + + // two first bytes are constant + if (remote_state[0] != 0x23 || remote_state[1] != 0xCB) + return false; + + if ((remote_state[5] & TCL112_POWER_MASK) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + auto mode = remote_state[6] & 0x0F; + switch (mode) { + case TCL112_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case TCL112_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case TCL112_DRY: + case TCL112_FAN: + case TCL112_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + auto temp = TCL112_TEMP_MAX - remote_state[7]; + if (remote_state[12] & TCL112_HALF_DEGREE) + temp += .5f; + this->target_temperature = temp; + this->publish_state(); + return true; +} + } // namespace tcl112 } // namespace esphome diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 0b80dedbef..7c5257a0f3 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -1,39 +1,23 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#include "esphome/components/sensor/sensor.h" +#include "esphome/components/climate_ir/climate_ir.h" namespace esphome { namespace tcl112 { -class Tcl112Climate : public climate::Climate, public Component { +// Temperature +const float TCL112_TEMP_MAX = 31.0; +const float TCL112_TEMP_MIN = 16.0; + +class Tcl112Climate : public climate::ClimateIR { public: - void setup() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } - void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + Tcl112Climate() : climate::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} 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; - /// Transmit via IR the state of this climate controller. - void transmit_state_(); - - bool supports_cool_{true}; - bool supports_heat_{true}; - - remote_transmitter::RemoteTransmitterComponent *transmitter_; - sensor::Sensor *sensor_{nullptr}; + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace tcl112 diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 808318ac81..13370d749c 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -4,7 +4,7 @@ from esphome import automation from esphome.components import cover from esphome.const import CONF_ASSUMED_STATE, CONF_CLOSE_ACTION, CONF_CURRENT_OPERATION, CONF_ID, \ CONF_LAMBDA, CONF_OPEN_ACTION, CONF_OPTIMISTIC, CONF_POSITION, CONF_RESTORE_MODE, \ - CONF_STATE, CONF_STOP_ACTION + CONF_STATE, CONF_STOP_ACTION, CONF_TILT, CONF_TILT_ACTION, CONF_TILT_LAMBDA from .. import template_ns TemplateCover = template_ns.class_('TemplateCover', cover.Cover, cg.Component) @@ -24,6 +24,8 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Optional(CONF_OPEN_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Optional(CONF_STOP_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_ACTION): automation.validate_automation(single=True), + cv.Optional(CONF_TILT_LAMBDA): cv.returning_lambda, cv.Optional(CONF_RESTORE_MODE, default='RESTORE'): cv.enum(RESTORE_MODES, upper=True), }).extend(cv.COMPONENT_SCHEMA) @@ -42,6 +44,14 @@ def to_code(config): yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) if CONF_STOP_ACTION in config: yield automation.build_automation(var.get_stop_trigger(), [], config[CONF_STOP_ACTION]) + if CONF_TILT_ACTION in config: + yield automation.build_automation(var.get_tilt_trigger(), [(float, 'tilt')], + config[CONF_TILT_ACTION]) + cg.add(var.set_has_tilt(True)) + if CONF_TILT_LAMBDA in config: + tilt_template_ = yield cg.process_lambda(config[CONF_TILT_LAMBDA], [], + return_type=cg.optional.template(float)) + cg.add(var.set_tilt_lambda(tilt_template_)) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) @@ -53,6 +63,7 @@ def to_code(config): cv.Exclusive(CONF_STATE, 'pos'): cv.templatable(cover.validate_cover_state), cv.Exclusive(CONF_POSITION, 'pos'): cv.templatable(cv.zero_to_one_float), cv.Optional(CONF_CURRENT_OPERATION): cv.templatable(cover.validate_cover_operation), + cv.Optional(CONF_TILT): cv.templatable(cv.zero_to_one_float), })) def cover_template_publish_to_code(config, action_id, template_arg, args): paren = yield cg.get_variable(config[CONF_ID]) @@ -63,6 +74,9 @@ def cover_template_publish_to_code(config, action_id, template_arg, args): if CONF_POSITION in config: template_ = yield cg.templatable(config[CONF_POSITION], args, float) cg.add(var.set_position(template_)) + if CONF_TILT in config: + template_ = yield cg.templatable(config[CONF_TILT], args, float) + cg.add(var.set_tilt(template_)) if CONF_CURRENT_OPERATION in config: template_ = yield cg.templatable(config[CONF_CURRENT_OPERATION], args, cover.CoverOperation) cg.add(var.set_current_operation(template_)) diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index dd1a081aaa..381e6dd6cd 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -74,19 +74,12 @@ void TemplateCover::control(const CoverCall &call) { this->stop_prev_trigger_(); this->stop_trigger_->trigger(); this->prev_command_trigger_ = this->stop_trigger_; - this->current_operation = COVER_OPERATION_IDLE; this->publish_state(); } if (call.get_position().has_value()) { auto pos = *call.get_position(); this->stop_prev_trigger_(); - if (pos < this->position) { - this->current_operation = COVER_OPERATION_CLOSING; - } else if (pos > this->position) { - this->current_operation = COVER_OPERATION_OPENING; - } - if (pos == COVER_OPEN) { this->open_trigger_->trigger(); this->prev_command_trigger_ = this->open_trigger_; diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index a81255a254..ca1ac375ba 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -2,6 +2,7 @@ import bisect import datetime import logging import math +import string import pytz import tzlocal @@ -52,6 +53,25 @@ def _tz_dst_str(dt): _tz_timedelta(td)) +def _safe_tzname(tz, dt): + tzname = tz.tzname(dt) + # pytz does not always return valid tznames + # For example: 'Europe/Saratov' returns '+04' + # Work around it by using a generic name for the timezone + if not all(c in string.ascii_letters for c in tzname): + return 'TZ' + return tzname + + +def _non_dst_tz(tz, dt): + tzname = _safe_tzname(tz, dt) + utcoffset = tz.utcoffset(dt) + _LOGGER.info("Detected timezone '%s' with UTC offset %s", + tzname, _tz_timedelta(utcoffset)) + tzbase = '{}{}'.format(tzname, _tz_timedelta(-1 * utcoffset)) + return tzbase + + def convert_tz(pytz_obj): tz = pytz_obj @@ -59,23 +79,29 @@ def convert_tz(pytz_obj): first_january = datetime.datetime(year=now.year, month=1, day=1) if not isinstance(tz, pytz.tzinfo.DstTzInfo): - tzname = tz.tzname(first_january) - utcoffset = tz.utcoffset(first_january) - _LOGGER.info("Detected timezone '%s' with UTC offset %s", - tzname, _tz_timedelta(utcoffset)) - tzbase = '{}{}'.format(tzname, _tz_timedelta(-1 * utcoffset)) - return tzbase + return _non_dst_tz(tz, first_january) # pylint: disable=protected-access transition_times = tz._utc_transition_times transition_info = tz._transition_info idx = max(0, bisect.bisect_right(transition_times, now)) + if idx >= len(transition_times): + return _non_dst_tz(tz, now) + idx1, idx2 = idx, idx + 1 dstoffset1 = transition_info[idx1][1] if dstoffset1 == datetime.timedelta(seconds=0): # Normalize to 1 being DST on idx1, idx2 = idx + 1, idx + 2 + if idx2 >= len(transition_times): + return _non_dst_tz(tz, now) + + if transition_times[idx2].year > now.year + 1: + # Next transition is scheduled after this year + # Probably a scheduler timezone change. + return _non_dst_tz(tz, now) + utcoffset_on, _, tzname_on = transition_info[idx1] utcoffset_off, _, tzname_off = transition_info[idx2] dst_begins_utc = transition_times[idx1] @@ -89,8 +115,9 @@ def convert_tz(pytz_obj): _tz_dst_str(dst_begins_local), _tz_dst_str(dst_ends_local)) _LOGGER.info("Detected timezone '%s' with UTC offset %s and daylight savings time from " "%s to %s", - tzname_off, _tz_timedelta(utcoffset_off), dst_begins_local.strftime("%x %X"), - dst_ends_local.strftime("%x %X")) + tzname_off, _tz_timedelta(utcoffset_off), + dst_begins_local.strftime("%d %B %X"), + dst_ends_local.strftime("%d %B %X")) return tzbase + tzext @@ -244,10 +271,12 @@ def validate_tz(value): value = cv.string_strict(value) try: - return convert_tz(pytz.timezone(value)) - except Exception: # pylint: disable=broad-except + pytz_obj = pytz.timezone(value) + except pytz.UnknownTimeZoneError: # pylint: disable=broad-except return value + return convert_tz(pytz_obj) + TIME_SCHEMA = cv.Schema({ cv.Optional(CONF_TIMEZONE, default=detect_tz): validate_tz, diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 81524826be..cb66dc3ce6 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -12,7 +12,6 @@ static const char *TAG = "time"; RealTimeClock::RealTimeClock() = default; void RealTimeClock::call_setup() { - this->setup_internal_(); setenv("TZ", this->timezone_.c_str(), 1); tzset(); this->setup(); @@ -85,12 +84,12 @@ template bool increment_time_value(T ¤t, uint16_t begin, uint1 static bool is_leap_year(uint32_t year) { return (year % 4) == 0 && ((year % 100) != 0 || (year % 400) == 0); } -static bool days_in_month(uint8_t month, uint16_t year) { +static uint8_t days_in_month(uint8_t month, uint16_t year) { static const uint8_t DAYS_IN_MONTH[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - uint8_t days_in_month = DAYS_IN_MONTH[month]; + uint8_t days = DAYS_IN_MONTH[month]; if (month == 2 && is_leap_year(year)) - days_in_month = 29; - return days_in_month; + return 29; + return days; } void ESPTime::increment_second() { @@ -128,13 +127,13 @@ void ESPTime::recalc_timestamp_utc(bool use_day_of_year) { return; } - for (uint16_t i = 1970; i < this->year; i++) + for (int i = 1970; i < this->year; i++) res += is_leap_year(i) ? 366 : 365; if (use_day_of_year) { res += this->day_of_year - 1; } else { - for (uint8_t i = 1; i < this->month; ++i) + for (int i = 1; i < this->month; i++) res += days_in_month(i, this->year); res += this->day_of_month - 1; diff --git a/esphome/components/time_based/cover.py b/esphome/components/time_based/cover.py index 85f606e6cc..6a7c9b6835 100644 --- a/esphome/components/time_based/cover.py +++ b/esphome/components/time_based/cover.py @@ -8,6 +8,8 @@ from esphome.const import CONF_CLOSE_ACTION, CONF_CLOSE_DURATION, CONF_ID, CONF_ time_based_ns = cg.esphome_ns.namespace('time_based') TimeBasedCover = time_based_ns.class_('TimeBasedCover', cover.Cover, cg.Component) +CONF_HAS_BUILT_IN_ENDSTOP = 'has_built_in_endstop' + CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(TimeBasedCover), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), @@ -17,6 +19,8 @@ CONFIG_SCHEMA = cover.COVER_SCHEMA.extend({ cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, + + cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, }).extend(cv.COMPONENT_SCHEMA) @@ -32,3 +36,5 @@ def to_code(config): cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) yield automation.build_automation(var.get_close_trigger(), [], config[CONF_CLOSE_ACTION]) + + cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index bbc887debc..bdb4e5379c 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -30,13 +30,19 @@ void TimeBasedCover::loop() { // Recompute position every loop cycle this->recompute_position_(); - if (this->current_operation != COVER_OPERATION_IDLE && this->is_at_target_()) { - this->start_direction_(COVER_OPERATION_IDLE); + if (this->is_at_target_()) { + if (this->has_built_in_endstop_ && + (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) { + // Don't trigger stop, let the cover stop by itself. + this->current_operation = COVER_OPERATION_IDLE; + } else { + this->start_direction_(COVER_OPERATION_IDLE); + } this->publish_state(); } // Send current position every second - if (this->current_operation != COVER_OPERATION_IDLE && now - this->last_publish_time_ > 1000) { + if (now - this->last_publish_time_ > 1000) { this->publish_state(false); this->last_publish_time_ = now; } @@ -57,6 +63,12 @@ void TimeBasedCover::control(const CoverCall &call) { auto pos = *call.get_position(); if (pos == this->position) { // already at target + // for covers with built in end stop, we should send the command again + if (this->has_built_in_endstop_ && (pos == COVER_OPEN || pos == COVER_CLOSED)) { + auto op = pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; + this->target_position_ = pos; + this->start_direction_(op); + } } else { auto op = pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING; this->target_position_ = pos; @@ -82,7 +94,7 @@ bool TimeBasedCover::is_at_target_() const { } } void TimeBasedCover::start_direction_(CoverOperation dir) { - if (dir == this->current_operation) + if (dir == this->current_operation && dir != COVER_OPERATION_IDLE) return; this->recompute_position_(); diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index 60819d797b..be3a55c546 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -20,6 +20,7 @@ class TimeBasedCover : public cover::Cover, public Component { void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } cover::CoverTraits get_traits() override; + void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } protected: void control(const cover::CoverCall &call) override; @@ -41,6 +42,7 @@ class TimeBasedCover : public cover::Cover, public Component { uint32_t start_dir_time_{0}; uint32_t last_publish_time_{0}; float target_position_{0}; + bool has_built_in_endstop_{false}; }; } // namespace time_based diff --git a/esphome/components/tlc59208f/__init__.py b/esphome/components/tlc59208f/__init__.py new file mode 100644 index 0000000000..4666b63b46 --- /dev/null +++ b/esphome/components/tlc59208f/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +tlc59208f_ns = cg.esphome_ns.namespace('tlc59208f') +TLC59208FOutput = tlc59208f_ns.class_('TLC59208FOutput', cg.Component, i2c.I2CDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(TLC59208FOutput), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x20)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/tlc59208f/output.py b/esphome/components/tlc59208f/output.py new file mode 100644 index 0000000000..f61f7729e7 --- /dev/null +++ b/esphome/components/tlc59208f/output.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import output +from esphome.const import CONF_CHANNEL, CONF_ID +from . import TLC59208FOutput, tlc59208f_ns + +DEPENDENCIES = ['tlc59208f'] + +TLC59208FChannel = tlc59208f_ns.class_('TLC59208FChannel', output.FloatOutput) +CONF_TLC59208F_ID = 'tlc59208f_id' + +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ + cv.Required(CONF_ID): cv.declare_id(TLC59208FChannel), + cv.GenerateID(CONF_TLC59208F_ID): cv.use_id(TLC59208FOutput), + + cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), +}) + + +def to_code(config): + paren = yield cg.get_variable(config[CONF_TLC59208F_ID]) + rhs = paren.create_channel(config[CONF_CHANNEL]) + var = cg.Pvariable(config[CONF_ID], rhs) + yield output.register_output(var, config) diff --git a/esphome/components/tlc59208f/tlc59208f_output.cpp b/esphome/components/tlc59208f/tlc59208f_output.cpp new file mode 100644 index 0000000000..6e65ff4e76 --- /dev/null +++ b/esphome/components/tlc59208f/tlc59208f_output.cpp @@ -0,0 +1,155 @@ +#include "tlc59208f_output.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tlc59208f { + +static const char *TAG = "tlc59208f"; + +// * marks register defaults +// 0*: Register auto increment disabled, 1: Register auto increment enabled +const uint8_t TLC59208F_MODE1_AI2 = (1 << 7); +// 0*: don't auto increment bit 1, 1: auto increment bit 1 +const uint8_t TLC59208F_MODE1_AI1 = (1 << 6); +// 0*: don't auto increment bit 0, 1: auto increment bit 0 +const uint8_t TLC59208F_MODE1_AI0 = (1 << 5); +// 0: normal mode, 1*: low power mode, osc off +const uint8_t TLC59208F_MODE1_SLEEP = (1 << 4); +// 0*: device doesn't respond to i2c bus sub-address 1, 1: responds +const uint8_t TLC59208F_MODE1_SUB1 = (1 << 3); +// 0*: device doesn't respond to i2c bus sub-address 2, 1: responds +const uint8_t TLC59208F_MODE1_SUB2 = (1 << 2); +// 0*: device doesn't respond to i2c bus sub-address 3, 1: responds +const uint8_t TLC59208F_MODE1_SUB3 = (1 << 1); +// 0: device doesn't respond to i2c all-call 3, 1*: responds to all-call +const uint8_t TLC59208F_MODE1_ALLCALL = (1 << 0); + +// 0*: Group dimming, 1: Group blinking +const uint8_t TLC59208F_MODE2_DMBLNK = (1 << 5); +// 0*: Output change on Stop command, 1: Output change on ACK +const uint8_t TLC59208F_MODE2_OCH = (1 << 3); +// 0*: WDT disabled, 1: WDT enabled +const uint8_t TLC59208F_MODE2_WDTEN = (1 << 2); +// WDT timeouts +const uint8_t TLC59208F_MODE2_WDT_5MS = (0 << 0); +const uint8_t TLC59208F_MODE2_WDT_15MS = (1 << 0); +const uint8_t TLC59208F_MODE2_WDT_25MS = (2 << 0); +const uint8_t TLC59208F_MODE2_WDT_35MS = (3 << 0); + +// --- Special function --- +// Call address to perform software reset, no devices will ACK +const uint8_t TLC59208F_SWRST_ADDR = 0x96; //(0x4b 7-bit addr + ~W) +const uint8_t TLC59208F_SWRST_SEQ[2] = {0xa5, 0x5a}; + +// --- Registers ---2 +// Mode register 1 +const uint8_t TLC59208F_REG_MODE1 = 0x00; +// Mode register 2 +const uint8_t TLC59208F_REG_MODE2 = 0x01; +// PWM0 +const uint8_t TLC59208F_REG_PWM0 = 0x02; +// Group PWM +const uint8_t TLC59208F_REG_GROUPPWM = 0x0a; +// Group Freq +const uint8_t TLC59208F_REG_GROUPFREQ = 0x0b; +// LEDOUTx registers +const uint8_t TLC59208F_REG_LEDOUT0 = 0x0c; +const uint8_t TLC59208F_REG_LEDOUT1 = 0x0d; +// Sub-address registers +const uint8_t TLC59208F_REG_SUBADR1 = 0x0e; // default: 0x92 (8-bit addr) +const uint8_t TLC59208F_REG_SUBADR2 = 0x0f; // default: 0x94 (8-bit addr) +const uint8_t TLC59208F_REG_SUBADR3 = 0x10; // default: 0x98 (8-bit addr) +// All call address register +const uint8_t TLC59208F_REG_ALLCALLADR = 0x11; // default: 0xd0 (8-bit addr) + +// --- Output modes --- +static const uint8_t LDR_OFF = 0x00; +static const uint8_t LDR_ON = 0x01; +static const uint8_t LDR_PWM = 0x02; +static const uint8_t LDR_GRPPWM = 0x03; + +void TLC59208FOutput::setup() { + ESP_LOGCONFIG(TAG, "Setting up TLC59208FOutputComponent..."); + + ESP_LOGV(TAG, " Resetting all devices on the bus..."); + + // Reset all devices on the bus + if (!this->parent_->write_byte(TLC59208F_SWRST_ADDR >> 1, TLC59208F_SWRST_SEQ[0], TLC59208F_SWRST_SEQ[1])) { + ESP_LOGE(TAG, "RESET failed"); + this->mark_failed(); + return; + } + + // Auto increment registers, and respond to all-call address + if (!this->write_byte(TLC59208F_REG_MODE1, TLC59208F_MODE1_AI2 | TLC59208F_MODE1_ALLCALL)) { + ESP_LOGE(TAG, "MODE1 failed"); + this->mark_failed(); + return; + } + if (!this->write_byte(TLC59208F_REG_MODE2, this->mode_)) { + ESP_LOGE(TAG, "MODE2 failed"); + this->mark_failed(); + return; + } + // Set all 3 outputs to be individually controlled + // TODO: think of a way to support group dimming + if (!this->write_byte(TLC59208F_REG_LEDOUT0, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) { + ESP_LOGE(TAG, "LEDOUT0 failed"); + this->mark_failed(); + return; + } + if (!this->write_byte(TLC59208F_REG_LEDOUT1, (LDR_PWM << 6) | (LDR_PWM << 4) | (LDR_PWM << 2) | (LDR_PWM << 0))) { + ESP_LOGE(TAG, "LEDOUT1 failed"); + this->mark_failed(); + return; + } + delayMicroseconds(500); + + this->loop(); +} + +void TLC59208FOutput::dump_config() { + ESP_LOGCONFIG(TAG, "TLC59208F:"); + ESP_LOGCONFIG(TAG, " Mode: 0x%02X", this->mode_); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Setting up TLC59208F failed!"); + } +} + +void TLC59208FOutput::loop() { + if (this->min_channel_ == 0xFF || !this->update_) + return; + + for (uint8_t channel = this->min_channel_; channel <= this->max_channel_; channel++) { + uint8_t pwm = this->pwm_amounts_[channel]; + ESP_LOGVV(TAG, "Channel %02u: pwm=%04u ", channel, pwm); + + uint8_t reg = TLC59208F_REG_PWM0 + channel; + if (!this->write_byte(reg, pwm)) { + this->status_set_warning(); + return; + } + } + + this->status_clear_warning(); + this->update_ = false; +} + +TLC59208FChannel *TLC59208FOutput::create_channel(uint8_t channel) { + this->min_channel_ = std::min(this->min_channel_, channel); + this->max_channel_ = std::max(this->max_channel_, channel); + auto *c = new TLC59208FChannel(this, channel); + return c; +} + +void TLC59208FChannel::write_state(float state) { + const uint8_t max_duty = 255; + const float duty_rounded = roundf(state * max_duty); + auto duty = static_cast(duty_rounded); + this->parent_->set_channel_value_(this->channel_, duty); +} + +} // namespace tlc59208f +} // namespace esphome diff --git a/esphome/components/tlc59208f/tlc59208f_output.h b/esphome/components/tlc59208f/tlc59208f_output.h new file mode 100644 index 0000000000..06b7adc882 --- /dev/null +++ b/esphome/components/tlc59208f/tlc59208f_output.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/output/float_output.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tlc59208f { + +// 0*: Group dimming, 1: Group blinking +extern const uint8_t TLC59208F_MODE2_DMBLNK; +// 0*: Output change on Stop command, 1: Output change on ACK +extern const uint8_t TLC59208F_MODE2_OCH; +// 0*: WDT disabled, 1: WDT enabled +extern const uint8_t TLC59208F_MODE2_WDTEN; +// WDT timeouts +extern const uint8_t TLC59208F_MODE2_WDT_5MS; +extern const uint8_t TLC59208F_MODE2_WDT_15MS; +extern const uint8_t TLC59208F_MODE2_WDT_25MS; +extern const uint8_t TLC59208F_MODE2_WDT_35MS; + +class TLC59208FOutput; + +class TLC59208FChannel : public output::FloatOutput { + public: + TLC59208FChannel(TLC59208FOutput *parent, uint8_t channel) : parent_(parent), channel_(channel) {} + + protected: + void write_state(float state) override; + + TLC59208FOutput *parent_; + uint8_t channel_; +}; + +/// TLC59208F float output component. +class TLC59208FOutput : public Component, public i2c::I2CDevice { + public: + TLC59208FOutput(uint8_t mode = TLC59208F_MODE2_OCH) : mode_(mode) {} + + TLC59208FChannel *create_channel(uint8_t channel); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + + protected: + friend TLC59208FChannel; + + void set_channel_value_(uint8_t channel, uint8_t value) { + if (this->pwm_amounts_[channel] != value) + this->update_ = true; + this->pwm_amounts_[channel] = value; + } + + uint8_t mode_; + + uint8_t min_channel_{0xFF}; + uint8_t max_channel_{0x00}; + uint8_t pwm_amounts_[256] = { + 0, + }; + bool update_{true}; +}; + +} // namespace tlc59208f +} // namespace esphome diff --git a/esphome/components/tx20/__init__.py b/esphome/components/tx20/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tx20/sensor.py b/esphome/components/tx20/sensor.py new file mode 100644 index 0000000000..daa6677196 --- /dev/null +++ b/esphome/components/tx20/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import sensor +from esphome.const import CONF_ID, CONF_WIND_SPEED, CONF_PIN, \ + CONF_WIND_DIRECTION_DEGREES, UNIT_KILOMETER_PER_HOUR, \ + UNIT_EMPTY, ICON_WEATHER_WINDY, ICON_SIGN_DIRECTION + +tx20_ns = cg.esphome_ns.namespace('tx20') +Tx20Component = tx20_ns.class_('Tx20Component', cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Tx20Component), + cv.Optional(CONF_WIND_SPEED): + sensor.sensor_schema(UNIT_KILOMETER_PER_HOUR, ICON_WEATHER_WINDY, 1), + cv.Optional(CONF_WIND_DIRECTION_DEGREES): + sensor.sensor_schema(UNIT_EMPTY, ICON_SIGN_DIRECTION, 1), + cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema, + pins.validate_has_interrupt), +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + if CONF_WIND_SPEED in config: + conf = config[CONF_WIND_SPEED] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_wind_speed_sensor(sens)) + + if CONF_WIND_DIRECTION_DEGREES in config: + conf = config[CONF_WIND_DIRECTION_DEGREES] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_wind_direction_degrees_sensor(sens)) + + pin = yield cg.gpio_pin_expression(config[CONF_PIN]) + cg.add(var.set_pin(pin)) diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp new file mode 100644 index 0000000000..f3dafda288 --- /dev/null +++ b/esphome/components/tx20/tx20.cpp @@ -0,0 +1,195 @@ +#include "tx20.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tx20 { + +static const char *TAG = "tx20"; +static const uint8_t MAX_BUFFER_SIZE = 41; +static const uint16_t TX20_MAX_TIME = MAX_BUFFER_SIZE * 1200 + 5000; +static const uint16_t TX20_BIT_TIME = 1200; +static const char *DIRECTIONS[] = {"N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", + "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"}; + +void Tx20Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up Tx20"); + this->pin_->setup(); + + this->store_.buffer = new uint16_t[MAX_BUFFER_SIZE]; + this->store_.pin = this->pin_->to_isr(); + this->store_.reset(); + + this->pin_->attach_interrupt(Tx20ComponentStore::gpio_intr, &this->store_, CHANGE); +} +void Tx20Component::dump_config() { + ESP_LOGCONFIG(TAG, "Tx20:"); + + LOG_SENSOR(" ", "Wind speed:", this->wind_speed_sensor_); + LOG_SENSOR(" ", "Wind direction degrees:", this->wind_direction_degrees_sensor_); + + LOG_PIN(" Pin: ", this->pin_); +} +void Tx20Component::loop() { + if (this->store_.tx20_available) { + this->decode_and_publish_(); + this->store_.reset(); + } +} + +float Tx20Component::get_setup_priority() const { return setup_priority::DATA; } + +std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; } + +void Tx20Component::decode_and_publish_() { + ESP_LOGVV(TAG, "Decode Tx20..."); + + std::string string_buffer; + std::string string_buffer_2; + std::vector bit_buffer; + bool current_bit = true; + + for (int i = 1; i <= this->store_.buffer_index; i++) { + string_buffer_2 += to_string(this->store_.buffer[i]) + ", "; + uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME; + // ignore segments at the end that were too short + string_buffer.append(repeat, current_bit ? '1' : '0'); + bit_buffer.insert(bit_buffer.end(), repeat, current_bit); + current_bit = !current_bit; + } + current_bit = !current_bit; + if (string_buffer.length() < MAX_BUFFER_SIZE) { + uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length(); + string_buffer_2 += to_string(remain) + ", "; + string_buffer.append(remain, current_bit ? '1' : '0'); + bit_buffer.insert(bit_buffer.end(), remain, current_bit); + } + + uint8_t tx20_sa = 0; + uint8_t tx20_sb = 0; + uint8_t tx20_sd = 0; + uint8_t tx20_se = 0; + uint16_t tx20_sc = 0; + uint16_t tx20_sf = 0; + uint8_t tx20_wind_direction = 0; + float tx20_wind_speed_kmh = 0; + uint8_t bit_count = 0; + + for (int i = 41; i > 0; i--) { + uint8_t bit = bit_buffer.at(bit_count); + bit_count++; + if (i > 41 - 5) { + // start, inverted + tx20_sa = (tx20_sa << 1) | (bit ^ 1); + } else if (i > 41 - 5 - 4) { + // wind dir, inverted + tx20_sb = tx20_sb >> 1 | ((bit ^ 1) << 3); + } else if (i > 41 - 5 - 4 - 12) { + // windspeed, inverted + tx20_sc = tx20_sc >> 1 | ((bit ^ 1) << 11); + } else if (i > 41 - 5 - 4 - 12 - 4) { + // checksum, inverted + tx20_sd = tx20_sd >> 1 | ((bit ^ 1) << 3); + } else if (i > 41 - 5 - 4 - 12 - 4 - 4) { + // wind dir + tx20_se = tx20_se >> 1 | (bit << 3); + } else { + // windspeed + tx20_sf = tx20_sf >> 1 | (bit << 11); + } + } + + uint8_t chk = (tx20_sb + (tx20_sc & 0xf) + ((tx20_sc >> 4) & 0xf) + ((tx20_sc >> 8) & 0xf)); + chk &= 0xf; + bool value_set = false; + // checks: + // 1. Check that the start frame is 00100 (0x04) + // 2. Check received checksum matches calculated checksum + // 3. Check that Wind Direction matches Wind Direction (Inverted) + // 4. Check that Wind Speed matches Wind Speed (Inverted) + ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str()); + ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str()); + + if (tx20_sa == 4) { + if (chk == tx20_sd) { + if (tx20_sf == tx20_sc) { + tx20_wind_speed_kmh = float(tx20_sc) * 0.36; + ESP_LOGV(TAG, "WindSpeed %f", tx20_wind_speed_kmh); + if (this->wind_speed_sensor_ != nullptr) + this->wind_speed_sensor_->publish_state(tx20_wind_speed_kmh); + value_set = true; + } + if (tx20_se == tx20_sb) { + tx20_wind_direction = tx20_se; + if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) { + wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction]; + } + ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction); + if (this->wind_direction_degrees_sensor_ != nullptr) + this->wind_direction_degrees_sensor_->publish_state(float(tx20_wind_direction) * 22.5f); + value_set = true; + } + if (!value_set) { + ESP_LOGW(TAG, "No value set!"); + } + } else { + ESP_LOGW(TAG, "Checksum wrong!"); + } + } else { + ESP_LOGW(TAG, "Start wrong!"); + } +} + +void ICACHE_RAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) { + arg->pin_state = arg->pin->digital_read(); + const uint32_t now = micros(); + if (!arg->start_time) { + // only detect a start if the bit is high + if (!arg->pin_state) { + return; + } + arg->buffer[arg->buffer_index] = 1; + arg->start_time = now; + arg->buffer_index++; + return; + } + const uint32_t delay = now - arg->start_time; + const uint8_t index = arg->buffer_index; + + // first delay has to be ~2400 + if (index == 1 && (delay > 3000 || delay < 2400)) { + arg->reset(); + return; + } + // second delay has to be ~1200 + if (index == 2 && (delay > 1500 || delay < 1200)) { + arg->reset(); + return; + } + // third delay has to be ~2400 + if (index == 3 && (delay > 3000 || delay < 2400)) { + arg->reset(); + return; + } + + if (arg->tx20_available || ((arg->spent_time + delay > TX20_MAX_TIME) && arg->start_time)) { + arg->tx20_available = true; + return; + } + if (index <= MAX_BUFFER_SIZE) { + arg->buffer[index] = delay; + } + arg->spent_time += delay; + arg->start_time = now; + arg->buffer_index++; +} +void ICACHE_RAM_ATTR Tx20ComponentStore::reset() { + tx20_available = false; + buffer_index = 0; + spent_time = 0; + // rearm it! + start_time = 0; +} + +} // namespace tx20 +} // namespace esphome diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h new file mode 100644 index 0000000000..8b79deffbc --- /dev/null +++ b/esphome/components/tx20/tx20.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace tx20 { + +/// Store data in a class that doesn't use multiple-inheritance (vtables in flash) +struct Tx20ComponentStore { + volatile uint16_t *buffer; + volatile uint32_t start_time; + volatile uint8_t buffer_index; + volatile uint32_t spent_time; + volatile bool tx20_available; + volatile bool pin_state; + ISRInternalGPIOPin *pin; + + void reset(); + static void gpio_intr(Tx20ComponentStore *arg); +}; + +/// This class implements support for the Tx20 Wind sensor. +class Tx20Component : public Component { + public: + /// Get the textual representation of the wind direction ('N', 'SSE', ..). + std::string get_wind_cardinal_direction() const; + + void set_pin(GPIOPin *pin) { pin_ = pin; } + void set_wind_speed_sensor(sensor::Sensor *wind_speed_sensor) { wind_speed_sensor_ = wind_speed_sensor; } + void set_wind_direction_degrees_sensor(sensor::Sensor *wind_direction_degrees_sensor) { + wind_direction_degrees_sensor_ = wind_direction_degrees_sensor; + } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void decode_and_publish_(); + + std::string wind_cardinal_direction_; + GPIOPin *pin_; + sensor::Sensor *wind_speed_sensor_; + sensor::Sensor *wind_direction_degrees_sensor_; + Tx20ComponentStore store_; +}; + +} // namespace tx20 +} // namespace esphome diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index c374568149..110bd64c81 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -31,13 +31,14 @@ def validate_rx_pin(value): CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTComponent), - cv.Required(CONF_BAUD_RATE): cv.int_range(min=1, max=115200), + 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, }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) def to_code(config): + cg.add_global(uart_ns.using) var = cg.new_Pvariable(config[CONF_ID]) yield cg.register_component(var, config) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 56661b8aa7..83ae81490e 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -20,10 +20,6 @@ void UARTComponent::setup() { // is 1 we still want to use Serial. if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; - } else if (this->tx_pin_.value_or(9) == 9 && this->rx_pin_.value_or(10) == 10) { - this->hw_serial_ = &Serial1; - } else if (this->tx_pin_.value_or(16) == 16 && this->rx_pin_.value_or(17) == 17) { - this->hw_serial_ = &Serial2; } else { this->hw_serial_ = new HardwareSerial(next_uart_num++); } @@ -295,12 +291,12 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { this->write_bit_(true, &wait, start); enable_interrupts(); } -void ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { +void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) { while (ESP.getCycleCount() - start < *wait) ; *wait += this->bit_time_; } -bool ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { +bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) { this->wait_(wait, start); return this->rx_pin_->digital_read(); } @@ -320,7 +316,9 @@ uint8_t ESP8266SoftwareSerial::peek_byte() { return 0; return this->rx_buffer_[this->rx_out_pos_]; } -void ESP8266SoftwareSerial::flush() { this->rx_in_pos_ = this->rx_out_pos_ = 0; } +void ESP8266SoftwareSerial::flush() { + // Flush is a NO-OP with software serial, all bytes are written immediately. +} int ESP8266SoftwareSerial::available() { int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); if (avail < 0) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 666b8e2fb2..3b347c1ff7 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -24,13 +24,13 @@ class ESP8266SoftwareSerial { protected: static void gpio_intr(ESP8266SoftwareSerial *arg); - inline void wait_(uint32_t *wait, const uint32_t &start); - inline bool read_bit_(uint32_t *wait, const uint32_t &start); - inline void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); + void wait_(uint32_t *wait, const uint32_t &start); + bool read_bit_(uint32_t *wait, const uint32_t &start); + void write_bit_(bool bit, uint32_t *wait, const uint32_t &start); uint32_t bit_time_{0}; uint8_t *rx_buffer_{nullptr}; - size_t rx_buffer_size_{64}; + size_t rx_buffer_size_{512}; volatile size_t rx_in_pos_{0}; size_t rx_out_pos_{0}; ISRInternalGPIOPin *tx_pin_{nullptr}; @@ -61,6 +61,7 @@ class UARTComponent : public Component, public Stream { int available() override; + /// Block until all bytes have been written to the UART bus. void flush() override; float get_setup_priority() const override { return setup_priority::BUS; } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 5d4cd48bc1..f8130f7d1f 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -26,7 +26,6 @@ void UltrasonicSensorComponent::update() { this->publish_state(NAN); } else { float result = UltrasonicSensorComponent::us_to_m(time); - this->publish_state(result); ESP_LOGD(TAG, "'%s' - Got distance: %.2f m", this->name_.c_str(), result); this->publish_state(result); } diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 1e59deff96..6aedfdedcd 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -1,6 +1,7 @@ #include "version_text_sensor.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/version.h" namespace esphome { namespace version { diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 77a0bbc689..a8ffbcc538 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -21,10 +21,11 @@ WaveshareEPaperTypeBModel = waveshare_epaper_ns.enum('WaveshareEPaperTypeBModel' MODELS = { '1.54in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_1_54_IN), '2.13in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_13_IN), + '2.13in-ttgo': ('a', WaveshareEPaperTypeAModel.TTGO_EPAPER_2_13_IN), '2.90in': ('a', WaveshareEPaperTypeAModel.WAVESHARE_EPAPER_2_9_IN), '2.70in': ('b', WaveshareEPaper2P7In), '4.20in': ('b', WaveshareEPaper4P2In), - '7.50in': ('b', WaveshareEPaperTypeBModel), + '7.50in': ('b', WaveshareEPaper7P5In), } diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 2fe12dc102..c2f7acde40 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -8,13 +8,45 @@ namespace waveshare_epaper { static const char *TAG = "waveshare_epaper"; -static const uint8_t FULL_UPDATE_LUT[30] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, - 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, - 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00}; +static const uint8_t LUT_SIZE_WAVESHARE = 30; +static const uint8_t FULL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = {0x02, 0x02, 0x01, 0x11, 0x12, 0x12, 0x22, 0x22, 0x66, 0x69, + 0x69, 0x59, 0x58, 0x99, 0x99, 0x88, 0x00, 0x00, 0x00, 0x00, + 0xF8, 0xB4, 0x13, 0x51, 0x35, 0x51, 0x51, 0x19, 0x01, 0x00}; -static const uint8_t PARTIAL_UPDATE_LUT[30] = {0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t PARTIAL_UPDATE_LUT[LUT_SIZE_WAVESHARE] = { + 0x10, 0x18, 0x18, 0x08, 0x18, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x14, 0x44, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + +static const uint8_t LUT_SIZE_TTGO = 70; +static const uint8_t FULL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { + 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 + 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 + 0x80, 0x60, 0x40, 0x00, 0x00, 0x00, 0x00, // LUT2: WB: VS 0 ~7 + 0x10, 0x60, 0x20, 0x00, 0x00, 0x00, 0x00, // LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM: VS 0 ~7 + 0x03, 0x03, 0x00, 0x00, 0x02, // TP0 A~D RP0 + 0x09, 0x09, 0x00, 0x00, 0x02, // TP1 A~D RP1 + 0x03, 0x03, 0x00, 0x00, 0x02, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 +}; + +static const uint8_t PARTIAL_UPDATE_LUT_TTGO[LUT_SIZE_TTGO] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT0: BB: VS 0 ~7 + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT1: BW: VS 0 ~7 + 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT2: WB: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT3: WW: VS 0 ~7 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // LUT4: VCOM: VS 0 ~7 + 0x0A, 0x00, 0x00, 0x00, 0x00, // TP0 A~D RP0 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP1 A~D RP1 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP2 A~D RP2 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP3 A~D RP3 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP4 A~D RP4 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP5 A~D RP5 + 0x00, 0x00, 0x00, 0x00, 0x00, // TP6 A~D RP6 +}; void WaveshareEPaper::setup_pins_() { this->init_internal_(this->get_buffer_length_()); @@ -42,7 +74,6 @@ void WaveshareEPaper::data(uint8_t value) { this->write_byte(value); this->end_data_(); } -bool WaveshareEPaper::is_device_msb_first() { return true; } bool WaveshareEPaper::wait_until_idle_() { if (this->busy_pin_ == nullptr) { return true; @@ -81,7 +112,6 @@ void HOT WaveshareEPaper::draw_absolute_pixel_internal(int x, int y, int color) this->buffer_[pos] &= ~(0x80 >> subpos); } uint32_t WaveshareEPaper::get_buffer_length_() { return this->get_width_internal() * this->get_height_internal() / 8u; } -bool WaveshareEPaper::is_device_high_speed() { return true; } void WaveshareEPaper::start_command_() { this->dc_pin_->digital_write(false); this->enable(); @@ -136,6 +166,9 @@ void WaveshareEPaperTypeA::dump_config() { case WAVESHARE_EPAPER_2_13_IN: ESP_LOGCONFIG(TAG, " Model: 2.13in"); break; + case TTGO_EPAPER_2_13_IN: + ESP_LOGCONFIG(TAG, " Model: 2.13in (TTGO)"); + break; case WAVESHARE_EPAPER_2_9_IN: ESP_LOGCONFIG(TAG, " Model: 2.9in"); break; @@ -156,7 +189,11 @@ void HOT WaveshareEPaperTypeA::display() { bool prev_full_update = this->at_update_ == 1; bool full_update = this->at_update_ == 0; if (full_update != prev_full_update) { - this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT); + if (this->model_ == TTGO_EPAPER_2_13_IN) { + this->write_lut_(full_update ? FULL_UPDATE_LUT_TTGO : PARTIAL_UPDATE_LUT_TTGO, LUT_SIZE_TTGO); + } else { + this->write_lut_(full_update ? FULL_UPDATE_LUT : PARTIAL_UPDATE_LUT, LUT_SIZE_WAVESHARE); + } } this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; } @@ -208,6 +245,8 @@ int WaveshareEPaperTypeA::get_width_internal() { return 200; case WAVESHARE_EPAPER_2_13_IN: return 128; + case TTGO_EPAPER_2_13_IN: + return 128; case WAVESHARE_EPAPER_2_9_IN: return 128; } @@ -219,15 +258,17 @@ int WaveshareEPaperTypeA::get_height_internal() { return 200; case WAVESHARE_EPAPER_2_13_IN: return 250; + case TTGO_EPAPER_2_13_IN: + return 250; case WAVESHARE_EPAPER_2_9_IN: return 296; } return 0; } -void WaveshareEPaperTypeA::write_lut_(const uint8_t *lut) { +void WaveshareEPaperTypeA::write_lut_(const uint8_t *lut, const uint8_t size) { // COMMAND WRITE LUT REGISTER this->command(0x32); - for (uint8_t i = 0; i < 30; i++) + for (uint8_t i = 0; i < size; i++) this->data(lut[i]); } WaveshareEPaperTypeA::WaveshareEPaperTypeA(WaveshareEPaperTypeAModel model) : model_(model) {} @@ -495,7 +536,6 @@ void HOT WaveshareEPaper4P2In::display() { } int WaveshareEPaper4P2In::get_width_internal() { return 400; } int WaveshareEPaper4P2In::get_height_internal() { return 300; } -bool WaveshareEPaper4P2In::is_device_high_speed() { return false; } void WaveshareEPaper4P2In::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 4.2in"); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index 192e85275e..13aebd4ec9 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -7,14 +7,16 @@ namespace esphome { namespace waveshare_epaper { -class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public display::DisplayBuffer { +class WaveshareEPaper : public PollingComponent, + public display::DisplayBuffer, + public spi::SPIDevice { public: void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } float get_setup_priority() const override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } - bool is_device_msb_first() override; void command(uint8_t value); void data(uint8_t value); @@ -51,8 +53,6 @@ class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public d uint32_t get_buffer_length_(); - bool is_device_high_speed() override; - void start_command_(); void end_command_(); void start_data_(); @@ -67,6 +67,7 @@ enum WaveshareEPaperTypeAModel { WAVESHARE_EPAPER_1_54_IN = 0, WAVESHARE_EPAPER_2_13_IN, WAVESHARE_EPAPER_2_9_IN, + TTGO_EPAPER_2_13_IN, }; class WaveshareEPaperTypeA : public WaveshareEPaper { @@ -88,7 +89,7 @@ class WaveshareEPaperTypeA : public WaveshareEPaper { void set_full_update_every(uint32_t full_update_every); protected: - void write_lut_(const uint8_t *lut); + void write_lut_(const uint8_t *lut, uint8_t size); int get_width_internal() override; @@ -166,8 +167,6 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { int get_width_internal() override; int get_height_internal() override; - - bool is_device_high_speed() override; }; class WaveshareEPaper7P5In : public WaveshareEPaper { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 206fc2c733..04f3cc5c04 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -1,10 +1,13 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.const import CONF_CSS_URL, CONF_ID, CONF_JS_URL, CONF_PORT -from esphome.core import CORE, coroutine_with_priority +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_URL, CONF_ID, CONF_JS_URL, CONF_PORT, + CONF_AUTH, CONF_USERNAME, CONF_PASSWORD) +from esphome.core import coroutine_with_priority -DEPENDENCIES = ['network'] -AUTO_LOAD = ['json'] +AUTO_LOAD = ['json', 'web_server_base'] web_server_ns = cg.esphome_ns.namespace('web_server') WebServer = web_server_ns.class_('WebServer', cg.Component, cg.Controller) @@ -14,18 +17,25 @@ CONFIG_SCHEMA = cv.Schema({ cv.Optional(CONF_PORT, default=80): cv.port, cv.Optional(CONF_CSS_URL, default="https://esphome.io/_static/webserver-v1.min.css"): cv.string, cv.Optional(CONF_JS_URL, default="https://esphome.io/_static/webserver-v1.min.js"): cv.string, + cv.Optional(CONF_AUTH): cv.Schema({ + cv.Required(CONF_USERNAME): cv.string_strict, + cv.Required(CONF_PASSWORD): cv.string_strict, + }), + + cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(web_server_base.WebServerBase), }).extend(cv.COMPONENT_SCHEMA) @coroutine_with_priority(40.0) def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + paren = yield cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) + + var = cg.new_Pvariable(config[CONF_ID], paren) yield cg.register_component(var, config) - cg.add(var.set_port(config[CONF_PORT])) + cg.add(paren.set_port(config[CONF_PORT])) cg.add(var.set_css_url(config[CONF_CSS_URL])) cg.add(var.set_js_url(config[CONF_JS_URL])) - - if CORE.is_esp32: - cg.add_library('FS', None) - cg.add_library('ESP Async WebServer', '1.1.1') + if CONF_AUTH in config: + cg.add(var.set_username(config[CONF_AUTH][CONF_USERNAME])) + cg.add(var.set_password(config[CONF_AUTH][CONF_PASSWORD])) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 882af4b995..b51ad2cf51 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -6,15 +6,11 @@ #include "StreamString.h" -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include -#endif - #include + +#ifdef USE_LOGGER #include +#endif namespace esphome { namespace web_server { @@ -66,7 +62,7 @@ void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::setup() { ESP_LOGCONFIG(TAG, "Setting up web server..."); - this->server_ = new AsyncWebServer(this->port_); + this->base_->init(); this->events_.onConnect([this](AsyncEventSourceClient *client) { // Configure reconnect timeout @@ -114,91 +110,21 @@ void WebServer::setup() { logger::global_logger->add_on_log_callback( [this](int level, const char *tag, const char *message) { this->events_.send(message, "log", millis()); }); #endif - this->server_->addHandler(this); - this->server_->addHandler(&this->events_); - - this->server_->begin(); + this->base_->add_handler(&this->events_); + this->base_->add_handler(this); + this->base_->add_ota_handler(); this->set_interval(10000, [this]() { this->events_.send("", "ping", millis(), 30000); }); } void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); - ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->port_); + ESP_LOGCONFIG(TAG, " Address: %s:%u", network_get_address().c_str(), this->base_->get_port()); + if (this->using_auth()) { + ESP_LOGCONFIG(TAG, " Basic authentication enabled"); + } } float WebServer::get_setup_priority() const { return setup_priority::WIFI - 1.0f; } -void WebServer::handle_update_request(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response; - if (!Update.hasError()) { - response = request->beginResponse(200, "text/plain", "Update Successful!"); - } else { - StreamString ss; - ss.print("Update Failed: "); - Update.printError(ss); - response = request->beginResponse(200, "text/plain", ss); - } - response->addHeader("Connection", "close"); - request->send(response); -} - -void report_ota_error() { - StreamString ss; - Update.printError(ss); - ESP_LOGW(TAG, "OTA Update failed! Error: %s", ss.c_str()); -} - -void WebServer::handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, - size_t len, bool final) { - bool success; - if (index == 0) { - ESP_LOGI(TAG, "OTA Update Start: %s", filename.c_str()); - this->ota_read_length_ = 0; -#ifdef ARDUINO_ARCH_ESP8266 - Update.runAsync(true); - success = Update.begin((ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000); -#endif -#ifdef ARDUINO_ARCH_ESP32 - if (Update.isRunning()) - Update.abort(); - success = Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH); -#endif - if (!success) { - report_ota_error(); - return; - } - } else if (Update.hasError()) { - // don't spam logs with errors if something failed at start - return; - } - - success = Update.write(data, len) == len; - if (!success) { - report_ota_error(); - return; - } - this->ota_read_length_ += len; - - const uint32_t now = millis(); - if (now - this->last_ota_progress_ > 1000) { - if (request->contentLength() != 0) { - float percentage = (this->ota_read_length_ * 100.0f) / request->contentLength(); - ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage); - } else { - ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_); - } - this->last_ota_progress_ = now; - } - - if (final) { - if (Update.end(true)) { - ESP_LOGI(TAG, "OTA update successful!"); - this->set_timeout(100, []() { App.safe_reboot(); }); - } else { - report_ota_error(); - } - } -} - void WebServer::handle_index_request(AsyncWebServerRequest *request) { AsyncResponseStream *stream = request->beginResponseStream("text/html"); std::string title = App.get_name() + " Web Server"; @@ -248,7 +174,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream->print(F("

See ESPHome Web API for " "REST API documentation.

" - "

OTA Update

OTA Update
" "

Debug Log

"
                   "
@@ -31,19 +31,23 @@
       
Enter credentials -

- Please login using your Home Assistant credentials. -

+ {% if hassio %} +

+ Please login using your Home Assistant credentials. +

+ {% end %} {% if error is not None %}

{{ escape(error) }}

{% end %}
-
- - -
+ {% if has_username or hassio %} +
+ + +
+ {% end %}
@@ -65,7 +69,7 @@