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 3278827486..3db0b982ae 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.8.3' + BASE_VERSION: '2.0.1' 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; +} +void CaptivePortal::dump_config() { ESP_LOGCONFIG(TAG, "Captive Portal:"); } + +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..3af47546cf --- /dev/null +++ b/esphome/components/captive_portal/captive_portal.h @@ -0,0 +1,82 @@ +#pragma once + +#include +#include "esphome/core/component.h" +#include "esphome/core/helpers.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 dump_config() 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..4dd872bbed 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -9,6 +9,11 @@ namespace esphome { namespace climate { +#define LOG_CLIMATE(prefix, type, obj) \ + if (obj != nullptr) { \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + } + class Climate; /** This class is used to encode all control actions on a climate device. @@ -121,6 +126,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..1163705faa --- /dev/null +++ b/esphome/components/climate_ir/__init__.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate, remote_transmitter, remote_receiver, sensor, remote_base +from esphome.components.remote_base import CONF_RECEIVER_ID, CONF_TRANSMITTER_ID +from esphome.const import CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT, CONF_SENSOR +from esphome.core import coroutine + +AUTO_LOAD = ['sensor', 'remote_base'] + +climate_ir_ns = cg.esphome_ns.namespace('climate_ir') +ClimateIR = climate_ir_ns.class_('ClimateIR', climate.Climate, cg.Component, + remote_base.RemoteReceiverListener) + +CLIMATE_IR_SCHEMA = climate.CLIMATE_SCHEMA.extend({ + cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + 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), +}).extend(cv.COMPONENT_SCHEMA) + +CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend({ + cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), +}) + + +@coroutine +def register_climate_ir(var, config): + yield cg.register_component(var, config) + yield climate.register_climate(var, config) + + cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) + cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) + 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/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp new file mode 100644 index 0000000000..4b9a1c0baa --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -0,0 +1,67 @@ +#include "climate_ir.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace climate_ir { + +static const char *TAG = "climate_ir"; + +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(); +} +void ClimateIR::dump_config() { + LOG_CLIMATE("", "IR Climate", this); + ESP_LOGCONFIG(TAG, " Min. Temperature: %.1f°C", this->minimum_temperature_); + ESP_LOGCONFIG(TAG, " Max. Temperature: %.1f°C", this->maximum_temperature_); + ESP_LOGCONFIG(TAG, " Supports HEAT: %s", YESNO(this->supports_heat_)); + ESP_LOGCONFIG(TAG, " Supports COOL: %s", YESNO(this->supports_cool_)); +} + +} // namespace climate_ir +} // 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..b4c036f3d6 --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.h @@ -0,0 +1,55 @@ +#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_ir { + +/* 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 dump_config() 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() = 0; + + bool supports_cool_{true}; + bool supports_heat_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; + +} // namespace climate_ir +} // namespace esphome diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index 750a97d087..81412bb586 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -1,36 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components import climate_ir +from esphome.const import CONF_ID -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['climate_ir'] coolix_ns = cg.esphome_ns.namespace('coolix') -CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) +CoolixClimate = coolix_ns.class_('CoolixClimate', climate_ir.ClimateIR) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(CoolixClimate), - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - 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), -}).extend(cv.COMPONENT_SCHEMA)) +}) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - - cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_sensor(sens)) - - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) + yield climate_ir.register_climate_ir(var, config) 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..ed03a2fd1e 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_ir::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_ir::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/cover/cover.h b/esphome/components/cover/cover.h index 12011e1b4c..839cf9207e 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -13,13 +13,13 @@ const extern float COVER_CLOSED; #define LOG_COVER(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ auto traits_ = obj->get_traits(); \ if (traits_.get_is_assumed_state()) { \ - ESP_LOGCONFIG(TAG, prefix " Assumed State: YES"); \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ if (!obj->get_device_class().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Device Class: '%s'", obj->get_device_class().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, obj->get_device_class().c_str()); \ } \ } diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 358453a63a..6c014138fd 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -172,6 +172,7 @@ void CSE7766Component::dump_config() { LOG_SENSOR(" ", "Voltage", this->voltage_sensor_); LOG_SENSOR(" ", "Current", this->current_sensor_); LOG_SENSOR(" ", "Power", this->power_sensor_); + this->check_uart_settings(4800); } } // namespace cse7766 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/custom/output/__init__.py b/esphome/components/custom/output/__init__.py index 6042863872..efe6f19dab 100644 --- a/esphome/components/custom/output/__init__.py +++ b/esphome/components/custom/output/__init__.py @@ -1,13 +1,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import output -from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_OUTPUTS, CONF_TYPE, CONF_BINARY from .. import custom_ns CustomBinaryOutputConstructor = custom_ns.class_('CustomBinaryOutputConstructor') CustomFloatOutputConstructor = custom_ns.class_('CustomFloatOutputConstructor') -CONF_BINARY = 'binary' CONF_FLOAT = 'float' CONFIG_SCHEMA = cv.typed_schema({ 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/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/dfplayer/__init__.py b/esphome/components/dfplayer/__init__.py new file mode 100644 index 0000000000..890c2bede4 --- /dev/null +++ b/esphome/components/dfplayer/__init__.py @@ -0,0 +1,219 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome.const import CONF_ID, CONF_TRIGGER_ID, CONF_FILE, CONF_DEVICE +from esphome.components import uart + +DEPENDENCIES = ['uart'] + +dfplayer_ns = cg.esphome_ns.namespace('dfplayer') +DFPlayer = dfplayer_ns.class_('DFPlayer', cg.Component) +DFPlayerFinishedPlaybackTrigger = dfplayer_ns.class_('DFPlayerFinishedPlaybackTrigger', + automation.Trigger.template()) +DFPlayerIsPlayingCondition = dfplayer_ns.class_('DFPlayerIsPlayingCondition', automation.Condition) + +MULTI_CONF = True +CONF_FOLDER = 'folder' +CONF_LOOP = 'loop' +CONF_VOLUME = 'volume' +CONF_EQ_PRESET = 'eq_preset' +CONF_ON_FINISHED_PLAYBACK = 'on_finished_playback' + +EqPreset = dfplayer_ns.enum("EqPreset") +EQ_PRESET = { + 'NORMAL': EqPreset.NORMAL, + 'POP': EqPreset.POP, + 'ROCK': EqPreset.ROCK, + 'JAZZ': EqPreset.JAZZ, + 'CLASSIC': EqPreset.CLASSIC, + 'BASS': EqPreset.BASS, +} +Device = dfplayer_ns.enum("Device") +DEVICE = { + 'USB': Device.USB, + 'TF_CARD': Device.TF_CARD, +} + +NextAction = dfplayer_ns.class_('NextAction', automation.Action) +PreviousAction = dfplayer_ns.class_('PreviousAction', automation.Action) +PlayFileAction = dfplayer_ns.class_('PlayFileAction', automation.Action) +PlayFolderAction = dfplayer_ns.class_('PlayFolderAction', automation.Action) +SetVolumeAction = dfplayer_ns.class_('SetVolumeAction', automation.Action) +SetEqAction = dfplayer_ns.class_('SetEqAction', automation.Action) +SleepAction = dfplayer_ns.class_('SleepAction', automation.Action) +ResetAction = dfplayer_ns.class_('ResetAction', automation.Action) +StartAction = dfplayer_ns.class_('StartAction', automation.Action) +PauseAction = dfplayer_ns.class_('PauseAction', automation.Action) +StopAction = dfplayer_ns.class_('StopAction', automation.Action) +RandomAction = dfplayer_ns.class_('RandomAction', automation.Action) +SetDeviceAction = dfplayer_ns.class_('SetDeviceAction', automation.Action) + +CONFIG_SCHEMA = cv.All(cv.Schema({ + cv.GenerateID(): cv.declare_id(DFPlayer), + cv.Optional(CONF_ON_FINISHED_PLAYBACK): automation.validate_automation({ + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DFPlayerFinishedPlaybackTrigger), + }), +}).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_FINISHED_PLAYBACK, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + +@automation.register_action('dfplayer.play_next', NextAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_next_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play_previous', PreviousAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_previous_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.play', PlayFileAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +}, key=CONF_FILE)) +def dfplayer_play_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.play_folder', PlayFolderAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_FOLDER): cv.templatable(cv.int_), + cv.Optional(CONF_FILE): cv.templatable(cv.int_), + cv.Optional(CONF_LOOP): cv.templatable(cv.boolean), +})) +def dfplayer_play_folder_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_FOLDER], args, float) + cg.add(var.set_folder(template_)) + if CONF_FILE in config: + template_ = yield cg.templatable(config[CONF_FILE], args, float) + cg.add(var.set_file(template_)) + if CONF_LOOP in config: + template_ = yield cg.templatable(config[CONF_LOOP], args, float) + cg.add(var.set_loop(template_)) + yield var + + +@automation.register_action('dfplayer.set_device', SetDeviceAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_DEVICE): cv.enum(DEVICE, upper=True), +}, key=CONF_DEVICE)) +def dfplayer_set_device_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_DEVICE], args, Device) + cg.add(var.set_device(template_)) + yield var + + +@automation.register_action('dfplayer.set_volume', SetVolumeAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_VOLUME): cv.templatable(cv.int_), +}, key=CONF_VOLUME)) +def dfplayer_set_volume_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_VOLUME], args, float) + cg.add(var.set_volume(template_)) + yield var + + +@automation.register_action('dfplayer.set_eq', SetEqAction, cv.maybe_simple_value({ + cv.GenerateID(): cv.use_id(DFPlayer), + cv.Required(CONF_EQ_PRESET): cv.templatable(cv.enum(EQ_PRESET, upper=True)), +}, key=CONF_EQ_PRESET)) +def dfplayer_set_eq_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + template_ = yield cg.templatable(config[CONF_EQ_PRESET], args, EqPreset) + cg.add(var.set_eq(template_)) + yield var + + +@automation.register_action('dfplayer.sleep', SleepAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_sleep_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.reset', ResetAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_reset_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.start', StartAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_start_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.pause', PauseAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_pause_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.stop', StopAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_stop_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action('dfplayer.random', RandomAction, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplayer_random_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_condition('dfplayer.is_playing', DFPlayerIsPlayingCondition, cv.Schema({ + cv.GenerateID(): cv.use_id(DFPlayer), +})) +def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): + var = cg.new_Pvariable(condition_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp new file mode 100644 index 0000000000..5ce4998796 --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -0,0 +1,124 @@ +#include "dfplayer.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace dfplayer { + +static const char* TAG = "dfplayer"; + +void DFPlayer::play_folder(uint16_t folder, uint16_t file) { + if (folder < 100 && file < 256) { + this->send_cmd_(0x0F, (uint8_t) folder, (uint8_t) file); + } else if (folder <= 10 && file <= 1000) { + this->send_cmd_(0x14, (((uint16_t) folder) << 12) | file); + } else { + ESP_LOGE(TAG, "Cannot play folder %d file %d.", folder, file); + } +} + +void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { + uint8_t buffer[10]{0x7e, 0xff, 0x06, cmd, 0x01, (uint8_t)(argument >> 8), (uint8_t) argument, 0x00, 0x00, 0xef}; + uint16_t checksum = 0; + for (uint8_t i = 1; i < 7; i++) + checksum += buffer[i]; + checksum = -checksum; + buffer[7] = checksum >> 8; + buffer[8] = (uint8_t) checksum; + + this->sent_cmd_ = cmd; + + ESP_LOGD(TAG, "Send Command %#02x arg %#04x", cmd, argument); + this->write_array(buffer, 10); +} + +void DFPlayer::loop() { + // Read message + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + + if (this->read_pos_ == DFPLAYER_READ_BUFFER_LENGTH) + this->read_pos_ = 0; + + switch (this->read_pos_) { + case 0: // Start mark + if (byte != 0x7E) + continue; + break; + case 1: // Version + if (byte != 0xFF) { + ESP_LOGW(TAG, "Expected Version 0xFF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 2: // Buffer length + if (byte != 0x06) { + ESP_LOGW(TAG, "Expected Buffer length 0x06, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + break; + case 9: // End byte + if (byte != 0xEF) { + ESP_LOGW(TAG, "Expected end byte 0xEF, got %#02x", byte); + this->read_pos_ = 0; + continue; + } + // Parse valid received command + uint8_t cmd = this->read_buffer_[3]; + uint16_t argument = (this->read_buffer_[5] << 8) | this->read_buffer_[6]; + + ESP_LOGV(TAG, "Received message cmd: %#02x arg %#04x", cmd, argument); + + switch (cmd) { + case 0x3A: + if (argument == 1) { + ESP_LOGI(TAG, "USB loaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card loaded"); + break; + case 0x3B: + if (argument == 1) { + ESP_LOGI(TAG, "USB unloaded"); + } else if (argument == 2) + ESP_LOGI(TAG, "TF Card unloaded"); + break; + case 0x3F: + if (argument == 1) { + ESP_LOGI(TAG, "USB available"); + } else if (argument == 2) { + ESP_LOGI(TAG, "TF Card available"); + } else if (argument == 3) { + ESP_LOGI(TAG, "USB, TF Card available"); + } + break; + case 0x41: + ESP_LOGV(TAG, "Ack ok"); + this->is_playing_ |= this->ack_set_is_playing_; + this->is_playing_ &= !this->ack_reset_is_playing_; + this->ack_set_is_playing_ = false; + this->ack_reset_is_playing_ = false; + break; + case 0x3D: // Playback finished + this->is_playing_ = false; + this->on_finished_playback_callback_.call(); + break; + default: + ESP_LOGD(TAG, "Command %#02x arg %#04x", cmd, argument); + } + this->sent_cmd_ = 0; + this->read_pos_ = 0; + continue; + } + this->read_buffer_[this->read_pos_] = byte; + this->read_pos_++; + } +} +void DFPlayer::dump_config() { + ESP_LOGCONFIG(TAG, "DFPlayer:"); + this->check_uart_settings(9600); +} + +} // namespace dfplayer +} // namespace esphome diff --git a/esphome/components/dfplayer/dfplayer.h b/esphome/components/dfplayer/dfplayer.h new file mode 100644 index 0000000000..86efd62138 --- /dev/null +++ b/esphome/components/dfplayer/dfplayer.h @@ -0,0 +1,166 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +const size_t DFPLAYER_READ_BUFFER_LENGTH = 25; // two messages + some extra + +namespace esphome { +namespace dfplayer { + +enum EqPreset { + NORMAL = 0, + POP = 1, + ROCK = 2, + JAZZ = 3, + CLASSIC = 4, + BASS = 5, +}; + +enum Device { + USB = 1, + TF_CARD = 2, +}; + +class DFPlayer : public uart::UARTDevice, public Component { + public: + void loop() override; + + void next() { this->send_cmd_(0x01); } + void previous() { this->send_cmd_(0x02); } + void play_file(uint16_t file) { + this->ack_set_is_playing_ = true; + this->send_cmd_(0x03, file); + } + void play_file_loop(uint16_t file) { this->send_cmd_(0x08, file); } + void play_folder(uint16_t folder, uint16_t file); + void play_folder_loop(uint16_t folder) { this->send_cmd_(0x17, folder); } + void volume_up() { this->send_cmd_(0x04); } + void volume_down() { this->send_cmd_(0x05); } + void set_device(Device device) { this->send_cmd_(0x09, device); } + void set_volume(uint8_t volume) { this->send_cmd_(0x06, volume); } + void set_eq(EqPreset preset) { this->send_cmd_(0x07, preset); } + void sleep() { this->send_cmd_(0x0A); } + void reset() { this->send_cmd_(0x0C); } + void start() { this->send_cmd_(0x0D); } + void pause() { + this->ack_reset_is_playing_ = true; + this->send_cmd_(0x0E); + } + void stop() { this->send_cmd_(0x16); } + void random() { this->send_cmd_(0x18); } + + bool is_playing() { return is_playing_; } + void dump_config() override; + + void add_on_finished_playback_callback(std::function callback) { + this->on_finished_playback_callback_.add(std::move(callback)); + } + + protected: + void send_cmd_(uint8_t cmd, uint16_t argument = 0); + void send_cmd_(uint8_t cmd, uint16_t high, uint16_t low) { + this->send_cmd_(cmd, ((high & 0xFF) << 8) | (low & 0xFF)); + } + uint8_t sent_cmd_{0}; + + char read_buffer_[DFPLAYER_READ_BUFFER_LENGTH]; + size_t read_pos_{0}; + + bool is_playing_{false}; + bool ack_set_is_playing_{false}; + bool ack_reset_is_playing_{false}; + + CallbackManager on_finished_playback_callback_; +}; + +#define DFPLAYER_SIMPLE_ACTION(ACTION_CLASS, ACTION_METHOD) \ + template class ACTION_CLASS : public Action, public Parented { \ + public: \ + void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \ + }; + +DFPLAYER_SIMPLE_ACTION(NextAction, next) +DFPLAYER_SIMPLE_ACTION(PreviousAction, previous) + +template class PlayFileAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_file_loop(file); + } else { + this->parent_->play_file(file); + } + } +}; + +template class PlayFolderAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, folder) + TEMPLATABLE_VALUE(uint16_t, file) + TEMPLATABLE_VALUE(boolean, loop) + void play(Ts... x) override { + auto folder = this->folder_.value(x...); + auto file = this->file_.value(x...); + auto loop = this->loop_.value(x...); + if (loop) { + this->parent_->play_folder_loop(folder); + } else { + this->parent_->play_folder(folder, file); + } + } +}; + +template class SetDeviceAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(Device, device) + void play(Ts... x) override { + auto device = this->device_.value(x...); + this->parent_->set_device(device); + } +}; + +template class SetVolumeAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, volume) + void play(Ts... x) override { + auto volume = this->volume_.value(x...); + this->parent_->set_volume(volume); + } +}; + +template class SetEqAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(EqPreset, eq) + void play(Ts... x) override { + auto eq = this->eq_.value(x...); + this->parent_->set_eq(eq); + } +}; + +DFPLAYER_SIMPLE_ACTION(SleepAction, sleep) +DFPLAYER_SIMPLE_ACTION(ResetAction, reset) +DFPLAYER_SIMPLE_ACTION(StartAction, start) +DFPLAYER_SIMPLE_ACTION(PauseAction, pause) +DFPLAYER_SIMPLE_ACTION(StopAction, stop) +DFPLAYER_SIMPLE_ACTION(RandomAction, random) + +template class DFPlayerIsPlayingCondition : public Condition, public Parented { + public: + bool check(Ts... x) override { return this->parent_->is_playing(); } +}; + +class DFPlayerFinishedPlaybackTrigger : public Trigger<> { + public: + explicit DFPlayerFinishedPlaybackTrigger(DFPlayer *parent) { + parent->add_on_finished_playback_callback([this]() { this->trigger(); }); + } +}; + +} // namespace dfplayer +} // namespace esphome diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 797137c08c..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,13 +155,15 @@ 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); @@ -170,18 +174,20 @@ bool HOT DHT::read_sensor_(float *temperature, float *humidity, bool report_erro if (raw_temperature == 1 && raw_humidity == 10) { if (report_errors) { - ESP_LOGE(TAG, "Invalid temperature+humidity! Sensor reported 1°C and 1%% Hum"); + 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/display/display_buffer.h b/esphome/components/display/display_buffer.h index 57b95eee29..b12fad8c8a 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -84,8 +84,8 @@ using display_writer_t = std::function; #define LOG_DISPLAY(prefix, type, obj) \ if (obj != nullptr) { \ ESP_LOGCONFIG(TAG, prefix type); \ - ESP_LOGCONFIG(TAG, prefix " Rotations: %d °", obj->rotation_); \ - ESP_LOGCONFIG(TAG, prefix " Dimensions: %dpx x %dpx", obj->get_width(), obj->get_height()); \ + ESP_LOGCONFIG(TAG, "%s Rotations: %d °", prefix, obj->rotation_); \ + ESP_LOGCONFIG(TAG, "%s Dimensions: %dpx x %dpx", prefix, obj->get_width(), obj->get_height()); \ } class DisplayBuffer { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index 1f3bf01a86..b7810bd056 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -31,6 +31,11 @@ static esp_ble_adv_params_t ble_adv_params = { static esp_ble_ibeacon_head_t ibeacon_common_head = { .flags = {0x02, 0x01, 0x06}, .length = 0x1A, .type = 0xFF, .company_id = 0x004C, .beacon_type = 0x1502}; +void ESP32BLEBeacon::dump_config() { + ESP_LOGCONFIG(TAG, "ESP32 BLE Beacon:"); + ESP_LOGCONFIG(TAG, " Major: %u, Minor: %u", this->major_, this->minor_); +} + void ESP32BLEBeacon::setup() { ESP_LOGCONFIG(TAG, "Setting up ESP32 BLE beacon..."); global_esp32_ble_beacon = this; @@ -50,7 +55,7 @@ void ESP32BLEBeacon::ble_core_task(void *params) { ble_setup(); while (true) { - delay(1000); + delay(1000); // NOLINT } } void ESP32BLEBeacon::ble_setup() { diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 1c52b41d73..aba02830b3 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -34,6 +34,7 @@ class ESP32BLEBeacon : public Component { explicit ESP32BLEBeacon(const std::array &uuid) : uuid_(uuid) {} void setup() override; + void dump_config() override; float get_setup_priority() const override; void set_major(uint16_t major) { this->major_ = major; } 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 3aaf8eaa44..7a5bd733a2 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -1,6 +1,7 @@ #include "esp32_ble_tracker.h" #include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #ifdef ARDUINO_ARCH_ESP32 @@ -133,14 +134,14 @@ bool ESP32BLETracker::ble_setup() { } // BLE takes some time to be fully set up, 200ms should be more than enough - delay(200); + delay(200); // NOLINT return true; } 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; } @@ -150,21 +151,16 @@ 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_interval_ * 2000, []() { + this->set_timeout("scan", this->scan_duration_ * 2000, []() { ESP_LOGW(TAG, "ESP-IDF BLE scan never terminated, rebooting to restore BLE stack..."); App.reboot(); }); @@ -207,20 +203,8 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } -std::string hexencode(const std::string &raw_data) { - char buf[20]; - std::string res; - for (size_t i = 0; i < raw_data.size(); i++) { - if (i + 1 != raw_data.size()) { - sprintf(buf, "0x%02X.", static_cast(raw_data[i])); - } else { - sprintf(buf, "0x%02X ", static_cast(raw_data[i])); - } - res += buf; - } - sprintf(buf, "(%zu)", raw_data.size()); - res += buf; - return res; +std::string hexencode_string(const std::string &raw_data) { + return hexencode(reinterpret_cast(raw_data.c_str()), raw_data.size()); } ESPBTUUID::ESPBTUUID() : uuid_() {} @@ -332,15 +316,15 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e for (auto uuid : this->service_uuids_) { ESP_LOGVV(TAG, " Service UUID: %s", uuid.to_string().c_str()); } - ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode(this->manufacturer_data_).c_str()); - ESP_LOGVV(TAG, " Service data: %s", hexencode(this->service_data_).c_str()); + ESP_LOGVV(TAG, " Manufacturer data: %s", hexencode_string(this->manufacturer_data_).c_str()); + ESP_LOGVV(TAG, " Service data: %s", hexencode_string(this->service_data_).c_str()); if (this->service_data_uuid_.has_value()) { ESP_LOGVV(TAG, " Service Data UUID: %s", this->service_data_uuid_->to_string().c_str()); } ESP_LOGVV(TAG, "Adv data: %s", - hexencode(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); + hexencode_string(std::string(reinterpret_cast(param.ble_adv), param.adv_data_len)).c_str()); #endif } void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { @@ -454,10 +438,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_camera/__init__.py b/esphome/components/esp32_camera/__init__.py index c69e0a5710..81980d9d38 100644 --- a/esphome/components/esp32_camera/__init__.py +++ b/esphome/components/esp32_camera/__init__.py @@ -2,7 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import CONF_FREQUENCY, CONF_ID, CONF_NAME, CONF_PIN, CONF_SCL, CONF_SDA, \ - ESP_PLATFORM_ESP32 + ESP_PLATFORM_ESP32, CONF_DATA_PINS, CONF_RESET_PIN, CONF_RESOLUTION, CONF_BRIGHTNESS ESP_PLATFORMS = [ESP_PLATFORM_ESP32] DEPENDENCIES = ['api'] @@ -35,23 +35,19 @@ FRAME_SIZES = { 'UXGA': ESP32CameraFrameSize.ESP32_CAMERA_SIZE_1600X1200, } -CONF_DATA_PINS = 'data_pins' CONF_VSYNC_PIN = 'vsync_pin' CONF_HREF_PIN = 'href_pin' CONF_PIXEL_CLOCK_PIN = 'pixel_clock_pin' CONF_EXTERNAL_CLOCK = 'external_clock' CONF_I2C_PINS = 'i2c_pins' -CONF_RESET_PIN = 'reset_pin' CONF_POWER_DOWN_PIN = 'power_down_pin' CONF_MAX_FRAMERATE = 'max_framerate' CONF_IDLE_FRAMERATE = 'idle_framerate' -CONF_RESOLUTION = 'resolution' CONF_JPEG_QUALITY = 'jpeg_quality' CONF_VERTICAL_FLIP = 'vertical_flip' CONF_HORIZONTAL_MIRROR = 'horizontal_mirror' CONF_CONTRAST = 'contrast' -CONF_BRIGHTNESS = 'brightness' CONF_SATURATION = 'saturation' CONF_TEST_PATTERN = 'test_pattern' diff --git a/esphome/components/esp32_touch/esp32_touch.cpp b/esphome/components/esp32_touch/esp32_touch.cpp index e85d0ef5c2..ce0028159e 100644 --- a/esphome/components/esp32_touch/esp32_touch.cpp +++ b/esphome/components/esp32_touch/esp32_touch.cpp @@ -108,6 +108,8 @@ void ESP32TouchComponent::dump_config() { } void ESP32TouchComponent::loop() { + const uint32_t now = millis(); + bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; for (auto *child : this->children_) { uint16_t value; if (this->iir_filter_enabled_()) { @@ -116,35 +118,20 @@ 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_) { + if (should_print) { ESP_LOGD(TAG, "Touch Pad '%s' (T%u): %u", child->get_name().c_str(), child->get_touch_pad(), value); } } - if (this->setup_mode_) { + if (should_print) { // Avoid spamming logs - delay(250); + this->setup_mode_last_log_print_ = now; } } -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 +142,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..45d459a2ff 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}; @@ -46,6 +50,7 @@ class ESP32TouchComponent : public Component { touch_volt_atten_t voltage_attenuation_{}; std::vector children_; bool setup_mode_{false}; + uint32_t setup_mode_last_log_print_{}; uint32_t iir_filter_{0}; }; @@ -54,14 +59,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/fastled_base/__init__.py b/esphome/components/fastled_base/__init__.py index 7354f9ae9f..b552c917c0 100644 --- a/esphome/components/fastled_base/__init__.py +++ b/esphome/components/fastled_base/__init__.py @@ -34,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/fujitsu_general/__init__.py b/esphome/components/fujitsu_general/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/fujitsu_general/climate.py b/esphome/components/fujitsu_general/climate.py new file mode 100644 index 0000000000..a6774c397a --- /dev/null +++ b/esphome/components/fujitsu_general/climate.py @@ -0,0 +1,18 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import climate_ir +from esphome.const import CONF_ID + +AUTO_LOAD = ['climate_ir'] + +fujitsu_general_ns = cg.esphome_ns.namespace('fujitsu_general') +FujitsuGeneralClimate = fujitsu_general_ns.class_('FujitsuGeneralClimate', climate_ir.ClimateIR) + +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield climate_ir.register_climate_ir(var, config) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp new file mode 100644 index 0000000000..261d8be258 --- /dev/null +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -0,0 +1,212 @@ +#include "fujitsu_general.h" + +namespace esphome { +namespace fujitsu_general { + +static const char *TAG = "fujitsu_general.climate"; + +// Control packet +const uint16_t FUJITSU_GENERAL_STATE_LENGTH = 16; + +const uint8_t FUJITSU_GENERAL_BASE_BYTE0 = 0x14; +const uint8_t FUJITSU_GENERAL_BASE_BYTE1 = 0x63; +const uint8_t FUJITSU_GENERAL_BASE_BYTE2 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE3 = 0x10; +const uint8_t FUJITSU_GENERAL_BASE_BYTE4 = 0x10; +const uint8_t FUJITSU_GENERAL_BASE_BYTE5 = 0xFE; +const uint8_t FUJITSU_GENERAL_BASE_BYTE6 = 0x09; +const uint8_t FUJITSU_GENERAL_BASE_BYTE7 = 0x30; + +// Temperature and POWER ON +const uint8_t FUJITSU_GENERAL_POWER_ON_MASK_BYTE8 = 0b00000001; +const uint8_t FUJITSU_GENERAL_BASE_BYTE8 = 0x40; + +// Mode +const uint8_t FUJITSU_GENERAL_MODE_AUTO_BYTE9 = 0x00; +const uint8_t FUJITSU_GENERAL_MODE_HEAT_BYTE9 = 0x04; +const uint8_t FUJITSU_GENERAL_MODE_COOL_BYTE9 = 0x01; +const uint8_t FUJITSU_GENERAL_MODE_DRY_BYTE9 = 0x02; +const uint8_t FUJITSU_GENERAL_MODE_FAN_BYTE9 = 0x03; +const uint8_t FUJITSU_GENERAL_MODE_10C_BYTE9 = 0x0B; +const uint8_t FUJITSU_GENERAL_BASE_BYTE9 = 0x01; + +// Fan speed and swing +const uint8_t FUJITSU_GENERAL_FAN_AUTO_BYTE10 = 0x00; +const uint8_t FUJITSU_GENERAL_FAN_HIGH_BYTE10 = 0x01; +const uint8_t FUJITSU_GENERAL_FAN_MEDIUM_BYTE10 = 0x02; +const uint8_t FUJITSU_GENERAL_FAN_LOW_BYTE10 = 0x03; +const uint8_t FUJITSU_GENERAL_FAN_SILENT_BYTE10 = 0x04; +const uint8_t FUJITSU_GENERAL_SWING_MASK_BYTE10 = 0b00010000; +const uint8_t FUJITSU_GENERAL_BASE_BYTE10 = 0x00; + +const uint8_t FUJITSU_GENERAL_BASE_BYTE11 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE12 = 0x00; +const uint8_t FUJITSU_GENERAL_BASE_BYTE13 = 0x00; + +// Outdoor Unit Low Noise +const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; +const uint8_t FUJITSU_GENERAL_BASE_BYTE14 = 0x20; + +// CRC +const uint8_t FUJITSU_GENERAL_BASE_BYTE15 = 0x6F; + +// Power off packet is specific +const uint16_t FUJITSU_GENERAL_OFF_LENGTH = 7; + +const uint8_t FUJITSU_GENERAL_OFF_BYTE0 = FUJITSU_GENERAL_BASE_BYTE0; +const uint8_t FUJITSU_GENERAL_OFF_BYTE1 = FUJITSU_GENERAL_BASE_BYTE1; +const uint8_t FUJITSU_GENERAL_OFF_BYTE2 = FUJITSU_GENERAL_BASE_BYTE2; +const uint8_t FUJITSU_GENERAL_OFF_BYTE3 = FUJITSU_GENERAL_BASE_BYTE3; +const uint8_t FUJITSU_GENERAL_OFF_BYTE4 = FUJITSU_GENERAL_BASE_BYTE4; +const uint8_t FUJITSU_GENERAL_OFF_BYTE5 = 0x02; +const uint8_t FUJITSU_GENERAL_OFF_BYTE6 = 0xFD; + +const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius +const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius + +const uint16_t FUJITSU_GENERAL_HEADER_MARK = 3300; +const uint16_t FUJITSU_GENERAL_HEADER_SPACE = 1600; +const uint16_t FUJITSU_GENERAL_BIT_MARK = 420; +const uint16_t FUJITSU_GENERAL_ONE_SPACE = 1200; +const uint16_t FUJITSU_GENERAL_ZERO_SPACE = 420; +const uint16_t FUJITSU_GENERAL_TRL_MARK = 420; +const uint16_t FUJITSU_GENERAL_TRL_SPACE = 8000; + +const uint32_t FUJITSU_GENERAL_CARRIER_FREQUENCY = 38000; + +FujitsuGeneralClimate::FujitsuGeneralClimate() : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1) {} + +void FujitsuGeneralClimate::transmit_state() { + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->transmit_off_(); + return; + } + uint8_t remote_state[FUJITSU_GENERAL_STATE_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_BASE_BYTE0; + remote_state[1] = FUJITSU_GENERAL_BASE_BYTE1; + remote_state[2] = FUJITSU_GENERAL_BASE_BYTE2; + remote_state[3] = FUJITSU_GENERAL_BASE_BYTE3; + remote_state[4] = FUJITSU_GENERAL_BASE_BYTE4; + remote_state[5] = FUJITSU_GENERAL_BASE_BYTE5; + remote_state[6] = FUJITSU_GENERAL_BASE_BYTE6; + remote_state[7] = FUJITSU_GENERAL_BASE_BYTE7; + remote_state[8] = FUJITSU_GENERAL_BASE_BYTE8; + remote_state[9] = FUJITSU_GENERAL_BASE_BYTE9; + remote_state[10] = FUJITSU_GENERAL_BASE_BYTE10; + remote_state[11] = FUJITSU_GENERAL_BASE_BYTE11; + remote_state[12] = FUJITSU_GENERAL_BASE_BYTE12; + remote_state[13] = FUJITSU_GENERAL_BASE_BYTE13; + remote_state[14] = FUJITSU_GENERAL_BASE_BYTE14; + remote_state[15] = FUJITSU_GENERAL_BASE_BYTE15; + + // Set temperature + uint8_t safecelsius = std::max((uint8_t) this->target_temperature, FUJITSU_GENERAL_TEMP_MIN); + safecelsius = std::min(safecelsius, FUJITSU_GENERAL_TEMP_MAX); + remote_state[8] = (byte) safecelsius - 16; + remote_state[8] = remote_state[8] << 4; + + // If not powered - set power on flag + if (!this->power_) { + remote_state[8] = (byte) remote_state[8] | FUJITSU_GENERAL_POWER_ON_MASK_BYTE8; + } + + // Set mode + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + remote_state[9] = FUJITSU_GENERAL_MODE_COOL_BYTE9; + break; + case climate::CLIMATE_MODE_HEAT: + remote_state[9] = FUJITSU_GENERAL_MODE_HEAT_BYTE9; + break; + case climate::CLIMATE_MODE_AUTO: + default: + remote_state[9] = FUJITSU_GENERAL_MODE_AUTO_BYTE9; + break; + // TODO: CLIMATE_MODE_FAN_ONLY, CLIMATE_MODE_DRY, CLIMATE_MODE_10C are missing in esphome + } + + // TODO: missing support for fan speed + remote_state[10] = FUJITSU_GENERAL_FAN_AUTO_BYTE10; + + // TODO: missing support for swing + // remote_state[10] = (byte) remote_state[10] | FUJITSU_GENERAL_SWING_MASK_BYTE10; + + // TODO: missing support for outdoor unit low noise + // remote_state[14] = (byte) remote_state[14] | FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14; + + // CRC + remote_state[15] = 0; + for (int i = 7; i < 15; i++) { + remote_state[15] += (byte) remote_state[i]; // Addiction + } + remote_state[15] = 0x100 - remote_state[15]; // mod 256 + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); + + // Header + data->mark(FUJITSU_GENERAL_HEADER_MARK); + data->space(FUJITSU_GENERAL_HEADER_SPACE); + // Data + for (uint8_t i : remote_state) { + // Send all Bits from Byte Data in Reverse Order + for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(FUJITSU_GENERAL_BIT_MARK); + bool bit = i & mask; + data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); + // Next bits + } + } + // Footer + data->mark(FUJITSU_GENERAL_TRL_MARK); + data->space(FUJITSU_GENERAL_TRL_SPACE); + + transmit.perform(); + + this->power_ = true; +} + +void FujitsuGeneralClimate::transmit_off_() { + uint8_t remote_state[FUJITSU_GENERAL_OFF_LENGTH] = {0}; + + remote_state[0] = FUJITSU_GENERAL_OFF_BYTE0; + remote_state[1] = FUJITSU_GENERAL_OFF_BYTE1; + remote_state[2] = FUJITSU_GENERAL_OFF_BYTE2; + remote_state[3] = FUJITSU_GENERAL_OFF_BYTE3; + remote_state[4] = FUJITSU_GENERAL_OFF_BYTE4; + remote_state[5] = FUJITSU_GENERAL_OFF_BYTE5; + remote_state[6] = FUJITSU_GENERAL_OFF_BYTE6; + + auto transmit = this->transmitter_->transmit(); + auto data = transmit.get_data(); + + data->set_carrier_frequency(FUJITSU_GENERAL_CARRIER_FREQUENCY); + + // Header + data->mark(FUJITSU_GENERAL_HEADER_MARK); + data->space(FUJITSU_GENERAL_HEADER_SPACE); + + // Data + for (uint8_t i : remote_state) { + // Send all Bits from Byte Data in Reverse Order + for (uint8_t mask = 00000001; mask > 0; mask <<= 1) { // iterate through bit mask + data->mark(FUJITSU_GENERAL_BIT_MARK); + bool bit = i & mask; + data->space(bit ? FUJITSU_GENERAL_ONE_SPACE : FUJITSU_GENERAL_ZERO_SPACE); + // Next bits + } + } + // Footer + data->mark(FUJITSU_GENERAL_TRL_MARK); + data->space(FUJITSU_GENERAL_TRL_SPACE); + + transmit.perform(); + + this->power_ = false; +} + +} // namespace fujitsu_general +} // namespace esphome diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h new file mode 100644 index 0000000000..80db81a167 --- /dev/null +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/climate_ir/climate_ir.h" + +namespace esphome { +namespace fujitsu_general { + +class FujitsuGeneralClimate : public climate_ir::ClimateIR { + public: + FujitsuGeneralClimate(); + + protected: + /// Transmit via IR the state of this climate controller. + void transmit_state() override; + /// Transmit via IR power off command. + void transmit_off_(); + + bool power_{false}; +}; + +} // namespace fujitsu_general +} // namespace esphome 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/gpio/switch/__init__.py b/esphome/components/gpio/switch/__init__.py index 7b383cb8a9..f75bc71009 100644 --- a/esphome/components/gpio/switch/__init__.py +++ b/esphome/components/gpio/switch/__init__.py @@ -15,12 +15,14 @@ RESTORE_MODES = { 'ALWAYS_ON': GPIOSwitchRestoreMode.GPIO_SWITCH_ALWAYS_ON, } +CONF_INTERLOCK_WAIT_TIME = 'interlock_wait_time' CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(GPIOSwitch), cv.Required(CONF_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_RESTORE_MODE, default='RESTORE_DEFAULT_OFF'): cv.enum(RESTORE_MODES, upper=True, space='_'), cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), + cv.Optional(CONF_INTERLOCK_WAIT_TIME, default='0ms'): cv.positive_time_period_milliseconds, }).extend(cv.COMPONENT_SCHEMA) @@ -40,3 +42,4 @@ def to_code(config): lock = yield cg.get_variable(it) interlock.append(lock) cg.add(var.set_interlock(interlock)) + cg.add(var.set_interlock_wait_time(config[CONF_INTERLOCK_WAIT_TIME])) diff --git a/esphome/components/gpio/switch/gpio_switch.cpp b/esphome/components/gpio/switch/gpio_switch.cpp index d22a74847e..d87e5a61e6 100644 --- a/esphome/components/gpio/switch/gpio_switch.cpp +++ b/esphome/components/gpio/switch/gpio_switch.cpp @@ -69,13 +69,29 @@ void GPIOSwitch::dump_config() { void GPIOSwitch::write_state(bool state) { if (state != this->inverted_) { // Turning ON, check interlocking + + bool found = false; for (auto *lock : this->interlock_) { if (lock == this) continue; - if (lock->state) + if (lock->state) { lock->turn_off(); + found = true; + } } + if (found && this->interlock_wait_time_ != 0) { + this->set_timeout("interlock", this->interlock_wait_time_, [this, state] { + // Don't write directly, call the function again + // (some other switch may have changed state while we were waiting) + this->write_state(state); + }); + return; + } + } else if (this->interlock_wait_time_ != 0) { + // If we are switched off during the interlock wait time, cancel any pending + // re-activations + this->cancel_timeout("interlock"); } this->pin_->digital_write(state); diff --git a/esphome/components/gpio/switch/gpio_switch.h b/esphome/components/gpio/switch/gpio_switch.h index ceace477b2..dc0dd9bc95 100644 --- a/esphome/components/gpio/switch/gpio_switch.h +++ b/esphome/components/gpio/switch/gpio_switch.h @@ -26,6 +26,7 @@ class GPIOSwitch : public switch_::Switch, public Component { void setup() override; void dump_config() override; void set_interlock(const std::vector &interlock); + void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } protected: void write_state(bool state) override; @@ -33,6 +34,7 @@ class GPIOSwitch : public switch_::Switch, public Component { GPIOPin *pin_; GPIOSwitchRestoreMode restore_mode_{GPIO_SWITCH_RESTORE_DEFAULT_OFF}; std::vector interlock_; + uint32_t interlock_wait_time_{0}; }; } // namespace gpio 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/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 3fa9d2c37a..562bd26771 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(); @@ -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); @@ -200,5 +208,30 @@ void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; } uint8_t next_i2c_bus_num_ = 0; #endif +I2CRegister &I2CRegister::operator=(uint8_t value) { + this->parent_->write_byte(this->register_, value); + return *this; +} + +I2CRegister &I2CRegister::operator&=(uint8_t value) { + this->parent_->write_byte(this->register_, this->get() & value); + return *this; +} + +I2CRegister &I2CRegister::operator|=(uint8_t value) { + this->parent_->write_byte(this->register_, this->get() | value); + return *this; +} + +uint8_t I2CRegister::get() { + uint8_t value = 0x00; + this->parent_->read_byte(this->register_, &value); + return value; +} +I2CRegister &I2CRegister::operator=(const std::vector &value) { + this->parent_->write_bytes(this->register_, value); + return *this; +} + } // namespace i2c } // namespace esphome diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index e41bd6c5e8..c4ed40e268 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. * @@ -132,6 +134,24 @@ class I2CComponent : public Component { extern uint8_t next_i2c_bus_num_; #endif +class I2CDevice; + +class I2CRegister { + public: + I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} + + I2CRegister &operator=(uint8_t value); + I2CRegister &operator=(const std::vector &value); + I2CRegister &operator&=(uint8_t value); + I2CRegister &operator|=(uint8_t value); + + uint8_t get(); + + protected: + I2CDevice *parent_; + uint8_t register_; +}; + /** All components doing communication on the I2C bus should subclass I2CDevice. * * This class stores 1. the address of the i2c device and has a helper function to allow @@ -151,7 +171,8 @@ class I2CDevice { /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); - protected: + I2CRegister reg(uint8_t a_register) { return {this, a_register}; } + /** 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 +182,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 +208,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 +221,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 +230,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 +241,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 +260,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/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..25ac143817 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); } @@ -148,6 +148,11 @@ void LCDDisplay::printf(const char *format, ...) { if (ret > 0) this->print(0, 0, buffer); } +void LCDDisplay::clear() { + // clear display, also sets DDRAM address to 0 (home) + this->command_(LCD_DISPLAY_COMMAND_CLEAR_DISPLAY); + delay(2); +} #ifdef USE_TIME void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { char buffer[64]; diff --git a/esphome/components/lcd_base/lcd_display.h b/esphome/components/lcd_base/lcd_display.h index 200600eb9c..ee150059c6 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; @@ -26,6 +23,8 @@ class LCDDisplay : public PollingComponent { float get_setup_priority() const override; void update() override; void display(); + //// Clear LCD display + void clear(); /// Print the given text at the specified column and row. void print(uint8_t column, uint8_t row, const char *str); @@ -54,11 +53,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..2b1c181a62 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(i); + const float max_frequency = ledc_max_frequency_for_bit_depth(i); + 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.value_or(8); + 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 c93392418c..b5dc70a083 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -113,7 +113,7 @@ void ESPRangeView::fade_to_white(uint8_t amnt) { } void ESPRangeView::fade_to_black(uint8_t amnt) { for (auto c : *this) - c.fade_to_white(amnt); + c.fade_to_black(amnt); } void ESPRangeView::lighten(uint8_t delta) { for (auto c : *this) @@ -162,7 +162,6 @@ int32_t HOT interpret_index(int32_t index, int32_t size) { } void AddressableLight::call_setup() { - this->setup_internal_(); this->setup(); #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE @@ -180,5 +179,101 @@ void AddressableLight::call_setup() { #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 4383b4b245..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), @@ -468,23 +452,7 @@ class AddressableLight : public LightOutput, public Component { } 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)))); @@ -524,6 +492,8 @@ class AddressableLight : public LightOutput, public Component { 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 1e0b540285..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}; }; @@ -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}; @@ -312,11 +318,16 @@ class AddressableFlickerEffect : public AddressableLightEffect { 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() % intensity; - var = (var.get() * inv_intensity) + (current_color * flicker); + // 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/base_light_effects.h b/esphome/components/light/base_light_effects.h index 55aa007f56..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); 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 185105831d..fb88d021f2 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -9,6 +9,8 @@ AddressableLightState = light_ns.class_('LightState', LightState) LightOutput = light_ns.class_('LightOutput') 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..b8a252c7bd 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -30,17 +30,11 @@ 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); + uint32_t get_baud_rate() const { return baud_rate_; } /// 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 +51,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..8e28d04dea 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,46 @@ 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_); + this->check_uart_settings(9600); + + 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/modbus/__init__.py b/esphome/components/modbus/__init__.py new file mode 100644 index 0000000000..cada835905 --- /dev/null +++ b/esphome/components/modbus/__init__.py @@ -0,0 +1,44 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID, CONF_ADDRESS +from esphome.core import coroutine + +DEPENDENCIES = ['uart'] + +modbus_ns = cg.esphome_ns.namespace('modbus') +Modbus = modbus_ns.class_('Modbus', cg.Component, uart.UARTDevice) +ModbusDevice = modbus_ns.class_('ModbusDevice') +MULTI_CONF = True + +CONF_MODBUS_ID = 'modbus_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Modbus), +}).extend(cv.COMPONENT_SCHEMA).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + cg.add_global(modbus_ns.using) + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + + yield uart.register_uart_device(var, config) + + +def modbus_device_schema(default_address): + schema = { + cv.GenerateID(CONF_MODBUS_ID): cv.use_id(Modbus), + } + if default_address is None: + schema[cv.Required(CONF_ADDRESS)] = cv.hex_uint8_t + else: + schema[cv.Optional(CONF_ADDRESS, default=default_address)] = cv.hex_uint8_t + return cv.Schema(schema) + + +@coroutine +def register_modbus_device(var, config): + parent = yield cg.get_variable(config[CONF_MODBUS_ID]) + cg.add(var.set_parent(parent)) + cg.add(var.set_address(config[CONF_ADDRESS])) + cg.add(parent.register_device(var)) diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp new file mode 100644 index 0000000000..74d0c40986 --- /dev/null +++ b/esphome/components/modbus/modbus.cpp @@ -0,0 +1,119 @@ +#include "modbus.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace modbus { + +static const char *TAG = "modbus"; + +void Modbus::loop() { + const uint32_t now = millis(); + if (now - this->last_modbus_byte_ > 50) { + this->rx_buffer_.clear(); + this->last_modbus_byte_ = now; + } + + while (this->available()) { + uint8_t byte; + this->read_byte(&byte); + if (this->parse_modbus_byte_(byte)) { + this->last_modbus_byte_ = now; + } else { + this->rx_buffer_.clear(); + } + } +} + +uint16_t crc16(const uint8_t *data, uint8_t len) { + uint16_t crc = 0xFFFF; + while (len--) { + crc ^= *data++; + for (uint8_t i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool Modbus::parse_modbus_byte_(uint8_t byte) { + size_t at = this->rx_buffer_.size(); + this->rx_buffer_.push_back(byte); + const uint8_t *raw = &this->rx_buffer_[0]; + + // Byte 0: modbus address (match all) + if (at == 0) + return true; + uint8_t address = raw[0]; + + // Byte 1: Function (msb indicates error) + if (at == 1) + return (byte & 0x80) != 0x80; + + // Byte 2: Size (with modbus rtu function code 4/3) + // See also https://en.wikipedia.org/wiki/Modbus + if (at == 2) + return true; + + uint8_t data_len = raw[2]; + // Byte 3..3+data_len-1: Data + if (at < 3 + data_len) + return true; + + // Byte 3+data_len: CRC_LO (over all bytes) + if (at == 3 + data_len) + return true; + // Byte 3+len+1: CRC_HI (over all bytes) + uint16_t computed_crc = crc16(raw, 3 + data_len); + uint16_t remote_crc = uint16_t(raw[3 + data_len]) | (uint16_t(raw[3 + data_len + 1]) << 8); + if (computed_crc != remote_crc) { + ESP_LOGW(TAG, "Modbus CRC Check failed! %02X!=%02X", computed_crc, remote_crc); + return false; + } + + std::vector data(this->rx_buffer_.begin() + 3, this->rx_buffer_.begin() + 3 + data_len); + + bool found = false; + for (auto *device : this->devices_) { + if (device->address_ == address) { + device->on_modbus_data(data); + found = true; + } + } + if (!found) { + ESP_LOGW(TAG, "Got Modbus frame from unknown address 0x%02X!", address); + } + + // return false to reset buffer + return false; +} + +void Modbus::dump_config() { + ESP_LOGCONFIG(TAG, "Modbus:"); + this->check_uart_settings(9600, 2); +} +float Modbus::get_setup_priority() const { + // After UART bus + return setup_priority::BUS - 1.0f; +} +void Modbus::send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count) { + uint8_t frame[8]; + frame[0] = address; + frame[1] = function; + frame[2] = start_address >> 8; + frame[3] = start_address >> 0; + frame[4] = register_count >> 8; + frame[5] = register_count >> 0; + auto crc = crc16(frame, 6); + frame[6] = crc >> 0; + frame[7] = crc >> 8; + + this->write_array(frame, 8); +} + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/modbus/modbus.h b/esphome/components/modbus/modbus.h new file mode 100644 index 0000000000..b75de147b1 --- /dev/null +++ b/esphome/components/modbus/modbus.h @@ -0,0 +1,51 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace modbus { + +class ModbusDevice; + +class Modbus : public uart::UARTDevice, public Component { + public: + Modbus() = default; + + void loop() override; + + void dump_config() override; + + void register_device(ModbusDevice *device) { this->devices_.push_back(device); } + + float get_setup_priority() const override; + + void send(uint8_t address, uint8_t function, uint16_t start_address, uint16_t register_count); + + protected: + bool parse_modbus_byte_(uint8_t byte); + + std::vector rx_buffer_; + uint32_t last_modbus_byte_{0}; + std::vector devices_; +}; + +class ModbusDevice { + public: + void set_parent(Modbus *parent) { parent_ = parent; } + void set_address(uint8_t address) { address_ = address; } + virtual void on_modbus_data(const std::vector &data) = 0; + + void send(uint8_t function, uint16_t start_address, uint16_t register_count) { + this->parent_->send(this->address_, function, start_address, register_count); + } + + protected: + friend Modbus; + + Modbus *parent_; + uint8_t address_; +}; + +} // namespace modbus +} // namespace esphome diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index a24a703306..2025bc5b3f 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -10,7 +10,7 @@ void MPR121Component::setup() { ESP_LOGCONFIG(TAG, "Setting up MPR121..."); // soft reset device this->write_byte(MPR121_SOFTRESET, 0x63); - delay(100); + delay(100); // NOLINT if (!this->write_byte(MPR121_ECR, 0x0)) { this->error_code_ = COMMUNICATION_FAILED; this->mark_failed(); diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index d30e25c187..073bb3cede 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): @@ -121,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, @@ -154,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/OttoWinter/async-mqtt-client/blob/master/library.json + cg.add_library('AsyncMqttClient-esphome', '0.8.3') cg.add_define('USE_MQTT') cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index e07204d559..2eb1c52153 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -201,7 +201,7 @@ void MQTTClientComponent::check_connected() { this->status_clear_warning(); ESP_LOGI(TAG, "MQTT Connected!"); // MQTT Client needs some time to be fully set up. - delay(100); + delay(100); // NOLINT this->resubscribe_subscriptions_(); 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 53c4e89e98..4201d41c44 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -149,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; @@ -173,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_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index a241cf6ed6..f87e7651b9 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -55,6 +55,9 @@ void MQTTSensorComponent::send_discovery(JsonObject &root, mqtt::SendDiscoveryCo if (!this->sensor_->get_icon().empty()) root["icon"] = this->sensor_->get_icon(); + if (this->sensor_->get_force_update()) + root["force_update"] = true; + config.command_topic = false; } bool MQTTSensorComponent::send_initial_state() { 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/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 33ed6b1899..39bce9f32c 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -19,7 +19,7 @@ void MS5611Component::setup() { this->mark_failed(); return; } - delay(100); + delay(100); // NOLINT for (uint8_t offset = 0; offset < 6; offset++) { if (!this->read_byte_16(MS5611_CMD_READ_PROM + (offset * 2), &this->prom_[offset])) { this->mark_failed(); diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index 694ac028fc..e5106d4bd6 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -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/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/nextion/nextion.h b/esphome/components/nextion/nextion.h index 92b41a88af..bd37e241e9 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -394,7 +394,7 @@ class Nextion : public PollingComponent, public uart::UARTDevice { bool wait_for_ack_{true}; }; -class NextionTouchComponent : public binary_sensor::BinarySensor { +class NextionTouchComponent : public binary_sensor::BinarySensorInitiallyOff { public: void set_page_id(uint8_t page_id) { page_id_ = page_id; } void set_component_id(uint8_t component_id) { component_id_ = component_id; } diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index 1b5c5182c7..9446508b0b 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -19,9 +19,9 @@ void NTC::process_(float value) { return; } - float lr = logf(value); - float v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; - float temp = 1 / v - 273.15f; + double lr = log(double(value)); + double v = this->a_ + this->b_ * lr + this->c_ * lr * lr * lr; + auto temp = float(1.0 / v - 273.15); ESP_LOGD(TAG, "'%s' - Temperature: %.1f°C", this->name_.c_str(), temp); this->publish_state(temp); diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index 9d6b37412d..c8592e0fe8 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -9,9 +9,9 @@ namespace ntc { class NTC : public Component, public sensor::Sensor { public: void set_sensor(Sensor *sensor) { sensor_ = sensor; } - void set_a(float a) { a_ = a; } - void set_b(float b) { b_ = b; } - void set_c(float c) { c_ = c; } + void set_a(double a) { a_ = a; } + void set_b(double b) { b_ = b; } + void set_c(double c) { c_ = c; } void setup() override; void dump_config() override; float get_setup_priority() const override; @@ -20,9 +20,9 @@ class NTC : public Component, public sensor::Sensor { void process_(float value); sensor::Sensor *sensor_; - float a_; - float b_; - float c_; + double a_; + double b_; + double c_; }; } // namespace ntc 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..2041c688eb 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; } @@ -266,7 +266,7 @@ void OTAComponent::handle_() { delay(10); ESP_LOGI(TAG, "OTA update finished!"); this->status_clear_warning(); - delay(100); + delay(100); // NOLINT App.safe_reboot(); 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/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/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index 548099a495..489442c637 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -169,6 +169,7 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); LOG_SENSOR(" ", "Formaldehyde", this->formaldehyde_sensor_); + this->check_uart_settings(9600); } } // namespace pmsx003 diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index fa9a92d430..0cbaf1bf29 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -12,24 +12,24 @@ pmsx003_ns = cg.esphome_ns.namespace('pmsx003') PMSX003Component = pmsx003_ns.class_('PMSX003Component', uart.UARTDevice, cg.Component) PMSX003Sensor = pmsx003_ns.class_('PMSX003Sensor', sensor.Sensor) -CONF_PMSX003 = 'PMSX003' -CONF_PMS5003T = 'PMS5003T' -CONF_PMS5003ST = 'PMS5003ST' +TYPE_PMSX003 = 'PMSX003' +TYPE_PMS5003T = 'PMS5003T' +TYPE_PMS5003ST = 'PMS5003ST' PMSX003Type = pmsx003_ns.enum('PMSX003Type') PMSX003_TYPES = { - CONF_PMSX003: PMSX003Type.PMSX003_TYPE_X003, - CONF_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, - CONF_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, + TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, + TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, + TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, } SENSORS_TO_TYPE = { - CONF_PM_1_0: [CONF_PMSX003, CONF_PMS5003ST], - CONF_PM_2_5: [CONF_PMSX003, CONF_PMS5003T, CONF_PMS5003ST], - CONF_PM_10_0: [CONF_PMSX003, CONF_PMS5003ST], - CONF_TEMPERATURE: [CONF_PMS5003T, CONF_PMS5003ST], - CONF_HUMIDITY: [CONF_PMS5003T, CONF_PMS5003ST], - CONF_FORMALDEHYDE: [CONF_PMS5003ST], + CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003ST], + CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], + CONF_FORMALDEHYDE: [TYPE_PMS5003ST], } 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/pulse_counter_sensor.cpp b/esphome/components/pulse_counter/pulse_counter_sensor.cpp index 6503711e35..c71e51eb32 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.cpp +++ b/esphome/components/pulse_counter/pulse_counter_sensor.cpp @@ -149,6 +149,7 @@ void PulseCounterSensor::dump_config() { ESP_LOGCONFIG(TAG, " Rising Edge: %s", EDGE_MODE_TO_STRING[this->storage_.rising_edge_mode]); ESP_LOGCONFIG(TAG, " Falling Edge: %s", EDGE_MODE_TO_STRING[this->storage_.falling_edge_mode]); ESP_LOGCONFIG(TAG, " Filtering pulses shorter than %u µs", this->storage_.filter_us); + LOG_UPDATE_INTERVAL(this); } void PulseCounterSensor::update() { 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/pzem004t/__init__.py b/esphome/components/pzem004t/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzem004t/pzem004t.cpp b/esphome/components/pzem004t/pzem004t.cpp new file mode 100644 index 0000000000..cbdc14f0d0 --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.cpp @@ -0,0 +1,109 @@ +#include "pzem004t.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzem004t { + +static const char *TAG = "pzem004t"; + +void PZEM004T::loop() { + const uint32_t now = millis(); + if (now - this->last_read_ > 500 && this->available()) { + while (this->available()) + this->read(); + this->last_read_ = now; + } + + // PZEM004T packet size is 7 byte + while (this->available() >= 7) { + auto resp = *this->read_array<7>(); + // packet format: + // 0: packet type + // 1-5: data + // 6: checksum (sum of other bytes) + // see https://github.com/olehs/PZEM004T + uint8_t sum = 0; + for (int i = 0; i < 6; i++) + sum += resp[i]; + + if (sum != resp[6]) { + ESP_LOGV(TAG, "PZEM004T invalid checksum! 0x%02X != 0x%02X", sum, resp[6]); + continue; + } + + switch (resp[0]) { + case 0xA4: { // Set Module Address Response + this->write_state_(READ_VOLTAGE); + break; + } + case 0xA0: { // Voltage Response + uint16_t int_voltage = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float voltage = int_voltage + (resp[3] / 10.0f); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + ESP_LOGD(TAG, "Got Voltage %.1f V", voltage); + this->write_state_(READ_CURRENT); + break; + } + case 0xA1: { // Current Response + uint16_t int_current = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + float current = int_current + (resp[3] / 100.0f); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + ESP_LOGD(TAG, "Got Current %.2f A", current); + this->write_state_(READ_POWER); + break; + } + case 0xA2: { // Active Power Response + uint16_t power = (uint16_t(resp[1]) << 8) | (uint16_t(resp[2]) << 0); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); + ESP_LOGD(TAG, "Got Power %u W", power); + this->write_state_(DONE); + break; + } + + case 0xA3: // Energy Response + case 0xA5: // Set Power Alarm Response + case 0xB0: // Voltage Request + case 0xB1: // Current Request + case 0xB2: // Active Power Response + case 0xB3: // Energy Request + case 0xB4: // Set Module Address Request + case 0xB5: // Set Power Alarm Request + default: + break; + } + + this->last_read_ = now; + } +} +void PZEM004T::update() { this->write_state_(SET_ADDRESS); } +void PZEM004T::write_state_(PZEM004T::PZEM004TReadState state) { + if (state == DONE) { + this->read_state_ = state; + return; + } + std::array data{}; + data[0] = state; + data[1] = 192; + data[2] = 168; + data[3] = 1; + data[4] = 1; + data[5] = 0; + data[6] = 0; + for (int i = 0; i < 6; i++) + data[6] += data[i]; + + this->write_array(data); + this->read_state_ = state; +} +void PZEM004T::dump_config() { + ESP_LOGCONFIG(TAG, "PZEM004T:"); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/pzem004t.h b/esphome/components/pzem004t/pzem004t.h new file mode 100644 index 0000000000..f0208d415a --- /dev/null +++ b/esphome/components/pzem004t/pzem004t.h @@ -0,0 +1,41 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace pzem004t { + +class PZEM004T : public PollingComponent, public uart::UARTDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + + void loop() override; + + void update() override; + + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + + enum PZEM004TReadState { + SET_ADDRESS = 0xB4, + READ_VOLTAGE = 0xB0, + READ_CURRENT = 0xB1, + READ_POWER = 0xB2, + DONE = 0x00, + } read_state_{DONE}; + + void write_state_(PZEM004TReadState state); + + uint32_t last_read_{0}; +}; + +} // namespace pzem004t +} // namespace esphome diff --git a/esphome/components/pzem004t/sensor.py b/esphome/components/pzem004t/sensor.py new file mode 100644 index 0000000000..6e3628c5ec --- /dev/null +++ b/esphome/components/pzem004t/sensor.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['uart'] + +pzem004t_ns = cg.esphome_ns.namespace('pzem004t') +PZEM004T = pzem004t_ns.class_('PZEM004T', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEM004T), + + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 0), +}).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_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/pzemac/__init__.py b/esphome/components/pzemac/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemac/pzemac.cpp b/esphome/components/pzemac/pzemac.cpp new file mode 100644 index 0000000000..f05ce15711 --- /dev/null +++ b/esphome/components/pzemac/pzemac.cpp @@ -0,0 +1,71 @@ +#include "pzemac.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemac { + +static const char *TAG = "pzemac"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMAC::on_modbus_data(const std::vector &data) { + if (data.size() < 20) { + ESP_LOGW(TAG, "Invalid size for PZEM AC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 + // 01 04 14 08 D1 00 6C 00 00 00 F4 00 00 00 26 00 00 01 F4 00 64 00 00 51 34 + // Id Cc Sz Volt- Current---- Power------ Energy----- Frequ PFact Alarm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 10.0f; // max 6553.5 V + + uint32_t raw_current = pzem_get_32bit(2); + float current = raw_current / 1000.0f; // max 4294967.295 A + + uint32_t raw_active_power = pzem_get_32bit(6); + float active_power = raw_active_power / 10.0f; // max 429496729.5 W + + uint16_t raw_frequency = pzem_get_16bit(14); + float frequency = raw_frequency / 10.0f; + + uint16_t raw_power_factor = pzem_get_16bit(16); + float power_factor = raw_power_factor / 100.0f; + + ESP_LOGD(TAG, "PZEM AC: V=%.1f V, I=%.3f A, P=%.1f W, F=%.1f Hz, PF=%.2f", voltage, current, active_power, frequency, + power_factor); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(active_power); + if (this->frequency_sensor_ != nullptr) + this->frequency_sensor_->publish_state(frequency); + if (this->power_factor_sensor_ != nullptr) + this->power_factor_sensor_->publish_state(power_factor); +} + +void PZEMAC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, PZEM_REGISTER_COUNT); } +void PZEMAC::dump_config() { + ESP_LOGCONFIG(TAG, "PZEMAC:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); + LOG_SENSOR("", "Frequency", this->frequency_sensor_); + LOG_SENSOR("", "Power Factor", this->power_factor_sensor_); +} + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/pzemac.h b/esphome/components/pzemac/pzemac.h new file mode 100644 index 0000000000..d396b7cddf --- /dev/null +++ b/esphome/components/pzemac/pzemac.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemac { + +class PZEMAC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemac +} // namespace esphome diff --git a/esphome/components/pzemac/sensor.py b/esphome/components/pzemac/sensor.py new file mode 100644 index 0000000000..35d8069767 --- /dev/null +++ b/esphome/components/pzemac/sensor.py @@ -0,0 +1,47 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + CONF_FREQUENCY, UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, UNIT_EMPTY, \ + ICON_POWER, CONF_POWER_FACTOR, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemac_ns = cg.esphome_ns.namespace('pzemac') +PZEMAC = pzemac_ns.class_('PZEMAC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMAC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), + cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(UNIT_EMPTY, ICON_CURRENT_AC, 1), + cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 2), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) + if CONF_FREQUENCY in config: + conf = config[CONF_FREQUENCY] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_frequency_sensor(sens)) + if CONF_POWER_FACTOR in config: + conf = config[CONF_POWER_FACTOR] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_factor_sensor(sens)) diff --git a/esphome/components/pzemdc/__init__.py b/esphome/components/pzemdc/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/pzemdc/pzemdc.cpp b/esphome/components/pzemdc/pzemdc.cpp new file mode 100644 index 0000000000..9bd58410c0 --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.cpp @@ -0,0 +1,59 @@ +#include "pzemdc.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace pzemdc { + +static const char *TAG = "pzemdc"; + +static const uint8_t PZEM_CMD_READ_IN_REGISTERS = 0x04; +static const uint8_t PZEM_REGISTER_COUNT = 10; // 10x 16-bit registers + +void PZEMDC::on_modbus_data(const std::vector &data) { + if (data.size() < 16) { + ESP_LOGW(TAG, "Invalid size for PZEM DC!"); + return; + } + + // See https://github.com/esphome/feature-requests/issues/49#issuecomment-538636809 + // 0 1 2 3 4 5 6 7 = ModBus register + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 = Buffer index + // 01 04 10 05 40 00 0A 00 0D 00 00 00 02 00 00 00 00 00 00 D6 29 + // Id Cc Sz Volt- Curre Power------ Energy----- HiAlm LoAlm Crc-- + + auto pzem_get_16bit = [&](size_t i) -> uint16_t { + return (uint16_t(data[i + 0]) << 8) | (uint16_t(data[i + 1]) << 0); + }; + auto pzem_get_32bit = [&](size_t i) -> uint32_t { + return (uint32_t(pzem_get_16bit(i + 2)) << 16) | (uint32_t(pzem_get_16bit(i + 0)) << 0); + }; + + uint16_t raw_voltage = pzem_get_16bit(0); + float voltage = raw_voltage / 100.0f; // max 655.35 V + + uint16_t raw_current = pzem_get_16bit(2); + float current = raw_current / 100.0f; // max 655.35 A + + uint32_t raw_power = pzem_get_32bit(4); + float power = raw_power / 10.0f; // max 429496729.5 W + + ESP_LOGD(TAG, "PZEM DC: V=%.1f V, I=%.3f A, P=%.1f W", voltage, current, power); + if (this->voltage_sensor_ != nullptr) + this->voltage_sensor_->publish_state(voltage); + if (this->current_sensor_ != nullptr) + this->current_sensor_->publish_state(current); + if (this->power_sensor_ != nullptr) + this->power_sensor_->publish_state(power); +} + +void PZEMDC::update() { this->send(PZEM_CMD_READ_IN_REGISTERS, 0, 8); } +void PZEMDC::dump_config() { + ESP_LOGCONFIG(TAG, "PZEMDC:"); + ESP_LOGCONFIG(TAG, " Address: 0x%02X", this->address_); + LOG_SENSOR("", "Voltage", this->voltage_sensor_); + LOG_SENSOR("", "Current", this->current_sensor_); + LOG_SENSOR("", "Power", this->power_sensor_); +} + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/pzemdc.h b/esphome/components/pzemdc/pzemdc.h new file mode 100644 index 0000000000..d838eb4167 --- /dev/null +++ b/esphome/components/pzemdc/pzemdc.h @@ -0,0 +1,33 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/modbus/modbus.h" + +namespace esphome { +namespace pzemdc { + +class PZEMDC : public PollingComponent, public modbus::ModbusDevice { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } + void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } + void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } + void set_powerfactor_sensor(sensor::Sensor *powerfactor_sensor) { power_factor_sensor_ = powerfactor_sensor; } + + void update() override; + + void on_modbus_data(const std::vector &data) override; + + void dump_config() override; + + protected: + sensor::Sensor *voltage_sensor_; + sensor::Sensor *current_sensor_; + sensor::Sensor *power_sensor_; + sensor::Sensor *frequency_sensor_; + sensor::Sensor *power_factor_sensor_; +}; + +} // namespace pzemdc +} // namespace esphome diff --git a/esphome/components/pzemdc/sensor.py b/esphome/components/pzemdc/sensor.py new file mode 100644 index 0000000000..8c6fd08868 --- /dev/null +++ b/esphome/components/pzemdc/sensor.py @@ -0,0 +1,36 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, modbus +from esphome.const import CONF_CURRENT, CONF_ID, CONF_POWER, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT, ICON_POWER, ICON_CURRENT_AC + +AUTO_LOAD = ['modbus'] + +pzemdc_ns = cg.esphome_ns.namespace('pzemdc') +PZEMDC = pzemdc_ns.class_('PZEMDC', cg.PollingComponent, modbus.ModbusDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(PZEMDC), + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT): sensor.sensor_schema(UNIT_AMPERE, ICON_CURRENT_AC, 3), + cv.Optional(CONF_POWER): sensor.sensor_schema(UNIT_WATT, ICON_POWER, 1), +}).extend(cv.polling_component_schema('60s')).extend(modbus.modbus_device_schema(0x01)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield modbus.register_modbus_device(var, config) + + if CONF_VOLTAGE in config: + conf = config[CONF_VOLTAGE] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_voltage_sensor(sens)) + if CONF_CURRENT in config: + conf = config[CONF_CURRENT] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_current_sensor(sens)) + if CONF_POWER in config: + conf = config[CONF_POWER] + sens = yield sensor.new_sensor(conf) + cg.add(var.set_power_sensor(sens)) diff --git a/esphome/components/rdm6300/rdm6300.h b/esphome/components/rdm6300/rdm6300.h index a67b6e7ce8..13df400754 100644 --- a/esphome/components/rdm6300/rdm6300.h +++ b/esphome/components/rdm6300/rdm6300.h @@ -28,7 +28,7 @@ class RDM6300Component : public Component, public uart::UARTDevice { uint32_t last_id_{0}; }; -class RDM6300BinarySensor : public binary_sensor::BinarySensor { +class RDM6300BinarySensor : public binary_sensor::BinarySensorInitiallyOff { public: void set_id(uint32_t id) { id_ = id; } diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index d3da238b08..a62304c87d 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -88,7 +88,7 @@ def validate_repeat(value): 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_milliseconds), + cv.templatable(cv.positive_time_period_microseconds), })(value) return validate_repeat({CONF_TIMES: value}) @@ -250,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_)) @@ -420,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, @@ -438,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] @@ -457,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({ @@ -490,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), }), }) @@ -624,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 029f1fccf2..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[33]; + 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 6035e2fd57..36be25add7 100644 --- a/esphome/components/remote_base/remote_base.h +++ b/esphome/components/remote_base/remote_base.h @@ -267,11 +267,11 @@ class RemoteReceiverBase : public RemoteComponentBase { uint8_t tolerance_{25}; }; -class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensor, +class RemoteReceiverBinarySensorBase : public binary_sensor::BinarySensorInitiallyOff, public Component, public RemoteReceiverListener { public: - explicit RemoteReceiverBinarySensorBase() : BinarySensor() {} + explicit RemoteReceiverBinarySensorBase() : BinarySensorInitiallyOff() {} void dump_config() override; virtual bool matches(RemoteReceiveData src) = 0; bool on_receive(RemoteReceiveData src) override { diff --git a/esphome/components/restart/restart_switch.cpp b/esphome/components/restart/restart_switch.cpp index cd0979b9d3..f66ebc616e 100644 --- a/esphome/components/restart/restart_switch.cpp +++ b/esphome/components/restart/restart_switch.cpp @@ -13,8 +13,8 @@ void RestartSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Restarting device..."); - // then execute - delay(100); // Let MQTT settle a bit + // Let MQTT settle a bit + delay(100); // NOLINT App.safe_reboot(); } } 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 fb4881e18b..214ccbd056 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, CONF_PIN_A, CONF_PIN_B rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') @@ -13,11 +13,11 @@ RESOLUTIONS = { 4: RotaryEncoderResolution.ROTARY_ENCODER_4_PULSES_PER_CYCLE, } -CONF_PIN_A = 'pin_a' -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): @@ -60,3 +60,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..55ab07879e --- /dev/null +++ b/esphome/components/scd30/scd30.cpp @@ -0,0 +1,181 @@ +#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..7a60725276 --- /dev/null +++ b/esphome/components/scd30/sensor.py @@ -0,0 +1,37 @@ +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, CONF_CO2 + +DEPENDENCIES = ['i2c'] + +scd30_ns = cg.esphome_ns.namespace('scd30') +SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice) + +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 e1983689a6..9590679f83 100644 --- a/esphome/components/script/__init__.py +++ b/esphome/components/script/__init__.py @@ -8,6 +8,7 @@ script_ns = cg.esphome_ns.namespace('script') Script = script_ns.class_('Script', automation.Trigger.template()) ScriptExecuteAction = script_ns.class_('ScriptExecuteAction', automation.Action) ScriptStopAction = script_ns.class_('ScriptStopAction', automation.Action) +ScriptWaitAction = script_ns.class_('ScriptWaitAction', automation.Action) IsRunningCondition = script_ns.class_('IsRunningCondition', automation.Condition) CONFIG_SCHEMA = automation.validate_automation({ @@ -42,6 +43,14 @@ def script_stop_action_to_code(config, action_id, template_arg, args): yield cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_action('script.wait', ScriptWaitAction, maybe_simple_id({ + cv.Required(CONF_ID): cv.use_id(Script) +})) +def script_wait_action_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + yield cg.new_Pvariable(action_id, template_arg, paren) + + @automation.register_condition('script.is_running', IsRunningCondition, automation.maybe_simple_id({ cv.Required(CONF_ID): cv.use_id(Script) })) diff --git a/esphome/components/script/script.h b/esphome/components/script/script.h index f937b9d637..3b97327da8 100644 --- a/esphome/components/script/script.h +++ b/esphome/components/script/script.h @@ -49,5 +49,47 @@ template class IsRunningCondition : public Condition { Script *parent_; }; +template class ScriptWaitAction : public Action, public Component { + public: + ScriptWaitAction(Script *script) : script_(script) {} + + void play(Ts... x) { /* ignore - see play_complex */ + } + + void play_complex(Ts... x) override { + // Check if we can continue immediately. + if (!this->script_->is_running()) { + this->triggered_ = false; + this->play_next(x...); + return; + } + this->var_ = std::make_tuple(x...); + this->triggered_ = true; + this->loop(); + } + + void stop() override { this->triggered_ = false; } + + void loop() override { + if (!this->triggered_) + return; + + if (this->script_->is_running()) + return; + + this->triggered_ = false; + this->play_next_tuple(this->var_); + } + + float get_setup_priority() const override { return setup_priority::DATA; } + + bool is_running() override { return this->triggered_ || this->is_running_next(); } + + protected: + Script *script_; + bool triggered_{false}; + std::tuple var_{}; +}; + } // namespace script } // namespace esphome diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 1abb6210ce..1a5be0adc3 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -56,6 +56,7 @@ void SDS011Component::dump_config() { ESP_LOGCONFIG(TAG, " RX-only mode: %s", ONOFF(this->rx_mode_only_)); LOG_SENSOR(" ", "PM2.5", this->pm_2_5_sensor_); LOG_SENSOR(" ", "PM10.0", this->pm_10_0_sensor_); + this->check_uart_settings(9600); } void SDS011Component::loop() { @@ -94,7 +95,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..8b41a441ad --- /dev/null +++ b/esphome/components/senseair/senseair.cpp @@ -0,0 +1,80 @@ +#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_); + this->check_uart_settings(9600); +} + +} // 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..605f72a103 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -6,10 +6,9 @@ from esphome import automation from esphome.components import mqtt from esphome.const import CONF_ABOVE, CONF_ACCURACY_DECIMALS, CONF_ALPHA, CONF_BELOW, \ CONF_EXPIRE_AFTER, CONF_FILTERS, CONF_FROM, CONF_ICON, CONF_ID, CONF_INTERNAL, \ - CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, \ - CONF_SEND_EVERY, CONF_SEND_FIRST_AT, CONF_TO, CONF_TRIGGER_ID, \ - CONF_UNIT_OF_MEASUREMENT, \ - CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID + CONF_ON_RAW_VALUE, CONF_ON_VALUE, CONF_ON_VALUE_RANGE, CONF_SEND_EVERY, CONF_SEND_FIRST_AT, \ + CONF_TO, CONF_TRIGGER_ID, CONF_UNIT_OF_MEASUREMENT, CONF_WINDOW_SIZE, CONF_NAME, CONF_MQTT_ID, \ + CONF_FORCE_UPDATE from esphome.core import CORE, coroutine, coroutine_with_priority from esphome.util import Registry @@ -61,6 +60,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 +73,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 @@ -85,6 +86,7 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend({ cv.Optional(CONF_UNIT_OF_MEASUREMENT): unit_of_measurement, cv.Optional(CONF_ICON): icon, cv.Optional(CONF_ACCURACY_DECIMALS): accuracy_decimals, + cv.Optional(CONF_FORCE_UPDATE, default=False): cv.boolean, cv.Optional(CONF_EXPIRE_AFTER): cv.All(cv.requires_component('mqtt'), cv.Any(None, cv.positive_time_period_milliseconds)), cv.Optional(CONF_FILTERS): validate_filters, @@ -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) @@ -210,7 +258,8 @@ def setup_sensor_core_(var, config): cg.add(var.set_icon(config[CONF_ICON])) if CONF_ACCURACY_DECIMALS in config: cg.add(var.set_accuracy_decimals(config[CONF_ACCURACY_DECIMALS])) - if CONF_FILTERS in config: + cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) + if config.get(CONF_FILTERS): # must exist and not be empty filters = yield build_filters(config[CONF_FILTERS]) cg.add(var.set_filters(filters)) @@ -303,6 +352,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/sensor/sensor.h b/esphome/components/sensor/sensor.h index 1c7c854394..f23f022767 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -9,14 +9,17 @@ namespace sensor { #define LOG_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ - ESP_LOGCONFIG(TAG, prefix " Unit of Measurement: '%s'", obj->get_unit_of_measurement().c_str()); \ - ESP_LOGCONFIG(TAG, prefix " Accuracy Decimals: %d", obj->get_accuracy_decimals()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Unit of Measurement: '%s'", prefix, obj->get_unit_of_measurement().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Accuracy Decimals: %d", prefix, obj->get_accuracy_decimals()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ + } \ + if (obj->get_force_update()) { \ + ESP_LOGV(TAG, "%s Force Update: YES", prefix); \ } \ } @@ -142,6 +145,15 @@ class Sensor : public Nameable { void internal_send_state_to_frontend(float state); + bool get_force_update() const { return force_update_; } + /** Set this sensor's force_update mode. + * + * If the sensor is in force_update mode, the frontend is required to save all + * state changes to the database when they are published, even if the state is the + * same as before. + */ + void set_force_update(bool force_update) { force_update_ = force_update; } + protected: /** Override this to set the Home Assistant unit of measurement for this sensor. * @@ -174,6 +186,7 @@ class Sensor : public Nameable { optional accuracy_decimals_; Filter *filter_list_{nullptr}; ///< Store all active filters. bool has_state_{false}; + bool force_update_{false}; }; class PollingSensorComponent : public PollingComponent, public Sensor { diff --git a/esphome/components/sgp30/__init__.py b/esphome/components/sgp30/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp30/sensor.py b/esphome/components/sgp30/sensor.py new file mode 100644 index 0000000000..6329b122fd --- /dev/null +++ b/esphome/components/sgp30/sensor.py @@ -0,0 +1,54 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_RADIATOR, UNIT_PARTS_PER_MILLION, \ + UNIT_PARTS_PER_BILLION, ICON_PERIODIC_TABLE_CO2 + +DEPENDENCIES = ['i2c'] + +sgp30_ns = cg.esphome_ns.namespace('sgp30') +SGP30Component = sgp30_ns.class_('SGP30Component', cg.PollingComponent, i2c.I2CDevice) + +CONF_ECO2 = 'eco2' +CONF_TVOC = 'tvoc' +CONF_BASELINE = 'baseline' +CONF_UPTIME = 'uptime' +CONF_COMPENSATION = 'compensation' +CONF_HUMIDITY_SOURCE = 'humidity_source' +CONF_TEMPERATURE_SOURCE = 'temperature_source' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SGP30Component), + cv.Required(CONF_ECO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, + ICON_PERIODIC_TABLE_CO2, 0), + cv.Required(CONF_TVOC): sensor.sensor_schema(UNIT_PARTS_PER_BILLION, ICON_RADIATOR, 0), + cv.Optional(CONF_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema({ + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor) + }), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x58)) + + +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_ECO2 in config: + sens = yield sensor.new_sensor(config[CONF_ECO2]) + cg.add(var.set_eco2_sensor(sens)) + + if CONF_TVOC in config: + sens = yield sensor.new_sensor(config[CONF_TVOC]) + cg.add(var.set_tvoc_sensor(sens)) + + if CONF_BASELINE in config: + cg.add(var.set_baseline(config[CONF_BASELINE])) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + cg.add(var.set_temperature_sensor(sens)) diff --git a/esphome/components/sgp30/sgp30.cpp b/esphome/components/sgp30/sgp30.cpp new file mode 100644 index 0000000000..9a73295447 --- /dev/null +++ b/esphome/components/sgp30/sgp30.cpp @@ -0,0 +1,295 @@ +#include "sgp30.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sgp30 { + +static const char *TAG = "sgp30"; + +static const uint16_t SGP30_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP30_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP30_CMD_IAQ_INIT = 0x2003; +static const uint16_t SGP30_CMD_MEASURE_IAQ = 0x2008; +static const uint16_t SGP30_CMD_SET_ABSOLUTE_HUMIDITY = 0x2061; +static const uint16_t SGP30_CMD_GET_IAQ_BASELINE = 0x2015; +static const uint16_t SGP30_CMD_SET_IAQ_BASELINE = 0x201E; + +// Sensor baseline should first be relied on after 1H of operation, +// if the sensor starts with a baseline value provided +const long IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED = 3600; + +// Sensor baseline could first be relied on after 12H of operation, +// if the sensor starts without any prior baseline value provided +const long IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE = 43200; + +void SGP30Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SGP30..."); + + // Serial Number identification + if (!this->write_command_(SGP30_CMD_GET_SERIAL_ID)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + uint16_t raw_serial_number[3]; + + if (!this->read_data_(raw_serial_number, 3)) { + this->mark_failed(); + return; + } + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + (uint64_t(raw_serial_number[2])); + ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + + // Featureset identification for future use + if (!this->write_command_(SGP30_CMD_GET_FEATURESET)) { + this->mark_failed(); + return; + } + uint16_t raw_featureset[1]; + if (!this->read_data_(raw_featureset, 1)) { + this->mark_failed(); + return; + } + this->featureset_ = raw_featureset[0]; + if (uint16_t(this->featureset_ >> 12) != 0x0) { + if (uint16_t(this->featureset_ >> 12) == 0x1) { + // ID matching a different sensor: SGPC3 + this->error_code_ = UNSUPPORTED_ID; + } else { + // Unknown ID + this->error_code_ = INVALID_ID; + } + this->mark_failed(); + return; + } + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + // Sensor initialization + if (!this->write_command_(SGP30_CMD_IAQ_INIT)) { + ESP_LOGE(TAG, "Sensor sgp30_iaq_init failed."); + this->error_code_ = MEASUREMENT_INIT_FAILED; + this->mark_failed(); + return; + } + + // Sensor baseline reliability timer + if (this->baseline_ > 0) { + this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITH_BASELINE_PROVIDED; + this->write_iaq_baseline_(this->baseline_); + } else { + this->required_warm_up_time_ = IAQ_BASELINE_WARM_UP_SECONDS_WITHOUT_BASELINE; + } +} + +bool SGP30Component::is_sensor_baseline_reliable_() { + if ((this->required_warm_up_time_ == 0) || (std::floor(millis() / 1000) >= this->required_warm_up_time_)) { + // requirement for warm up is removed once the millis uptime surpasses the required warm_up_time + // this avoids the repetitive warm up when the millis uptime is rolled over every ~40 days + this->required_warm_up_time_ = 0; + return true; + } + return false; +} + +void SGP30Component::read_iaq_baseline_() { + if (this->is_sensor_baseline_reliable_()) { + if (!this->write_command_(SGP30_CMD_GET_IAQ_BASELINE)) { + ESP_LOGD(TAG, "Error getting baseline"); + this->status_set_warning(); + return; + } + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + uint8_t eco2baseline = (raw_data[0]); + uint8_t tvocbaseline = (raw_data[1]); + + ESP_LOGI(TAG, "Current eCO2 & TVOC baseline: 0x%04X", uint16_t((eco2baseline << 8) | (tvocbaseline & 0xFF))); + this->status_clear_warning(); + }); + } else { + ESP_LOGD(TAG, "Baseline reading not available for: %.0fs", + (this->required_warm_up_time_ - std::floor(millis() / 1000))); + } +} + +void SGP30Component::send_env_data_() { + if (this->humidity_sensor_ == nullptr && this->temperature_sensor_ == nullptr) + return; + float humidity = NAN; + if (this->humidity_sensor_ != nullptr) + humidity = this->humidity_sensor_->state; + if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + ESP_LOGW(TAG, "Compensation not possible yet: bad humidity data."); + return; + } else { + ESP_LOGD(TAG, "External compensation data received: Humidity %0.2f%%", humidity); + } + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + ESP_LOGW(TAG, "Compensation not possible yet: bad temperature value data."); + return; + } else { + ESP_LOGD(TAG, "External compensation data received: Temperature %0.2f°C", temperature); + } + + float absolute_humidity = + 216.7f * (((humidity / 100) * 6.112f * std::exp((17.62f * temperature) / (243.12f + temperature))) / + (273.15f + temperature)); + uint8_t humidity_full = uint8_t(std::floor(absolute_humidity)); + uint8_t humidity_dec = uint8_t(std::floor((absolute_humidity - std::floor(absolute_humidity)) * 256)); + ESP_LOGD(TAG, "Calculated Absolute humidity: %0.3f g/m³ (0x%04X)", absolute_humidity, + uint16_t(uint16_t(humidity_full) << 8 | uint16_t(humidity_dec))); + uint8_t crc = sht_crc_(humidity_full, humidity_dec); + uint8_t data[4]; + data[0] = SGP30_CMD_SET_ABSOLUTE_HUMIDITY & 0xFF; + data[1] = humidity_full; + data[2] = humidity_dec; + data[3] = crc; + if (!this->write_bytes(SGP30_CMD_SET_ABSOLUTE_HUMIDITY >> 8, data, 4)) { + ESP_LOGE(TAG, "Error sending compensation data."); + } +} + +void SGP30Component::write_iaq_baseline_(uint16_t baseline) { + uint8_t e_c_o2_baseline = baseline >> 8; + uint8_t tvoc_baseline = baseline & 0xFF; + uint8_t data[4]; + data[0] = SGP30_CMD_SET_IAQ_BASELINE & 0xFF; + data[1] = e_c_o2_baseline; + data[2] = tvoc_baseline; + data[3] = sht_crc_(e_c_o2_baseline, tvoc_baseline); + if (!this->write_bytes(SGP30_CMD_SET_IAQ_BASELINE >> 8, data, 4)) { + ESP_LOGE(TAG, "Error applying baseline: 0x%04X", baseline); + } else + ESP_LOGI(TAG, "Initial baseline 0x%04X applied successfully!", baseline); +} + +void SGP30Component::dump_config() { + ESP_LOGCONFIG(TAG, "SGP30:"); + 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 INVALID_ID: + ESP_LOGW(TAG, "Sensor reported an invalid ID. Is this an SGP30?"); + break; + case UNSUPPORTED_ID: + ESP_LOGW(TAG, "Sensor reported an unsupported ID (SGPC3)."); + break; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Baseline: 0x%04X%s", this->baseline_, + ((this->baseline_ != 0x0000) ? " (enabled)" : " (disabled)")); + ESP_LOGCONFIG(TAG, " Warm up time: %lds", this->required_warm_up_time_); + } + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "eCO2", this->eco2_sensor_); + LOG_SENSOR(" ", "TVOC", this->tvoc_sensor_); + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } +} + +void SGP30Component::update() { + if (!this->write_command_(SGP30_CMD_MEASURE_IAQ)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + float eco2 = (raw_data[0]); + float tvoc = (raw_data[1]); + + ESP_LOGD(TAG, "Got eCO2=%.1fppm TVOC=%.1fppb", eco2, tvoc); + if (this->eco2_sensor_ != nullptr) + this->eco2_sensor_->publish_state(eco2); + if (this->tvoc_sensor_ != nullptr) + this->tvoc_sensor_->publish_state(tvoc); + this->status_clear_warning(); + this->send_env_data_(); + this->read_iaq_baseline_(); + }); +} + +bool SGP30Component::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 SGP30Component::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 SGP30Component::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 sgp30 +} // namespace esphome diff --git a/esphome/components/sgp30/sgp30.h b/esphome/components/sgp30/sgp30.h new file mode 100644 index 0000000000..2362d1bca6 --- /dev/null +++ b/esphome/components/sgp30/sgp30.h @@ -0,0 +1,54 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include + +namespace esphome { +namespace sgp30 { + +/// This class implements support for the Sensirion SGP30 i2c GAS (VOC and CO2eq) sensors. +class SGP30Component : public PollingComponent, public i2c::I2CDevice { + public: + void set_eco2_sensor(sensor::Sensor *eco2) { eco2_sensor_ = eco2; } + void set_tvoc_sensor(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } + void set_baseline(uint16_t baseline) { baseline_ = baseline; } + 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); + void send_env_data_(); + void read_iaq_baseline_(); + bool is_sensor_baseline_reliable_(); + void write_iaq_baseline_(uint16_t baseline); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + uint64_t serial_number_; + uint16_t featureset_; + long required_warm_up_time_; + + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + UNKNOWN + } error_code_{UNKNOWN}; + + sensor::Sensor *eco2_sensor_{nullptr}; + sensor::Sensor *tvoc_sensor_{nullptr}; + uint16_t baseline_{0x0000}; + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; +}; + +} // namespace sgp30 +} // 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/shtcx/__init__.py b/esphome/components/shtcx/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/shtcx/sensor.py b/esphome/components/shtcx/sensor.py new file mode 100644 index 0000000000..eb215078e7 --- /dev/null +++ b/esphome/components/shtcx/sensor.py @@ -0,0 +1,32 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_HUMIDITY, CONF_ID, CONF_TEMPERATURE, ICON_WATER_PERCENT, \ + ICON_THERMOMETER, UNIT_CELSIUS, UNIT_PERCENT + +DEPENDENCIES = ['i2c'] + +shtcx_ns = cg.esphome_ns.namespace('shtcx') +SHTCXComponent = shtcx_ns.class_('SHTCXComponent', cg.PollingComponent, i2c.I2CDevice) + +SHTCXType = shtcx_ns.enum('SHTCXType') + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SHTCXComponent), + 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(0x70)) + + +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_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature_sensor(sens)) + + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity_sensor(sens)) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp new file mode 100644 index 0000000000..b8daceb1af --- /dev/null +++ b/esphome/components/shtcx/shtcx.cpp @@ -0,0 +1,166 @@ +#include "shtcx.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace shtcx { + +static const char *TAG = "shtcx"; + +static const uint16_t SHTCX_COMMAND_SLEEP = 0xB098; +static const uint16_t SHTCX_COMMAND_WAKEUP = 0x3517; +static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; +static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; +static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; + +inline const char *to_string(SHTCXType type) { + switch (type) { + case SHTCX_TYPE_SHTC3: + return "SHTC3"; + case SHTCX_TYPE_SHTC1: + return "SHTC1"; + default: + return "[Unknown model]"; + } +} + +void SHTCXComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up SHTCx..."); + this->soft_reset(); + + if (!this->write_command_(SHTCX_COMMAND_READ_ID_REGISTER)) { + ESP_LOGE(TAG, "Error requesting Device ID"); + this->mark_failed(); + return; + } + + uint16_t device_id_register[1]; + if (!this->read_data_(device_id_register, 1)) { + ESP_LOGE(TAG, "Error reading Device ID"); + this->mark_failed(); + return; + } + + if (((device_id_register[0] << 2) & 0x1C) == 0x1C) { + if ((device_id_register[0] & 0x847) == 0x847) { + this->type_ = SHTCX_TYPE_SHTC3; + } else { + this->type_ = SHTCX_TYPE_SHTC1; + } + } else { + this->type_ = SHTCX_TYPE_UNKNOWN; + } + ESP_LOGCONFIG(TAG, " Device identified: %s", to_string(this->type_)); +} +void SHTCXComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SHTCx:"); + ESP_LOGCONFIG(TAG, " Model: %s", to_string(this->type_)); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with SHTCx failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); +} +float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; } +void SHTCXComponent::update() { + if (this->status_has_warning()) { + ESP_LOGW(TAG, "Retrying to reconnect the sensor."); + this->soft_reset(); + } + if (this->type_ != SHTCX_TYPE_SHTC1) { + this->wake_up(); + } + if (!this->write_command_(SHTCX_COMMAND_POLLING_H)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[2]; + if (!this->read_data_(raw_data, 2)) { + this->status_set_warning(); + return; + } + + float temperature = 175.0f * float(raw_data[0]) / 65536.0f - 45.0f; + float humidity = 100.0f * float(raw_data[1]) / 65536.0f; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); + this->status_clear_warning(); + if (this->type_ != SHTCX_TYPE_SHTC1) { + this->sleep(); + } + }); +} + +bool SHTCXComponent::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 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 SHTCXComponent::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; +} + +void SHTCXComponent::soft_reset() { + this->write_command_(SHTCX_COMMAND_SOFT_RESET); + delayMicroseconds(200); +} +void SHTCXComponent::sleep() { this->write_command_(SHTCX_COMMAND_SLEEP); } + +void SHTCXComponent::wake_up() { + this->write_command_(SHTCX_COMMAND_WAKEUP); + delayMicroseconds(200); +} + +} // namespace shtcx +} // namespace esphome diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h new file mode 100644 index 0000000000..ccc6533bfa --- /dev/null +++ b/esphome/components/shtcx/shtcx.h @@ -0,0 +1,35 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace shtcx { + +enum SHTCXType { SHTCX_TYPE_SHTC3 = 0, SHTCX_TYPE_SHTC1, SHTCX_TYPE_UNKNOWN }; + +/// This class implements support for the SHT3x-DIS family of temperature+humidity i2c sensors. +class SHTCXComponent : public PollingComponent, public i2c::I2CDevice { + public: + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + void soft_reset(); + void sleep(); + void wake_up(); + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + SHTCXType type_; + sensor::Sensor *temperature_sensor_; + sensor::Sensor *humidity_sensor_; +}; + +} // namespace shtcx +} // namespace esphome diff --git a/esphome/components/shutdown/shutdown_switch.cpp b/esphome/components/shutdown/shutdown_switch.cpp index d27bb8aadc..ce33cd187f 100644 --- a/esphome/components/shutdown/shutdown_switch.cpp +++ b/esphome/components/shutdown/shutdown_switch.cpp @@ -14,7 +14,7 @@ void ShutdownSwitch::write_state(bool state) { if (state) { ESP_LOGI(TAG, "Shutting down..."); - delay(100); // Let MQTT settle a bit + delay(100); // NOLINT App.run_safe_shutdown_hooks(); #ifdef ARDUINO_ARCH_ESP8266 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..1390ef8b49 --- /dev/null +++ b/esphome/components/sim800l/sim800l.cpp @@ -0,0 +1,264 @@ +#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(); +} +void Sim800LComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SIM800L:"); + ESP_LOGCONFIG(TAG, " RSSI: %d dB", this->rssi_); +} + +} // namespace sim800l +} // namespace esphome diff --git a/esphome/components/sim800l/sim800l.h b/esphome/components/sim800l/sim800l.h new file mode 100644 index 0000000000..696eb8890f --- /dev/null +++ b/esphome/components/sim800l/sim800l.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/automation.h" + +namespace esphome { +namespace sim800l { + +const uint8_t SIM800L_READ_BUFFER_LENGTH = 255; + +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 dump_config() 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..85f78c8cf5 --- /dev/null +++ b/esphome/components/sm16716/sm16716.h @@ -0,0 +1,71 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.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/speed/fan/__init__.py b/esphome/components/speed/fan/__init__.py index ae6d09dfac..65ee5960f0 100644 --- a/esphome/components/speed/fan/__init__.py +++ b/esphome/components/speed/fan/__init__.py @@ -29,4 +29,4 @@ def to_code(config): if CONF_OSCILLATION_OUTPUT in config: oscillation_output = yield cg.get_variable(config[CONF_OSCILLATION_OUTPUT]) - cg.add(var.set_oscillation(oscillation_output)) + cg.add(var.set_oscillating(oscillation_output)) 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/ssd1325_base/__init__.py b/esphome/components/ssd1325_base/__init__.py new file mode 100644 index 0000000000..69e11ec0d1 --- /dev/null +++ b/esphome/components/ssd1325_base/__init__.py @@ -0,0 +1,42 @@ +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.core import coroutine + +ssd1325_base_ns = cg.esphome_ns.namespace('ssd1325_base') +SSD1325 = ssd1325_base_ns.class_('SSD1325', cg.PollingComponent, display.DisplayBuffer) +SSD1325Model = ssd1325_base_ns.enum('SSD1325Model') + +MODELS = { + 'SSD1325_128X32': SSD1325Model.SSD1325_MODEL_128_32, + 'SSD1325_128X64': SSD1325Model.SSD1325_MODEL_128_64, + 'SSD1325_96X16': SSD1325Model.SSD1325_MODEL_96_16, + 'SSD1325_64X48': SSD1325Model.SSD1325_MODEL_64_48, +} + +SSD1325_MODEL = cv.enum(MODELS, upper=True, space="_") + +SSD1325_SCHEMA = display.FULL_DISPLAY_SCHEMA.extend({ + cv.Required(CONF_MODEL): SSD1325_MODEL, + cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_EXTERNAL_VCC): cv.boolean, +}).extend(cv.polling_component_schema('1s')) + + +@coroutine +def setup_ssd1036(var, config): + yield cg.register_component(var, config) + yield display.register_display(var, config) + + cg.add(var.set_model(config[CONF_MODEL])) + if CONF_RESET_PIN in config: + reset = yield cg.gpio_pin_expression(config[CONF_RESET_PIN]) + cg.add(var.set_reset_pin(reset)) + if CONF_EXTERNAL_VCC in config: + cg.add(var.set_external_vcc(config[CONF_EXTERNAL_VCC])) + if CONF_LAMBDA in config: + lambda_ = yield cg.process_lambda( + config[CONF_LAMBDA], [(display.DisplayBufferRef, 'it')], return_type=cg.void) + cg.add(var.set_writer(lambda_)) diff --git a/esphome/components/ssd1325_base/ssd1325_base.cpp b/esphome/components/ssd1325_base/ssd1325_base.cpp new file mode 100644 index 0000000000..3079e19cc8 --- /dev/null +++ b/esphome/components/ssd1325_base/ssd1325_base.cpp @@ -0,0 +1,177 @@ +#include "ssd1325_base.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace ssd1325_base { + +static const char *TAG = "ssd1325"; + +static const uint8_t BLACK = 0; +static const uint8_t WHITE = 1; + +static const uint8_t SSD1325_SETCOLADDR = 0x15; +static const uint8_t SSD1325_SETROWADDR = 0x75; +static const uint8_t SSD1325_SETCONTRAST = 0x81; +static const uint8_t SSD1325_SETCURRENT = 0x84; + +static const uint8_t SSD1325_SETREMAP = 0xA0; +static const uint8_t SSD1325_SETSTARTLINE = 0xA1; +static const uint8_t SSD1325_SETOFFSET = 0xA2; +static const uint8_t SSD1325_NORMALDISPLAY = 0xA4; +static const uint8_t SSD1325_DISPLAYALLON = 0xA5; +static const uint8_t SSD1325_DISPLAYALLOFF = 0xA6; +static const uint8_t SSD1325_INVERTDISPLAY = 0xA7; +static const uint8_t SSD1325_SETMULTIPLEX = 0xA8; +static const uint8_t SSD1325_MASTERCONFIG = 0xAD; +static const uint8_t SSD1325_DISPLAYOFF = 0xAE; +static const uint8_t SSD1325_DISPLAYON = 0xAF; + +static const uint8_t SSD1325_SETPRECHARGECOMPENABLE = 0xB0; +static const uint8_t SSD1325_SETPHASELEN = 0xB1; +static const uint8_t SSD1325_SETROWPERIOD = 0xB2; +static const uint8_t SSD1325_SETCLOCK = 0xB3; +static const uint8_t SSD1325_SETPRECHARGECOMP = 0xB4; +static const uint8_t SSD1325_SETGRAYTABLE = 0xB8; +static const uint8_t SSD1325_SETPRECHARGEVOLTAGE = 0xBC; +static const uint8_t SSD1325_SETVCOMLEVEL = 0xBE; +static const uint8_t SSD1325_SETVSL = 0xBF; + +static const uint8_t SSD1325_GFXACCEL = 0x23; +static const uint8_t SSD1325_DRAWRECT = 0x24; +static const uint8_t SSD1325_COPY = 0x25; + +void SSD1325::setup() { + this->init_internal_(this->get_buffer_length_()); + + this->command(SSD1325_DISPLAYOFF); /* display off */ + this->command(SSD1325_SETCLOCK); /* set osc division */ + this->command(0xF1); /* 145 */ + this->command(SSD1325_SETMULTIPLEX); /* multiplex ratio */ + this->command(0x3f); /* duty = 1/64 */ + this->command(SSD1325_SETOFFSET); /* set display offset --- */ + this->command(0x4C); /* 76 */ + this->command(SSD1325_SETSTARTLINE); /*set start line */ + this->command(0x00); /* ------ */ + this->command(SSD1325_MASTERCONFIG); /*Set Master Config DC/DC Converter*/ + this->command(0x02); + this->command(SSD1325_SETREMAP); /* set segment remap------ */ + this->command(0x56); + this->command(SSD1325_SETCURRENT + 0x2); /* Set Full Current Range */ + this->command(SSD1325_SETGRAYTABLE); + this->command(0x01); + this->command(0x11); + this->command(0x22); + this->command(0x32); + this->command(0x43); + this->command(0x54); + this->command(0x65); + this->command(0x76); + this->command(SSD1325_SETCONTRAST); /* set contrast current */ + this->command(0x7F); // max! + this->command(SSD1325_SETROWPERIOD); + this->command(0x51); + this->command(SSD1325_SETPHASELEN); + this->command(0x55); + this->command(SSD1325_SETPRECHARGECOMP); + this->command(0x02); + this->command(SSD1325_SETPRECHARGECOMPENABLE); + this->command(0x28); + this->command(SSD1325_SETVCOMLEVEL); // Set High Voltage Level of COM Pin + this->command(0x1C); //? + this->command(SSD1325_SETVSL); // set Low Voltage Level of SEG Pin + this->command(0x0D | 0x02); + this->command(SSD1325_NORMALDISPLAY); /* set display mode */ + this->command(SSD1325_DISPLAYON); /* display ON */ +} +void SSD1325::display() { + this->command(SSD1325_SETCOLADDR); /* set column address */ + this->command(0x00); /* set column start address */ + this->command(0x3F); /* set column end address */ + this->command(SSD1325_SETROWADDR); /* set row address */ + this->command(0x00); /* set row start address */ + this->command(0x3F); /* set row end address */ + + this->write_display_data(); +} +void SSD1325::update() { + this->do_update_(); + this->display(); +} +int SSD1325::get_height_internal() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + return 32; + case SSD1325_MODEL_128_64: + return 64; + case SSD1325_MODEL_96_16: + return 16; + case SSD1325_MODEL_64_48: + return 48; + default: + return 0; + } +} +int SSD1325::get_width_internal() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + case SSD1325_MODEL_128_64: + return 128; + case SSD1325_MODEL_96_16: + return 96; + case SSD1325_MODEL_64_48: + return 64; + default: + return 0; + } +} +size_t SSD1325::get_buffer_length_() { + return size_t(this->get_width_internal()) * size_t(this->get_height_internal()) / 8u; +} + +void HOT SSD1325::draw_absolute_pixel_internal(int x, int y, int color) { + if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) + return; + + uint16_t pos = x + (y / 8) * this->get_width_internal(); + uint8_t subpos = y % 8; + if (color) { + this->buffer_[pos] |= (1 << subpos); + } else { + this->buffer_[pos] &= ~(1 << subpos); + } +} +void SSD1325::fill(int color) { + uint8_t fill = color ? 0xFF : 0x00; + for (uint32_t i = 0; i < this->get_buffer_length_(); i++) + this->buffer_[i] = fill; +} +void SSD1325::init_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->setup(); + this->reset_pin_->digital_write(true); + delay(1); + // Trigger Reset + this->reset_pin_->digital_write(false); + delay(10); + // Wake up + this->reset_pin_->digital_write(true); + } +} +const char *SSD1325::model_str_() { + switch (this->model_) { + case SSD1325_MODEL_128_32: + return "SSD1325 128x32"; + case SSD1325_MODEL_128_64: + return "SSD1325 128x64"; + case SSD1325_MODEL_96_16: + return "SSD1325 96x16"; + case SSD1325_MODEL_64_48: + return "SSD1325 64x48"; + default: + return "Unknown"; + } +} + +} // namespace ssd1325_base +} // namespace esphome diff --git a/esphome/components/ssd1325_base/ssd1325_base.h b/esphome/components/ssd1325_base/ssd1325_base.h new file mode 100644 index 0000000000..e227f68f86 --- /dev/null +++ b/esphome/components/ssd1325_base/ssd1325_base.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/esphal.h" +#include "esphome/components/display/display_buffer.h" + +namespace esphome { +namespace ssd1325_base { + +enum SSD1325Model { + SSD1325_MODEL_128_32 = 0, + SSD1325_MODEL_128_64, + SSD1325_MODEL_96_16, + SSD1325_MODEL_64_48, +}; + +class SSD1325 : public PollingComponent, public display::DisplayBuffer { + public: + void setup() override; + + void display(); + + void update() override; + + void set_model(SSD1325Model model) { this->model_ = model; } + void set_reset_pin(GPIOPin *reset_pin) { this->reset_pin_ = reset_pin; } + void set_external_vcc(bool external_vcc) { this->external_vcc_ = external_vcc; } + + float get_setup_priority() const override { return setup_priority::PROCESSOR; } + void fill(int color) override; + + protected: + virtual void command(uint8_t value) = 0; + virtual void write_display_data() = 0; + void init_reset_(); + + void draw_absolute_pixel_internal(int x, int y, int color) override; + + int get_height_internal() override; + int get_width_internal() override; + size_t get_buffer_length_(); + const char *model_str_(); + + SSD1325Model model_{SSD1325_MODEL_128_64}; + GPIOPin *reset_pin_{nullptr}; + bool external_vcc_{false}; +}; + +} // namespace ssd1325_base +} // namespace esphome diff --git a/esphome/components/ssd1325_spi/__init__.py b/esphome/components/ssd1325_spi/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ssd1325_spi/display.py b/esphome/components/ssd1325_spi/display.py new file mode 100644 index 0000000000..4615d45393 --- /dev/null +++ b/esphome/components/ssd1325_spi/display.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import pins +from esphome.components import spi, ssd1325_base +from esphome.const import CONF_DC_PIN, CONF_ID, CONF_LAMBDA, CONF_PAGES + +AUTO_LOAD = ['ssd1325_base'] +DEPENDENCIES = ['spi'] + +ssd1325_spi = cg.esphome_ns.namespace('ssd1325_spi') +SPISSD1325 = ssd1325_spi.class_('SPISSD1325', ssd1325_base.SSD1325, spi.SPIDevice) + +CONFIG_SCHEMA = cv.All(ssd1325_base.SSD1325_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SPISSD1325), + cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema, +}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA), + cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield ssd1325_base.setup_ssd1036(var, config) + yield spi.register_spi_device(var, config) + + dc = yield cg.gpio_pin_expression(config[CONF_DC_PIN]) + cg.add(var.set_dc_pin(dc)) diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.cpp b/esphome/components/ssd1325_spi/ssd1325_spi.cpp new file mode 100644 index 0000000000..399700f1dd --- /dev/null +++ b/esphome/components/ssd1325_spi/ssd1325_spi.cpp @@ -0,0 +1,64 @@ +#include "ssd1325_spi.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace ssd1325_spi { + +static const char *TAG = "ssd1325_spi"; + +void SPISSD1325::setup() { + ESP_LOGCONFIG(TAG, "Setting up SPI SSD1325..."); + this->spi_setup(); + this->dc_pin_->setup(); // OUTPUT + this->cs_->setup(); // OUTPUT + + this->init_reset_(); + delay(500); // NOLINT + SSD1325::setup(); +} +void SPISSD1325::dump_config() { + LOG_DISPLAY("", "SPI SSD1325", this); + ESP_LOGCONFIG(TAG, " Model: %s", this->model_str_()); + LOG_PIN(" CS Pin: ", this->cs_); + LOG_PIN(" DC Pin: ", this->dc_pin_); + LOG_PIN(" Reset Pin: ", this->reset_pin_); + ESP_LOGCONFIG(TAG, " External VCC: %s", YESNO(this->external_vcc_)); + LOG_UPDATE_INTERVAL(this); +} +void SPISSD1325::command(uint8_t value) { + this->cs_->digital_write(true); + this->dc_pin_->digital_write(false); + delay(1); + this->enable(); + this->cs_->digital_write(false); + this->write_byte(value); + this->cs_->digital_write(true); + this->disable(); +} +void HOT SPISSD1325::write_display_data() { + this->cs_->digital_write(true); + this->dc_pin_->digital_write(true); + this->cs_->digital_write(false); + delay(1); + this->enable(); + for (uint16_t x = 0; x < this->get_width_internal(); x += 2) { + for (uint16_t y = 0; y < this->get_height_internal(); y += 8) { // we write 8 pixels at once + uint8_t left8 = this->buffer_[y * 16 + x]; + uint8_t right8 = this->buffer_[y * 16 + x + 1]; + for (uint8_t p = 0; p < 8; p++) { + uint8_t d = 0; + if (left8 & (1 << p)) + d |= 0xF0; + if (right8 & (1 << p)) + d |= 0x0F; + this->write_byte(d); + } + } + } + this->cs_->digital_write(true); + this->disable(); +} + +} // namespace ssd1325_spi +} // namespace esphome diff --git a/esphome/components/ssd1325_spi/ssd1325_spi.h b/esphome/components/ssd1325_spi/ssd1325_spi.h new file mode 100644 index 0000000000..e4e7d55769 --- /dev/null +++ b/esphome/components/ssd1325_spi/ssd1325_spi.h @@ -0,0 +1,29 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ssd1325_base/ssd1325_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace ssd1325_spi { + +class SPISSD1325 : public ssd1325_base::SSD1325, + public spi::SPIDevice { + public: + void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } + + void setup() override; + + void dump_config() override; + + protected: + void command(uint8_t value) override; + + void write_display_data() override; + + GPIOPin *dc_pin_; +}; + +} // namespace ssd1325_spi +} // namespace esphome diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp index 7fbeb8c171..90ac1faad7 100644 --- a/esphome/components/status/status_binary_sensor.cpp +++ b/esphome/components/status/status_binary_sensor.cpp @@ -30,7 +30,7 @@ void StatusBinarySensor::loop() { this->publish_state(status); } -void StatusBinarySensor::setup() { this->publish_state(false); } +void StatusBinarySensor::setup() { this->publish_initial_state(false); } void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); } } // namespace status diff --git a/esphome/components/sts3x/__init__.py b/esphome/components/sts3x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sts3x/sensor.py b/esphome/components/sts3x/sensor.py new file mode 100644 index 0000000000..f48deeeae5 --- /dev/null +++ b/esphome/components/sts3x/sensor.py @@ -0,0 +1,22 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, ICON_THERMOMETER, UNIT_CELSIUS + +DEPENDENCIES = ['i2c'] + +sts3x_ns = cg.esphome_ns.namespace('sts3x') + +STS3XComponent = sts3x_ns.class_('STS3XComponent', sensor.Sensor, + cg.PollingComponent, i2c.I2CDevice) + +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({ + cv.GenerateID(): cv.declare_id(STS3XComponent), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x4A)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp new file mode 100644 index 0000000000..1a24a17caf --- /dev/null +++ b/esphome/components/sts3x/sts3x.cpp @@ -0,0 +1,123 @@ +#include "sts3x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sts3x { + +static const char *TAG = "sts3x"; + +static const uint16_t STS3X_COMMAND_READ_SERIAL_NUMBER = 0x3780; +static const uint16_t STS3X_COMMAND_READ_STATUS = 0xF32D; +static const uint16_t STS3X_COMMAND_SOFT_RESET = 0x30A2; +static const uint16_t STS3X_COMMAND_POLLING_H = 0x2400; + +/// Commands for future use +static const uint16_t STS3X_COMMAND_CLEAR_STATUS = 0x3041; +static const uint16_t STS3X_COMMAND_HEATER_ENABLE = 0x306D; +static const uint16_t STS3X_COMMAND_HEATER_DISABLE = 0x3066; +static const uint16_t STS3X_COMMAND_FETCH_DATA = 0xE000; + +void STS3XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up STS3x..."); + if (!this->write_command_(STS3X_COMMAND_READ_SERIAL_NUMBER)) { + this->mark_failed(); + return; + } + + uint16_t raw_serial_number[2]; + if (!this->read_data_(raw_serial_number, 1)) { + this->mark_failed(); + return; + } + uint32_t serial_number = (uint32_t(raw_serial_number[0]) << 16); + ESP_LOGV(TAG, " Serial Number: 0x%08X", serial_number); +} +void STS3XComponent::dump_config() { + ESP_LOGCONFIG(TAG, "STS3x:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with ST3x failed!"); + } + LOG_UPDATE_INTERVAL(this); + + LOG_SENSOR(" ", "STS3x", this); +} +float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; } +void STS3XComponent::update() { + if (this->status_has_warning()) { + ESP_LOGD(TAG, "Retrying to reconnect the sensor."); + this->write_command_(STS3X_COMMAND_SOFT_RESET); + } + if (!this->write_command_(STS3X_COMMAND_POLLING_H)) { + this->status_set_warning(); + return; + } + + this->set_timeout(50, [this]() { + uint16_t raw_data[1]; + if (!this->read_data_(raw_data, 1)) { + this->status_set_warning(); + return; + } + + float temperature = 175.0f * float(raw_data[0]) / 65535.0f - 45.0f; + ESP_LOGD(TAG, "Got temperature=%.2f°C", temperature); + this->publish_state(temperature); + this->status_clear_warning(); + }); +} + +bool STS3XComponent::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 sts3x_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 STS3XComponent::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 = sts3x_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 sts3x +} // namespace esphome diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h new file mode 100644 index 0000000000..436cf938d8 --- /dev/null +++ b/esphome/components/sts3x/sts3x.h @@ -0,0 +1,24 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sts3x { + +/// This class implements support for the ST3x-DIS family of temperature i2c sensors. +class STS3XComponent : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void update() override; + + protected: + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); +}; + +} // namespace sts3x +} // namespace esphome diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 0bcdedcabd..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') @@ -31,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) diff --git a/esphome/components/switch/switch.h b/esphome/components/switch/switch.h index be4fc24c4a..cd6cec429f 100644 --- a/esphome/components/switch/switch.h +++ b/esphome/components/switch/switch.h @@ -9,15 +9,15 @@ namespace switch_ { #define LOG_SWITCH(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (obj->assumed_state()) { \ - ESP_LOGCONFIG(TAG, prefix " Assumed State: YES"); \ + ESP_LOGCONFIG(TAG, "%s Assumed State: YES", prefix); \ } \ if (obj->is_inverted()) { \ - ESP_LOGCONFIG(TAG, prefix " Inverted: YES"); \ + ESP_LOGCONFIG(TAG, "%s Inverted: YES", prefix); \ } \ } diff --git a/esphome/components/sx1509/__init__.py b/esphome/components/sx1509/__init__.py new file mode 100644 index 0000000000..11fcfe3955 --- /dev/null +++ b/esphome/components/sx1509/__init__.py @@ -0,0 +1,77 @@ +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 + +CONF_KEYPAD = 'keypad' +CONF_KEY_ROWS = 'key_rows' +CONF_KEY_COLUMNS = 'key_columns' +CONF_SLEEP_TIME = 'sleep_time' +CONF_SCAN_TIME = 'scan_time' +CONF_DEBOUNCE_TIME = 'debounce_time' + +DEPENDENCIES = ['i2c'] +MULTI_CONF = True + +sx1509_ns = cg.esphome_ns.namespace('sx1509') +SX1509GPIOMode = sx1509_ns.enum('SX1509GPIOMode') +SX1509_GPIO_MODES = { + 'INPUT': SX1509GPIOMode.SX1509_INPUT, + 'INPUT_PULLUP': SX1509GPIOMode.SX1509_INPUT_PULLUP, + 'OUTPUT': SX1509GPIOMode.SX1509_OUTPUT +} + +SX1509Component = sx1509_ns.class_('SX1509Component', cg.Component, i2c.I2CDevice) +SX1509GPIOPin = sx1509_ns.class_('SX1509GPIOPin', cg.GPIOPin) + +KEYPAD_SCHEMA = cv.Schema({ + cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8), + cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8), + cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192), + cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128), + cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64), +}) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SX1509Component), + cv.Optional(CONF_KEYPAD): cv.Schema(KEYPAD_SCHEMA), +}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x3E)) + + +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_KEYPAD in config: + keypad = config[CONF_KEYPAD] + cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS])) + if CONF_SLEEP_TIME in keypad and CONF_SCAN_TIME in keypad and CONF_DEBOUNCE_TIME in keypad: + cg.add(var.set_sleep_time(keypad[CONF_SLEEP_TIME])) + cg.add(var.set_scan_time(keypad[CONF_SCAN_TIME])) + cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME])) + + +CONF_SX1509 = 'sx1509' +CONF_SX1509_ID = 'sx1509_id' + +SX1509_OUTPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SX1509): cv.use_id(SX1509Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="OUTPUT"): cv.enum(SX1509_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) +SX1509_INPUT_PIN_SCHEMA = cv.Schema({ + cv.Required(CONF_SX1509): cv.use_id(SX1509Component), + cv.Required(CONF_NUMBER): cv.int_, + cv.Optional(CONF_MODE, default="INPUT"): cv.enum(SX1509_GPIO_MODES, upper=True), + cv.Optional(CONF_INVERTED, default=False): cv.boolean, +}) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_SX1509, + (SX1509_OUTPUT_PIN_SCHEMA, SX1509_INPUT_PIN_SCHEMA)) +def sx1509_pin_to_code(config): + parent = yield cg.get_variable(config[CONF_SX1509]) + yield SX1509GPIOPin.new(parent, config[CONF_NUMBER], config[CONF_MODE], + config[CONF_INVERTED]) diff --git a/esphome/components/sx1509/binary_sensor/__init__.py b/esphome/components/sx1509/binary_sensor/__init__.py new file mode 100644 index 0000000000..9a65524383 --- /dev/null +++ b/esphome/components/sx1509/binary_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ID +from .. import SX1509Component, sx1509_ns, CONF_SX1509_ID + +CONF_ROW = 'row' +CONF_COL = 'col' + +DEPENDENCIES = ['sx1509'] + +SX1509BinarySensor = sx1509_ns.class_('SX1509BinarySensor', binary_sensor.BinarySensor) + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({ + cv.GenerateID(): cv.declare_id(SX1509BinarySensor), + cv.GenerateID(CONF_SX1509_ID): cv.use_id(SX1509Component), + cv.Required(CONF_ROW): cv.int_range(min=0, max=4), + cv.Required(CONF_COL): cv.int_range(min=0, max=4), +}) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield binary_sensor.register_binary_sensor(var, config) + hub = yield cg.get_variable(config[CONF_SX1509_ID]) + cg.add(var.set_row_col(config[CONF_ROW], config[CONF_COL])) + + cg.add(hub.register_keypad_binary_sensor(var)) diff --git a/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h new file mode 100644 index 0000000000..2eef19782c --- /dev/null +++ b/esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/components/sx1509/sx1509.h" +#include "esphome/components/binary_sensor/binary_sensor.h" + +namespace esphome { +namespace sx1509 { + +class SX1509BinarySensor : public sx1509::SX1509Processor, public binary_sensor::BinarySensor { + public: + void set_row_col(uint8_t row, uint8_t col) { this->key_ = (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..3c94f4a243 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,36 +1,18 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor -from esphome.const import CONF_ID, CONF_SENSOR +from esphome.components import climate_ir +from esphome.const import CONF_ID -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') -Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) +Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate_ir.ClimateIR) -CONF_TRANSMITTER_ID = 'transmitter_id' -CONF_SUPPORTS_HEAT = 'supports_heat' -CONF_SUPPORTS_COOL = 'supports_cool' - -CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ +CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Tcl112Climate), - cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), - 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), -}).extend(cv.COMPONENT_SCHEMA)) +}) def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield climate.register_climate(var, config) - - cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) - cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) - if CONF_SENSOR in config: - sens = yield cg.get_variable(config[CONF_SENSOR]) - cg.add(var.set_sensor(sens)) - - transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) - cg.add(var.set_transmitter(transmitter)) + yield climate_ir.register_climate_ir(var, config) 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..273162662d 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_ir::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_ir::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/output/__init__.py b/esphome/components/template/output/__init__.py index 5cc9e089bd..cc85a9da68 100644 --- a/esphome/components/template/output/__init__.py +++ b/esphome/components/template/output/__init__.py @@ -2,13 +2,12 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import output -from esphome.const import CONF_ID, CONF_TYPE +from esphome.const import CONF_ID, CONF_TYPE, CONF_BINARY from .. import template_ns TemplateBinaryOutput = template_ns.class_('TemplateBinaryOutput', output.BinaryOutput) TemplateFloatOutput = template_ns.class_('TemplateFloatOutput', output.FloatOutput) -CONF_BINARY = 'binary' CONF_FLOAT = 'float' CONF_WRITE_ACTION = 'write_action' diff --git a/esphome/components/text_sensor/text_sensor.h b/esphome/components/text_sensor/text_sensor.h index 719f0b0d62..85c2b644a0 100644 --- a/esphome/components/text_sensor/text_sensor.h +++ b/esphome/components/text_sensor/text_sensor.h @@ -8,12 +8,12 @@ namespace text_sensor { #define LOG_TEXT_SENSOR(prefix, type, obj) \ if (obj != nullptr) { \ - ESP_LOGCONFIG(TAG, prefix type " '%s'", obj->get_name().c_str()); \ + ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, type, obj->get_name().c_str()); \ if (!obj->get_icon().empty()) { \ - ESP_LOGCONFIG(TAG, prefix " Icon: '%s'", obj->get_icon().c_str()); \ + ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, obj->get_icon().c_str()); \ } \ if (!obj->unique_id().empty()) { \ - ESP_LOGV(TAG, prefix " Unique ID: '%s'", obj->unique_id().c_str()); \ + ESP_LOGV(TAG, "%s Unique ID: '%s'", prefix, obj->unique_id().c_str()); \ } \ } diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index 634de26f00..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,8 +53,18 @@ def _tz_dst_str(dt): _tz_timedelta(td)) -def _non_dst_tz(tz, dt): +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)) @@ -104,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 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/tuya/__init__.py b/esphome/components/tuya/__init__.py new file mode 100644 index 0000000000..541f10f862 --- /dev/null +++ b/esphome/components/tuya/__init__.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import CONF_ID + +DEPENDENCIES = ['uart'] + +tuya_ns = cg.esphome_ns.namespace('tuya') +Tuya = tuya_ns.class_('Tuya', cg.Component, uart.UARTDevice) + +CONF_TUYA_ID = 'tuya_id' +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(Tuya), +}).extend(cv.COMPONENT_SCHEMA).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) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py new file mode 100644 index 0000000000..605bdae32e --- /dev/null +++ b/esphome/components/tuya/light/__init__.py @@ -0,0 +1,44 @@ +from esphome.components import light +import esphome.config_validation as cv +import esphome.codegen as cg +from esphome.const import CONF_OUTPUT_ID, CONF_MIN_VALUE, CONF_MAX_VALUE, CONF_GAMMA_CORRECT, \ + CONF_DEFAULT_TRANSITION_LENGTH +from .. import tuya_ns, CONF_TUYA_ID, Tuya + +DEPENDENCIES = ['tuya'] + +CONF_DIMMER_DATAPOINT = "dimmer_datapoint" +CONF_SWITCH_DATAPOINT = "switch_datapoint" + +TuyaLight = tuya_ns.class_('TuyaLight', light.LightOutput, cg.Component) + +CONFIG_SCHEMA = light.BRIGHTNESS_ONLY_LIGHT_SCHEMA.extend({ + cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaLight), + cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya), + cv.Required(CONF_DIMMER_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_MIN_VALUE): cv.int_, + cv.Optional(CONF_MAX_VALUE): cv.int_, + + # Change the default gamma_correct and default transition length settings. + # The Tuya MCU handles transitions and gamma correction on its own. + cv.Optional(CONF_GAMMA_CORRECT, default=1.0): cv.positive_float, + cv.Optional(CONF_DEFAULT_TRANSITION_LENGTH, default='0s'): cv.positive_time_period_milliseconds, +}).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) + yield cg.register_component(var, config) + yield light.register_light(var, config) + + if CONF_DIMMER_DATAPOINT in config: + cg.add(var.set_dimmer_id(config[CONF_DIMMER_DATAPOINT])) + if CONF_SWITCH_DATAPOINT in config: + cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) + if CONF_MIN_VALUE in 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])) + paren = yield cg.get_variable(config[CONF_TUYA_ID]) + cg.add(var.set_tuya_parent(paren)) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp new file mode 100644 index 0000000000..9696252049 --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -0,0 +1,85 @@ +#include "esphome/core/log.h" +#include "tuya_light.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya.light"; + +void TuyaLight::setup() { + if (this->dimmer_id_.has_value()) { + this->parent_->register_listener(*this->dimmer_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_brightness(float(datapoint.value_uint) / this->max_value_); + call.perform(); + }); + } + if (switch_id_.has_value()) { + this->parent_->register_listener(*this->switch_id_, [this](TuyaDatapoint datapoint) { + auto call = this->state_->make_call(); + call.set_state(datapoint.value_bool); + call.perform(); + }); + } +} + +void TuyaLight::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya Dimmer:"); + if (this->dimmer_id_.has_value()) + ESP_LOGCONFIG(TAG, " Dimmer has datapoint ID %u", *this->dimmer_id_); + if (this->switch_id_.has_value()) + ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); +} + +light::LightTraits TuyaLight::get_traits() { + auto traits = light::LightTraits(); + traits.set_supports_brightness(this->dimmer_id_.has_value()); + return traits; +} + +void TuyaLight::setup_state(light::LightState *state) { state_ = state; } + +void TuyaLight::write_state(light::LightState *state) { + float brightness; + state->current_values_as_brightness(&brightness); + + if (brightness == 0.0f) { + // turning off, first try via switch (if exists), then dimmer + if (switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = false; + + parent_->set_datapoint_value(datapoint); + } else if (dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = 0; + parent_->set_datapoint_value(datapoint); + } + return; + } + + auto brightness_int = static_cast(brightness * this->max_value_); + brightness_int = std::max(brightness_int, this->min_value_); + + if (this->dimmer_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->dimmer_id_; + datapoint.type = TuyaDatapointType::INTEGER; + datapoint.value_int = brightness_int; + parent_->set_datapoint_value(datapoint); + } + if (this->switch_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->switch_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + datapoint.value_bool = true; + parent_->set_datapoint_value(datapoint); + } +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h new file mode 100644 index 0000000000..581512c29c --- /dev/null +++ b/esphome/components/tuya/light/tuya_light.h @@ -0,0 +1,36 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/tuya/tuya.h" +#include "esphome/components/light/light_output.h" + +namespace esphome { +namespace tuya { + +class TuyaLight : public Component, public light::LightOutput { + public: + void setup() override; + void dump_config() override; + void set_dimmer_id(uint8_t dimmer_id) { this->dimmer_id_ = dimmer_id; } + void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } + void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } + void set_min_value(uint32_t min_value) { min_value_ = min_value; } + void set_max_value(uint32_t max_value) { max_value_ = max_value; } + light::LightTraits get_traits() override; + void setup_state(light::LightState *state) override; + void write_state(light::LightState *state) override; + + protected: + void update_dimmer_(uint32_t value); + void update_switch_(uint32_t value); + + Tuya *parent_; + optional dimmer_id_{}; + optional switch_id_{}; + uint32_t min_value_ = 0; + uint32_t max_value_ = 255; + light::LightState *state_{nullptr}; +}; + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp new file mode 100644 index 0000000000..cb796644c8 --- /dev/null +++ b/esphome/components/tuya/tuya.cpp @@ -0,0 +1,298 @@ +#include "tuya.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" + +namespace esphome { +namespace tuya { + +static const char *TAG = "tuya"; + +void Tuya::setup() { + this->send_empty_command_(TuyaCommandType::MCU_CONF); + this->set_interval("heartbeat", 1000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); +} + +void Tuya::loop() { + while (this->available()) { + uint8_t c; + this->read_byte(&c); + this->handle_char_(c); + } +} + +void Tuya::dump_config() { + ESP_LOGCONFIG(TAG, "Tuya:"); + if ((gpio_status_ != -1) || (gpio_reset_ != -1)) + ESP_LOGCONFIG(TAG, " GPIO MCU configuration not supported!"); + for (auto &info : this->datapoints_) { + if (info.type == TuyaDatapointType::BOOLEAN) + ESP_LOGCONFIG(TAG, " Datapoint %d: switch (value: %s)", info.id, ONOFF(info.value_bool)); + else if (info.type == TuyaDatapointType::INTEGER) + ESP_LOGCONFIG(TAG, " Datapoint %d: int value (value: %d)", info.id, info.value_int); + else if (info.type == TuyaDatapointType::ENUM) + ESP_LOGCONFIG(TAG, " Datapoint %d: enum (value: %d)", info.id, info.value_enum); + else if (info.type == TuyaDatapointType::BITMASK) + ESP_LOGCONFIG(TAG, " Datapoint %d: bitmask (value: %x)", info.id, info.value_bitmask); + else + ESP_LOGCONFIG(TAG, " Datapoint %d: unknown", info.id); + } + if (this->datapoints_.empty()) { + ESP_LOGCONFIG(TAG, " Received no datapoints! Please make sure this is a supported Tuya device."); + } + this->check_uart_settings(9600); +} + +bool Tuya::validate_message_() { + uint32_t at = this->rx_message_.size() - 1; + auto *data = &this->rx_message_[0]; + uint8_t new_byte = data[at]; + + // Byte 0: HEADER1 (always 0x55) + if (at == 0) + return new_byte == 0x55; + // Byte 1: HEADER2 (always 0xAA) + if (at == 1) + return new_byte == 0xAA; + + // Byte 2: VERSION + // no validation for the following fields: + uint8_t version = data[2]; + if (at == 2) + return true; + // Byte 3: COMMAND + uint8_t command = data[3]; + if (at == 3) + return true; + + // Byte 4: LENGTH1 + // Byte 5: LENGTH2 + if (at <= 5) + // no validation for these fields + return true; + + uint16_t length = (uint16_t(data[4]) << 8) | (uint16_t(data[5])); + + // wait until all data is read + if (at - 6 < length) + return true; + + // Byte 6+LEN: CHECKSUM - sum of all bytes (including header) modulo 256 + uint8_t rx_checksum = new_byte; + uint8_t calc_checksum = 0; + for (uint32_t i = 0; i < 6 + length; i++) + calc_checksum += data[i]; + + if (rx_checksum != calc_checksum) { + ESP_LOGW(TAG, "Tuya Received invalid message checksum %02X!=%02X", rx_checksum, calc_checksum); + return false; + } + + // valid message + const uint8_t *message_data = data + 6; + ESP_LOGV(TAG, "Received Tuya: CMD=0x%02X VERSION=%u DATA=[%s]", command, version, + hexencode(message_data, length).c_str()); + this->handle_command_(command, version, message_data, length); + + // return false to reset rx buffer + return false; +} + +void Tuya::handle_char_(uint8_t c) { + this->rx_message_.push_back(c); + if (!this->validate_message_()) { + this->rx_message_.clear(); + } +} + +void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len) { + uint8_t c; + switch ((TuyaCommandType) command) { + case TuyaCommandType::HEARTBEAT: + ESP_LOGV(TAG, "MCU Heartbeat (0x%02X)", buffer[0]); + if (buffer[0] == 0) { + ESP_LOGI(TAG, "MCU restarted"); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + } + break; + case TuyaCommandType::QUERY_PRODUCT: { + // check it is a valid string + bool valid = false; + for (int i = 0; i < len; i++) { + if (buffer[i] == 0x00) { + valid = true; + break; + } + } + if (valid) { + ESP_LOGD(TAG, "Tuya Product Code: %s", reinterpret_cast(buffer)); + } + break; + } + case TuyaCommandType::MCU_CONF: + if (len >= 2) { + gpio_status_ = buffer[0]; + gpio_reset_ = buffer[1]; + } + // set wifi state LED to off or on depending on the MCU firmware + // but it shouldn't be blinking + c = 0x3; + this->send_command_(TuyaCommandType::WIFI_STATE, &c, 1); + this->send_empty_command_(TuyaCommandType::QUERY_STATE); + break; + case TuyaCommandType::WIFI_STATE: + break; + case TuyaCommandType::WIFI_RESET: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_RESET is not handled"); + break; + case TuyaCommandType::WIFI_SELECT: + ESP_LOGE(TAG, "TUYA_CMD_WIFI_SELECT is not handled"); + break; + case TuyaCommandType::SET_DATAPOINT: + break; + case TuyaCommandType::STATE: { + this->handle_datapoint_(buffer, len); + break; + } + case TuyaCommandType::QUERY_STATE: + break; + default: + ESP_LOGE(TAG, "invalid command (%02x) received", command); + } +} + +void Tuya::handle_datapoint_(const uint8_t *buffer, size_t len) { + if (len < 2) + return; + + TuyaDatapoint datapoint{}; + datapoint.id = buffer[0]; + datapoint.type = (TuyaDatapointType) buffer[1]; + datapoint.value_uint = 0; + + size_t data_size = (buffer[2] << 8) + buffer[3]; + const uint8_t *data = buffer + 4; + size_t data_len = len - 4; + if (data_size != data_len) { + ESP_LOGW(TAG, "invalid datapoint update"); + return; + } + + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + if (data_len != 1) + return; + datapoint.value_bool = data[0]; + break; + case TuyaDatapointType::INTEGER: + if (data_len != 4) + return; + datapoint.value_uint = + (uint32_t(data[0]) << 24) | (uint32_t(data[1]) << 16) | (uint32_t(data[2]) << 8) | (uint32_t(data[3]) << 0); + break; + case TuyaDatapointType::ENUM: + if (data_len != 1) + return; + datapoint.value_enum = data[0]; + break; + case TuyaDatapointType::BITMASK: + if (data_len != 2) + return; + datapoint.value_bitmask = (uint16_t(data[0]) << 8) | (uint16_t(data[1]) << 0); + break; + default: + return; + } + ESP_LOGV(TAG, "Datapoint %u update to %u", datapoint.id, datapoint.value_uint); + + // Update internal datapoints + bool found = false; + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + other = datapoint; + found = true; + } + } + if (!found) { + this->datapoints_.push_back(datapoint); + // New datapoint found, reprint dump_config after a delay. + this->set_timeout("datapoint_dump", 100, [this] { this->dump_config(); }); + } + + // Run through listeners + for (auto &listener : this->listeners_) + if (listener.datapoint_id == datapoint.id) + listener.on_datapoint(datapoint); +} + +void Tuya::send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len) { + uint8_t len_hi = len >> 8; + uint8_t len_lo = len >> 0; + this->write_array({0x55, 0xAA, + 0x00, // version + (uint8_t) command, len_hi, len_lo}); + if (len != 0) + this->write_array(buffer, len); + + uint8_t checksum = 0x55 + 0xAA + (uint8_t) command + len_hi + len_lo; + for (int i = 0; i < len; i++) + checksum += buffer[i]; + this->write_byte(checksum); +} + +void Tuya::set_datapoint_value(TuyaDatapoint datapoint) { + std::vector buffer; + ESP_LOGV(TAG, "Datapoint %u set to %u", datapoint.id, datapoint.value_uint); + for (auto &other : this->datapoints_) { + if (other.id == datapoint.id) { + if (other.value_uint == datapoint.value_uint) { + ESP_LOGV(TAG, "Not sending unchanged value"); + return; + } + } + } + buffer.push_back(datapoint.id); + buffer.push_back(static_cast(datapoint.type)); + + std::vector data; + switch (datapoint.type) { + case TuyaDatapointType::BOOLEAN: + data.push_back(datapoint.value_bool); + break; + case TuyaDatapointType::INTEGER: + data.push_back(datapoint.value_uint >> 24); + data.push_back(datapoint.value_uint >> 16); + data.push_back(datapoint.value_uint >> 8); + data.push_back(datapoint.value_uint >> 0); + break; + case TuyaDatapointType::ENUM: + data.push_back(datapoint.value_enum); + break; + case TuyaDatapointType::BITMASK: + data.push_back(datapoint.value_bitmask >> 8); + data.push_back(datapoint.value_bitmask >> 0); + break; + default: + return; + } + + buffer.push_back(data.size() >> 8); + buffer.push_back(data.size() >> 0); + buffer.insert(buffer.end(), data.begin(), data.end()); + this->send_command_(TuyaCommandType::SET_DATAPOINT, buffer.data(), buffer.size()); +} + +void Tuya::register_listener(uint8_t datapoint_id, const std::function &func) { + auto listener = TuyaDatapointListener{ + .datapoint_id = datapoint_id, + .on_datapoint = func, + }; + this->listeners_.push_back(listener); + + // Run through existing datapoints + for (auto &datapoint : this->datapoints_) + if (datapoint.id == datapoint_id) + func(datapoint); +} + +} // namespace tuya +} // namespace esphome diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h new file mode 100644 index 0000000000..6bc6d92da0 --- /dev/null +++ b/esphome/components/tuya/tuya.h @@ -0,0 +1,73 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace tuya { + +enum class TuyaDatapointType : uint8_t { + RAW = 0x00, // variable length + BOOLEAN = 0x01, // 1 byte (0/1) + INTEGER = 0x02, // 4 byte + STRING = 0x03, // variable length + ENUM = 0x04, // 1 byte + BITMASK = 0x05, // 2 bytes +}; + +struct TuyaDatapoint { + uint8_t id; + TuyaDatapointType type; + union { + bool value_bool; + int value_int; + uint32_t value_uint; + uint8_t value_enum; + uint16_t value_bitmask; + }; +}; + +struct TuyaDatapointListener { + uint8_t datapoint_id; + std::function on_datapoint; +}; + +enum class TuyaCommandType : uint8_t { + HEARTBEAT = 0x00, + QUERY_PRODUCT = 0x01, + MCU_CONF = 0x02, + WIFI_STATE = 0x03, + WIFI_RESET = 0x04, + WIFI_SELECT = 0x05, + SET_DATAPOINT = 0x06, + STATE = 0x07, + QUERY_STATE = 0x08, +}; + +class Tuya : public Component, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::HARDWARE; } + void setup() override; + void loop() override; + void dump_config() override; + void register_listener(uint8_t datapoint_id, const std::function &func); + void set_datapoint_value(TuyaDatapoint datapoint); + + protected: + void handle_char_(uint8_t c); + void handle_datapoint_(const uint8_t *buffer, size_t len); + bool validate_message_(); + + void handle_command_(uint8_t command, uint8_t version, const uint8_t *buffer, size_t len); + void send_command_(TuyaCommandType command, const uint8_t *buffer, uint16_t len); + void send_empty_command_(TuyaCommandType command) { this->send_command_(command, nullptr, 0); } + + int gpio_status_ = -1; + int gpio_reset_ = -1; + std::vector listeners_; + std::vector datapoints_; + std::vector rx_message_; +}; + +} // namespace tuya +} // 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..2511cf28b1 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -29,15 +29,18 @@ def validate_rx_pin(value): return value +CONF_STOP_BITS = 'stop_bits' 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, + cv.Optional(CONF_STOP_BITS, default=1): cv.one_of(1, 2, int=True), }).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) @@ -47,6 +50,7 @@ def to_code(config): cg.add(var.set_tx_pin(config[CONF_TX_PIN])) if CONF_RX_PIN in config: cg.add(var.set_rx_pin(config[CONF_RX_PIN])) + cg.add(var.set_stop_bits(config[CONF_STOP_BITS])) # A schema to use for all UART devices, all UART integrations must extend this! diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 56661b8aa7..3284d4cb67 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -2,6 +2,11 @@ #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include "esphome/core/application.h" +#include "esphome/core/defines.h" + +#ifdef USE_LOGGER +#include "esphome/components/logger/logger.h" +#endif namespace esphome { namespace uart { @@ -20,16 +25,15 @@ 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++); } int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->hw_serial_->begin(this->baud_rate_, SERIAL_8N1, rx, tx); + uint32_t config = SERIAL_8N1; + if (this->stop_bits_ == 2) + config = SERIAL_8N2; + this->hw_serial_->begin(this->baud_rate_, config, rx, tx); } void UARTComponent::dump_config() { @@ -41,6 +45,13 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); +#ifdef USE_LOGGER + if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif } void UARTComponent::write_byte(uint8_t data) { @@ -106,21 +117,27 @@ void UARTComponent::setup() { // Use Arduino HardwareSerial UARTs if all used pins match the ones // preconfigured by the platform. For example if RX disabled but TX pin // is 1 we still want to use Serial. + uint32_t mode = UART_NB_BIT_8 | UART_PARITY_NONE; + if (this->stop_bits_ == 1) + mode |= UART_NB_STOP_BIT_1; + else + mode |= UART_NB_STOP_BIT_2; + SerialConfig config = static_cast(mode); if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else if (this->tx_pin_.value_or(15) == 15 && this->rx_pin_.value_or(13) == 13) { this->hw_serial_ = &Serial; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); this->hw_serial_->swap(); } else if (this->tx_pin_.value_or(2) == 2 && this->rx_pin_.value_or(8) == 8) { this->hw_serial_ = &Serial1; - this->hw_serial_->begin(this->baud_rate_); + this->hw_serial_->begin(this->baud_rate_, config); } else { this->sw_serial_ = new ESP8266SoftwareSerial(); int8_t tx = this->tx_pin_.has_value() ? *this->tx_pin_ : -1; int8_t rx = this->rx_pin_.has_value() ? *this->rx_pin_ : -1; - this->sw_serial_->setup(tx, rx, this->baud_rate_); + this->sw_serial_->setup(tx, rx, this->baud_rate_, this->stop_bits_); } } @@ -133,11 +150,19 @@ void UARTComponent::dump_config() { ESP_LOGCONFIG(TAG, " RX Pin: GPIO%d", *this->rx_pin_); } ESP_LOGCONFIG(TAG, " Baud Rate: %u baud", this->baud_rate_); + ESP_LOGCONFIG(TAG, " Stop bits: %u", this->stop_bits_); if (this->hw_serial_ != nullptr) { ESP_LOGCONFIG(TAG, " Using hardware serial interface."); } else { ESP_LOGCONFIG(TAG, " Using software serial"); } + +#ifdef USE_LOGGER + if (this->hw_serial_ == &Serial && logger::global_logger->get_baud_rate() != 0) { + ESP_LOGW(TAG, " You're using the same serial port for logging and the UART component. Please " + "disable logging over the serial port by setting logger->baud_rate to 0."); + } +#endif } void UARTComponent::write_byte(uint8_t data) { @@ -235,7 +260,7 @@ void UARTComponent::flush() { } } -void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate) { +void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits) { this->bit_time_ = F_CPU / baud_rate; if (tx_pin != -1) { auto pin = GPIOPin(tx_pin, OUTPUT); @@ -250,6 +275,7 @@ void ESP8266SoftwareSerial::setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_ra this->rx_buffer_ = new uint8_t[this->rx_buffer_size_]; pin.attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, FALLING); } + this->stop_bits_ = stop_bits; } void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) { uint32_t wait = arg->bit_time_ + arg->bit_time_ / 3 - 500; @@ -266,6 +292,8 @@ void ICACHE_RAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg rec |= arg->read_bit_(&wait, start) << 7; // Stop bit arg->wait_(&wait, start); + if (arg->stop_bits_ == 2) + arg->wait_(&wait, start); arg->rx_buffer_[arg->rx_in_pos_] = rec; arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_; @@ -293,14 +321,16 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) { this->write_bit_(data & (1 << 7), &wait, start); // Stop bit this->write_bit_(true, &wait, start); + if (this->stop_bits_ == 2) + this->wait_(&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 +350,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) @@ -346,5 +378,16 @@ int UARTComponent::peek() { return data; } +void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits) { + if (this->parent_->baud_rate_ != baud_rate) { + ESP_LOGE(TAG, " Invalid baud_rate: Integration requested baud_rate %u but you have %u!", baud_rate, + this->parent_->baud_rate_); + } + if (this->parent_->stop_bits_ != stop_bits) { + ESP_LOGE(TAG, " Invalid stop bits: Integration requested stop_bits %u but you have %u!", stop_bits, + this->parent_->stop_bits_); + } +} + } // namespace uart } // namespace esphome diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 666b8e2fb2..0e92fed0dc 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -10,7 +10,7 @@ namespace uart { #ifdef ARDUINO_ARCH_ESP8266 class ESP8266SoftwareSerial { public: - void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate); + void setup(int8_t tx_pin, int8_t rx_pin, uint32_t baud_rate, uint8_t stop_bits); uint8_t read_byte(); uint8_t peek_byte(); @@ -24,15 +24,16 @@ 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}; + uint8_t stop_bits_; ISRInternalGPIOPin *tx_pin_{nullptr}; ISRInternalGPIOPin *rx_pin_{nullptr}; }; @@ -61,6 +62,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; } @@ -71,9 +73,11 @@ class UARTComponent : public Component, public Stream { void set_tx_pin(uint8_t tx_pin) { this->tx_pin_ = tx_pin; } void set_rx_pin(uint8_t rx_pin) { this->rx_pin_ = rx_pin; } + void set_stop_bits(uint8_t stop_bits) { this->stop_bits_ = stop_bits; } protected: bool check_read_timeout_(size_t len = 1); + friend class UARTDevice; HardwareSerial *hw_serial_{nullptr}; #ifdef ARDUINO_ARCH_ESP8266 @@ -82,6 +86,7 @@ class UARTComponent : public Component, public Stream { optional tx_pin_; optional rx_pin_; uint32_t baud_rate_; + uint8_t stop_bits_; }; #ifdef ARDUINO_ARCH_ESP32 @@ -99,6 +104,9 @@ class UARTDevice : public Stream { void write_array(const uint8_t *data, size_t len) { this->parent_->write_array(data, len); } void write_array(const std::vector &data) { this->parent_->write_array(data); } + template void write_array(const std::array &data) { + this->parent_->write_array(data.data(), data.size()); + } void write_str(const char *str) { this->parent_->write_str(str); } @@ -106,6 +114,13 @@ class UARTDevice : public Stream { bool peek_byte(uint8_t *data) { return this->parent_->peek_byte(data); } bool read_array(uint8_t *data, size_t len) { return this->parent_->read_array(data, len); } + template optional> read_array() { // NOLINT + std::array res; + if (!this->read_array(res.data(), N)) { + return {}; + } + return res; + } int available() override { return this->parent_->available(); } @@ -115,6 +130,9 @@ class UARTDevice : public Stream { int read() override { return this->parent_->read(); } int peek() override { return this->parent_->peek(); } + /// Check that the configuration of the UART bus matches the provided values and otherwise print a warning + void check_uart_settings(uint32_t baud_rate, uint8_t stop_bits = 1); + protected: UARTComponent *parent_{nullptr}; }; 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/uptime/uptime_sensor.cpp b/esphome/components/uptime/uptime_sensor.cpp index f047724768..5d117ab61d 100644 --- a/esphome/components/uptime/uptime_sensor.cpp +++ b/esphome/components/uptime/uptime_sensor.cpp @@ -27,6 +27,7 @@ void UptimeSensor::update() { } std::string UptimeSensor::unique_id() { return get_mac_address() + "-uptime"; } float UptimeSensor::get_setup_priority() const { return setup_priority::HARDWARE; } +void UptimeSensor::dump_config() { LOG_SENSOR("", "Uptime Sensor", this); } } // namespace uptime } // namespace esphome diff --git a/esphome/components/uptime/uptime_sensor.h b/esphome/components/uptime/uptime_sensor.h index 184022503d..dab380d2d9 100644 --- a/esphome/components/uptime/uptime_sensor.h +++ b/esphome/components/uptime/uptime_sensor.h @@ -9,6 +9,7 @@ namespace uptime { class UptimeSensor : public sensor::Sensor, public PollingComponent { public: void update() override; + void dump_config() override; float get_setup_priority() const override; diff --git a/esphome/components/vl53l0x/LICENSE.txt b/esphome/components/vl53l0x/LICENSE.txt new file mode 100644 index 0000000000..fe33583414 --- /dev/null +++ b/esphome/components/vl53l0x/LICENSE.txt @@ -0,0 +1,80 @@ +Most of the code in this integration is based on the VL53L0x library +by Pololu (Pololu Corporation), which in turn is based on the VL53L0X +API from ST. The code has been adapted to work with ESPHome's i2c APIs. +Please see the top-level LICENSE.txt for information about ESPHome's license. +The licenses for Pololu's and ST's software are included below. +Orignally taken from https://github.com/pololu/vl53l0x-arduino (accessed 20th october 2019). + +================================================================= + +Copyright (c) 2017 Pololu Corporation. For more information, see + +https://www.pololu.com/ +https://forum.pololu.com/ + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +================================================================= + +Most of the functionality of this library is based on the VL53L0X +API provided by ST (STSW-IMG005), and some of the explanatory +comments are quoted or paraphrased from the API source code, API +user manual (UM2039), and the VL53L0X datasheet. + +The following applies to source code reproduced or derived from +the API: + +----------------------------------------------------------------- + +Copyright © 2016, STMicroelectronics International N.V. All +rights reserved. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: +* Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following +disclaimer in the documentation and/or other materials provided +with the distribution. +* Neither the name of STMicroelectronics nor the +names of its contributors may be used to endorse or promote +products derived from this software without specific prior +written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND +NON-INFRINGEMENT OF INTELLECTUAL PROPERTY RIGHTS ARE DISCLAIMED. +IN NO EVENT SHALL STMICROELECTRONICS INTERNATIONAL N.V. BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT +OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +----------------------------------------------------------------- diff --git a/esphome/components/vl53l0x/__init__.py b/esphome/components/vl53l0x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/vl53l0x/sensor.py b/esphome/components/vl53l0x/sensor.py new file mode 100644 index 0000000000..6740d53e13 --- /dev/null +++ b/esphome/components/vl53l0x/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL + +DEPENDENCIES = ['i2c'] + +vl53l0x_ns = cg.esphome_ns.namespace('vl53l0x') +VL53L0XSensor = vl53l0x_ns.class_('VL53L0XSensor', sensor.Sensor, cg.PollingComponent, + i2c.I2CDevice) + +CONF_SIGNAL_RATE_LIMIT = 'signal_rate_limit' +CONFIG_SCHEMA = sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 2).extend({ + cv.GenerateID(): cv.declare_id(VL53L0XSensor), + cv.Optional(CONF_SIGNAL_RATE_LIMIT, default=0.25): cv.float_range( + min=0.0, max=512.0, min_included=False, max_included=False) +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x29)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.cpp b/esphome/components/vl53l0x/vl53l0x_sensor.cpp new file mode 100644 index 0000000000..231bed99ac --- /dev/null +++ b/esphome/components/vl53l0x/vl53l0x_sensor.cpp @@ -0,0 +1,249 @@ +#include "vl53l0x_sensor.h" +#include "esphome/core/log.h" + +/* + * Most of the code in this integration is based on the VL53L0x library + * by Pololu (Pololu Corporation), which in turn is based on the VL53L0X + * API from ST. + * + * For more information about licensing, please view the included LICENSE.txt file + * in the vl53l0x integration directory. + */ + +namespace esphome { +namespace vl53l0x { + +static const char *TAG = "vl53l0x"; + +void VL53L0XSensor::dump_config() { + LOG_SENSOR("", "VL53L0X", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} +void VL53L0XSensor::setup() { + reg(0x89) |= 0x01; + reg(0x88) = 0x00; + + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + stop_variable_ = reg(0x91).get(); + + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + reg(0x60) |= 0x12; + + auto rate_value = static_cast(signal_rate_limit_ * 128); + write_byte_16(0x44, rate_value); + + reg(0x01) = 0xFF; + + // getSpadInfo() + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + reg(0xFF) = 0x06; + reg(0x83) |= 0x04; + reg(0xFF) = 0x07; + reg(0x81) = 0x01; + reg(0x80) = 0x01; + reg(0x94) = 0x6B; + reg(0x83) = 0x00; + + while (reg(0x83).get() == 0x00) + yield(); + + reg(0x83) = 0x01; + uint8_t tmp = reg(0x92).get(); + uint8_t spad_count = tmp & 0x7F; + bool spad_type_is_aperture = tmp & 0x80; + + reg(0x81) = 0x00; + reg(0xFF) = 0x06; + reg(0x83) &= ~0x04; + reg(0xFF) = 0x01; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + uint8_t ref_spad_map[6]; + this->read_bytes(0xB0, ref_spad_map, 6); + + reg(0xFF) = 0x01; + reg(0x4F) = 0x00; + reg(0x4E) = 0x2C; + reg(0xFF) = 0x00; + reg(0xB6) = 0xB4; + + uint8_t first_spad_to_enable = spad_type_is_aperture ? 12 : 0; + uint8_t spads_enabled = 0; + for (int i = 0; i < 48; i++) { + uint8_t &val = ref_spad_map[i / 8]; + uint8_t mask = 1 << (i % 8); + + if (i < first_spad_to_enable || spads_enabled == spad_count) + val &= ~mask; + else if (val & mask) + spads_enabled += 1; + } + + this->write_bytes(0xB0, ref_spad_map, 6); + + reg(0xFF) = 0x01; + reg(0x00) = 0x00; + reg(0xFF) = 0x00; + reg(0x09) = 0x00; + reg(0x10) = 0x00; + reg(0x11) = 0x00; + reg(0x24) = 0x01; + reg(0x25) = 0xFF; + reg(0x75) = 0x00; + reg(0xFF) = 0x01; + reg(0x4E) = 0x2C; + reg(0x48) = 0x00; + reg(0x30) = 0x20; + reg(0xFF) = 0x00; + reg(0x30) = 0x09; + reg(0x54) = 0x00; + reg(0x31) = 0x04; + reg(0x32) = 0x03; + reg(0x40) = 0x83; + reg(0x46) = 0x25; + reg(0x60) = 0x00; + reg(0x27) = 0x00; + reg(0x50) = 0x06; + reg(0x51) = 0x00; + reg(0x52) = 0x96; + reg(0x56) = 0x08; + reg(0x57) = 0x30; + reg(0x61) = 0x00; + reg(0x62) = 0x00; + reg(0x64) = 0x00; + reg(0x65) = 0x00; + reg(0x66) = 0xA0; + reg(0xFF) = 0x01; + reg(0x22) = 0x32; + reg(0x47) = 0x14; + reg(0x49) = 0xFF; + reg(0x4A) = 0x00; + reg(0xFF) = 0x00; + reg(0x7A) = 0x0A; + reg(0x7B) = 0x00; + reg(0x78) = 0x21; + reg(0xFF) = 0x01; + reg(0x23) = 0x34; + reg(0x42) = 0x00; + reg(0x44) = 0xFF; + reg(0x45) = 0x26; + reg(0x46) = 0x05; + reg(0x40) = 0x40; + reg(0x0E) = 0x06; + reg(0x20) = 0x1A; + reg(0x43) = 0x40; + reg(0xFF) = 0x00; + reg(0x34) = 0x03; + reg(0x35) = 0x44; + reg(0xFF) = 0x01; + reg(0x31) = 0x04; + reg(0x4B) = 0x09; + reg(0x4C) = 0x05; + reg(0x4D) = 0x04; + reg(0xFF) = 0x00; + reg(0x44) = 0x00; + reg(0x45) = 0x20; + reg(0x47) = 0x08; + reg(0x48) = 0x28; + reg(0x67) = 0x00; + reg(0x70) = 0x04; + reg(0x71) = 0x01; + reg(0x72) = 0xFE; + reg(0x76) = 0x00; + reg(0x77) = 0x00; + reg(0xFF) = 0x01; + reg(0x0D) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x01; + reg(0x01) = 0xF8; + reg(0xFF) = 0x01; + reg(0x8E) = 0x01; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + reg(0x0A) = 0x04; + reg(0x84) &= ~0x10; + reg(0x0B) = 0x01; + + measurement_timing_budget_us_ = get_measurement_timing_budget_(); + reg(0x01) = 0xE8; + set_measurement_timing_budget_(measurement_timing_budget_us_); + reg(0x01) = 0x01; + + if (!perform_single_ref_calibration_(0x40)) { + ESP_LOGW(TAG, "1st reference calibration failed!"); + this->mark_failed(); + return; + } + reg(0x01) = 0x02; + if (!perform_single_ref_calibration_(0x00)) { + ESP_LOGW(TAG, "2nd reference calibration failed!"); + this->mark_failed(); + return; + } + reg(0x01) = 0xE8; +} +void VL53L0XSensor::update() { + if (this->initiated_read_ || this->waiting_for_interrupt_) { + this->publish_state(NAN); + this->status_set_warning(); + } + + // initiate single shot measurement + reg(0x80) = 0x01; + reg(0xFF) = 0x01; + + reg(0x00) = 0x00; + reg(0x91) = stop_variable_; + reg(0x00) = 0x01; + reg(0xFF) = 0x00; + reg(0x80) = 0x00; + + reg(0x00) = 0x01; + this->waiting_for_interrupt_ = false; + this->initiated_read_ = true; + // wait for timeout +} +void VL53L0XSensor::loop() { + if (this->initiated_read_) { + if (reg(0x00).get() & 0x01) { + // waiting + } else { + // done + // wait until reg(0x13) & 0x07 is set + this->initiated_read_ = false; + this->waiting_for_interrupt_ = true; + } + } + if (this->waiting_for_interrupt_) { + if (reg(0x13).get() & 0x07) { + uint16_t range_mm; + this->read_byte_16(0x14 + 10, &range_mm); + reg(0x0B) = 0x01; + this->waiting_for_interrupt_ = false; + + if (range_mm >= 8190) { + ESP_LOGW(TAG, "'%s' - Distance is out of range, please move the target closer", this->name_.c_str()); + this->publish_state(NAN); + return; + } + + float range_m = range_mm / 1e3f; + ESP_LOGD(TAG, "'%s' - Got distance %.3f m", this->name_.c_str(), range_m); + this->publish_state(range_m); + } + } +} + +} // namespace vl53l0x +} // namespace esphome diff --git a/esphome/components/vl53l0x/vl53l0x_sensor.h b/esphome/components/vl53l0x/vl53l0x_sensor.h new file mode 100644 index 0000000000..1825383cee --- /dev/null +++ b/esphome/components/vl53l0x/vl53l0x_sensor.h @@ -0,0 +1,257 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace vl53l0x { + +struct SequenceStepEnables { + bool tcc, msrc, dss, pre_range, final_range; +}; + +struct SequenceStepTimeouts { + uint16_t pre_range_vcsel_period_pclks, final_range_vcsel_period_pclks; + + uint16_t msrc_dss_tcc_mclks, pre_range_mclks, final_range_mclks; + uint32_t msrc_dss_tcc_us, pre_range_us, final_range_us; +}; + +class VL53L0XSensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; + + void loop() override; + + void set_signal_rate_limit(float signal_rate_limit) { signal_rate_limit_ = signal_rate_limit; } + + protected: + uint32_t get_measurement_timing_budget_() { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1910; + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + // "Start and end overhead times always present" + uint32_t budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) + budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + + if (enables.dss) + budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + else if (enables.msrc) + budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + + if (enables.pre_range) + budget_us += (timeouts.pre_range_us + pre_range_overhead); + + if (enables.final_range) + budget_us += (timeouts.final_range_us + final_range_overhead); + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + return budget_us; + } + + bool set_measurement_timing_budget_(uint32_t budget_us) { + SequenceStepEnables enables{}; + SequenceStepTimeouts timeouts{}; + + uint16_t start_overhead = 1320; // note that this is different than the value in get_ + uint16_t end_overhead = 960; + uint16_t msrc_overhead = 660; + uint16_t tcc_overhead = 590; + uint16_t dss_overhead = 690; + uint16_t pre_range_overhead = 660; + uint16_t final_range_overhead = 550; + + uint32_t min_timing_budget = 20000; + + if (budget_us < min_timing_budget) { + return false; + } + + uint32_t used_budget_us = start_overhead + end_overhead; + + get_sequence_step_enables_(&enables); + get_sequence_step_timeouts_(&enables, &timeouts); + + if (enables.tcc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + tcc_overhead); + } + + if (enables.dss) { + used_budget_us += 2 * (timeouts.msrc_dss_tcc_us + dss_overhead); + } else if (enables.msrc) { + used_budget_us += (timeouts.msrc_dss_tcc_us + msrc_overhead); + } + + if (enables.pre_range) { + used_budget_us += (timeouts.pre_range_us + pre_range_overhead); + } + + if (enables.final_range) { + used_budget_us += final_range_overhead; + + // "Note that the final range timeout is determined by the timing + // budget and the sum of all other timeouts within the sequence. + // If there is no room for the final range timeout, then an error + // will be set. Otherwise the remaining time will be applied to + // the final range." + + if (used_budget_us > budget_us) { + // "Requested timeout too big." + return false; + } + + uint32_t final_range_timeout_us = budget_us - used_budget_us; + + // set_sequence_step_timeout() begin + // (SequenceStepId == VL53L0X_SEQUENCESTEP_FINAL_RANGE) + + // "For the final range timeout, the pre-range timeout + // must be added. To do this both final and pre-range + // timeouts must be expressed in macro periods MClks + // because they have different vcsel periods." + + uint16_t final_range_timeout_mclks = + timeout_microseconds_to_mclks_(final_range_timeout_us, timeouts.final_range_vcsel_period_pclks); + + if (enables.pre_range) { + final_range_timeout_mclks += timeouts.pre_range_mclks; + } + + write_byte_16(0x71, encode_timeout_(final_range_timeout_mclks)); + + // set_sequence_step_timeout() end + + measurement_timing_budget_us_ = budget_us; // store for internal reuse + } + return true; + } + + void get_sequence_step_enables_(SequenceStepEnables *enables) { + uint8_t sequence_config = reg(0x01).get(); + enables->tcc = (sequence_config >> 4) & 0x1; + enables->dss = (sequence_config >> 3) & 0x1; + enables->msrc = (sequence_config >> 2) & 0x1; + enables->pre_range = (sequence_config >> 6) & 0x1; + enables->final_range = (sequence_config >> 7) & 0x1; + } + + enum VcselPeriodType { VCSEL_PERIOD_PRE_RANGE, VCSEL_PERIOD_FINAL_RANGE }; + + void get_sequence_step_timeouts_(SequenceStepEnables const *enables, SequenceStepTimeouts *timeouts) { + timeouts->pre_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_PRE_RANGE); + + timeouts->msrc_dss_tcc_mclks = reg(0x46).get() + 1; + timeouts->msrc_dss_tcc_us = + timeout_mclks_to_microseconds_(timeouts->msrc_dss_tcc_mclks, timeouts->pre_range_vcsel_period_pclks); + + uint16_t value; + read_byte_16(0x51, &value); + timeouts->pre_range_mclks = decode_timeout_(value); + timeouts->pre_range_us = + timeout_mclks_to_microseconds_(timeouts->pre_range_mclks, timeouts->pre_range_vcsel_period_pclks); + + timeouts->final_range_vcsel_period_pclks = get_vcsel_pulse_period_(VCSEL_PERIOD_FINAL_RANGE); + + read_byte_16(0x71, &value); + timeouts->final_range_mclks = decode_timeout_(value); + + if (enables->pre_range) { + timeouts->final_range_mclks -= timeouts->pre_range_mclks; + } + + timeouts->final_range_us = + timeout_mclks_to_microseconds_(timeouts->final_range_mclks, timeouts->final_range_vcsel_period_pclks); + } + + uint8_t get_vcsel_pulse_period_(VcselPeriodType type) { + uint8_t vcsel; + if (type == VCSEL_PERIOD_PRE_RANGE) + vcsel = reg(0x50).get(); + else if (type == VCSEL_PERIOD_FINAL_RANGE) + vcsel = reg(0x70).get(); + else + return 255; + + return (vcsel + 1) << 1; + } + + uint32_t get_macro_period_(uint8_t vcsel_period_pclks) { + return ((2304UL * vcsel_period_pclks * 1655UL) + 500UL) / 1000UL; + } + + uint32_t timeout_mclks_to_microseconds_(uint16_t timeout_period_mclks, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return ((timeout_period_mclks * macro_period_ns) + (macro_period_ns / 2)) / 1000; + } + uint32_t timeout_microseconds_to_mclks_(uint32_t timeout_period_us, uint8_t vcsel_period_pclks) { + uint32_t macro_period_ns = get_macro_period_(vcsel_period_pclks); + return (((timeout_period_us * 1000) + (macro_period_ns / 2)) / macro_period_ns); + } + + uint16_t decode_timeout_(uint16_t reg_val) { + // format: "(LSByte * 2^MSByte) + 1" + uint8_t msb = (reg_val >> 8) & 0xFF; + uint8_t lsb = (reg_val >> 0) & 0xFF; + return (uint16_t(lsb) << msb) + 1; + } + uint16_t encode_timeout_(uint16_t timeout_mclks) { + // format: "(LSByte * 2^MSByte) + 1" + uint32_t ls_byte = 0; + uint16_t ms_byte = 0; + + if (timeout_mclks <= 0) + return 0; + + ls_byte = timeout_mclks - 1; + + while ((ls_byte & 0xFFFFFF00) > 0) { + ls_byte >>= 1; + ms_byte++; + } + + return (ms_byte << 8) | (ls_byte & 0xFF); + } + + bool perform_single_ref_calibration_(uint8_t vhv_init_byte) { + reg(0x00) = 0x01 | vhv_init_byte; // VL53L0X_REG_SYSRANGE_MODE_START_STOP + + uint32_t start = millis(); + while ((reg(0x13).get() & 0x07) == 0) { + if (millis() - start > 1000) + return false; + yield(); + } + + reg(0x0B) = 0x01; + reg(0x00) = 0x00; + + return true; + } + + float signal_rate_limit_; + uint32_t measurement_timing_budget_us_; + bool initiated_read_{false}; + bool waiting_for_interrupt_{false}; + uint8_t stop_variable_; +}; + +} // namespace vl53l0x +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index cb7de80918..a8ffbcc538 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -21,6 +21,7 @@ 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), 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..eff6b895a9 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); @@ -43,16 +45,14 @@ class WaveshareEPaper : public PollingComponent, public spi::SPIDevice, public d void reset_() { if (this->reset_pin_ != nullptr) { this->reset_pin_->digital_write(false); - delay(200); + delay(200); // NOLINT this->reset_pin_->digital_write(true); - delay(200); + delay(200); // NOLINT } } 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; @@ -143,7 +144,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { // COMMAND PANEL SETTING this->command(0x00); - delay(100); + delay(100); // NOLINT // COMMAND POWER SETTING this->command(0x01); @@ -152,7 +153,7 @@ class WaveshareEPaper4P2In : public WaveshareEPaper { this->data(0x00); this->data(0x00); this->data(0x00); - delay(100); + delay(100); // NOLINT // COMMAND POWER OFF this->command(0x02); @@ -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..4fdbbbce7d 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 %}
diff --git a/esphome/espota2.py b/esphome/espota2.py index 786f49dbdf..40417b9ab2 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -127,7 +127,8 @@ def check_error(data, expect): "correct 'board' option (esp01_1m always works) and then flash over USB.") if dat == RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG: raise OTAError("Error: ESP does not have the requested flash size (wrong board). Please " - "choose the correct 'board' option (esp01_1m always works) and try again.") + "choose the correct 'board' option (esp01_1m always works) and try " + "uploading again.") if dat == RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE: raise OTAError("Error: ESP does not have enough space to store OTA file. Please try " "flashing a minimal firmware (remove everything except ota)") @@ -299,3 +300,4 @@ def run_ota(remote_host, remote_port, password, filename): return run_ota_impl_(remote_host, remote_port, password, filename) except OTAError as err: _LOGGER.error(err) + return 1 diff --git a/esphome/helpers.py b/esphome/helpers.py index 30a06d842f..6fd1fa2ad7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -1,10 +1,11 @@ from __future__ import print_function import codecs + import logging import os -from esphome.py_compat import char_to_byte, text_type +from esphome.py_compat import char_to_byte, text_type, IS_PY2, encode_text _LOGGER = logging.getLogger(__name__) @@ -79,15 +80,15 @@ def run_system_command(*args): def mkdir_p(path): - import errno - try: os.makedirs(path) - except OSError as exc: - if exc.errno == errno.EEXIST and os.path.isdir(path): + except OSError as err: + import errno + if err.errno == errno.EEXIST and os.path.isdir(path): pass else: - raise + from esphome.core import EsphomeError + raise EsphomeError(u"Error creating directories {}: {}".format(path, err)) def is_ip_address(host): @@ -151,17 +152,6 @@ def is_hassio(): return get_bool_env('ESPHOME_IS_HASSIO') -def copy_file_if_changed(src, dst): - src_text = read_file(src) - if os.path.isfile(dst): - dst_text = read_file(dst) - else: - dst_text = None - if src_text == dst_text: - return - write_file(dst, src_text) - - def walk_files(path): for root, _, files in os.walk(path): for name in files: @@ -172,28 +162,99 @@ def read_file(path): try: with codecs.open(path, 'r', encoding='utf-8') as f_handle: return f_handle.read() - except OSError: + except OSError as err: from esphome.core import EsphomeError - raise EsphomeError(u"Could not read file at {}".format(path)) + raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + except UnicodeDecodeError as err: + from esphome.core import EsphomeError + raise EsphomeError(u"Error reading file {}: {}".format(path, err)) + + +def _write_file(path, text): + import tempfile + directory = os.path.dirname(path) + mkdir_p(directory) + + tmp_path = None + data = encode_text(text) + try: + with tempfile.NamedTemporaryFile(mode="wb", dir=directory, delete=False) as f_handle: + tmp_path = f_handle.name + f_handle.write(data) + # Newer tempfile implementations create the file with mode 0o600 + os.chmod(tmp_path, 0o644) + if IS_PY2: + if os.path.exists(path): + os.remove(path) + os.rename(tmp_path, path) + else: + # If destination exists, will be overwritten + os.replace(tmp_path, path) + finally: + if tmp_path is not None and os.path.exists(tmp_path): + try: + os.remove(tmp_path) + except OSError as err: + _LOGGER.error("Write file cleanup failed: %s", err) def write_file(path, text): try: - mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w+', encoding='utf-8') as f_handle: - f_handle.write(text) + _write_file(path, text) except OSError: from esphome.core import EsphomeError raise EsphomeError(u"Could not write file at {}".format(path)) -def write_file_if_changed(text, dst): +def write_file_if_changed(path, text): src_content = None - if os.path.isfile(dst): - src_content = read_file(dst) + if os.path.isfile(path): + src_content = read_file(path) if src_content != text: - write_file(dst, text) + write_file(path, text) + + +def copy_file_if_changed(src, dst): + import shutil + if file_compare(src, dst): + return + mkdir_p(os.path.dirname(dst)) + try: + shutil.copy(src, dst) + except OSError as err: + from esphome.core import EsphomeError + raise EsphomeError(u"Error copying file {} to {}: {}".format(src, dst, err)) def list_starts_with(list_, sub): return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub)) + + +def file_compare(path1, path2): + """Return True if the files path1 and path2 have the same contents.""" + import stat + + try: + stat1, stat2 = os.stat(path1), os.stat(path2) + except OSError: + # File doesn't exist or another error -> not equal + return False + + if stat.S_IFMT(stat1.st_mode) != stat.S_IFREG or stat.S_IFMT(stat2.st_mode) != stat.S_IFREG: + # At least one of them is not a regular file (or does not exist) + return False + if stat1.st_size != stat2.st_size: + # Different sizes + return False + + bufsize = 8*1024 + # Read files in blocks until a mismatch is found + with open(path1, 'rb') as fh1, open(path2, 'rb') as fh2: + while True: + blob1, blob2 = fh1.read(bufsize), fh2.read(bufsize) + if blob1 != blob2: + # Different content + return False + if not blob1: + # Reached end + return True diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 0e00459944..e89a6d9578 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -15,6 +15,7 @@ from esphome.const import CONF_BROKER, CONF_DISCOVERY_PREFIX, CONF_ESPHOME, \ CONF_TOPIC, CONF_TOPIC_PREFIX, CONF_USERNAME from esphome.core import CORE, EsphomeError from esphome.helpers import color +from esphome.py_compat import decode_text from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -22,6 +23,7 @@ _LOGGER = logging.getLogger(__name__) def initialize(config, subscriptions, on_message, username, password, client_id): def on_connect(client, userdata, flags, return_code): + _LOGGER.info("Connected to MQTT broker!") for topic in subscriptions: client.subscribe(topic) @@ -94,7 +96,8 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): def on_message(client, userdata, msg): time_ = datetime.now().time().strftime(u'[%H:%M:%S]') - message = time_ + msg.payload + payload = decode_text(msg.payload) + message = time_ + payload safe_print(message) return initialize(config, [topic], on_message, username, password, client_id) diff --git a/esphome/pins.py b/esphome/pins.py index e96dfe2e39..42c8548da4 100644 --- a/esphome/pins.py +++ b/esphome/pins.py @@ -36,6 +36,7 @@ ESP8266_BOARD_PINS = { 'gen4iod': {}, 'heltec_wifi_kit_8': 'd1_mini', 'huzzah': {'LED': 0}, + 'inventone': {}, 'modwifi': {}, 'nodemcu': {'D0': 16, 'D1': 5, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 3, 'D10': 1, 'LED': 16}, @@ -84,6 +85,7 @@ ESP8266_FLASH_SIZES = { 'gen4iod': FLASH_SIZE_512_KB, 'heltec_wifi_kit_8': FLASH_SIZE_4_MB, 'huzzah': FLASH_SIZE_4_MB, + 'inventone': FLASH_SIZE_4_MB, 'modwifi': FLASH_SIZE_2_MB, 'nodemcu': FLASH_SIZE_4_MB, 'nodemcuv2': FLASH_SIZE_4_MB, @@ -118,113 +120,137 @@ ESP32_BASE_PINS = { } ESP32_BOARD_PINS = { - 'alksesp32': {'D0': 40, 'D1': 41, 'D2': 15, 'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17, - 'D8': 5, 'D9': 18, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'A0': 32, 'A1': 33, - 'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15, 'L_R': 22, 'L_G': 17, - 'L_Y': 23, 'L_B': 5, 'L_RGB_R': 4, 'L_RGB_G': 21, 'L_RGB_B': 16, 'SW1': 15, - 'SW2': 2, 'SW3': 0, 'POT1': 32, 'POT2': 33, 'PIEZO1': 19, 'PIEZO2': 18, - 'PHOTO': 25, 'DHT_PIN': 26, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19, 'S5': 21, - 'SDA': 27, 'SCL': 14, 'SS': 19, 'MOSI': 21, 'MISO': 22, 'SCK': 23}, - 'bpi-bit': {'BUZZER': 25, 'BUTTON_A': 35, 'BUTTON_B': 27, 'RGB_LED': 4, 'LIGHT_SENSOR1': 36, - 'LIGHT_SENSOR2': 39, 'TEMPERATURE_SENSOR': 34, 'MPU9250_INT': 0, 'P0': 25, 'P1': 32, - 'P2': 33, 'P3': 13, 'P4': 15, 'P5': 35, 'P6': 12, 'P7': 14, 'P8': 16, 'P9': 17, - 'P10': 26, 'P11': 27, 'P12': 2, 'P13': 18, 'P14': 19, 'P15': 23, 'P16': 5, - 'P19': 22, 'P20': 21, 'DAC1': 26}, - 'd-duino-32': {'SDA': 5, 'SCL': 4, 'SS ': 15, 'MOSI ': 13, 'MISO ': 12, 'SCK ': 14, 'D1': 5, - 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 3, - 'D10': 1}, + 'alksesp32': {'A0': 32, 'A1': 33, 'A2': 25, 'A3': 26, 'A4': 27, 'A5': 14, 'A6': 12, 'A7': 15, + 'D0': 40, 'D1': 41, 'D10': 19, 'D11': 21, 'D12': 22, 'D13': 23, 'D2': 15, + 'D3': 2, 'D4': 0, 'D5': 4, 'D6': 16, 'D7': 17, 'D8': 5, 'D9': 18, 'DHT_PIN': 26, + 'LED': 23, 'L_B': 5, 'L_G': 17, 'L_R': 22, 'L_RGB_B': 16, 'L_RGB_G': 21, + 'L_RGB_R': 4, 'L_Y': 23, 'MISO': 22, 'MOSI': 21, 'PHOTO': 25, 'PIEZO1': 19, + 'PIEZO2': 18, 'POT1': 32, 'POT2': 33, 'S1': 4, 'S2': 16, 'S3': 18, 'S4': 19, + 'S5': 21, 'SCK': 23, 'SCL': 14, 'SDA': 27, 'SS': 19, 'SW1': 15, 'SW2': 2, + 'SW3': 0}, + 'bpi-bit': {'BUTTON_A': 35, 'BUTTON_B': 27, 'BUZZER': 25, 'LIGHT_SENSOR1': 36, + 'LIGHT_SENSOR2': 39, 'MPU9250_INT': 0, 'P0': 25, 'P1': 32, 'P10': 26, 'P11': 27, + 'P12': 2, 'P13': 18, 'P14': 19, 'P15': 23, 'P16': 5, 'P19': 22, 'P2': 33, + 'P20': 21, 'P3': 13, 'P4': 15, 'P5': 35, 'P6': 12, 'P7': 14, 'P8': 16, 'P9': 17, + 'RGB_LED': 4, 'TEMPERATURE_SENSOR': 34}, + 'd-duino-32': {'D1': 5, 'D10': 1, 'D2': 4, 'D3': 0, 'D4': 2, 'D5': 14, 'D6': 12, 'D7': 13, + 'D8': 15, 'D9': 3, 'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SCL': 4, 'SDA': 5, + 'SS': 15}, 'esp-wrover-kit': {}, - 'esp32-evb': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'SS': 17, 'MOSI': 2, 'MISO': 15, 'SCK': 14}, - 'esp32-gateway': {'LED': 33, 'BUTTON': 34, 'SCL': 16, 'SDA': 17}, - 'esp32-poe': {'BUTTON': 34, 'SDA': 13, 'SCL': 16, 'MOSI': 2, 'MISO': 15, 'SCK': 14}, - 'esp320': {'LED': 5, 'SDA': 2, 'SCL': 14, 'SS': 15, 'MOSI': 13, 'MISO': 12, 'SCK': 14}, + 'esp32-devkitlipo': {}, + 'esp32-evb': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13, 'SS': 17}, + 'esp32-gateway': {'BUTTON': 34, 'LED': 33, 'SCL': 16, 'SDA': 32}, + 'esp32-poe-iso': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13}, + 'esp32-poe': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13}, + 'esp32-pro': {'BUTTON': 34, 'MISO': 15, 'MOSI': 2, 'SCK': 14, 'SCL': 16, 'SDA': 13, 'SS': 17}, + 'esp320': {'LED': 5, 'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SCL': 14, 'SDA': 2, 'SS': 15}, + 'esp32cam': {}, 'esp32dev': {}, 'esp32doit-devkit-v1': {'LED': 2}, - 'esp32thing': {'LED': 5, 'BUTTON': 0, 'SS': 2}, + 'esp32thing': {'BUTTON': 0, 'LED': 5, 'SS': 2}, 'esp32vn-iot-uno': {}, - 'espea32': {'LED': 5, 'BUTTON': 0}, + 'espea32': {'BUTTON': 0, 'LED': 5}, 'espectro32': {'LED': 15, 'SD_SS': 33}, - 'espino32': {'LED': 16, 'BUTTON': 0}, - 'featheresp32': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5, - 'A0': 26, 'A1': 25, 'A2': 34, 'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15, - 'A9': 33, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35}, + 'espino32': {'BUTTON': 0, 'LED': 16}, + 'featheresp32': {'A0': 26, 'A1': 25, 'A10': 27, 'A11': 12, 'A12': 13, 'A13': 35, 'A2': 34, + 'A4': 36, 'A5': 4, 'A6': 14, 'A7': 32, 'A8': 15, 'A9': 33, 'Ax': 2, + 'LED': 13, 'MOSI': 18, 'RX': 16, 'SCK': 5, 'SDA': 23, 'SS': 33, 'TX': 17}, 'firebeetle32': {'LED': 2}, - 'fm-devkit': {'LED': 5, 'SW1': 4, 'SW2': 18, 'SW3': 19, 'SW4': 21, 'I2S_MCLK': 2, - 'I2S_LRCLK': 25, 'I2S_SCLK': 26, 'I2S_DOUT': 22, 'D0': 34, 'D1': 35, 'D2': 32, - 'D3': 33, 'D4': 27, 'D5': 14, 'D6': 12, 'D7': 13, 'D8': 15, 'D9': 23, 'D10': 0, - 'SDA': 16, 'SCL': 17}, - 'heltec_wifi_kit_32': {'LED': 25, 'BUTTON': 0, 'A1': 37, 'A2': 38}, - 'heltec_wifi_lora_32': {'LED': 25, 'BUTTON': 0, 'SDA': 4, 'SCL': 15, 'SS': 18, 'MOSI': 27, - 'SCK': 5, 'A1': 37, 'A2': 38, 'OLED_SCL': 15, 'OLED_SDA': 4, - 'OLED_RST': 16, 'LORA_SCK': 5, 'LORA_MOSI': 27, 'LORA_MISO': 19, - 'LORA_CS': 18, 'LORA_RST': 14, 'LORA_IRQ': 26}, - 'hornbill32dev': {'LED': 13, 'BUTTON': 0}, + 'fm-devkit': {'D0': 34, 'D1': 35, 'D10': 0, 'D2': 32, 'D3': 33, 'D4': 27, 'D5': 14, 'D6': 12, + 'D7': 13, 'D8': 15, 'D9': 23, 'I2S_DOUT': 22, 'I2S_LRCLK': 25, 'I2S_MCLK': 2, + 'I2S_SCLK': 26, 'LED': 5, 'SCL': 17, 'SDA': 16, 'SW1': 4, 'SW2': 18, 'SW3': 19, + 'SW4': 21}, + 'frogboard': {}, + 'heltec_wifi_kit_32': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 25, 'RST_OLED': 16, + 'SCL_OLED': 15, 'SDA_OLED': 4, 'Vext': 21}, + 'heltec_wifi_lora_32': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 33, 'DIO2': 32, 'LED': 25, + 'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, 'SCL_OLED': 15, + 'SDA_OLED': 4, 'SS': 18, 'Vext': 21}, + 'heltec_wifi_lora_32_V2': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 35, 'DIO2': 34, 'LED': 25, + 'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, + 'SCL_OLED': 15, 'SDA_OLED': 4, 'SS': 18, 'Vext': 21}, + 'heltec_wireless_stick': {'BUTTON': 0, 'DIO0': 26, 'DIO1': 35, 'DIO2': 34, 'LED': 25, + 'MOSI': 27, 'RST_LoRa': 14, 'RST_OLED': 16, 'SCK': 5, + 'SCL_OLED': 15, 'SDA_OLED': 4, 'SS': 18, 'Vext': 21}, + 'hornbill32dev': {'BUTTON': 0, 'LED': 13}, 'hornbill32minima': {'SS': 2}, - 'intorobot': {'LED': 4, 'LED_RED': 27, 'LED_GREEN': 21, 'LED_BLUE': 22, - 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, 'A2': 35, - 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, 'D0': 19, - 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, 'T1': 23, - 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'intorobot': {'A1': 39, 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, + 'A9': 2, 'BUTTON': 0, 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, + 'D6': 4, 'LED': 4, 'MISO': 17, 'MOSI': 16, 'RGB_B_BUILTIN': 22, + 'RGB_G_BUILTIN': 21, 'RGB_R_BUILTIN': 27, 'SCL': 19, 'SDA': 23, 'T0': 19, + 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'iotaap_magnolia': {}, + 'iotbusio': {}, + 'iotbusproteus': {}, 'lolin32': {'LED': 5}, - 'lolin_d32': {'LED': 5, 'VBAT': 35}, - 'lolin_d32_pro': {'LED': 5, 'VBAT': 35, 'TF_CS': 4, 'TS_CS': 12, 'TFT_CS': 14, 'TFT_LED': 32, - 'TFT_RST': 33, 'TFT_DC': 27}, - 'lopy': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 17, 'LORA_RST': 18, - 'LORA_IRQ': 23, 'LED': 0, 'ANT_SELECT': 16, 'SDA': 12, 'SCL': 13, 'SS': 17, - 'MOSI': 22, 'MISO': 37, 'SCK': 13, 'A1': 37, 'A2': 38}, - 'lopy4': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_IRQ': 23, - 'LED': 0, 'ANT_SELECT': 21, 'SDA': 12, 'SCL': 13, 'SS': 18, 'MOSI': 22, 'MISO': 37, - 'SCK': 13, 'A1': 37, 'A2': 38}, - 'm5stack-core-esp32': {'TXD2': 17, 'RXD2': 16, 'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, - 'G16': 16, 'G21': 21, 'G2': 2, 'G12': 12, 'G15': 15, 'G35': 35, - 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, 'G17': 17, 'G22': 22, 'G5': 5, - 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, 'ADC2': 36}, - 'm5stack-fire': {'G23': 23, 'G19': 19, 'G18': 18, 'G3': 3, 'G16': 16, 'G21': 21, 'G2': 2, - 'G12': 12, 'G15': 15, 'G35': 35, 'G36': 36, 'G25': 25, 'G26': 26, 'G1': 1, - 'G17': 17, 'G22': 22, 'G5': 5, 'G13': 13, 'G0': 0, 'G34': 34, 'ADC1': 35, - 'ADC2': 36}, + 'lolin_d32': {'LED': 5, '_VBAT': 35}, + 'lolin_d32_pro': {'LED': 5, '_VBAT': 35}, + 'lopy': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13, + 'SDA': 12, 'SS': 17}, + 'lopy4': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13, + 'SDA': 12, 'SS': 18}, + 'm5stack-core-esp32': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, + 'G15': 15, 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, + 'G21': 21, 'G22': 22, 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, + 'G34': 34, 'G35': 35, 'G36': 36, 'G5': 5, 'RXD2': 16, 'TXD2': 17}, + 'm5stack-fire': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, 'G15': 15, + 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, 'G21': 21, 'G22': 22, + 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, 'G34': 34, 'G35': 35, 'G36': 36, + 'G5': 5}, + 'm5stack-grey': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G1': 1, 'G12': 12, 'G13': 13, 'G15': 15, + 'G16': 16, 'G17': 17, 'G18': 18, 'G19': 19, 'G2': 2, 'G21': 21, 'G22': 22, + 'G23': 23, 'G25': 25, 'G26': 26, 'G3': 3, 'G34': 34, 'G35': 35, 'G36': 36, + 'G5': 5, 'RXD2': 16, 'TXD2': 17}, + 'm5stick-c': {'ADC1': 35, 'ADC2': 36, 'G0': 0, 'G10': 10, 'G26': 26, 'G32': 32, 'G33': 33, + 'G36': 36, 'G37': 37, 'G39': 39, 'G9': 9, 'MISO': 36, 'MOSI': 15, 'SCK': 13, + 'SCL': 33, 'SDA': 32}, + 'magicbit': {'BLUE_LED': 17, 'BUZZER': 25, 'GREEN_LED': 16, 'LDR': 36, 'LED': 16, + 'LEFT_BUTTON': 35, 'MOTOR1A': 27, 'MOTOR1B': 18, 'MOTOR2A': 16, 'MOTOR2B': 17, + 'POT': 39, 'RED_LED': 27, 'RIGHT_PUTTON': 34, 'YELLOW_LED': 18}, 'mhetesp32devkit': {'LED': 2}, 'mhetesp32minikit': {'LED': 2}, - 'microduino-core-esp32': {'SDA': 22, 'SCL': 21, 'SDA1': 12, 'SCL1': 13, 'A0': 12, 'A1': 13, - 'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'A10': 25, - 'A11': 26, 'A12': 27, 'A13': 14, 'D0': 3, 'D1': 1, 'D2': 16, 'D3': 17, - 'D4': 32, 'D5': 33, 'D6': 25, 'D7': 26, 'D8': 27, 'D9': 14, 'D10': 5, - 'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12, 'D15': 13, 'D16': 15, - 'D17': 4, 'D18': 22, 'D19': 21, 'D20': 38, 'D21': 37}, - 'nano32': {'LED': 16, 'BUTTON': 0}, - 'nina_w10': {'LED_GREEN': 33, 'LED_RED': 23, 'LED_BLUE': 21, 'SW1': 33, 'SW2': 27, 'SDA': 12, - 'SCL': 13, 'D0': 3, 'D1': 1, 'D2': 26, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22, - 'D7': 0, 'D8': 15, 'D9': 14, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13, - 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D20': 36, 'D21': 39}, + 'microduino-core-esp32': {'A0': 12, 'A1': 13, 'A10': 25, 'A11': 26, 'A12': 27, 'A13': 14, + 'A2': 15, 'A3': 4, 'A6': 38, 'A7': 37, 'A8': 32, 'A9': 33, 'D0': 3, + 'D1': 1, 'D10': 5, 'D11': 23, 'D12': 19, 'D13': 18, 'D14': 12, + 'D15': 13, 'D16': 15, 'D17': 4, 'D18': 22, 'D19': 21, 'D2': 16, + 'D20': 38, 'D21': 37, 'D3': 17, 'D4': 32, 'D5': 33, 'D6': 25, + 'D7': 26, 'D8': 27, 'D9': 14, 'SCL': 21, 'SCL1': 13, 'SDA': 22, + 'SDA1': 12}, + 'nano32': {'BUTTON': 0, 'LED': 16}, + 'nina_w10': {'D0': 3, 'D1': 1, 'D10': 5, 'D11': 19, 'D12': 23, 'D13': 18, 'D14': 13, + 'D15': 12, 'D16': 32, 'D17': 33, 'D18': 21, 'D19': 34, 'D2': 26, 'D20': 36, + 'D21': 39, 'D3': 25, 'D4': 35, 'D5': 27, 'D6': 22, 'D7': 0, 'D8': 15, 'D9': 14, + 'LED_BLUE': 21, 'LED_GREEN': 33, 'LED_RED': 23, 'SCL': 13, 'SDA': 12, 'SW1': 33, + 'SW2': 27}, 'node32s': {}, - 'nodemcu-32s': {'LED': 2, 'BUTTON': 0}, - 'odroid_esp32': {'LED': 2, 'SDA': 15, 'SCL': 4, 'SS': 22, 'ADC1': 35, 'ADC2': 36}, - 'onehorse32dev': {'LED': 5, 'BUTTON': 0, 'A1': 37, 'A2': 38}, - 'oroca_edubot': {'LED': 13, 'TX': 17, 'RX': 16, 'SDA': 23, 'SS': 2, 'MOSI': 18, 'SCK': 5, - 'A0': 34, 'A1': 39, 'A2': 36, 'A3': 33, 'D0': 4, 'D1': 16, 'D2': 17, 'D3': 22, - 'D4': 23, 'D5': 5, 'D6': 18, 'D7': 19, 'D8': 33, 'VBAT': 35}, + 'nodemcu-32s': {'BUTTON': 0, 'LED': 2}, + 'odroid_esp32': {'ADC1': 35, 'ADC2': 36, 'LED': 2, 'SCL': 4, 'SDA': 15, 'SS': 22}, + 'onehorse32dev': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 5}, + 'oroca_edubot': {'A0': 34, 'A1': 39, 'A2': 36, 'A3': 33, 'D0': 4, 'D1': 16, 'D2': 17, + 'D3': 22, 'D4': 23, 'D5': 5, 'D6': 18, 'D7': 19, 'D8': 33, 'LED': 13, + 'MOSI': 18, 'RX': 16, 'SCK': 5, 'SDA': 23, 'SS': 2, 'TX': 17, 'VBAT': 35}, 'pico32': {}, 'pocket_32': {'LED': 16}, + 'pycom_gpy': {'A1': 37, 'A2': 38, 'LED': 0, 'MISO': 37, 'MOSI': 22, 'SCK': 13, 'SCL': 13, + 'SDA': 12, 'SS': 17}, 'quantum': {}, - 'ttgo-lora32-v1': {'LED': 2, 'BUTTON': 0, 'SS': 18, 'MOSI': 27, 'SCK': 5, 'A1': 37, 'A2': 38, - 'OLED_SDA': 4, 'OLED_SCL': 15, 'OLED_RST': 16, 'LORA_SCK': 5, - 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_RST': 14, - 'LORA_IRQ': 26}, - 'ttgo-t-beam': {'LORA_SCK': 5, 'LORA_MISO': 19, 'LORA_MOSI': 27, 'LORA_CS': 18, 'LORA_RST': 23, - 'LORA_IRQ': 26, 'LORA_IO1': 33, 'LORA_IO2': 32, 'SS': 18, 'MOSI': 27, 'SCK': 5, - 'T8': 32, 'T9': 33, 'DAC2': 25}, - 'turta_iot_node': {'LED': 13, 'TX': 10, 'RX': 9, 'SDA': 23, 'SS': 21, 'MOSI': 18, 'SCK': 5, - 'A0': 4, 'A1': 25, 'A2': 26, 'A3': 27, 'A8': 38, 'T1': 25, 'T2': 26, - 'T3': 27, 'T4': 32, 'T5': 33, 'T6': 34, 'T7': 35, 'T8': 22, 'T9': 23, - 'T10': 10, 'T11': 9, 'T12': 21, 'T13': 5, 'T14': 18, 'T15': 19, - 'T16': 37, 'T17': 14, 'T18': 2, 'T19': 38}, - 'wemosbat': 'pocket_32', - 'wesp32': {'SCL': 4, 'SDA': 2, 'MISO': 32, 'ETH_PHY_ADDR': 0, 'ETH_PHY_MDC': 16, - 'ETH_PHY_MDIO': 17}, - 'widora-air': {'LED': 25, 'BUTTON': 0, 'SDA': 23, 'SCL': 19, 'MOSI': 16, 'MISO': 17, 'A1': 39, - 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, 'A9': 2, - 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, 'D5': 5, 'D6': 4, 'T0': 19, - 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, + 'sparkfun_lora_gateway_1-channel': {'MISO': 12, 'MOSI': 13, 'SCK': 14, 'SS': 16}, + 'tinypico': {}, + 'ttgo-lora32-v1': {'A1': 37, 'A2': 38, 'BUTTON': 0, 'LED': 2, 'MOSI': 27, 'SCK': 5, 'SS': 18}, + 'ttgo-t-beam': {'BUTTON': 39, 'LED': 14, 'MOSI': 27, 'SCK': 5, 'SS': 18}, + 'ttgo-t-watch': {'BUTTON': 36, 'MISO': 2, 'MOSI': 15, 'SCK': 14, 'SS': 13}, + 'ttgo-t1': {'LED': 22, 'MISO': 2, 'MOSI': 15, 'SCK': 14, 'SCL': 23, 'SS': 13}, + 'turta_iot_node': {}, + 'vintlabs-devkit-v1': {'LED': 2, 'PWM0': 12, 'PWM1': 13, 'PWM2': 14, 'PWM3': 15, 'PWM4': 16, + 'PWM5': 17, 'PWM6': 18, 'PWM7': 19}, + 'wemos_d1_mini32': {'D0': 26, 'D1': 22, 'D2': 21, 'D3': 17, 'D4': 16, 'D5': 18, 'D6': 19, + 'D7': 23, 'D8': 5, 'LED': 2, 'RXD': 3, 'TXD': 1, '_VBAT': 35}, + 'wemosbat': {'LED': 16}, + 'wesp32': {'MISO': 32, 'SCL': 4, 'SDA': 15}, + 'widora-air': {'A1': 39, 'A2': 35, 'A3': 25, 'A4': 26, 'A5': 14, 'A6': 12, 'A7': 15, 'A8': 13, + 'A9': 2, 'BUTTON': 0, 'D0': 19, 'D1': 23, 'D2': 18, 'D3': 17, 'D4': 16, + 'D5': 5, 'D6': 4, 'LED': 25, 'MISO': 17, 'MOSI': 16, 'SCL': 19, 'SDA': 23, + 'T0': 19, 'T1': 23, 'T2': 18, 'T3': 17, 'T4': 16, 'T5': 5, 'T6': 4}, 'xinabox_cw02': {'LED': 27}, } diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index 1656f1ad33..5cc4fad998 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -7,7 +7,7 @@ import re import subprocess from esphome.core import CORE -from esphome.py_compat import IS_PY2 +from esphome.py_compat import decode_text from esphome.util import run_external_command, run_external_process _LOGGER = logging.getLogger(__name__) @@ -18,15 +18,13 @@ def patch_structhash(): # removed/added. This might have unintended consequences, but this improves compile # times greatly when adding/removing components and a simple clean build solves # all issues - # pylint: disable=no-member,no-name-in-module - from platformio.commands import run - from platformio import util - from platformio.util import get_project_dir - from os.path import join, isdir, getmtime, isfile + from platformio.commands.run import helpers, command + from os.path import join, isdir, getmtime from os import makedirs - def patched_clean_build_dir(build_dir): - structhash_file = join(build_dir, "structure.hash") + def patched_clean_build_dir(build_dir, *args): + from platformio import util + from platformio.project.helpers import get_project_dir platformio_ini = join(get_project_dir(), "platformio.ini") # if project's config is modified @@ -36,27 +34,33 @@ def patch_structhash(): if not isdir(build_dir): makedirs(build_dir) - proj_hash = run.calculate_project_hash() - - # check project structure - if isdir(build_dir) and isfile(structhash_file): - with open(structhash_file) as f: - if f.read() == proj_hash: - return - - with open(structhash_file, "w") as f: - f.write(proj_hash) - # pylint: disable=protected-access - orig = run._clean_build_dir + helpers.clean_build_dir = patched_clean_build_dir + command.clean_build_dir = patched_clean_build_dir - def patched_safe(*args, **kwargs): - try: - return patched_clean_build_dir(*args, **kwargs) - except Exception: # pylint: disable=broad-except - return orig(*args, **kwargs) - run._clean_build_dir = patched_safe +IGNORE_LIB_WARNINGS = r'(?:' + '|'.join(['Hash', 'Update']) + r')' +FILTER_PLATFORMIO_LINES = [ + r'Verbose mode can be enabled via `-v, --verbose` option.*', + r'CONFIGURATION: https://docs.platformio.org/.*', + r'PLATFORM: .*', + r'DEBUG: Current.*', + r'PACKAGES: .*', + r'LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf.*', + r'LDF Modes: Finder ~ chain, Compatibility ~ soft.*', + r'Looking for ' + IGNORE_LIB_WARNINGS + r' library in registry', + r"Warning! Library `.*'" + IGNORE_LIB_WARNINGS + + r".*` has not been found in PlatformIO Registry.", + r"You can ignore this message, if `.*" + IGNORE_LIB_WARNINGS + r".*` is a built-in library.*", + r'Scanning dependencies...', + r"Found \d+ compatible libraries", + r'Memory Usage -> http://bit.ly/pio-memory-usage', + r'esptool.py v.*', + r"Found: https://platformio.org/lib/show/.*", + r"Using cache: .*", + r'Installing dependencies', + r'.* @ .* is already installed', +] def run_platformio_cli(*args, **kwargs): @@ -65,18 +69,16 @@ def run_platformio_cli(*args, **kwargs): os.environ["PLATFORMIO_LIBDEPS_DIR"] = os.path.abspath(CORE.relative_piolibdeps_path()) cmd = ['platformio'] + list(args) - if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: - import platformio.__main__ - try: - if IS_PY2: - patch_structhash() - except Exception: # pylint: disable=broad-except - # Ignore when patch fails - pass - return run_external_command(platformio.__main__.main, - *cmd, **kwargs) + if not CORE.verbose: + kwargs['filter_lines'] = FILTER_PLATFORMIO_LINES - return run_external_process(*cmd, **kwargs) + if os.environ.get('ESPHOME_USE_SUBPROCESS') is not None: + return run_external_process(*cmd, **kwargs) + + import platformio.__main__ + patch_structhash() + return run_external_command(platformio.__main__.main, + *cmd, **kwargs) def run_platformio_cli_run(config, verbose, *args, **kwargs): @@ -98,6 +100,7 @@ def run_upload(config, verbose, port): def run_idedata(config): args = ['-t', 'idedata'] stdout = run_platformio_cli_run(config, False, *args, capture_stdout=True) + stdout = decode_text(stdout) match = re.search(r'{.*}', stdout) if match is None: return IDEData(None) diff --git a/esphome/py_compat.py b/esphome/py_compat.py index 6833a55801..6cdaa5b047 100644 --- a/esphome/py_compat.py +++ b/esphome/py_compat.py @@ -1,5 +1,6 @@ import functools import sys +import codecs PYTHON_MAJOR = sys.version_info[0] IS_PY2 = PYTHON_MAJOR == 2 @@ -75,15 +76,14 @@ def indexbytes(buf, i): return ord(buf[i]) -if IS_PY2: - def decode_text(data, encoding='utf-8', errors='strict'): - # type: (str, str, str) -> unicode - if isinstance(data, unicode): - return data - return unicode(data, encoding=encoding, errors=errors) -else: - def decode_text(data, encoding='utf-8', errors='strict'): - # type: (bytes, str, str) -> str - if isinstance(data, str): - return data - return data.decode(encoding=encoding, errors=errors) +def decode_text(data, encoding='utf-8', errors='strict'): + if isinstance(data, text_type): + return data + return codecs.decode(data, encoding, errors) + + +def encode_text(data, encoding='utf-8', errors='strict'): + if isinstance(data, binary_type): + return data + + return codecs.encode(data, encoding, errors) diff --git a/esphome/storage_json.py b/esphome/storage_json.py index b04f056f11..0305b59ef5 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -7,12 +7,13 @@ import os from esphome import const from esphome.core import CORE -from esphome.helpers import mkdir_p +from esphome.helpers import mkdir_p, write_file_if_changed # pylint: disable=unused-import, wrong-import-order from esphome.core import CoreType # noqa from typing import Any, Dict, Optional # noqa +from esphome.py_compat import text_type _LOGGER = logging.getLogger(__name__) @@ -35,7 +36,7 @@ def trash_storage_path(base_path): # type: (str) -> str # pylint: disable=too-many-instance-attributes class StorageJSON(object): - def __init__(self, storage_version, name, esphome_version, + def __init__(self, storage_version, name, comment, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, firmware_bin_path, loaded_integrations): # Version of the storage JSON schema @@ -43,6 +44,8 @@ class StorageJSON(object): self.storage_version = storage_version # type: int # The name of the node self.name = name # type: str + # The comment of the node + self.comment = comment # type: str # The esphome version this was compiled with self.esphome_version = esphome_version # type: str # The version of the file in src/main.cpp - Used to migrate the file @@ -69,6 +72,7 @@ class StorageJSON(object): return { 'storage_version': self.storage_version, 'name': self.name, + 'comment': self.comment, 'esphome_version': self.esphome_version, 'src_version': self.src_version, 'arduino_version': self.arduino_version, @@ -85,14 +89,14 @@ class StorageJSON(object): def save(self, path): mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w', encoding='utf-8') as f_handle: - f_handle.write(self.to_json()) + write_file_if_changed(path, self.to_json()) @staticmethod def from_esphome_core(esph, old): # type: (CoreType, Optional[StorageJSON]) -> StorageJSON return StorageJSON( storage_version=1, name=esph.name, + comment=esph.comment, esphome_version=const.__version__, src_version=1, arduino_version=esph.arduino_version, @@ -110,6 +114,7 @@ class StorageJSON(object): return StorageJSON( storage_version=1, name=name, + comment=None, esphome_version=const.__version__, src_version=1, arduino_version=None, @@ -124,10 +129,10 @@ class StorageJSON(object): @staticmethod def _load_impl(path): # type: (str) -> Optional[StorageJSON] with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - storage = json.loads(text, encoding='utf-8') + storage = json.load(f_handle) storage_version = storage['storage_version'] name = storage.get('name') + comment = storage.get('comment') esphome_version = storage.get('esphome_version', storage.get('esphomeyaml_version')) src_version = storage.get('src_version') arduino_version = storage.get('arduino_version') @@ -137,7 +142,7 @@ class StorageJSON(object): build_path = storage.get('build_path') firmware_bin_path = storage.get('firmware_bin_path') loaded_integrations = storage.get('loaded_integrations', []) - return StorageJSON(storage_version, name, esphome_version, + return StorageJSON(storage_version, name, comment, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, firmware_bin_path, loaded_integrations) @@ -188,15 +193,12 @@ class EsphomeStorageJSON(object): return json.dumps(self.as_dict(), indent=2) + u'\n' def save(self, path): # type: (str) -> None - mkdir_p(os.path.dirname(path)) - with codecs.open(path, 'w', encoding='utf-8') as f_handle: - f_handle.write(self.to_json()) + write_file_if_changed(path, self.to_json()) @staticmethod def _load_impl(path): # type: (str) -> Optional[EsphomeStorageJSON] with codecs.open(path, 'r', encoding='utf-8') as f_handle: - text = f_handle.read() - storage = json.loads(text, encoding='utf-8') + storage = json.load(f_handle) storage_version = storage['storage_version'] cookie_secret = storage.get('cookie_secret') last_update_check = storage.get('last_update_check') @@ -215,7 +217,7 @@ class EsphomeStorageJSON(object): def get_default(): # type: () -> EsphomeStorageJSON return EsphomeStorageJSON( storage_version=1, - cookie_secret=binascii.hexlify(os.urandom(64)), + cookie_secret=text_type(binascii.hexlify(os.urandom(64))), last_update_check=None, remote_version=None, ) diff --git a/esphome/util.py b/esphome/util.py index 0ae0b9e32e..098d5e52da 100644 --- a/esphome/util.py +++ b/esphome/util.py @@ -3,12 +3,13 @@ from __future__ import print_function import collections import io import logging +import os import re import subprocess import sys from esphome import const -from esphome.py_compat import IS_PY2 +from esphome.py_compat import IS_PY2, decode_text, text_type _LOGGER = logging.getLogger(__name__) @@ -87,24 +88,67 @@ def shlex_quote(s): return u"'" + s.replace(u"'", u"'\"'\"'") + u"'" +ANSI_ESCAPE = re.compile(r'\033[@-_][0-?]*[ -/]*[@-~]') + + class RedirectText(object): - def __init__(self, out): + def __init__(self, out, filter_lines=None): self._out = out + if filter_lines is None: + self._filter_pattern = None + else: + pattern = r'|'.join(r'(?:' + pattern + r')' for pattern in filter_lines) + self._filter_pattern = re.compile(pattern) + self._line_buffer = '' def __getattr__(self, item): return getattr(self._out, item) - def write(self, s): + def _write_color_replace(self, s): from esphome.core import CORE if CORE.dashboard: - try: - s = s.replace('\033', '\\033') - except UnicodeEncodeError: - pass - + # With the dashboard, we must create a little hack to make color output + # work. The shell we create in the dashboard is not a tty, so python removes + # all color codes from the resulting stream. We just convert them to something + # we can easily recognize later here. + s = s.replace('\033', '\\033') self._out.write(s) + def write(self, s): + # s is usually a text_type already (self._out is of type TextIOWrapper) + # However, s is sometimes also a bytes object in python3. Let's make sure it's a + # text_type + # If the conversion fails, we will create an exception, which is okay because we won't + # be able to print it anyway. + text = decode_text(s) + assert isinstance(text, text_type) + + if self._filter_pattern is not None: + self._line_buffer += text + lines = self._line_buffer.splitlines(True) + for line in lines: + if '\n' not in line and '\r' not in line: + # Not a complete line, set line buffer + self._line_buffer = line + break + self._line_buffer = '' + + line_without_ansi = ANSI_ESCAPE.sub('', line) + line_without_end = line_without_ansi.rstrip() + if self._filter_pattern.match(line_without_end) is not None: + # Filter pattern matched, ignore the line + continue + + self._write_color_replace(line) + else: + self._write_color_replace(text) + + # write() returns the number of characters written + # Let's print the number of characters of the original string in order to not confuse + # any caller. + return len(s) + # pylint: disable=no-self-use def isatty(self): return True @@ -119,14 +163,15 @@ def run_external_command(func, *cmd, **kwargs): full_cmd = u' '.join(shlex_quote(x) for x in cmd) _LOGGER.info(u"Running: %s", full_cmd) + filter_lines = kwargs.get('filter_lines') orig_stdout = sys.stdout - sys.stdout = RedirectText(sys.stdout) + sys.stdout = RedirectText(sys.stdout, filter_lines=filter_lines) orig_stderr = sys.stderr - sys.stderr = RedirectText(sys.stderr) + sys.stderr = RedirectText(sys.stderr, filter_lines=filter_lines) capture_stdout = kwargs.get('capture_stdout', False) if capture_stdout: - cap_stdout = sys.stdout = io.BytesIO() + cap_stdout = sys.stdout = io.StringIO() try: sys.argv = list(cmd) @@ -154,14 +199,15 @@ def run_external_command(func, *cmd, **kwargs): def run_external_process(*cmd, **kwargs): full_cmd = u' '.join(shlex_quote(x) for x in cmd) _LOGGER.info(u"Running: %s", full_cmd) + filter_lines = kwargs.get('filter_lines') capture_stdout = kwargs.get('capture_stdout', False) if capture_stdout: sub_stdout = io.BytesIO() else: - sub_stdout = RedirectText(sys.stdout) + sub_stdout = RedirectText(sys.stdout, filter_lines=filter_lines) - sub_stderr = RedirectText(sys.stderr) + sub_stderr = RedirectText(sys.stderr, filter_lines=filter_lines) try: return subprocess.call(cmd, @@ -207,3 +253,16 @@ class OrderedDict(collections.OrderedDict): root[1] = first[0] = link else: super(OrderedDict, self).move_to_end(key, last=last) # pylint: disable=no-member + + +def list_yaml_files(folder): + files = filter_yaml_files([os.path.join(folder, p) for p in os.listdir(folder)]) + files.sort() + return files + + +def filter_yaml_files(files): + files = [f for f in files if os.path.splitext(f)[1] == '.yaml'] + files = [f for f in files if os.path.basename(f) != 'secrets.yaml'] + files = [f for f in files if not os.path.basename(f).startswith('.')] + return files diff --git a/esphome/voluptuous_schema.py b/esphome/voluptuous_schema.py index 13f4c5d6b8..09fa1a6756 100644 --- a/esphome/voluptuous_schema.py +++ b/esphome/voluptuous_schema.py @@ -21,8 +21,8 @@ def ensure_multiple_invalid(err): # pylint: disable=protected-access, unidiomatic-typecheck class _Schema(vol.Schema): """Custom cv.Schema that prints similar keys on error.""" - def __init__(self, schema, extra=vol.PREVENT_EXTRA, extra_schemas=None): - super(_Schema, self).__init__(schema, extra=extra) + def __init__(self, schema, required=False, extra=vol.PREVENT_EXTRA, extra_schemas=None): + super(_Schema, self).__init__(schema, required=required, extra=extra) # List of extra schemas to apply after validation # Should be used sparingly, as it's not a very voluptuous-way/clean way of # doing things. diff --git a/esphome/vscode.py b/esphome/vscode.py index f0fa4ef52a..151bfb5281 100644 --- a/esphome/vscode.py +++ b/esphome/vscode.py @@ -59,7 +59,7 @@ def read_config(args): CORE.ace = args.ace f = data['file'] if CORE.ace: - CORE.config_path = os.path.join(args.configuration, f) + CORE.config_path = os.path.join(args.configuration[0], f) else: CORE.config_path = data['file'] vs = VSCodeResult() diff --git a/esphome/wizard.py b/esphome/wizard.py index ac77915497..67ec2c8960 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,13 +1,14 @@ from __future__ import print_function -import codecs import os +import random +import string import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.helpers import color, get_bool_env +from esphome.helpers import color, get_bool_env, write_file # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS from esphome.py_compat import safe_input, text_type @@ -52,6 +53,13 @@ wifi: ssid: "{ssid}" password: "{psk}" + # Enable fallback hotspot (captive portal) in case wifi connection fails + ap: + ssid: "{fallback_name}" + password: "{fallback_psk}" + +captive_portal: + # Enable logging logger: @@ -65,6 +73,14 @@ def sanitize_double_quotes(value): def wizard_file(**kwargs): + letters = string.ascii_letters + string.digits + ap_name_base = kwargs['name'].replace('_', ' ').title() + ap_name = "{} Fallback Hotspot".format(ap_name_base) + if len(ap_name) > 32: + ap_name = ap_name_base + kwargs['fallback_name'] = ap_name + kwargs['fallback_psk'] = ''.join(random.choice(letters) for _ in range(12)) + config = BASE_CONFIG.format(**kwargs) if kwargs['password']: @@ -87,8 +103,7 @@ def wizard_write(path, **kwargs): kwargs['platform'] = 'ESP8266' if board in ESP8266_BOARD_PINS else 'ESP32' platform = kwargs['platform'] - with codecs.open(path, 'w', 'utf-8') as f_handle: - f_handle.write(wizard_file(**kwargs)) + write_file(path, wizard_file(**kwargs)) storage = StorageJSON.from_wizard(name, name + '.local', platform, board) storage_path = ext_storage_path(os.path.dirname(path), os.path.basename(path)) storage.save(storage_path) @@ -117,8 +132,8 @@ def default_input(text, default): # From https://stackoverflow.com/a/518232/8924614 -def strip_accents(string): - return u''.join(c for c in unicodedata.normalize('NFD', text_type(string)) +def strip_accents(value): + return u''.join(c for c in unicodedata.normalize('NFD', text_type(value)) if unicodedata.category(c) != 'Mn') diff --git a/esphome/writer.py b/esphome/writer.py index bda3f09df2..4c2e03569f 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -8,7 +8,8 @@ from esphome.config import iter_components from esphome.const import CONF_BOARD_FLASH_MODE, CONF_ESPHOME, CONF_PLATFORMIO_OPTIONS, \ HEADER_FILE_EXTENSIONS, SOURCE_FILE_EXTENSIONS, __version__ from esphome.core import CORE, EsphomeError -from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files +from esphome.helpers import mkdir_p, read_file, write_file_if_changed, walk_files, \ + copy_file_if_changed from esphome.storage_json import StorageJSON, storage_path _LOGGER = logging.getLogger(__name__) @@ -112,7 +113,7 @@ def migrate_src_version_0_to_1(): "auto-generated again.", main_cpp, main_cpp) _LOGGER.info("Migration: Added include section to %s", main_cpp) - write_file_if_changed(content, main_cpp) + write_file_if_changed(main_cpp, content) def migrate_src_version(old, new): @@ -171,9 +172,7 @@ def format_ini(data): def gather_lib_deps(): - lib_deps_l = [x.as_lib_dep for x in CORE.libraries] - lib_deps_l.sort() - return lib_deps_l + return [x.as_lib_dep for x in CORE.libraries] def gather_build_flags(): @@ -193,7 +192,7 @@ def get_ini_content(): 'framework': 'arduino', 'lib_deps': lib_deps + ['${common.lib_deps}'], 'build_flags': build_flags + ['${common.build_flags}'], - 'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 115200), + 'upload_speed': UPLOAD_SPEED_OVERRIDE.get(CORE.board, 460800), } if CORE.is_esp32: @@ -253,7 +252,7 @@ def write_platformio_ini(content): content_format = INI_BASE_FORMAT full_file = content_format[0] + INI_AUTO_GENERATE_BEGIN + '\n' + content full_file += INI_AUTO_GENERATE_END + content_format[1] - write_file_if_changed(full_file, path) + write_file_if_changed(path, full_file) def write_platformio_project(): @@ -287,7 +286,6 @@ or use the custom_components folder. def copy_src_tree(): - import filecmp import shutil source_files = {} @@ -323,9 +321,7 @@ def copy_src_tree(): os.remove(path) else: src_path = source_files_copy.pop(target) - if not filecmp.cmp(path, src_path): - # Files are not same, copy - shutil.copy(src_path, path) + copy_file_if_changed(src_path, path) # Now copy new files for target, src_path in source_files_copy.items(): @@ -334,14 +330,14 @@ def copy_src_tree(): shutil.copy(src_path, dst_path) # Finally copy defines - write_file_if_changed(generate_defines_h(), - CORE.relative_src_path('esphome', 'core', 'defines.h')) - write_file_if_changed(ESPHOME_README_TXT, - CORE.relative_src_path('esphome', 'README.txt')) - write_file_if_changed(ESPHOME_H_FORMAT.format(include_s), - CORE.relative_src_path('esphome.h')) - write_file_if_changed(VERSION_H_FORMAT.format(__version__), - CORE.relative_src_path('esphome', 'core', 'version.h')) + write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'defines.h'), + generate_defines_h()) + write_file_if_changed(CORE.relative_src_path('esphome', 'README.txt'), + ESPHOME_README_TXT) + write_file_if_changed(CORE.relative_src_path('esphome.h'), + ESPHOME_H_FORMAT.format(include_s)) + write_file_if_changed(CORE.relative_src_path('esphome', 'core', 'version.h'), + VERSION_H_FORMAT.format(__version__)) def generate_defines_h(): @@ -367,7 +363,7 @@ def write_cpp(code_s): full_file = code_format[0] + CPP_INCLUDE_BEGIN + u'\n' + global_s + CPP_INCLUDE_END full_file += code_format[1] + CPP_AUTO_GENERATE_BEGIN + u'\n' + code_s + CPP_AUTO_GENERATE_END full_file += code_format[2] - write_file_if_changed(full_file, path) + write_file_if_changed(path, full_file) def clean_build(): diff --git a/esphome/yaml_util.py b/esphome/yaml_util.py index 30e8bb82de..fb04d7d5b0 100644 --- a/esphome/yaml_util.py +++ b/esphome/yaml_util.py @@ -14,7 +14,7 @@ from esphome import core from esphome.config_helpers import read_config_file from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange from esphome.py_compat import text_type, IS_PY2 -from esphome.util import OrderedDict +from esphome.util import OrderedDict, filter_yaml_files _LOGGER = logging.getLogger(__name__) @@ -260,12 +260,12 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_list(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) return [_load_yaml_internal(f) for f in files] @_add_data_ref def construct_include_dir_merge_list(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) merged_list = [] for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -275,7 +275,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_named(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) mapping = OrderedDict() for fname in files: filename = os.path.splitext(os.path.basename(fname))[0] @@ -284,7 +284,7 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors @_add_data_ref def construct_include_dir_merge_named(self, node): - files = _filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) + files = filter_yaml_files(_find_files(self._rel_path(node.value), '*.yaml')) mapping = OrderedDict() for fname in files: loaded_yaml = _load_yaml_internal(fname) @@ -297,12 +297,6 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors return Lambda(text_type(node.value)) -def _filter_yaml_files(files): - files = [f for f in files if os.path.basename(f) != SECRET_YAML] - files = [f for f in files if not os.path.basename(f).startswith('.')] - return files - - ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:int', ESPHomeLoader.construct_yaml_int) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:float', ESPHomeLoader.construct_yaml_float) ESPHomeLoader.add_constructor(u'tag:yaml.org,2002:binary', ESPHomeLoader.construct_yaml_binary) diff --git a/platformio.ini b/platformio.ini index c5a0d9a21b..19b850c546 100644 --- a/platformio.ini +++ b/platformio.ini @@ -4,20 +4,20 @@ ; It's *not* used during runtime. [platformio] -env_default = livingroom8266 +default_envs = livingroom8266 src_dir = . include_dir = include [common] lib_deps = - AsyncTCP@1.0.3 - AsyncMqttClient@0.8.2 + AsyncTCP@1.1.1 + AsyncMqttClient-esphome@0.8.3 ArduinoJson-esphomelib@5.13.3 - ESP Async WebServer@1.1.1 - FastLED@3.2.0 - NeoPixelBus@2.4.1 - ESPAsyncTCP@1.2.0 - TinyGPSPlus@1.0.2 + ESPAsyncWebServer-esphome@1.2.5 + FastLED@3.2.9 + NeoPixelBus-esphome@2.5.2 + ESPAsyncTCP-esphome@1.2.2 + 1655@1.0.2 ; TinyGPSPlus (has name conflict) build_flags = -Wno-reorder -DUSE_WEB_SERVER @@ -29,15 +29,8 @@ build_flags = ; log messages src_filter = + -[env:livingroom32] -platform = espressif32@1.6.0 -board = nodemcu-32s -framework = arduino -lib_deps = ${common.lib_deps} -build_flags = ${common.build_flags} -DUSE_ETHERNET -src_filter = ${common.src_filter} + - [env:livingroom8266] +; use Arduino framework v2.3.0 for clang-tidy (latest 2.5.2 breaks static code analysis, see #760) platform = espressif8266@1.8.0 board = nodemcuv2 framework = arduino @@ -47,3 +40,11 @@ lib_deps = Hash build_flags = ${common.build_flags} src_filter = ${common.src_filter} + + +[env:livingroom32] +platform = espressif32@1.11.0 +board = nodemcu-32s +framework = arduino +lib_deps = ${common.lib_deps} +build_flags = ${common.build_flags} -DUSE_ETHERNET +src_filter = ${common.src_filter} + diff --git a/requirements.txt b/requirements.txt index 178d3d4459..14ce062000 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,13 @@ -voluptuous>=0.11.5,<0.12 -PyYAML>=5.1,<6 -paho-mqtt>=1.4,<2 -colorlog>=4.0.2 -tornado>=5.1.1,<6 +voluptuous==0.11.7 +PyYAML==5.1.2 +paho-mqtt==1.4.0 +colorlog==4.0.2 +tornado==5.1.1 typing>=3.6.6;python_version<"3.5" -protobuf>=3.7,<3.8 -tzlocal>=1.5.1 -pyserial>=3.4,<4 -ifaddr>=0.1.6,<1 -platformio>=3.6.5 ; python_version<"3" -https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" -esptool>=2.6,<3 +protobuf==3.10.0 +tzlocal==2.0.0 +pytz==2019.3 +pyserial==3.4 +ifaddr==0.1.6 +platformio==4.0.3 +esptool==2.7 diff --git a/requirements_test.txt b/requirements_test.txt index 079b0bf85f..26f14434d8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,16 +1,17 @@ -voluptuous>=0.11.5,<0.12 -PyYAML>=5.1,<6 -paho-mqtt>=1.4,<2 -colorlog>=4.0.2 -tornado>=5.1.1,<6 -typing>=3.6.6 ; python_version<"3.5" -protobuf>=3.7,<3.8 -tzlocal>=1.5.1 -pyserial>=3.4,<4 -ifaddr>=0.1.6,<1 -platformio>=3.6.5 ; python_version<"3" -https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" -esptool>=2.6,<3 +voluptuous==0.11.7 +PyYAML==5.1.2 +paho-mqtt==1.4.0 +colorlog==4.0.2 +tornado==5.1.1 +typing>=3.6.6;python_version<"3.5" +protobuf==3.10.0 +tzlocal==2.0.0 +pytz==2019.3 +pyserial==3.4 +ifaddr==0.1.6 +platformio==4.0.3 +esptool==2.7 + pylint==1.9.4 ; python_version<"3" pylint==2.3.0 ; python_version>"3" flake8==3.6.0 diff --git a/script/.neopixelbus.patch b/script/.neopixelbus.patch deleted file mode 100644 index 8dc2e74b66..0000000000 --- a/script/.neopixelbus.patch +++ /dev/null @@ -1,26 +0,0 @@ ---- .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h 2018-12-25 06:37:53.000000000 +0100 -+++ .piolibdeps/NeoPixelBus_ID547/src/internal/NeoEsp8266DmaMethod.h.2 2019-03-01 22:18:10.000000000 +0100 -@@ -169,7 +169,7 @@ - _i2sBufDesc[indexDesc].sub_sof = 0; - _i2sBufDesc[indexDesc].datalen = blockSize; - _i2sBufDesc[indexDesc].blocksize = blockSize; -- _i2sBufDesc[indexDesc].buf_ptr = (uint32_t)is2Buffer; -+ _i2sBufDesc[indexDesc].buf_ptr = is2Buffer; - _i2sBufDesc[indexDesc].unused = 0; - _i2sBufDesc[indexDesc].next_link_ptr = (uint32_t)&(_i2sBufDesc[indexDesc + 1]); - -@@ -329,11 +329,13 @@ - case NeoDmaState_Sending: - { - slc_queue_item* finished_item = (slc_queue_item*)SLCRXEDA; -+ uint32_t **ptr = reinterpret_cast(&finished_item); -+ uint32_t dat = *reinterpret_cast(ptr); - - // the data block had actual data sent - // point last state block to first state block thus - // just looping and not sending the data blocks -- (finished_item + 1)->next_link_ptr = (uint32_t)(finished_item); -+ (finished_item + 1)->next_link_ptr = dat; - - s_this->_dmaState = NeoDmaState_Idle; - } diff --git a/script/api_protobuf/api_options_pb2.py b/script/api_protobuf/api_options_pb2.py new file mode 100644 index 0000000000..52cbbde678 --- /dev/null +++ b/script/api_protobuf/api_options_pb2.py @@ -0,0 +1,168 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: api_options.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import descriptor_pb2 as google_dot_protobuf_dot_descriptor__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='api_options.proto', + package='', + syntax='proto2', + serialized_options=None, + serialized_pb=_b('\n\x11\x61pi_options.proto\x1a google/protobuf/descriptor.proto\"\x06\n\x04void*F\n\rAPISourceType\x12\x0f\n\x0bSOURCE_BOTH\x10\x00\x12\x11\n\rSOURCE_SERVER\x10\x01\x12\x11\n\rSOURCE_CLIENT\x10\x02:E\n\x16needs_setup_connection\x12\x1e.google.protobuf.MethodOptions\x18\x8e\x08 \x01(\x08:\x04true:C\n\x14needs_authentication\x12\x1e.google.protobuf.MethodOptions\x18\x8f\x08 \x01(\x08:\x04true:/\n\x02id\x12\x1f.google.protobuf.MessageOptions\x18\x8c\x08 \x01(\r:\x01\x30:M\n\x06source\x12\x1f.google.protobuf.MessageOptions\x18\x8d\x08 \x01(\x0e\x32\x0e.APISourceType:\x0bSOURCE_BOTH:/\n\x05ifdef\x12\x1f.google.protobuf.MessageOptions\x18\x8e\x08 \x01(\t:3\n\x03log\x12\x1f.google.protobuf.MessageOptions\x18\x8f\x08 \x01(\x08:\x04true:9\n\x08no_delay\x12\x1f.google.protobuf.MessageOptions\x18\x90\x08 \x01(\x08:\x05\x66\x61lse') + , + dependencies=[google_dot_protobuf_dot_descriptor__pb2.DESCRIPTOR,]) + +_APISOURCETYPE = _descriptor.EnumDescriptor( + name='APISourceType', + full_name='APISourceType', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='SOURCE_BOTH', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOURCE_SERVER', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SOURCE_CLIENT', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=63, + serialized_end=133, +) +_sym_db.RegisterEnumDescriptor(_APISOURCETYPE) + +APISourceType = enum_type_wrapper.EnumTypeWrapper(_APISOURCETYPE) +SOURCE_BOTH = 0 +SOURCE_SERVER = 1 +SOURCE_CLIENT = 2 + +NEEDS_SETUP_CONNECTION_FIELD_NUMBER = 1038 +needs_setup_connection = _descriptor.FieldDescriptor( + name='needs_setup_connection', full_name='needs_setup_connection', index=0, + number=1038, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +NEEDS_AUTHENTICATION_FIELD_NUMBER = 1039 +needs_authentication = _descriptor.FieldDescriptor( + name='needs_authentication', full_name='needs_authentication', index=1, + number=1039, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +ID_FIELD_NUMBER = 1036 +id = _descriptor.FieldDescriptor( + name='id', full_name='id', index=2, + number=1036, type=13, cpp_type=3, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +SOURCE_FIELD_NUMBER = 1037 +source = _descriptor.FieldDescriptor( + name='source', full_name='source', index=3, + number=1037, type=14, cpp_type=8, label=1, + has_default_value=True, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +IFDEF_FIELD_NUMBER = 1038 +ifdef = _descriptor.FieldDescriptor( + name='ifdef', full_name='ifdef', index=4, + number=1038, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +LOG_FIELD_NUMBER = 1039 +log = _descriptor.FieldDescriptor( + name='log', full_name='log', index=5, + number=1039, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=True, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) +NO_DELAY_FIELD_NUMBER = 1040 +no_delay = _descriptor.FieldDescriptor( + name='no_delay', full_name='no_delay', index=6, + number=1040, type=8, cpp_type=7, label=1, + has_default_value=True, default_value=False, + message_type=None, enum_type=None, containing_type=None, + is_extension=True, extension_scope=None, + serialized_options=None, file=DESCRIPTOR) + + +_VOID = _descriptor.Descriptor( + name='void', + full_name='void', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto2', + extension_ranges=[], + oneofs=[ + ], + serialized_start=55, + serialized_end=61, +) + +DESCRIPTOR.message_types_by_name['void'] = _VOID +DESCRIPTOR.enum_types_by_name['APISourceType'] = _APISOURCETYPE +DESCRIPTOR.extensions_by_name['needs_setup_connection'] = needs_setup_connection +DESCRIPTOR.extensions_by_name['needs_authentication'] = needs_authentication +DESCRIPTOR.extensions_by_name['id'] = id +DESCRIPTOR.extensions_by_name['source'] = source +DESCRIPTOR.extensions_by_name['ifdef'] = ifdef +DESCRIPTOR.extensions_by_name['log'] = log +DESCRIPTOR.extensions_by_name['no_delay'] = no_delay +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +void = _reflection.GeneratedProtocolMessageType('void', (_message.Message,), dict( + DESCRIPTOR = _VOID, + __module__ = 'api_options_pb2' + # @@protoc_insertion_point(class_scope:void) + )) +_sym_db.RegisterMessage(void) + +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_setup_connection) +google_dot_protobuf_dot_descriptor__pb2.MethodOptions.RegisterExtension(needs_authentication) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(id) +source.enum_type = _APISOURCETYPE +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(source) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(ifdef) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(log) +google_dot_protobuf_dot_descriptor__pb2.MessageOptions.RegisterExtension(no_delay) + +# @@protoc_insertion_point(module_scope) diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py new file mode 100644 index 0000000000..6357ae38ed --- /dev/null +++ b/script/api_protobuf/api_protobuf.py @@ -0,0 +1,867 @@ +"""Python 3 script to automatically generate C++ classes for ESPHome's native API. + +It's pretty crappy spaghetti code, but it works. +""" + +import re +from pathlib import Path +from textwrap import dedent +from subprocess import call + +# Generate with +# protoc --python_out=script/api_protobuf -I esphome/components/api/ api_options.proto +import api_options_pb2 as pb +import google.protobuf.descriptor_pb2 as descriptor + +cwd = Path(__file__).parent +root = cwd.parent.parent / 'esphome' / 'components' / 'api' +prot = cwd / 'api.protoc' +call(['protoc', '-o', prot, '-I', root, 'api.proto']) +content = prot.read_bytes() + +d = descriptor.FileDescriptorSet.FromString(content) + + +def indent_list(text, padding=u' '): + return [padding + line for line in text.splitlines()] + + +def indent(text, padding=u' '): + return u'\n'.join(indent_list(text, padding)) + + +def camel_to_snake(name): + # https://stackoverflow.com/a/1176023 + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +class TypeInfo(): + def __init__(self, field): + self._field = field + + @property + def default_value(self): + return '' + + @property + def name(self): + return self._field.name + + @property + def arg_name(self): + return self.name + + @property + def field_name(self): + return self.name + + @property + def number(self): + return self._field.number + + @property + def repeated(self): + return self._field.label == 3 + + @property + def cpp_type(self): + raise NotImplementedError + + @property + def reference_type(self): + return f'{self.cpp_type} ' + + @property + def const_reference_type(self): + return f'{self.cpp_type} ' + + @property + def public_content(self) -> str: + return [self.class_member] + + @property + def protected_content(self) -> str: + return [] + + @property + def class_member(self) -> str: + return f'{self.cpp_type} {self.field_name}{{{self.default_value}}}; // NOLINT' + + @property + def decode_varint_content(self) -> str: + content = self.decode_varint + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_varint = None + + @property + def decode_length_content(self) -> str: + content = self.decode_length + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_length = None + + @property + def decode_32bit_content(self) -> str: + content = self.decode_32bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_32bit = None + + @property + def decode_64bit_content(self) -> str: + content = self.decode_64bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name} = {content}; + return true; + }}''') + + decode_64bit = None + + @property + def encode_content(self): + return f'buffer.{self.encode_func}({self.number}, this->{self.field_name});' + + encode_func = None + + @property + def dump_content(self): + o = f'out.append(" {self.name}: ");\n' + o += self.dump(f'this->{self.field_name}') + '\n' + o += f'out.append("\\n");\n' + return o + + dump = None + + +TYPE_INFO = {} + + +def register_type(name): + def func(value): + TYPE_INFO[name] = value + return value + + return func + + +@register_type(1) +class DoubleType(TypeInfo): + cpp_type = 'double' + default_value = '0.0' + decode_64bit = 'value.as_double()' + encode_func = 'encode_double' + + def dump(self, name): + o = f'sprintf(buffer, "%g", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(2) +class FloatType(TypeInfo): + cpp_type = 'float' + default_value = '0.0f' + decode_32bit = 'value.as_float()' + encode_func = 'encode_float' + + def dump(self, name): + o = f'sprintf(buffer, "%g", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(3) +class Int64Type(TypeInfo): + cpp_type = 'int64_t' + default_value = '0' + decode_varint = 'value.as_int64()' + encode_func = 'encode_int64' + + def dump(self, name): + o = f'sprintf(buffer, "%ll", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(4) +class UInt64Type(TypeInfo): + cpp_type = 'uint64_t' + default_value = '0' + decode_varint = 'value.as_uint64()' + encode_func = 'encode_uint64' + + def dump(self, name): + o = f'sprintf(buffer, "%ull", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(5) +class Int32Type(TypeInfo): + cpp_type = 'int32_t' + default_value = '0' + decode_varint = 'value.as_int32()' + encode_func = 'encode_int32' + + def dump(self, name): + o = f'sprintf(buffer, "%d", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(6) +class Fixed64Type(TypeInfo): + cpp_type = 'uint64_t' + default_value = '0' + decode_64bit = 'value.as_fixed64()' + encode_func = 'encode_fixed64' + + def dump(self, name): + o = f'sprintf(buffer, "%ull", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(7) +class Fixed32Type(TypeInfo): + cpp_type = 'uint32_t' + default_value = '0' + decode_32bit = 'value.as_fixed32()' + encode_func = 'encode_fixed32' + + def dump(self, name): + o = f'sprintf(buffer, "%u", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(8) +class BoolType(TypeInfo): + cpp_type = 'bool' + default_value = 'false' + decode_varint = 'value.as_bool()' + encode_func = 'encode_bool' + + def dump(self, name): + o = f'out.append(YESNO({name}));' + return o + + +@register_type(9) +class StringType(TypeInfo): + cpp_type = 'std::string' + default_value = '' + reference_type = 'std::string &' + const_reference_type = 'const std::string &' + decode_length = 'value.as_string()' + encode_func = 'encode_string' + + def dump(self, name): + o = f'out.append("\'").append({name}).append("\'");' + return o + + +@register_type(11) +class MessageType(TypeInfo): + @property + def cpp_type(self): + return self._field.type_name[1:] + + default_value = '' + + @property + def reference_type(self): + return f'{self.cpp_type} &' + + @property + def const_reference_type(self): + return f'const {self.cpp_type} &' + + @property + def encode_func(self): + return f'encode_message<{self.cpp_type}>' + + @property + def decode_length(self): + return f'value.as_message<{self.cpp_type}>()' + + def dump(self, name): + o = f'{name}.dump_to(out);' + return o + + +@register_type(12) +class BytesType(TypeInfo): + cpp_type = 'std::string' + default_value = '' + reference_type = 'std::string &' + const_reference_type = 'const std::string &' + decode_length = 'value.as_string()' + encode_func = 'encode_string' + + def dump(self, name): + o = f'out.append("\'").append({name}).append("\'");' + return o + + +@register_type(13) +class UInt32Type(TypeInfo): + cpp_type = 'uint32_t' + default_value = '0' + decode_varint = 'value.as_uint32()' + encode_func = 'encode_uint32' + + def dump(self, name): + o = f'sprintf(buffer, "%u", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(14) +class EnumType(TypeInfo): + @property + def cpp_type(self): + return "Enum" + self._field.type_name[1:] + + @property + def decode_varint(self): + return f'value.as_enum<{self.cpp_type}>()' + + default_value = '' + + @property + def encode_func(self): + return f'encode_enum<{self.cpp_type}>' + + def dump(self, name): + o = f'out.append(proto_enum_to_string<{self.cpp_type}>({name}));' + return o + + +@register_type(15) +class SFixed32Type(TypeInfo): + cpp_type = 'int32_t' + default_value = '0' + decode_32bit = 'value.as_sfixed32()' + encode_func = 'encode_sfixed32' + + def dump(self, name): + o = f'sprintf(buffer, "%d", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(16) +class SFixed64Type(TypeInfo): + cpp_type = 'int64_t' + default_value = '0' + decode_64bit = 'value.as_sfixed64()' + encode_func = 'encode_sfixed64' + + def dump(self, name): + o = f'sprintf(buffer, "%ll", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(17) +class SInt32Type(TypeInfo): + cpp_type = 'int32_t' + default_value = '0' + decode_varint = 'value.as_sint32()' + encode_func = 'encode_sint32' + + def dump(self, name): + o = f'sprintf(buffer, "%d", {name});\n' + o += f'out.append(buffer);' + return o + + +@register_type(18) +class SInt64Type(TypeInfo): + cpp_type = 'int64_t' + default_value = '0' + decode_varint = 'value.as_sint64()' + encode_func = 'encode_sin64' + + def dump(self): + o = f'sprintf(buffer, "%ll", {name});\n' + o += f'out.append(buffer);' + return o + + +class RepeatedTypeInfo(TypeInfo): + def __init__(self, field): + super(RepeatedTypeInfo, self).__init__(field) + self._ti = TYPE_INFO[field.type](field) + + @property + def cpp_type(self): + return f'std::vector<{self._ti.cpp_type}>' + + @property + def reference_type(self): + return f'{self.cpp_type} &' + + @property + def const_reference_type(self): + return f'const {self.cpp_type} &' + + @property + def decode_varint_content(self) -> str: + content = self._ti.decode_varint + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def decode_length_content(self) -> str: + content = self._ti.decode_length + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def decode_32bit_content(self) -> str: + content = self._ti.decode_32bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def decode_64bit_content(self) -> str: + content = self._ti.decode_64bit + if content is None: + return None + return dedent(f'''\ + case {self.number}: {{ + this->{self.field_name}.push_back({content}); + return true; + }}''') + + @property + def _ti_is_bool(self): + # std::vector is specialized for bool, reference does not work + return isinstance(self._ti, BoolType) + + @property + def encode_content(self): + return f"""\ + for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{ + buffer.{self._ti.encode_func}({self.number}, it, true); + }}""" + + @property + def dump_content(self): + o = f'for (const auto {"" if self._ti_is_bool else "&"}it : this->{self.field_name}) {{\n' + o += f' out.append(" {self.name}: ");\n' + o += indent(self._ti.dump('it')) + '\n' + o += f' out.append("\\n");\n' + o += f'}}\n' + return o + + +def build_enum_type(desc): + name = "Enum" + desc.name + out = f"enum {name} : uint32_t {{\n" + for v in desc.value: + out += f' {v.name} = {v.number},\n' + out += '};\n' + + cpp = f"template<>\n" + cpp += f"const char *proto_enum_to_string<{name}>({name} value) {{\n" + cpp += f" switch (value) {{\n" + for v in desc.value: + cpp += f' case {v.name}: return "{v.name}";\n' + cpp += f' default: return "UNKNOWN";\n' + cpp += f' }}\n' + cpp += f'}}\n' + + return out, cpp + + +def build_message_type(desc): + public_content = [] + protected_content = [] + decode_varint = [] + decode_length = [] + decode_32bit = [] + decode_64bit = [] + encode = [] + dump = [] + + for field in desc.field: + if field.label == 3: + ti = RepeatedTypeInfo(field) + else: + ti = TYPE_INFO[field.type](field) + protected_content.extend(ti.protected_content) + public_content.extend(ti.public_content) + encode.append(ti.encode_content) + + if ti.decode_varint_content: + decode_varint.append(ti.decode_varint_content) + if ti.decode_length_content: + decode_length.append(ti.decode_length_content) + if ti.decode_32bit_content: + decode_32bit.append(ti.decode_32bit_content) + if ti.decode_64bit_content: + decode_64bit.append(ti.decode_64bit_content) + if ti.dump_content: + dump.append(ti.dump_content) + + cpp = '' + if decode_varint: + decode_varint.append('default:\n return false;') + o = f'bool {desc.name}::decode_varint(uint32_t field_id, ProtoVarInt value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_varint), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_varint(uint32_t field_id, ProtoVarInt value) override;' + protected_content.insert(0, prot) + if decode_length: + decode_length.append('default:\n return false;') + o = f'bool {desc.name}::decode_length(uint32_t field_id, ProtoLengthDelimited value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_length), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;' + protected_content.insert(0, prot) + if decode_32bit: + decode_32bit.append('default:\n return false;') + o = f'bool {desc.name}::decode_32bit(uint32_t field_id, Proto32Bit value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_32bit), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_32bit(uint32_t field_id, Proto32Bit value) override;' + protected_content.insert(0, prot) + if decode_64bit: + decode_64bit.append('default:\n return false;') + o = f'bool {desc.name}::decode_64bit(uint32_t field_id, Proto64bit value) {{\n' + o += ' switch (field_id) {\n' + o += indent("\n".join(decode_64bit), ' ') + '\n' + o += ' }\n' + o += '}\n' + cpp += o + prot = 'bool decode_64bit(uint32_t field_id, Proto64bit value) override;' + protected_content.insert(0, prot) + + o = f"void {desc.name}::encode(ProtoWriteBuffer buffer) const {{\n" + o += indent('\n'.join(encode)) + '\n' + o += '}\n' + cpp += o + prot = 'void encode(ProtoWriteBuffer buffer) const override;' + public_content.append(prot) + + o = f"void {desc.name}::dump_to(std::string &out) const {{\n" + if dump: + o += f" char buffer[64];\n" + o += f' out.append("{desc.name} {{\\n");\n' + o += indent('\n'.join(dump)) + '\n' + o += f' out.append("}}");\n' + else: + o += f' out.append("{desc.name} {{}}");\n' + o += '}\n' + cpp += o + prot = 'void dump_to(std::string &out) const override;' + public_content.append(prot) + + out = f"class {desc.name} : public ProtoMessage {{\n" + out += ' public:\n' + out += indent('\n'.join(public_content)) + '\n' + out += ' protected:\n' + out += indent('\n'.join(protected_content)) + '\n' + out += "};\n" + return out, cpp + + +file = d.file[0] +content = '''\ +#pragma once + +#include "proto.h" + +namespace esphome { +namespace api { + +''' + +cpp = '''\ +#include "api_pb2.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace api { + +''' + +for enum in file.enum_type: + s, c = build_enum_type(enum) + content += s + cpp += c + +mt = file.message_type + +for m in mt: + s, c = build_message_type(m) + content += s + cpp += c + +content += '''\ + +} // namespace api +} // namespace esphome +''' +cpp += '''\ + +} // namespace api +} // namespace esphome +''' + +with open(root / 'api_pb2.h', 'w') as f: + f.write(content) + +with open(root / 'api_pb2.cpp', 'w') as f: + f.write(cpp) + +SOURCE_BOTH = 0 +SOURCE_SERVER = 1 +SOURCE_CLIENT = 2 + +RECEIVE_CASES = {} + +class_name = 'APIServerConnectionBase' + +ifdefs = {} + + +def get_opt(desc, opt, default=None): + if not desc.options.HasExtension(opt): + return default + return desc.options.Extensions[opt] + + +def build_service_message_type(mt): + snake = camel_to_snake(mt.name) + id_ = get_opt(mt, pb.id) + if id_ is None: + return None + + source = get_opt(mt, pb.source, 0) + + ifdef = get_opt(mt, pb.ifdef) + log = get_opt(mt, pb.log, True) + nodelay = get_opt(mt, pb.no_delay, False) + hout = '' + cout = '' + + if ifdef is not None: + ifdefs[str(mt.name)] = ifdef + hout += f'#ifdef {ifdef}\n' + cout += f'#ifdef {ifdef}\n' + + if source in (SOURCE_BOTH, SOURCE_SERVER): + # Generate send + func = f'send_{snake}' + hout += f'bool {func}(const {mt.name} &msg);\n' + cout += f'bool {class_name}::{func}(const {mt.name} &msg) {{\n' + if log: + cout += f' ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + cout += f' this->set_nodelay({str(nodelay).lower()});\n' + cout += f' return this->send_message_<{mt.name}>(msg, {id_});\n' + cout += f'}}\n' + if source in (SOURCE_BOTH, SOURCE_CLIENT): + # Generate receive + func = f'on_{snake}' + hout += f'virtual void {func}(const {mt.name} &value){{}};\n' + case = '' + if ifdef is not None: + case += f'#ifdef {ifdef}\n' + case += f'{mt.name} msg;\n' + case += f'msg.decode(msg_data, msg_size);\n' + if log: + case += f'ESP_LOGVV(TAG, "{func}: %s", msg.dump().c_str());\n' + case += f'this->{func}(msg);\n' + if ifdef is not None: + case += f'#endif\n' + case += 'break;' + RECEIVE_CASES[id_] = case + + if ifdef is not None: + hout += f'#endif\n' + cout += f'#endif\n' + + return hout, cout + + +hpp = '''\ +#pragma once + +#include "api_pb2.h" +#include "esphome/core/defines.h" + +namespace esphome { +namespace api { + +''' + +cpp = '''\ +#include "api_pb2_service.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace api { + +static const char *TAG = "api.service"; + +''' + +hpp += f'class {class_name} : public ProtoService {{\n' +hpp += ' public:\n' + +for mt in file.message_type: + obj = build_service_message_type(mt) + if obj is None: + continue + hout, cout = obj + hpp += indent(hout) + '\n' + cpp += cout + +cases = list(RECEIVE_CASES.items()) +cases.sort() +hpp += ' protected:\n' +hpp += f' bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;\n' +out = f'bool {class_name}::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {{\n' +out += f' switch(msg_type) {{\n' +for i, case in cases: + c = f'case {i}: {{\n' + c += indent(case) + '\n' + c += f'}}' + out += indent(c, ' ') + '\n' +out += ' default: \n' +out += ' return false;\n' +out += ' }\n' +out += ' return true;\n' +out += '}\n' +cpp += out +hpp += '};\n' + +serv = file.service[0] +class_name = 'APIServerConnection' +hpp += '\n' +hpp += f'class {class_name} : public {class_name}Base {{\n' +hpp += ' public:\n' +hpp_protected = '' +cpp += '\n' + +m = serv.method[0] +for m in serv.method: + func = m.name + inp = m.input_type[1:] + ret = m.output_type[1:] + is_void = ret == 'void' + snake = camel_to_snake(inp) + on_func = f'on_{snake}' + needs_conn = get_opt(m, pb.needs_setup_connection, True) + needs_auth = get_opt(m, pb.needs_authentication, True) + + ifdef = ifdefs.get(inp, None) + + if ifdef is not None: + hpp += f'#ifdef {ifdef}\n' + hpp_protected += f'#ifdef {ifdef}\n' + cpp += f'#ifdef {ifdef}\n' + + hpp_protected += f' void {on_func}(const {inp} &msg) override;\n' + hpp += f' virtual {ret} {func}(const {inp} &msg) = 0;\n' + cpp += f'void {class_name}::{on_func}(const {inp} &msg) {{\n' + body = '' + if needs_conn: + body += 'if (!this->is_connection_setup()) {\n' + body += ' this->on_no_setup_connection();\n' + body += ' return;\n' + body += '}\n' + if needs_auth: + body += 'if (!this->is_authenticated()) {\n' + body += ' this->on_unauthenticated_access();\n' + body += ' return;\n' + body += '}\n' + + if is_void: + body += f'this->{func}(msg);\n' + else: + body += f'{ret} ret = this->{func}(msg);\n' + ret_snake = camel_to_snake(ret) + body += f'if (!this->send_{ret_snake}(ret)) {{\n' + body += f' this->on_fatal_error();\n' + body += '}\n' + cpp += indent(body) + '\n' + '}\n' + + if ifdef is not None: + hpp += f'#endif\n' + hpp_protected += f'#endif\n' + cpp += f'#endif\n' + +hpp += ' protected:\n' +hpp += hpp_protected +hpp += '};\n' + +hpp += '''\ + +} // namespace api +} // namespace esphome +''' +cpp += '''\ + +} // namespace api +} // namespace esphome +''' + +with open(root / 'api_pb2_service.h', 'w') as f: + f.write(hpp) + +with open(root / 'api_pb2_service.cpp', 'w') as f: + f.write(cpp) + +prot.unlink() diff --git a/script/ci-custom.py b/script/ci-custom.py index 27b45dbe27..922d94f2aa 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -7,6 +7,7 @@ import fnmatch import os.path import subprocess import sys +import re def find_all(a_str, sub): @@ -28,15 +29,18 @@ EXECUTABLE_BIT = { s[3].strip(): int(s[0]) for s in lines } files = [s[3].strip() for s in lines] +files = list(filter(os.path.exists, files)) files.sort() -file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', - '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg') +file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg', + '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg', + '.woff', '.woff2', '') cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc') -ignore_types = ('.ico',) +ignore_types = ('.ico', '.woff', '.woff2', '') LINT_FILE_CHECKS = [] LINT_CONTENT_CHECKS = [] +LINT_POST_CHECKS = [] def run_check(lint_obj, fname, *args): @@ -82,6 +86,31 @@ def lint_content_check(**kwargs): return decorator +def lint_post_check(func): + _add_check(LINT_POST_CHECKS, func) + return func + + +def lint_re_check(regex, **kwargs): + prog = re.compile(regex, re.MULTILINE) + decor = lint_content_check(**kwargs) + + def decorator(func): + def new_func(fname, content): + errors = [] + for match in prog.finditer(content): + if 'NOLINT' in match.group(0): + continue + lineno = content.count("\n", 0, match.start()) + 1 + err = func(fname, match) + if err is None: + continue + errors.append("{} See line {}.".format(err, lineno)) + return errors + return decor(new_func) + return decorator + + def lint_content_find_check(find, **kwargs): decor = lint_content_check(**kwargs) @@ -90,9 +119,12 @@ def lint_content_find_check(find, **kwargs): find_ = find if callable(find): find_ = find(fname, content) + errors = [] for line, col in find_all(content, find_): err = func(fname) - return "{err} See line {line}:{col}.".format(err=err, line=line+1, col=col+1) + errors.append("{err} See line {line}:{col}." + "".format(err=err, line=line+1, col=col+1)) + return errors return decor(new_func) return decorator @@ -104,7 +136,7 @@ def lint_ino(fname): @lint_file_check(exclude=['*{}'.format(f) for f in file_types] + [ '.clang-*', '.dockerignore', '.editorconfig', '*.gitignore', 'LICENSE', 'pylintrc', - 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*' + 'MANIFEST.in', 'docker/Dockerfile*', 'docker/rootfs/*', 'script/*', ]) def lint_ext_check(fname): return "This file extension is not a registered file type. If this is an error, please " \ @@ -124,7 +156,6 @@ def lint_executable_bit(fname): @lint_content_find_check('\t', exclude=[ 'esphome/dashboard/static/ace.js', 'esphome/dashboard/static/ext-searchbox.js', - 'script/.neopixelbus.patch', ]) def lint_tabs(fname): return "File contains tab character. Please convert tabs to spaces." @@ -135,13 +166,105 @@ def lint_newline(fname): return "File contains windows newline. Please set your editor to unix newline mode." -@lint_content_check() +@lint_content_check(exclude=['*.svg']) def lint_end_newline(fname, content): if content and not content.endswith('\n'): return "File does not end with a newline, please add an empty line at the end of the file." return None +CPP_RE_EOL = r'\s*?(?://.*?)?$' + + +def highlight(s): + return '\033[36m{}\033[0m'.format(s) + + +@lint_re_check(r'^#define\s+([a-zA-Z0-9_]+)\s+([0-9bx]+)' + CPP_RE_EOL, + include=cpp_include, exclude=['esphome/core/log.h']) +def lint_no_defines(fname, match): + s = highlight('static const uint8_t {} = {};'.format(match.group(1), match.group(2))) + return ("#define macros for integer constants are not allowed, please use " + "{} style instead (replace uint8_t with the appropriate " + "datatype). See also Google style guide.".format(s)) + + +@lint_re_check(r'^\s*delay\((\d+)\);' + CPP_RE_EOL, include=cpp_include) +def lint_no_long_delays(fname, match): + duration_ms = int(match.group(1)) + if duration_ms < 50: + return None + return ( + "{} - long calls to delay() are not allowed in ESPHome because everything executes " + "in one thread. Calling delay() will block the main thread and slow down ESPHome.\n" + "If there's no way to work around the delay() and it doesn't execute often, please add " + "a '// NOLINT' comment to the line." + "".format(highlight(match.group(0).strip())) + ) + + +@lint_content_check(include=['esphome/const.py']) +def lint_const_ordered(fname, content): + lines = content.splitlines() + errors = [] + for start in ['CONF_', 'ICON_', 'UNIT_']: + matching = [(i+1, line) for i, line in enumerate(lines) if line.startswith(start)] + ordered = list(sorted(matching, key=lambda x: x[1].replace('_', ' '))) + ordered = [(mi, ol) for (mi, _), (_, ol) in zip(matching, ordered)] + for (mi, ml), (oi, ol) in zip(matching, ordered): + if ml == ol: + continue + target = next(i for i, l in ordered if l == ml) + target_text = next(l for i, l in matching if target == i) + errors.append("Constant {} is not ordered, please make sure all constants are ordered. " + "See line {} (should go to line {}, {})" + "".format(highlight(ml), mi, target, target_text)) + return errors + + +@lint_re_check(r'^\s*CONF_([A-Z_0-9a-z]+)\s+=\s+[\'"](.*?)[\'"]\s*?$', include=['*.py']) +def lint_conf_matches(fname, match): + const = match.group(1) + value = match.group(2) + const_norm = const.lower() + value_norm = value.replace('.', '_') + if const_norm == value_norm: + return None + return ("Constant {} does not match value {}! Please make sure the constant's name matches its " + "value!" + "".format(highlight('CONF_' + const), highlight(value))) + + +CONF_RE = r'^(CONF_[a-zA-Z0-9_]+)\s*=\s*[\'"].*?[\'"]\s*?$' +with codecs.open('esphome/const.py', 'r', encoding='utf-8') as f_handle: + constants_content = f_handle.read() +CONSTANTS = [m.group(1) for m in re.finditer(CONF_RE, constants_content, re.MULTILINE)] + +CONSTANTS_USES = collections.defaultdict(list) + + +@lint_re_check(CONF_RE, include=['*.py'], exclude=['esphome/const.py']) +def lint_conf_from_const_py(fname, match): + name = match.group(1) + if name not in CONSTANTS: + CONSTANTS_USES[name].append(fname) + return None + return ("Constant {} has already been defined in const.py - please import the constant from " + "const.py directly.".format(highlight(name))) + + +@lint_post_check +def lint_constants_usage(): + errors = [] + for constant, uses in CONSTANTS_USES.items(): + if len(uses) < 4: + continue + errors.append("Constant {} is defined in {} files. Please move all definitions of the " + "constant to const.py (Uses: {})" + "".format(highlight(constant), len(uses), ', '.join(uses))) + return errors + + def relative_cpp_search_text(fname, content): parts = fname.split('/') integration = parts[2] @@ -164,7 +287,8 @@ def relative_py_search_text(fname, content): return 'esphome.components.{}'.format(integration) -@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py']) +@lint_content_find_check(relative_py_search_text, include=['esphome/components/*.py'], + exclude=['esphome/components/web_server/__init__.py']) def lint_relative_py_import(fname): return ("Component contains absolute import - Components must always use " "relative imports within the integration.\n" @@ -174,6 +298,19 @@ def lint_relative_py_import(fname): ' from . import abc_ns\n\n') +@lint_content_check(include=['esphome/components/*.h', 'esphome/components/*.cpp', + 'esphome/components/*.tcc']) +def lint_namespace(fname, content): + expected_name = re.match(r'^esphome/components/([^/]+)/.*', + fname.replace(os.path.sep, '/')).group(1) + search = 'namespace {}'.format(expected_name) + if search in content: + return None + return 'Invalid namespace found in C++ file. All integration C++ files should put all ' \ + 'functions in a separate namespace that matches the integration\'s name. ' \ + 'Please make sure the file contains {}'.format(highlight(search)) + + @lint_content_find_check('"esphome.h"', include=cpp_include, exclude=['tests/custom.h']) def lint_esphome_h(fname): return ("File contains reference to 'esphome.h' - This file is " @@ -201,6 +338,7 @@ def lint_pragma_once(fname, content): 'esphome/components/stepper/stepper.h', 'esphome/components/switch/switch.h', 'esphome/components/text_sensor/text_sensor.h', + 'esphome/components/climate/climate.h', 'esphome/core/component.h', 'esphome/core/esphal.h', 'esphome/core/log.h', @@ -229,7 +367,7 @@ def add_errors(fname, errs): for fname in files: _, ext = os.path.splitext(fname) run_checks(LINT_FILE_CHECKS, fname, fname) - if ext in ('.ico',): + if ext in ignore_types: continue try: with codecs.open(fname, 'r', encoding='utf-8') as f_handle: @@ -239,6 +377,8 @@ for fname in files: continue run_checks(LINT_CONTENT_CHECKS, fname, fname, content) +run_checks(LINT_POST_CHECKS, 'POST') + for f, errs in sorted(errors.items()): print("\033[0;32m************* File \033[1;32m{}\033[0m".format(f)) for err in errs: diff --git a/script/clang-tidy b/script/clang-tidy index 39df87df22..f178e036b1 100755 --- a/script/clang-tidy +++ b/script/clang-tidy @@ -54,7 +54,6 @@ def run_tidy(args, tmpdir, queue, lock, failed_files): if rc != 0: print() print("\033[0;32m************* File \033[1;32m{}\033[0m".format(path)) - print(invocation_s) print(output) print() failed_files.append(path) diff --git a/script/lint-cpp b/script/lint-cpp index 88728c9abd..170d61d539 100755 --- a/script/lint-cpp +++ b/script/lint-cpp @@ -6,9 +6,6 @@ cd "$(dirname "$0")/.." if [[ ! -e ".gcc-flags.json" ]]; then pio init --ide atom fi -if ! patch -R -p0 -s -f --dry-run