From 6e7d25ed42175819c4ebe88fda064098449d2498 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 27 May 2019 21:51:43 +0200 Subject: [PATCH 001/222] Bump version to v1.14.0-dev --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index d285105df4..2ba461262c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -2,7 +2,7 @@ """Constants used by esphome.""" MAJOR_VERSION = 1 -MINOR_VERSION = 13 +MINOR_VERSION = 14 PATCH_VERSION = '0-dev' __short_version__ = '{}.{}'.format(MAJOR_VERSION, MINOR_VERSION) __version__ = '{}.{}'.format(__short_version__, PATCH_VERSION) From f39d45955546a7b136aa019d348e014ba0f4b92e Mon Sep 17 00:00:00 2001 From: gitolicious Date: Tue, 28 May 2019 10:19:17 +0200 Subject: [PATCH 002/222] added link from dashboard to web server, if configured (#556) * added link from dashboard to web server, if configured * linter fixes * simplified integration lookup * included loaded_integration in storage json * included loaded_integration in storage json * fixed loaded_integrations plus linter changes * fixed comment: List Co-Authored-By: Otto Winter * return empty list Co-Authored-By: Otto Winter * convert to list Co-Authored-By: Otto Winter * default to empty list on missing loaded_integrations Co-Authored-By: Otto Winter * None check no longer needed Co-Authored-By: Otto Winter * None check no longer needed Co-Authored-By: Otto Winter * removed newline --- esphome/const.py | 1 + esphome/dashboard/dashboard.py | 6 ++++++ esphome/dashboard/templates/index.html | 3 +++ esphome/storage_json.py | 11 +++++++++-- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index 2ba461262c..4c60151e72 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -201,6 +201,7 @@ CONF_LEVEL = 'level' CONF_LG = 'lg' CONF_LIBRARIES = 'libraries' CONF_LIGHT = 'light' +CONF_LOADED_INTEGRATIONS = 'loaded_integrations' CONF_LOCAL = 'local' CONF_LOGGER = 'logger' CONF_LOGS = 'logs' diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 0d6a26c6ee..14b06a3c82 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -430,6 +430,12 @@ class DashboardEntry(object): def update_new(self): return const.__version__ + @property + def loaded_integrations(self): + if self.storage is None: + return [] + return self.storage.loaded_integrations + class MainRequestHandler(BaseHandler): @authenticated diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index b7df4509cc..b912bf8467 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -67,6 +67,9 @@
{{ escape(entry.name) }} + {% if 'web_server' in entry.loaded_integrations %} + launch + {% end %} more_vert

diff --git a/esphome/storage_json.py b/esphome/storage_json.py index 4d789b969c..b04f056f11 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -37,7 +37,7 @@ def trash_storage_path(base_path): # type: (str) -> str class StorageJSON(object): def __init__(self, storage_version, name, esphome_version, src_version, arduino_version, address, esp_platform, board, build_path, - firmware_bin_path): + firmware_bin_path, loaded_integrations): # Version of the storage JSON schema assert storage_version is None or isinstance(storage_version, int) self.storage_version = storage_version # type: int @@ -61,6 +61,9 @@ class StorageJSON(object): self.build_path = build_path # type: str # The absolute path to the firmware binary self.firmware_bin_path = firmware_bin_path # type: str + # A list of strings of names of loaded integrations + self.loaded_integrations = loaded_integrations # type: List[str] + self.loaded_integrations.sort() def as_dict(self): return { @@ -74,6 +77,7 @@ class StorageJSON(object): 'board': self.board, 'build_path': self.build_path, 'firmware_bin_path': self.firmware_bin_path, + 'loaded_integrations': self.loaded_integrations, } def to_json(self): @@ -97,6 +101,7 @@ class StorageJSON(object): board=esph.board, build_path=esph.build_path, firmware_bin_path=esph.firmware_bin, + loaded_integrations=list(esph.loaded_integrations), ) @staticmethod @@ -113,6 +118,7 @@ class StorageJSON(object): board=board, build_path=None, firmware_bin_path=None, + loaded_integrations=[], ) @staticmethod @@ -130,9 +136,10 @@ class StorageJSON(object): board = storage.get('board') 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, src_version, arduino_version, address, esp_platform, board, build_path, - firmware_bin_path) + firmware_bin_path, loaded_integrations) @staticmethod def load(path): # type: (str) -> Optional[StorageJSON] From a23ebead687ea84597a5f407164c2c77f040f36b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 28 May 2019 10:23:15 +0200 Subject: [PATCH 003/222] Update gitlab CI script, add cpp lint --- .gitlab-ci.yml | 184 ++++++++++++++++++++--------------------- docker/Dockerfile.lint | 20 ++++- requirements_test.txt | 1 + script/lint-python | 29 ++++--- 4 files changed, 127 insertions(+), 107 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 94116bcee1..5b247e386f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,8 @@ variables: DOCKER_DRIVER: overlay2 DOCKER_HOST: tcp://docker:2375/ + BASE_VERSION: '1.5.1' + TZ: UTC stages: - lint @@ -10,23 +12,20 @@ stages: - deploy .lint: &lint - image: esphome/esphome-base-amd64 + image: esphome/esphome-lint:latest stage: lint before_script: - - pip install -e . - - pip install flake8==3.6.0 pylint==1.9.4 pillow + - script/setup tags: - docker .test: &test - image: esphome/esphome-base-amd64 + image: esphome/esphome-base-amd64:${BASE_VERSION} stage: test before_script: - - pip install -e . + - script/setup tags: - docker - variables: - TZ: UTC .docker-base: &docker-base image: esphome/esphome-base-builder @@ -41,11 +40,11 @@ stages: - | if [[ "${IS_HASSIO}" == "YES" ]]; then - BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:1.5.1 + BUILD_FROM=esphome/esphome-hassio-base-${BUILD_ARCH}:${BASE_VERSION} BUILD_TO=esphome/esphome-hassio-${BUILD_ARCH} DOCKERFILE=docker/Dockerfile.hassio else - BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:1.5.1 + BUILD_FROM=esphome/esphome-base-${BUILD_ARCH}:${BASE_VERSION} if [[ "${BUILD_ARCH}" == "amd64" ]]; then BUILD_TO=esphome/esphome else @@ -94,15 +93,32 @@ stages: - docker stage: deploy -flake8: +lint-custom: <<: *lint script: - - flake8 esphome + - script/ci-custom.py -pylint: +lint-python: <<: *lint script: - - pylint esphome + - script/lint-python + +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("")); + } + + stream->print(F("

WiFi Settings







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

OTA Update

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

WiFi Networks

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

WiFi Settings

+
+
+
+
+ +
+

+
+ +

OTA Update

+
+ + +
+
+ + diff --git a/esphome/components/captive_portal/lock.svg b/esphome/components/captive_portal/lock.svg new file mode 100644 index 0000000000..743a1cc55a --- /dev/null +++ b/esphome/components/captive_portal/lock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/stylesheet.css b/esphome/components/captive_portal/stylesheet.css new file mode 100644 index 0000000000..73f82f05f1 --- /dev/null +++ b/esphome/components/captive_portal/stylesheet.css @@ -0,0 +1,58 @@ +* { + box-sizing: inherit; +} + +div, input { + padding: 5px; + font-size: 1em; +} + +input { + width: 95%; +} + +body { + text-align: center; + font-family: sans-serif; +} + +button { + border: 0; + border-radius: 0.3rem; + background-color: #1fa3ec; + color: #fff; + line-height: 2.4rem; + font-size: 1.2rem; + width: 100%; + padding: 0; +} + +.main { + text-align: left; + display: inline-block; + min-width: 260px; +} + +.network { + display: flex; + justify-content: space-between; + align-items: center; +} + +.network-left { + display: flex; + align-items: center; +} + +.network-ssid { + margin-bottom: -7px; + margin-left: 10px; +} + +.info { + border: 1px solid; + margin: 10px 0px; + padding: 15px 10px; + color: #4f8a10; + background-color: #dff2bf; +} diff --git a/esphome/components/captive_portal/wifi-strength-1.svg b/esphome/components/captive_portal/wifi-strength-1.svg new file mode 100644 index 0000000000..189a38193c --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-2.svg b/esphome/components/captive_portal/wifi-strength-2.svg new file mode 100644 index 0000000000..9b4b2d2396 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-3.svg b/esphome/components/captive_portal/wifi-strength-3.svg new file mode 100644 index 0000000000..44b7532bb7 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-3.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/captive_portal/wifi-strength-4.svg b/esphome/components/captive_portal/wifi-strength-4.svg new file mode 100644 index 0000000000..a22b0b8281 --- /dev/null +++ b/esphome/components/captive_portal/wifi-strength-4.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index c93392418c..68a303f23d 100644 --- a/esphome/components/light/addressable_light.cpp +++ b/esphome/components/light/addressable_light.cpp @@ -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 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/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..7a00c5bb41 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -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/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 81524826be..96722229b1 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(); diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 206fc2c733..ea7b179d1e 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -1,10 +1,11 @@ import esphome.codegen as cg import esphome.config_validation as cv +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 -from esphome.core import CORE, coroutine_with_priority +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 +15,18 @@ 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.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') diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 882af4b995..fe36d6c2ce 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -6,13 +6,6 @@ #include "StreamString.h" -#ifdef ARDUINO_ARCH_ESP32 -#include -#endif -#ifdef ARDUINO_ARCH_ESP8266 -#include -#endif - #include #include @@ -66,7 +59,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 +107,18 @@ 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()); } 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 +168,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

"
                   "
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 7033c9721d..9827248480 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -32,9 +32,10 @@ files = list(filter(os.path.exists, files))
 files.sort()
 
 file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg',
-              '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg')
+              '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg',
+              '.eot', '.ttf', '.woff', '.woff2')
 cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc')
-ignore_types = ('.ico',)
+ignore_types = ('.ico', '.eot', '.ttf', '.woff', '.woff2')
 
 LINT_FILE_CHECKS = []
 LINT_CONTENT_CHECKS = []
@@ -231,7 +232,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:

From 655327a8b19cd35d66a54a08ddcd1a31906ebcd2 Mon Sep 17 00:00:00 2001
From: Jack Wozny 
Date: Tue, 27 Aug 2019 12:19:55 -0500
Subject: [PATCH 127/222] Corrected ESP32 hardware UART pins (#701)

The UART pins for Serial1 and Serial2 on the ESP32 were reversed.
---
 esphome/components/uart/uart.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp
index 56661b8aa7..3a139bd1d5 100644
--- a/esphome/components/uart/uart.cpp
+++ b/esphome/components/uart/uart.cpp
@@ -20,9 +20,9 @@ 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) {
+  } else if (this->tx_pin_.value_or(10) == 10 && this->rx_pin_.value_or(9) == 9) {
     this->hw_serial_ = &Serial1;
-  } else if (this->tx_pin_.value_or(16) == 16 && this->rx_pin_.value_or(17) == 17) {
+  } else if (this->tx_pin_.value_or(17) == 17 && this->rx_pin_.value_or(16) == 16) {
     this->hw_serial_ = &Serial2;
   } else {
     this->hw_serial_ = new HardwareSerial(next_uart_num++);

From 071272a27f10aefe18f79f419efe6c15e31bcd94 Mon Sep 17 00:00:00 2001
From: Pauline Middelink 
Date: Tue, 27 Aug 2019 19:28:50 +0200
Subject: [PATCH 128/222] Fix mqtt_text_sensor to honor unique_id when set.
 (#698)

* Fix mqtt_text_sensor to honor unique_id when set.
* Remove setting of unique_id in json tree, as the mqtt_component already does this, and in fact overrides what we do here.
* Add unqiue_id() and dump_config() to the wifi_info sensors.
---
 esphome/components/mqtt/mqtt_text_sensor.cpp         | 4 +---
 esphome/components/mqtt/mqtt_text_sensor.h           | 2 ++
 esphome/components/wifi_info/wifi_info_text_sensor.h | 8 ++++++++
 3 files changed, 11 insertions(+), 3 deletions(-)

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/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h
index 13e632bde1..258c185c53 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.h
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.h
@@ -7,6 +7,8 @@
 namespace esphome {
 namespace wifi_info {
 
+static const char TAG[] = "wifi_info.text_sensor";
+
 class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
  public:
   void loop() override {
@@ -16,7 +18,9 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
       this->publish_state(ip.toString().c_str());
     }
   }
+  void dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo IPAddress Text Sensor", this); }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+  std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; }
 
  protected:
   IPAddress last_ip_;
@@ -31,7 +35,9 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor {
       this->publish_state(this->last_ssid_);
     }
   }
+  void dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo SSDID Text Sensor", this); }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+  std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; }
 
  protected:
   std::string last_ssid_;
@@ -48,7 +54,9 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
       this->publish_state(buf);
     }
   }
+  void dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo BSSID Text Sensor", this); }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
+  std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; }
 
  protected:
   wifi::bssid_t last_bssid_;

From 15a7d2ef75a8ad75560cd34984a7c870a94154f6 Mon Sep 17 00:00:00 2001
From: Pauline Middelink 
Date: Tue, 27 Aug 2019 19:30:13 +0200
Subject: [PATCH 129/222] The display component should not be handling
 update_interval, (#693)

as that is already done when registering the component.
---
 esphome/components/display/__init__.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

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:

From c5db457700eb94d6e7f5a878d08703c4e825cd1f Mon Sep 17 00:00:00 2001
From: Nikolay Vasilchuk 
Date: Tue, 27 Aug 2019 20:33:25 +0300
Subject: [PATCH 130/222] MH-Z19 calibration support (#683)

* Allow configuration to enable or disable automatic baseline calibration on boot
* Add actions to enable or disable automatic baseline calibration
* Add action to calibrate zero point
---
 esphome/components/mhz19/mhz19.cpp | 32 ++++++++++++++++++++++++++
 esphome/components/mhz19/mhz19.h   | 37 ++++++++++++++++++++++++++++++
 esphome/components/mhz19/sensor.py | 27 ++++++++++++++++++++++
 tests/test1.yaml                   |  1 +
 4 files changed, 97 insertions(+)

diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp
index 8f46e288b6..cbac28f9c7 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,6 +61,21 @@ 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();
   this->write_array(command, MHZ19_REQUEST_LENGTH);
@@ -67,6 +93,12 @@ void MHZ19Component::dump_config() {
   ESP_LOGCONFIG(TAG, "MH-Z19:");
   LOG_SENSOR("  ", "CO2", this->co2_sensor_);
   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
+
+  if (this->abc_boot_logic_ == MHZ19_ABC_ENABLED) {
+    ESP_LOGCONFIG(TAG, "  Automatic baseline calibration enabled on boot");
+  } else if (this->abc_boot_logic_ == MHZ19_ABC_DISABLED) {
+    ESP_LOGCONFIG(TAG, "  Automatic baseline calibration disabled on boot");
+  }
 }
 
 }  // namespace mhz19
diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h
index 3604628afc..2201fc87f0 100644
--- a/esphome/components/mhz19/mhz19.h
+++ b/esphome/components/mhz19/mhz19.h
@@ -1,27 +1,64 @@
 #pragma once
 
 #include "esphome/core/component.h"
+#include "esphome/core/automation.h"
 #include "esphome/components/sensor/sensor.h"
 #include "esphome/components/uart/uart.h"
 
 namespace esphome {
 namespace mhz19 {
 
+enum MHZ19ABCLogic { MHZ19_ABC_NONE = 0, MHZ19_ABC_ENABLED, MHZ19_ABC_DISABLED };
+
 class MHZ19Component : public PollingComponent, public uart::UARTDevice {
  public:
   float get_setup_priority() const override;
 
+  void setup() override;
   void update() override;
   void dump_config() override;
 
+  void calibrate_zero();
+  void abc_enable();
+  void abc_disable();
+
   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
   void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; }
+  void set_abc_enabled(bool abc_enabled) { abc_boot_logic_ = abc_enabled ? MHZ19_ABC_ENABLED : MHZ19_ABC_DISABLED; }
 
  protected:
   bool mhz19_write_command_(const uint8_t *command, uint8_t *response);
 
   sensor::Sensor *temperature_sensor_{nullptr};
   sensor::Sensor *co2_sensor_{nullptr};
+  MHZ19ABCLogic abc_boot_logic_{MHZ19_ABC_NONE};
+};
+
+template class MHZ19CalibrateZeroAction : public Action {
+ public:
+  MHZ19CalibrateZeroAction(MHZ19Component *mhz19) : mhz19_(mhz19) {}
+  void play(Ts... x) override { this->mhz19_->calibrate_zero(); }
+
+ protected:
+  MHZ19Component *mhz19_;
+};
+
+template class MHZ19ABCEnableAction : public Action {
+ public:
+  MHZ19ABCEnableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {}
+  void play(Ts... x) override { this->mhz19_->abc_enable(); }
+
+ protected:
+  MHZ19Component *mhz19_;
+};
+
+template class MHZ19ABCDisableAction : public Action {
+ public:
+  MHZ19ABCDisableAction(MHZ19Component *mhz19) : mhz19_(mhz19) {}
+  void play(Ts... x) override { this->mhz19_->abc_disable(); }
+
+ protected:
+  MHZ19Component *mhz19_;
 };
 
 }  // namespace mhz19
diff --git a/esphome/components/mhz19/sensor.py b/esphome/components/mhz19/sensor.py
index 368426e6f7..bdcecf12cb 100644
--- a/esphome/components/mhz19/sensor.py
+++ b/esphome/components/mhz19/sensor.py
@@ -1,18 +1,26 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
+from esphome import automation
+from esphome.automation import maybe_simple_id
 from esphome.components import sensor, uart
 from esphome.const import CONF_CO2, CONF_ID, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \
     UNIT_PARTS_PER_MILLION, UNIT_CELSIUS, ICON_THERMOMETER
 
 DEPENDENCIES = ['uart']
 
+CONF_AUTOMATIC_BASELINE_CALIBRATION = 'automatic_baseline_calibration'
+
 mhz19_ns = cg.esphome_ns.namespace('mhz19')
 MHZ19Component = mhz19_ns.class_('MHZ19Component', cg.PollingComponent, uart.UARTDevice)
+MHZ19CalibrateZeroAction = mhz19_ns.class_('MHZ19CalibrateZeroAction', automation.Action)
+MHZ19ABCEnableAction = mhz19_ns.class_('MHZ19ABCEnableAction', automation.Action)
+MHZ19ABCDisableAction = mhz19_ns.class_('MHZ19ABCDisableAction', automation.Action)
 
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(): cv.declare_id(MHZ19Component),
     cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0),
     cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 0),
+    cv.Optional(CONF_AUTOMATIC_BASELINE_CALIBRATION): cv.boolean,
 }).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA)
 
 
@@ -28,3 +36,22 @@ def to_code(config):
     if CONF_TEMPERATURE in config:
         sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
         cg.add(var.set_temperature_sensor(sens))
+
+    if CONF_AUTOMATIC_BASELINE_CALIBRATION in config:
+        cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION]))
+
+
+CALIBRATION_ACTION_SCHEMA = maybe_simple_id({
+    cv.Required(CONF_ID): cv.use_id(MHZ19Component),
+})
+
+
+@automation.register_action('mhz19.calibrate_zero', MHZ19CalibrateZeroAction,
+                            CALIBRATION_ACTION_SCHEMA)
+@automation.register_action('mhz19.abc_enable', MHZ19ABCEnableAction,
+                            CALIBRATION_ACTION_SCHEMA)
+@automation.register_action('mhz19.abc_disable', MHZ19ABCDisableAction,
+                            CALIBRATION_ACTION_SCHEMA)
+def mhz19_calibration_to_code(config, action_id, template_arg, args):
+    paren = yield cg.get_variable(config[CONF_ID])
+    yield cg.new_Pvariable(action_id, template_arg, paren)
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 8fdcdd57ea..d1329620dc 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -449,6 +449,7 @@ sensor:
     temperature:
       name: "MH-Z19 Temperature"
     update_interval: 15s
+    automatic_baseline_calibration: false
   - platform: mpu6050
     address: 0x68
     accel_x:

From 0fc267dfc734c16868c8801d4e85782cc4e19f1f Mon Sep 17 00:00:00 2001
From: Jasper van der Neut - Stulen 
Date: Tue, 27 Aug 2019 19:39:04 +0200
Subject: [PATCH 131/222] Implement median filter (#697)

Add median filter to sensors component
---
 esphome/components/sensor/__init__.py | 14 ++++++++++
 esphome/components/sensor/filter.cpp  | 38 +++++++++++++++++++++++++++
 esphome/components/sensor/filter.h    | 30 +++++++++++++++++++++
 tests/test1.yaml                      |  4 +++
 4 files changed, 86 insertions(+)

diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py
index c006dfad57..5211322615 100644
--- a/esphome/components/sensor/__init__.py
+++ b/esphome/components/sensor/__init__.py
@@ -61,6 +61,7 @@ SensorPublishAction = sensor_ns.class_('SensorPublishAction', automation.Action)
 
 # Filters
 Filter = sensor_ns.class_('Filter')
+MedianFilter = sensor_ns.class_('MedianFilter', Filter)
 SlidingWindowMovingAverageFilter = sensor_ns.class_('SlidingWindowMovingAverageFilter', Filter)
 ExponentialMovingAverageFilter = sensor_ns.class_('ExponentialMovingAverageFilter', Filter)
 LambdaFilter = sensor_ns.class_('LambdaFilter', Filter)
@@ -127,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,
diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp
index 79323bd8ab..3f0d39fcfc 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)
diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h
index 17a583e40e..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
diff --git a/tests/test1.yaml b/tests/test1.yaml
index d1329620dc..729ea2b2bf 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -191,6 +191,10 @@ sensor:
         - 100.0 -> 102.5
       - filter_out: 42.0
       - filter_out: nan
+      - median:
+          window_size: 5
+          send_every: 5
+          send_first_at: 3
       - sliding_window_moving_average:
           window_size: 15
           send_every: 15

From 54c9dd417395d0f5ab634b2a297286b71fcb39c2 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 20:07:48 +0200
Subject: [PATCH 132/222] Fix WiFi Info dump_config change

Fixes https://github.com/esphome/esphome/pull/698#discussion_r318212018
---
 .../components/wifi_info/wifi_info_text_sensor.cpp | 14 ++++++++++++++
 .../components/wifi_info/wifi_info_text_sensor.h   |  3 ---
 2 files changed, 14 insertions(+), 3 deletions(-)
 create mode 100644 esphome/components/wifi_info/wifi_info_text_sensor.cpp

diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
new file mode 100644
index 0000000000..283b876b5d
--- /dev/null
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
@@ -0,0 +1,14 @@
+#include "wifi_info_text_sensor.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace wifi_info_text_sensor {
+
+static const char *TAG = "wifi_info";
+
+void IPAddressWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); }
+void SSIDWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
+void BSSIDWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
+
+}  // namespace wifi_info_text_sensor
+}  // namespace esphome
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h
index 258c185c53..73ea8511a3 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.h
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.h
@@ -18,7 +18,6 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
       this->publish_state(ip.toString().c_str());
     }
   }
-  void dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo IPAddress Text Sensor", this); }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; }
 
@@ -35,7 +34,6 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor {
       this->publish_state(this->last_ssid_);
     }
   }
-  void dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo SSDID Text Sensor", this); }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; }
 
@@ -54,7 +52,6 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
       this->publish_state(buf);
     }
   }
-  void dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo BSSID Text Sensor", this); }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; }
 

From 22f9f759145fefce66cba07957e84d16078176f9 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 20:13:50 +0200
Subject: [PATCH 133/222] Remove ESP32 uart pin entries

See also https://github.com/esphome/esphome/commit/655327a8b19cd35d66a54a08ddcd1a31906ebcd2
---
 esphome/components/uart/uart.cpp | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp
index 3a139bd1d5..5e0bc3277e 100644
--- a/esphome/components/uart/uart.cpp
+++ b/esphome/components/uart/uart.cpp
@@ -20,10 +20,6 @@ void UARTComponent::setup() {
   // is 1 we still want to use Serial.
   if (this->tx_pin_.value_or(1) == 1 && this->rx_pin_.value_or(3) == 3) {
     this->hw_serial_ = &Serial;
-  } else if (this->tx_pin_.value_or(10) == 10 && this->rx_pin_.value_or(9) == 9) {
-    this->hw_serial_ = &Serial1;
-  } else if (this->tx_pin_.value_or(17) == 17 && this->rx_pin_.value_or(16) == 16) {
-    this->hw_serial_ = &Serial2;
   } else {
     this->hw_serial_ = new HardwareSerial(next_uart_num++);
   }

From 9770bc371b076a02678bea2ce75d7c6c353e52ce Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 20:27:56 +0200
Subject: [PATCH 134/222] Remove duplicate TAG value

---
 esphome/components/wifi_info/wifi_info_text_sensor.h | 2 --
 1 file changed, 2 deletions(-)

diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h
index 73ea8511a3..47aa68ea0f 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.h
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.h
@@ -7,8 +7,6 @@
 namespace esphome {
 namespace wifi_info {
 
-static const char TAG[] = "wifi_info.text_sensor";
-
 class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
  public:
   void loop() override {

From 65d08beaa4ca4fe9ab19a1e108f6af0eb45941ab Mon Sep 17 00:00:00 2001
From: junnikokuki 
Date: Wed, 28 Aug 2019 03:06:39 +0800
Subject: [PATCH 135/222] add xiaomi BLE Thermometer lywsd02 model support
 (#664)

* add xiaomi BLE Thermometer lywsd02 model support

* remove battery level

* Update sensor.py

to pass the lint test
https://github.com/esphome/esphome/pull/664

* fix trailing space


Co-authored-by: Guoxue 
Co-authored-by: mr G1K 
---
 esphome/components/xiaomi_ble/xiaomi_ble.cpp  | 17 +++++--
 esphome/components/xiaomi_ble/xiaomi_ble.h    |  2 +-
 esphome/components/xiaomi_lywsd02/__init__.py |  0
 esphome/components/xiaomi_lywsd02/sensor.py   | 34 ++++++++++++++
 .../xiaomi_lywsd02/xiaomi_lywsd02.cpp         | 20 ++++++++
 .../xiaomi_lywsd02/xiaomi_lywsd02.h           | 46 +++++++++++++++++++
 6 files changed, 115 insertions(+), 4 deletions(-)
 create mode 100644 esphome/components/xiaomi_lywsd02/__init__.py
 create mode 100644 esphome/components/xiaomi_lywsd02/sensor.py
 create mode 100644 esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
 create mode 100644 esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h

diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
index 4367b5fd1e..5bb6709e5f 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp
@@ -83,8 +83,9 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d
 
   bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01;
   bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00;
+  bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04;
 
-  if (!is_mijia && !is_miflora) {
+  if (!is_mijia && !is_miflora && !is_lywsd02) {
     // ESP_LOGVV(TAG, "Xiaomi no magic bytes");
     return {};
   }
@@ -101,7 +102,12 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d
     return {};
   }
   XiaomiParseResult result;
-  result.type = is_miflora ? XiaomiParseResult::TYPE_MIFLORA : XiaomiParseResult::TYPE_MIJIA;
+  result.type = XiaomiParseResult::TYPE_MIFLORA;
+  if (is_mijia) {
+    result.type = XiaomiParseResult::TYPE_MIJIA;
+  } else if (is_lywsd02) {
+    result.type = XiaomiParseResult::TYPE_LYWSD02;
+  }
   bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result);
   if (!success)
     return {};
@@ -113,7 +119,12 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
   if (!res.has_value())
     return false;
 
-  const char *name = res->type == XiaomiParseResult::TYPE_MIFLORA ? "Mi Flora" : "Mi Jia";
+  const char *name = "Mi Flora";
+  if (res->type == XiaomiParseResult::TYPE_MIJIA) {
+    name = "Mi Jia";
+  } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) {
+    name = "LYWSD02";
+  }
 
   ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str());
 
diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h
index 058a89927b..b8b602ecef 100644
--- a/esphome/components/xiaomi_ble/xiaomi_ble.h
+++ b/esphome/components/xiaomi_ble/xiaomi_ble.h
@@ -9,7 +9,7 @@ namespace esphome {
 namespace xiaomi_ble {
 
 struct XiaomiParseResult {
-  enum { TYPE_MIJIA, TYPE_MIFLORA } type;
+  enum { TYPE_MIJIA, TYPE_MIFLORA, TYPE_LYWSD02 } type;
   optional temperature;
   optional humidity;
   optional battery_level;
diff --git a/esphome/components/xiaomi_lywsd02/__init__.py b/esphome/components/xiaomi_lywsd02/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/esphome/components/xiaomi_lywsd02/sensor.py b/esphome/components/xiaomi_lywsd02/sensor.py
new file mode 100644
index 0000000000..8e4d59316b
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd02/sensor.py
@@ -0,0 +1,34 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor, esp32_ble_tracker
+from esphome.const import CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \
+    UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, CONF_ID
+
+DEPENDENCIES = ['esp32_ble_tracker']
+AUTO_LOAD = ['xiaomi_ble']
+
+xiaomi_lywsd02_ns = cg.esphome_ns.namespace('xiaomi_lywsd02')
+XiaomiLYWSD02 = xiaomi_lywsd02_ns.class_('XiaomiLYWSD02', esp32_ble_tracker.ESPBTDeviceListener,
+                                         cg.Component)
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(XiaomiLYWSD02),
+    cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
+    cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield esp32_ble_tracker.register_ble_device(var, config)
+
+    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature(sens))
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity(sens))
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
new file mode 100644
index 0000000000..cd77c133a5
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.cpp
@@ -0,0 +1,20 @@
+#include "xiaomi_lywsd02.h"
+#include "esphome/core/log.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsd02 {
+
+static const char *TAG = "xiaomi_lywsd02";
+
+void XiaomiLYWSD02::dump_config() {
+  ESP_LOGCONFIG(TAG, "Xiaomi LYWSD02");
+  LOG_SENSOR("  ", "Temperature", this->temperature_);
+  LOG_SENSOR("  ", "Humidity", this->humidity_);
+}
+
+}  // namespace xiaomi_lywsd02
+}  // namespace esphome
+
+#endif
diff --git a/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
new file mode 100644
index 0000000000..9b8aba1bb0
--- /dev/null
+++ b/esphome/components/xiaomi_lywsd02/xiaomi_lywsd02.h
@@ -0,0 +1,46 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
+#include "esphome/components/xiaomi_ble/xiaomi_ble.h"
+
+#ifdef ARDUINO_ARCH_ESP32
+
+namespace esphome {
+namespace xiaomi_lywsd02 {
+
+class XiaomiLYWSD02 : public Component, public esp32_ble_tracker::ESPBTDeviceListener {
+ public:
+  void set_address(uint64_t address) { address_ = address; }
+
+  bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
+    if (device.address_uint64() != this->address_)
+      return false;
+
+    auto res = xiaomi_ble::parse_xiaomi(device);
+    if (!res.has_value())
+      return false;
+
+    if (res->temperature.has_value() && this->temperature_ != nullptr)
+      this->temperature_->publish_state(*res->temperature);
+    if (res->humidity.has_value() && this->humidity_ != nullptr)
+      this->humidity_->publish_state(*res->humidity);
+    return true;
+  }
+
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
+  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
+
+ protected:
+  uint64_t address_;
+  sensor::Sensor *temperature_{nullptr};
+  sensor::Sensor *humidity_{nullptr};
+};
+
+}  // namespace xiaomi_lywsd02
+}  // namespace esphome
+
+#endif

From 947a6034e3d29647bbddf4dd2df1fa0a4aa9574e Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 21:33:01 +0200
Subject: [PATCH 136/222] Update platformio patch for latest change

See also https://github.com/platformio/platformio-core/commit/8059e04499fd4d54195c3d75b74a2804aadb6be8
---
 esphome/platformio_api.py | 20 ++------------------
 1 file changed, 2 insertions(+), 18 deletions(-)

diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py
index f113d3067a..a2626c604e 100644
--- a/esphome/platformio_api.py
+++ b/esphome/platformio_api.py
@@ -29,11 +29,10 @@ def patch_structhash():
         from platformio.project.helpers import get_project_dir
     else:
         from platformio.util import get_project_dir
-    from os.path import join, isdir, getmtime, isfile
+    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):
         platformio_ini = join(get_project_dir(), "platformio.ini")
 
         # if project's config is modified
@@ -43,21 +42,6 @@ def patch_structhash():
         if not isdir(build_dir):
             makedirs(build_dir)
 
-        if is_platformio4():
-            from platformio.project import helpers
-            proj_hash = helpers.calculate_project_hash()
-        else:
-            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
     if is_platformio4():
         run.helpers.clean_build_dir = patched_clean_build_dir

From 5348b36a7c58e63462a789fef921681958c07ea1 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 21:51:59 +0200
Subject: [PATCH 137/222] Fix warnings about comments in lambdas

Fixes https://github.com/esphome/issues/issues/593
---
 esphome/__main__.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/esphome/__main__.py b/esphome/__main__.py
index 05de49e8d0..98d302c82b 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -130,6 +130,7 @@ def wrap_to_code(name, comp):
             conf_str = yaml_util.dump(conf)
             if IS_PY2:
                 conf_str = conf_str.decode('utf-8')
+            conf_str = conf_str.replace('//', '')
             cg.add(cg.LineComment(indent(conf_str)))
         yield coro(conf)
 

From ccf3da2a5aab0be231ce3b0b948eeeab15a835db Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 22:00:34 +0200
Subject: [PATCH 138/222] Improve handling of no upload option

Fixes https://github.com/esphome/issues/issues/596
---
 esphome/__main__.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/__main__.py b/esphome/__main__.py
index 98d302c82b..bb41e43011 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -35,7 +35,9 @@ def get_serial_ports():
 
 def choose_prompt(options):
     if not options:
-        raise ValueError
+        raise EsphomeError("Found no valid options for upload/logging, please make sure relevant "
+                           "sections (ota, mqtt, ...) are in your configuration and/or the device "
+                           "is plugged in.")
 
     if len(options) == 1:
         return options[0][1]

From 2822fa4a40de029ca49d9ba3621597a03dba6b57 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 22:10:23 +0200
Subject: [PATCH 139/222] Also scan for symlinks in comports

Fixes https://github.com/esphome/feature-requests/issues/56
---
 esphome/__main__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/__main__.py b/esphome/__main__.py
index bb41e43011..bb9f789600 100644
--- a/esphome/__main__.py
+++ b/esphome/__main__.py
@@ -24,7 +24,7 @@ def get_serial_ports():
     # from https://github.com/pyserial/pyserial/blob/master/serial/tools/list_ports.py
     from serial.tools.list_ports import comports
     result = []
-    for port, desc, info in comports():
+    for port, desc, info in comports(include_links=True):
         if not port:
             continue
         if "VID:PID" in info:

From 2c995cf145595c5d14f480eeb08b8dd952560b89 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Tue, 27 Aug 2019 22:11:50 +0200
Subject: [PATCH 140/222] Fix GPS time source. (#704)

* Change ESP32 default power_save_mode to light

* Update
---
 esphome/components/gps/gps.cpp              | 36 +++++++++++++++++++++
 esphome/components/gps/gps.h                |  9 +-----
 esphome/components/gps/time/gps_time.cpp    | 24 ++++++++++++++
 esphome/components/gps/time/gps_time.h      | 15 +--------
 esphome/components/time/__init__.py         |  5 +--
 esphome/components/time/real_time_clock.cpp | 12 +++----
 esphome/components/uart/uart.cpp            |  4 +--
 esphome/components/uart/uart.h              |  6 ++--
 8 files changed, 76 insertions(+), 35 deletions(-)

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/time/__init__.py b/esphome/components/time/__init__.py
index 2097be3a26..ca1ac375ba 100644
--- a/esphome/components/time/__init__.py
+++ b/esphome/components/time/__init__.py
@@ -115,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 96722229b1..cb66dc3ce6 100644
--- a/esphome/components/time/real_time_clock.cpp
+++ b/esphome/components/time/real_time_clock.cpp
@@ -84,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() {
@@ -127,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/uart/uart.cpp b/esphome/components/uart/uart.cpp
index 5e0bc3277e..ea15af5053 100644
--- a/esphome/components/uart/uart.cpp
+++ b/esphome/components/uart/uart.cpp
@@ -291,12 +291,12 @@ void ICACHE_RAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
   this->write_bit_(true, &wait, start);
   enable_interrupts();
 }
-void ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) {
+void ICACHE_RAM_ATTR ESP8266SoftwareSerial::wait_(uint32_t *wait, const uint32_t &start) {
   while (ESP.getCycleCount() - start < *wait)
     ;
   *wait += this->bit_time_;
 }
-bool ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) {
+bool ICACHE_RAM_ATTR ESP8266SoftwareSerial::read_bit_(uint32_t *wait, const uint32_t &start) {
   this->wait_(wait, start);
   return this->rx_pin_->digital_read();
 }
diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h
index f642d4ee81..93caaf3006 100644
--- a/esphome/components/uart/uart.h
+++ b/esphome/components/uart/uart.h
@@ -24,9 +24,9 @@ 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};

From d27291b997a230f44e0c983d757f6083b5d50678 Mon Sep 17 00:00:00 2001
From: Nikolay Vasilchuk 
Date: Thu, 29 Aug 2019 16:42:31 +0300
Subject: [PATCH 141/222] License for Material Design Icons (#708)

---
 esphome/dashboard/static/fonts/LICENSE        | 202 ++++++++++++++++++
 .../static/fonts/MaterialIcons-Regular.eot    | Bin 143258 -> 0 bytes
 .../static/fonts/MaterialIcons-Regular.ttf    | Bin 128180 -> 0 bytes
 .../dashboard/static/fonts/material-icons.css |   4 +-
 script/ci-custom.py                           |   4 +-
 5 files changed, 205 insertions(+), 5 deletions(-)
 create mode 100644 esphome/dashboard/static/fonts/LICENSE
 delete mode 100644 esphome/dashboard/static/fonts/MaterialIcons-Regular.eot
 delete mode 100644 esphome/dashboard/static/fonts/MaterialIcons-Regular.ttf

diff --git a/esphome/dashboard/static/fonts/LICENSE b/esphome/dashboard/static/fonts/LICENSE
new file mode 100644
index 0000000000..7a4a3ea242
--- /dev/null
+++ b/esphome/dashboard/static/fonts/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
\ No newline at end of file
diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.eot b/esphome/dashboard/static/fonts/MaterialIcons-Regular.eot
deleted file mode 100644
index 70508ebabc9992e64f1314f866b2d7ab90438c58..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 143258
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�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
Tz+(f93=#~WQ{2=T)EUG9`1`~4

diff --git a/esphome/dashboard/static/fonts/material-icons.css b/esphome/dashboard/static/fonts/material-icons.css
index 2270c09d01..51f2e0a0d1 100644
--- a/esphome/dashboard/static/fonts/material-icons.css
+++ b/esphome/dashboard/static/fonts/material-icons.css
@@ -2,12 +2,10 @@
   font-family: 'Material Icons';
   font-style: normal;
   font-weight: 400;
-  src: url(MaterialIcons-Regular.eot); /* For IE6-8 */
   src: local('Material Icons'),
        local('MaterialIcons-Regular'),
        url(MaterialIcons-Regular.woff2) format('woff2'),
-       url(MaterialIcons-Regular.woff) format('woff'),
-       url(MaterialIcons-Regular.ttf) format('truetype');
+       url(MaterialIcons-Regular.woff) format('woff');
 }
 
 .material-icons {
diff --git a/script/ci-custom.py b/script/ci-custom.py
index 9827248480..5ea7229863 100755
--- a/script/ci-custom.py
+++ b/script/ci-custom.py
@@ -33,9 +33,9 @@ files.sort()
 
 file_types = ('.h', '.c', '.cpp', '.tcc', '.yaml', '.yml', '.ini', '.txt', '.ico', '.svg',
               '.py', '.html', '.js', '.md', '.sh', '.css', '.proto', '.conf', '.cfg',
-              '.eot', '.ttf', '.woff', '.woff2')
+              '.woff', '.woff2', '')
 cpp_include = ('*.h', '*.c', '*.cpp', '*.tcc')
-ignore_types = ('.ico', '.eot', '.ttf', '.woff', '.woff2')
+ignore_types = ('.ico', '.woff', '.woff2', '')
 
 LINT_FILE_CHECKS = []
 LINT_CONTENT_CHECKS = []

From 83a92f03fce298cca43ef0306ae0593beb0d9e26 Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino 
Date: Thu, 29 Aug 2019 11:07:41 -0300
Subject: [PATCH 142/222] add time based cover, has built in endstop (#665)

* add has built in endstop

* rewrite as proposed

* Update esphome/components/time_based/time_based_cover.h

Co-Authored-By: Otto Winter 

* lint

* Re trigger stop_operation if stop called

* allow se triggering open/close command if safe

* using COVER_OPEN/CLOSE constants
---
 esphome/components/time_based/cover.py        |  6 ++++++
 .../time_based/time_based_cover.cpp           | 19 +++++++++++++++----
 .../components/time_based/time_based_cover.h  |  2 ++
 3 files changed, 23 insertions(+), 4 deletions(-)

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..c353b552d3 100644
--- a/esphome/components/time_based/time_based_cover.cpp
+++ b/esphome/components/time_based/time_based_cover.cpp
@@ -30,13 +30,18 @@ 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 +62,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 +93,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

From f9b3e61c0f6bc7d65b3bae35a7a19d99ff8f59c0 Mon Sep 17 00:00:00 2001
From: Robert Kiss 
Date: Thu, 29 Aug 2019 16:09:37 +0200
Subject: [PATCH 143/222] Add delayed_on_off binary_sensor filter (#700)

* add delayed_on_off binary_sensor filter

* fix formatting

* remove unwanted file modification

* add newline to fix linter error
---
 esphome/components/binary_sensor/__init__.py |  9 +++++++++
 esphome/components/binary_sensor/filter.cpp  | 14 ++++++++++++++
 esphome/components/binary_sensor/filter.h    | 12 ++++++++++++
 3 files changed, 35 insertions(+)

diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py
index 9f92207b19..c391e12895 100644
--- a/esphome/components/binary_sensor/__init__.py
+++ b/esphome/components/binary_sensor/__init__.py
@@ -41,6 +41,7 @@ BinarySensorCondition = binary_sensor_ns.class_('BinarySensorCondition', Conditi
 
 # Filters
 Filter = binary_sensor_ns.class_('Filter')
+DelayedOnOffFilter = binary_sensor_ns.class_('DelayedOnOffFilter', Filter, cg.Component)
 DelayedOnFilter = binary_sensor_ns.class_('DelayedOnFilter', Filter, cg.Component)
 DelayedOffFilter = binary_sensor_ns.class_('DelayedOffFilter', Filter, cg.Component)
 InvertFilter = binary_sensor_ns.class_('InvertFilter', Filter)
@@ -55,6 +56,14 @@ def invert_filter_to_code(config, filter_id):
     yield cg.new_Pvariable(filter_id)
 
 
+@FILTER_REGISTRY.register('delayed_on_off', DelayedOnOffFilter,
+                          cv.positive_time_period_milliseconds)
+def delayed_on_off_filter_to_code(config, filter_id):
+    var = cg.new_Pvariable(filter_id, config)
+    yield cg.register_component(var, {})
+    yield var
+
+
 @FILTER_REGISTRY.register('delayed_on', DelayedOnFilter,
                           cv.positive_time_period_milliseconds)
 def delayed_on_filter_to_code(config, filter_id):
diff --git a/esphome/components/binary_sensor/filter.cpp b/esphome/components/binary_sensor/filter.cpp
index b7ac2c4a79..f4612d62e9 100644
--- a/esphome/components/binary_sensor/filter.cpp
+++ b/esphome/components/binary_sensor/filter.cpp
@@ -23,6 +23,19 @@ void Filter::input(bool value, bool is_initial) {
     this->output(*b, is_initial);
   }
 }
+
+DelayedOnOffFilter::DelayedOnOffFilter(uint32_t delay) : delay_(delay) {}
+optional DelayedOnOffFilter::new_value(bool value, bool is_initial) {
+  if (value) {
+    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(true, is_initial); });
+  } else {
+    this->set_timeout("ON_OFF", this->delay_, [this, is_initial]() { this->output(false, is_initial); });
+  }
+  return {};
+}
+
+float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
+
 DelayedOnFilter::DelayedOnFilter(uint32_t delay) : delay_(delay) {}
 optional DelayedOnFilter::new_value(bool value, bool is_initial) {
   if (value) {
@@ -46,6 +59,7 @@ optional DelayedOffFilter::new_value(bool value, bool is_initial) {
     return true;
   }
 }
+
 float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 
 optional InvertFilter::new_value(bool value, bool is_initial) { return !value; }
diff --git a/esphome/components/binary_sensor/filter.h b/esphome/components/binary_sensor/filter.h
index d1e9a0d23a..0b54251cda 100644
--- a/esphome/components/binary_sensor/filter.h
+++ b/esphome/components/binary_sensor/filter.h
@@ -25,6 +25,18 @@ class Filter {
   Deduplicator dedup_;
 };
 
+class DelayedOnOffFilter : public Filter, public Component {
+ public:
+  explicit DelayedOnOffFilter(uint32_t delay);
+
+  optional new_value(bool value, bool is_initial) override;
+
+  float get_setup_priority() const override;
+
+ protected:
+  uint32_t delay_;
+};
+
 class DelayedOnFilter : public Filter, public Component {
  public:
   explicit DelayedOnFilter(uint32_t delay);

From 2b30cde1256c900177e984d22bdf63b3678e09b1 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Thu, 29 Aug 2019 16:20:56 +0200
Subject: [PATCH 144/222] Fixup dev branch again

Closes https://github.com/esphome/esphome/pull/706
---
 esphome/components/wifi_info/wifi_info_text_sensor.cpp | 4 ++--
 esphome/components/wifi_info/wifi_info_text_sensor.h   | 3 +++
 2 files changed, 5 insertions(+), 2 deletions(-)

diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
index 283b876b5d..0f96d89737 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
@@ -2,7 +2,7 @@
 #include "esphome/core/log.h"
 
 namespace esphome {
-namespace wifi_info_text_sensor {
+namespace wifi_info {
 
 static const char *TAG = "wifi_info";
 
@@ -10,5 +10,5 @@ void IPAddressWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo I
 void SSIDWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
 void BSSIDWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
 
-}  // namespace wifi_info_text_sensor
+}  // namespace wifi_info
 }  // namespace esphome
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h
index 47aa68ea0f..9dfa684b4b 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.h
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.h
@@ -18,6 +18,7 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; }
+  void dump_config() override;
 
  protected:
   IPAddress last_ip_;
@@ -34,6 +35,7 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor {
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; }
+  void dump_config() override;
 
  protected:
   std::string last_ssid_;
@@ -52,6 +54,7 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; }
+  void dump_config() override;
 
  protected:
   wifi::bssid_t last_bssid_;

From 9b28c732c6f60be8d8d99e3a2b1662850e5e1ea8 Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino 
Date: Thu, 29 Aug 2019 21:34:29 -0300
Subject: [PATCH 145/222] fix wifi info (#709)

* fix wifi info

* lint time based cover
---
 esphome/components/time_based/time_based_cover.cpp     |  3 ++-
 esphome/components/wifi_info/wifi_info_text_sensor.cpp | 10 +++++-----
 esphome/components/wifi_info/wifi_info_text_sensor.h   |  3 +++
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp
index c353b552d3..bdb4e5379c 100644
--- a/esphome/components/time_based/time_based_cover.cpp
+++ b/esphome/components/time_based/time_based_cover.cpp
@@ -31,7 +31,8 @@ void TimeBasedCover::loop() {
   this->recompute_position_();
 
   if (this->is_at_target_()) {
-    if (this->has_built_in_endstop_ && (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) {
+    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 {
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.cpp b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
index 283b876b5d..704d9b3099 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.cpp
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.cpp
@@ -2,13 +2,13 @@
 #include "esphome/core/log.h"
 
 namespace esphome {
-namespace wifi_info_text_sensor {
+namespace wifi_info {
 
 static const char *TAG = "wifi_info";
 
-void IPAddressWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); }
-void SSIDWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
-void BSSIDWiFiInfo::dump_config() override { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
+void IPAddressWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo IPAddress", this); }
+void SSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo SSID", this); }
+void BSSIDWiFiInfo::dump_config() { LOG_TEXT_SENSOR("", "WifiInfo BSSID", this); }
 
-}  // namespace wifi_info_text_sensor
+}  // namespace wifi_info
 }  // namespace esphome
diff --git a/esphome/components/wifi_info/wifi_info_text_sensor.h b/esphome/components/wifi_info/wifi_info_text_sensor.h
index 47aa68ea0f..9dfa684b4b 100644
--- a/esphome/components/wifi_info/wifi_info_text_sensor.h
+++ b/esphome/components/wifi_info/wifi_info_text_sensor.h
@@ -18,6 +18,7 @@ class IPAddressWiFiInfo : public Component, public text_sensor::TextSensor {
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-ip"; }
+  void dump_config() override;
 
  protected:
   IPAddress last_ip_;
@@ -34,6 +35,7 @@ class SSIDWiFiInfo : public Component, public text_sensor::TextSensor {
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-ssid"; }
+  void dump_config() override;
 
  protected:
   std::string last_ssid_;
@@ -52,6 +54,7 @@ class BSSIDWiFiInfo : public Component, public text_sensor::TextSensor {
   }
   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
   std::string unique_id() override { return get_mac_address() + "-wifiinfo-bssid"; }
+  void dump_config() override;
 
  protected:
   wifi::bssid_t last_bssid_;

From 244c4be8cc33cb48dcb06eda54d8dee1ee53d7ca Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino 
Date: Sat, 31 Aug 2019 13:45:06 -0300
Subject: [PATCH 146/222] fix integration sensor (#711)

* fix integration sensor

* revert rtc_.save conditional
---
 esphome/components/integration/integration_sensor.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp
index 9ddfd2ad0b..22fab290dd 100644
--- a/esphome/components/integration/integration_sensor.cpp
+++ b/esphome/components/integration/integration_sensor.cpp
@@ -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

From 4c03cebef3499b49c2fbbcb2f98bbe36f8973387 Mon Sep 17 00:00:00 2001
From: Nad <15346053+valordk@users.noreply.github.com>
Date: Sat, 31 Aug 2019 19:24:37 +0200
Subject: [PATCH 147/222] Add support for Sensirion SCD30 CO2 sensors (#712)

* Add support for Sensirion SCD30 CO2 sensors

* Fixed few lint issues

* Lint fixes

* Fixed line ending for lint

* Cleanup

* Refactored float conversion

* Refactor unnecessary return


Co-authored-by: Otto Winter 
---
 esphome/components/scd30/__init__.py |   0
 esphome/components/scd30/scd30.cpp   | 176 +++++++++++++++++++++++++++
 esphome/components/scd30/scd30.h     |  40 ++++++
 esphome/components/scd30/sensor.py   |  39 ++++++
 tests/test1.yaml                     |   9 ++
 5 files changed, 264 insertions(+)
 create mode 100644 esphome/components/scd30/__init__.py
 create mode 100644 esphome/components/scd30/scd30.cpp
 create mode 100644 esphome/components/scd30/scd30.h
 create mode 100644 esphome/components/scd30/sensor.py

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..ac03d9699e
--- /dev/null
+++ b/esphome/components/scd30/scd30.cpp
@@ -0,0 +1,176 @@
+#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;
+    }
+    uint32_t temp_c_o2_u32 = (((uint32_t(raw_data[0])) << 16) | (uint32_t(raw_data[1])));
+    float co2 = *reinterpret_cast(&temp_c_o2_u32);
+
+    uint32_t temp_temp_u32 = (((uint32_t(raw_data[2])) << 16) | (uint32_t(raw_data[3])));
+    float temperature = *reinterpret_cast(&temp_temp_u32);
+
+    uint32_t temp_hum_u32 = (((uint32_t(raw_data[4])) << 16) | (uint32_t(raw_data[5])));
+    float humidity = *reinterpret_cast(&temp_hum_u32);
+
+    ESP_LOGD(TAG, "Got CO2=%.2fppm temperature=%.2f°C humidity=%.2f%%", co2, temperature, humidity);
+    if (this->co2_sensor_ != nullptr)
+      this->co2_sensor_->publish_state(co2);
+    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();
+  });
+}
+
+bool SCD30Component::write_command_(uint16_t command) {
+  // Warning ugly, trick the I2Ccomponent base by setting register to the first 8 bit.
+  return this->write_byte(command >> 8, command & 0xFF);
+}
+
+uint8_t SCD30Component::sht_crc_(uint8_t data1, uint8_t data2) {
+  uint8_t bit;
+  uint8_t crc = 0xFF;
+
+  crc ^= data1;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  crc ^= data2;
+  for (bit = 8; bit > 0; --bit) {
+    if (crc & 0x80)
+      crc = (crc << 1) ^ 0x131;
+    else
+      crc = (crc << 1);
+  }
+
+  return crc;
+}
+
+bool SCD30Component::read_data_(uint16_t *data, uint8_t len) {
+  const uint8_t num_bytes = len * 3;
+  auto *buf = new uint8_t[num_bytes];
+
+  if (!this->parent_->raw_receive(this->address_, buf, num_bytes)) {
+    delete[](buf);
+    return false;
+  }
+
+  for (uint8_t i = 0; i < len; i++) {
+    const uint8_t j = 3 * i;
+    uint8_t crc = sht_crc_(buf[j], buf[j + 1]);
+    if (crc != buf[j + 2]) {
+      ESP_LOGE(TAG, "CRC8 Checksum invalid! 0x%02X != 0x%02X", buf[j + 2], crc);
+      delete[](buf);
+      return false;
+    }
+    data[i] = (buf[j] << 8) | buf[j + 1];
+  }
+
+  delete[](buf);
+  return true;
+}
+
+}  // namespace scd30
+}  // namespace esphome
diff --git a/esphome/components/scd30/scd30.h b/esphome/components/scd30/scd30.h
new file mode 100644
index 0000000000..999e66414d
--- /dev/null
+++ b/esphome/components/scd30/scd30.h
@@ -0,0 +1,40 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/i2c/i2c.h"
+
+namespace esphome {
+namespace scd30 {
+
+/// This class implements support for the Sensirion scd30 i2c GAS (VOC and CO2eq) sensors.
+class SCD30Component : public PollingComponent, public i2c::I2CDevice {
+ public:
+  void set_co2_sensor(sensor::Sensor *co2) { co2_sensor_ = co2; }
+  void set_humidity_sensor(sensor::Sensor *humidity) { humidity_sensor_ = humidity; }
+  void set_temperature_sensor(sensor::Sensor *temperature) { temperature_sensor_ = temperature; }
+
+  void setup() override;
+  void update() override;
+  void dump_config() override;
+  float get_setup_priority() const override { return setup_priority::DATA; }
+
+ protected:
+  bool write_command_(uint16_t command);
+  bool read_data_(uint16_t *data, uint8_t len);
+  uint8_t sht_crc_(uint8_t data1, uint8_t data2);
+
+  enum ErrorCode {
+    COMMUNICATION_FAILED,
+    FIRMWARE_IDENTIFICATION_FAILED,
+    MEASUREMENT_INIT_FAILED,
+    UNKNOWN
+  } error_code_{UNKNOWN};
+
+  sensor::Sensor *co2_sensor_{nullptr};
+  sensor::Sensor *humidity_sensor_{nullptr};
+  sensor::Sensor *temperature_sensor_{nullptr};
+};
+
+}  // namespace scd30
+}  // namespace esphome
diff --git a/esphome/components/scd30/sensor.py b/esphome/components/scd30/sensor.py
new file mode 100644
index 0000000000..aa863b904a
--- /dev/null
+++ b/esphome/components/scd30/sensor.py
@@ -0,0 +1,39 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import i2c, sensor
+from esphome.const import CONF_ID, UNIT_PARTS_PER_MILLION, \
+    CONF_HUMIDITY, CONF_TEMPERATURE, ICON_PERIODIC_TABLE_CO2, \
+    UNIT_CELSIUS, ICON_THERMOMETER, ICON_WATER_PERCENT, UNIT_PERCENT
+
+DEPENDENCIES = ['i2c']
+
+scd30_ns = cg.esphome_ns.namespace('scd30')
+SCD30Component = scd30_ns.class_('SCD30Component', cg.PollingComponent, i2c.I2CDevice)
+
+CONF_CO2 = 'co2'
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(SCD30Component),
+    cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION,
+                                                ICON_PERIODIC_TABLE_CO2, 0),
+    cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1),
+    cv.Required(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1),
+}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x61))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield cg.register_component(var, config)
+    yield i2c.register_i2c_device(var, config)
+
+    if CONF_CO2 in config:
+        sens = yield sensor.new_sensor(config[CONF_CO2])
+        cg.add(var.set_co2_sensor(sens))
+
+    if CONF_HUMIDITY in config:
+        sens = yield sensor.new_sensor(config[CONF_HUMIDITY])
+        cg.add(var.set_humidity_sensor(sens))
+
+    if CONF_TEMPERATURE in config:
+        sens = yield sensor.new_sensor(config[CONF_TEMPERATURE])
+        cg.add(var.set_temperature_sensor(sens))
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 729ea2b2bf..66320f876f 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -507,6 +507,15 @@ sensor:
       name: "Living Room Humidity 8"
     address: 0x44
     update_interval: 15s
+  - platform: scd30
+    co2:
+      name: "Living Room CO2 9"
+    temperature:
+      name: "Living Room Temperature 9"
+    humidity:
+      name: "Living Room Humidity 9"
+    address: 0x61
+    update_interval: 15s
   - platform: template
     name: "Template Sensor"
     id: template_sensor

From b6920025b2429c9a4c14759cf23aea19c19b422d Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino 
Date: Sat, 31 Aug 2019 14:45:34 -0300
Subject: [PATCH 148/222] Fixes sim800l (#678)

* Fix receive message quickly

* fix case

* lint
---
 esphome/components/sim800l/sim800l.cpp | 27 +++++++++++++-------------
 1 file changed, 14 insertions(+), 13 deletions(-)

diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp
index 646f20833f..b2d58c5043 100644
--- a/esphome/components/sim800l/sim800l.cpp
+++ b/esphome/components/sim800l/sim800l.cpp
@@ -67,12 +67,18 @@ void Sim800LComponent::parse_cmd_(std::string message) {
   }
 
   switch (this->state_) {
-    case STATE_INIT:
-      if (message.compare(0, 6, "+CMTI:") == 0) {
-        // While we were waiting for update to check for messages, this notifies a message
-        // is available. Grab it quickly
-        this->state_ = STATE_CHECK_SMS;
-      }
+    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");
@@ -95,7 +101,6 @@ void Sim800LComponent::parse_cmd_(std::string message) {
       if (registered) {
         if (!this->registered_)
           ESP_LOGD(TAG, "Registered OK");
-        send_cmd_("AT+CSQ");
         this->state_ = STATE_CSQ;
         this->expect_ack_ = true;
       } else {
@@ -113,6 +118,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
       break;
     }
     case STATE_CSQ:
+      send_cmd_("AT+CSQ");
       this->state_ = STATE_CSQ_RESPONSE;
       break;
     case STATE_CSQ_RESPONSE:
@@ -123,6 +129,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
           ESP_LOGD(TAG, "RSSI: %d", this->rssi_);
         }
       }
+      this->expect_ack_ = true;
       this->state_ = STATE_CHECK_SMS;
       break;
     case STATE_PARSE_SMS:
@@ -209,12 +216,6 @@ void Sim800LComponent::parse_cmd_(std::string message) {
       ESP_LOGD(TAG, "Unhandled: %s - %d", message.c_str(), this->state_);
       break;
   }
-  if (this->state_ == STATE_CHECK_SMS) {
-    send_cmd_("AT+CMGL=\"ALL\"");
-    this->state_ = STATE_PARSE_SMS;
-    this->parse_index_ = 0;
-    this->expect_ack_ = true;
-  }
 }
 
 void Sim800LComponent::loop() {

From 93cfee802617534c763b46534d9a476c93bf4f63 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Sat, 31 Aug 2019 20:23:06 +0200
Subject: [PATCH 149/222] Fix strobe effect

Fixes https://github.com/esphome/issues/issues/620
---
 esphome/components/light/base_light_effects.h | 1 +
 1 file changed, 1 insertion(+)

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

From 4b0f203049a60276f63a991263c7a9d2a951cdee Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Sat, 31 Aug 2019 21:13:41 +0200
Subject: [PATCH 150/222] Use unique enum names for native API

Fixes https://github.com/esphome/issues/issues/617
---
 esphome/components/api/api_connection.cpp |  8 +--
 esphome/components/api/api_pb2.cpp        | 80 +++++++++++------------
 esphome/components/api/api_pb2.h          | 58 ++++++++--------
 esphome/components/api/user_services.cpp  | 16 ++---
 esphome/components/api/user_services.h    |  4 +-
 script/api_protobuf/api_protobuf.py       |  7 +-
 6 files changed, 87 insertions(+), 86 deletions(-)

diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp
index bb0371b347..76cf72f8ff 100644
--- a/esphome/components/api/api_connection.cpp
+++ b/esphome/components/api/api_connection.cpp
@@ -189,7 +189,7 @@ bool APIConnection::send_cover_state(cover::Cover *cover) {
   resp.position = cover->position;
   if (traits.get_supports_tilt())
     resp.tilt = cover->tilt;
-  resp.current_operation = static_cast(cover->current_operation);
+  resp.current_operation = static_cast(cover->current_operation);
   return this->send_cover_state_response(resp);
 }
 bool APIConnection::send_cover_info(cover::Cover *cover) {
@@ -246,7 +246,7 @@ bool APIConnection::send_fan_state(fan::FanState *fan) {
   if (traits.supports_oscillation())
     resp.oscillating = fan->oscillating;
   if (traits.supports_speed())
-    resp.speed = static_cast(fan->speed);
+    resp.speed = static_cast(fan->speed);
   return this->send_fan_state_response(resp);
 }
 bool APIConnection::send_fan_info(fan::FanState *fan) {
@@ -441,7 +441,7 @@ bool APIConnection::send_climate_state(climate::Climate *climate) {
   auto traits = climate->get_traits();
   ClimateStateResponse resp{};
   resp.key = climate->get_object_id_hash();
-  resp.mode = static_cast(climate->mode);
+  resp.mode = static_cast(climate->mode);
   if (traits.get_supports_current_temperature())
     resp.current_temperature = climate->current_temperature;
   if (traits.get_supports_two_point_target_temperature()) {
@@ -466,7 +466,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) {
   for (auto mode : {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL,
                     climate::CLIMATE_MODE_HEAT}) {
     if (traits.supports_mode(mode))
-      msg.supported_modes.push_back(static_cast(mode));
+      msg.supported_modes.push_back(static_cast(mode));
   }
   msg.visual_min_temperature = traits.get_visual_min_temperature();
   msg.visual_max_temperature = traits.get_visual_max_temperature();
diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp
index 65a0531ea4..815feedea8 100644
--- a/esphome/components/api/api_pb2.cpp
+++ b/esphome/components/api/api_pb2.cpp
@@ -4,7 +4,7 @@
 namespace esphome {
 namespace api {
 
-template<> const char *proto_enum_to_string(LegacyCoverState value) {
+template<> const char *proto_enum_to_string(EnumLegacyCoverState value) {
   switch (value) {
     case LEGACY_COVER_STATE_OPEN:
       return "LEGACY_COVER_STATE_OPEN";
@@ -14,7 +14,7 @@ template<> const char *proto_enum_to_string(LegacyCoverState v
       return "UNKNOWN";
   }
 }
-template<> const char *proto_enum_to_string(CoverOperation value) {
+template<> const char *proto_enum_to_string(EnumCoverOperation value) {
   switch (value) {
     case COVER_OPERATION_IDLE:
       return "COVER_OPERATION_IDLE";
@@ -26,7 +26,7 @@ template<> const char *proto_enum_to_string(CoverOperation value
       return "UNKNOWN";
   }
 }
-template<> const char *proto_enum_to_string(LegacyCoverCommand value) {
+template<> const char *proto_enum_to_string(EnumLegacyCoverCommand value) {
   switch (value) {
     case LEGACY_COVER_COMMAND_OPEN:
       return "LEGACY_COVER_COMMAND_OPEN";
@@ -38,7 +38,7 @@ template<> const char *proto_enum_to_string(LegacyCoverComma
       return "UNKNOWN";
   }
 }
-template<> const char *proto_enum_to_string(FanSpeed value) {
+template<> const char *proto_enum_to_string(EnumFanSpeed value) {
   switch (value) {
     case FAN_SPEED_LOW:
       return "FAN_SPEED_LOW";
@@ -50,7 +50,7 @@ template<> const char *proto_enum_to_string(FanSpeed value) {
       return "UNKNOWN";
   }
 }
-template<> const char *proto_enum_to_string(LogLevel value) {
+template<> const char *proto_enum_to_string(EnumLogLevel value) {
   switch (value) {
     case LOG_LEVEL_NONE:
       return "LOG_LEVEL_NONE";
@@ -70,7 +70,7 @@ template<> const char *proto_enum_to_string(LogLevel value) {
       return "UNKNOWN";
   }
 }
-template<> const char *proto_enum_to_string(ServiceArgType value) {
+template<> const char *proto_enum_to_string(EnumServiceArgType value) {
   switch (value) {
     case SERVICE_ARG_TYPE_BOOL:
       return "SERVICE_ARG_TYPE_BOOL";
@@ -92,7 +92,7 @@ template<> const char *proto_enum_to_string(ServiceArgType value
       return "UNKNOWN";
   }
 }
-template<> const char *proto_enum_to_string(ClimateMode value) {
+template<> const char *proto_enum_to_string(EnumClimateMode value) {
   switch (value) {
     case CLIMATE_MODE_OFF:
       return "CLIMATE_MODE_OFF";
@@ -523,11 +523,11 @@ void ListEntitiesCoverResponse::dump_to(std::string &out) const {
 bool CoverStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
-      this->legacy_state = value.as_enum();
+      this->legacy_state = value.as_enum();
       return true;
     }
     case 5: {
-      this->current_operation = value.as_enum();
+      this->current_operation = value.as_enum();
       return true;
     }
     default:
@@ -554,10 +554,10 @@ bool CoverStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 }
 void CoverStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
-  buffer.encode_enum(2, this->legacy_state);
+  buffer.encode_enum(2, this->legacy_state);
   buffer.encode_float(3, this->position);
   buffer.encode_float(4, this->tilt);
-  buffer.encode_enum(5, this->current_operation);
+  buffer.encode_enum(5, this->current_operation);
 }
 void CoverStateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -568,7 +568,7 @@ void CoverStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  legacy_state: ");
-  out.append(proto_enum_to_string(this->legacy_state));
+  out.append(proto_enum_to_string(this->legacy_state));
   out.append("\n");
 
   out.append("  position: ");
@@ -582,7 +582,7 @@ void CoverStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  current_operation: ");
-  out.append(proto_enum_to_string(this->current_operation));
+  out.append(proto_enum_to_string(this->current_operation));
   out.append("\n");
   out.append("}");
 }
@@ -593,7 +593,7 @@ bool CoverCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
       return true;
     }
     case 3: {
-      this->legacy_command = value.as_enum();
+      this->legacy_command = value.as_enum();
       return true;
     }
     case 4: {
@@ -633,7 +633,7 @@ bool CoverCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
 void CoverCommandRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->has_legacy_command);
-  buffer.encode_enum(3, this->legacy_command);
+  buffer.encode_enum(3, this->legacy_command);
   buffer.encode_bool(4, this->has_position);
   buffer.encode_float(5, this->position);
   buffer.encode_bool(6, this->has_tilt);
@@ -653,7 +653,7 @@ void CoverCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  legacy_command: ");
-  out.append(proto_enum_to_string(this->legacy_command));
+  out.append(proto_enum_to_string(this->legacy_command));
   out.append("\n");
 
   out.append("  has_position: ");
@@ -769,7 +769,7 @@ bool FanStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
       return true;
     }
     case 4: {
-      this->speed = value.as_enum();
+      this->speed = value.as_enum();
       return true;
     }
     default:
@@ -790,7 +790,7 @@ void FanStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->state);
   buffer.encode_bool(3, this->oscillating);
-  buffer.encode_enum(4, this->speed);
+  buffer.encode_enum(4, this->speed);
 }
 void FanStateResponse::dump_to(std::string &out) const {
   char buffer[64];
@@ -809,7 +809,7 @@ void FanStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  speed: ");
-  out.append(proto_enum_to_string(this->speed));
+  out.append(proto_enum_to_string(this->speed));
   out.append("\n");
   out.append("}");
 }
@@ -828,7 +828,7 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
       return true;
     }
     case 5: {
-      this->speed = value.as_enum();
+      this->speed = value.as_enum();
       return true;
     }
     case 6: {
@@ -858,7 +858,7 @@ void FanCommandRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_bool(2, this->has_state);
   buffer.encode_bool(3, this->state);
   buffer.encode_bool(4, this->has_speed);
-  buffer.encode_enum(5, this->speed);
+  buffer.encode_enum(5, this->speed);
   buffer.encode_bool(6, this->has_oscillating);
   buffer.encode_bool(7, this->oscillating);
 }
@@ -883,7 +883,7 @@ void FanCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  speed: ");
-  out.append(proto_enum_to_string(this->speed));
+  out.append(proto_enum_to_string(this->speed));
   out.append("\n");
 
   out.append("  has_oscillating: ");
@@ -1719,7 +1719,7 @@ void TextSensorStateResponse::dump_to(std::string &out) const {
 bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
-      this->level = value.as_enum();
+      this->level = value.as_enum();
       return true;
     }
     case 2: {
@@ -1731,14 +1731,14 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
   }
 }
 void SubscribeLogsRequest::encode(ProtoWriteBuffer buffer) const {
-  buffer.encode_enum(1, this->level);
+  buffer.encode_enum(1, this->level);
   buffer.encode_bool(2, this->dump_config);
 }
 void SubscribeLogsRequest::dump_to(std::string &out) const {
   char buffer[64];
   out.append("SubscribeLogsRequest {\n");
   out.append("  level: ");
-  out.append(proto_enum_to_string(this->level));
+  out.append(proto_enum_to_string(this->level));
   out.append("\n");
 
   out.append("  dump_config: ");
@@ -1749,7 +1749,7 @@ void SubscribeLogsRequest::dump_to(std::string &out) const {
 bool SubscribeLogsResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 1: {
-      this->level = value.as_enum();
+      this->level = value.as_enum();
       return true;
     }
     case 4: {
@@ -1775,7 +1775,7 @@ bool SubscribeLogsResponse::decode_length(uint32_t field_id, ProtoLengthDelimite
   }
 }
 void SubscribeLogsResponse::encode(ProtoWriteBuffer buffer) const {
-  buffer.encode_enum(1, this->level);
+  buffer.encode_enum(1, this->level);
   buffer.encode_string(2, this->tag);
   buffer.encode_string(3, this->message);
   buffer.encode_bool(4, this->send_failed);
@@ -1784,7 +1784,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
   char buffer[64];
   out.append("SubscribeLogsResponse {\n");
   out.append("  level: ");
-  out.append(proto_enum_to_string(this->level));
+  out.append(proto_enum_to_string(this->level));
   out.append("\n");
 
   out.append("  tag: ");
@@ -1989,7 +1989,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
 bool ListEntitiesServicesArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
-      this->type = value.as_enum();
+      this->type = value.as_enum();
       return true;
     }
     default:
@@ -2008,7 +2008,7 @@ bool ListEntitiesServicesArgument::decode_length(uint32_t field_id, ProtoLengthD
 }
 void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_string(1, this->name);
-  buffer.encode_enum(2, this->type);
+  buffer.encode_enum(2, this->type);
 }
 void ListEntitiesServicesArgument::dump_to(std::string &out) const {
   char buffer[64];
@@ -2018,7 +2018,7 @@ void ListEntitiesServicesArgument::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  type: ");
-  out.append(proto_enum_to_string(this->type));
+  out.append(proto_enum_to_string(this->type));
   out.append("\n");
   out.append("}");
 }
@@ -2387,7 +2387,7 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v
       return true;
     }
     case 7: {
-      this->supported_modes.push_back(value.as_enum());
+      this->supported_modes.push_back(value.as_enum());
       return true;
     }
     case 11: {
@@ -2446,7 +2446,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_bool(5, this->supports_current_temperature);
   buffer.encode_bool(6, this->supports_two_point_target_temperature);
   for (auto &it : this->supported_modes) {
-    buffer.encode_enum(7, it, true);
+    buffer.encode_enum(7, it, true);
   }
   buffer.encode_float(8, this->visual_min_temperature);
   buffer.encode_float(9, this->visual_max_temperature);
@@ -2483,7 +2483,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 
   for (const auto &it : this->supported_modes) {
     out.append("  supported_modes: ");
-    out.append(proto_enum_to_string(it));
+    out.append(proto_enum_to_string(it));
     out.append("\n");
   }
 
@@ -2510,7 +2510,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
   switch (field_id) {
     case 2: {
-      this->mode = value.as_enum();
+      this->mode = value.as_enum();
       return true;
     }
     case 7: {
@@ -2549,7 +2549,7 @@ bool ClimateStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
 }
 void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
-  buffer.encode_enum(2, this->mode);
+  buffer.encode_enum(2, this->mode);
   buffer.encode_float(3, this->current_temperature);
   buffer.encode_float(4, this->target_temperature);
   buffer.encode_float(5, this->target_temperature_low);
@@ -2565,7 +2565,7 @@ void ClimateStateResponse::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
+  out.append(proto_enum_to_string(this->mode));
   out.append("\n");
 
   out.append("  current_temperature: ");
@@ -2600,7 +2600,7 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
       return true;
     }
     case 3: {
-      this->mode = value.as_enum();
+      this->mode = value.as_enum();
       return true;
     }
     case 4: {
@@ -2652,7 +2652,7 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
 void ClimateCommandRequest::encode(ProtoWriteBuffer buffer) const {
   buffer.encode_fixed32(1, this->key);
   buffer.encode_bool(2, this->has_mode);
-  buffer.encode_enum(3, this->mode);
+  buffer.encode_enum(3, this->mode);
   buffer.encode_bool(4, this->has_target_temperature);
   buffer.encode_float(5, this->target_temperature);
   buffer.encode_bool(6, this->has_target_temperature_low);
@@ -2675,7 +2675,7 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
   out.append("\n");
 
   out.append("  mode: ");
-  out.append(proto_enum_to_string(this->mode));
+  out.append(proto_enum_to_string(this->mode));
   out.append("\n");
 
   out.append("  has_target_temperature: ");
diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h
index 6836bb847d..50bf3117c0 100644
--- a/esphome/components/api/api_pb2.h
+++ b/esphome/components/api/api_pb2.h
@@ -5,26 +5,26 @@
 namespace esphome {
 namespace api {
 
-enum LegacyCoverState : uint32_t {
+enum EnumLegacyCoverState : uint32_t {
   LEGACY_COVER_STATE_OPEN = 0,
   LEGACY_COVER_STATE_CLOSED = 1,
 };
-enum CoverOperation : uint32_t {
+enum EnumCoverOperation : uint32_t {
   COVER_OPERATION_IDLE = 0,
   COVER_OPERATION_IS_OPENING = 1,
   COVER_OPERATION_IS_CLOSING = 2,
 };
-enum LegacyCoverCommand : uint32_t {
+enum EnumLegacyCoverCommand : uint32_t {
   LEGACY_COVER_COMMAND_OPEN = 0,
   LEGACY_COVER_COMMAND_CLOSE = 1,
   LEGACY_COVER_COMMAND_STOP = 2,
 };
-enum FanSpeed : uint32_t {
+enum EnumFanSpeed : uint32_t {
   FAN_SPEED_LOW = 0,
   FAN_SPEED_MEDIUM = 1,
   FAN_SPEED_HIGH = 2,
 };
-enum LogLevel : uint32_t {
+enum EnumLogLevel : uint32_t {
   LOG_LEVEL_NONE = 0,
   LOG_LEVEL_ERROR = 1,
   LOG_LEVEL_WARN = 2,
@@ -33,7 +33,7 @@ enum LogLevel : uint32_t {
   LOG_LEVEL_VERBOSE = 5,
   LOG_LEVEL_VERY_VERBOSE = 6,
 };
-enum ServiceArgType : uint32_t {
+enum EnumServiceArgType : uint32_t {
   SERVICE_ARG_TYPE_BOOL = 0,
   SERVICE_ARG_TYPE_INT = 1,
   SERVICE_ARG_TYPE_FLOAT = 2,
@@ -43,7 +43,7 @@ enum ServiceArgType : uint32_t {
   SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
   SERVICE_ARG_TYPE_STRING_ARRAY = 7,
 };
-enum ClimateMode : uint32_t {
+enum EnumClimateMode : uint32_t {
   CLIMATE_MODE_OFF = 0,
   CLIMATE_MODE_AUTO = 1,
   CLIMATE_MODE_COOL = 2,
@@ -207,11 +207,11 @@ class ListEntitiesCoverResponse : public ProtoMessage {
 };
 class CoverStateResponse : public ProtoMessage {
  public:
-  uint32_t key{0};                     // NOLINT
-  LegacyCoverState legacy_state{};     // NOLINT
-  float position{0.0f};                // NOLINT
-  float tilt{0.0f};                    // NOLINT
-  CoverOperation current_operation{};  // NOLINT
+  uint32_t key{0};                         // NOLINT
+  EnumLegacyCoverState legacy_state{};     // NOLINT
+  float position{0.0f};                    // NOLINT
+  float tilt{0.0f};                        // NOLINT
+  EnumCoverOperation current_operation{};  // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -221,14 +221,14 @@ class CoverStateResponse : public ProtoMessage {
 };
 class CoverCommandRequest : public ProtoMessage {
  public:
-  uint32_t key{0};                      // NOLINT
-  bool has_legacy_command{false};       // NOLINT
-  LegacyCoverCommand legacy_command{};  // NOLINT
-  bool has_position{false};             // NOLINT
-  float position{0.0f};                 // NOLINT
-  bool has_tilt{false};                 // NOLINT
-  float tilt{0.0f};                     // NOLINT
-  bool stop{false};                     // NOLINT
+  uint32_t key{0};                          // NOLINT
+  bool has_legacy_command{false};           // NOLINT
+  EnumLegacyCoverCommand legacy_command{};  // NOLINT
+  bool has_position{false};                 // NOLINT
+  float position{0.0f};                     // NOLINT
+  bool has_tilt{false};                     // NOLINT
+  float tilt{0.0f};                         // NOLINT
+  bool stop{false};                         // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -257,7 +257,7 @@ class FanStateResponse : public ProtoMessage {
   uint32_t key{0};          // NOLINT
   bool state{false};        // NOLINT
   bool oscillating{false};  // NOLINT
-  FanSpeed speed{};         // NOLINT
+  EnumFanSpeed speed{};     // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -271,7 +271,7 @@ class FanCommandRequest : public ProtoMessage {
   bool has_state{false};        // NOLINT
   bool state{false};            // NOLINT
   bool has_speed{false};        // NOLINT
-  FanSpeed speed{};             // NOLINT
+  EnumFanSpeed speed{};         // NOLINT
   bool has_oscillating{false};  // NOLINT
   bool oscillating{false};      // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
@@ -442,7 +442,7 @@ class TextSensorStateResponse : public ProtoMessage {
 };
 class SubscribeLogsRequest : public ProtoMessage {
  public:
-  LogLevel level{};         // NOLINT
+  EnumLogLevel level{};     // NOLINT
   bool dump_config{false};  // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
@@ -452,7 +452,7 @@ class SubscribeLogsRequest : public ProtoMessage {
 };
 class SubscribeLogsResponse : public ProtoMessage {
  public:
-  LogLevel level{};         // NOLINT
+  EnumLogLevel level{};     // NOLINT
   std::string tag{};        // NOLINT
   std::string message{};    // NOLINT
   bool send_failed{false};  // NOLINT
@@ -538,8 +538,8 @@ class GetTimeResponse : public ProtoMessage {
 };
 class ListEntitiesServicesArgument : public ProtoMessage {
  public:
-  std::string name{};     // NOLINT
-  ServiceArgType type{};  // NOLINT
+  std::string name{};         // NOLINT
+  EnumServiceArgType type{};  // NOLINT
   void encode(ProtoWriteBuffer buffer) const override;
   void dump_to(std::string &out) const override;
 
@@ -633,7 +633,7 @@ class ListEntitiesClimateResponse : public ProtoMessage {
   std::string unique_id{};                            // NOLINT
   bool supports_current_temperature{false};           // NOLINT
   bool supports_two_point_target_temperature{false};  // NOLINT
-  std::vector supported_modes{};         // NOLINT
+  std::vector supported_modes{};     // NOLINT
   float visual_min_temperature{0.0f};                 // NOLINT
   float visual_max_temperature{0.0f};                 // NOLINT
   float visual_temperature_step{0.0f};                // NOLINT
@@ -649,7 +649,7 @@ class ListEntitiesClimateResponse : public ProtoMessage {
 class ClimateStateResponse : public ProtoMessage {
  public:
   uint32_t key{0};                      // NOLINT
-  ClimateMode mode{};                   // NOLINT
+  EnumClimateMode mode{};               // NOLINT
   float current_temperature{0.0f};      // NOLINT
   float target_temperature{0.0f};       // NOLINT
   float target_temperature_low{0.0f};   // NOLINT
@@ -666,7 +666,7 @@ class ClimateCommandRequest : public ProtoMessage {
  public:
   uint32_t key{0};                          // NOLINT
   bool has_mode{false};                     // NOLINT
-  ClimateMode mode{};                       // NOLINT
+  EnumClimateMode mode{};                   // NOLINT
   bool has_target_temperature{false};       // NOLINT
   float target_temperature{0.0f};           // NOLINT
   bool has_target_temperature_low{false};   // NOLINT
diff --git a/esphome/components/api/user_services.cpp b/esphome/components/api/user_services.cpp
index 9e2560d3c8..0667d26ff6 100644
--- a/esphome/components/api/user_services.cpp
+++ b/esphome/components/api/user_services.cpp
@@ -25,14 +25,14 @@ template<> std::vector get_execute_arg_value ServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_BOOL; }
-template<> ServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_INT; }
-template<> ServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_FLOAT; }
-template<> ServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_STRING; }
-template<> ServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_BOOL_ARRAY; }
-template<> ServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_INT_ARRAY; }
-template<> ServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_FLOAT_ARRAY; }
-template<> ServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_STRING_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_BOOL; }
+template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_INT; }
+template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_FLOAT; }
+template<> EnumServiceArgType to_service_arg_type() { return SERVICE_ARG_TYPE_STRING; }
+template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_BOOL_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_INT_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_FLOAT_ARRAY; }
+template<> EnumServiceArgType to_service_arg_type>() { return SERVICE_ARG_TYPE_STRING_ARRAY; }
 
 }  // namespace api
 }  // namespace esphome
diff --git a/esphome/components/api/user_services.h b/esphome/components/api/user_services.h
index dcc13a528d..3b99d426a9 100644
--- a/esphome/components/api/user_services.h
+++ b/esphome/components/api/user_services.h
@@ -16,7 +16,7 @@ class UserServiceDescriptor {
 
 template T get_execute_arg_value(const ExecuteServiceArgument &arg);
 
-template ServiceArgType to_service_arg_type();
+template EnumServiceArgType to_service_arg_type();
 
 template class UserServiceBase : public UserServiceDescriptor {
  public:
@@ -29,7 +29,7 @@ template class UserServiceBase : public UserServiceDescriptor {
     ListEntitiesServicesResponse msg;
     msg.name = this->name_;
     msg.key = this->key_;
-    std::array arg_types = {to_service_arg_type()...};
+    std::array arg_types = {to_service_arg_type()...};
     for (int i = 0; i < sizeof...(Ts); i++) {
       ListEntitiesServicesArgument arg;
       arg.type = arg_types[i];
diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py
index e6189cd386..6357ae38ed 100644
--- a/script/api_protobuf/api_protobuf.py
+++ b/script/api_protobuf/api_protobuf.py
@@ -344,7 +344,7 @@ class UInt32Type(TypeInfo):
 class EnumType(TypeInfo):
     @property
     def cpp_type(self):
-        return self._field.type_name[1:]
+        return "Enum" + self._field.type_name[1:]
 
     @property
     def decode_varint(self):
@@ -497,13 +497,14 @@ class RepeatedTypeInfo(TypeInfo):
 
 
 def build_enum_type(desc):
-    out = f"enum {desc.name} : uint32_t {{\n"
+    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<{desc.name}>({desc.name} value) {{\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'

From c2028f73786ad1d969958bf058ef42374a242916 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Sat, 31 Aug 2019 21:14:10 +0200
Subject: [PATCH 151/222] DHT publish NAN on invalid reading

Fixes https://github.com/esphome/issues/issues/590
---
 esphome/components/dht/dht.cpp | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp
index 936f87e3fa..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();
   }
 }

From be1e4c0a1d730a0436c4310ff328105e753a1891 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Sat, 31 Aug 2019 21:14:33 +0200
Subject: [PATCH 152/222] Fix nextion display_picture argument order

Fixes https://github.com/esphome/issues/issues/613
---
 esphome/components/nextion/nextion.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

From fd1dc24ac6eaddad42fd5d9e9221542d82413db0 Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Sun, 1 Sep 2019 11:42:37 +0200
Subject: [PATCH 153/222] Also accept invalid spelling from Updater

Fixes https://github.com/esphome/issues/issues/564 partly.

At least the error message will now be a better one.
---
 esphome/components/ota/ota_component.cpp | 4 ++--
 esphome/espota2.py                       | 3 ++-
 2 files changed, 4 insertions(+), 3 deletions(-)

diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp
index 7a00c5bb41..6d2a0dd7c3 100644
--- a/esphome/components/ota/ota_component.cpp
+++ b/esphome/components/ota/ota_component.cpp
@@ -182,11 +182,11 @@ void OTAComponent::handle_() {
       error_code = OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING;
       goto error;
     }
-    if (ss.indexOf("new Flash config wrong") != -1) {
+    if (ss.indexOf("new Flash config wrong") != -1 || ss.indexOf("new Flash config wsong") != -1) {
       error_code = OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
       goto error;
     }
-    if (ss.indexOf("Flash config wrong real") != -1) {
+    if (ss.indexOf("Flash config wrong real") != -1 || ss.indexOf("Flash config wsong real") != -1) {
       error_code = OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
       goto error;
     }
diff --git a/esphome/espota2.py b/esphome/espota2.py
index e50f3a4eb7..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)")

From 1d5f8d5a5267710f247c85f31ed8ba7949df37f7 Mon Sep 17 00:00:00 2001
From: Fritz Mueller 
Date: Wed, 4 Sep 2019 02:06:18 -0700
Subject: [PATCH 154/222] Use default format to render FloatLiteral (#717)

Fixes https://github.com/esphome/issues/issues/557
---
 esphome/cpp_generator.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py
index c1e4a87179..09b542b3cc 100644
--- a/esphome/cpp_generator.py
+++ b/esphome/cpp_generator.py
@@ -250,7 +250,7 @@ class FloatLiteral(Literal):
     def __str__(self):
         if math.isnan(self.float_):
             return u"NAN"
-        return u"{:f}f".format(self.float_)
+        return u"{}f".format(self.float_)
 
 
 # pylint: disable=bad-continuation

From 4118a289a69e12093fbe9282a8d196364b05451b Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino 
Date: Sun, 8 Sep 2019 22:14:39 -0300
Subject: [PATCH 155/222] Add coolix receiver (#645)

* add coolix receiver

a

* lint - added comments

* Lint

* target temp neve be nan
---
 esphome/components/coolix/climate.py |   7 +-
 esphome/components/coolix/coolix.cpp | 160 ++++++++++++++++++++-------
 esphome/components/coolix/coolix.h   |  10 +-
 3 files changed, 135 insertions(+), 42 deletions(-)

diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py
index 750a97d087..ed88f6ad6a 100644
--- a/esphome/components/coolix/climate.py
+++ b/esphome/components/coolix/climate.py
@@ -1,6 +1,6 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import climate, remote_transmitter, sensor
+from esphome.components import climate, remote_transmitter, remote_receiver, sensor
 from esphome.const import CONF_ID, CONF_SENSOR
 
 AUTO_LOAD = ['sensor']
@@ -9,12 +9,14 @@ coolix_ns = cg.esphome_ns.namespace('coolix')
 CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component)
 
 CONF_TRANSMITTER_ID = 'transmitter_id'
+CONF_RECEIVER_ID = 'receiver_id'
 CONF_SUPPORTS_HEAT = 'supports_heat'
 CONF_SUPPORTS_COOL = 'supports_cool'
 
 CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(CoolixClimate),
     cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent),
+    cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent),
     cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
     cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
     cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
@@ -31,6 +33,9 @@ def to_code(config):
     if CONF_SENSOR in config:
         sens = yield cg.get_variable(config[CONF_SENSOR])
         cg.add(var.set_sensor(sens))
+    if CONF_RECEIVER_ID in config:
+        receiver = yield cg.get_variable(config[CONF_RECEIVER_ID])
+        cg.add(receiver.register_listener(var))
 
     transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID])
     cg.add(var.set_transmitter(transmitter))
diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp
index ffc67adeb3..4da307a737 100644
--- a/esphome/components/coolix/coolix.cpp
+++ b/esphome/components/coolix/coolix.cpp
@@ -7,15 +7,24 @@ 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
@@ -41,22 +50,13 @@ 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;
 
@@ -90,10 +90,13 @@ void CoolixClimate::setup() {
     restore->apply(this);
   } else {
     // restore from defaults
-    this->mode = climate::CLIMATE_MODE_AUTO;
+    this->mode = climate::CLIMATE_MODE_OFF;
     // initialize target temperature to some value so that it's not NAN
-    this->target_temperature = roundf(this->current_temperature);
+    this->target_temperature = (uint8_t) roundf(clamp(this->current_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX));
   }
+  // never send nan as temperature. HA will disable the user to change the temperature.
+  if (isnan(this->target_temperature))
+    this->target_temperature = 24;
 }
 
 void CoolixClimate::control(const climate::ClimateCall &call) {
@@ -111,10 +114,10 @@ void CoolixClimate::transmit_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 +130,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 +142,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..392728c654 100644
--- a/esphome/components/coolix/coolix.h
+++ b/esphome/components/coolix/coolix.h
@@ -10,7 +10,9 @@
 namespace esphome {
 namespace coolix {
 
-class CoolixClimate : public climate::Climate, public Component {
+using namespace remote_base;
+
+class CoolixClimate : public climate::Climate, public Component, public RemoteReceiverListener {
  public:
   void setup() override;
   void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) {
@@ -29,8 +31,10 @@ class CoolixClimate : public climate::Climate, public Component {
   /// Transmit via IR the state of this climate controller.
   void transmit_state_();
 
-  bool supports_cool_{true};
-  bool supports_heat_{true};
+  bool on_receive(RemoteReceiveData data) override;
+
+  bool supports_cool_;
+  bool supports_heat_;
 
   remote_transmitter::RemoteTransmitterComponent *transmitter_;
   sensor::Sensor *sensor_{nullptr};

From bd0be41064937399b31909faace3923880e8ada5 Mon Sep 17 00:00:00 2001
From: C W 
Date: Tue, 10 Sep 2019 19:37:33 -0700
Subject: [PATCH 156/222] Fix https://github.com/esphome/issues/issues/658
 (#724)

* Fix https://github.com/esphome/issues/issues/658

* Update to gross code style.
---
 esphome/components/web_server/web_server.cpp | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp
index fe36d6c2ce..3c244e9666 100644
--- a/esphome/components/web_server/web_server.cpp
+++ b/esphome/components/web_server/web_server.cpp
@@ -7,7 +7,10 @@
 #include "StreamString.h"
 
 #include 
+
+#ifdef USE_LOGGER
 #include 
+#endif
 
 namespace esphome {
 namespace web_server {

From d4e0e1518a7682bdafe59c57a650b7c794fd5ad3 Mon Sep 17 00:00:00 2001
From: shbatm 
Date: Sun, 22 Sep 2019 19:59:30 -0500
Subject: [PATCH 157/222] Update MANIFEST.in to fix esphome/issues#650 (#733)

---
 MANIFEST.in | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/MANIFEST.in b/MANIFEST.in
index f4a2cc672d..e5c7b8f748 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,5 +1,5 @@
 include LICENSE
 include README.md
 include esphome/dashboard/templates/*.html
-recursive-include esphome/dashboard/static *.ico *.js *.css
+recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE
 recursive-include esphome *.cpp *.h *.tcc

From f4f1164b9447b25aa775f267cc132c12b864d16b Mon Sep 17 00:00:00 2001
From: Guillermo Ruffino 
Date: Sat, 28 Sep 2019 10:26:48 -0300
Subject: [PATCH 158/222] fixes samsung ir (#738)

fixes https://github.com/esphome/issues/issues/691
---
 esphome/components/remote_base/__init__.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py
index f54fd1df9b..f3042e5598 100644
--- a/esphome/components/remote_base/__init__.py
+++ b/esphome/components/remote_base/__init__.py
@@ -640,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_))
 
 

From 4d31ad3bdc7d7182de4abb22079a9629eba50321 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= 
Date: Sat, 12 Oct 2019 13:42:27 +0100
Subject: [PATCH 159/222] Allow 64 bit codes and add nexa remote support.
 (#662)

* add nexa remote support.

This is inspired by: https://github.com/sui77/rc-switch/pull/124
As described there: "The remotes sold in ClasOhlson in scandinavia have
a slightly longer sync sequence(added a skip pulse field in the
protocol) and a 64 bit code word. Part of the code gets lost but that
seems to be OK until support for 64 bit codes is added."

* add default value to ctor

* allow 64bit codes

* lint

* make vars 64 bits
---
 esphome/components/remote_base/__init__.py    | 10 ++---
 .../remote_base/rc_switch_protocol.cpp        | 37 ++++++++++---------
 .../remote_base/rc_switch_protocol.h          | 36 +++++++++---------
 3 files changed, 42 insertions(+), 41 deletions(-)

diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py
index f3042e5598..a62304c87d 100644
--- a/esphome/components/remote_base/__init__.py
+++ b/esphome/components/remote_base/__init__.py
@@ -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,8 +438,8 @@ 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")
@@ -454,8 +454,8 @@ def validate_rc_switch_raw_code(value):
             raise cv.Invalid(
                 "Invalid RCSwitch raw code character '{}'.Only '0', '1' and 'x' are allowed"
                 .format(c))
-    if len(value) > 32:
-        raise cv.Invalid("Maximum length for RCSwitch raw codes is 32, code '{}' has length {}"
+    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")
diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp
index a04b13ecfd..258e6352e2 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 = 0; *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;
@@ -127,7 +128,7 @@ bool RCSwitchBase::decode(RemoteReceiveData &src, uint32_t *out_data, uint8_t *o
   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;
@@ -137,7 +138,7 @@ 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;
@@ -154,7 +155,7 @@ void RCSwitchBase::type_a_code(uint8_t switch_group, uint8_t switch_device, bool
   simple_code_to_tristate(code, 10, out_code);
   *out_nbits = 20;
 }
-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;
@@ -172,7 +173,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;
@@ -190,7 +191,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;
@@ -207,8 +208,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');
@@ -216,8 +217,8 @@ uint32_t decode_binary_string(const std::string &data) {
   return ret;
 }
 
-uint32_t decode_binary_string_mask(const std::string &data) {
-  uint32_t ret = 0;
+uint64_t decode_binary_string_mask(const std::string &data) {
+  uint64_t ret = 0;
   for (char c : data) {
     ret <<= 1UL;
     ret |= (c != 'x');
@@ -226,7 +227,7 @@ uint32_t decode_binary_string_mask(const std::string &data) {
 }
 
 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;
@@ -234,13 +235,13 @@ bool RCSwitchRawReceiver::matches(RemoteReceiveData src) {
   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 d937efbd25..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,11 +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);
 
-uint32_t decode_binary_string_mask(const std::string &data);
+uint64_t decode_binary_string_mask(const std::string &data);
 
 template class RCSwitchRawAction : public RemoteTransmitterActionBase {
  public:
@@ -64,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...);
@@ -86,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);
 
@@ -107,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);
 
@@ -132,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);
 
@@ -154,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);
 
@@ -166,7 +166,7 @@ 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);
@@ -194,8 +194,8 @@ class RCSwitchRawReceiver : public RemoteReceiverBinarySensorBase {
   bool matches(RemoteReceiveData src) override;
 
   RCSwitchBase protocol_;
-  uint32_t code_;
-  uint32_t mask_{0xFFFFFFFF};
+  uint64_t code_;
+  uint64_t mask_{0xFFFFFFFFFFFFFFFF};
   uint8_t nbits_;
 };
 

From 68e7e5a51ca9e6d3b181488c533ebce7714b1c42 Mon Sep 17 00:00:00 2001
From: Thomas Eckerstorfer 
Date: Sat, 12 Oct 2019 15:03:35 +0200
Subject: [PATCH 160/222] AS3935 Lightning sensor (#666)

* added tx20 wind speed sensor

* added test

* fixed lint errors

* fixed more lint errors

* updated tx20

* updated tx20 sensor

* updated to new structure and removed static variables

* removed content from __init__.py

* fixing lint errors

* resolved issues from review

* added as3935 sensor

* updated as3935 with more settings

* update

* support for i2c + spi updated

* added tests and various fixes

* added tx20 wind speed sensor

* fixed lint errors

* fixed more lint errors

* updated tx20

* updated tx20 sensor

* updated to new structure and removed static variables

* removed content from __init__.py

* fixing lint errors

* resolved issues from review

* added as3935 sensor

* updated as3935 with more settings

* update

* support for i2c + spi updated

* added tests and various fixes

* updated tests

* fixed style issues

* Remove debug line

* Update log levels

* Reformat

* Auto-convert to int


Co-authored-by: Thomas 
Co-authored-by: Otto Winter 
---
 esphome/components/as3935_base/__init__.py    |  54 +++++
 .../components/as3935_base/as3935_base.cpp    | 206 ++++++++++++++++++
 esphome/components/as3935_base/as3935_base.h  | 102 +++++++++
 .../components/as3935_base/binary_sensor.py   |  16 ++
 esphome/components/as3935_base/sensor.py      |  30 +++
 esphome/components/as3935_i2c/__init__.py     |  20 ++
 esphome/components/as3935_i2c/as3935_i2c.cpp  |  36 +++
 esphome/components/as3935_i2c/as3935_i2c.h    |  21 ++
 esphome/components/as3935_spi/__init__.py     |  20 ++
 esphome/components/as3935_spi/as3935_spi.cpp  |  49 +++++
 esphome/components/as3935_spi/as3935_spi.h    |  27 +++
 esphome/components/i2c/i2c.cpp                |   2 +-
 esphome/const.py                              |  14 +-
 script/quicklint                              |   2 +-
 script/setup                                  |   2 +-
 tests/test1.yaml                              |  12 +-
 tests/test2.yaml                              |  11 +
 17 files changed, 619 insertions(+), 5 deletions(-)
 create mode 100644 esphome/components/as3935_base/__init__.py
 create mode 100644 esphome/components/as3935_base/as3935_base.cpp
 create mode 100644 esphome/components/as3935_base/as3935_base.h
 create mode 100644 esphome/components/as3935_base/binary_sensor.py
 create mode 100644 esphome/components/as3935_base/sensor.py
 create mode 100644 esphome/components/as3935_i2c/__init__.py
 create mode 100644 esphome/components/as3935_i2c/as3935_i2c.cpp
 create mode 100644 esphome/components/as3935_i2c/as3935_i2c.h
 create mode 100644 esphome/components/as3935_spi/__init__.py
 create mode 100644 esphome/components/as3935_spi/as3935_spi.cpp
 create mode 100644 esphome/components/as3935_spi/as3935_spi.h

diff --git a/esphome/components/as3935_base/__init__.py b/esphome/components/as3935_base/__init__.py
new file mode 100644
index 0000000000..86b7128bb2
--- /dev/null
+++ b/esphome/components/as3935_base/__init__.py
@@ -0,0 +1,54 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import CONF_PIN, CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \
+    CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \
+    CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAP
+from esphome.core import coroutine
+
+
+AUTO_LOAD = ['sensor', 'binary_sensor']
+MULTI_CONF = True
+
+CONF_AS3935_ID = 'as3935_id'
+
+as3935_base_ns = cg.esphome_ns.namespace('as3935_base')
+AS3935 = as3935_base_ns.class_('AS3935Component', cg.Component)
+
+AS3935_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(AS3935),
+    cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema,
+                                  pins.validate_has_interrupt),
+    cv.Optional(CONF_INDOOR): cv.boolean,
+    cv.Optional(CONF_WATCHDOG_THRESHOLD): cv.int_range(min=1, max=10),
+    cv.Optional(CONF_NOISE_LEVEL): cv.int_range(min=1, max=7),
+    cv.Optional(CONF_SPIKE_REJECTION): cv.int_range(min=1, max=11),
+    cv.Optional(CONF_LIGHTNING_THRESHOLD): cv.one_of(0, 1, 5, 9, 16, int=True),
+    cv.Optional(CONF_MASK_DISTURBER): cv.boolean,
+    cv.Optional(CONF_DIV_RATIO): cv.one_of(0, 16, 22, 64, 128, int=True),
+    cv.Optional(CONF_CAP): cv.int_range(min=0, max=15),
+})
+
+
+@coroutine
+def setup_as3935(var, config):
+    yield cg.register_component(var, config)
+
+    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    cg.add(var.set_pin(pin))
+    if CONF_INDOOR in config:
+        cg.add(var.set_indoor(config[CONF_INDOOR]))
+    if CONF_WATCHDOG_THRESHOLD in config:
+        cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD]))
+    if CONF_NOISE_LEVEL in config:
+        cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL]))
+    if CONF_SPIKE_REJECTION in config:
+        cg.add(var.set_spike_rejection(config[CONF_SPIKE_REJECTION]))
+    if CONF_LIGHTNING_THRESHOLD in config:
+        cg.add(var.set_lightning_threshold(config[CONF_LIGHTNING_THRESHOLD]))
+    if CONF_MASK_DISTURBER in config:
+        cg.add(var.set_mask_disturber(config[CONF_MASK_DISTURBER]))
+    if CONF_DIV_RATIO in config:
+        cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))
+    if CONF_CAP in config:
+        cg.add(var.set_cap(config[CONF_CAP]))
diff --git a/esphome/components/as3935_base/as3935_base.cpp b/esphome/components/as3935_base/as3935_base.cpp
new file mode 100644
index 0000000000..a0aedb4a16
--- /dev/null
+++ b/esphome/components/as3935_base/as3935_base.cpp
@@ -0,0 +1,206 @@
+#include "as3935_base.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace as3935_base {
+
+static const char *TAG = "as3935_base";
+
+void AS3935Component::setup() {
+  ESP_LOGCONFIG(TAG, "Setting up AS3935...");
+
+  this->pin_->setup();
+  this->store_.pin = this->pin_->to_isr();
+  LOG_PIN("  Interrupt Pin: ", this->pin_);
+  this->pin_->attach_interrupt(AS3935ComponentStore::gpio_intr, &this->store_, RISING);
+}
+
+void AS3935Component::dump_config() {
+  ESP_LOGCONFIG(TAG, "AS3935:");
+  LOG_PIN("  Interrupt Pin: ", this->pin_);
+}
+
+float AS3935Component::get_setup_priority() const { return setup_priority::DATA; }
+
+void AS3935Component::loop() {
+  if (!this->store_.interrupt)
+    return;
+
+  uint8_t int_value = this->read_interrupt_register_();
+  if (int_value == NOISE_INT) {
+    ESP_LOGI(TAG, "Noise was detected - try increasing the noise level value!");
+  } else if (int_value == DISTURBER_INT) {
+    ESP_LOGI(TAG, "Disturber was detected - try increasing the spike rejection value!");
+  } else if (int_value == LIGHTNING_INT) {
+    ESP_LOGI(TAG, "Lightning has been detected!");
+    if (this->thunder_alert_binary_sensor_ != nullptr)
+      this->thunder_alert_binary_sensor_->publish_state(true);
+    uint8_t distance = this->get_distance_to_storm_();
+    if (this->distance_sensor_ != nullptr)
+      this->distance_sensor_->publish_state(distance);
+    uint32_t energy = this->get_lightning_energy_();
+    if (this->energy_sensor_ != nullptr)
+      this->energy_sensor_->publish_state(energy);
+  }
+  this->thunder_alert_binary_sensor_->publish_state(false);
+  this->store_.interrupt = false;
+}
+
+void AS3935Component::set_indoor(bool indoor) {
+  ESP_LOGD(TAG, "Setting indoor to %d", indoor);
+  if (indoor)
+    this->write_register(AFE_GAIN, GAIN_MASK, INDOOR, 1);
+  else
+    this->write_register(AFE_GAIN, GAIN_MASK, OUTDOOR, 1);
+}
+// REG0x01, bits[3:0], manufacturer default: 0010 (2).
+// This setting determines the threshold for events that trigger the
+// IRQ Pin.
+void AS3935Component::set_watchdog_threshold(uint8_t sensitivity) {
+  ESP_LOGD(TAG, "Setting watchdog sensitivity to %d", sensitivity);
+  if ((sensitivity < 1) || (sensitivity > 10))  // 10 is the max sensitivity setting
+    return;
+  this->write_register(THRESHOLD, THRESH_MASK, sensitivity, 0);
+}
+
+// REG0x01, bits [6:4], manufacturer default: 010 (2).
+// The noise floor level is compared to a known reference voltage. If this
+// level is exceeded the chip will issue an interrupt to the IRQ pin,
+// broadcasting that it can not operate properly due to noise (INT_NH).
+// Check datasheet for specific noise level tolerances when setting this register.
+void AS3935Component::set_noise_level(uint8_t floor) {
+  ESP_LOGD(TAG, "Setting noise level to %d", floor);
+  if ((floor < 1) || (floor > 7))
+    return;
+
+  this->write_register(THRESHOLD, NOISE_FLOOR_MASK, floor, 4);
+}
+// REG0x02, bits [3:0], manufacturer default: 0010 (2).
+// This setting, like the watchdog threshold, can help determine between false
+// events and actual lightning. The shape of the spike is analyzed during the
+// chip's signal validation routine. Increasing this value increases robustness
+// at the cost of sensitivity to distant events.
+void AS3935Component::set_spike_rejection(uint8_t spike_sensitivity) {
+  ESP_LOGD(TAG, "Setting spike rejection to %d", spike_sensitivity);
+  if ((spike_sensitivity < 1) || (spike_sensitivity > 11))
+    return;
+
+  this->write_register(LIGHTNING_REG, SPIKE_MASK, spike_sensitivity, 0);
+}
+// REG0x02, bits [5:4], manufacturer default: 0 (single lightning strike).
+// The number of lightning events before IRQ is set high. 15 minutes is The
+// window of time before the number of detected lightning events is reset.
+// The number of lightning strikes can be set to 1,5,9, or 16.
+void AS3935Component::set_lightning_threshold(uint8_t strikes) {
+  ESP_LOGD(TAG, "Setting lightning threshold to %d", strikes);
+  if (strikes == 1)
+    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 0, 4);  // Demonstrative
+  if (strikes == 5)
+    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 4);
+  if (strikes == 9)
+    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 5);
+  if (strikes == 16)
+    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 3, 4);
+}
+// REG0x03, bit [5], manufacturer default: 0.
+// This setting will return whether or not disturbers trigger the IRQ Pin.
+void AS3935Component::set_mask_disturber(bool enabled) {
+  ESP_LOGD(TAG, "Setting mask disturber to %d", enabled);
+  if (enabled) {
+    this->write_register(INT_MASK_ANT, (1 << 5), 1, 5);
+  } else {
+    this->write_register(INT_MASK_ANT, (1 << 5), 0, 5);
+  }
+}
+// REG0x03, bit [7:6], manufacturer default: 0 (16 division ratio).
+// The antenna is designed to resonate at 500kHz and so can be tuned with the
+// following setting. The accuracy of the antenna must be within 3.5 percent of
+// that value for proper signal validation and distance estimation.
+void AS3935Component::set_div_ratio(uint8_t div_ratio) {
+  ESP_LOGD(TAG, "Setting div ratio to %d", div_ratio);
+  if (div_ratio == 16)
+    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 0, 6);
+  else if (div_ratio == 22)
+    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 6);
+  else if (div_ratio == 64)
+    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 7);
+  else if (div_ratio == 128)
+    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 3, 6);
+}
+// REG0x08, bits [3:0], manufacturer default: 0.
+// This setting will add capacitance to the series RLC antenna on the product
+// to help tune its resonance. The datasheet specifies being within 3.5 percent
+// of 500kHz to get optimal lightning detection and distance sensing.
+// It's possible to add up to 120pF in steps of 8pF to the antenna.
+void AS3935Component::set_cap(uint8_t eight_pico_farad) {
+  ESP_LOGD(TAG, "Setting tune cap to %d pF", eight_pico_farad * 8);
+  if (eight_pico_farad > 15)
+    return;
+
+  this->write_register(FREQ_DISP_IRQ, CAP_MASK, eight_pico_farad, 0);
+}
+
+// REG0x03, bits [3:0], manufacturer default: 0.
+// When there is an event that exceeds the watchdog threshold, the register is written
+// with the type of event. This consists of two messages: INT_D (disturber detected) and
+// INT_L (Lightning detected). A third interrupt INT_NH (noise level too HIGH)
+// indicates that the noise level has been exceeded and will persist until the
+// noise has ended. Events are active HIGH. There is a one second window of time to
+// read the interrupt register after lightning is detected, and 1.5 after
+// disturber.
+uint8_t AS3935Component::read_interrupt_register_() {
+  // A 2ms delay is added to allow for the memory register to be populated
+  // after the interrupt pin goes HIGH. See "Interrupt Management" in
+  // datasheet.
+  ESP_LOGV(TAG, "Calling read_interrupt_register_");
+  delay(2);
+  return this->read_register_(INT_MASK_ANT, INT_MASK);
+}
+
+// REG0x02, bit [6], manufacturer default: 1.
+// This register clears the number of lightning strikes that has been read in
+// the last 15 minute block.
+void AS3935Component::clear_statistics_() {
+  // Write high, then low, then high to clear.
+  ESP_LOGV(TAG, "Calling clear_statistics_");
+  this->write_register(LIGHTNING_REG, (1 << 6), 1, 6);
+  this->write_register(LIGHTNING_REG, (1 << 6), 0, 6);
+  this->write_register(LIGHTNING_REG, (1 << 6), 1, 6);
+}
+
+// REG0x07, bit [5:0], manufacturer default: 0.
+// This register holds the distance to the front of the storm and not the
+// distance to a lightning strike.
+uint8_t AS3935Component::get_distance_to_storm_() {
+  ESP_LOGV(TAG, "Calling get_distance_to_storm_");
+  return this->read_register_(DISTANCE, DISTANCE_MASK);
+}
+
+uint32_t AS3935Component::get_lightning_energy_() {
+  ESP_LOGV(TAG, "Calling get_lightning_energy_");
+  uint32_t pure_light = 0;  // Variable for lightning energy which is just a pure number.
+  uint32_t temp = 0;
+  // Temp variable for lightning energy.
+  temp = this->read_register_(ENERGY_LIGHT_MMSB, ENERGY_MASK);
+  // Temporary Value is large enough to handle a shift of 16 bits.
+  pure_light = temp << 16;
+  temp = this->read_register(ENERGY_LIGHT_MSB);
+  // Temporary value is large enough to handle a shift of 8 bits.
+  pure_light |= temp << 8;
+  // No shift here, directly OR'ed into pure_light variable.
+  temp = this->read_register(ENERGY_LIGHT_LSB);
+  pure_light |= temp;
+  return pure_light;
+}
+
+uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) {
+  uint8_t value = this->read_register(reg);
+
+  value &= (~mask);
+  return value;
+}
+
+void ICACHE_RAM_ATTR AS3935ComponentStore::gpio_intr(AS3935ComponentStore *arg) { arg->interrupt = true; }
+
+}  // namespace as3935_base
+}  // namespace esphome
diff --git a/esphome/components/as3935_base/as3935_base.h b/esphome/components/as3935_base/as3935_base.h
new file mode 100644
index 0000000000..f11a589d50
--- /dev/null
+++ b/esphome/components/as3935_base/as3935_base.h
@@ -0,0 +1,102 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace as3935_base {
+
+enum AS3935RegisterNames {
+  AFE_GAIN = 0x00,
+  THRESHOLD,
+  LIGHTNING_REG,
+  INT_MASK_ANT,
+  ENERGY_LIGHT_LSB,
+  ENERGY_LIGHT_MSB,
+  ENERGY_LIGHT_MMSB,
+  DISTANCE,
+  FREQ_DISP_IRQ,
+  CALIB_TRCO = 0x3A,
+  CALIB_SRCO = 0x3B,
+  DEFAULT_RESET = 0x3C,
+  CALIB_RCO = 0x3D
+};
+
+enum AS3935RegisterMasks {
+  GAIN_MASK = 0x3E,
+  SPIKE_MASK = 0xF,
+  IO_MASK = 0xC1,
+  DISTANCE_MASK = 0xC0,
+  INT_MASK = 0xF0,
+  THRESH_MASK = 0x0F,
+  R_SPIKE_MASK = 0xF0,
+  ENERGY_MASK = 0xF0,
+  CAP_MASK = 0xF0,
+  LIGHT_MASK = 0xCF,
+  DISTURB_MASK = 0xDF,
+  NOISE_FLOOR_MASK = 0x70,
+  OSC_MASK = 0xE0,
+  CALIB_MASK = 0x7F,
+  DIV_MASK = 0x3F
+};
+
+enum AS3935Values {
+  AS3935_ADDR = 0x03,
+  INDOOR = 0x12,
+  OUTDOOR = 0xE,
+  LIGHTNING_INT = 0x08,
+  DISTURBER_INT = 0x04,
+  NOISE_INT = 0x01
+};
+
+/// Store data in a class that doesn't use multiple-inheritance (vtables in flash)
+struct AS3935ComponentStore {
+  volatile bool interrupt;
+
+  ISRInternalGPIOPin *pin;
+  static void gpio_intr(AS3935ComponentStore *arg);
+};
+
+class AS3935Component : public Component {
+ public:
+  void setup() override;
+  void dump_config() override;
+  float get_setup_priority() const override;
+  void loop() override;
+
+  void set_pin(GPIOPin *pin) { pin_ = pin; }
+  void set_distance_sensor(sensor::Sensor *distance_sensor) { distance_sensor_ = distance_sensor; }
+  void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; }
+  void set_thunder_alert_binary_sensor(binary_sensor::BinarySensor *thunder_alert_binary_sensor) {
+    thunder_alert_binary_sensor_ = thunder_alert_binary_sensor;
+  }
+  void set_indoor(bool indoor);
+  void set_watchdog_threshold(uint8_t sensitivity);
+  void set_noise_level(uint8_t floor);
+  void set_spike_rejection(uint8_t spike_sensitivity);
+  void set_lightning_threshold(uint8_t strikes);
+  void set_mask_disturber(bool enabled);
+  void set_div_ratio(uint8_t div_ratio);
+  void set_cap(uint8_t eight_pico_farad);
+
+ protected:
+  uint8_t read_interrupt_register_();
+  void clear_statistics_();
+  uint8_t get_distance_to_storm_();
+  uint32_t get_lightning_energy_();
+
+  virtual uint8_t read_register(uint8_t reg) = 0;
+  uint8_t read_register_(uint8_t reg, uint8_t mask);
+
+  virtual void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) = 0;
+
+  sensor::Sensor *distance_sensor_;
+  sensor::Sensor *energy_sensor_;
+  binary_sensor::BinarySensor *thunder_alert_binary_sensor_;
+  GPIOPin *pin_;
+  AS3935ComponentStore store_;
+};
+
+}  // namespace as3935_base
+}  // namespace esphome
diff --git a/esphome/components/as3935_base/binary_sensor.py b/esphome/components/as3935_base/binary_sensor.py
new file mode 100644
index 0000000000..8bef1696d1
--- /dev/null
+++ b/esphome/components/as3935_base/binary_sensor.py
@@ -0,0 +1,16 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import binary_sensor
+from . import AS3935, CONF_AS3935_ID
+
+DEPENDENCIES = ['as3935_base']
+
+CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
+    cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
+})
+
+
+def to_code(config):
+    hub = yield cg.get_variable(config[CONF_AS3935_ID])
+    var = yield binary_sensor.new_binary_sensor(config)
+    cg.add(hub.set_thunder_alert_binary_sensor(var))
diff --git a/esphome/components/as3935_base/sensor.py b/esphome/components/as3935_base/sensor.py
new file mode 100644
index 0000000000..64df519035
--- /dev/null
+++ b/esphome/components/as3935_base/sensor.py
@@ -0,0 +1,30 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import sensor
+from esphome.const import CONF_DISTANCE, CONF_LIGHTNING_ENERGY, \
+    UNIT_KILOMETER, UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH
+from . import AS3935, CONF_AS3935_ID
+
+DEPENDENCIES = ['as3935_base']
+
+CONFIG_SCHEMA = cv.Schema({
+    cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
+    cv.Optional(CONF_DISTANCE):
+        sensor.sensor_schema(UNIT_KILOMETER, ICON_SIGNAL_DISTANCE_VARIANT, 1),
+    cv.Optional(CONF_LIGHTNING_ENERGY):
+        sensor.sensor_schema(UNIT_EMPTY, ICON_FLASH, 1),
+}).extend(cv.COMPONENT_SCHEMA)
+
+
+def to_code(config):
+    hub = yield cg.get_variable(config[CONF_AS3935_ID])
+
+    if CONF_DISTANCE in config:
+        conf = config[CONF_DISTANCE]
+        distance_sensor = yield sensor.new_sensor(conf)
+        cg.add(hub.set_distance_sensor(distance_sensor))
+
+    if CONF_LIGHTNING_ENERGY in config:
+        conf = config[CONF_LIGHTNING_ENERGY]
+        lightning_energy_sensor = yield sensor.new_sensor(conf)
+        cg.add(hub.set_distance_sensor(lightning_energy_sensor))
diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py
new file mode 100644
index 0000000000..a474ad19e5
--- /dev/null
+++ b/esphome/components/as3935_i2c/__init__.py
@@ -0,0 +1,20 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import as3935_base, i2c
+from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES
+
+AUTO_LOAD = ['as3935_base']
+DEPENDENCIES = ['i2c']
+
+as3935_i2c_ns = cg.esphome_ns.namespace('as3935_i2c')
+I2CAS3935 = as3935_i2c_ns.class_('I2CAS3935Component', as3935_base.AS3935, i2c.I2CDevice)
+
+CONFIG_SCHEMA = cv.All(as3935_base.AS3935_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(I2CAS3935),
+}).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x03)))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield as3935_base.setup_as3935(var, config)
+    yield i2c.register_i2c_device(var, config)
diff --git a/esphome/components/as3935_i2c/as3935_i2c.cpp b/esphome/components/as3935_i2c/as3935_i2c.cpp
new file mode 100644
index 0000000000..7792bd3e29
--- /dev/null
+++ b/esphome/components/as3935_i2c/as3935_i2c.cpp
@@ -0,0 +1,36 @@
+#include "as3935_i2c.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace as3935_i2c {
+
+static const char *TAG = "as3935_i2c";
+
+void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) {
+  uint8_t write_reg;
+  if (!this->read_byte(reg, &write_reg)) {
+    this->mark_failed();
+    ESP_LOGW(TAG, "read_byte failed - increase log level for more details!");
+    return;
+  }
+
+  write_reg &= (~mask);
+  write_reg |= (bits << start_pos);
+
+  if (!this->write_byte(reg, write_reg)) {
+    ESP_LOGW(TAG, "write_byte failed - increase log level for more details!");
+    return;
+  }
+}
+
+uint8_t I2CAS3935Component::read_register(uint8_t reg) {
+  uint8_t value;
+  if (!this->read_byte(reg, &value, 2)) {
+    ESP_LOGW(TAG, "Read failed!");
+    return 0;
+  }
+  return value;
+}
+
+}  // namespace as3935_i2c
+}  // namespace esphome
diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h
new file mode 100644
index 0000000000..93cedf537b
--- /dev/null
+++ b/esphome/components/as3935_i2c/as3935_i2c.h
@@ -0,0 +1,21 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/as3935_base/as3935_base.h"
+#include "esphome/components/i2c/i2c.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace as3935_i2c {
+
+class I2CAS3935Component : public as3935_base::AS3935Component, public i2c::I2CDevice {
+ public:
+
+ protected:
+  void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override;
+  uint8_t read_register(uint8_t reg) override;
+};
+
+}  // namespace as3935_i2c
+}  // namespace esphome
diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py
new file mode 100644
index 0000000000..c1daf58150
--- /dev/null
+++ b/esphome/components/as3935_spi/__init__.py
@@ -0,0 +1,20 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome.components import as3935_base, spi
+from esphome.const import CONF_ID
+
+AUTO_LOAD = ['as3935_base']
+DEPENDENCIES = ['spi']
+
+as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi')
+SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935_base.AS3935, spi.SPIDevice)
+
+CONFIG_SCHEMA = cv.All(as3935_base.AS3935_SCHEMA.extend({
+    cv.GenerateID(): cv.declare_id(SPIAS3935)
+}).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA))
+
+
+def to_code(config):
+    var = cg.new_Pvariable(config[CONF_ID])
+    yield as3935_base.setup_as3935(var, config)
+    yield spi.register_spi_device(var, config)
diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp
new file mode 100644
index 0000000000..97847de080
--- /dev/null
+++ b/esphome/components/as3935_spi/as3935_spi.cpp
@@ -0,0 +1,49 @@
+#include "as3935_spi.h"
+#include "esphome/core/log.h"
+
+namespace esphome {
+namespace as3935_spi {
+
+static const char *TAG = "as3935_spi";
+
+void SPIAS3935Component::setup() {
+  ESP_LOGI(TAG, "SPIAS3935Component setup started!");
+  this->spi_setup();
+  ESP_LOGI(TAG, "SPI setup finished!");
+  AS3935Component::setup();
+  
+}
+
+void SPIAS3935Component::dump_config() {
+  AS3935Component::dump_config();
+  LOG_PIN("  CS Pin: ", this->cs_);
+}
+
+void SPIAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_pos) {
+  uint8_t write_reg = this->read_register(reg);
+
+  write_reg &= (~mask);
+  write_reg |= (bits << start_pos);
+
+  this->enable();
+  this->write_byte(reg);
+  this->write_byte(write_reg);
+  this->disable();
+}
+
+uint8_t SPIAS3935Component::read_register(uint8_t reg) {
+  uint8_t value = 0;
+  this->enable();
+  this->write_byte(reg |= SPI_READ_M);
+  value = this->read_byte();
+  // According to datsheet, the chip select must be written HIGH, LOW, HIGH
+  // to correctly end the READ command. 
+  this->cs_->digital_write(true);
+  this->cs_->digital_write(false);
+  this->disable();
+  ESP_LOGV(TAG, "read_register_: %d", value);
+  return value;
+}
+
+}  // namespace as3935_spi
+}  // namespace esphome
diff --git a/esphome/components/as3935_spi/as3935_spi.h b/esphome/components/as3935_spi/as3935_spi.h
new file mode 100644
index 0000000000..ec80d76821
--- /dev/null
+++ b/esphome/components/as3935_spi/as3935_spi.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "esphome/core/component.h"
+#include "esphome/components/as3935_base/as3935_base.h"
+#include "esphome/components/spi/spi.h"
+#include "esphome/components/sensor/sensor.h"
+#include "esphome/components/binary_sensor/binary_sensor.h"
+
+namespace esphome {
+namespace as3935_spi {
+
+enum AS3935RegisterMasks { SPI_READ_M = 0x40 };
+
+class SPIAS3935Component : public as3935_base::AS3935Component,
+                           public spi::SPIDevice {
+ public:
+  void setup() override;
+  void dump_config() override;
+
+ protected:
+  void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override;
+  uint8_t read_register(uint8_t reg) override;
+};
+
+}  // namespace as3935_spi
+}  // namespace esphome
diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp
index 3fa9d2c37a..b93c7d6053 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();
 
diff --git a/esphome/const.py b/esphome/const.py
index 3811d5b9cf..86a0a43bdc 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -63,6 +63,7 @@ CONF_BUSY_PIN = 'busy_pin'
 CONF_BUS_VOLTAGE = 'bus_voltage'
 CONF_CALIBRATE_LINEAR = 'calibrate_linear'
 CONF_CALIBRATION = 'calibration'
+CONF_CAP = 'cap'
 CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent'
 CONF_CARRIER_FREQUENCY = 'carrier_frequency'
 CONF_CHANGE_MODE_EVERY = 'change_mode_every'
@@ -119,8 +120,10 @@ CONF_DIMENSIONS = 'dimensions'
 CONF_DIRECTION = 'direction'
 CONF_DIR_PIN = 'dir_pin'
 CONF_DISCOVERY = 'discovery'
+CONF_DISTANCE = 'distance'
 CONF_DISCOVERY_PREFIX = 'discovery_prefix'
 CONF_DISCOVERY_RETAIN = 'discovery_retain'
+CONF_DIV_RATIO = 'div_ratio'
 CONF_DNS1 = 'dns1'
 CONF_DNS2 = 'dns2'
 CONF_DOMAIN = 'domain'
@@ -183,6 +186,7 @@ CONF_IIR_FILTER = 'iir_filter'
 CONF_ILLUMINANCE = 'illuminance'
 CONF_INCLUDES = 'includes'
 CONF_INDEX = 'index'
+CONF_INDOOR = 'indoor'
 CONF_INITIAL_MODE = 'initial_mode'
 CONF_INITIAL_VALUE = 'initial_value'
 CONF_INTEGRATION_TIME = 'integration_time'
@@ -204,6 +208,8 @@ CONF_LEVEL = 'level'
 CONF_LG = 'lg'
 CONF_LIBRARIES = 'libraries'
 CONF_LIGHT = 'light'
+CONF_LIGHTNING_ENERGY = 'lightning_energy'
+CONF_LIGHTNING_THRESHOLD = 'lightning_threshold'
 CONF_LOADED_INTEGRATIONS = 'loaded_integrations'
 CONF_LOCAL = 'local'
 CONF_LOGGER = 'logger'
@@ -214,6 +220,7 @@ CONF_LOW_VOLTAGE_REFERENCE = 'low_voltage_reference'
 CONF_MAC_ADDRESS = 'mac_address'
 CONF_MAKE_ID = 'make_id'
 CONF_MANUAL_IP = 'manual_ip'
+CONF_MASK_DISTURBER = 'mask_disturber'
 CONF_MAX_CURRENT = 'max_current'
 CONF_MAX_DURATION = 'max_duration'
 CONF_MAX_LENGTH = 'max_length'
@@ -248,6 +255,7 @@ CONF_NAME = 'name'
 CONF_NBITS = 'nbits'
 CONF_NEC = 'nec'
 CONF_NETWORKS = 'networks'
+CONF_NOISE_LEVEL = 'noise_level'
 CONF_NUMBER = 'number'
 CONF_NUM_ATTEMPTS = 'num_attempts'
 CONF_NUM_CHANNELS = 'num_channels'
@@ -383,6 +391,7 @@ CONF_SPEED = 'speed'
 CONF_SPEED_COMMAND_TOPIC = 'speed_command_topic'
 CONF_SPEED_STATE_TOPIC = 'speed_state_topic'
 CONF_SPI_ID = 'spi_id'
+CONF_SPIKE_REJECTION = 'spike_rejection'
 CONF_SSID = 'ssid'
 CONF_SSL_FINGERPRINTS = 'ssl_fingerprints'
 CONF_STATE = 'state'
@@ -449,6 +458,7 @@ CONF_WAIT_UNTIL = 'wait_until'
 CONF_WAKEUP_PIN = 'wakeup_pin'
 CONF_WARM_WHITE = 'warm_white'
 CONF_WARM_WHITE_COLOR_TEMPERATURE = 'warm_white_color_temperature'
+CONF_WATCHDOG_THRESHOLD = 'watchdog_threshold'
 CONF_WHILE = 'while'
 CONF_WHITE = 'white'
 CONF_WIDTH = 'width'
@@ -482,7 +492,8 @@ ICON_RESTART = 'mdi:restart'
 ICON_ROTATE_RIGHT = 'mdi:rotate-right'
 ICON_SCALE = 'mdi:scale'
 ICON_SCREEN_ROTATION = 'mdi:screen-rotation'
-ICON_SIGNAL = 'mdi:signal'
+ICON_SIGNAL = 'mdi: signal-distance-variant'
+ICON_SIGNAL_DISTANCE_VARIANT = 'mdi:signal'
 ICON_SIGN_DIRECTION = 'mdi:sign-direction'
 ICON_WEATHER_SUNSET = 'mdi:weather-sunset'
 ICON_WEATHER_SUNSET_DOWN = 'mdi:weather-sunset-down'
@@ -502,6 +513,7 @@ UNIT_EMPTY = ''
 UNIT_HZ = 'hz'
 UNIT_HECTOPASCAL = 'hPa'
 UNIT_KELVIN = 'K'
+UNIT_KILOMETER = 'km'
 UNIT_KILOMETER_PER_HOUR = 'km/h'
 UNIT_LUX = 'lx'
 UNIT_METER = 'm'
diff --git a/script/quicklint b/script/quicklint
index 06c31d519d..e391ca3276 100755
--- a/script/quicklint
+++ b/script/quicklint
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/bin/bash
 
 set -e
 
diff --git a/script/setup b/script/setup
index a65eb23a63..b6cff39f0c 100755
--- a/script/setup
+++ b/script/setup
@@ -1,4 +1,4 @@
-#!/usr/bin/env bash
+#!/bin/bash
 # Set up ESPHome dev environment
 
 set -e
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 66320f876f..9723daed09 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -171,6 +171,10 @@ ads1115:
 dallas:
   pin: GPIO23
 
+as3935_spi:
+  cs_pin: GPIO12
+  pin: GPIO13
+
 sensor:
   - platform: adc
     pin: A0
@@ -595,7 +599,11 @@ sensor:
       name: "ZyAura Temperature"
     humidity:
       name: "ZyAura Humidity"
-
+  - platform: as3935_base
+    lightning_energy:
+      name: "Lightning Energy"
+    distance:
+      name: "Distance Storm"
 
 esp32_touch:
   setup_mode: False
@@ -730,6 +738,8 @@ binary_sensor:
              3700, -2263, 1712, -4254, 1711, -4249, 1715, -2266, 1710, -2267,
              1709, -2265, 3704, -4250, 1712, -4254, 3700, -2260, 1714, -2265,
              1712, -2262, 1714, -2267, 1709]
+  - platform: as3935_base
+    name: "Storm Alert"
 
 pca9685:
   frequency: 500
diff --git a/tests/test2.yaml b/tests/test2.yaml
index e3a9b0da85..1164ebfe4f 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -50,6 +50,10 @@ deep_sleep:
   run_duration: 20s
   sleep_duration: 50s
 
+as3935_i2c:
+  pin: GPIO12
+
+
 sensor:
   - platform: ble_rssi
     mac_address: AC:37:43:77:5F:4C
@@ -125,6 +129,11 @@ sensor:
   - platform: homeassistant
     entity_id: sensor.hello_world
     id: ha_hello_world
+  - platform: as3935_base
+    lightning_energy:
+      name: "Lightning Energy"
+    distance:
+      name: "Distance Storm"
 
 time:
 - platform: homeassistant
@@ -163,6 +172,8 @@ binary_sensor:
   - platform: homeassistant
     entity_id: binary_sensor.hello_world
     id: ha_hello_world_binary
+  - platform: as3935_base
+    name: "Storm Alert"
 
 remote_receiver:
   pin: GPIO32

From fa351cd37c1cb8f6982124bb4ba4841d297d873a Mon Sep 17 00:00:00 2001
From: Otto Winter 
Date: Sat, 12 Oct 2019 17:03:01 +0200
Subject: [PATCH 161/222] Cleanup AS3935

---
 esphome/components/as3935/__init__.py         |  47 +++++++
 .../as3935_base.cpp => as3935/as3935.cpp}     | 116 +++++++++++-------
 .../as3935_base.h => as3935/as3935.h}         |  37 ++++--
 .../{as3935_base => as3935}/binary_sensor.py  |   2 +-
 .../{as3935_base => as3935}/sensor.py         |   2 +-
 esphome/components/as3935_base/__init__.py    |  54 --------
 esphome/components/as3935_i2c/__init__.py     |  12 +-
 esphome/components/as3935_i2c/as3935_i2c.h    |   6 +-
 esphome/components/as3935_spi/__init__.py     |  10 +-
 esphome/components/as3935_spi/as3935_spi.h    |   4 +-
 esphome/const.py                              |   2 +-
 tests/test1.yaml                              |   4 +-
 tests/test2.yaml                              |   4 +-
 13 files changed, 165 insertions(+), 135 deletions(-)
 create mode 100644 esphome/components/as3935/__init__.py
 rename esphome/components/{as3935_base/as3935_base.cpp => as3935/as3935.cpp} (68%)
 rename esphome/components/{as3935_base/as3935_base.h => as3935/as3935.h} (63%)
 rename esphome/components/{as3935_base => as3935}/binary_sensor.py (93%)
 rename esphome/components/{as3935_base => as3935}/sensor.py (97%)
 delete mode 100644 esphome/components/as3935_base/__init__.py

diff --git a/esphome/components/as3935/__init__.py b/esphome/components/as3935/__init__.py
new file mode 100644
index 0000000000..f8ac4eea01
--- /dev/null
+++ b/esphome/components/as3935/__init__.py
@@ -0,0 +1,47 @@
+import esphome.codegen as cg
+import esphome.config_validation as cv
+from esphome import pins
+from esphome.const import CONF_PIN, CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \
+    CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \
+    CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAPACITANCE
+from esphome.core import coroutine
+
+
+AUTO_LOAD = ['sensor', 'binary_sensor']
+MULTI_CONF = True
+
+CONF_AS3935_ID = 'as3935_id'
+
+as3935_ns = cg.esphome_ns.namespace('as3935')
+AS3935 = as3935_ns.class_('AS3935Component', cg.Component)
+
+AS3935_SCHEMA = cv.Schema({
+    cv.GenerateID(): cv.declare_id(AS3935),
+    cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema,
+                                  pins.validate_has_interrupt),
+
+    cv.Optional(CONF_INDOOR, default=True): cv.boolean,
+    cv.Optional(CONF_NOISE_LEVEL, default=2): cv.int_range(min=1, max=7),
+    cv.Optional(CONF_WATCHDOG_THRESHOLD, default=2): cv.int_range(min=1, max=10),
+    cv.Optional(CONF_SPIKE_REJECTION, default=2): cv.int_range(min=1, max=11),
+    cv.Optional(CONF_LIGHTNING_THRESHOLD, default=1): cv.one_of(1, 5, 9, 16, int=True),
+    cv.Optional(CONF_MASK_DISTURBER, default=False): cv.boolean,
+    cv.Optional(CONF_DIV_RATIO, default=0): cv.one_of(0, 16, 22, 64, 128, int=True),
+    cv.Optional(CONF_CAPACITANCE, default=0): cv.int_range(min=0, max=15),
+})
+
+
+@coroutine
+def setup_as3935(var, config):
+    yield cg.register_component(var, config)
+
+    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
+    cg.add(var.set_pin(pin))
+    cg.add(var.set_indoor(config[CONF_INDOOR]))
+    cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL]))
+    cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD]))
+    cg.add(var.set_spike_rejection(config[CONF_SPIKE_REJECTION]))
+    cg.add(var.set_lightning_threshold(config[CONF_LIGHTNING_THRESHOLD]))
+    cg.add(var.set_mask_disturber(config[CONF_MASK_DISTURBER]))
+    cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))
+    cg.add(var.set_capacitance(config[CONF_CAPACITANCE]))
diff --git a/esphome/components/as3935_base/as3935_base.cpp b/esphome/components/as3935/as3935.cpp
similarity index 68%
rename from esphome/components/as3935_base/as3935_base.cpp
rename to esphome/components/as3935/as3935.cpp
index a0aedb4a16..0fc8f086d2 100644
--- a/esphome/components/as3935_base/as3935_base.cpp
+++ b/esphome/components/as3935/as3935.cpp
@@ -1,10 +1,10 @@
-#include "as3935_base.h"
+#include "as3935.h"
 #include "esphome/core/log.h"
 
 namespace esphome {
-namespace as3935_base {
+namespace as3935 {
 
-static const char *TAG = "as3935_base";
+static const char *TAG = "as3935";
 
 void AS3935Component::setup() {
   ESP_LOGCONFIG(TAG, "Setting up AS3935...");
@@ -13,6 +13,16 @@ void AS3935Component::setup() {
   this->store_.pin = this->pin_->to_isr();
   LOG_PIN("  Interrupt Pin: ", this->pin_);
   this->pin_->attach_interrupt(AS3935ComponentStore::gpio_intr, &this->store_, RISING);
+
+  // Write properties to sensor
+  this->write_indoor(this->indoor_);
+  this->write_noise_level(this->noise_level_);
+  this->write_watchdog_threshold(this->watchdog_threshold_);
+  this->write_spike_rejection(this->spike_rejection_);
+  this->write_lightning_threshold(this->lightning_threshold_);
+  this->write_mask_disturber(this->mask_disturber_);
+  this->write_div_ratio(this->div_ratio_);
+  this->write_capacitance(this->capacitance_);
 }
 
 void AS3935Component::dump_config() {
@@ -46,8 +56,8 @@ void AS3935Component::loop() {
   this->store_.interrupt = false;
 }
 
-void AS3935Component::set_indoor(bool indoor) {
-  ESP_LOGD(TAG, "Setting indoor to %d", indoor);
+void AS3935Component::write_indoor(bool indoor) {
+  ESP_LOGV(TAG, "Setting indoor to %d", indoor);
   if (indoor)
     this->write_register(AFE_GAIN, GAIN_MASK, INDOOR, 1);
   else
@@ -56,11 +66,11 @@ void AS3935Component::set_indoor(bool indoor) {
 // REG0x01, bits[3:0], manufacturer default: 0010 (2).
 // This setting determines the threshold for events that trigger the
 // IRQ Pin.
-void AS3935Component::set_watchdog_threshold(uint8_t sensitivity) {
-  ESP_LOGD(TAG, "Setting watchdog sensitivity to %d", sensitivity);
-  if ((sensitivity < 1) || (sensitivity > 10))  // 10 is the max sensitivity setting
+void AS3935Component::write_watchdog_threshold(uint8_t watchdog_threshold) {
+  ESP_LOGV(TAG, "Setting watchdog sensitivity to %d", watchdog_threshold);
+  if ((watchdog_threshold < 1) || (watchdog_threshold > 10))  // 10 is the max sensitivity setting
     return;
-  this->write_register(THRESHOLD, THRESH_MASK, sensitivity, 0);
+  this->write_register(THRESHOLD, THRESH_MASK, watchdog_threshold, 0);
 }
 
 // REG0x01, bits [6:4], manufacturer default: 010 (2).
@@ -68,44 +78,52 @@ void AS3935Component::set_watchdog_threshold(uint8_t sensitivity) {
 // level is exceeded the chip will issue an interrupt to the IRQ pin,
 // broadcasting that it can not operate properly due to noise (INT_NH).
 // Check datasheet for specific noise level tolerances when setting this register.
-void AS3935Component::set_noise_level(uint8_t floor) {
-  ESP_LOGD(TAG, "Setting noise level to %d", floor);
-  if ((floor < 1) || (floor > 7))
+void AS3935Component::write_noise_level(uint8_t noise_level) {
+  ESP_LOGV(TAG, "Setting noise level to %d", noise_level);
+  if ((noise_level < 1) || (noise_level > 7))
     return;
 
-  this->write_register(THRESHOLD, NOISE_FLOOR_MASK, floor, 4);
+  this->write_register(THRESHOLD, NOISE_FLOOR_MASK, noise_level, 4);
 }
 // REG0x02, bits [3:0], manufacturer default: 0010 (2).
 // This setting, like the watchdog threshold, can help determine between false
 // events and actual lightning. The shape of the spike is analyzed during the
 // chip's signal validation routine. Increasing this value increases robustness
 // at the cost of sensitivity to distant events.
-void AS3935Component::set_spike_rejection(uint8_t spike_sensitivity) {
-  ESP_LOGD(TAG, "Setting spike rejection to %d", spike_sensitivity);
-  if ((spike_sensitivity < 1) || (spike_sensitivity > 11))
+void AS3935Component::write_spike_rejection(uint8_t spike_rejection) {
+  ESP_LOGV(TAG, "Setting spike rejection to %d", spike_rejection);
+  if ((spike_rejection < 1) || (spike_rejection > 11))
     return;
 
-  this->write_register(LIGHTNING_REG, SPIKE_MASK, spike_sensitivity, 0);
+  this->write_register(LIGHTNING_REG, SPIKE_MASK, spike_rejection, 0);
 }
 // REG0x02, bits [5:4], manufacturer default: 0 (single lightning strike).
 // The number of lightning events before IRQ is set high. 15 minutes is The
 // window of time before the number of detected lightning events is reset.
 // The number of lightning strikes can be set to 1,5,9, or 16.
-void AS3935Component::set_lightning_threshold(uint8_t strikes) {
-  ESP_LOGD(TAG, "Setting lightning threshold to %d", strikes);
-  if (strikes == 1)
-    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 0, 4);  // Demonstrative
-  if (strikes == 5)
-    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 4);
-  if (strikes == 9)
-    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 5);
-  if (strikes == 16)
-    this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 3, 4);
+void AS3935Component::write_lightning_threshold(uint8_t lightning_threshold) {
+  ESP_LOGV(TAG, "Setting lightning threshold to %d", lightning_threshold);
+  switch (lightning_threshold) {
+    case 1:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 0, 4);  // Demonstrative
+      break;
+    case 5:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 4);
+      break;
+    case 9:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 1, 5);
+      break;
+    case 16:
+      this->write_register(LIGHTNING_REG, ((1 << 5) | (1 << 4)), 3, 4);
+      break;
+    default:
+      return;
+  }
 }
 // REG0x03, bit [5], manufacturer default: 0.
 // This setting will return whether or not disturbers trigger the IRQ Pin.
-void AS3935Component::set_mask_disturber(bool enabled) {
-  ESP_LOGD(TAG, "Setting mask disturber to %d", enabled);
+void AS3935Component::write_mask_disturber(bool enabled) {
+  ESP_LOGV(TAG, "Setting mask disturber to %d", enabled);
   if (enabled) {
     this->write_register(INT_MASK_ANT, (1 << 5), 1, 5);
   } else {
@@ -116,28 +134,33 @@ void AS3935Component::set_mask_disturber(bool enabled) {
 // The antenna is designed to resonate at 500kHz and so can be tuned with the
 // following setting. The accuracy of the antenna must be within 3.5 percent of
 // that value for proper signal validation and distance estimation.
-void AS3935Component::set_div_ratio(uint8_t div_ratio) {
-  ESP_LOGD(TAG, "Setting div ratio to %d", div_ratio);
-  if (div_ratio == 16)
-    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 0, 6);
-  else if (div_ratio == 22)
-    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 6);
-  else if (div_ratio == 64)
-    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 7);
-  else if (div_ratio == 128)
-    this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 3, 6);
+void AS3935Component::write_div_ratio(uint8_t div_ratio) {
+  ESP_LOGV(TAG, "Setting div ratio to %d", div_ratio);
+  switch (div_ratio) {
+    case 16:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 0, 6);
+      break;
+    case 22:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 6);
+      break;
+    case 64:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 1, 7);
+      break;
+    case 128:
+      this->write_register(INT_MASK_ANT, ((1 << 7) | (1 << 6)), 3, 6);
+      break;
+    default:
+      return;
+  }
 }
 // REG0x08, bits [3:0], manufacturer default: 0.
 // This setting will add capacitance to the series RLC antenna on the product
 // to help tune its resonance. The datasheet specifies being within 3.5 percent
 // of 500kHz to get optimal lightning detection and distance sensing.
 // It's possible to add up to 120pF in steps of 8pF to the antenna.
-void AS3935Component::set_cap(uint8_t eight_pico_farad) {
-  ESP_LOGD(TAG, "Setting tune cap to %d pF", eight_pico_farad * 8);
-  if (eight_pico_farad > 15)
-    return;
-
-  this->write_register(FREQ_DISP_IRQ, CAP_MASK, eight_pico_farad, 0);
+void AS3935Component::write_capacitance(uint8_t capacitance) {
+  ESP_LOGV(TAG, "Setting tune cap to %d pF", capacitance * 8);
+  this->write_register(FREQ_DISP_IRQ, CAP_MASK, capacitance, 0);
 }
 
 // REG0x03, bits [3:0], manufacturer default: 0.
@@ -195,12 +218,11 @@ uint32_t AS3935Component::get_lightning_energy_() {
 
 uint8_t AS3935Component::read_register_(uint8_t reg, uint8_t mask) {
   uint8_t value = this->read_register(reg);
-
   value &= (~mask);
   return value;
 }
 
 void ICACHE_RAM_ATTR AS3935ComponentStore::gpio_intr(AS3935ComponentStore *arg) { arg->interrupt = true; }
 
-}  // namespace as3935_base
+}  // namespace as3935
 }  // namespace esphome
diff --git a/esphome/components/as3935_base/as3935_base.h b/esphome/components/as3935/as3935.h
similarity index 63%
rename from esphome/components/as3935_base/as3935_base.h
rename to esphome/components/as3935/as3935.h
index f11a589d50..ca7d409282 100644
--- a/esphome/components/as3935_base/as3935_base.h
+++ b/esphome/components/as3935/as3935.h
@@ -5,7 +5,7 @@
 #include "esphome/components/binary_sensor/binary_sensor.h"
 
 namespace esphome {
-namespace as3935_base {
+namespace as3935 {
 
 enum AS3935RegisterNames {
   AFE_GAIN = 0x00,
@@ -71,14 +71,22 @@ class AS3935Component : public Component {
   void set_thunder_alert_binary_sensor(binary_sensor::BinarySensor *thunder_alert_binary_sensor) {
     thunder_alert_binary_sensor_ = thunder_alert_binary_sensor;
   }
-  void set_indoor(bool indoor);
-  void set_watchdog_threshold(uint8_t sensitivity);
-  void set_noise_level(uint8_t floor);
-  void set_spike_rejection(uint8_t spike_sensitivity);
-  void set_lightning_threshold(uint8_t strikes);
-  void set_mask_disturber(bool enabled);
-  void set_div_ratio(uint8_t div_ratio);
-  void set_cap(uint8_t eight_pico_farad);
+  void set_indoor(bool indoor) { indoor_ = indoor; }
+  void write_indoor(bool indoor);
+  void set_noise_level(uint8_t noise_level) { noise_level_ = noise_level; }
+  void write_noise_level(uint8_t noise_level);
+  void set_watchdog_threshold(uint8_t watchdog_threshold) { watchdog_threshold_ = watchdog_threshold; }
+  void write_watchdog_threshold(uint8_t watchdog_threshold);
+  void set_spike_rejection(uint8_t spike_rejection) { spike_rejection_ = spike_rejection; }
+  void write_spike_rejection(uint8_t write_spike_rejection);
+  void set_lightning_threshold(uint8_t lightning_threshold) { lightning_threshold_ = lightning_threshold; }
+  void write_lightning_threshold(uint8_t lightning_threshold);
+  void set_mask_disturber(bool mask_disturber) { mask_disturber_ = mask_disturber; }
+  void write_mask_disturber(bool enabled);
+  void set_div_ratio(uint8_t div_ratio) { div_ratio_ = div_ratio; }
+  void write_div_ratio(uint8_t div_ratio);
+  void set_capacitance(uint8_t capacitance) { capacitance_ = capacitance; }
+  void write_capacitance(uint8_t capacitance);
 
  protected:
   uint8_t read_interrupt_register_();
@@ -96,7 +104,16 @@ class AS3935Component : public Component {
   binary_sensor::BinarySensor *thunder_alert_binary_sensor_;
   GPIOPin *pin_;
   AS3935ComponentStore store_;
+
+  bool indoor_;
+  uint8_t noise_level_;
+  uint8_t watchdog_threshold_;
+  uint8_t spike_rejection_;
+  uint8_t lightning_threshold_;
+  bool mask_disturber_;
+  uint8_t div_ratio_;
+  uint8_t capacitance_;
 };
 
-}  // namespace as3935_base
+}  // namespace as3935
 }  // namespace esphome
diff --git a/esphome/components/as3935_base/binary_sensor.py b/esphome/components/as3935/binary_sensor.py
similarity index 93%
rename from esphome/components/as3935_base/binary_sensor.py
rename to esphome/components/as3935/binary_sensor.py
index 8bef1696d1..3748c3484a 100644
--- a/esphome/components/as3935_base/binary_sensor.py
+++ b/esphome/components/as3935/binary_sensor.py
@@ -3,7 +3,7 @@ import esphome.config_validation as cv
 from esphome.components import binary_sensor
 from . import AS3935, CONF_AS3935_ID
 
-DEPENDENCIES = ['as3935_base']
+DEPENDENCIES = ['as3935']
 
 CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend({
     cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
diff --git a/esphome/components/as3935_base/sensor.py b/esphome/components/as3935/sensor.py
similarity index 97%
rename from esphome/components/as3935_base/sensor.py
rename to esphome/components/as3935/sensor.py
index 64df519035..3374ada6a8 100644
--- a/esphome/components/as3935_base/sensor.py
+++ b/esphome/components/as3935/sensor.py
@@ -5,7 +5,7 @@ from esphome.const import CONF_DISTANCE, CONF_LIGHTNING_ENERGY, \
     UNIT_KILOMETER, UNIT_EMPTY, ICON_SIGNAL_DISTANCE_VARIANT, ICON_FLASH
 from . import AS3935, CONF_AS3935_ID
 
-DEPENDENCIES = ['as3935_base']
+DEPENDENCIES = ['as3935']
 
 CONFIG_SCHEMA = cv.Schema({
     cv.GenerateID(CONF_AS3935_ID): cv.use_id(AS3935),
diff --git a/esphome/components/as3935_base/__init__.py b/esphome/components/as3935_base/__init__.py
deleted file mode 100644
index 86b7128bb2..0000000000
--- a/esphome/components/as3935_base/__init__.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import esphome.codegen as cg
-import esphome.config_validation as cv
-from esphome import pins
-from esphome.const import CONF_PIN, CONF_INDOOR, CONF_WATCHDOG_THRESHOLD, \
-    CONF_NOISE_LEVEL, CONF_SPIKE_REJECTION, CONF_LIGHTNING_THRESHOLD, \
-    CONF_MASK_DISTURBER, CONF_DIV_RATIO, CONF_CAP
-from esphome.core import coroutine
-
-
-AUTO_LOAD = ['sensor', 'binary_sensor']
-MULTI_CONF = True
-
-CONF_AS3935_ID = 'as3935_id'
-
-as3935_base_ns = cg.esphome_ns.namespace('as3935_base')
-AS3935 = as3935_base_ns.class_('AS3935Component', cg.Component)
-
-AS3935_SCHEMA = cv.Schema({
-    cv.GenerateID(): cv.declare_id(AS3935),
-    cv.Required(CONF_PIN): cv.All(pins.internal_gpio_input_pin_schema,
-                                  pins.validate_has_interrupt),
-    cv.Optional(CONF_INDOOR): cv.boolean,
-    cv.Optional(CONF_WATCHDOG_THRESHOLD): cv.int_range(min=1, max=10),
-    cv.Optional(CONF_NOISE_LEVEL): cv.int_range(min=1, max=7),
-    cv.Optional(CONF_SPIKE_REJECTION): cv.int_range(min=1, max=11),
-    cv.Optional(CONF_LIGHTNING_THRESHOLD): cv.one_of(0, 1, 5, 9, 16, int=True),
-    cv.Optional(CONF_MASK_DISTURBER): cv.boolean,
-    cv.Optional(CONF_DIV_RATIO): cv.one_of(0, 16, 22, 64, 128, int=True),
-    cv.Optional(CONF_CAP): cv.int_range(min=0, max=15),
-})
-
-
-@coroutine
-def setup_as3935(var, config):
-    yield cg.register_component(var, config)
-
-    pin = yield cg.gpio_pin_expression(config[CONF_PIN])
-    cg.add(var.set_pin(pin))
-    if CONF_INDOOR in config:
-        cg.add(var.set_indoor(config[CONF_INDOOR]))
-    if CONF_WATCHDOG_THRESHOLD in config:
-        cg.add(var.set_watchdog_threshold(config[CONF_WATCHDOG_THRESHOLD]))
-    if CONF_NOISE_LEVEL in config:
-        cg.add(var.set_noise_level(config[CONF_NOISE_LEVEL]))
-    if CONF_SPIKE_REJECTION in config:
-        cg.add(var.set_spike_rejection(config[CONF_SPIKE_REJECTION]))
-    if CONF_LIGHTNING_THRESHOLD in config:
-        cg.add(var.set_lightning_threshold(config[CONF_LIGHTNING_THRESHOLD]))
-    if CONF_MASK_DISTURBER in config:
-        cg.add(var.set_mask_disturber(config[CONF_MASK_DISTURBER]))
-    if CONF_DIV_RATIO in config:
-        cg.add(var.set_div_ratio(config[CONF_DIV_RATIO]))
-    if CONF_CAP in config:
-        cg.add(var.set_cap(config[CONF_CAP]))
diff --git a/esphome/components/as3935_i2c/__init__.py b/esphome/components/as3935_i2c/__init__.py
index a474ad19e5..e22937ab81 100644
--- a/esphome/components/as3935_i2c/__init__.py
+++ b/esphome/components/as3935_i2c/__init__.py
@@ -1,20 +1,20 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import as3935_base, i2c
-from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES
+from esphome.components import as3935, i2c
+from esphome.const import CONF_ID
 
-AUTO_LOAD = ['as3935_base']
+AUTO_LOAD = ['as3935']
 DEPENDENCIES = ['i2c']
 
 as3935_i2c_ns = cg.esphome_ns.namespace('as3935_i2c')
-I2CAS3935 = as3935_i2c_ns.class_('I2CAS3935Component', as3935_base.AS3935, i2c.I2CDevice)
+I2CAS3935 = as3935_i2c_ns.class_('I2CAS3935Component', as3935.AS3935, i2c.I2CDevice)
 
-CONFIG_SCHEMA = cv.All(as3935_base.AS3935_SCHEMA.extend({
+CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(I2CAS3935),
 }).extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x03)))
 
 
 def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield as3935_base.setup_as3935(var, config)
+    yield as3935.setup_as3935(var, config)
     yield i2c.register_i2c_device(var, config)
diff --git a/esphome/components/as3935_i2c/as3935_i2c.h b/esphome/components/as3935_i2c/as3935_i2c.h
index 93cedf537b..0f4167fc64 100644
--- a/esphome/components/as3935_i2c/as3935_i2c.h
+++ b/esphome/components/as3935_i2c/as3935_i2c.h
@@ -1,7 +1,7 @@
 #pragma once
 
 #include "esphome/core/component.h"
-#include "esphome/components/as3935_base/as3935_base.h"
+#include "esphome/components/as3935/as3935.h"
 #include "esphome/components/i2c/i2c.h"
 #include "esphome/components/sensor/sensor.h"
 #include "esphome/components/binary_sensor/binary_sensor.h"
@@ -9,9 +9,7 @@
 namespace esphome {
 namespace as3935_i2c {
 
-class I2CAS3935Component : public as3935_base::AS3935Component, public i2c::I2CDevice {
- public:
-
+class I2CAS3935Component : public as3935::AS3935Component, public i2c::I2CDevice {
  protected:
   void write_register(uint8_t reg, uint8_t mask, uint8_t bits, uint8_t start_position) override;
   uint8_t read_register(uint8_t reg) override;
diff --git a/esphome/components/as3935_spi/__init__.py b/esphome/components/as3935_spi/__init__.py
index c1daf58150..fa27c2b0f5 100644
--- a/esphome/components/as3935_spi/__init__.py
+++ b/esphome/components/as3935_spi/__init__.py
@@ -1,20 +1,20 @@
 import esphome.codegen as cg
 import esphome.config_validation as cv
-from esphome.components import as3935_base, spi
+from esphome.components import as3935, spi
 from esphome.const import CONF_ID
 
-AUTO_LOAD = ['as3935_base']
+AUTO_LOAD = ['as3935']
 DEPENDENCIES = ['spi']
 
 as3935_spi_ns = cg.esphome_ns.namespace('as3935_spi')
-SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935_base.AS3935, spi.SPIDevice)
+SPIAS3935 = as3935_spi_ns.class_('SPIAS3935Component', as3935.AS3935, spi.SPIDevice)
 
-CONFIG_SCHEMA = cv.All(as3935_base.AS3935_SCHEMA.extend({
+CONFIG_SCHEMA = cv.All(as3935.AS3935_SCHEMA.extend({
     cv.GenerateID(): cv.declare_id(SPIAS3935)
 }).extend(cv.COMPONENT_SCHEMA).extend(spi.SPI_DEVICE_SCHEMA))
 
 
 def to_code(config):
     var = cg.new_Pvariable(config[CONF_ID])
-    yield as3935_base.setup_as3935(var, config)
+    yield as3935.setup_as3935(var, config)
     yield spi.register_spi_device(var, config)
diff --git a/esphome/components/as3935_spi/as3935_spi.h b/esphome/components/as3935_spi/as3935_spi.h
index ec80d76821..073f5c09a4 100644
--- a/esphome/components/as3935_spi/as3935_spi.h
+++ b/esphome/components/as3935_spi/as3935_spi.h
@@ -1,7 +1,7 @@
 #pragma once
 
 #include "esphome/core/component.h"
-#include "esphome/components/as3935_base/as3935_base.h"
+#include "esphome/components/as3935/as3935.h"
 #include "esphome/components/spi/spi.h"
 #include "esphome/components/sensor/sensor.h"
 #include "esphome/components/binary_sensor/binary_sensor.h"
@@ -11,7 +11,7 @@ namespace as3935_spi {
 
 enum AS3935RegisterMasks { SPI_READ_M = 0x40 };
 
-class SPIAS3935Component : public as3935_base::AS3935Component,
+class SPIAS3935Component : public as3935::AS3935Component,
                            public spi::SPIDevice {
  public:
diff --git a/esphome/const.py b/esphome/const.py
index 86a0a43bdc..8ea20773bc 100644
--- a/esphome/const.py
+++ b/esphome/const.py
@@ -63,7 +63,7 @@ CONF_BUSY_PIN = 'busy_pin'
 CONF_BUS_VOLTAGE = 'bus_voltage'
 CONF_CALIBRATE_LINEAR = 'calibrate_linear'
 CONF_CALIBRATION = 'calibration'
-CONF_CAP = 'cap'
+CONF_CAPACITANCE = 'capacitance'
 CONF_CARRIER_DUTY_PERCENT = 'carrier_duty_percent'
 CONF_CARRIER_FREQUENCY = 'carrier_frequency'
 CONF_CHANGE_MODE_EVERY = 'change_mode_every'
diff --git a/tests/test1.yaml b/tests/test1.yaml
index 9723daed09..4a68b58edd 100644
--- a/tests/test1.yaml
+++ b/tests/test1.yaml
@@ -599,7 +599,7 @@ sensor:
       name: "ZyAura Temperature"
     humidity:
       name: "ZyAura Humidity"
-  - platform: as3935_base
+  - platform: as3935
     lightning_energy:
       name: "Lightning Energy"
     distance:
@@ -738,7 +738,7 @@ binary_sensor:
              3700, -2263, 1712, -4254, 1711, -4249, 1715, -2266, 1710, -2267,
              1709, -2265, 3704, -4250, 1712, -4254, 3700, -2260, 1714, -2265,
              1712, -2262, 1714, -2267, 1709]
-  - platform: as3935_base
+  - platform: as3935
     name: "Storm Alert"
 
 pca9685:
diff --git a/tests/test2.yaml b/tests/test2.yaml
index 1164ebfe4f..fc72655056 100644
--- a/tests/test2.yaml
+++ b/tests/test2.yaml
@@ -129,7 +129,7 @@ sensor:
   - platform: homeassistant
     entity_id: sensor.hello_world
     id: ha_hello_world
-  - platform: as3935_base
+  - platform: as3935
     lightning_energy:
       name: "Lightning Energy"
     distance:
@@ -172,7 +172,7 @@ binary_sensor:
   - platform: homeassistant
     entity_id: binary_sensor.hello_world
     id: ha_hello_world_binary
-  - platform: as3935_base
+  - platform: as3935
     name: "Storm Alert"
 
 remote_receiver:

From 57bee74225984704e3361bc7a7d28ffe3f5cd9a4 Mon Sep 17 00:00:00 2001
From: Nikolay Vasilchuk 
Date: Sun, 13 Oct 2019 13:55:26 +0300
Subject: [PATCH 162/222] Fill log height (#673)

---
 esphome/dashboard/static/esphome.css | 1 +
 1 file changed, 1 insertion(+)

diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css
index 5300e73861..183cfbed39 100644
--- a/esphome/dashboard/static/esphome.css
+++ b/esphome/dashboard/static/esphome.css
@@ -47,6 +47,7 @@ i.very-large {
 }
 
 .log {
+  height: 100%;
   max-height: calc(100% - 56px);
   background-color: #1c1c1c;
   margin-top: 0;

From 7c315928508f16802dabd2eab59214fc3c67ca6c Mon Sep 17 00:00:00 2001
From: Nikolay Vasilchuk 
Date: Sun, 13 Oct 2019 13:57:28 +0300
Subject: [PATCH 163/222] Secrets editor (#672)

* Secrets editor

* Check file exists
---
 esphome/dashboard/dashboard.py         | 9 ++++++---
 esphome/dashboard/static/esphome.js    | 7 ++++++-
 esphome/dashboard/templates/index.html | 4 ++--
 3 files changed, 14 insertions(+), 6 deletions(-)

diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py
index 60acc1edf3..5382ef855c 100644
--- a/esphome/dashboard/dashboard.py
+++ b/esphome/dashboard/dashboard.py
@@ -530,9 +530,12 @@ class EditRequestHandler(BaseHandler):
     @authenticated
     @bind_config
     def get(self, configuration=None):
-        # pylint: disable=no-value-for-parameter
-        with open(settings.rel_path(configuration), 'r') as f:
-            content = f.read()
+        filename = settings.rel_path(configuration)
+        content = ''
+        if os.path.isfile(filename):
+            # pylint: disable=no-value-for-parameter
+            with open(filename, 'r') as f:
+                content = f.read()
         self.write(content)
 
     @authenticated
diff --git a/esphome/dashboard/static/esphome.js b/esphome/dashboard/static/esphome.js
index 2fb61be9cc..e56521c945 100644
--- a/esphome/dashboard/static/esphome.js
+++ b/esphome/dashboard/static/esphome.js
@@ -574,6 +574,7 @@ const editModalElem = document.getElementById("modal-editor");
 const editorElem = editModalElem.querySelector("#editor");
 const editor = ace.edit(editorElem);
 let activeEditorConfig = null;
+let activeEditorSecrets = false;
 let aceWs = null;
 let aceValidationScheduled = false;
 let aceValidationRunning = false;
@@ -685,7 +686,7 @@ editor.commands.addCommand({
 });
 
 editor.session.on('change', debounce(() => {
-  aceValidationScheduled = true;
+  aceValidationScheduled = !activeEditorSecrets;
 }, 250));
 
 setInterval(() => {
@@ -708,9 +709,13 @@ editorUploadButton.addEventListener('click', saveEditor);
 document.querySelectorAll(".action-edit").forEach((btn) => {
   btn.addEventListener('click', (e) => {
     activeEditorConfig = e.target.getAttribute('data-node');
+    activeEditorSecrets = activeEditorConfig === 'secrets.yaml';
     const modalInstance = M.Modal.getInstance(editModalElem);
     const filenameField = editModalElem.querySelector('.filename');
     editorUploadButton.setAttribute('data-node', activeEditorConfig);
+    if (activeEditorSecrets) {
+      editorUploadButton.classList.add('disabled');
+    }
     filenameField.innerHTML = activeEditorConfig;
 
     editor.setValue("Loading configuration yaml...");
diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html
index fe80392fe6..077cc7a3ba 100644
--- a/esphome/dashboard/templates/index.html
+++ b/esphome/dashboard/templates/index.html
@@ -38,8 +38,8 @@
   
From 38dfab11b49b9096f63681d286eb7e95de2aca07 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Sun, 13 Oct 2019 13:51:34 +0200 Subject: [PATCH 164/222] Fix dev branch --- esphome/components/as3935_spi/as3935_spi.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/as3935_spi/as3935_spi.cpp b/esphome/components/as3935_spi/as3935_spi.cpp index 97847de080..73752a8ee0 100644 --- a/esphome/components/as3935_spi/as3935_spi.cpp +++ b/esphome/components/as3935_spi/as3935_spi.cpp @@ -11,7 +11,6 @@ void SPIAS3935Component::setup() { this->spi_setup(); ESP_LOGI(TAG, "SPI setup finished!"); AS3935Component::setup(); - } void SPIAS3935Component::dump_config() { @@ -37,7 +36,7 @@ uint8_t SPIAS3935Component::read_register(uint8_t reg) { this->write_byte(reg |= SPI_READ_M); value = this->read_byte(); // According to datsheet, the chip select must be written HIGH, LOW, HIGH - // to correctly end the READ command. + // to correctly end the READ command. this->cs_->digital_write(true); this->cs_->digital_write(false); this->disable(); From 1a763ae97437c555c95053c25e96167e00a25401 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Sun, 13 Oct 2019 14:52:02 +0300 Subject: [PATCH 165/222] Authorization by username and password (#668) * Auth * Logout * Lint fix * Small hassio fix * Reverted uppercase * Secrets editor * Reverted secrets editor * Reverted log height * Fix default username --- docker/Dockerfile | 3 ++ esphome/__main__.py | 6 ++- esphome/dashboard/dashboard.py | 58 ++++++++++++++++---------- esphome/dashboard/templates/index.html | 3 +- esphome/dashboard/templates/login.html | 18 ++++---- 5 files changed, 58 insertions(+), 30 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a5edeeec00..9a6ebb1564 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -4,6 +4,9 @@ FROM ${BUILD_FROM} COPY . . RUN pip2 install --no-cache-dir -e . +ENV USERNAME="" +ENV PASSWORD="" + WORKDIR /config ENTRYPOINT ["esphome"] CMD ["/config", "dashboard"] diff --git a/esphome/__main__.py b/esphome/__main__.py index bb9f789600..9c0b7a33d9 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -477,7 +477,11 @@ def parse_args(argv): help="Create a simple web server for a dashboard.") dashboard.add_argument("--port", help="The HTTP port to open connections on. Defaults to 6052.", type=int, default=6052) - dashboard.add_argument("--password", help="The optional password to require for all requests.", + dashboard.add_argument("--username", help="The optional username to require " + "for authentication.", + type=str, default='') + dashboard.add_argument("--password", help="The optional password to require " + "for authentication.", type=str, default='') dashboard.add_argument("--open-ui", help="Open the dashboard UI in a browser.", action='store_true') diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 5382ef855c..cc89fbd881 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -46,19 +46,22 @@ class DashboardSettings(object): def __init__(self): self.config_dir = '' self.password_digest = '' + self.username = '' self.using_password = False self.on_hassio = False self.cookie_secret = None def parse_args(self, args): self.on_hassio = args.hassio + password = args.password or os.getenv('PASSWORD', '') if not self.on_hassio: - self.using_password = bool(args.password) + self.username = args.username or os.getenv('USERNAME', '') + self.using_password = bool(password) if self.using_password: if IS_PY2: - self.password_digest = hmac.new(args.password).digest() + self.password_digest = hmac.new(password).digest() else: - self.password_digest = hmac.new(args.password.encode()).digest() + self.password_digest = hmac.new(password.encode()).digest() self.config_dir = args.configuration[0] @property @@ -79,7 +82,7 @@ class DashboardSettings(object): def using_auth(self): return self.using_password or self.using_hassio_auth - def check_password(self, password): + def check_password(self, username, password): if not self.using_auth: return True @@ -87,7 +90,7 @@ class DashboardSettings(object): password = hmac.new(password).digest() else: password = hmac.new(password.encode()).digest() - return hmac.compare_digest(self.password_digest, password) + return username == self.username and hmac.compare_digest(self.password_digest, password) def rel_path(self, *args): return os.path.join(self.config_dir, *args) @@ -585,16 +588,14 @@ PING_REQUEST = threading.Event() class LoginHandler(BaseHandler): def get(self): - if settings.using_hassio_auth: - self.render_hassio_login() - return - self.write('
' - 'Password: ' - '' - '
') + if is_authenticated(self): + self.redirect('/') + else: + self.render_login_page() - def render_hassio_login(self, error=None): - self.render("templates/login.html", error=error, **template_args()) + def render_login_page(self, error=None): + self.render("templates/login.html", error=error, hassio=settings.using_hassio_auth, + has_username=bool(settings.username), **template_args()) def post_hassio_login(self): import requests @@ -615,20 +616,34 @@ class LoginHandler(BaseHandler): except Exception as err: # pylint: disable=broad-except _LOGGER.warning("Error during Hass.io auth request: %s", err) self.set_status(500) - self.render_hassio_login(error="Internal server error") + self.render_login_page(error="Internal server error") return self.set_status(401) - self.render_hassio_login(error="Invalid username or password") + self.render_login_page(error="Invalid username or password") + + def post_native_login(self): + username = str(self.get_argument("username", '').encode('utf-8')) + password = str(self.get_argument("password", '').encode('utf-8')) + if settings.check_password(username, password): + self.set_secure_cookie("authenticated", cookie_authenticated_yes) + self.redirect("/") + return + error_str = "Invalid username or password" if settings.username else "Invalid password" + self.set_status(401) + self.render_login_page(error=error_str) def post(self): if settings.using_hassio_auth: self.post_hassio_login() - return + else: + self.post_native_login() - password = str(self.get_argument("password", '')) - if settings.check_password(password): - self.set_secure_cookie("authenticated", cookie_authenticated_yes) - self.redirect("/") + +class LogoutHandler(BaseHandler): + @authenticated + def get(self): + self.clear_cookie("authenticated") + self.redirect('./login') _STATIC_FILE_HASHES = {} @@ -681,6 +696,7 @@ def make_app(debug=False): app = tornado.web.Application([ (rel + "", MainRequestHandler), (rel + "login", LoginHandler), + (rel + "logout", LogoutHandler), (rel + "logs", EsphomeLogsHandler), (rel + "upload", EsphomeUploadHandler), (rel + "compile", EsphomeCompileHandler), diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 077cc7a3ba..1539632e78 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -38,8 +38,9 @@ diff --git a/esphome/dashboard/templates/login.html b/esphome/dashboard/templates/login.html index d7b73fd0bb..414617c17f 100644 --- a/esphome/dashboard/templates/login.html +++ b/esphome/dashboard/templates/login.html @@ -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 %}
From b2388b6fe73d986e614172946ea565ab66997e9b Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Sun, 13 Oct 2019 15:27:44 +0300 Subject: [PATCH 166/222] Basic Auth for web_server component (#674) * Basic auth * Test * Linter fix * Make username/password strict strings Reason: passwords only consisting of digits (012345) will be silently converted (to "12345") Co-authored-by: Otto Winter --- esphome/components/web_server/__init__.py | 11 ++++++++++- esphome/components/web_server/web_server.cpp | 7 +++++++ esphome/components/web_server/web_server.h | 9 +++++++++ esphome/const.py | 1 + tests/test2.yaml | 3 +++ 5 files changed, 30 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index ea7b179d1e..04f3cc5c04 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -2,7 +2,9 @@ import esphome.codegen as cg import esphome.config_validation as cv 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 +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 AUTO_LOAD = ['json', 'web_server_base'] @@ -15,6 +17,10 @@ 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) @@ -30,3 +36,6 @@ def to_code(config): 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 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 3c244e9666..b51ad2cf51 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -119,6 +119,9 @@ void WebServer::setup() { void WebServer::dump_config() { ESP_LOGCONFIG(TAG, "Web Server:"); 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; } @@ -490,6 +493,10 @@ bool WebServer::canHandle(AsyncWebServerRequest *request) { return false; } void WebServer::handleRequest(AsyncWebServerRequest *request) { + if (this->using_auth() && !request->authenticate(this->username_, this->password_)) { + return request->requestAuthentication(); + } + if (request->url() == "/") { this->handle_index_request(request); return; diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 7ecfbb3f3d..4dca8200cc 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -29,6 +29,11 @@ struct UrlMatch { class WebServer : public Controller, public Component, public AsyncWebHandler { public: WebServer(web_server_base::WebServerBase *base) : base_(base) {} + + void set_username(const char *username) { username_ = username; } + + void set_password(const char *password) { password_ = password; } + /** Set the URL to the CSS that's sent to each client. Defaults to * https://esphome.io/_static/webserver-v1.min.css * @@ -56,6 +61,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { /// Handle an index request under '/'. void handle_index_request(AsyncWebServerRequest *request); + bool using_auth() { return username_ != nullptr && password_ != nullptr; } + #ifdef USE_SENSOR void on_sensor_update(sensor::Sensor *obj, float state) override; /// Handle a sensor request under '/sensor/'. @@ -125,6 +132,8 @@ class WebServer : public Controller, public Component, public AsyncWebHandler { protected: web_server_base::WebServerBase *base_; AsyncEventSource events_{"/events"}; + const char *username_{nullptr}; + const char *password_{nullptr}; const char *css_url_{nullptr}; const char *js_url_{nullptr}; }; diff --git a/esphome/const.py b/esphome/const.py index 8ea20773bc..463c82bbc6 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -38,6 +38,7 @@ CONF_ARGS = 'args' CONF_ASSUMED_STATE = 'assumed_state' CONF_AT = 'at' CONF_ATTENUATION = 'attenuation' +CONF_AUTH = 'auth' CONF_AUTOMATION_ID = 'automation_id' CONF_AVAILABILITY = 'availability' CONF_AWAY = 'away' diff --git a/tests/test2.yaml b/tests/test2.yaml index fc72655056..b0dfc27e96 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -45,6 +45,9 @@ logger: level: DEBUG web_server: + auth: + username: admin + password: admin deep_sleep: run_duration: 20s From 0eadda77b02ab1d907a9cfdd1d407dffd8e2b1d3 Mon Sep 17 00:00:00 2001 From: Nad <15346053+valordk@users.noreply.github.com> Date: Sun, 13 Oct 2019 17:46:21 +0200 Subject: [PATCH 167/222] Improve SHT3xD reconnect handling (#675) * Add support for Sensirion STS3x Temperature sensors * Removed humidty reading from STS3x sensor * Fixed line error and operand error * Fixed syntax * Add test snippet for STS3x sensor * Clean up * #550 Fix STH3x component reporting WARNING status and reinitialzing the sensor upon reconnecting. * #550 Fix lint issues * Delete __init__.py * Delete sensor.py * Delete sts3x.cpp * Delete sts3x.h * Delete test1.yaml * Revert "Delete test1.yaml" This reverts commit 33e69fb703d9e97d6308ab7d93bf22494e7b1a5b. * Removed leaked STS3x changes from test --- esphome/components/sht3xd/sht3xd.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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]; From be91cfb261f67f4054d7875c2e49342ca112c7d7 Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Mon, 14 Oct 2019 12:27:07 +0300 Subject: [PATCH 168/222] Device description in dashboard (#707) * Description * Review fixes * Test * Label * Description renamed to comment --- esphome/const.py | 1 + esphome/core.py | 11 +++++++++-- esphome/core_config.py | 3 ++- esphome/dashboard/dashboard.py | 6 ++++++ esphome/dashboard/static/esphome.css | 7 +++++++ esphome/dashboard/templates/index.html | 5 +++++ esphome/storage_json.py | 10 ++++++++-- tests/test3.yaml | 2 ++ 8 files changed, 40 insertions(+), 5 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index 463c82bbc6..c169bd1393 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -86,6 +86,7 @@ CONF_COLOR_CORRECT = 'color_correct' CONF_COLOR_TEMPERATURE = 'color_temperature' CONF_COMMAND = 'command' CONF_COMMAND_TOPIC = 'command_topic' +CONF_COMMENT = 'comment' CONF_COMMIT = 'commit' CONF_COMPONENTS = 'components' CONF_COMPONENT_ID = 'component_id' diff --git a/esphome/core.py b/esphome/core.py index 814b92802e..b1fd866c76 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -10,8 +10,8 @@ import re # pylint: disable=unused-import, wrong-import-order from typing import Any, Dict, List # noqa -from esphome.const import CONF_ARDUINO_VERSION, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI, \ - SOURCE_FILE_EXTENSIONS +from esphome.const import CONF_ARDUINO_VERSION, SOURCE_FILE_EXTENSIONS, \ + CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, CONF_WIFI from esphome.helpers import ensure_unique_string, is_hassio from esphome.py_compat import IS_PY2, integer_types, text_type, string_types from esphome.util import OrderedDict @@ -539,6 +539,13 @@ class EsphomeCore(object): return None + @property + def comment(self): # type: () -> str + if CONF_COMMENT in self.config[CONF_ESPHOME]: + return self.config[CONF_ESPHOME][CONF_COMMENT] + + return None + def _add_active_coroutine(self, instance_id, obj): self.active_coroutines[instance_id] = obj diff --git a/esphome/core_config.py b/esphome/core_config.py index 573094db5c..7654544379 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -7,7 +7,7 @@ import esphome.config_validation as cv from esphome import automation, pins from esphome.const import ARDUINO_VERSION_ESP32_DEV, ARDUINO_VERSION_ESP8266_DEV, \ CONF_ARDUINO_VERSION, CONF_BOARD, CONF_BOARD_FLASH_MODE, CONF_BUILD_PATH, \ - CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ + CONF_COMMENT, CONF_ESPHOME, CONF_INCLUDES, CONF_LIBRARIES, \ CONF_NAME, CONF_ON_BOOT, CONF_ON_LOOP, CONF_ON_SHUTDOWN, CONF_PLATFORM, \ CONF_PLATFORMIO_OPTIONS, CONF_PRIORITY, CONF_TRIGGER_ID, \ CONF_ESP8266_RESTORE_FROM_FLASH, ARDUINO_VERSION_ESP8266_2_3_0, \ @@ -113,6 +113,7 @@ CONFIG_SCHEMA = cv.Schema({ cv.Required(CONF_NAME): cv.valid_name, cv.Required(CONF_PLATFORM): cv.one_of('ESP8266', 'ESP32', upper=True), cv.Required(CONF_BOARD): validate_board, + cv.Optional(CONF_COMMENT): cv.string, cv.Optional(CONF_ARDUINO_VERSION, default='recommended'): validate_arduino_version, cv.Optional(CONF_BUILD_PATH, default=default_build_path): cv.string, cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema({ diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index cc89fbd881..0927fe2dfe 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -401,6 +401,12 @@ class DashboardEntry(object): return self.filename[:-len('.yaml')] return self.storage.name + @property + def comment(self): + if self.storage is None: + return None + return self.storage.comment + @property def esp_platform(self): if self.storage is None: diff --git a/esphome/dashboard/static/esphome.css b/esphome/dashboard/static/esphome.css index 183cfbed39..fddfb5cf86 100644 --- a/esphome/dashboard/static/esphome.css +++ b/esphome/dashboard/static/esphome.css @@ -247,3 +247,10 @@ ul.stepper:not(.horizontal) .step.active::before, ul.stepper:not(.horizontal) .s padding: 10px 15px; margin-top: 15px; } + +.card-comment { + margin-bottom: 8px; + font-size: 14px; + color: #444; + font-style: italic; +} diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 1539632e78..2d713c8ef3 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -79,6 +79,11 @@ {% end %} more_vert + {% if entry.comment %} +
+ {{ escape(entry.comment) }} +
+ {% end %}

diff --git a/esphome/storage_json.py b/esphome/storage_json.py index b04f056f11..a7431d0c56 100644 --- a/esphome/storage_json.py +++ b/esphome/storage_json.py @@ -35,7 +35,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 +43,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 +71,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, @@ -93,6 +96,7 @@ class StorageJSON(object): 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, @@ -128,6 +133,7 @@ class StorageJSON(object): storage = json.loads(text, encoding='utf-8') 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 +143,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) diff --git a/tests/test3.yaml b/tests/test3.yaml index 797153bb12..458021d0d3 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1,5 +1,6 @@ esphome: name: $devicename + comment: $devicecomment platform: ESP8266 board: d1_mini build_path: build/test3 @@ -12,6 +13,7 @@ esphome: substitutions: devicename: test3 + devicecomment: test3 device api: port: 8000 From 5f2808ec2f8efb2735439ec78ed038232e218d5c Mon Sep 17 00:00:00 2001 From: Michiel van Turnhout Date: Mon, 14 Oct 2019 05:27:50 -0400 Subject: [PATCH 169/222] support for the sx1509 i2c device (#651) * added ANALOG_OUTPUT as first functionality * added gpio * seperated the code for different functions * fixed code * Revert "fixed code" This reverts commit 0c6eacb225522f7d738fa371d0db976a97f2f3e8. * add timings for breathe and blink * made the sx1509_float_output am output component * add keypad * implementation for sx1509 keypad * keypad code cleanup and first device tests * debounce * keypad working now. * update for timings. does not compile yet * added all options for breathe and blink fixed var namings * blink and breath still not ok * fixed ms for timings * sync with repo * fixed issue with gpio pin output * code cleanup * lint * more lint * remove log from header * Update esphome/components/sx1509/__init__.py Co-Authored-By: Otto Winter * review * feedback * fixed review issues did some extended testing with mqtt spy * code cleanup (comments) * fixed row col swap for binarysensor_keypad * flake and lint * travis * travis * travis * Update esphome/components/sx1509/sx1509.cpp Co-Authored-By: Otto Winter * review * separated platforms * code cleanup * travis relative paths in python * remove blink/breathe code cleanup * cpp lint * feedback * travis * lint line to long * check keypad settings to be valid * clang * keypad config * text * Remove wrong .gitignore from .gitignore * Remove .pio folder from .gitignore (merge) * Formatting * Formatting * Add i2c log in dump_config * Remove unused variables * Disable static for header files We don't need internal linkage * Use consistent member default argument style * Run clang-format Co-authored-by: null Co-authored-by: Otto Winter --- esphome/components/sx1509/__init__.py | 77 ++++++ .../sx1509/binary_sensor/__init__.py | 28 ++ .../sx1509_binary_keypad_sensor.h | 19 ++ esphome/components/sx1509/output/__init__.py | 25 ++ .../sx1509/output/sx1509_float_output.cpp | 30 +++ .../sx1509/output/sx1509_float_output.h | 27 ++ esphome/components/sx1509/sx1509.cpp | 253 ++++++++++++++++++ esphome/components/sx1509/sx1509.h | 89 ++++++ esphome/components/sx1509/sx1509_gpio_pin.cpp | 20 ++ esphome/components/sx1509/sx1509_gpio_pin.h | 24 ++ esphome/components/sx1509/sx1509_registers.h | 109 ++++++++ 11 files changed, 701 insertions(+) create mode 100644 esphome/components/sx1509/__init__.py create mode 100644 esphome/components/sx1509/binary_sensor/__init__.py create mode 100644 esphome/components/sx1509/binary_sensor/sx1509_binary_keypad_sensor.h create mode 100644 esphome/components/sx1509/output/__init__.py create mode 100644 esphome/components/sx1509/output/sx1509_float_output.cpp create mode 100644 esphome/components/sx1509/output/sx1509_float_output.h create mode 100644 esphome/components/sx1509/sx1509.cpp create mode 100644 esphome/components/sx1509/sx1509.h create mode 100644 esphome/components/sx1509/sx1509_gpio_pin.cpp create mode 100644 esphome/components/sx1509/sx1509_gpio_pin.h create mode 100644 esphome/components/sx1509/sx1509_registers.h 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..e780505edb --- /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_COLUMN = '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_COLUMN): 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_COLUMN])) + + 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 From 9d7f76773d87ec37a6279b6fd7243af700e3b58c Mon Sep 17 00:00:00 2001 From: Levente Tamas Date: Mon, 14 Oct 2019 12:30:41 +0300 Subject: [PATCH 170/222] Add support for TI TLC59208F (#718) * Add support for TI TLC59208F The chip is a 8-BIT FM+ I2C BUS LED DRIVER with 8 open-drain output channels. Its features include: - 256 linear levels - group dimming - group blinking - 64 slave addresses - customizable sub addresses and all call address - output update on stop or on ACK - 3.3V or 5V supply with 5V tolerant IO - no glitch startup - 50mA / output continuous current up to 17V * Convert macro to uint8_t Variables had to be renamed, clang-format would protest against mixed case in global variable name. * Change gen-call reset to use the correct i2c bus --- esphome/components/tlc59208f/__init__.py | 20 +++ esphome/components/tlc59208f/output.py | 24 +++ .../components/tlc59208f/tlc59208f_output.cpp | 155 ++++++++++++++++++ .../components/tlc59208f/tlc59208f_output.h | 67 ++++++++ tests/test1.yaml | 44 +++++ 5 files changed, 310 insertions(+) create mode 100644 esphome/components/tlc59208f/__init__.py create mode 100644 esphome/components/tlc59208f/output.py create mode 100644 esphome/components/tlc59208f/tlc59208f_output.cpp create mode 100644 esphome/components/tlc59208f/tlc59208f_output.h 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/tests/test1.yaml b/tests/test1.yaml index 4a68b58edd..1fed061275 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -745,6 +745,14 @@ pca9685: frequency: 500 address: 0x0 +tlc59208f: + - address: 0x20 + id: tlc59208f_1 + - address: 0x22 + id: tlc59208f_2 + - address: 0x24 + id: tlc59208f_3 + my9231: data_pin: GPIO12 clock_pin: GPIO14 @@ -789,6 +797,42 @@ output: - platform: pca9685 id: pca_7 channel: 7 + - platform: tlc59208f + id: tlc_0 + channel: 0 + tlc59208f_id: 'tlc59208f_1' + - platform: tlc59208f + id: tlc_1 + channel: 1 + tlc59208f_id: 'tlc59208f_1' + - platform: tlc59208f + id: tlc_2 + channel: 2 + tlc59208f_id: 'tlc59208f_1' + - platform: tlc59208f + id: tlc_3 + channel: 0 + tlc59208f_id: 'tlc59208f_2' + - platform: tlc59208f + id: tlc_4 + channel: 1 + tlc59208f_id: 'tlc59208f_2' + - platform: tlc59208f + id: tlc_5 + channel: 2 + tlc59208f_id: 'tlc59208f_2' + - platform: tlc59208f + id: tlc_6 + channel: 0 + tlc59208f_id: 'tlc59208f_3' + - platform: tlc59208f + id: tlc_7 + channel: 1 + tlc59208f_id: 'tlc59208f_3' + - platform: tlc59208f + id: tlc_8 + channel: 2 + tlc59208f_id: 'tlc59208f_3' - platform: gpio id: id2 pin: From e207c6ad841240a2b4e3b5012140e37a0106e55e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Oct 2019 12:06:23 +0200 Subject: [PATCH 171/222] Fix ct_clamp update Fixes https://github.com/esphome/issues/issues/684 --- esphome/components/ct_clamp/ct_clamp_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ct_clamp/ct_clamp_sensor.cpp b/esphome/components/ct_clamp/ct_clamp_sensor.cpp index 8819f9711e..674cc0ae98 100644 --- a/esphome/components/ct_clamp/ct_clamp_sensor.cpp +++ b/esphome/components/ct_clamp/ct_clamp_sensor.cpp @@ -59,7 +59,7 @@ void CTClampSensor::update() { } void CTClampSensor::loop() { - if (!this->is_sampling_ || !this->is_calibrating_offset_) + if (!this->is_sampling_ && !this->is_calibrating_offset_) return; // Perform a single sample From e30512931b0602b1558ae8b73205a56cd4a098db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Mayoral=20Mart=C3=ADnez?= Date: Mon, 14 Oct 2019 13:25:08 +0200 Subject: [PATCH 172/222] Add Xiaomi Cleargrass Temperature and Humidity Sensor (#735) * Add Xiaomi Cleargrass Temperature and Humidity Sensor * fix CI Travis * fix CI Travis 2 * Improve device detection (more accurate) Co-authored-by: t151602 --- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 9 +++- esphome/components/xiaomi_ble/xiaomi_ble.h | 2 +- .../components/xiaomi_cleargrass/__init__.py | 0 .../components/xiaomi_cleargrass/sensor.py | 38 ++++++++++++++ .../xiaomi_cleargrass/xiaomi_cleargrass.cpp | 21 ++++++++ .../xiaomi_cleargrass/xiaomi_cleargrass.h | 50 +++++++++++++++++++ 6 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 esphome/components/xiaomi_cleargrass/__init__.py create mode 100644 esphome/components/xiaomi_cleargrass/sensor.py create mode 100644 esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp create mode 100644 esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 5bb6709e5f..33e64246b4 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -84,13 +84,14 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; + bool is_cleargrass = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03; - if (!is_mijia && !is_miflora && !is_lywsd02) { + if (!is_mijia && !is_miflora && !is_lywsd02 && !is_cleargrass) { // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); return {}; } - uint8_t raw_offset = is_mijia ? 11 : 12; + uint8_t raw_offset = is_mijia || is_cleargrass ? 11 : 12; const uint8_t raw_type = raw[raw_offset]; const uint8_t data_length = raw[raw_offset + 2]; @@ -107,6 +108,8 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d result.type = XiaomiParseResult::TYPE_MIJIA; } else if (is_lywsd02) { result.type = XiaomiParseResult::TYPE_LYWSD02; + } else if (is_cleargrass) { + result.type = XiaomiParseResult::TYPE_CLEARGRASS; } bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result); if (!success) @@ -124,6 +127,8 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) name = "Mi Jia"; } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { name = "LYWSD02"; + } else if (res->type == XiaomiParseResult::TYPE_CLEARGRASS) { + name = "Cleargrass"; } ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str()); diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index b8b602ecef..1fbd8ae6b0 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -9,7 +9,7 @@ namespace esphome { namespace xiaomi_ble { struct XiaomiParseResult { - enum { TYPE_MIJIA, TYPE_MIFLORA, TYPE_LYWSD02 } type; + enum { TYPE_MIJIA, TYPE_MIFLORA, TYPE_LYWSD02, TYPE_CLEARGRASS } type; optional temperature; optional humidity; optional battery_level; diff --git a/esphome/components/xiaomi_cleargrass/__init__.py b/esphome/components/xiaomi_cleargrass/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_cleargrass/sensor.py b/esphome/components/xiaomi_cleargrass/sensor.py new file mode 100644 index 0000000000..66fcbc0fcd --- /dev/null +++ b/esphome/components/xiaomi_cleargrass/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_cleargrass_ns = cg.esphome_ns.namespace('xiaomi_cleargrass') +XiaomiCleargrass = xiaomi_cleargrass_ns.class_( + 'XiaomiCleargrass', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiCleargrass), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp b/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp new file mode 100644 index 0000000000..e50994ca4e --- /dev/null +++ b/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp @@ -0,0 +1,21 @@ +#include "xiaomi_cleargrass.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cleargrass { + +static const char *TAG = "xiaomi_cleargrass"; + +void XiaomiCleargrass::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi Cleargrass"); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Battery Level", this->battery_level_); +} + +} // namespace xiaomi_cleargrass +} // namespace esphome + +#endif diff --git a/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h b/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h new file mode 100644 index 0000000000..ea5c80a64f --- /dev/null +++ b/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h @@ -0,0 +1,50 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/xiaomi_ble/xiaomi_ble.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace xiaomi_cleargrass { + +class XiaomiCleargrass : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; } + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { + if (device.address_uint64() != this->address_) + return false; + + auto res = xiaomi_ble::parse_xiaomi(device); + if (!res.has_value()) + return false; + + if (res->temperature.has_value() && this->temperature_ != nullptr) + this->temperature_->publish_state(*res->temperature); + if (res->humidity.has_value() && this->humidity_ != nullptr) + this->humidity_->publish_state(*res->humidity); + if (res->battery_level.has_value() && this->battery_level_ != nullptr) + this->battery_level_->publish_state(*res->battery_level); + return true; + } + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_battery_level(sensor::Sensor *battery_level) { battery_level_ = battery_level; } + + protected: + uint64_t address_; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *battery_level_{nullptr}; +}; + +} // namespace xiaomi_cleargrass +} // namespace esphome + +#endif From a0046a2e55690bc6717cb49f2dcf595d47571b16 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Mon, 14 Oct 2019 14:42:18 +0200 Subject: [PATCH 173/222] Pin voluptuous version Closes https://github.com/esphome/esphome/pull/745 --- requirements.txt | 2 +- requirements_test.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 15dac3eb38..4ee61c7f43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -voluptuous>=0.11.5,<0.12 +voluptuous==0.11.5 PyYAML>=5.1,<6 paho-mqtt>=1.4,<2 colorlog>=4.0.2 diff --git a/requirements_test.txt b/requirements_test.txt index 110475d34c..cb96f95607 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -voluptuous>=0.11.5,<0.12 +voluptuous==0.11.5 PyYAML>=5.1,<6 paho-mqtt>=1.4,<2 colorlog>=4.0.2 diff --git a/setup.py b/setup.py index 981db858b7..fb45965a8c 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__) REQUIRES = [ - 'voluptuous>=0.11.5,<0.12', + 'voluptuous==0.11.5', 'PyYAML>=5.1,<6', 'paho-mqtt>=1.4,<2', 'colorlog>=4.0.2', From 23f99908dbcf3ab25a19b0394cd6cb5c14b744ca Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 15 Oct 2019 20:53:59 +0200 Subject: [PATCH 174/222] Apply HDC1080 patch from @Hsxsky See also https://github.com/Hsxsky/esphome/commit/105ac63d625a05d9b36cd6ca48c2706330bfa644 --- esphome/components/hdc1080/hdc1080.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; } From acf3f6fb65aeb141d02ed07061a3c4278feb9887 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Tue, 15 Oct 2019 21:00:58 +0200 Subject: [PATCH 175/222] Fix outdated voluptuous pinning See also https://github.com/esphome/esphome/pull/745#issuecomment-541831600 --- requirements.txt | 2 +- requirements_test.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4ee61c7f43..147c0cb2e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -voluptuous==0.11.5 +voluptuous==0.11.7 PyYAML>=5.1,<6 paho-mqtt>=1.4,<2 colorlog>=4.0.2 diff --git a/requirements_test.txt b/requirements_test.txt index cb96f95607..de09b1c760 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,4 +1,4 @@ -voluptuous==0.11.5 +voluptuous==0.11.7 PyYAML>=5.1,<6 paho-mqtt>=1.4,<2 colorlog>=4.0.2 diff --git a/setup.py b/setup.py index fb45965a8c..f2692f9387 100755 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ GITHUB_URL = 'https://github.com/{}'.format(GITHUB_PATH) DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__) REQUIRES = [ - 'voluptuous==0.11.5', + 'voluptuous==0.11.7', 'PyYAML>=5.1,<6', 'paho-mqtt>=1.4,<2', 'colorlog>=4.0.2', From 9c30f4cc68b67434a547fee7b8b062e1754f80e3 Mon Sep 17 00:00:00 2001 From: amishv <41266094+amishv@users.noreply.github.com> Date: Wed, 16 Oct 2019 01:01:52 +0530 Subject: [PATCH 176/222] Fix for PCF8574 output chattering at the start/reboot (#744) * Fix for PCF8574 output chattering at the start/reboot * Fix for PCF8574 output chattering at the start/reboot * Fix for PCF8574 output chattering at the start/reboot Co-authored-by: Amish Vishwakarma --- esphome/components/pcf8574/pcf8574.cpp | 2 -- 1 file changed, 2 deletions(-) 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()) From cdb9c59662c42d5b51b3946d406ca58787beab37 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 16 Oct 2019 13:19:41 +0200 Subject: [PATCH 177/222] Add ADE7953 Support (#593) * Add ADE795 support * Lint * Fix * Fix, add test --- esphome/components/ade7953/__init__.py | 0 esphome/components/ade7953/ade7953.cpp | 51 ++++++++++++++++++++ esphome/components/ade7953/ade7953.h | 67 ++++++++++++++++++++++++++ esphome/components/ade7953/sensor.py | 39 +++++++++++++++ esphome/components/i2c/i2c.cpp | 8 +++ esphome/components/i2c/i2c.h | 43 ++++++++++++----- script/clang-tidy | 1 - tests/test3.yaml | 11 +++++ 8 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 esphome/components/ade7953/__init__.py create mode 100644 esphome/components/ade7953/ade7953.cpp create mode 100644 esphome/components/ade7953/ade7953.h create mode 100644 esphome/components/ade7953/sensor.py diff --git a/esphome/components/ade7953/__init__.py b/esphome/components/ade7953/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/ade7953/ade7953.cpp b/esphome/components/ade7953/ade7953.cpp new file mode 100644 index 0000000000..9316d9cad0 --- /dev/null +++ b/esphome/components/ade7953/ade7953.cpp @@ -0,0 +1,51 @@ +#include "ade7953.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace ade7953 { + +static const char *TAG = "ade7953"; + +void ADE7953::dump_config() { + ESP_LOGCONFIG(TAG, "ADE7953:"); + LOG_I2C_DEVICE(this); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Voltage Sensor", this->voltage_sensor_); + LOG_SENSOR(" ", "Current A Sensor", this->current_a_sensor_); + LOG_SENSOR(" ", "Current B Sensor", this->current_b_sensor_); + LOG_SENSOR(" ", "Active Power A Sensor", this->active_power_a_sensor_); + LOG_SENSOR(" ", "Active Power B Sensor", this->active_power_b_sensor_); +} + +#define ADE_PUBLISH_(name, factor) \ + if (name) { \ + float value = *name / factor; \ + this->name##_sensor_->publish_state(value); \ + } +#define ADE_PUBLISH(name, factor) ADE_PUBLISH_(name, factor) + +void ADE7953::update() { + if (!this->is_setup_) + return; + + auto active_power_a = this->ade_read_(0x0312); + ADE_PUBLISH(active_power_a, 154.0f); + auto active_power_b = this->ade_read_(0x0313); + ADE_PUBLISH(active_power_b, 154.0f); + auto current_a = this->ade_read_(0x031A); + ADE_PUBLISH(current_a, 100000.0f); + auto current_b = this->ade_read_(0x031B); + ADE_PUBLISH(current_b, 100000.0f); + auto voltage = this->ade_read_(0x031C); + ADE_PUBLISH(voltage, 26000.0f); + + // auto apparent_power_a = this->ade_read_(0x0310); + // auto apparent_power_b = this->ade_read_(0x0311); + // auto reactive_power_a = this->ade_read_(0x0314); + // auto reactive_power_b = this->ade_read_(0x0315); + // auto power_factor_a = this->ade_read_(0x010A); + // auto power_factor_b = this->ade_read_(0x010B); +} + +} // namespace ade7953 +} // namespace esphome diff --git a/esphome/components/ade7953/ade7953.h b/esphome/components/ade7953/ade7953.h new file mode 100644 index 0000000000..7591bc1684 --- /dev/null +++ b/esphome/components/ade7953/ade7953.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace ade7953 { + +class ADE7953 : public i2c::I2CDevice, public PollingComponent { + public: + void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } + void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } + void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } + void set_active_power_a_sensor(sensor::Sensor *active_power_a_sensor) { + active_power_a_sensor_ = active_power_a_sensor; + } + void set_active_power_b_sensor(sensor::Sensor *active_power_b_sensor) { + active_power_b_sensor_ = active_power_b_sensor; + } + + void setup() override { + this->set_timeout(100, [this]() { + this->ade_write_(0x0010, 0x04); + this->ade_write_(0x00FE, 0xAD); + this->ade_write_(0x0120, 0x0030); + this->is_setup_ = true; + }); + } + + void dump_config() override; + + void update() override; + + protected: + template bool ade_write_(uint16_t reg, T value) { + std::vector data; + data.push_back(reg >> 8); + data.push_back(reg >> 0); + for (int i = sizeof(T) - 1; i >= 0; i--) + data.push_back(value >> (i * 8)); + return this->write_bytes_raw(data); + } + template optional ade_read_(uint16_t reg) { + uint8_t hi = reg >> 8; + uint8_t lo = reg >> 0; + if (!this->write_bytes_raw({hi, lo})) + return {}; + auto ret = this->read_bytes_raw(); + if (!ret.has_value()) + return {}; + T result = 0; + for (int i = 0, j = sizeof(T) - 1; i < sizeof(T); i++, j--) + result |= T((*ret)[i]) << (j * 8); + return result; + } + + bool is_setup_{false}; + sensor::Sensor *voltage_sensor_{nullptr}; + sensor::Sensor *current_a_sensor_{nullptr}; + sensor::Sensor *current_b_sensor_{nullptr}; + sensor::Sensor *active_power_a_sensor_{nullptr}; + sensor::Sensor *active_power_b_sensor_{nullptr}; +}; + +} // namespace ade7953 +} // namespace esphome diff --git a/esphome/components/ade7953/sensor.py b/esphome/components/ade7953/sensor.py new file mode 100644 index 0000000000..4fcd307332 --- /dev/null +++ b/esphome/components/ade7953/sensor.py @@ -0,0 +1,39 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, i2c +from esphome.const import CONF_ID, CONF_VOLTAGE, \ + UNIT_VOLT, ICON_FLASH, UNIT_AMPERE, UNIT_WATT + +DEPENDENCIES = ['i2c'] + +ace7953_ns = cg.esphome_ns.namespace('ade7953') +ADE7953 = ace7953_ns.class_('ADE7953', cg.PollingComponent, i2c.I2CDevice) + +CONF_CURRENT_A = 'current_a' +CONF_CURRENT_B = 'current_b' +CONF_ACTIVE_POWER_A = 'active_power_a' +CONF_ACTIVE_POWER_B = 'active_power_b' + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(ADE7953), + + cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(UNIT_VOLT, ICON_FLASH, 1), + cv.Optional(CONF_CURRENT_A): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_CURRENT_B): sensor.sensor_schema(UNIT_AMPERE, ICON_FLASH, 2), + cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), + cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema(UNIT_WATT, ICON_FLASH, 1), +}).extend(cv.polling_component_schema('60s')).extend(i2c.i2c_device_schema(0x38)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + for key in [CONF_VOLTAGE, CONF_CURRENT_A, CONF_CURRENT_B, CONF_ACTIVE_POWER_A, + CONF_ACTIVE_POWER_B]: + if key not in config: + continue + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(var, 'set_{}_sensor'.format(key))(sens)) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index b93c7d6053..840944748c 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -135,6 +135,9 @@ bool I2CComponent::read_bytes(uint8_t address, uint8_t a_register, uint8_t *data delay(conversion); return this->raw_receive(address, data, len); } +bool I2CComponent::read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len) { + return this->raw_receive(address, data, len); +} bool I2CComponent::read_bytes_16(uint8_t address, uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(address, a_register, nullptr, 0)) @@ -156,6 +159,11 @@ bool I2CComponent::write_bytes(uint8_t address, uint8_t a_register, const uint8_ this->raw_write(address, data, len); return this->raw_end_transmission(address); } +bool I2CComponent::write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len) { + this->raw_begin_transmission(address); + this->raw_write(address, data, len); + return this->raw_end_transmission(address); +} bool I2CComponent::write_bytes_16(uint8_t address, uint8_t a_register, const uint16_t *data, uint8_t len) { this->raw_begin_transmission(address); this->raw_write(address, &a_register, 1); diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index e41bd6c5e8..67cd0373d3 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -42,6 +42,7 @@ class I2CComponent : public Component { * @return If the operation was successful. */ bool read_bytes(uint8_t address, uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); + bool read_bytes_raw(uint8_t address, uint8_t *data, uint8_t len); /** Read len amount of 16-bit words (MSB first) from a register into data. * @@ -69,6 +70,7 @@ class I2CComponent : public Component { * @return If the operation was successful. */ bool write_bytes(uint8_t address, uint8_t a_register, const uint8_t *data, uint8_t len); + bool write_bytes_raw(uint8_t address, const uint8_t *data, uint8_t len); /** Write len amount of 16-bit words (MSB first) to the specified register for address. * @@ -151,7 +153,6 @@ class I2CDevice { /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); - protected: /** Read len amount of bytes from a register into data. Optionally with a conversion time after * writing the register value to the bus. * @@ -161,15 +162,23 @@ class I2CDevice { * @param conversion The time in ms between writing the register value and reading out the value. * @return If the operation was successful. */ - bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + bool read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion = 0); + bool read_bytes_raw(uint8_t *data, uint8_t len) { return this->parent_->read_bytes_raw(this->address_, data, len); } - template optional> read_bytes(uint8_t a_register) { // NOLINT + template optional> read_bytes(uint8_t a_register) { std::array res; if (!this->read_bytes(a_register, res.data(), N)) { return {}; } return res; } + template optional> read_bytes_raw() { + std::array res; + if (!this->read_bytes_raw(res.data(), N)) { + return {}; + } + return res; + } /** Read len amount of 16-bit words (MSB first) from a register into data. * @@ -179,12 +188,12 @@ class I2CDevice { * @param conversion The time in ms between writing the register value and reading out the value. * @return If the operation was successful. */ - bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); // NOLINT + bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion = 0); /// Read a single byte from a register into the data variable. Return true if successful. - bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); // NOLINT + bool read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion = 0); - optional read_byte(uint8_t a_register) { // NOLINT + optional read_byte(uint8_t a_register) { uint8_t data; if (!this->read_byte(a_register, &data)) return {}; @@ -192,7 +201,7 @@ class I2CDevice { } /// Read a single 16-bit words (MSB first) from a register into the data variable. Return true if successful. - bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); // NOLINT + bool read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion = 0); /** Write len amount of 8-bit bytes to the specified register. * @@ -201,7 +210,10 @@ class I2CDevice { * @param len The amount of bytes to write to the bus. * @return If the operation was successful. */ - bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); // NOLINT + bool write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len); + bool write_bytes_raw(const uint8_t *data, uint8_t len) { + return this->parent_->write_bytes_raw(this->address_, data, len); + } /** Write a vector of data to a register. * @@ -209,13 +221,17 @@ class I2CDevice { * @param data The data to write. * @return If the operation was successful. */ - bool write_bytes(uint8_t a_register, const std::vector &data) { // NOLINT + bool write_bytes(uint8_t a_register, const std::vector &data) { return this->write_bytes(a_register, data.data(), data.size()); } + bool write_bytes_raw(const std::vector &data) { return this->write_bytes_raw(data.data(), data.size()); } - template bool write_bytes(uint8_t a_register, const std::array &data) { // NOLINT + template bool write_bytes(uint8_t a_register, const std::array &data) { return this->write_bytes(a_register, data.data(), data.size()); } + template bool write_bytes_raw(const std::array &data) { + return this->write_bytes_raw(data.data(), data.size()); + } /** Write len amount of 16-bit words (MSB first) to the specified register. * @@ -224,14 +240,15 @@ class I2CDevice { * @param len The amount of bytes to write to the bus. * @return If the operation was successful. */ - bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); // NOLINT + bool write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len); /// Write a single byte of data into the specified register. Return true if successful. - bool write_byte(uint8_t a_register, uint8_t data); // NOLINT + bool write_byte(uint8_t a_register, uint8_t data); /// Write a single 16-bit word of data into the specified register. Return true if successful. - bool write_byte_16(uint8_t a_register, uint16_t data); // NOLINT + bool write_byte_16(uint8_t a_register, uint16_t data); + protected: uint8_t address_{0x00}; I2CComponent *parent_{nullptr}; }; diff --git a/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/tests/test3.yaml b/tests/test3.yaml index 458021d0d3..c572b5efc5 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -203,6 +203,17 @@ sensor: value: 15.0 - binary_sensor: bin3 value: 100.0 + - platform: ade7953 + voltage: + name: ADE7953 Voltage + current_a: + name: ADE7953 Current A + current_b: + name: ADE7953 Current B + active_power_a: + name: ADE7953 Active Power A + active_power_b: + name: ADE7953 Active Power B time: - platform: homeassistant From cdfbe5b5237306986b07ba4efc99e782d5227342 Mon Sep 17 00:00:00 2001 From: Alexander Leisentritt Date: Wed, 16 Oct 2019 13:29:56 +0200 Subject: [PATCH 178/222] refactored xiaomi sensors (#755) * refactored xiaomi sensors * fix lint * fixed and added tests * fix namespace * LYWSD02 has no battery level * fixed enum * fix * fix case * fix spaces in empty line... * inform users of old sensors about the change --- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 28 +++++------ esphome/components/xiaomi_ble/xiaomi_ble.h | 2 +- .../__init__.py | 0 .../sensor.py | 8 +-- .../xiaomi_cgg1.cpp} | 12 ++--- .../xiaomi_cgg1.h} | 6 +-- .../components/xiaomi_hhccjcy01/__init__.py | 0 esphome/components/xiaomi_hhccjcy01/sensor.py | 49 +++++++++++++++++++ .../xiaomi_hhccjcy01.cpp} | 12 ++--- .../xiaomi_hhccjcy01.h} | 6 +-- .../components/xiaomi_lywsdcgq/__init__.py | 0 esphome/components/xiaomi_lywsdcgq/sensor.py | 38 ++++++++++++++ .../xiaomi_lywsdcgq.cpp} | 12 ++--- .../xiaomi_lywsdcgq.h} | 6 +-- esphome/components/xiaomi_miflora/sensor.py | 48 +----------------- esphome/components/xiaomi_mijia/sensor.py | 37 +------------- tests/test2.yaml | 34 +++++++++---- 17 files changed, 159 insertions(+), 139 deletions(-) rename esphome/components/{xiaomi_cleargrass => xiaomi_cgg1}/__init__.py (100%) rename esphome/components/{xiaomi_cleargrass => xiaomi_cgg1}/sensor.py (85%) rename esphome/components/{xiaomi_mijia/xiaomi_mijia.cpp => xiaomi_cgg1/xiaomi_cgg1.cpp} (59%) rename esphome/components/{xiaomi_mijia/xiaomi_mijia.h => xiaomi_cgg1/xiaomi_cgg1.h} (91%) create mode 100644 esphome/components/xiaomi_hhccjcy01/__init__.py create mode 100644 esphome/components/xiaomi_hhccjcy01/sensor.py rename esphome/components/{xiaomi_miflora/xiaomi_miflora.cpp => xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp} (64%) rename esphome/components/{xiaomi_miflora/xiaomi_miflora.h => xiaomi_hhccjcy01/xiaomi_hhccjcy01.h} (93%) create mode 100644 esphome/components/xiaomi_lywsdcgq/__init__.py create mode 100644 esphome/components/xiaomi_lywsdcgq/sensor.py rename esphome/components/{xiaomi_cleargrass/xiaomi_cleargrass.cpp => xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp} (55%) rename esphome/components/{xiaomi_cleargrass/xiaomi_cleargrass.h => xiaomi_lywsdcgq/xiaomi_lywsdcgq.h} (90%) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 33e64246b4..0ccb28667e 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -81,17 +81,17 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d return {}; } - bool is_mijia = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; - bool is_miflora = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; + bool is_lywsdcgq = (raw[1] & 0x20) == 0x20 && raw[2] == 0xAA && raw[3] == 0x01; + bool is_hhccjcy01 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x98 && raw[3] == 0x00; bool is_lywsd02 = (raw[1] & 0x20) == 0x20 && raw[2] == 0x5b && raw[3] == 0x04; - bool is_cleargrass = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03; + bool is_cgg1 = (raw[1] & 0x30) == 0x30 && raw[2] == 0x47 && raw[3] == 0x03; - if (!is_mijia && !is_miflora && !is_lywsd02 && !is_cleargrass) { + if (!is_lywsdcgq && !is_hhccjcy01 && !is_lywsd02 && !is_cgg1) { // ESP_LOGVV(TAG, "Xiaomi no magic bytes"); return {}; } - uint8_t raw_offset = is_mijia || is_cleargrass ? 11 : 12; + uint8_t raw_offset = is_lywsdcgq || is_cgg1 ? 11 : 12; const uint8_t raw_type = raw[raw_offset]; const uint8_t data_length = raw[raw_offset + 2]; @@ -103,13 +103,13 @@ optional parse_xiaomi(const esp32_ble_tracker::ESPBTDevice &d return {}; } XiaomiParseResult result; - result.type = XiaomiParseResult::TYPE_MIFLORA; - if (is_mijia) { - result.type = XiaomiParseResult::TYPE_MIJIA; + result.type = XiaomiParseResult::TYPE_HHCCJCY01; + if (is_lywsdcgq) { + result.type = XiaomiParseResult::TYPE_LYWSDCGQ; } else if (is_lywsd02) { result.type = XiaomiParseResult::TYPE_LYWSD02; - } else if (is_cleargrass) { - result.type = XiaomiParseResult::TYPE_CLEARGRASS; + } else if (is_cgg1) { + result.type = XiaomiParseResult::TYPE_CGG1; } bool success = parse_xiaomi_data_byte(raw_type, data, data_length, result); if (!success) @@ -122,12 +122,12 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) if (!res.has_value()) return false; - const char *name = "Mi Flora"; - if (res->type == XiaomiParseResult::TYPE_MIJIA) { - name = "Mi Jia"; + const char *name = "HHCCJCY01"; + if (res->type == XiaomiParseResult::TYPE_LYWSDCGQ) { + name = "LYWSDCGQ"; } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { name = "LYWSD02"; - } else if (res->type == XiaomiParseResult::TYPE_CLEARGRASS) { + } else if (res->type == XiaomiParseResult::TYPE_CGG1) { name = "Cleargrass"; } diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.h b/esphome/components/xiaomi_ble/xiaomi_ble.h index 1fbd8ae6b0..824ea80edf 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.h +++ b/esphome/components/xiaomi_ble/xiaomi_ble.h @@ -9,7 +9,7 @@ namespace esphome { namespace xiaomi_ble { struct XiaomiParseResult { - enum { TYPE_MIJIA, TYPE_MIFLORA, TYPE_LYWSD02, TYPE_CLEARGRASS } type; + enum { TYPE_LYWSDCGQ, TYPE_HHCCJCY01, TYPE_LYWSD02, TYPE_CGG1 } type; optional temperature; optional humidity; optional battery_level; diff --git a/esphome/components/xiaomi_cleargrass/__init__.py b/esphome/components/xiaomi_cgg1/__init__.py similarity index 100% rename from esphome/components/xiaomi_cleargrass/__init__.py rename to esphome/components/xiaomi_cgg1/__init__.py diff --git a/esphome/components/xiaomi_cleargrass/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py similarity index 85% rename from esphome/components/xiaomi_cleargrass/sensor.py rename to esphome/components/xiaomi_cgg1/sensor.py index 66fcbc0fcd..897687c68a 100644 --- a/esphome/components/xiaomi_cleargrass/sensor.py +++ b/esphome/components/xiaomi_cgg1/sensor.py @@ -7,12 +7,12 @@ from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, C DEPENDENCIES = ['esp32_ble_tracker'] AUTO_LOAD = ['xiaomi_ble'] -xiaomi_cleargrass_ns = cg.esphome_ns.namespace('xiaomi_cleargrass') -XiaomiCleargrass = xiaomi_cleargrass_ns.class_( - 'XiaomiCleargrass', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) +xiaomi_cgg1_ns = cg.esphome_ns.namespace('xiaomi_cgg1') +XiaomiCGG1 = xiaomi_cgg1_ns.class_( + 'XiaomiCGG1', esp32_ble_tracker.ESPBTDeviceListener, cg.Component) CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(XiaomiCleargrass), + cv.GenerateID(): cv.declare_id(XiaomiCGG1), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp similarity index 59% rename from esphome/components/xiaomi_mijia/xiaomi_mijia.cpp rename to esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index 544af32d7b..6cc14f5a8e 100644 --- a/esphome/components/xiaomi_mijia/xiaomi_mijia.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -1,21 +1,21 @@ -#include "xiaomi_mijia.h" +#include "xiaomi_cgg1.h" #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_mijia { +namespace xiaomi_cgg1 { -static const char *TAG = "xiaomi_mijia"; +static const char *TAG = "xiaomi_cgg1"; -void XiaomiMijia::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); +void XiaomiCGG1::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi CGG1"); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); } -} // namespace xiaomi_mijia +} // namespace xiaomi_cgg1 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_mijia/xiaomi_mijia.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h similarity index 91% rename from esphome/components/xiaomi_mijia/xiaomi_mijia.h rename to esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 814e33fa75..7f73011275 100644 --- a/esphome/components/xiaomi_mijia/xiaomi_mijia.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -8,9 +8,9 @@ #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_mijia { +namespace xiaomi_cgg1 { -class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } @@ -44,7 +44,7 @@ class XiaomiMijia : public Component, public esp32_ble_tracker::ESPBTDeviceListe sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_mijia +} // namespace xiaomi_cgg1 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_hhccjcy01/__init__.py b/esphome/components/xiaomi_hhccjcy01/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_hhccjcy01/sensor.py b/esphome/components/xiaomi_hhccjcy01/sensor.py new file mode 100644 index 0000000000..495446ba11 --- /dev/null +++ b/esphome/components/xiaomi_hhccjcy01/sensor.py @@ -0,0 +1,49 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ + CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ + UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_hhccjcy01_ns = cg.esphome_ns.namespace('xiaomi_hhccjcy01') +XiaomiHHCCJCY01 = xiaomi_hhccjcy01_ns.class_('XiaomiHHCCJCY01', + esp32_ble_tracker.ESPBTDeviceListener, cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiHHCCJCY01), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), + cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), + cv.Optional(CONF_CONDUCTIVITY): + sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_MOISTURE in config: + sens = yield sensor.new_sensor(config[CONF_MOISTURE]) + cg.add(var.set_moisture(sens)) + if CONF_ILLUMINANCE in config: + sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) + cg.add(var.set_illuminance(sens)) + if CONF_CONDUCTIVITY in config: + sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) + cg.add(var.set_conductivity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp similarity index 64% rename from esphome/components/xiaomi_miflora/xiaomi_miflora.cpp rename to esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp index 966c78a1a6..8c8152c54c 100644 --- a/esphome/components/xiaomi_miflora/xiaomi_miflora.cpp +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.cpp @@ -1,15 +1,15 @@ -#include "xiaomi_miflora.h" +#include "xiaomi_hhccjcy01.h" #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_miflora { +namespace xiaomi_hhccjcy01 { -static const char *TAG = "xiaomi_miflora"; +static const char *TAG = "xiaomi_hhccjcy01"; -void XiaomiMiflora::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi Mijia"); +void XiaomiHHCCJCY01::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi HHCCJCY01"); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Moisture", this->moisture_); LOG_SENSOR(" ", "Conductivity", this->conductivity_); @@ -17,7 +17,7 @@ void XiaomiMiflora::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_level_); } -} // namespace xiaomi_miflora +} // namespace xiaomi_hhccjcy01 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_miflora/xiaomi_miflora.h b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h similarity index 93% rename from esphome/components/xiaomi_miflora/xiaomi_miflora.h rename to esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h index d1f05cdcc7..c1b8511bb8 100644 --- a/esphome/components/xiaomi_miflora/xiaomi_miflora.h +++ b/esphome/components/xiaomi_hhccjcy01/xiaomi_hhccjcy01.h @@ -8,9 +8,9 @@ #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_miflora { +namespace xiaomi_hhccjcy01 { -class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiHHCCJCY01 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } @@ -52,7 +52,7 @@ class XiaomiMiflora : public Component, public esp32_ble_tracker::ESPBTDeviceLis sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_miflora +} // namespace xiaomi_hhccjcy01 } // namespace esphome #endif diff --git a/esphome/components/xiaomi_lywsdcgq/__init__.py b/esphome/components/xiaomi_lywsdcgq/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/xiaomi_lywsdcgq/sensor.py b/esphome/components/xiaomi_lywsdcgq/sensor.py new file mode 100644 index 0000000000..e13c860464 --- /dev/null +++ b/esphome/components/xiaomi_lywsdcgq/sensor.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, esp32_ble_tracker +from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ + UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID + +DEPENDENCIES = ['esp32_ble_tracker'] +AUTO_LOAD = ['xiaomi_ble'] + +xiaomi_lywsdcgq_ns = cg.esphome_ns.namespace('xiaomi_lywsdcgq') +XiaomiLYWSDCGQ = xiaomi_lywsdcgq_ns.class_('XiaomiLYWSDCGQ', esp32_ble_tracker.ESPBTDeviceListener, + cg.Component) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(XiaomiLYWSDCGQ), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), + cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), +}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_ble_device(var, config) + + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + + if CONF_TEMPERATURE in config: + sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) + cg.add(var.set_temperature(sens)) + if CONF_HUMIDITY in config: + sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) + cg.add(var.set_humidity(sens)) + if CONF_BATTERY_LEVEL in config: + sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) + cg.add(var.set_battery_level(sens)) diff --git a/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp similarity index 55% rename from esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp rename to esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp index e50994ca4e..2dacff2876 100644 --- a/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.cpp +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.cpp @@ -1,21 +1,21 @@ -#include "xiaomi_cleargrass.h" +#include "xiaomi_lywsdcgq.h" #include "esphome/core/log.h" #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_cleargrass { +namespace xiaomi_lywsdcgq { -static const char *TAG = "xiaomi_cleargrass"; +static const char *TAG = "xiaomi_lywsdcgq"; -void XiaomiCleargrass::dump_config() { - ESP_LOGCONFIG(TAG, "Xiaomi Cleargrass"); +void XiaomiLYWSDCGQ::dump_config() { + ESP_LOGCONFIG(TAG, "Xiaomi LYWSDCGQ"); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); } -} // namespace xiaomi_cleargrass +} // namespace xiaomi_lywsdcgq } // namespace esphome #endif diff --git a/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h similarity index 90% rename from esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h rename to esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h index ea5c80a64f..b6756eec61 100644 --- a/esphome/components/xiaomi_cleargrass/xiaomi_cleargrass.h +++ b/esphome/components/xiaomi_lywsdcgq/xiaomi_lywsdcgq.h @@ -8,9 +8,9 @@ #ifdef ARDUINO_ARCH_ESP32 namespace esphome { -namespace xiaomi_cleargrass { +namespace xiaomi_lywsdcgq { -class XiaomiCleargrass : public Component, public esp32_ble_tracker::ESPBTDeviceListener { +class XiaomiLYWSDCGQ : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } @@ -44,7 +44,7 @@ class XiaomiCleargrass : public Component, public esp32_ble_tracker::ESPBTDevice sensor::Sensor *battery_level_{nullptr}; }; -} // namespace xiaomi_cleargrass +} // namespace xiaomi_lywsdcgq } // namespace esphome #endif diff --git a/esphome/components/xiaomi_miflora/sensor.py b/esphome/components/xiaomi_miflora/sensor.py index 8be06a93f3..0a0b3ff63f 100644 --- a/esphome/components/xiaomi_miflora/sensor.py +++ b/esphome/components/xiaomi_miflora/sensor.py @@ -1,49 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID, \ - CONF_MOISTURE, CONF_ILLUMINANCE, ICON_BRIGHTNESS_5, UNIT_LUX, CONF_CONDUCTIVITY, \ - UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER -DEPENDENCIES = ['esp32_ble_tracker'] -AUTO_LOAD = ['xiaomi_ble'] - -xiaomi_miflora_ns = cg.esphome_ns.namespace('xiaomi_miflora') -XiaomiMiflora = xiaomi_miflora_ns.class_('XiaomiMiflora', esp32_ble_tracker.ESPBTDeviceListener, - cg.Component) - -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(XiaomiMiflora), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_MOISTURE): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 0), - cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(UNIT_LUX, ICON_BRIGHTNESS_5, 0), - cv.Optional(CONF_CONDUCTIVITY): - sensor.sensor_schema(UNIT_MICROSIEMENS_PER_CENTIMETER, ICON_FLOWER, 0), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) - - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) - - if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) - cg.add(var.set_temperature(sens)) - if CONF_MOISTURE in config: - sens = yield sensor.new_sensor(config[CONF_MOISTURE]) - cg.add(var.set_moisture(sens)) - if CONF_ILLUMINANCE in config: - sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) - cg.add(var.set_illuminance(sens)) - if CONF_CONDUCTIVITY in config: - sens = yield sensor.new_sensor(config[CONF_CONDUCTIVITY]) - cg.add(var.set_conductivity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) +CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_hhccjcy01") diff --git a/esphome/components/xiaomi_mijia/sensor.py b/esphome/components/xiaomi_mijia/sensor.py index 995a6cbf25..597d8d1bce 100644 --- a/esphome/components/xiaomi_mijia/sensor.py +++ b/esphome/components/xiaomi_mijia/sensor.py @@ -1,38 +1,3 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor, esp32_ble_tracker -from esphome.const import CONF_BATTERY_LEVEL, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, \ - UNIT_CELSIUS, ICON_THERMOMETER, UNIT_PERCENT, ICON_WATER_PERCENT, ICON_BATTERY, CONF_ID -DEPENDENCIES = ['esp32_ble_tracker'] -AUTO_LOAD = ['xiaomi_ble'] - -xiaomi_mijia_ns = cg.esphome_ns.namespace('xiaomi_mijia') -XiaomiMijia = xiaomi_mijia_ns.class_('XiaomiMijia', esp32_ble_tracker.ESPBTDeviceListener, - cg.Component) - -CONFIG_SCHEMA = cv.Schema({ - cv.GenerateID(): cv.declare_id(XiaomiMijia), - cv.Required(CONF_MAC_ADDRESS): cv.mac_address, - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(UNIT_PERCENT, ICON_WATER_PERCENT, 1), - cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(UNIT_PERCENT, ICON_BATTERY, 0), -}).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA).extend(cv.COMPONENT_SCHEMA) - - -def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - yield cg.register_component(var, config) - yield esp32_ble_tracker.register_ble_device(var, config) - - cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) - - if CONF_TEMPERATURE in config: - sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) - cg.add(var.set_temperature(sens)) - if CONF_HUMIDITY in config: - sens = yield sensor.new_sensor(config[CONF_HUMIDITY]) - cg.add(var.set_humidity(sens)) - if CONF_BATTERY_LEVEL in config: - sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) - cg.add(var.set_battery_level(sens)) +CONFIG_SCHEMA = cv.invalid("This sensor has been renamed to xiaomi_lywsdcgq") diff --git a/tests/test2.yaml b/tests/test2.yaml index b0dfc27e96..337deece91 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -61,26 +61,40 @@ sensor: - platform: ble_rssi mac_address: AC:37:43:77:5F:4C name: "BLE Google Home Mini RSSI value" - - platform: xiaomi_miflora + - platform: xiaomi_hhccjcy01 mac_address: 94:2B:FF:5C:91:61 temperature: - name: "Xiaomi MiFlora Temperature" + name: "Xiaomi HHCCJCY01 Temperature" moisture: - name: "Xiaomi MiFlora Moisture" + name: "Xiaomi HHCCJCY01 Moisture" illuminance: - name: "Xiaomi MiFlora Illuminance" + name: "Xiaomi HHCCJCY01 Illuminance" conductivity: - name: "Xiaomi MiFlora Soil Conductivity" + name: "Xiaomi HHCCJCY01 Soil Conductivity" battery_level: - name: "Xiaomi MiFlora Battery Level" - - platform: xiaomi_mijia + name: "Xiaomi HHCCJCY01 Battery Level" + - platform: xiaomi_lywsdcgq mac_address: 7A:80:8E:19:36:BA temperature: - name: "Xiaomi MiJia Temperature" + name: "Xiaomi LYWSDCGQ Temperature" humidity: - name: "Xiaomi MiJia Humidity" + name: "Xiaomi LYWSDCGQ Humidity" battery_level: - name: "Xiaomi MiJia Battery Level" + name: "Xiaomi LYWSDCGQ Battery Level" + - platform: xiaomi_lywsd02 + mac_address: 3F:5B:7D:82:58:4E + temperature: + name: "Xiaomi LYWSD02 Temperature" + humidity: + name: "Xiaomi LYWSD02 Humidity" + - platform: xiaomi_cgg1 + mac_address: 7A:80:8E:19:36:BA + temperature: + name: "Xiaomi CGG1 Temperature" + humidity: + name: "Xiaomi CGG1 Humidity" + battery_level: + name: "Xiaomi CGG1 Battery Level" - platform: pmsx003 type: PMSX003 pm_1_0: From 45736707bdd90fa2ae4c2e32f6342e417dbce82a Mon Sep 17 00:00:00 2001 From: Alexander Leisentritt Date: Thu, 17 Oct 2019 14:02:41 +0200 Subject: [PATCH 179/222] fix CGG1 log message (#757) --- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 0ccb28667e..1172f6ee0a 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -128,7 +128,7 @@ bool XiaomiListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) } else if (res->type == XiaomiParseResult::TYPE_LYWSD02) { name = "LYWSD02"; } else if (res->type == XiaomiParseResult::TYPE_CGG1) { - name = "Cleargrass"; + name = "CGG1"; } ESP_LOGD(TAG, "Got Xiaomi %s (%s):", name, device.address_str().c_str()); From 81b7653c9cfc5ea862a3b6a6f456cceb2c00baa7 Mon Sep 17 00:00:00 2001 From: TomFahey Date: Thu, 17 Oct 2019 15:18:41 +0100 Subject: [PATCH 180/222] Add mcp23008 support (#649) * Add support for mcp23008 8-port io expander * add-mcp23008-support * Revert "add-mcp23008-support" This reverts commit b4bc7785b19bf27843b140e56707b935716e3759. * Fixed spacing typo * removed extra space in mcp23008.cpp, line 23 * Fixed trailing whitespace issue * Added mcp23008 component * Added component mcp23008 * Edited typo in test/test1.ymal Removed additional ' in line 1337 * Another typo --- esphome/components/mcp23008/__init__.py | 51 +++++++++++++ esphome/components/mcp23008/mcp23008.cpp | 91 ++++++++++++++++++++++++ esphome/components/mcp23008/mcp23008.h | 69 ++++++++++++++++++ tests/test1.yaml | 21 +++++- tests/test3.yaml | 16 ++++- 5 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 esphome/components/mcp23008/__init__.py create mode 100644 esphome/components/mcp23008/mcp23008.cpp create mode 100644 esphome/components/mcp23008/mcp23008.h 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/tests/test1.yaml b/tests/test1.yaml index 1fed061275..fad67ada7e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -725,12 +725,20 @@ binary_sensor: mode: INPUT inverted: True - platform: gpio - name: "MCP binary sensor" + name: "MCP21 binary sensor" pin: mcp23017: mcp23017_hub number: 1 mode: INPUT inverted: True + - platform: gpio + name: "MCP22 binary sensor" + pin: + mcp23008: mcp23008_hub + number: 7 + mode: INPUT_PULLUP + inverted: False + - platform: remote_receiver name: "Raw Remote Receiver Test" raw: @@ -847,6 +855,13 @@ output: number: 0 mode: OUTPUT inverted: False + - platform: gpio + id: id23 + pin: + mcp23008: mcp23008_hub + number: 0 + mode: OUTPUT + inverted: False - platform: my9231 id: my_0 channel: 0 @@ -1372,6 +1387,10 @@ pcf8574: mcp23017: - id: 'mcp23017_hub' +mcp23008: + - id: 'mcp23008_hub' + address: 0x22 + stepper: - platform: a4988 id: my_stepper diff --git a/tests/test3.yaml b/tests/test3.yaml index c572b5efc5..a7e8d26168 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -344,12 +344,19 @@ switch: - platform: gpio id: gpio_switch1 pin: - mcp23017: mcp + mcp23017: mcp23017_hub number: 0 mode: OUTPUT - interlock: &interlock [gpio_switch1, gpio_switch2] + interlock: &interlock [gpio_switch1, gpio_switch2, gpio_switch3] - platform: gpio id: gpio_switch2 + pin: + mcp23008: mcp23008_hub + number: 0 + mode: OUTPUT + interlock: *interlock + - platform: gpio + id: gpio_switch3 pin: GPIO1 interlock: *interlock - platform: custom @@ -492,7 +499,10 @@ output: - id: custom_float mcp23017: - id: mcp + id: mcp23017_hub + +mcp23008: + id: mcp23008_hub light: - platform: neopixelbus From 428684bc1eda0c9de4bcbff011e210362308297c Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 17 Oct 2019 11:36:11 -0300 Subject: [PATCH 181/222] Brightness ssd1306 (#723) * added brightness for oled display ssd1306 * lint Co-authored-by: waiet --- esphome/components/ssd1306_base/__init__.py | 6 +++++- esphome/components/ssd1306_base/ssd1306_base.cpp | 7 ++----- esphome/components/ssd1306_base/ssd1306_base.h | 2 ++ tests/test1.yaml | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) 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/tests/test1.yaml b/tests/test1.yaml index fad67ada7e..f2adaa19fd 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1298,10 +1298,11 @@ display: it.set_component_value("gauge", 50); it.set_component_text("textview", "Hello World!"); - platform: ssd1306_i2c - model: "SSD1306 128x64" + model: "SSD1306_128X64" reset_pin: GPIO23 address: 0x3C id: display1 + brightness: 60% pages: - id: page1 lambda: |- From ac48ff1fd6b23a8b81ca684b0db0148066680574 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 16:53:39 +0200 Subject: [PATCH 182/222] Fix potential ISR digital_write issue (#753) --- esphome/core/esphal.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/esphal.cpp b/esphome/core/esphal.cpp index f0749894c0..13d54e726d 100644 --- a/esphome/core/esphal.cpp +++ b/esphome/core/esphal.cpp @@ -148,7 +148,7 @@ void ICACHE_RAM_ATTR HOT GPIOPin::digital_write(bool value) { } #endif } -void ISRInternalGPIOPin::digital_write(bool value) { +void ICACHE_RAM_ATTR HOT ISRInternalGPIOPin::digital_write(bool value) { #ifdef ARDUINO_ARCH_ESP8266 if (this->pin_ != 16) { if (value != this->inverted_) { From e15071228e42a6d79d470d223809e090ea78b3f3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 16:53:55 +0200 Subject: [PATCH 183/222] Fix addressable light fade to black function (#752) Fixes https://github.com/esphome/issues/issues/517 --- esphome/components/light/addressable_light.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/light/addressable_light.cpp b/esphome/components/light/addressable_light.cpp index 68a303f23d..6b1fa36bc1 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) From 78c1adafcdb6536c8cff7d997afbc28ac238e6a9 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 16:54:38 +0200 Subject: [PATCH 184/222] Make UART flush function consistent (#748) See also https://github.com/esphome/esphome/commit/78be9d29376d5fdcff81874540c540eb80a338f6 --- esphome/components/mhz19/mhz19.cpp | 9 +++++---- esphome/components/sds011/sds011.cpp | 1 - esphome/components/uart/uart.cpp | 4 +++- esphome/components/uart/uart.h | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index cbac28f9c7..36ccf70d84 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -77,16 +77,17 @@ void MHZ19Component::abc_disable() { } 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() { diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 1abb6210ce..6ca414c55d 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -94,7 +94,6 @@ float SDS011Component::get_setup_priority() const { return setup_priority::DATA; void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void SDS011Component::sds011_write_command_(const uint8_t *command_data) { - this->flush(); this->write_byte(SDS011_MSG_HEAD); this->write_byte(SDS011_COMMAND_ID_REQUEST); this->write_array(command_data, SDS011_DATA_REQUEST_LENGTH); diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index ea15af5053..83ae81490e 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -316,7 +316,9 @@ uint8_t ESP8266SoftwareSerial::peek_byte() { return 0; return this->rx_buffer_[this->rx_out_pos_]; } -void ESP8266SoftwareSerial::flush() { this->rx_in_pos_ = this->rx_out_pos_ = 0; } +void ESP8266SoftwareSerial::flush() { + // Flush is a NO-OP with software serial, all bytes are written immediately. +} int ESP8266SoftwareSerial::available() { int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); if (avail < 0) diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 93caaf3006..3b347c1ff7 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -61,6 +61,7 @@ class UARTComponent : public Component, public Stream { int available() override; + /// Block until all bytes have been written to the UART bus. void flush() override; float get_setup_priority() const override { return setup_priority::BUS; } From 35a725fa1e387814db5af40fec3f7b5e2ec2b317 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 18:23:39 +0200 Subject: [PATCH 185/222] Update and pin all requirements (#759) --- requirements.txt | 23 +++++++++++------------ requirements_test.txt | 26 +++++++++++++------------- setup.py | 22 +++++++++++----------- 3 files changed, 35 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index 147c0cb2e6..14ce062000 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,14 +1,13 @@ voluptuous==0.11.7 -PyYAML>=5.1,<6 -paho-mqtt>=1.4,<2 -colorlog>=4.0.2 -tornado>=5.1.1,<6 +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 -pytz>=2019.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 de09b1c760..26f14434d8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,17 +1,17 @@ voluptuous==0.11.7 -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 -pytz>=2019.1 -pyserial>=3.4,<4 -ifaddr>=0.1.6,<1 -platformio>=3.6.7 ; python_version<"3" -https://github.com/platformio/platformio-core/archive/develop.zip ; python_version>"3" -esptool>=2.6,<3 +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/setup.py b/setup.py index f2692f9387..53acef5a30 100755 --- a/setup.py +++ b/setup.py @@ -24,16 +24,16 @@ DOWNLOAD_URL = '{}/archive/v{}.zip'.format(GITHUB_URL, const.__version__) REQUIRES = [ 'voluptuous==0.11.7', - 'PyYAML>=5.1,<6', - 'paho-mqtt>=1.4,<2', - 'colorlog>=4.0.2', - 'tornado>=5.1.1,<6', + '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', - 'pytz>=2019.1', - 'pyserial>=3.4,<4', - 'ifaddr>=0.1.6,<1', + 'protobuf==3.10.0', + 'tzlocal==2.0.0', + 'pytz==2019.3', + 'pyserial==3.4', + 'ifaddr==0.1.6', ] # If you have problems importing platformio and esptool as modules you can set @@ -41,8 +41,8 @@ REQUIRES = [ # This means they have to be in your $PATH. if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: REQUIRES.extend([ - 'platformio>=3.6.5', - 'esptool>=2.6,<3', + 'platformio==4.0.3', + 'esptool==2.7', ]) CLASSIFIERS = [ From 95c883ae9b759fe751903392d38c8e3e0decb1d7 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 19:14:25 +0200 Subject: [PATCH 186/222] Fix MCP23017 setup priority (#751) Fixes https://github.com/esphome/issues/issues/535 --- esphome/components/mcp23017/mcp23017.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 996c50e8f2d2b05f134a9663ede20ba9509c887d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 19:14:52 +0200 Subject: [PATCH 187/222] Add rotary_encoder.set_value action (#747) * Add rotary_encoder.set_value action Fixes https://github.com/esphome/feature-requests/issues/389 * Fix --- .../rotary_encoder/rotary_encoder.h | 17 +++++++++++++++++ esphome/components/rotary_encoder/sensor.py | 19 +++++++++++++++++-- tests/test1.yaml | 8 ++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) 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..742096cd5a 100644 --- a/esphome/components/rotary_encoder/sensor.py +++ b/esphome/components/rotary_encoder/sensor.py @@ -1,9 +1,9 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome import pins +from esphome import pins, automation from esphome.components import sensor from esphome.const import CONF_ID, CONF_RESOLUTION, CONF_MIN_VALUE, CONF_MAX_VALUE, UNIT_STEPS, \ - ICON_ROTATE_RIGHT + ICON_ROTATE_RIGHT, CONF_VALUE rotary_encoder_ns = cg.esphome_ns.namespace('rotary_encoder') RotaryEncoderResolution = rotary_encoder_ns.enum('RotaryEncoderResolution') @@ -18,6 +18,8 @@ CONF_PIN_B = 'pin_b' CONF_PIN_RESET = 'pin_reset' RotaryEncoderSensor = rotary_encoder_ns.class_('RotaryEncoderSensor', sensor.Sensor, cg.Component) +RotaryEncoderSetValueAction = rotary_encoder_ns.class_('RotaryEncoderSetValueAction', + automation.Action) def validate_min_max_value(config): @@ -60,3 +62,16 @@ def to_code(config): cg.add(var.set_min_value(config[CONF_MIN_VALUE])) if CONF_MAX_VALUE in config: cg.add(var.set_max_value(config[CONF_MAX_VALUE])) + + +@automation.register_action('sensor.rotary_encoder.set_value', RotaryEncoderSetValueAction, + cv.Schema({ + cv.Required(CONF_ID): cv.use_id(sensor.Sensor), + cv.Required(CONF_VALUE): cv.templatable(cv.int_), + })) +def sensor_template_publish_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_VALUE], args, int) + cg.add(var.set_value(template_)) + yield var diff --git a/tests/test1.yaml b/tests/test1.yaml index f2adaa19fd..f35b9caf2f 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -491,6 +491,7 @@ sensor: update_interval: 15s - platform: rotary_encoder name: "Rotary Encoder" + id: rotary_encoder1 pin_a: GPIO23 pin_b: GPIO24 pin_reset: GPIO25 @@ -501,6 +502,13 @@ sensor: resolution: 4 min_value: -10 max_value: 30 + on_value: + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: 10 + - sensor.rotary_encoder.set_value: + id: rotary_encoder1 + value: !lambda 'return -1;' - platform: pulse_width name: Pulse Width pin: GPIO12 From aae633277fc906018bcdbd0cec006b903009b497 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 19:15:02 +0200 Subject: [PATCH 188/222] Fix strobe/flicker effect not using selected value (#749) Fixes https://github.com/esphome/issues/issues/562 --- esphome/components/light/light_state.cpp | 11 +++++++---- esphome/components/light/light_state.h | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index cafced27fc..2b16319ddb 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_) { diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index d67aa2c53d..c460be09be 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -291,7 +291,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); From 3bb643049536cb847117b7282882eae6abb7363f Mon Sep 17 00:00:00 2001 From: Thomas Klingbeil Date: Thu, 17 Oct 2019 20:55:27 +0200 Subject: [PATCH 189/222] Add support for TTGO ePaper module (#730) * Add support for TTGO ePaper module Use 2.13in-ttgo as type. Only different LUTs were needed, everything else is the same. relates to issue #233. * fix styling errors * styling fixes Co-authored-by: null --- .../components/waveshare_epaper/display.py | 1 + .../waveshare_epaper/waveshare_epaper.cpp | 61 ++++++++++++++++--- .../waveshare_epaper/waveshare_epaper.h | 3 +- 3 files changed, 55 insertions(+), 10 deletions(-) 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 c4d73c49f6..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_()); @@ -134,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; @@ -154,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_; } @@ -206,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; } @@ -217,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) {} diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index a8b85c93e9..13aebd4ec9 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -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; From 1242f4376977ed4d7cc4d1666903e266423c6d1b Mon Sep 17 00:00:00 2001 From: Lazar Obradovic Date: Thu, 17 Oct 2019 22:58:59 +0400 Subject: [PATCH 190/222] BME280: Increase sensor timeout (#727) I'm facing some occasional timeouts when reading BME280. Looking at Adafruit driver (that this code is based on), I see that base math is using 1.25ms, increased by 2.3*oversampliing + 0.575 for each value being read. I've added 1.5ms as baseline, to be on the same safe. --- esphome/components/bme280/bme280.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280/bme280.cpp index f32a0d2861..b7c7f12f6f 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280/bme280.cpp @@ -178,7 +178,7 @@ void BME280Component::update() { return; } - float meas_time = 1; + float meas_time = 1.5; meas_time += 2.3f * oversampling_to_time(this->temperature_oversampling_); meas_time += 2.3f * oversampling_to_time(this->pressure_oversampling_) + 0.575f; meas_time += 2.3f * oversampling_to_time(this->humidity_oversampling_) + 0.575f; From 578e5a0d7a692f6894e4d141460956978a70350c Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 17 Oct 2019 16:01:02 -0300 Subject: [PATCH 191/222] Base climate ir (#726) * add ClimateIR * update climate ir * update class comment * lint * moved to climate_ir * fix include path * use climateir * updates * update include path * lint * fixed variable assigned to itself --- esphome/components/climate_ir/__init__.py | 0 esphome/components/climate_ir/climate_ir.cpp | 57 ++++++++++++++++++++ esphome/components/climate_ir/climate_ir.h | 53 ++++++++++++++++++ esphome/components/coolix/climate.py | 2 +- esphome/components/coolix/coolix.cpp | 53 +----------------- esphome/components/coolix/coolix.h | 38 ++++--------- 6 files changed, 121 insertions(+), 82 deletions(-) create mode 100644 esphome/components/climate_ir/__init__.py create mode 100644 esphome/components/climate_ir/climate_ir.cpp create mode 100644 esphome/components/climate_ir/climate_ir.h diff --git a/esphome/components/climate_ir/__init__.py b/esphome/components/climate_ir/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/climate_ir/climate_ir.cpp b/esphome/components/climate_ir/climate_ir.cpp new file mode 100644 index 0000000000..3d8b97e131 --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.cpp @@ -0,0 +1,57 @@ +#include "climate_ir.h" + +namespace esphome { +namespace climate { + +climate::ClimateTraits ClimateIR::traits() { + auto traits = climate::ClimateTraits(); + traits.set_supports_current_temperature(this->sensor_ != nullptr); + traits.set_supports_auto_mode(true); + traits.set_supports_cool_mode(this->supports_cool_); + traits.set_supports_heat_mode(this->supports_heat_); + traits.set_supports_two_point_target_temperature(false); + traits.set_supports_away(false); + traits.set_visual_min_temperature(this->minimum_temperature_); + traits.set_visual_max_temperature(this->maximum_temperature_); + traits.set_visual_temperature_step(this->temperature_step_); + return traits; +} + +void ClimateIR::setup() { + if (this->sensor_) { + this->sensor_->add_on_state_callback([this](float state) { + this->current_temperature = state; + // current temperature changed, publish state + this->publish_state(); + }); + this->current_temperature = this->sensor_->state; + } else + this->current_temperature = NAN; + // restore set points + auto restore = this->restore_state_(); + if (restore.has_value()) { + restore->apply(this); + } else { + // restore from defaults + this->mode = climate::CLIMATE_MODE_OFF; + // initialize target temperature to some value so that it's not NAN + this->target_temperature = + roundf(clamp(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_)); + } + // Never send nan to HA + if (isnan(this->target_temperature)) + this->target_temperature = 24; +} + +void ClimateIR::control(const climate::ClimateCall &call) { + if (call.get_mode().has_value()) + this->mode = *call.get_mode(); + if (call.get_target_temperature().has_value()) + this->target_temperature = *call.get_target_temperature(); + + this->transmit_state(); + this->publish_state(); +} + +} // namespace climate +} // namespace esphome diff --git a/esphome/components/climate_ir/climate_ir.h b/esphome/components/climate_ir/climate_ir.h new file mode 100644 index 0000000000..85f56c6b6b --- /dev/null +++ b/esphome/components/climate_ir/climate_ir.h @@ -0,0 +1,53 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/components/remote_base/remote_base.h" +#include "esphome/components/remote_transmitter/remote_transmitter.h" +#include "esphome/components/sensor/sensor.h" + +namespace esphome { +namespace climate { + +/* A base for climate which works by sending (and receiving) IR codes + + To send IR codes implement + void ClimateIR::transmit_state_() + + Likewise to decode a IR into the AC state, implement + bool RemoteReceiverListener::on_receive(remote_base::RemoteReceiveData data) and return true +*/ +class ClimateIR : public climate::Climate, public Component, public remote_base::RemoteReceiverListener { + public: + ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f) { + this->minimum_temperature_ = minimum_temperature; + this->maximum_temperature_ = maximum_temperature; + this->temperature_step_ = temperature_step; + } + + void setup() override; + void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { + this->transmitter_ = transmitter; + } + void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } + void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } + void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + + protected: + float minimum_temperature_, maximum_temperature_, temperature_step_; + + /// Override control to change settings of the climate device. + void control(const climate::ClimateCall &call) override; + /// Return the traits of this controller. + climate::ClimateTraits traits() override; + + /// Transmit via IR the state of this climate controller. + virtual void transmit_state() {} + + bool supports_cool_{true}; + bool supports_heat_{true}; + + remote_transmitter::RemoteTransmitterComponent *transmitter_; + sensor::Sensor *sensor_{nullptr}; +}; +} // namespace climate +} // namespace esphome diff --git a/esphome/components/coolix/climate.py b/esphome/components/coolix/climate.py index ed88f6ad6a..fe74798689 100644 --- a/esphome/components/coolix/climate.py +++ b/esphome/components/coolix/climate.py @@ -3,7 +3,7 @@ import esphome.config_validation as cv from esphome.components import climate, remote_transmitter, remote_receiver, sensor from esphome.const import CONF_ID, CONF_SENSOR -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['sensor', 'climate_ir'] coolix_ns = cg.esphome_ns.namespace('coolix') CoolixClimate = coolix_ns.class_('CoolixClimate', climate.Climate, cg.Component) diff --git a/esphome/components/coolix/coolix.cpp b/esphome/components/coolix/coolix.cpp index 4da307a737..c08571c2e9 100644 --- a/esphome/components/coolix/coolix.cpp +++ b/esphome/components/coolix/coolix.cpp @@ -27,8 +27,6 @@ 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; @@ -60,56 +58,7 @@ 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_OFF; - // initialize target temperature to some value so that it's not NAN - this->target_temperature = (uint8_t) roundf(clamp(this->current_temperature, COOLIX_TEMP_MIN, COOLIX_TEMP_MAX)); - } - // never send nan as temperature. HA will disable the user to change the temperature. - if (isnan(this->target_temperature)) - this->target_temperature = 24; -} - -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) { diff --git a/esphome/components/coolix/coolix.h b/esphome/components/coolix/coolix.h index 392728c654..125d8ffd37 100644 --- a/esphome/components/coolix/coolix.h +++ b/esphome/components/coolix/coolix.h @@ -1,43 +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 { -using namespace remote_base; +// Temperature +const uint8_t COOLIX_TEMP_MIN = 17; // Celsius +const uint8_t COOLIX_TEMP_MAX = 30; // Celsius -class CoolixClimate : public climate::Climate, public Component, public RemoteReceiverListener { +class CoolixClimate : public climate::ClimateIR { public: - void setup() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } - void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + CoolixClimate() : climate::ClimateIR(COOLIX_TEMP_MIN, COOLIX_TEMP_MAX) {} protected: - /// Override control to change settings of the climate device. - void control(const climate::ClimateCall &call) override; - /// Return the traits of this controller. - climate::ClimateTraits traits() override; - /// Transmit via IR the state of this climate controller. - void transmit_state_(); - - bool on_receive(RemoteReceiveData data) override; - - bool supports_cool_; - bool supports_heat_; - - 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 From 32195f77d95bf2b7873b6a9a809e2b3cb02ab10e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 21:34:58 +0200 Subject: [PATCH 192/222] Fix dallas not unknown (#716) * Fix dallas not sending unknown on disconnected sensor * Deep sleep --- esphome/components/deep_sleep/__init__.py | 3 +-- tests/test1.yaml | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) 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/tests/test1.yaml b/tests/test1.yaml index f35b9caf2f..09482f846e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -73,10 +73,10 @@ mqtt: ESP_LOGD("main", "Got message %s", x.c_str()); - topic: livingroom/ota_mode then: - - deep_sleep.prevent: deep_sleep_1 + - deep_sleep.prevent - topic: livingroom/ota_mode then: - - deep_sleep.enter: deep_sleep_1 + - deep_sleep.enter: on_json_message: topic: the/topic then: @@ -163,7 +163,6 @@ deep_sleep: sleep_duration: 50s wakeup_pin: GPIO39 wakeup_pin_mode: INVERT_WAKEUP - id: deep_sleep_1 ads1115: address: 0x48 From 9a40d6ef50579f07c1119988483cb5390d6fc2b2 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 21:35:31 +0200 Subject: [PATCH 193/222] Integration sensor use double precision (#715) Fixes https://github.com/esphome/issues/issues/534 Kept the RTC value as a float in order not to introduce a breaking preferences change. --- esphome/components/integration/integration_sensor.cpp | 10 +++++----- esphome/components/integration/integration_sensor.h | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/esphome/components/integration/integration_sensor.cpp b/esphome/components/integration/integration_sensor.cpp index 22fab290dd..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; 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}; }; From 6ceb975a3a88e967b26c993096a75f8da5d7ff0d Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 17 Oct 2019 21:35:59 +0200 Subject: [PATCH 194/222] calibrate_linear check not all from values same (#714) Fixes https://github.com/esphome/issues/issues/524 --- esphome/components/sensor/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 5211322615..a9b00b7c08 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -200,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] From d51c0f13c07e8b1b1965be562149bacd426ddb7c Mon Sep 17 00:00:00 2001 From: Nikolay Vasilchuk Date: Thu, 17 Oct 2019 22:37:24 +0300 Subject: [PATCH 195/222] SenseAir S8 CO2 sensor support (#705) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Компилится * Tests * Checksum calculation * Read status --- esphome/components/senseair/__init__.py | 0 esphome/components/senseair/senseair.cpp | 79 ++++++++++++++++++++++++ esphome/components/senseair/senseair.h | 26 ++++++++ esphome/components/senseair/sensor.py | 24 +++++++ tests/test1.yaml | 4 ++ 5 files changed, 133 insertions(+) create mode 100644 esphome/components/senseair/__init__.py create mode 100644 esphome/components/senseair/senseair.cpp create mode 100644 esphome/components/senseair/senseair.h create mode 100644 esphome/components/senseair/sensor.py diff --git a/esphome/components/senseair/__init__.py b/esphome/components/senseair/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/senseair/senseair.cpp b/esphome/components/senseair/senseair.cpp new file mode 100644 index 0000000000..96f456282f --- /dev/null +++ b/esphome/components/senseair/senseair.cpp @@ -0,0 +1,79 @@ +#include "senseair.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace senseair { + +static const char *TAG = "senseair"; +static const uint8_t SENSEAIR_REQUEST_LENGTH = 8; +static const uint8_t SENSEAIR_RESPONSE_LENGTH = 13; +static const uint8_t SENSEAIR_COMMAND_GET_PPM[] = {0xFE, 0x04, 0x00, 0x00, 0x00, 0x04, 0xE5, 0xC6}; + +void SenseAirComponent::update() { + uint8_t response[SENSEAIR_RESPONSE_LENGTH]; + if (!this->senseair_write_command_(SENSEAIR_COMMAND_GET_PPM, response)) { + ESP_LOGW(TAG, "Reading data from SenseAir failed!"); + this->status_set_warning(); + return; + } + + if (response[0] != 0xFE || response[1] != 0x04) { + ESP_LOGW(TAG, "Invalid preamble from SenseAir!"); + this->status_set_warning(); + return; + } + + uint16_t calc_checksum = this->senseair_checksum_(response, 11); + uint16_t resp_checksum = (uint16_t(response[12]) << 8) | response[11]; + if (resp_checksum != calc_checksum) { + ESP_LOGW(TAG, "SenseAir checksum doesn't match: 0x%02X!=0x%02X", resp_checksum, calc_checksum); + this->status_set_warning(); + return; + } + + this->status_clear_warning(); + const uint8_t length = response[2]; + const uint16_t status = (uint16_t(response[3]) << 8) | response[4]; + const uint16_t ppm = (uint16_t(response[length + 1]) << 8) | response[length + 2]; + + ESP_LOGD(TAG, "SenseAir Received CO₂=%uppm Status=0x%02X", ppm, status); + if (this->co2_sensor_ != nullptr) + this->co2_sensor_->publish_state(ppm); +} + +uint16_t SenseAirComponent::senseair_checksum_(uint8_t *ptr, uint8_t length) { + uint16_t crc = 0xFFFF; + uint8_t i; + while (length--) { + crc ^= *ptr++; + for (i = 0; i < 8; i++) { + if ((crc & 0x01) != 0) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + return crc; +} + +bool SenseAirComponent::senseair_write_command_(const uint8_t *command, uint8_t *response) { + this->flush(); + this->write_array(command, SENSEAIR_REQUEST_LENGTH); + + if (response == nullptr) + return true; + + bool ret = this->read_array(response, SENSEAIR_RESPONSE_LENGTH); + this->flush(); + return ret; +} + +void SenseAirComponent::dump_config() { + ESP_LOGCONFIG(TAG, "SenseAir:"); + LOG_SENSOR(" ", "CO2", this->co2_sensor_); +} + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/senseair.h b/esphome/components/senseair/senseair.h new file mode 100644 index 0000000000..23bcf40b5a --- /dev/null +++ b/esphome/components/senseair/senseair.h @@ -0,0 +1,26 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace senseair { + +class SenseAirComponent : public PollingComponent, public uart::UARTDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void set_co2_sensor(sensor::Sensor *co2_sensor) { co2_sensor_ = co2_sensor; } + + void update() override; + void dump_config() override; + + protected: + uint16_t senseair_checksum_(uint8_t *ptr, uint8_t length); + bool senseair_write_command_(const uint8_t *command, uint8_t *response); + + sensor::Sensor *co2_sensor_{nullptr}; +}; + +} // namespace senseair +} // namespace esphome diff --git a/esphome/components/senseair/sensor.py b/esphome/components/senseair/sensor.py new file mode 100644 index 0000000000..393bfd5182 --- /dev/null +++ b/esphome/components/senseair/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, uart +from esphome.const import CONF_CO2, CONF_ID, ICON_PERIODIC_TABLE_CO2, UNIT_PARTS_PER_MILLION + +DEPENDENCIES = ['uart'] + +senseair_ns = cg.esphome_ns.namespace('senseair') +SenseAirComponent = senseair_ns.class_('SenseAirComponent', cg.PollingComponent, uart.UARTDevice) + +CONFIG_SCHEMA = cv.Schema({ + cv.GenerateID(): cv.declare_id(SenseAirComponent), + cv.Required(CONF_CO2): sensor.sensor_schema(UNIT_PARTS_PER_MILLION, ICON_PERIODIC_TABLE_CO2, 0), +}).extend(cv.polling_component_schema('60s')).extend(uart.UART_DEVICE_SCHEMA) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield uart.register_uart_device(var, config) + + if CONF_CO2 in config: + sens = yield sensor.new_sensor(config[CONF_CO2]) + cg.add(var.set_co2_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 09482f846e..b2293969cd 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -511,6 +511,10 @@ sensor: - platform: pulse_width name: Pulse Width pin: GPIO12 + - platform: senseair + co2: + name: "SenseAir CO2 Value" + update_interval: 15s - platform: sht3xd temperature: name: "Living Room Temperature 8" From ea762b7295b9bea5aa129ae96175e063711bd322 Mon Sep 17 00:00:00 2001 From: nicuh Date: Fri, 18 Oct 2019 09:05:37 +0200 Subject: [PATCH 196/222] Fix remote_transmitter type_a encoding (#742) Co-authored-by: Nicu Hodos --- .../remote_base/rc_switch_protocol.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/esphome/components/remote_base/rc_switch_protocol.cpp b/esphome/components/remote_base/rc_switch_protocol.cpp index 258e6352e2..754b2fae49 100644 --- a/esphome/components/remote_base/rc_switch_protocol.cpp +++ b/esphome/components/remote_base/rc_switch_protocol.cpp @@ -141,19 +141,13 @@ void RCSwitchBase::simple_code_to_tristate(uint16_t code, uint8_t nbits, uint64_ 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, uint64_t *out_code, uint8_t *out_nbits) { From 84cfcf2b4ae86fdb382ea729d25d6ada078f7f0a Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 18 Oct 2019 04:17:16 -0300 Subject: [PATCH 197/222] vscode support check file exists (#763) * vscode support check file exists * ups. formatter got disabled --- esphome/config_validation.py | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index dfe05a7637..405d695b51 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -80,6 +80,7 @@ class Optional(vol.Optional): during config validation - specifically *not* in the C++ code or the code generation phase. """ + def __init__(self, key, default=UNDEFINED): super(Optional, self).__init__(key, default=default) @@ -91,6 +92,7 @@ class Required(vol.Required): All required values should be acceessed with the `config[CONF_]` syntax in code - *not* the `config.get(CONF_)` syntax. """ + def __init__(self, key): super(Required, self).__init__(key) @@ -1021,8 +1023,24 @@ def dimensions(value): def directory(value): + import json + from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) + + if CORE.vscode and (not CORE.ace or + os.path.abspath(path) == os.path.abspath(CORE.config_path)): + print(json.dumps({ + 'type': 'check_directory_exists', + 'path': path, + })) + data = json.loads(safe_input()) + assert data['type'] == 'directory_exists_response' + if data['content']: + return value + raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." + u"".format(path, os.path.abspath(path))) + if not os.path.exists(path): raise Invalid(u"Could not find directory '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) @@ -1033,8 +1051,24 @@ def directory(value): def file_(value): + import json + from esphome.py_compat import safe_input value = string(value) path = CORE.relative_config_path(value) + + if CORE.vscode and (not CORE.ace or + os.path.abspath(path) == os.path.abspath(CORE.config_path)): + print(json.dumps({ + 'type': 'check_file_exists', + 'path': path, + })) + data = json.loads(safe_input()) + assert data['type'] == 'file_exists_response' + if data['content']: + return value + raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." + u"".format(path, os.path.abspath(path))) + if not os.path.exists(path): raise Invalid(u"Could not find file '{}'. Please make sure it exists (full path: {})." u"".format(path, os.path.abspath(path))) @@ -1100,12 +1134,14 @@ def typed_schema(schemas, **kwargs): class GenerateID(Optional): """Mark this key as being an auto-generated ID key.""" + def __init__(self, key=CONF_ID): super(GenerateID, self).__init__(key, default=lambda: None) class SplitDefault(Optional): """Mark this key to have a split default for ESP8266/ESP32.""" + def __init__(self, key, esp8266=vol.UNDEFINED, esp32=vol.UNDEFINED): super(SplitDefault, self).__init__(key) self._esp8266_default = vol.default_factory(esp8266) @@ -1127,6 +1163,7 @@ class SplitDefault(Optional): class OnlyWith(Optional): """Set the default value only if the given component is loaded.""" + def __init__(self, key, component, default=None): super(OnlyWith, self).__init__(key) self._component = component From 8292024306431f3d7c8d5d4b22e2015d523b4885 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Fri, 18 Oct 2019 04:22:55 -0300 Subject: [PATCH 198/222] add tcl112 receiver (#762) --- esphome/components/tcl112/climate.py | 9 +- esphome/components/tcl112/tcl112.cpp | 131 ++++++++++++++++----------- esphome/components/tcl112/tcl112.h | 36 ++------ 3 files changed, 96 insertions(+), 80 deletions(-) diff --git a/esphome/components/tcl112/climate.py b/esphome/components/tcl112/climate.py index 50fef7b125..d21300d946 100644 --- a/esphome/components/tcl112/climate.py +++ b/esphome/components/tcl112/climate.py @@ -1,20 +1,22 @@ import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import climate, remote_transmitter, sensor +from esphome.components import climate, remote_transmitter, remote_receiver, sensor from esphome.const import CONF_ID, CONF_SENSOR -AUTO_LOAD = ['sensor'] +AUTO_LOAD = ['sensor', 'climate_ir'] tcl112_ns = cg.esphome_ns.namespace('tcl112') Tcl112Climate = tcl112_ns.class_('Tcl112Climate', climate.Climate, cg.Component) CONF_TRANSMITTER_ID = 'transmitter_id' +CONF_RECEIVER_ID = 'receiver_id' CONF_SUPPORTS_HEAT = 'supports_heat' CONF_SUPPORTS_COOL = 'supports_cool' CONFIG_SCHEMA = cv.All(climate.CLIMATE_SCHEMA.extend({ cv.GenerateID(): cv.declare_id(Tcl112Climate), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(remote_transmitter.RemoteTransmitterComponent), + cv.Optional(CONF_RECEIVER_ID): cv.use_id(remote_receiver.RemoteReceiverComponent), cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), @@ -31,6 +33,9 @@ def to_code(config): if CONF_SENSOR in config: sens = yield cg.get_variable(config[CONF_SENSOR]) cg.add(var.set_sensor(sens)) + if CONF_RECEIVER_ID in config: + receiver = yield cg.get_variable(config[CONF_RECEIVER_ID]) + cg.add(receiver.register_listener(var)) transmitter = yield cg.get_variable(config[CONF_TRANSMITTER_ID]) cg.add(var.set_transmitter(transmitter)) diff --git a/esphome/components/tcl112/tcl112.cpp b/esphome/components/tcl112/tcl112.cpp index cbe2f53402..2907ae1743 100644 --- a/esphome/components/tcl112/tcl112.cpp +++ b/esphome/components/tcl112/tcl112.cpp @@ -18,62 +18,15 @@ const uint8_t TCL112_AUTO = 8; const uint8_t TCL112_POWER_MASK = 0x04; const uint8_t TCL112_HALF_DEGREE = 0b00100000; -const float TCL112_TEMP_MAX = 31.0; -const float TCL112_TEMP_MIN = 16.0; -const uint16_t TCL112_HEADER_MARK = 3000; +const uint16_t TCL112_HEADER_MARK = 3100; const uint16_t TCL112_HEADER_SPACE = 1650; const uint16_t TCL112_BIT_MARK = 500; -const uint16_t TCL112_ONE_SPACE = 1050; -const uint16_t TCL112_ZERO_SPACE = 325; +const uint16_t TCL112_ONE_SPACE = 1100; +const uint16_t TCL112_ZERO_SPACE = 350; const uint32_t TCL112_GAP = TCL112_HEADER_SPACE; -climate::ClimateTraits Tcl112Climate::traits() { - auto traits = climate::ClimateTraits(); - traits.set_supports_current_temperature(this->sensor_ != nullptr); - traits.set_supports_auto_mode(true); - traits.set_supports_cool_mode(this->supports_cool_); - traits.set_supports_heat_mode(this->supports_heat_); - traits.set_supports_two_point_target_temperature(false); - traits.set_supports_away(false); - traits.set_visual_min_temperature(TCL112_TEMP_MIN); - traits.set_visual_max_temperature(TCL112_TEMP_MAX); - traits.set_visual_temperature_step(.5f); - return traits; -} - -void Tcl112Climate::setup() { - if (this->sensor_) { - this->sensor_->add_on_state_callback([this](float state) { - this->current_temperature = state; - // current temperature changed, publish state - this->publish_state(); - }); - this->current_temperature = this->sensor_->state; - } else - this->current_temperature = NAN; - // restore set points - auto restore = this->restore_state_(); - if (restore.has_value()) { - restore->apply(this); - } else { - // restore from defaults - this->mode = climate::CLIMATE_MODE_OFF; - this->target_temperature = 24; - } -} - -void Tcl112Climate::control(const climate::ClimateCall &call) { - if (call.get_mode().has_value()) - this->mode = *call.get_mode(); - if (call.get_target_temperature().has_value()) - this->target_temperature = *call.get_target_temperature(); - - this->transmit_state_(); - this->publish_state(); -} - -void Tcl112Climate::transmit_state_() { +void Tcl112Climate::transmit_state() { uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; // A known good state. (On, Cool, 24C) @@ -124,7 +77,10 @@ void Tcl112Climate::transmit_state_() { for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) remote_state[TCL112_STATE_LENGTH - 1] += remote_state[checksum_byte]; - ESP_LOGV(TAG, "Sending tcl code: %u", remote_state[7]); + ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0], + remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], + remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12], + remote_state[13]); auto transmit = this->transmitter_->transmit(); auto data = transmit.get_data(); @@ -148,5 +104,76 @@ void Tcl112Climate::transmit_state_() { transmit.perform(); } +bool Tcl112Climate::on_receive(remote_base::RemoteReceiveData data) { + // Validate header + if (!data.expect_item(TCL112_HEADER_MARK, TCL112_HEADER_SPACE)) { + ESP_LOGV(TAG, "Header fail"); + return false; + } + + uint8_t remote_state[TCL112_STATE_LENGTH] = {0}; + // Read all bytes. + for (int i = 0; i < TCL112_STATE_LENGTH; i++) { + // Read bit + for (int j = 0; j < 8; j++) { + if (data.expect_item(TCL112_BIT_MARK, TCL112_ONE_SPACE)) + remote_state[i] |= 1 << j; + else if (!data.expect_item(TCL112_BIT_MARK, TCL112_ZERO_SPACE)) { + ESP_LOGV(TAG, "Byte %d bit %d fail", i, j); + return false; + } + } + } + // Validate footer + if (!data.expect_mark(TCL112_BIT_MARK)) { + ESP_LOGV(TAG, "Footer fail"); + return false; + } + + uint8_t checksum = 0; + // Calculate & set the checksum for the current internal state of the remote. + // Stored the checksum value in the last byte. + for (uint8_t checksum_byte = 0; checksum_byte < TCL112_STATE_LENGTH - 1; checksum_byte++) + checksum += remote_state[checksum_byte]; + if (checksum != remote_state[TCL112_STATE_LENGTH - 1]) { + ESP_LOGV(TAG, "Checksum fail"); + return false; + } + + ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", + remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], + remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], + remote_state[12], remote_state[13]); + + // two first bytes are constant + if (remote_state[0] != 0x23 || remote_state[1] != 0xCB) + return false; + + if ((remote_state[5] & TCL112_POWER_MASK) == 0) { + this->mode = climate::CLIMATE_MODE_OFF; + } else { + auto mode = remote_state[6] & 0x0F; + switch (mode) { + case TCL112_HEAT: + this->mode = climate::CLIMATE_MODE_HEAT; + break; + case TCL112_COOL: + this->mode = climate::CLIMATE_MODE_COOL; + break; + case TCL112_DRY: + case TCL112_FAN: + case TCL112_AUTO: + this->mode = climate::CLIMATE_MODE_AUTO; + break; + } + } + auto temp = TCL112_TEMP_MAX - remote_state[7]; + if (remote_state[12] & TCL112_HALF_DEGREE) + temp += .5f; + this->target_temperature = temp; + this->publish_state(); + return true; +} + } // namespace tcl112 } // namespace esphome diff --git a/esphome/components/tcl112/tcl112.h b/esphome/components/tcl112/tcl112.h index 0b80dedbef..7c5257a0f3 100644 --- a/esphome/components/tcl112/tcl112.h +++ b/esphome/components/tcl112/tcl112.h @@ -1,39 +1,23 @@ #pragma once -#include "esphome/core/component.h" -#include "esphome/core/automation.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/remote_base/remote_base.h" -#include "esphome/components/remote_transmitter/remote_transmitter.h" -#include "esphome/components/sensor/sensor.h" +#include "esphome/components/climate_ir/climate_ir.h" namespace esphome { namespace tcl112 { -class Tcl112Climate : public climate::Climate, public Component { +// Temperature +const float TCL112_TEMP_MAX = 31.0; +const float TCL112_TEMP_MIN = 16.0; + +class Tcl112Climate : public climate::ClimateIR { public: - void setup() override; - void set_transmitter(remote_transmitter::RemoteTransmitterComponent *transmitter) { - this->transmitter_ = transmitter; - } - void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } - void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } - void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } + Tcl112Climate() : climate::ClimateIR(TCL112_TEMP_MIN, TCL112_TEMP_MAX, .5f) {} protected: - /// Override control to change settings of the climate device. - void control(const climate::ClimateCall &call) override; - /// Return the traits of this controller. - climate::ClimateTraits traits() override; - /// Transmit via IR the state of this climate controller. - void transmit_state_(); - - bool supports_cool_{true}; - bool supports_heat_{true}; - - remote_transmitter::RemoteTransmitterComponent *transmitter_; - sensor::Sensor *sensor_{nullptr}; + void transmit_state() override; + /// Handle received IR Buffer + bool on_receive(remote_base::RemoteReceiveData data) override; }; } // namespace tcl112 From ee0b6e835f5d656b90964bb3ea10c15681d1b1b5 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 18 Oct 2019 10:22:29 +0200 Subject: [PATCH 199/222] Sensor filter_out rounded (#765) Fixes https://github.com/esphome/issues/issues/741 --- esphome/components/sensor/filter.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 3f0d39fcfc..f7a5b5d7ad 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -166,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; From 22aecdfc6f4e18083df176a8421bd18ab54ff341 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 18 Oct 2019 10:23:06 +0200 Subject: [PATCH 200/222] Use higher default baudrate for USB upload (#761) See also https://github.com/espressif/esptool/issues/435 --- esphome/__main__.py | 2 +- esphome/components/uart/__init__.py | 2 +- esphome/writer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 9c0b7a33d9..62c15fda6b 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -166,7 +166,7 @@ def compile_program(args, config): def upload_using_esptool(config, port): path = CORE.firmware_bin cmd = ['esptool.py', '--before', 'default_reset', '--after', 'hard_reset', - '--baud', str(config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 115200)), + '--baud', str(config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get('upload_speed', 460800)), '--chip', 'esp8266', '--port', port, 'write_flash', '0x0', path] if os.environ.get('ESPHOME_USE_SUBPROCESS') is None: diff --git a/esphome/components/uart/__init__.py b/esphome/components/uart/__init__.py index 5c983be002..110bd64c81 100644 --- a/esphome/components/uart/__init__.py +++ b/esphome/components/uart/__init__.py @@ -31,7 +31,7 @@ def validate_rx_pin(value): CONFIG_SCHEMA = cv.All(cv.Schema({ cv.GenerateID(): cv.declare_id(UARTComponent), - cv.Required(CONF_BAUD_RATE): cv.int_range(min=1, max=115200), + cv.Required(CONF_BAUD_RATE): cv.int_range(min=1), cv.Optional(CONF_TX_PIN): pins.output_pin, cv.Optional(CONF_RX_PIN): validate_rx_pin, }).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN)) diff --git a/esphome/writer.py b/esphome/writer.py index ddf75faf6b..8fa239d608 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -191,7 +191,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: From 72d6471ab8cab724c0fc805ed7a5bb216811652b Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 18 Oct 2019 10:39:14 +0200 Subject: [PATCH 201/222] add support for climate action (#720) * add support for climate action: Following hass implementation of climate, action represents the current action the climate device is perfoming, e.g. cooling or heating fix bang_bang climate: make sure that the thresholds are always respected. fixes the issue where the component would just keep on heating, regardless of the temperature range * Updates - Use dedicated enum for action (otherwise it gets confusing because "auto" is not a valid action) - Add field to tell HA that action is supported - Revert semantic changes in bang_bang * Conditional print Co-authored-by: Otto Winter --- esphome/components/api/api.proto | 8 ++++ esphome/components/api/api_connection.cpp | 2 + esphome/components/api/api_pb2.cpp | 30 +++++++++++++++ esphome/components/api/api_pb2.h | 7 ++++ .../bang_bang/bang_bang_climate.cpp | 37 +++++++++---------- .../components/bang_bang/bang_bang_climate.h | 7 +--- esphome/components/climate/climate.cpp | 3 ++ esphome/components/climate/climate.h | 2 + esphome/components/climate/climate_mode.cpp | 12 ++++++ esphome/components/climate/climate_mode.h | 11 ++++++ esphome/components/climate/climate_traits.cpp | 2 + esphome/components/climate/climate_traits.h | 5 +++ 12 files changed, 101 insertions(+), 25 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index c776a96f86..e454bf1d31 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -644,6 +644,12 @@ enum ClimateMode { CLIMATE_MODE_COOL = 2; CLIMATE_MODE_HEAT = 3; } +enum ClimateAction { + CLIMATE_ACTION_OFF = 0; + // values same as mode for readability + CLIMATE_ACTION_COOLING = 2; + CLIMATE_ACTION_HEATING = 3; +} message ListEntitiesClimateResponse { option (id) = 46; option (source) = SOURCE_SERVER; @@ -661,6 +667,7 @@ message ListEntitiesClimateResponse { float visual_max_temperature = 9; float visual_temperature_step = 10; bool supports_away = 11; + bool supports_action = 12; } message ClimateStateResponse { option (id) = 47; @@ -675,6 +682,7 @@ message ClimateStateResponse { float target_temperature_low = 5; float target_temperature_high = 6; bool away = 7; + ClimateAction action = 8; } message ClimateCommandRequest { option (id) = 48; diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 76cf72f8ff..4a595a3f99 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -442,6 +442,7 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { ClimateStateResponse resp{}; resp.key = climate->get_object_id_hash(); resp.mode = static_cast(climate->mode); + resp.action = static_cast(climate->action); if (traits.get_supports_current_temperature()) resp.current_temperature = climate->current_temperature; if (traits.get_supports_two_point_target_temperature()) { @@ -472,6 +473,7 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_temperature_step = traits.get_visual_temperature_step(); msg.supports_away = traits.get_supports_away(); + msg.supports_action = traits.get_supports_action(); return this->send_list_entities_climate_response(msg); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 815feedea8..c4fa89ef97 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -106,6 +106,18 @@ template<> const char *proto_enum_to_string(EnumClimateMode val return "UNKNOWN"; } } +template<> const char *proto_enum_to_string(EnumClimateAction value) { + switch (value) { + case CLIMATE_ACTION_OFF: + return "CLIMATE_ACTION_OFF"; + case CLIMATE_ACTION_COOLING: + return "CLIMATE_ACTION_COOLING"; + case CLIMATE_ACTION_HEATING: + return "CLIMATE_ACTION_HEATING"; + default: + return "UNKNOWN"; + } +} bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { switch (field_id) { case 1: { @@ -2394,6 +2406,10 @@ bool ListEntitiesClimateResponse::decode_varint(uint32_t field_id, ProtoVarInt v this->supports_away = value.as_bool(); return true; } + case 12: { + this->supports_action = value.as_bool(); + return true; + } default: return false; } @@ -2452,6 +2468,7 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(9, this->visual_max_temperature); buffer.encode_float(10, this->visual_temperature_step); buffer.encode_bool(11, this->supports_away); + buffer.encode_bool(12, this->supports_action); } void ListEntitiesClimateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2505,6 +2522,10 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const { out.append(" supports_away: "); out.append(YESNO(this->supports_away)); out.append("\n"); + + out.append(" supports_action: "); + out.append(YESNO(this->supports_action)); + out.append("\n"); out.append("}"); } bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { @@ -2517,6 +2538,10 @@ bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { this->away = value.as_bool(); return true; } + case 8: { + this->action = value.as_enum(); + return true; + } default: return false; } @@ -2555,6 +2580,7 @@ void ClimateStateResponse::encode(ProtoWriteBuffer buffer) const { buffer.encode_float(5, this->target_temperature_low); buffer.encode_float(6, this->target_temperature_high); buffer.encode_bool(7, this->away); + buffer.encode_enum(8, this->action); } void ClimateStateResponse::dump_to(std::string &out) const { char buffer[64]; @@ -2591,6 +2617,10 @@ void ClimateStateResponse::dump_to(std::string &out) const { out.append(" away: "); out.append(YESNO(this->away)); out.append("\n"); + + out.append(" action: "); + out.append(proto_enum_to_string(this->action)); + out.append("\n"); out.append("}"); } bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 50bf3117c0..9997a68477 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -49,6 +49,11 @@ enum EnumClimateMode : uint32_t { CLIMATE_MODE_COOL = 2, CLIMATE_MODE_HEAT = 3, }; +enum EnumClimateAction : uint32_t { + CLIMATE_ACTION_OFF = 0, + CLIMATE_ACTION_COOLING = 2, + CLIMATE_ACTION_HEATING = 3, +}; class HelloRequest : public ProtoMessage { public: std::string client_info{}; // NOLINT @@ -638,6 +643,7 @@ class ListEntitiesClimateResponse : public ProtoMessage { float visual_max_temperature{0.0f}; // NOLINT float visual_temperature_step{0.0f}; // NOLINT bool supports_away{false}; // NOLINT + bool supports_action{false}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; @@ -655,6 +661,7 @@ class ClimateStateResponse : public ProtoMessage { float target_temperature_low{0.0f}; // NOLINT float target_temperature_high{0.0f}; // NOLINT bool away{false}; // NOLINT + EnumClimateAction action{}; // NOLINT void encode(ProtoWriteBuffer buffer) const override; void dump_to(std::string &out) const override; diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index 1bdabaec37..17c5c0bc48 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -46,52 +46,51 @@ climate::ClimateTraits BangBangClimate::traits() { traits.set_supports_heat_mode(this->supports_heat_); traits.set_supports_two_point_target_temperature(true); traits.set_supports_away(this->supports_away_); + traits.set_supports_action(true); return traits; } void BangBangClimate::compute_state_() { if (this->mode != climate::CLIMATE_MODE_AUTO) { // in non-auto mode - this->switch_to_mode_(this->mode); + this->switch_to_action_(static_cast(this->mode)); return; } - - // auto mode, compute target mode if (isnan(this->current_temperature) || isnan(this->target_temperature_low) || isnan(this->target_temperature_high)) { // if any control values are nan, go to OFF (idle) mode - this->switch_to_mode_(climate::CLIMATE_MODE_OFF); + this->switch_to_action_(climate::CLIMATE_ACTION_OFF); return; } const bool too_cold = this->current_temperature < this->target_temperature_low; const bool too_hot = this->current_temperature > this->target_temperature_high; - climate::ClimateMode target_mode; + climate::ClimateAction target_action; if (too_cold) { // too cold -> enable heating if possible, else idle if (this->supports_heat_) - target_mode = climate::CLIMATE_MODE_HEAT; + target_action = climate::CLIMATE_ACTION_HEATING; else - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else if (too_hot) { // too hot -> enable cooling if possible, else idle if (this->supports_cool_) - target_mode = climate::CLIMATE_MODE_COOL; + target_action = climate::CLIMATE_ACTION_COOLING; else - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else { // neither too hot nor too cold -> in range if (this->supports_cool_ && this->supports_heat_) { // if supports both ends, go to idle mode - target_mode = climate::CLIMATE_MODE_OFF; + target_action = climate::CLIMATE_ACTION_OFF; } else { // else use current mode and don't change (hysteresis) - target_mode = this->internal_mode_; + target_action = this->action; } } - this->switch_to_mode_(target_mode); + this->switch_to_action_(target_action); } -void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { - if (mode == this->internal_mode_) +void BangBangClimate::switch_to_action_(climate::ClimateAction action) { + if (action == this->action) // already in target mode return; @@ -100,14 +99,14 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { this->prev_trigger_ = nullptr; } Trigger<> *trig; - switch (mode) { - case climate::CLIMATE_MODE_OFF: + switch (action) { + case climate::CLIMATE_ACTION_OFF: trig = this->idle_trigger_; break; - case climate::CLIMATE_MODE_COOL: + case climate::CLIMATE_ACTION_COOLING: trig = this->cool_trigger_; break; - case climate::CLIMATE_MODE_HEAT: + case climate::CLIMATE_ACTION_HEATING: trig = this->heat_trigger_; break; default: @@ -116,7 +115,7 @@ void BangBangClimate::switch_to_mode_(climate::ClimateMode mode) { if (trig != nullptr) { // trig should never be null, but still check so that we don't crash trig->trigger(); - this->internal_mode_ = mode; + this->action = action; this->prev_trigger_ = trig; this->publish_state(); } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 716655d20f..0a79c1d7af 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -43,7 +43,7 @@ class BangBangClimate : public climate::Climate, public Component { void compute_state_(); /// Switch the climate device to the given climate mode. - void switch_to_mode_(climate::ClimateMode mode); + void switch_to_action_(climate::ClimateAction action); /// The sensor used for getting the current temperature sensor::Sensor *sensor_{nullptr}; @@ -74,11 +74,6 @@ class BangBangClimate : public climate::Climate, public Component { * This is so that the previous trigger can be stopped before enabling a new one. */ Trigger<> *prev_trigger_{nullptr}; - /** The climate mode that is currently active - for a `.mode = AUTO` this will - * contain the actual mode the device - * - */ - climate::ClimateMode internal_mode_{climate::CLIMATE_MODE_OFF}; BangBangClimateTargetTempConfig normal_config_{}; bool supports_away_{false}; diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 2b40f932f9..7c7da6bb0c 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -173,6 +173,9 @@ void Climate::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", climate_mode_to_string(this->mode)); + if (traits.get_supports_action()) { + ESP_LOGD(TAG, " Action: %s", climate_action_to_string(this->action)); + } if (traits.get_supports_current_temperature()) { ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature); } diff --git a/esphome/components/climate/climate.h b/esphome/components/climate/climate.h index c58eed1a7c..70c6bef13b 100644 --- a/esphome/components/climate/climate.h +++ b/esphome/components/climate/climate.h @@ -121,6 +121,8 @@ class Climate : public Nameable { /// The active mode of the climate device. ClimateMode mode{CLIMATE_MODE_OFF}; + /// The active state of the climate device. + ClimateAction action{CLIMATE_ACTION_OFF}; /// The current temperature of the climate device, as reported from the integration. float current_temperature{NAN}; diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index 07b97f4f33..34aa564fb0 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -17,6 +17,18 @@ const char *climate_mode_to_string(ClimateMode mode) { return "UNKNOWN"; } } +const char *climate_action_to_string(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return "OFF"; + case CLIMATE_ACTION_COOLING: + return "COOLING"; + case CLIMATE_ACTION_HEATING: + return "HEATING"; + default: + return "UNKNOWN"; + } +} } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_mode.h b/esphome/components/climate/climate_mode.h index 28608b7cd8..e5786286d8 100644 --- a/esphome/components/climate/climate_mode.h +++ b/esphome/components/climate/climate_mode.h @@ -17,8 +17,19 @@ enum ClimateMode : uint8_t { CLIMATE_MODE_HEAT = 3, }; +/// Enum for the current action of the climate device. Values match those of ClimateMode. +enum ClimateAction : uint8_t { + /// The climate device is off (inactive or no power) + CLIMATE_ACTION_OFF = 0, + /// The climate device is actively cooling (usually in cool or auto mode) + CLIMATE_ACTION_COOLING = 2, + /// The climate device is actively heating (usually in heat or auto mode) + CLIMATE_ACTION_HEATING = 3, +}; + /// Convert the given ClimateMode to a human-readable string. const char *climate_mode_to_string(ClimateMode mode); +const char *climate_action_to_string(ClimateAction action); } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.cpp b/esphome/components/climate/climate_traits.cpp index 712186aa80..a1db2bc696 100644 --- a/esphome/components/climate/climate_traits.cpp +++ b/esphome/components/climate/climate_traits.cpp @@ -30,6 +30,7 @@ void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_a void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; } void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; } void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; } +void ClimateTraits::set_supports_action(bool supports_action) { supports_action_ = supports_action; } float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; } void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) { visual_min_temperature_ = visual_min_temperature; @@ -52,6 +53,7 @@ int8_t ClimateTraits::get_temperature_accuracy_decimals() const { } void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; } bool ClimateTraits::get_supports_away() const { return supports_away_; } +bool ClimateTraits::get_supports_action() const { return supports_action_; } } // namespace climate } // namespace esphome diff --git a/esphome/components/climate/climate_traits.h b/esphome/components/climate/climate_traits.h index 34e03455b1..2d6f44eea6 100644 --- a/esphome/components/climate/climate_traits.h +++ b/esphome/components/climate/climate_traits.h @@ -23,6 +23,8 @@ namespace climate { * - heat mode (increases current temperature) * - supports away - away mode means that the climate device supports two different * target temperature settings: one target temp setting for "away" mode and one for non-away mode. + * - supports action - if the climate device supports reporting the active + * current action of the device with the action property. * * This class also contains static data for the climate device display: * - visual min/max temperature - tells the frontend what range of temperatures the climate device @@ -41,6 +43,8 @@ class ClimateTraits { void set_supports_heat_mode(bool supports_heat_mode); void set_supports_away(bool supports_away); bool get_supports_away() const; + void set_supports_action(bool supports_action); + bool get_supports_action() const; bool supports_mode(ClimateMode mode) const; float get_visual_min_temperature() const; @@ -58,6 +62,7 @@ class ClimateTraits { bool supports_cool_mode_{false}; bool supports_heat_mode_{false}; bool supports_away_{false}; + bool supports_action_{false}; float visual_min_temperature_{10}; float visual_max_temperature_{30}; From 68d0d045c026f6f7b0397b2eed1222fbef5e71bb Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 18 Oct 2019 11:22:08 +0200 Subject: [PATCH 202/222] Add LEDC set_frequency action (#754) * Add LEDC set_frequency Fixes https://github.com/esphome/feature-requests/issues/380 * Fix log * Fixes * Format * Update test1.yaml * Update test1.yaml * Fix --- esphome/components/ledc/ledc_output.cpp | 41 ++++++++++++--- esphome/components/ledc/ledc_output.h | 23 ++++++-- esphome/components/ledc/output.py | 70 +++++++++++-------------- platformio.ini | 16 +++--- tests/test1.yaml | 7 ++- 5 files changed, 98 insertions(+), 59 deletions(-) diff --git a/esphome/components/ledc/ledc_output.cpp b/esphome/components/ledc/ledc_output.cpp index 64094478c0..b3652c84d6 100644 --- a/esphome/components/ledc/ledc_output.cpp +++ b/esphome/components/ledc/ledc_output.cpp @@ -11,10 +11,10 @@ namespace ledc { static const char *TAG = "ledc.output"; void LEDCOutput::write_state(float state) { - if (this->pin_->is_inverted()) { + if (this->pin_->is_inverted()) state = 1.0f - state; - } + this->duty_ = state; const uint32_t max_duty = (uint32_t(1) << this->bit_depth_) - 1; const float duty_rounded = roundf(state * max_duty); auto duty = static_cast(duty_rounded); @@ -22,18 +22,45 @@ void LEDCOutput::write_state(float state) { } void LEDCOutput::setup() { - ledcSetup(this->channel_, this->frequency_, this->bit_depth_); - ledcAttachPin(this->pin_->get_pin(), this->channel_); - + this->apply_frequency(this->frequency_); this->turn_off(); + // Attach pin after setting default value + ledcAttachPin(this->pin_->get_pin(), this->channel_); } void LEDCOutput::dump_config() { ESP_LOGCONFIG(TAG, "LEDC Output:"); - LOG_PIN(" Pin", this->pin_); + LOG_PIN(" Pin ", this->pin_); ESP_LOGCONFIG(TAG, " LEDC Channel: %u", this->channel_); ESP_LOGCONFIG(TAG, " Frequency: %.1f Hz", this->frequency_); - ESP_LOGCONFIG(TAG, " Bit Depth: %u", this->bit_depth_); +} + +float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { return 80e6f / float(1 << bit_depth); } +float ledc_min_frequency_for_bit_depth(uint8_t bit_depth) { + const float max_div_num = ((1 << 20) - 1) / 256.0f; + return 80e6f / (max_div_num * float(1 << bit_depth)); +} +optional ledc_bit_depth_for_frequency(float frequency) { + for (int i = 20; i >= 1; i--) { + const float min_frequency = ledc_min_frequency_for_bit_depth(frequency); + const float max_frequency = ledc_max_frequency_for_bit_depth(frequency); + if (min_frequency <= frequency && frequency <= max_frequency) + return i; + } + return {}; +} + +void LEDCOutput::apply_frequency(float frequency) { + auto bit_depth_opt = ledc_bit_depth_for_frequency(frequency); + if (!bit_depth_opt.has_value()) { + ESP_LOGW(TAG, "Frequency %f can't be achieved with any bit depth", frequency); + this->status_set_warning(); + } + this->bit_depth_ = *bit_depth_opt; + this->frequency_ = frequency; + ledcSetup(this->channel_, frequency, this->bit_depth_); + // re-apply duty + this->write_state(this->duty_); } uint8_t next_ledc_channel = 0; diff --git a/esphome/components/ledc/ledc_output.h b/esphome/components/ledc/ledc_output.h index d1b9b099ee..3f56f502b0 100644 --- a/esphome/components/ledc/ledc_output.h +++ b/esphome/components/ledc/ledc_output.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/core/esphal.h" +#include "esphome/core/automation.h" #include "esphome/components/output/float_output.h" #ifdef ARDUINO_ARCH_ESP32 @@ -16,11 +17,10 @@ class LEDCOutput : public output::FloatOutput, public Component { explicit LEDCOutput(GPIOPin *pin) : pin_(pin) { this->channel_ = next_ledc_channel++; } void set_channel(uint8_t channel) { this->channel_ = channel; } - void set_bit_depth(uint8_t bit_depth) { this->bit_depth_ = bit_depth; } void set_frequency(float frequency) { this->frequency_ = frequency; } + /// Dynamically change frequency at runtime + void apply_frequency(float frequency); - // ========== INTERNAL METHODS ========== - // (In most use cases you won't need these) /// Setup LEDC. void setup() override; void dump_config() override; @@ -28,13 +28,28 @@ class LEDCOutput : public output::FloatOutput, public Component { float get_setup_priority() const override { return setup_priority::HARDWARE; } /// Override FloatOutput's write_state. - void write_state(float adjusted_value) override; + void write_state(float state) override; protected: GPIOPin *pin_; uint8_t channel_{}; uint8_t bit_depth_{}; float frequency_{}; + float duty_{0.0f}; +}; + +template class SetFrequencyAction : public Action { + public: + SetFrequencyAction(LEDCOutput *parent) : parent_(parent) {} + TEMPLATABLE_VALUE(float, frequency); + + void play(Ts... x) { + float freq = this->frequency_.value(x...); + this->parent_->apply_frequency(freq); + } + + protected: + LEDCOutput *parent_; }; } // namespace ledc diff --git a/esphome/components/ledc/output.py b/esphome/components/ledc/output.py index c507465ff9..b608e9bbf7 100644 --- a/esphome/components/ledc/output.py +++ b/esphome/components/ledc/output.py @@ -1,6 +1,4 @@ -import math - -from esphome import pins +from esphome import pins, automation from esphome.components import output import esphome.config_validation as cv import esphome.codegen as cg @@ -15,53 +13,36 @@ def calc_max_frequency(bit_depth): def calc_min_frequency(bit_depth): - # LEDC_DIV_NUM_HSTIMER is 15-bit unsigned integer - # lower 8 bits represent fractional part - max_div_num = ((1 << 16) - 1) / 256.0 + max_div_num = ((2**20) - 1) / 256.0 return 80e6 / (max_div_num * (2**bit_depth)) -def validate_frequency_bit_depth(obj): - frequency = obj[CONF_FREQUENCY] - if CONF_BIT_DEPTH not in obj: - obj = obj.copy() - for bit_depth in range(15, 0, -1): - if calc_min_frequency(bit_depth) <= frequency <= calc_max_frequency(bit_depth): - obj[CONF_BIT_DEPTH] = bit_depth - break - else: - min_freq = min(calc_min_frequency(x) for x in range(1, 16)) - max_freq = max(calc_max_frequency(x) for x in range(1, 16)) - if frequency < min_freq: - raise cv.Invalid("This frequency setting is not possible, please choose a higher " - "frequency (at least {}Hz)".format(int(min_freq))) - if frequency > max_freq: - raise cv.Invalid("This frequency setting is not possible, please choose a lower " - "frequency (at most {}Hz)".format(int(max_freq))) - raise cv.Invalid("Invalid frequency!") - - bit_depth = obj[CONF_BIT_DEPTH] - min_freq = calc_min_frequency(bit_depth) - max_freq = calc_max_frequency(bit_depth) - if frequency > max_freq: - raise cv.Invalid('Maximum frequency for bit depth {} is {}Hz. Please decrease the ' - 'bit_depth.'.format(bit_depth, int(math.floor(max_freq)))) - if frequency < calc_min_frequency(bit_depth): - raise cv.Invalid('Minimum frequency for bit depth {} is {}Hz. Please increase the ' - 'bit_depth.'.format(bit_depth, int(math.ceil(min_freq)))) - return obj +def validate_frequency(value): + value = cv.frequency(value) + min_freq = calc_min_frequency(20) + max_freq = calc_max_frequency(1) + if value < min_freq: + raise cv.Invalid("This frequency setting is not possible, please choose a higher " + "frequency (at least {}Hz)".format(int(min_freq))) + if value > max_freq: + raise cv.Invalid("This frequency setting is not possible, please choose a lower " + "frequency (at most {}Hz)".format(int(max_freq))) + return value ledc_ns = cg.esphome_ns.namespace('ledc') LEDCOutput = ledc_ns.class_('LEDCOutput', output.FloatOutput, cg.Component) +SetFrequencyAction = ledc_ns.class_('SetFrequencyAction', automation.Action) -CONFIG_SCHEMA = cv.All(output.FLOAT_OUTPUT_SCHEMA.extend({ +CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend({ cv.Required(CONF_ID): cv.declare_id(LEDCOutput), cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema, cv.Optional(CONF_FREQUENCY, default='1kHz'): cv.frequency, - cv.Optional(CONF_BIT_DEPTH): cv.int_range(min=1, max=15), cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15), -}).extend(cv.COMPONENT_SCHEMA), validate_frequency_bit_depth) + + cv.Optional(CONF_BIT_DEPTH): cv.invalid("The bit_depth option has been removed in v1.14, the " + "best bit depth is now automatically calculated."), +}).extend(cv.COMPONENT_SCHEMA) def to_code(config): @@ -72,4 +53,15 @@ def to_code(config): if CONF_CHANNEL in config: cg.add(var.set_channel(config[CONF_CHANNEL])) cg.add(var.set_frequency(config[CONF_FREQUENCY])) - cg.add(var.set_bit_depth(config[CONF_BIT_DEPTH])) + + +@automation.register_action('output.ledc.set_frequency', SetFrequencyAction, cv.Schema({ + cv.Required(CONF_ID): cv.use_id(LEDCOutput), + cv.Required(CONF_FREQUENCY): cv.templatable(validate_frequency), +})) +def ledc_set_frequency_to_code(config, action_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, paren) + template_ = yield cg.templatable(config[CONF_FREQUENCY], args, float) + cg.add(var.set_frequency(template_)) + yield var diff --git a/platformio.ini b/platformio.ini index 98f837a571..5094ae3d83 100644 --- a/platformio.ini +++ b/platformio.ini @@ -29,14 +29,6 @@ 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] platform = espressif8266@1.8.0 board = nodemcuv2 @@ -47,3 +39,11 @@ lib_deps = Hash build_flags = ${common.build_flags} src_filter = ${common.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} + diff --git a/tests/test1.yaml b/tests/test1.yaml index b2293969cd..ac199a0a4a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -722,6 +722,12 @@ binary_sensor: - binary_sensor.template.publish: id: garage_door state: OFF + - output.ledc.set_frequency: + id: gpio_19 + frequency: 500.0Hz + - output.ledc.set_frequency: + id: gpio_19 + frequency: !lambda 'return 500.0;' - platform: pn532 uid: 74-10-37-94 name: "PN532 NFC Tag" @@ -789,7 +795,6 @@ output: pin: 19 id: gpio_19 frequency: 1500Hz - bit_depth: 8 channel: 14 max_power: 0.5 - platform: pca9685 From c3aa834d809c5a6a8f60b6c0917a236d61b098d3 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 18 Oct 2019 14:46:09 +0200 Subject: [PATCH 203/222] Fork some base libraries (#758) * Fork some base libraries * Update ESPAsyncWebServer --- .gitlab-ci.yml | 4 --- .travis.yml | 4 --- esphome/components/async_tcp/__init__.py | 4 +-- esphome/components/mqtt/__init__.py | 2 +- esphome/components/neopixelbus/light.py | 2 +- .../components/web_server_base/__init__.py | 4 +-- platformio.ini | 8 ++--- script/.neopixelbus.patch | 35 ------------------- script/ci-custom.py | 1 - script/lint-cpp | 3 -- 10 files changed, 10 insertions(+), 57 deletions(-) delete mode 100644 script/.neopixelbus.patch diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3278827486..4ed0973cdd 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -107,10 +107,6 @@ lint-tidy: <<: *lint script: - pio init --ide atom - - | - if ! patch -R -p0 -s -f --dry-run