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
zcmeFad3;;dnKyoqti`)5$yc%^Tb3=$a&*1Kl4UuYqioJ%CnO|N7M40831EdRkc1F6
zmC_Kl5Ei)mw$t5k8E|9iKTcyj8v|KtH&n}d9;mLZWk)#?a
z%2Qv4^pwgabx3W}RO~x&PS5{*jz~gPs=yBQ>+M~&bWi)gluDYm=jr8z^B44LnY2=3
z|4@qS#)ZqzS~*(vyZi7wj_a<4DMcs^BV#fAOw{3z6x5JRi8=lJ(nv
z|7pjlBo*BvNqWr%yLY-I6Pqtd#)EjSy>R_y+fSX63h+Kbr|`lpS8TrIm8#n$$u?DD
zyS}(+)B272cW=G``BC{@7vYAsQv)bd9#Z2)m+aiLEx7sLaD76O3=eGCcES38?_G<$
zjd$VtzDw5c*{=0U@8S7HNblOZ{*p~qw;!J)v0IzbUx&7DyKLv0AHM&z#BO_4l7iDY
zESToaTYs_P_OBUd{!uCsJ^R+LAAAk(OX7GcqmeYczS)jP_Q*|pDkA|K=!ZQ)HUFoj
zI|Xk@hovfhn-VgfDvS=JpB9*OP}1RcKiiD!h1ugDrE2N=eC)zAHjYsACjH{l)KjOj
zrE7V;@@wFd6g)FgLP{gj(bfNfpCT!eEQPTT<1fOu;P_Qcit%TZiqa{lDV-(xp3dk^
zdY|5n;3+*9X~eUbl;BV4ssSk}mHH?194VIw&M0@vOO{%YR-B2uVg4+P)<&eu@hpa;
z$es3duJC%&TQPp6NCBRQ0MI#jKgKsoDd9}K)q^uCm40+5#^0elrSY7oWpszioRxj9
zNcGurC?%y((@_56O0NkNGR3C9h
zfv{&mna40LT!ujeFV5m_)D!^%v(==NwA>Q5h)E8c|ZOry{PBq{D}&a
zTs^48;wp@`(Ype2V%$<`be|yN1MU-SD7ApHfKnW9P<<&M+AAoX$`SccTEb;I(mj#C
zXbrtD-c_$D2T@1bi;~o9#`6&Os1!wVXVd0dKu;+T37f$Fa$X~_hZ?7=`HHDc=lb&LzFK{q4eTCx)ya3@S-v#+#~3g;3!HFu#kEFVX2irr+$;c
zr3e=Eg!a^ebv%{0t4N*5f$q|tYA5bT_>syHrBe;441opI9(ALdirUhfqBgXryyzV@
zhoI!;SVXz3!$!28>LfwnqO_tVl$sz+<4)9;_Eam{(-VoOruSy^w?vtu2K1i#hXnY6
z|3e;%LtP8OOlfK{etX$E&CMLdr*!}>XxQSUz7HZ7K~Pp){i!hwv2X+
zP8(f0x?}Xk(Z3&kdGy2gE_pBU-hJ==^u1T#d-J_tzIW`{#$zuZ`=?{CzhC%%)%)G=
zkG}uO2bK@oKA8Do*N2yW_^act;|q@e!|`7o|IP9DKKl3sJ5hc@J~92oiW8TeICA1=
zA4?zWKQ?}>jP*lylOSwB}Q{n4L{{&e(}_ZGZ&5G6l|
zlHYjmo%cqMH645L*efX6@P6_8_V>T@{;~Ie|AG9$)DK%foc7^QkCz{JAAeab`J+!x
zl$@vB^U1hFPSBa~*
z?v1+F>;9?k)w+x87S_$Fn^7043)HpLH9J3a9&q03yxFb`$79{_5)Sdirzr0
z|NDO=0ZW?EY{4^egv^b87x?lu9BC`VwjSGFY~_+Pio+=36Xe~|YOxV+jXJQ^<9;iS
zZfuP>@5GVP5$<*22)T5$1Lrev3}Bc%>3st>yvNqyh_UxRBz3k9M?1D|oL`LNcd(%h(r?~JIq!dhbL8>4fLkt<=9*}zYa&F
zI}R9W0GH#a`|*Fk`SUm;Ep3_?aQuzfG=GQVdt%d|tsi|XHqDQ5L>*6nc4*KiCjg5R
zC`0oWj?f2Apstz`9MShDrsEuNJh2qp3Y?>TCjg@pm*M;)9Cu+m0(1k6PM{tqeui_v
z^91?<#YqLgyN@;4^fd$p-}xpL0EP_j
zp_=^1kdH)L{!e|FcdtOJ(;Etlic3oKZhrM?nN)79Fqtitvei~qZFkhvI_q5Z4emxy
zllQCN`tNB&~{ClPAFsAO1?vWmlhNY*a7o?Y@*QB?k
zUr5KKlhS9>pO}UfGc&WZ2IgaaHl597OW4_LJG+}5W+yZinsu6|G_PnrEzlHH7ECX=
zrQj{CM%$(x)IO$tQ&*^)tGh;bSoaIvC;C!-r#`G-qu;APq<_btHB2|`HQa4T8IBbe
z7TOEj3O5(-EPSN!4@C_{^NY3=T~~BR(VInI6gL%bC_Y&HVo6KMXzAS2r^_xXd%av!
zzN-9Cd8+&~qt@7LJlA-W@hRhp3Twsr71vd~Tyfk~Z1S5{nRc2UFuh`~F)uRjH6Jh!
zn^Weu&7WDEmTt>t%R`pWDle)$Q29dT+j6noFYl9|lHZj-x7w{e)@auqbZqFjuj8eTQGc<&)4#-jssAbe$v{=WA6ORH5;zbz9C$NuqO-Dd
zZs&&1`#XOY)C8M?bA#sxuL<52d?fgJS9#aeu0>rtyPoL!NvJ&385#&(8afbqJ@o7D
zif(`R`Q7`wpYHyA+SF<5rd>L1|FlP@y*^!<-ZFjj^e3jjHT}yO%`>*n7@G0ojAJuP
zXKtLid*=R`ch7um=1*q6J!|T$MYA@}IymddthalLd;C4u^c?DWwdeEMO|v)8etPz=
z!<
zySMkQ-luwhy->f{
z@?~E`-~7JKeK+;}q;Ispw7;$Y?EWqN@%}^oBmKW!>RdW=>2*tATKeU(-ep6}-aM=P
ztms(}o%QSGJ#j+I#R-{(EeYXDW&1b*5vUFwJ%Knw-uY7D(@v6a9uSTjO
z=SS{~yd3#$bVGDsG!^}Pb@A#&tM6I;#OmW~O4oF**}Ue^nwQsnv9@XL;MzOZKC$+-
zb<#TXy2f>vt{YnS%(@fj*w0yZ&ZXzvf6m+I>d)P9?tSOJdhVC!b)C2UynD`j{ruAN
zr=LG~{v+p)d`M&h7r~L)%~9{>2X8j+r~Q?|5Lx>z7tt
zy5!RPF8$!LhRX&od*HI?FMIj2cP~3}S!QSB&aRz{cCOpGbLYODckg_0=dqn%?rPlC
zzia2NgS(#C_0q0)c73v2ySrw0=kA@mpW6M-<(kV|F5iCnBbR@$$G&ILp20nj?Kysh
z|B8#Q7{21my)*aj-g{{8D|?J?PJ#-kC(@n#1F<_{d(coyT88t>lb}}|JR@X`n%WZ
zud`pb`?}Y^q5Z~^Zyfx_>(>`v@4tTY^$%Tt?3?y)F8}7xH-EOzx372K_I(fSduiV%
zH?-Wa=!WZVc;<%B_qXlezW>GjpWj$>WBA7NZ`^m|{Wqp=JaJR>rad>^f78g#g*Q*V
z`MR5r-2CY+b8p#m%Llg>-`aEQ;H|^AetN)nVB>-N4!nJv_O`a$&c1E%wwrEy=b+}G
z|6uRI4F~T!`24}Q4`yz!xxMH1!P^ht{?hHo?l9lcd&l4%!*?9Nv+&OTJ1@QSkvreL
zYx-T+-*w+z&)jt)QIqIT>`fdx2_Y~eU_nss7d~k2oz5Vyb@4f%tSMNQ3U-^AK_ieuK
zj{DyJPUm;}zq9u{4}9mf@BHEZ=>0#tKl5GxcQ=0bk?+2BsQl2@Lq9w8#rKwf@22m)
z`n}8pJrC@C;GqZJebD^iss|4~_{@VN5B}+)s)v?6bkRfCJ@m*!BM*J~@P>yUc=*kS
zGmp%FWa}e`9{Jg$_D5Gey5-Sp9vyo0sYieI=qHakAM1YX?8mk|cGqLiJof9y^^Z3`
z-u?Jhk3aVKm&4}{-!%N%@aM_qI`N_FYZhUgz
zlSiKX(^D@!b^QAc-|zqaecylgaP#50hp#&P)Zur3;QT?)53c{gLr+&e-Tm~|r(b;f
z%OCdsaL*6#_~Dz+6hCwQGp|3Zf41k@OP_t_*)NYwKeFY>V@JMxZqIWsKKIGW{}!`z@+w>PaCw-zxscm*S$OQ=>iQ-
zr5_Z~Z5`cy+Ms7&&U_jo&A<8srhv&CFu6_pFWZ=c5Jr<8g6@`Uydi7xxs~vyW
z02`=9vOyX2cXc*3=qt_Hw?hHhEn|D(mQ9;(*|d^plR}2R2rf6>!W=v=OI#rfNCDYG
z*jTnM(5uwC7#xgOV`OYB9gRj5CCn0;7}CRn!%|3JBGtjVNa;~?DlibNV{&~k6zuGx
z&2Oz`P9_@y)Fc)O`@&)0HLaO=Yq+Dbrm`da<5krS)hCs382>9;ZMN2{0xL?4<&~A?
z#*!6*w8;*|j_RkvirS0Ts;$&#Ta9u!%>K<>T52w}Ebs1K-d)Js%w*gsHD8Py-OK+y
z3qUREA4dICpbkbZ!%JOMSLD&f^Xc+?10u^{XDDbhc=d9n)!!BDZ1T$P%52hjskyAo
z{0F|jQC_}%`=RYiZ+GsjUwzf;C0#yVtf-HZ*D8tU+m{baW4Ajmn-c67ls?@vd8=8Wq&aME9
zmllPK%F3`Sbt%d&e!_3z>NP;tlo>HsSDTq^Eh;T7iqg(n5-yo|HDouPk|;65Ui1a*
zVl0Vz5^aU9%21_x)QjjQi$tTRq^K{-!ehxKi)MyFi&a>cNDgWhumspb+i5ip$K#Pm
zWLVUV7aCSZ6vd~+sj~crk7}WT%`VQ4vN+Ck$kS$Un`}%zawK(x#p7`%#VJh?X_9Ih
zOUUH*hW>~YNAUQR6i1T0aioQPwHzafx5jG-*<^PCat(S6-e7`XCZH1ap^W|+
z1Kew=9yUV|nA;h$(O9zpnJW!m8gXvf8-imj(!0q~XY31R50OYb9EpZE_4=pOIqHLb
zOc_7LeY8~rn=YsoXq1uT9YAkDi`p1E0Ye$&XciOnsgmtz>st*7LL)5X>uPEkbV@a`*Ra91V
zo?hM$t}iy0mt0?BG`^8nGSP=Hr@7VeOu`gmVIWJnCI$Q
z1C@VF#x{egUgk87B_c{BqQs~GL6f3PLFUy=ru&gdO1%$SRUOHsebpvL>{qC0n7NG3=9|vqWvKAX@?#Gl
zs0{`SAtCU8T!y)!aZ~Ti){AGhe%iRn%o*~xT4(ldYRvqs@l!fsaaE?#fM~~9oF#a@
z3Ck2l90mQFj6mubVZIEkpm?g5O?{b_n+SWx%M62IRPnNs!}u|uT0|bbF97G_aud-%
zRfv2ZTW)SaD0Lk_k3zV@G=^saX=_56Nb8K^Bxd$7I4TeFxn(T$;nRGJS1Qu6PAXuGE+9sb5yoNVp99PCTrqmOi0WCpZ+N|7I
z;ISDX=;g^xAXmd6cbWg6)8z_QUVivb=Bg@lX4qUsTu4!pRCp)TY;9avZ3Z0zHDZVX
z&|%!gFzyn(tU7R4&}a|%7fEnkVzhTo9_)nS7U-I+WbVwlmo?SaHq|aCIqZ}~f*T9_
zKtU4~R=eF=@d}>RHm&B*kb+4?rD9BE$At#`f#i=gB1WM75?dBs?MxfRp;J9=HP-oAVgI^HKKBJlvtYh_=K*TWD;M2ZPfV8$)NN
zpJfmRNnp&S(NG%5i%8}^`s^tQnioloG*a+WdC5DF7)^-#ss%TeEC7^&MDvhJOH_>n
z0%VBlnu;%78eiIX)oN%53!n%b+RhS=4UVH592-Dtn${`awW&4qG_&LW9XsytuV3kM
zo$WfCV417CWT`jR$EhMzYf%@$mjz!lLK_>W_e2we5av7+@EdwTa%3dx8;KH=RQYw9
zj~GS4$VpC+((=nNNEN`1dc?qt>tD96POpI^wWjQVGLQsE5-;_7o}3W)U4z};V1LN(
zuWRyL)9d&5`gKhnr{B*SbG2X!lGqz=@*hB|&MuE0srqS--(^=wo*B6zs
zbY`DpzLw`UeZdF6D=q3neu&gyH$x870sACNAY}5o8!HXLSR@vUFjp4~$1=mQD|b7d
z0Nx~!7MdE;W>Y4jgM-k_D2dPosBS3^J13XID|^8+ygF#SK`8JfNqVsh
zTA9?k2NGqAXG*p0oLFSjCKj2|K4+EHRT5;KL6%(CIu`40A>FjxUdm)Q-X~E;(4%#p*SnrW8f`g%A8fJ=Cse4(H;zr
z@fQAF`NjJ$MsCYCFDqZU$Nq#>ZHp?Os`4El1zwB*vZzePMV5@pV`F11nnS&KDuwRE
zKX7oO!ke=d6o3v@OH%}G^~#X((T`2;d`*eemVB&dL8Xr?Lh&=GNCQyN@h~p5BPLDM
zadlFI)WP0j|BRk4l`5gHLH+P>8bek9U7(!>Y;LdH;I;(}7I%m(zwXF2SN|Y$-%6j!
zJ>adpq|*DN3;cmi{>K^|&owx%+t&7n&Q7WU4ozh^z&{
z60)%r$?lOzCLW2<88md3;t;9^BRM6Jhz-L)CKita-?e#kzY{SDkQF_Z0sm|UNP%m*
z8;lIh&>E`55dtt&;WmgyfFe==oM7}$jm;`U?>2z@7%&n~(w$*KvKQ+#T8*Yi
zQ&6BSC}`H`4O(0jX*GHr4EpSaL95lQt(2Pz?%KF87VGTqzo=gUzj@K1(P=e`R;w#O
z0!3>n)0XKq8m48sYNL9tDb<$*d-cXrNAE`PqA2H4;7*g;=am*pH%e0I43!N1`8GrS
zKds4bKS4-g`i5HOMwT2HAPo?v-3F<9(tpeYlSbpq
zg2_;!0ni9!g#6l3w073y_MV|z{$$XU=>i^&2AyA6fTB@8AYsG-
zEj6KzIt}5+;}anH>jGBLZnS6^V+8<8Q8|(q5?D~_@?tbfQ>N8v$|k~P60ZF1V}ZEZ
zX+qS*v7mPHna097G!|?^=E7L$?~KL%vaxX6X#!eqtjn;pT`t|6H`Y5L1)OQL|JPvt
z8DZlrA%sFdX$x>r>BQeY00L(MBJe^pfu#IxBjDM|z|M__PHEAf9S;RWBF-=#f@i>=
zFAW;wY2!hwW15o5BVt0E&p05o1x$?N+>m%~aw|xt7k-8m=lh7AVo>t3%BgGv?;%c9
zP^njNKd5q{tk)o!iL9o;;MB-UzFAm=2xwP%8qsm;E@V%>`tbvL!QH)V!}dFSEsb9bLx-cmMq)!eeb=O&;$ye0bt
z^gWaZVFh*PHVPGU^CiDzlig4rgQ4Pg0&gMQ@isgH6UFt)kliDb-p(Qugua|#kMtQU
zm-xK0XOiZ@%{cYJdKQ5ysA_N|%#tlcf%zhuTyXQfT-8YeVlR_HM8&l+=rg1{P&*{}
zO}rr+SqjFx^{6Yc-Zdwa9)=r-kE;^b(10s!_4$N~D8{f$BSCi5ArIKUNyEh-!6G}9
zL69NE1fi>nds%T(BO#}->t?tB@sFx008LmCqdCr>_3BTUU&p<4cGZP9uVr^T1t8W8GOpu3bh2=&X*GGcZf{@iFLotCH#~acc++DMo
z9{LarsColSZ(TvYmKC2kJ?*n2`WojH{n8q}qMvfG-Qx_*R%Xwh!_IzaZ=q7SKE59N
z%xzA3{G;~9y1?vf@J^tCzw~f>I}~fe*_@9C))N|Zr|B=;8fva4+@WC$1NCWY3wlS@
ziKw?pZ$TU@Q5Qi!h`y*~kNiE8)n+1mEa?b399{cd3K2i0+&q?}C7^XFK|SK}K2Gl}
zPv(=6n~A{D_#?k^3L}WO#VwzvA*E6&8rx!YK#iC*Bu>xc4Dd)z9AmL?cyccJyoYcI
zK4kJll6H6+C6!PmVl`@|U^w%^{SL#3O2LH>azfiEw=?84As;D`fbNu+3E2mP{YC@Z
z3a~mr^wF)$4w!lt-sKt};1auew-uwB<$SS(E6~RBAW;ov0e#c6%_AH$u@-|~b80tZUb#xTYg;edhL+#RSZSw|K4xdRj(@4;w>
zZ7)c@L1cus0-V#ex=p@@)fB2K%)W{eg&zh#GGgP$&#_CUGtqPkb}gT8I1P`00)3VD
zBUlvfO}faC@fj2^jJl>!*BaPNthP{>&B`q_eJwy+i8_SB2FCyqQ68Is^k*vW0WlnF&L
zTNp|pyfJO1u4=tmAJpqFW(^nXabnh2yGs2mY@BP<8LPydjT=RpDxtk}L+e-Gg2OG&~u?LMYv_!?&qpB;L{E
z>|x1{9nU@N$k<3Y!o43_vRhUxE-&#_5?s#6rN!k%V2JTYS>+Q`xLhTg!!saU18;~yTRHZQgAAz}rkMinjPRAvd)?l(UVDz8#E0|EKiHm8A=P$mt
zxu&MM=E|J0i{dg8t}3Uq%JoY;scBxFZ}ZBa8X!y%8Blg^Jh
zqa_6J3sHdMN08$P#0xhpgA>O}d;9tAMP-f6RynL7q=MQVwLUv(v&V`@#?kekn2LE6+P2VN~fk*
z^AYxIZlAW_!)@8(d^10Pk?%7v=RL?z%EtKBW^wPd^OyM@vA>6(i+x5MXw0g7g1k(?
zbBsRQIEH7r1xwN|ub1H}k12z~S_fv94d9`?MBG^!$(QI!h4C>0)Rb-5U{WUeoMim6
zqI3kVCb_5#T18~6sFca%cY@oU`Z8Qvlu6;ak^-urjw0S71&yH$G(BMOlFJd)1xyJ#
z9NbO51O+MA$7Mv4M~^*IFF4w@I_eq^|)DX0MoEksO^l@8}w
zS2UTe_incL|Es|-3xpWB)HH#zioBT)CJ3oRaS@uqF)pq2V)@5o;mBN0shOC3u7gSEvjotuI{N@-aE@|5=Hd<#<6w4u~
zF9`HFZgRgN*Qi8J0<$^kPUpXq!)Gmd&OHzdLX#?y%nU1erGpP-(h7s=I#)8mDInFd
zpMv+7P(IvOBQ#a72Yyua$}+aArn$6m#8lnQHhktE0>yqZb4$$>z1{S??t=gInV-i8
z{a48l6eAFX9F>+DK!@j|hBm@a0}zzQT-XculmmbBuj^Uqpyb)8Gz)Zu1+cahd@wIj~zs1@#=??DLFHkd0r
zsy(I|?xwoBwyw6bY@V76rZ1V+s;jK4yL!zmf1P(q@1|e6wMFncxQqINcJORT3PTP`
zVYJ)8%i7Uj5P<@ywE;duun;hR;W0OVmB;+*!a}#p*J9M`jV(Uct~JZo+{0byWcl&N
zB^qb{R^L1Q&i;rH4Qpyk+b)_QMY8rKtge6m;NUrZ&i;QFf}_10f}?SQNQruArE@R_
zABOLqbT$XkDbgCjZHViuwwpkcAKEYw$<`%K5D|HGad~<1UfQ`+3!L5ecRQ!ITmvGq
zt}c>Hrpt@}nI8Rfv5_8jJ7=}DIO{;6NY4;)6I@4RkhVOqu7DeHDT4&3Z1m69AtsHh
zWh^#=fmm(gJ5(F_VCFaIoTxYGfZH@q$5kSp|4xkCU#~m!tM9F6ErHocN4^<#KoEA=bY_v
zT$dHp7jiOGC}!p21t-EXbS(U`(Qkr&7oZ;!z~eZVRe?E;S#G07MKqF>MILafJGfi`
zN)_AEqbNNsCwp32dKA{aVgCG%%*l@V^EWJT3^-D_hlhNB7j7+}$BN6PWNjrf^k1@R
z8Nk!Wqo&AJL{djN1N$aRo%MPW%&H&!l@d9xY%U@p5$-uR%_Xgw(MY7J9X>wLBJ_V4
zIFteoQFN@zfdtM|fHUQa{A>mcN(@UHp^SqQTwY11;W~~)B61`uV;_a#5u}DAS6E?D
zC2X?aqdr0c!dZ%O5#<>m{RA=Uvor@)o-o=(JZDzoJFPxPEq2Q}#TBNKUBwj@#h$jt
z?{g}s)``ik(u#^wS4Bx5CF(0Sx)4}sJu@FZ5e^XIhfXh3
z@`fT)Dl28Pd2VyFwYD}wng&A%d2WooD0f<{)m3wwVjgSVSx~bJXw`-+%8?*WZq^`ax8Em)JJX)PQ!v26Ng~sMbxP$*aQc%j6qP~w~;snI|FXeMhv*nSat67!b(D^jbE}
z<#JJbRQ*JQkw~_+P(4>t#sx8z0!xs^V4ud+fILM-j-8UygM+6VF*RsU1oR|{Tt#$`
ztT&*45tSAU2q%KdD3~0LUTjk$Ly?RWAu}Z};pp<^ZN{zB_rj8FP7{zYP?0C`TL<#}
zCC<#CuO4_O5b~n;LZVW04tW`{9C0vua5-=zxd2opQQOhr+R7ewOwU}6Qp)SukFC2K
zq8fu{J2RQ8D`uEd&7}=>A7svPOou0G#8O(HIayu0+jUkeW813WW+Am2icDAZ-5kA08RRP-=$HB1_ClS_||GS^?IFC%NNnzgp)
z#-Gmo?hZ$@-C?P8uqjuq=~?^iPjB4u-S;dGd$YrVvdK0>cA^qs2d9%kC~9(mCE~s~
z{t@|$Bd3L7j7yS-MHDfl0AX7^P4kQ)b~Nhaq)y=C7!N0nrYFf-Ia!^eZztF*RehX^
zye{EEve|4-s)4eEXF_)+ogcND948y%wnLD9G$}t|_9R8OMkfT#T6mqKfNxk5?W9#(
zV4t(pd+ROe5wzBmAI(FQrQUD5gf&&3lD@ucNpuNQibVs9YZ1&Z8H-uMni<`tCDEVHgVhoeip>p&|xm1-W^I
zB7`TB1fXHiND<-(g!F{svRe1$nQg9MhH%1fpvXhayz%>ul@OqM{VXL9DSg%>)s5BF
zjUVy7Df5M#P@fz8>N}z@cswn-obVLX1u!6i#Pk)q#0=I0N@-&W--z#iK3Oa~mZ=`QDB}1rUg$dM+_Nc(w>4)m$!*Em)B^WuHuxZ
zxX@8&ao5z8=yiofMyq4$G~bkhV5h#PzNUF*h%ud^ytrnDsjRBR?JqG`7@4WWu+!62
z*i>dH`d^Kug$`$(v)S8dsxn&}XNDSr3wpec`bEWs2$9k)mW`DKB~u%XjSUVftITrO2A45}9-qYHfs5i8S%OR60#FcVJlI0SB1hC*%U6oe9WxF<>OO
zJ^|xMpCjiVML!_qE+98j$N|Owk*fno7HjG0X^BQ1UauoMIGoAg5Y66VvDs^8Pk|gb
zCGPW0#ept!ctmlhGWFEoXse8H9q4b*i-FH4dHZQ}K(ppV(ctZ20YAI3KecRXPxo4r
z-@0pYggKX{mNw7nte77qyo*VQ>Sr==TJn(FoqJ!$pri9KBWMxX4!E8?PGw^wmMjnR
zeFgu*pjF9~YGo#b%;#H!BO?=-);x-P5dLA|=Rq7CpCFgMXoq>}{fPuzL|;GWlGS_;
zijbWs9kp0ZX=FBsh68#LPhkB5;J;72w9;Xl_wLJhXEpboRZ
zK}AzwTuLH^Bpk+Pq`5Mx9OlMX(A@Yq$))*=B>yFWy*#o9{y8HU`
zWy>blrk2XrxQ!$pYib`GYhOpWhc)!rZ$Lu`qrkCXa|F>u@D4PHlEpi1e$(3So~g@H
z{j))p=T~&jX~swjp^iDZEgQ#N;4TP+0+Z<(gdw)7^^)dGTC4VK%V`MLwzTYG*38K;
z%elkB`2M(icRr5kAq}`tFYb7NBk(JUCUhrXbnrCF$&@H~Y)qw}LcGeOpW!TT$&(W?
zdkDA{2>p3EaSj&~&DD8g;%-QZKQFCNrNoL-H%W;qoG7-Yh%6A8IR>fABnM^z+A#a%
zjAF0^fkcxvVk{D|hQk0yo=O$1HtmW=;8tVB@Q7ENeRk6rfpg4crxBF1xtVB|63@Gw
z)v|h+A877q0Nyaw`bGuO6`qle%u;nqsl;B{ANw@UFtGOR-I
zUVB5Z42NFIo9S~jQn6_sP4roW89z|c#%K9Bw)^cf;}>9_&)JyolPl*Je{DIYjdL&H
z2ofSos=>h5~dBDm|i$WLB|5zTta?_%+
zyqElWMs7g~Vcg4LZJ0R#sttJ+lUabuAmm{OTkg0LMhxHZwXx*83w)P(c}oNxi!Co_byW-7|NJP)^BhKgNfu57ca7&3+eI1e
ziXkh&Isu(EhI$gO5}GuRQpnOiGGUR65g1CdP7%R!L8-}9dLQlF(KP1u!0xJ;N;5wq
zBOY5=YNCh(s-v2(j%=T^8JJZd&q6q!=NR#){5&Ivb{`r&Ekib*ALJ6^f8fp&>$WVf
zUMx8|gTM9t9Goa(6#iL0r#Rm~3s3Dt_iR4kI*gDMTv(f~sIs!bY;4|;SJ~eiWfF3!mVpq)UdRQrnww{xd+uI&18Kh-
z{({f+324mQY`iA)It!!;E5)k5ydNaAX`2p7L>^PSyeQU}m@15gpxT<-|
zuQRVYJIH`5!pPU
zj9>BDi7w1c9qeODejzMEW`W)Z@t|+;s=T789Lq33>u;9BE6n?*W2K_w9bYf3v7_x7aFh3m19eG
zdLQwACM{~|4sTg=^|lpVOIEL3vCZ6Fyre7EUBCM3H7lU8I?pe()F1%vpRBwHa
z9Shii@2YV)Wf(QpBu`L8u^=9^pwNeagfMe)cL_!MLArQM`9Ga5~umqnUfoDS2}Ua(V{ZXF)5-HH*F#
z$FGP)hMeMKzLOLcnIDhL6&c9+h(ZKy$37)yFo|f51}|aQr4*
z&M)^rEcGi(`>JvXzw_Hx%_9N46uJOE1#xnHu5Dc*9a>8P*T7I{8{RI_R(8(M>sI&p
zKXcG*)Ygz_jNZdmwzTLfTqUb}oDNr=2e)tf`4m5}cDjS+rF!%N6rt9QK5wnd;p|ym
z;;PWm&Hp>=^xx4JIhvotb8d?>V0{7_%{Cmpv>FF^RfM!w61t%;MF?}T=F>16&WH=H
zBMO^{2IH}wVIFgoA^v6Ku=we^66u0$(J53FF5kJ75Vb4Cx=3kMyg4x*UC|gC|
z(VUD*^G6^LFbeo3#QZMh^xu=0#P`WI!BikGiHFr>m
zxLtlS?bjDzjB+&u5pM_>ii5b(iEk(hvC(4x1_WH_{B(D0h(F^`8EP>!NEQGJt?fZNO#wD`!4+4Wmt3}oq5;{vSKzp3S@OKhuaZg7
zYBkn^{LOqOKiNjetb;fr()5T-QafmavyhZ%^&DP%p5l0?(P}gi5rr9?4udq0g}|8g
z@+wwqvC()Ftq@geHC4mrDNe~2k`34x;i0yuPkzjXgH{$elefV3XX`j25@f~X%84+K
z_;}@@N2bnPKfspYBeOpNaS1+Nz6#r)Et_oX4NcyFlXp~a_%5PfNfqZ
zJFOgCSg?n{mHA`Mit>$j7P|Bc*!JS${YCcTQ&Ms94MmRPPG{>CpY3UPvZEy>Mp?dx
z&krxLVx6B7eu_8Q{tITTzXfS;C@w}UroDK7FcPSl?PBj_7LH0Og_0KGu1
zAz~E_A@~D$ta-MYBp||!<8EVzYO|G^|MUH)r2Y3ZCaRV>#g7NaA9wLO%p9=nXVU)f
zW=@GJFvgFL>_ZMqRX!CoC2RfTel@|R$7jl0La>F&_)8KNk)@AaODoCIFE<>eIOiF;
z`6T4sKEu$r2;n9`27enipfT3CiswRyFpHHh7SXSk?e*@7Ex$s0vnEJM!@tj5@{U&
zpNThAPel{=jQI@AuYzJv2Jif6$-@89(LGHbP~(c9YA>TPsYTg&wN7JWtQteZgjqJ?E;
zg)!PW+v<*XpU=8<+M@DOtevLQ6_*%G%Zs$SZq2NvE>gE$<%JPS8YwJy*_?IIh|p$H
z|My_&#QYkIRRY2r3gGI8i3a%5Tgn`Z91B-|QQxYtH+KH2wdGp&UTNm|634;|Sy&Ku-(z@u!k`Vtfr}^T9`^?
z@}8Tk=ako2YVcn^r`p_7?x=J)Dr?GGhQX#GHIvLteU|1l4h|c2jUo({GsGNusrL*F
zvs4UCEKPkvG>ps8buF&0mY%*-(sr2qr|FZT7x)a+QcOcFbMc|o(=zJ<28(MyNE7jN
zKXegVccPX2w$R1UY;wYIJQ)vN)zQstd^T23q{vpVB~`k8?C{moI;`IIjR!XJXjK+*
z*UCP~t`x!wW$JeB&t<8r)=s~m#$!9Ht#{+b-u7iYK6UL{_Y9M#x`V>vMD!{&s1gJN
z*&Aq#CqVxZQAU&#stXq~V4D$;q5i-<=#4;OU^VC$&Vy{gg}}5xuOIMmVJ%M_aKYk#
z>{zg%gB4TA-6%og=uv{)D1}~SKBv1>9=_%k}#(_mSpsnfqmIgvHqCSb({khzH@3AQ1H6Kwxq_KCJ#@RpdC(
z2=Yf5^-rPxxxC2Ukw3pHgmpk>OY=x#atCFM4$=Whrle^iq(PAY#oa47Cwn1u+#pFa
zw5jKo>~)Oll0;-Z*+w!W;sh6vMrljKY!b(o;=YtLp9~hKz(u%hFi|H>Z^65Q$$WSR
zCrROOfeJ=;zW;n%7Q(A^7QVR%xC!Tx0Y{R($h=JFMEdEP+C|rNRL|*J-BcDq*Yun=
zMwVx42ii1DJN`?H+VgYxTeJ5`9v>FoB(#V(Yutx~R-(KwkWEIEmg!bB`vqNFu}}
zN`kvUN^*(f3{UV+UQZk(!vxsV+E4=i+~Pv`Kmn3On}~)Gd4|~k3%L4CsaAf|1pqp!XXCO3Fx6Qg?aE2bNYpE3Km=HZHl}xxm8@H`cj<&
z*(3Uiq+*qv((oZXq`Jh>J?I=1k6MY}UxsT`Cc%U7hG0N2VW^D`$9$MkooJ(bl%9@M
zN4kfK_&7x;EaRO^mC4aTl?npBNfp0%g@FWsBogB`{z3a_&;QXVe)X5~E)|B<9$rsW
zlg2sK6M{Y;Hw1BdPWPxC1ZO%|@A0w2kwBpz!J7;z{6ZiCmPi(D5tUUb7PTFB#0Z+u
z4B~_MKQm9Kd8-spLJ}#Tg^Keq&kI2%9G+NB7CLzfU=}cn(*ViM`ONWjJ_nX~H|3*l
zJg;m%adAXmR3jC0zB(_OpX$M;u1K1f7YsMt9GX%|1R#|Bvu(_)6V;996-TfcDn)tG
zadO4bPVzU!*_AxDn8%WG3p*rmteGfm?IfDg8njR$nXBB4H4wt2XLPrshDc
ziu!QpT?j;6-qJWztIHg9T(RuRyC@%MT;qJX9HS3o8jY)Gs_CmM*Y4Psj;;n%?Fzu4|8q%>#n@U?ZFE6$i?Lq=*?pp(J
zms4^0jwXAIy@+Tgc)-MZh1|~O)xp^la%#4-ZpSoFd$6x&AyW#Tn_jLgWCt^^I=Wqb
z!G^Ad-!+y!-&fIfCGsLY0~ta#%*L5mXvI1UU$KtjOy1ZWkbS$R+V)jV-Noe#>4s^U
zbQm)#N#6=jk}Julj|dnsvN&A>Fg$R%;}!w|(pd^YBPE)~C;}Q15rZfg8Mbo5|DVW{
zC>42wkaaFktRsLJDMkS_JX7LTDJ&I8nIa+qI1~pCl>k~a*08}*!M-IZ9rTX*d?QPi
z;2-(vp+5~k_CA!&FFRL@M!h;x`D`0_+jAZ-VVX?v2wPAe9QSh*BanFfAK?`!3jaZV
zi&@}QHP>wAdtNT-!YQ;_&<7dv5wxZJwh-0hkO)DapmS_E(s?ky?DSyR>kT)VWN5@;
zpG=%mG%{+fY{7pxY>lR@QERakN;t)TiuG+_j)5K~^kuG-K@@Y}i8
zb}@PP?nDxXX>RYVrlnruIL)pFk&6XVV{~dMj=YhK&H5(lEN~@!T!0_d=o)pLGtZJ4
zWxpu9pAYJ9@cj?+qIhneQeVG*e7IZe|1u$(M~&H}IB?Q@pzG8_<^@yC4{{UXDglzo
zDIWHuA`4hk+@`DyTVpYJJ>%Ia3If_7o3wI60briMxcRS-Lk?$o2(3XTg$(sD$O|&T
zXY>=9cy@!D}ba`+=;>^1SO9`K&k7+
z7+354*T(V3Ft#YZljfF@{hufcnI~fio{MvxC_OY3kB=yvrh*c&FlZRj)OwJgdUPAj
zmzOAh6}(sjdKKq3o;rNPSfaH}VXN~tv|-H!V1^sB%^{NTWe2gmBffe87cIr?Ki;5QOFeRD?f;}jB_`bGhtg%`|wagg4IIynsL7tO%z!M|TIsZ1_u
z=e-)`FF`NOFEb1ns
ze#TN>S8fiw$b`rqhb%YMWDwTF$|cn^7T4?R3_4*xTJ#Y)YM3NFDg5UNKfw0i^+p9@CJ4D~ZSTVwQ?BzhjgJ&9h`%KlDIR(Q`C(M(bKy>W&`mcv&^h
zD>BUdG?SbQViAo5*umaO-zUrR1;j+rbPy~1&Xno1TQXz8zPVH7{?_Shn6F@#uVr>$
zuw${#J9V+>BbC0J`FuW6CyG#k>w>D5gWCZ0cZuKaY&8^6AYuWBR}vlW4tQ$difzjK
zMu`TQi@ND(y%vf9_ZmE>*D-yob6bm(nJ3jWajSD{i*w1u>gMv)Q>>_mk2qWC^h{YS
z91X`Nw;mDZvEhk15*-zOB2k4U;1=2N9RT=Y5B1lZnNKNr
zNBL8*Z&83XOqt$VSKnlJ&F`qQxxDRwx#UydJaWwacK~g
zF8_Ue1B!n&1;UJ)32g9b=&^Yj0YQZ9C-Nel2^J02_n7y6l*u_KW^~RFi)@AS^G{eU
zHk-wogsG#tVcn#Ha&<1@F^`~&ppIZM0a_RCv=S%;-51TRZApeu@LK5Y*;(VXqA)dp
z;zLCI0oA{f^j-_ZNInZY{|Eqy@M&65A7mMvKwf{7vg53tHOlN3dlTdp0&pS$P6g3V
zxoQ3K?aJz&me~p>?oq4?=JhHJwL%9_^rtA3=uEvdMQY=`W^w_BYz1H%hyO6BUdnV^
ztm2<7PT@6S@jaDan;jjmP-;S{39312hYtx{F6$!8em8vq`p>fLzFo;@T9%Hd0(X(Ihaqaj=nE5h4=8rwFIWxWnP)
z8}=pWgv^!*B_ho!EX!dbJx|J$bi`bA2<&=U8^l-Vn&gHT1YuFWkT9rgAeAFu^8bf1
z254-O{z-lbtQSI72ro7+H{eJs(qcwE&iFP_*}&`_TD2h_i^cgKhRHO-!RQCji$wqx
zOwWO%bp$jc9EG(fGs1lYToV!chz~W0XMNNZ^Gfr)f!l&zc^+F<>feHZ&KvRdD2>>
zWV4|ej%q*V`}by`SU{6J;HoD){~W?PywX^FJA2HdB^>=}Vv^5FJvvPLaQ12_N4H>0
zqTAvh?v){tO1(q(^CRTQPV#rKldJ_50NsX&>b)Pa&~e=12LY#~bTeDbZl*NQe2|DO
zMiS?S$N65Qc~1H!z6JA5N<(&Ho8dX0!Gj_jD(5+r6QSp{tPRTHFFz-;puUKsFXE8y
zB?h;GC?=J8tDh3|LqS?fCreS7`W>Yk0plW}BhZ{En<0);sP0b4<8Sa2_*xb8F^wyA
zopE*U7HJGVP&zC)=kW$Ye@mxGkzutbzOrBB=tex8kqom{+e%CO5@+K?CF{K4a=PT^0(7*k^@Oz6|@odOrqp8YU(*>KpY8{e4(#R
zDH*h77<33E8(LWy@8#a5(df>_w-<2Ek|m&%xQqCv?htqk=qA>C%kh~QUmJlrPoB&m
zeBW-en@-XJJC2vRdV)ITco)7pn$Nu4lergyp5b2@QxrK51Bh;h!4p!v-2tpS3fe)G
zI7|PzcF&~_OgcpVSydL20H5bY<_Ru^1xV>7FE*lB`GlWpOaumH#+P65%UvXWk$7Tc
zgo0ZpKN9KSbc1-vVH&}hO0thp!%}YFfPB(bfKH)kCYtQw9vfP{n$gD$Q?pkOv5z00
zGm!ZWeP4AG+g3O*hvyMP9u%8Kbdq=s6gBG8$$W-wAH6rqKaM&~@-$pqaN)=(zR8~9
zcUaVpFio0UC4;_De!_Z-{17`uWW4gUJR_-<+-A9()TSiuc)s{pDV!ASNrX=gk)p>2
ztu9_y(XS+*{#VQa(V`4D@cUQL6=jL&5^4H^vysCFoC1;$CYdlET?k=_R$Zkb^)5*&qz}lY@DWHjsu0qMACmYmBcFft
z7W@r_h4eUe?7%m2rg-)V5$i&`o45`k#l;idrx;BmWGNUrRonjfsNp6C8bXjlCdhur
z`7#QXYi46T4*sDOKzAL9APBmLKrU9n{3agGUc+pB^)fpNhN213;Q%`wU`I4$4E%ol
zn|~Jd%V1XFbjYo>qgG{dO(6E;TPOU3)@3yBC~#qez1nQ5Zm+P}Dt=gDwN}7)K4R+v8t+ZzO5qj22T^H00jbEW#HQw*F!m6$G;Xso!MNW%mcFloU+@7UA@3#QVBb0%d^#?%J_jzXX0ykFIkPW$Rk
zR_hYq+ac&B@_-5MM5rX}18@?sB;ZMQAjwQIv7qY|nBex(WQc%GOmG~XBM-}SXvczl
z5@g#$oqPBCAM@|!xvb_c8afQUnioZ{SGI5EhA@lJGw`2po*1h!l|ln{YoL
z&m0(gAwe^;(*Ae|yXtid$>je)Rr!1*%#tFlM7Wqh$0CD;<>iHO3QWJ8clr{mH6qI(
z9~>jGL`X7SR+y$l>B6$tIFH-VROzUxY-+O9)>xV#5DG;U^AQObz(pZP5?yc*)=60|
zs@4V+jp#5GQ2yo536z-KL7sf`TwV`u;81x%TnrEH<3F44F)5SGA_-N0Z5)Sh&`&WLec@2Z6HgR=t
zfCF?JwjIzYXg;Lwe&t$LaikM+Jx`Zih6>}ErdT8*Oxxw5l_^X{EcG(h*nt%(^r(CJ
zIQ#`XXijKu&HL5T*Q5)jZM5pDi1V=d>Fa@5rYeggDztRj`|5Kq(gbi%jjva)4U^Ml
zudUhq$p=mS!5U~yvNCINamnWV9th(`3AP~i7M_Ie^xp}$Gg~%@v|O!
zp)bGF+>}X#ZpUfo;;CE4ufuefpfpQ(1{?Tuf|W{71gyw^J_R$(rb%<8#nMu6xbvh<
z+@sFfSrf-AYo&U$H2@@)DkLc4NT)(+QcCp=60tSeX9N`$I&@@CwUwf+*oRSLs&(cl
zkDXI+`UE8^J&zAhP6#Z0EH2a?uGwTCC3rg*^IO;hcB`${DCMi9Ev7;vK(R$9Oj8NkCJ9r~=rE4)Duf
z-Z25bhg3+?H=&_h{g6Y@YMsmBI0%3YsTT}n_hXJ`H#^ulHOm>n0$)m;sWF$9St?3q
z;s%6u{ln;!i}~vU;$r_C-6_LYC_cwy!e3)?1tSQS74bYr|2K7S0@&DD-;3&#EXlhp
z%aSbFmSoG8JUZUSwk*$hY@f`Y@g%cFp2=jvStbx0$b@D{NC=4Sh9p4QJha>kgtVm8
zg3`-v=t2u>N@*c1g<`)hw|(i}H-3e_zS7>y?N#pY_y5k3WREA%SK#)|SVz*4biVI=
z%YXm>{up&rzB(5QUBOJ4G6T-aY31!If2`}2vkn?Td$Zct)D~!IZb$2Nh(p4Oe+-2b
z7B)V-Q+}xz@kiv$SzP6b5dN*HwnjHWECNgNfkUtl_1xL?U;ueF6VOEuz{`0CNZxlj
z?gd4-GSlWB=Cvmi@Us
z6?&>%HESd%(`HYso=}@wd1T$#*V=w<6|316^D^r7at5J|O_)T?&r_k$+*MO2Bh~yf
z^3`4PHIga(e`gDlM)mq)d6=?z7czxvuKT|MN23;_OPLDlFMxts7YQg+Af@oNAj(lI)MkEh6%Sa`C2UDGn|DxTc?VJs|CEQt>-e6s
z5B(UBj3J*Oo>r29ndlP+-WQwcOEc+A{hG(~`I*ln*QflapzX=YUSF5`5Z>~n=dV)H
z>oHwJH<
zIUfAwub&t>ka*=&b?TYA>3cl?k#GOe$xk9<-nSDn6n}{9}
zZ|3U-ha5V{`U@<_^a3P(+GQY{i}xu5tU%Gw%YRLCFz(fEzaG)Vl)tUr5vr)(+nQ%k
zvV@jF^L}(|+n3R7LENX&!d1YIlliqvMtb7(!^M}k5c~1^LmC%FP7{q0`U-Rwd}lq+
zZ?9@#l&UR`{I(O`*bGO*rcrha3`+lgz0vig>ZRE$J9J1_C%8x2{grEV%U=IlC#whL
z2SsLOydX^?JBB>M4hI`8tXQUUEY5#kJ3F_>Ib#xM
zt+ki3o^36|e&&*twLn-Gp)Qb#)hmP(56kgv&Fj?@_s<_L$ZWH!+%RL=mj)Y-FD?o6n0NZM?8I
zxUMX)jEIiqWy4sOfh+0#R6^XIQ7#n=`ju>aik?IRs`KYVw`NP9wJCQP(x$A=a$a#8a*ZS>k(tXu63N_X!T-dfn*|xGadY};tDuZY{U?vc8m<1*3f$ELmcGyFC
zWX-fypu@vJnprHb7r6zj`(m6v?c3IU2_>_+F<9{S`+|dJE5Zry3tB#6oh#S)eq)2x
zQq+}>33+MtL-TBXjcQE8HOEGqltIO`jh{nb>#wruCRes5mRQl6V8OdB#L_9G6NAUq
zP2&w{;5w<0P)3FIjdSIq&?W0reh=N1_3&>UH|{}w&&1Q9JrEJ2aX?#Kjq{UPxUx6%
z%+~ewy$i=D4(Qy^tz$sjytcboWBCKs2--T3jLpo8jpy3)0h=8cq%A@0xK?S~dJ=NW
zlUN({e(D@MP;UMgWD%lA$m2=4piS)$w)Y9K`4|x}(Cd2pLl2dIR~1T(vO(mwJqLRH
z{=dbLS7Soxmk+_~g*~YPFez>~@&!ntU~A+?qoX%e5QeS237f+2k%o()Ri$Gas&0m|
z*jzJMFL~HBSFz6|ULCQCK=7O(;+grTO#_o}k&IlvA7h0PFp04oz
z{=^+XC~9kQM|bykMy&oYf6qhjlhq%QA}oRoLB_n>y8+sPTp;!=W0jJy`p5W^CSpWk
zz$!GBnV2=*Lx?;(sAJ=G06p#k8w#6kL%Leu)YuVhZ5rup^|goE+dI7Bwq|#0gH<16
z@pq)#ExtBiOKVeSe`l*3(DdGpWM{O)-3IF;`tos%)y?Qjkp3fuQAwj<7lsNpVV1Sc
z0mi}$BUhM2Ko_LtSF*}zs%!Lyyp45D5pPqlsrjwvuJe0UXKQQQU|VZzry3medpvb@
z9*=);&$O@3-#)m<`@meZduS%!=Jt5pZC&$dzv{Q(K8ffxboB-nl8g$eT4eD9sb`&)
z9T-5yW%^8$yxT+z0K<>YLVf?B#dn9gmhc@-vAbqJ0%(nGW
z%;Q$=k=UVORO^MSW#=_QG`AR|DQo5f`;|&#wY?p>r6(d^SdmgGg^REnO6-g57-Wd8%fg-_BwOautM*R81;<*s
z_H5Ox4PB=S+Vz0Wiu^tuLCvZHa-3>+vekYG;ZBHj*}f9(1wxxbbv(Rcv)|*`uSW#Z
zi9IS-OpsPCA(By&!PEEY7Bx@QOTp&B4sVwiFEvhtUx?^klwfqQ1~_`@Ap>8o_P;ux
z*&19@?s`49J3<#$b{Z985+N`JPHS<;@#L5CotRH-`UvO|;MrelbhUp|uQy#yItN(`
z`aD22&_}It0Ixw?Gb$2h7+ps4oWWR5tM458V0tUv7P=#gPgCzj3C=amAlD-f79~e!
zS$Vx6h5*RA`m0phM!|B$dOBD2(F=im74oVr^WhC7VT#jL
zIk+hDQLR2pO`(rA1Y7s|L9}-rW1-Cl6nv|O1j`foscWk2PS8Kb%N4QPA=6o?13EW?
z!C4X$6?(~>g}wc|vIZ>!*UVZNZ8|grh^ebH7qq<$_5yT+`ERfnD6n2=wosS1OV=|n
z<5^IdOUD-Os7263ikre+D`R0{`&I@roWdT^erk9vRiGD!%d|2|Gz6Jba9X}C4PU`^XdpG_1^C(*$OhFC{
zd%QSKc)Tgg!|X+I3zT3*3JLIM@gfvX<9#*+0V+L}dtGc`bpGDa{`kcFYq~v#XV(Ss
zKmV4eJGRSngPc(}>NonE1F?zuTVniRKu)iuhUX?eDjw>*T;>h9;KXZ2`Mr17e2;S6
zZiioeU3~{y0(PoxDe%&RhRBF1BwC6)O)_mDP-2_1ic!poI@&=GBs$t&V24Fi8e~5W
zZvF7^KzppuG#l!j$)291v%bMJ`(h}4QHk-`%Le^K%KYM0}M6P
z{q?kZ>jnmTXWCVrODR{K+UavlGv=U56RGReqADSa5D^)091y9}_8?JO&LL_z4T?mJ
zbXxN^Ganj>jXV<@iM>_Etyn1Z;6y%?#wk?*KG~b_7JsqdmC0~t-iSAM6$y=?0=a?M9+2hIYNhwtjm5946J6(SFqf6F=h>09Rg@aI`7NarE7%vS
z`0WaQi+vH$4X#3wxQyfi6hFh&Iaap)Nn=U2Akyz9HqT(P
zS8ME6^a<^uEA;)-m7VN_vvjZV0GkhWK+y!jA%dEdkek>!9W|Up=S!qTX_PNcFrJUh6D1f9i?6Qo$dKY%N?(w3FgFXIVP2}Ox7#*MJ9eT%
z`9s_Mn-!m^<(P(-#G(%R+5Y|)h#nZZPi*=33HIQOyw*5`W%(H2;GcYqoNsxzPhbc4
zi4A^6&U~TvSo5i@3mnv6slUPsq_?F_I3c|Nvsf3%K+7W71ocOB`U3K3F{((j*6;;s
zw+yk=vGl`OZw0Pe&nh2}XPS@UZ*ehxNBj@fm%EJeXAox$5Y`tRaYG#gU~$|ywx|#I
z_Oa&00w0TuDvn>Mw(?XLBf^duaj3-zjQNZa&n)5{4_rUC|FSj~Q{$@DcoP_5j4de9
zl1@5fy9zw)ORkx
zX&*=gI@R-cmA@K(`ufDZ*({dlrPV~BWmlph7+eb98)!iZmSKz)Ws?$Y945G93wC}F
z#t!?wR(|xNJ9f9+_+&!&fZYTsAov+^ijJAm7YK+8(s}~jc)>K60xf_sLh}W7qA?ik
zXdK2Hse3|xT#YY7yp_#uYzwmOt3NO6Tf7$&s#l`$3{)E5t}W)eVOJ1%xfzr*wPtj?nF5yU=UZF?7LyJ&;#)#gUfiq?l2!r~13QFl*v
zQkD&E7vR;($_i568jXZuB)(fC$0q>OQQr72PaDQkEs-ZltgKlK+e^rrwZ6M%jA%C~
zC+uRZH6H}7>I9#WC?RhU42$x*Q)IUC44)=O!_Ls$tfXre_^N;{N+j+>t$pyxRF8B$Czk
zzX6n0s45pCL@8X!#^#aGPdec9keKY=P)aMn(r3TdK9%Yo6?`^&#cKuv+Va%{!Mp)K
zqTpHLuW~UHvKa0k+PhJxoD{DOX{b#HShW*gJ+wkhX4W**1HMB$fh_S8(NNTn%WQ3}
zfP@9wrKn?H)gse$YsZP!3>l{##>iMSR;xF$x?vQrJw6>0tx#Dn0tpTxfF*PF5K%-0
zigkepxdlso55gK^7@h4{NS(j2WtdzwYd1j5BAahvwa_lafP_hs9^1lE@u|YBa_-S9
zB1cpTZJvWe@X=qi>qArC*SWu4B7wv5Ai-5g8zZ%#i#3^*42bQk2WT>mXBgE72#oX={Hn&ZS6y7%~V!lIY~e
zYDND2L>j>^e5gX+zIKRdrc7Qm5Rwcd3)TW9Y7v;^dIj890sIBAUv!>8O_W<=1act{
z+rc>MY^YLx1MmokCoCGl7_*;N-=ywj*VU%-I
zZqH4!_efv<5Wl5W&tV69!uCqv3XD&cSP^U@JLdn@60ojrK0Xa=5TM*pQ6gBB22@aj
z>N@t^5mF56N`)&o`3X1^%
zS6ZKCOh50jcIU`?sO%Jk4OVstV-7h!{HT{e$jp7ZRgIMzWLl4RctMPvDKMOI$PCVdCqlq{%t2#JKC`^>lpV0
z93kxHBZLYxn|FAy*Oe^a}
zSL>eF>}lmew`2ZL3C1(?Mt*n8WDEET=*)kQ&5i6=7^b{5^}BS%F3%w=e}J`
zBpB{cs|tbN=sD$j_)*8xRq6JRE_TU|VI$52wdN=|(HM9t_$cg7w*8t$-yGR@76xv3
zptWE#K7~+A5pj0pR7P-|K~Ha=~#o?8I4+
zV)dcv>D;N^Mxv{CZpbi(=FXo#RsO$jyz#~-o_JEdX8I%3)1C2_A>;K!Lx;}aa_DpT
zVBZr@JV8DFtm7p)AO6+m|ahsjDHG}>^%kQ;0Q|>gkLPjJzz`HJn15
zd9?ZSmHa%kIY3LYPid=ilE!0LXIde#S1`(
z9ZVO}&n_%XP2o|pqkAY+!MaRd2+J6*PiEBHHMLt2TB%&IQA6rhH-y)C<9)HF0)cFF(im{Mya5~hRL3d)==2%Y9aBzcoztrt8XE%jt@UpAP;>hpzk64z*W+nT
zdg_}4nPz`)Cez+rzW##8F#me0-|us^PG&nB+=;g4_9L(A?B28M)vwNmU2d1lN#}~w
z<7#j>H8u_S>e9`Pv9PDXRZC8*1A|vu+Fu2;6=w*;T^?VyxH*v^&IYh+HpH*
zAa4%F>m6?o6d3oXcz;{(&h<7W1Ji!@V91Y~^}5@-JKFWl2AV&b=?otmHJV!dnUQED(cnx)8(o<(e_JY}
zdc&?dmrJ>>x>cW#_lh<=EzO;BuN`hztSvLpj%>Y-`o`0}VRsY!b`8S;cR#od=U>;b
z-2@-AYk^xFY=G`COV0r>pedCs)GA?VWEkms^158Ct@TSpPL|r!&t~_-lHbK7I0HV&;ozbt4wbB;p-s=LY){V|x!|^TDt5?V3A>FuJ#OO(pI>v3G3d=GgWB
zr(qnA#k2eOj>Ylm*^YREE-v^vY&t=?kQH4Rg+Sae@+BD8ap(bWLNT6+6y!zg;PR6}
zRiGX}=5)UQVzcXwiP+rxoz8c^(bar$E_Mv;sZR2Z);(<@)9((=)wPvEZ5|WsYObTL
z{AqQXOiI00Qj;{j4T2^TM*vzmtP(on6p*wU?c9#cMMzQ7-@6X=1Y-{1HCjgy_J9{C
zEzjuFLO~$e5H$>S)_f87S!>U$tvZegr7|0aaY3@kX?MhfCtdsi4`m{Via|kea3N3y
zH5ZEO>rnm>mq4=*X28g-{ls6)sEVz8R^yW7=7?a4?H6MHI
zu~w8^R%{Ej_uqo8$rvEOr>Gaub1z=g3xZzT7_Djgx=ESf1Bde!rlNI*SGfuLsPwID`5Gj**<;M$I;B2GRLg?&ZaqouHX;nE}M_w#RF(6J3p_M
z#W|8N9zvJY0=|Hg-!hEX;VXKss{5DTDBAmi^V{8D)u4#tN_MKMXh<+3K&2hUV&d6a
zl>*UN(Dal6-VlWTxe2?Ewi^HzJB^_9E#^-MiA%?(R8w15R~sz{U2Us{CG&Z_*=Xxx
z8bezbB<$*SbY>9iw9+5B3puE}9aBL3h0zr&4O*#=?qs|!vGU+lLna7b&|A&nWc#u+
z;T292hJ{e_a>86cm`yJ!R0x9Q%BsN7rDo{W1xPjY-?lCoxQc*j>1w0HcjcBtBljS=
zKvxJefE`*i004|F-POi@%^N^<7!Yt@rT=G-B_2d7wn!ZEJ)l53&2RP
z;e6F~eqfk~k_n?|LCH-Jo~+FKUX$_QnyJp|mYm7$)B3CLx*H|))QT8wF|8o0vKGI)
z#h8lgdEry*Rlj8I2RrWzc`D4OadDqvy(7J50(P@eP8Dm+EMoy5#6#`J;dPnJoAC5D
zI*T%kah&bP+wy;prDG4ztC!*RE%R3j1++z34L9I)KsS$or|w28+w(TUS+qsg;nKxQ
zv1G@m(2qXySXN&JbfaOc(L8LRNS#0WMyaoQFxzv=RsS*`T>3;2Ul1MwJZPA}mGSnw
z_ANzFw6tuOHLa1eiG9vV
zgD?zRV@1r&O;9Yq*fEHZOpz4i(Dr+RyMX2Kq-7(X1>fU#f-0B&#$b03F6LuXbB4K!
z1V}6chF62F8s@K{rJnA=lyv3II3r39ICZ$KM#n8jJ;hunRJts
zLTDT7=h_1;pf5@21ui@`*AGsmo@JSl7e$9#7U$^d`GX_BE+t8*^5Fw!uE_PBxF;=7
zx^aDQ9V~LM-oc1~(ttASrP$NQW9(Dllb;MAkxJfgU;_W|VCw@9v<6j&l`}*h@+YwA
zHo*#_^<3?8naCkG&h~t>o~v=}*h)ilX7OiRTTAd@mpu4u_dJiPGpLVZ`r2?-&ftkM
zm&Sa4LFS%V16f9&Z1?J?g$mjlJbdo(aE>Z_>r`S1D20m#F*gKM?w<2=bLVFdjb;ut
zH}9(t1$?^_L-WJ_uK}-nZYXws7H$!Bs|4CC;Kb0}6ZrP*`P+BDbs67oZ{FA3yenZ0
z`+;;({q###T~FF*-~_>V=(qvU7{)BwR;}vJVTYg~da-&m&QG<^DB
zAS~h`I84gl8uSJs8khwW;zH0nsCEwwEDa3oR^ZyhyYl&6!caM-j#WEvL;zwY3*;etq!7wfd2RXnz=Baq
zM&hegVP#gPD!(GhqwT}boCC<%iw
zAW7G*8fM{Z_oCe-SBQ@PDqw9?S~8ZGFH2mP=Y@%6a^eN6iGK(8N32%F2&xyrc_Cj1
zT)=FA2^bu_Be@@X!iMix})P=mYbTAAggKUnuUe+*%|L|{$#=^18+8VYH
zN|LN*FC`MeU?OoTn_0QUfTl|;_&Edx?e|)R1WF78tx-Z^fG|0c71f?ZWi>e=bs&oS
zBv@3fOe9ODsm#)40=uUsaUByxR^vdy13CZ`NDj7bO7`#;IvU=SDwR_2Zv0TPR7w`T
z_xG%>_FSBQq-%8*Vi0-Yyy&$=0f&c*mD2Dtn6C**1NcWo3FiVhaajFi*U9Bl@c=fE9Ndt}3slofs>6M-Kvpt7=p~_dI29y|f8^h_pUcgN}}rEVyAoW}qtT7w|7J
z>mbtpqJu#NL`yGU31vVW@p-87LfZqhzpz1ae$pZ`pzy{N$|YDF8%PRbA;GesskBG+
zspH|p=X-l^ITAizjQF(})!xZhjQI0^@a$gqO*gssp51-8FDmf@$Zvu9fb2WZHf>pf
z2p)*K&Fxa!-l(G*At8&4LJ6DD*M>Y+$uXzw&x?U`$z5!~eQ-w}F3x<3u1o*OCg{%fdz
zN>pFOuEWx_va({z5@B7&2iCUsNGe=0n@{$j4i`?^LcAK3T4pQ3bo3tJl>EQ4TtRdaR-RLc^;j
zQ;D`V6N;1J>}=a;>vS?M^hzok*c%8OHKOWZ`H{9@unm{qVl!lJ0OT5n6*cIc%+b~W
z>xf0Gw8%|!9uyE7_sMRki-a*9kO%SI4vz{A#ZltpbMbg0)_-?^`u~A9`5z|dP9lEx)9K9>lzawc^_}6BNk*`L;NrAKXLyF{N0bcCtrFN
zIy|H4v*@WMIe0P31F|&5vW6k1ZJvu&?`Mpa?kEkDIrX4~Gs0GsV*e5lB%wP6{ZWJ~
zV&lg9h9MtH=wkEo@{BM1ydYIRMTkwnK}P*cBm*C;Lt&fNVVa%MFJk&@nwkWStRT?=
z1QL5W$k$`*@{xdY|1(toh`Z+oLx)al
z#1eI1rlrB{ej=1~xuK&pHfEoOip-$Yn*DCqiNlz1?OmO`3`qoeEQ`yc0s(df5$}AT
z&u?y1h<{i-hw_Z}*=t`%nv&m?HFxdSYO~{5w<{L#6l0-1FZLx3CpW?AYgr}D)s*+(
zLy_^rdC}=rleoe(yk4%ey}U-^L^@vn!s{YO#$}x1)?6W)$6;}J?-ZjF5z;hn0VtX$
zFWz_R)O{CKUijwy3s;UGzan#O+t`Up3hfm4kwu^+c0jTN_W*r4yj3-t0rCXS0DH&4
zrHr>trV6r>tO$QZ?QPDXI7TWu^np|q{}=&f>B5ZQLz`Yp9xGv`aph}E9o89TLKq3;
zK}uu4Cek|I%0uJ>ECUj1{Dbuf|7bVRFH!OgBnP4?v;OQC&^_hb@*B;+4Dfx%Mu>M@
z8w1=DR7`SbANNG8JIsnd0P?X;1bSUZpO*>%l=3+=AnSSyefMc
z^I+9BB8Q>Gj~xu?VT{5dlZHz(uYwRI7mJu+l
zW&n-F6JNoWE4Yej<%QN#S3|{dWu)cfB*QzfW+7ln=yU_Fhe29J1XWng#@`ov
z-}_>}*X)nPkB>COBj(4?M9w9#*vEHD1jp%-6_GRUd%PX~DlJHRe2hUK(CE1Rj%swg4
zU(K(O_VBR@TeHXmdLLA52LCD@JquFvgXrnmLiYP<>JtCS{X^N2<1RL@9cm}h5M^L;
zxltvIxp3~}xt|*A>l^F4MZ{s#giSKiUJUdM4D(RhmLN
z8zfChBeoS6hat+R7}y6(TMpi`?>5yxRCtX`jw}Cj_7Bz$1aXGS1Qi6JaTv3hva_x%m+VbLL*AtWnFC8+`^6tK8Dv}|6P>C3
zK)&I!az&J53481*j2B}-;jBSek>e@fd*0(y>*a@irU_+$cxpPAkFjBt@8PHbg0<9S
z>_aA{PmMT5=993Q<)|O|*nRIY6A9G41_{ms)Lt@SkjyhM;G2BC0U|BqXJ6M#DYf0&
z*T{AWcCr7D~i;Dbsqnt$cG
zbd*JtL}b&E_#7h>w4wyRqYG3|R;^Uyu=sx#ngXlMD@Q?@K@@}pjyuxRQCk!KRT!`e
zAk<)uSSt9f&RK%B^Cj|0Y@1>Uw)l2<*sqs)h9&~hM{_hOKF6u&3q;F6>*T>U*$RAf6Uh!$8zuWBZZfSr@*4=OFKY53z9<
z^_{Vy`UrV&q<$#YSwD=&P)sb5A$+1^Yv&vxu>nPCNf*R4adnMD%h9%56wbviP1{DG
z-(Ri2w=bQ!57wWcFW57GWTa)-7`|Ru^j$qh@RPpl?;dpq`zDO6nR%*^d0q?VTlPg0
zr3*-H6&rAENP-oH{t=!n9z)
zElxWYyy7^jQepR8eD>>w-Lvtz6E&}Ke%l%SDer}L6I++V^&z{9EX~*ju252KdOhK5
z3ab2dne5bZW~49MHv(U)D#UW-#WmC$?%VAwm7Kf#tU!dS-pUxZ9K9Tfp+bpN%c=GQ
zYpzA$BpF{3?RDl_s%v3&<*5bHUPr&5>Pp%{pEzY`iT)0BAV8EXbVhk>%ur}X;*jbIX3b;RaA=TxV)dj!dRmf5{IPn~Xgy8KEyP=ZA2JE@M-#y$
zuD+(r;I1#OSv?F-b$_+Qf?7xpmK-+(@-{QP7?G7n$bz&JlP8kyYfoZ?==dl+Z
z55|5@KgqrSaB_|8+Q_TucSU2POF^wZ_+ZTbQxDp&a0=sIQ@2kbPcbM=UW%!_JW2lw
z4jQ(v7deaw|EE@v*|+#*h1^Ev?5z>j5xLgtfeg2lrlzHF{#mX{IYmsu`v;3`*e5@$^Gi8Q%@
zu`l2i*Sp3?g#F!(hbc7^3M>o?(+n=OV(CbE2QuW8Av%)I7M10u$tDy(Yx!w2!w1FK
z@Yixg>Y_Z?9NC9;>^WHM?CaUrsXSX&Gkrb%+kJ8!jqvb6IdxEuG8c>19){$FT-G`6p)6hCbQ`1Kg$AjxR9UsQ^?nM7M
zZ^R!*^c_V#M&gi!p=vW5Xg`}iC=8SZuo-n9aKhCX6%NI
z7ae#d*QE_izRMbx;>?5KvWl9L-!Z;$L#%`s%CT`nY-=B+&wxP5kpNUXq3S@TWPA!(
zLyyZEY9M9uCv}BP8o(M@W)azbFFIoKO1}RfzK{Qyd3?WGr;)K(e_Hvz6llB?-{-!e
z|MbsfeZ*-6wJq_s4?hQ)rdO=(VT4;??_u?FNvfPkFAvD^le6WG+2QuxiGzvV?d88#
z85A;ArndLU@$4|Nh8Bj0{kz-RcKgeJoX;b#lKk41(TE4iEFoxE@D=)lIm&;Ktw8%+l2NmuR+ekI-;2?iH;H#Yw4u2ZW1)UI1^ecjXg
zv#0MKe|R#IIhR@79qEkFI6&K#_VIW{&%(Ki{Knk{+r#7(gZjA7H8M(ufumT%sv9nT
zjT7
zc+c2(28ujf_(_{|?{H0VEB1K4)lf+LV7pY?8(gYNQ
z)jR&igW=&|tKc4#8}zzv
z5Piz_O*8bCzFs?jhtw!^bdJWL8NBzhP?HU`_wmnv&1Vr|q
zLg`EFRFvpJmJR-TRS<-(gp20sMqcIt7Ow2zbg+CyU3Pkl8c`9EW;9XYO9^@ym!=0j
zfgV#`{!N9(ZC!U07K1o6Cuk(NKba9&IknYR*
z(c4EGTKZib9UbR=Gc&&3jk6;ovyIPg^I44Ex_h*tuK7O?c3d3nc%5(GKHu(B{NgE&
ziHjDp*j!ES+s=Z%(
zM39sj9h%RaojZ9EVQM>c014tL)TM{T?c&LWoyVZs2fYUNq7P#0vOUYNJ8xM7{+I00
z2Sc&odG=+6hxkxp=YAC%eW8%AvARAQ=^AH@+ZNxFHBX!qhvR_kksd~-4rsqz8i`G_
z3Q6hAXR=^_a#IOX60j+nOVsx~o-D#1yL*~PUNO>~h~RbB|M_E)){@#$~i!ohO$gf%sfV5NP0~M#4pM1@a>%xvN%2Ck!yF
zuig1>Rg=!`pdC{Cl(n1;eoSwuW%i^WvBDJsIS9)^{71O2_DHEWidG11u!@KUouZms
z&TCq>RQgn*^VeQ22!6g=z1Aj*AV9DJ*o)=;!lguBux;0r09B|$
z2vI>n{*`KpVO3tR`e*xpEB(_~WB*FkOCi~Z{d3e_3Tq}08Lo(Z+)B&g05y0L7C=J6
zWXB6qbfTRG8a>oj)Jg7&`xdg+6O6~GyklNCJiaU5)tg;VM&%{W%bJ*V%!(Y`3_1Z1
zRy7R_5pZ6C{PAW1=7{FPq7pFkRZu2XabhBx@TEq--336w@y^crhEL#K0cY>=G{z$+
zsWLHfXe<+t^&kMI5bFZa;CQQN$mRZoKBGT%dJ;bO7TJYW)(60cu>Nxr#li$>3S3z#
zYH8S=f~`og(cRh16vxMpPNzl(#`2kU{h}~FzF8=gCdLoX+}b+SvOkj@7%h~qApfG~
z{RE+&v6i%qyou=HM)6&EVM#(l6jTRmx=1>-zX}E+6LyIW&Y2uGkr}m&6asMbttE10
zT~YSlg#DAK)f`#9It`?uLefW?Hywc>jY!(S?D3hjl5da$ffj~c{_8dvN49*&EDYSNE?|c^p5d;%=6PS(hfr#
z;9P?mxjDgjsQlWqfLm%U@$g;H1f#rS;J;1}Z*MKj2g+=1FRyQG6p^NaLdXTQSGBKL
zlz}$En%El42?eDP?j_szR6KaFc<8fR_FWYpz7e*y94azmJ(7iRBZP7j*mu+=IE9wk
zRXis;FcGJ)B@sFbL*%tM5O5h@Ubk2_)T8HE|191MIQQlTqkv*V@y4OiU@UlZtyFRy
z(g8yEw1uv_f)j-Oz$iBeQ4pVC$!MT*Z#h#ym?pW$iuxP%vC8#4
zyj%>?scvr?%Xq*Gojs<0X>20o$Kzu@wc2{eni}(!|J(3q`eJIgoc52seDJ-4eSJ%H
zgD-pC{$>+%r?IqFuoEGNie$^Q!L4z#C{IzbeB%1mXsWQ#)pWG8IZ}QAQ3{2jxy-wpqn`fNRJ2&=Ty1jK
zUl0ML#Ie+J)q153>XZmIue1U|XG4;l=f}
zKZ$EMQBw}o_BF&-a@A&nUQF0Z3TdcA^M;yt1$;|ET#2n@V$A_dW-67R@r6yqViXH#
ziFSVSCdPLB0mhp3p-{U-9P+Rh!^krQk6I3}ym1*L=k0egLH=K0$
zxWWMy@+HS6$L2zJT^`)$0Vk{L@0{=UbR=6_y&a{cgxM2*Lr0>{)!a4EI8xttHg8O%
zzwzZTO4)WKyS)nWN0`B>ZWcR5Rhh+04E$vRQw~c@AQpH?fx34sI9_
z2zEOz9lFIuYw@*8Bf6e8`>KNZBI*TIudi!rw?jax=JWk4YIO6OXlAGD(N1i_u2o{(
zmF#+|$z+eVl|U`FMPXWuwjojAf
zFiA;Gi7W7!S}-s58u4WRp&Xwz&8eoCGuAX^mcN@ozKB1XKmr^KzQfOrWBI*>r(?0F
z3r}6*gj{+`#+fld9|bKWZlimXJ3qbDx^c4838vY*dx?Xps+$EnGk&O
zoE&>W3A`x}GQWZ@2w_}75C^AL%v-y64{S8~niHKydUw;IiE?J|5mT*AY+L((I?nc
z0zniC1+}v{Da14KRoGU==z#%%5%F_r_sp)L`Jg}gClBD}l{4B|?_pMMcqoc8g6Y92
z+-1c7>Ho^xFTX$9>BJC3nhk6Wd;QQSLm3cQKv;$1D4QTDu+0_}PjvjKa1?w3Odxa%
zt~}E$qBww{v9@ev*1$pyfAsRAX(Ciht(zwDe@(L>j?9S+L%1S-2^K({m!5OtkF$N|
z05Fdg`R~N+))EHtHIjhgL`x9$CsqMld_g_cJu`LS+@q;|YVp_y<<(vB&CF-ZOI`a?
zkDfa)HS>`V%Jbd4epM!7tQc1mgATN#Yh@!KHpNZQGut@ngkQmgU!jEl;QLZFQFf81
zZyklmxD{0CMaRo!Tj2UteTSG1;4%a77I^(J1>j*n3%aS$=e@5ye*Bf*&l{)iyYC8K
zs5PAUe0j0{;b$HeSy}5~br^rCq&|Rft0Y1%!+sA2d8A?t4emYTeSc}uoWJ+|-ZMWZ
zJwNL`vv(0FNC0=JfB)Vy6Z$U5Tk@jU&>jV%)o$H^$9e``z>rdi`_q}left*cWq+?T
zzZ6NRD#CL!7lkU^Iz5G)
zNIaFFoylK66FYkTo+GpBmDkT4ofx@e5$6{VADWDrCk|Gx`2^^Pj&1?fSimno5PLTN
zT6}XY_7$zCi7g!_gf2lg*K`tdXh8)N3dN!YP+meveWQRX1Ojj191P)**4s5q;T5|g
z73pgp8EH;7505nWMN-j~%nhMfr~{BcYYokTa4ZxK2j_y}1%K4r-|vmkP}7T;VElxS
zh`$#+)mRO++)4Auoh|%&F#Nfwzo}_{zNtx4JwX$zv7|N_AKTxvilkr1N-q{g0|!IqKdp`9ut#?MT~@;OV@a+x7-U
zA_}Afb`EW$Xl?!I+jvZ|2;C&Yi{vGoC3DaRWf9rMQ&qlu|8yba{nhFHx$^q-LrmZr
zK>Rr9z-C*2IGY`cDR~a}@5MR$50uv7C^#KhPugahq!(WZIa(R88ctb_h5P6s(Q6hm
zHpYuayNTf6ojeDg+Bz_8zDF)?0MaMW&!-=!tvcVZ-P_hM&cvQ457qLJj$PLp8!URH8Jvsl^H3Yj4g!fe$-;@WiitpCrTo*+>u`n^
z5Xyb^;IHA7HBE{!ROm}J7Ujo{C1v}0YTCtogp*fIxy=;p?2tb`Hq-J)Eq!9iev2Ic
z^E)oUiKKl%>$`e;@0D;4o$LDh_~gPnRF_3?Y2zW!e;ynk3<>eVND&1~=Ywny;UcKE
zM7U=d$bc&`F%0!#cmjF)i_xMH3>wSh$C`XpG<;3R#$g^n;G}IMdmd{```s*UG`GXm
zK|64Dl^?lrgf;r?29VOSxLVki%k3(Jc5DW7rQ0b<-&qF&{ymUxAeLFycImyoKEj4T
z+vL{3u~p&ou0|)63XfCn=GFa?#kY1o51{_?9~=ICUBB}sUvzZtJAv97H_;*2i24
zEiQ&ukUO;;S`-DMfVZrX$>;Jq&#jpH8*B1y+?$QD$r|6FN!s5@NK4#6k)GEd
zKTW39Xa#bxO_Sr$CUz4k#?H3GS{CBZ7&FG2+3&;HUl0PYUBNBH?jwF6P;!a|Q|f+E
zJe!&QKlHtt*DoCDkEWUuM~)s*hOt!6EE%J_cdJVhB6}-<2aGu#Zlzy;G}_qR+|m-o
zQ+H!@DIMRtH=Yi`q(d4~m3Jy!#14x6@+&Wu*S;o^>FcB5+NtvZO`bT+b*$z7BIpD?
z^vsEqkN{%c*i5Fv=o)z$B#x4A{6)>x-X@G>#kwZ-G2Iv9fJpB4RF!#Jk|$~&NIUj8
zzF;1oFR#sCFIRojxkXiXAA1_^&oBs@=OC%-t3?M0q3u10k
z4%t);c&l_=WtTEjO9G#*-ViQt{&|y=6)af^wz{7*zhjIlb}+cAJS#6WLO7fYeD%hGu-rnplx;-!MEg26V!fhD-rS
z(y_PuqxZNiNV9j29mp;Q8lbQ>G{a74-n0vP-8~)c*Hz)nFdnwf?}6L}9R|1Jg)Ju+
zWOBi#QQBN&q$}cWi|@a;aP#TYi>Jg_sKEP&g!jX1OOJ=BIfVI#ekaRU5=^zUjIamP
z^O%D8*gr5npgs_Yh(Z&%?4)cF?d-VhbpBNP&ksbR0|U{)=p1g0E*Xy=QB=`Is<&&FgWSHGR-Bo>;)`h?Hx!%%Iv@JjvQN-%u!Q
z!Ii9S&`i{{x8Lz>u*kXcXh7ZV*Uk!!6Q`^hhxv*^<
zu(Cp8NKbe~i<25(Y47&Fr~wh~ekK$i9tmGJGK@>Vrltc=q5}_w!^6Y+5iV_MRNHh*
z(4{3WuNF6_605r0P)097qebAub{geIkwjhY-To1S
zOvvf&_ZC=?6Z~E4+gy{C_$Yfd)~>uQOO5NfO5gN)zUtT5Ut(FPy~@u^wE3261tc^b
zH(}|A#JqJ3cykE!BS4aJyfyj2Pw(6J(+?!yee&{!mUq6h<-%pQ&%V=s&AcgE8o~^O
zK|l{gAN&-*I~hE3d;j_a$+zkY{PNf>bM=b{E?hpTFSGR;4ID>(4Ii#Du>Csyw;u_r
zfo&ICte?B(mufHQ#$7nni%v#C)AMkUmMnhm&vq2%t}-KeeX
zi*&}ie9hhQwzi&Vq}$gPO19P=?K#z|fyJDT$L^XTlGy*)^GKi9fAeT_s6G`8t(|G=
zZftC>Ywip;wSpdo{l1p2`sVN-0&aEvco@G2HfqCKx#GY)odNXZPuW4IKdFDY<*_xUnhe5Q$oySfRJnF$j%IB)(k
z;(3+h^5x4|f2PboV@;aqCAQ=L8B5OWr3%aLC7-owkxZzgLYK4aLGA{zZv&>P#1}fe
z=*Zfd0qdcL)B??4I7q1%-2be2JA9PVhrOtvdt6qEy5jU_U}$v=V_
z9nI(_SA#*MH9jCpI>d5&S7MBJ*i*R?)#s_yjIu}iQ%{eob2pw-AC!yyl$gKnIn3xA!H9>Gju+J4(cJAfUg;fCPbTZ5~iwn{6D}v66=r{niX1N(-7!!$Q2@OhF>wKGcFGS0r#nk&*$4N9JsUVPIa{F&Mpmu
zTLuK~{U7eSI3C?sxa)M{^y%I+iQ0C!ek~7lt&C_>d%I0W;4&srLCnQ1?Rx!LM7!!n
z0giKL*Yj*uOTgx);P2{&kE6K+nw5J?3VrYj;qK(*J$p@$%~`@&RA!U`ZRj-6J4DUKTci@
z^fsJTzvhei%1?bWlLZj(!^g{ii?5H~f8ub?-w_Y=jG%Al8WJ7%pBNfUbav03tsNI_
z>yYsW0YyJ-8NnWr9L}+Se|B%Ua4_Vn8IV`lGw|hQe)M$BaJ->11H@&cMSa~_hw<=2
zcTG9QSrv^MGv#dr{q|xQQgA*}Rf3n63z7k2V$u|a#ClC2Sxb`UXSyg1o)O1q1{Z@*
zW?ytncGtIg6VZ4qngH7Kb+YZtKQB*tMffquW*IyX(xg@?G|Z;1+dt5a?U*OpVeS~;jyKi}4RvZ%cPBp4&r#K(lz!03WCE7six!PaH
zSP5D)c0@y>NDuoikI3)a>KH_p0`eY~)lm6cQt!Qze_AkC4F0jtKPtV4vEHWmY_vWV
zfZn{tqiW-aTxBnqBd0_<6qP9*Zw`xuO&xFcCNc@Hf(Iop)>{I;5H!U8T)+l9I|G&b
z+7x;cro?GSf!ZzGn&vfnz+`kySat0CRlQh=F)cl&971a!xkg87#+ZE}ege3Zonvj=
zvjfnwY>pvT6B&hnmT-HTO~HKZTeOB?Roq8kh4jxDHv7ZM{o8zsaZIhWN6~KxU|{>^
zuW{3e?ObV)RjARz;&;EpI5gD*lz6UYe2-zAdG-h?SQ$-yv9tk{<;{M-7TER$}i6Xyp;FA63#(Fld;;kcSBY
z08x4B-CY+B=kwVG-~D*R<&C;!*ZAQJ=3?Gl$i`v^7qY!wsDLMDxECQXT3FN`?0Bh?
z3NLR?)L#)}U;-A;r^5EdpuJ|--ua>OW_Dr!Sn&Ap-g69STy?>*Jin0LeZ4z9c70W^
zUdN7rH(C%Ax6S=TAzNE&2e~PUtLyAA-%I1ZG>R-6^rx*5NYs;AMXUkUiXx=4y!P`r
zE8njZxg0TGIa%cP&o)*Nn!;Z~G%Rn9D*x8s(~iu1oRaVey-v)=eRld(~-$f*aW{#L_dit@&L~(h5FVx
zKHbjYq}~KxQ2X!_^GzE+8FZttAUvV>p8~0iD>OcFbqOj{VeY%gz^I?hTOw
z0^VNbpjYhes!c%i+|NZ~Ieo=5}}7$fZ$`M0iR>Wl^G
zj0N
z6u@Fs!PmalTw4QVwH1zyZEs$B0UHM_D37j5(9xoRvQ*^?j#Qjp1DofG9o%+CLHEWD5?><)WyWFPArR8vFGcd9gju{Dq{)%G@@<
z&i~($JIh=;S^1#hOrZ@^-ff6I7*prtFFGEI-~8BPvB&V6uVU|$=VIVaY8l%LV;;P*!O#3Jpr!6lq
znh};BToPj$ApPTVwhcc38E1?vR^2OeU;A1vhKfq0)FKO_cteKeJ^bY>c#a6MFY
zPmLpl-QiQVB?w};*mBh9sZX666f*F9M@vgb%QrrC>QgE&wBS0tZRx<6la6dc=fupT
zw-tN0ne<2PS6^?dcvHIhgC&eQbi~W8#0{g#)G?iatBAoy1VA)Zc4K=Ti1f$2(q~XI=^A+pI`6~F
zSXLVv0`-JtLD1lZ6b9iEAT`KZ(-xE8KHW~k;848#_NG=6Ed(SM3*QiX#|@{u<3n@6
zes6zSy#`k;XnAK!eHk>mo$WB1l`{@&ke9p@mClgv<%I*Enk6%u)m<--zhZCn>&i%$K;xF%!S7QVHKRY%sbxhu1jkQ>NUdt$Lmeqo28H{C_
z1X}7o(gBTUfM81jk@O>^y~9JgP79Tr*1iBaB8D`{Fh>__HjqG|cUdYrC?-LA-5vo4
zK}+#{CDgVq)eh)!TQ3E=s5q(>MAs4V{>)2fSCqa
z!8mK%?+<1xuF?c;s|J;}t%V3J6Pyqm_1z{8YJMmIw9`BYPJAfOLt;Yc-jj0XH|5NW
z4tb*tOBkxsP};Gu3YA{utlAA?15{B6u#<4qvp|DIQLpO&5LgBw!Y$EACKAaUkXBS%
zM>p!(CNZ#mEnQu`O}$<3$z;^BZm8N0?*|@BP&k?*JM7-OC9@Um3ml=={;ry1xt#sC
z#SHJV&*Edu2W2|aGPmF#!O#FCKIyTRMA;aE91s@{gp9;O+bw%(a+{6~%_86X^=HjR
z=NTvj=ZB-Yb=c^p<3li=ot;~BojECUM_H;QGYvHnC6%z?lk3ao`ntKim`AyuB4*j;
zD1eVZS_4HF>pWoBAe#UuX~NZO14a^_16BOiu*UMl8qBMv{K7&hpI<U>hZq$J2&tz9L|5VKYWwp9`0qz(R2UE7*(15JWE)wX8ZD+0;&
z!OhGqHSVes>uVdEwN9$v!uG259RuGRgAVPg7C>@~*s-~>VU-_;B#-IB{zkb6*h}*z
zc(9P#!h-5*Xc58&_ATZ3N$7UJ2ZG7Z+T0*j4|4?e^UqD
zgz`S1*0%jL513fhQuBL&xomv9atW+bTb|Hy-4VzXNt_$gH6;cn5R8|ECmfZ^JPgcB
zdOBdfMA_f;RL~iDcFiW{8zU~)-7eR7cgyLP
zJ&hf+iNtJ2(&Fj0RIqCQ4~%9WkSEe
zm9)P1C*H9xQ;egyTDdX=^3p93M-g?)csh(Ue>*%LK;
zo}@rx?KL?dKeKf);s-Rz>e5<7@TH^tYulgOA=JOxK8AUxXn(s0Rr=EgJrFE;0PKXA
zQ7lCXu`^yNigGoCu)5e54~2bgsD2oV`)>K97}&QV3jbOgP{rHYeEi}qs#$^}w!sSv
zUw|*#>5!OeTh4^6Ay!eW9vBX?s#uL30~O35_@oAXxsV6ru-6Xt+E&fI(g6@LFqt4z
zCw9E6kN^ra|gLbomd6VI6dd9T%Oxrg#t>qXEn&_;x{lXF>CHK;SdiANiBdh`6C!cFJ~kJL4o>4^-~VH8C`JLUKmto30J+-c56>23
z#}Y8#Zj?J;$L&!xtWGX6kFB&1WFy#Bx2xig7aFuX;&4>%p(
zf&(Md0RB284FcW*>;$S7OyWu9OG&
z>aMHnb~ZFQ@z&Mu@;d9+Zr@+us*3;Y1dUx03?$N6ZKtfYQdUyCD+(C=P@F$Gp!8-&
zYa&~cyEh62t5?On^pAueqX?qJGYMOFyU_9KQrjUx1$pqj2(2LN5Ukf1`
z_D>bOrTj%TbDrx~+nxDa&gYI^Qa9-HcJC-M%6D*$LRY{x!2Zvx?}EOgFOV?-Rq9Ft
zoz#mg*VUz^{L=Kt^ZH2u)bA?)18lQ*DF~#sVy-E*m
zT57wvyGCtLD;M{49R!F%t>Pf}Z_CLol4VM}pIC`{u~e{l;5QP}UvE~ev^1=vp14A}
z769txH{pN>fof<*QBazEO;~+P_}nOQTxm}>z;o9nGNhM2)SMrIgO&gAM^*k)W^-E)
zGqLkOmC$w-+Exs;R{N#$V$Hkn+OB3DBSC$59nrLukP@u73BM|QG%zVbh?YieB5k`N
z?HwbzSKKq=A(ZrU8hwCoR(#cP(yXsHlf$n<(d0Gl3f0G&LPn#ApYQExYU)6mRo!}F
zU*v34)7i+r!ZHj3rUpfoV=U4RVM7iKr|e`4P1V&NSuuU@l;
z!_sR5-fDALR6VHjS5jMW)(U^^_%q2JxH9Z@8+8rYps(IpzlcY`pKYi!+}>fOuKm%o
z$T}FBYfQQuj(Gu#?U`<9n67ULy1d64+(~b@_gWud4Qbh=1PqKLjuVg*Ces$MJ2YGv
z^Pw(aFQjmcD-ZnPcnZy{5RT|SWCh$UjzIb%EvRaoh(xw5VHZ`xT^DnC23nduPWL_}
z1hq8`;7px6p|1T=ic4P0JK{Hr{!@p$v(7W=cKRCKX^&^n>-9N36P~(G_q9IwdaYEn
zDn=U4nZuID=|zAK3%#@*v6lG@j#}OIsqtTAIrjY%vB6OgaT>t#=B8Xol|b$XV_jkZ
z%jZwdi%y7Zd$122>R~jaF%dN?%8sarRMF`=6-jS)aB#K;6VWs_V1oVn;7R2@Ie5#h
z4+9VJAz&pwU3Ngaqk!jz?F$?q?bvb;eNkx1eur?QrBI%_7
z@QHPc?*2*})G>6-61~9@iTD9z{$$FVO#{3q2iI>HY6c=;uLd}PnuLsi`x1$){rAGw
zXeQKeK%C8LiC6UF>53v?X^5-C9nq*RHV8ug<RCYd0OJhngSp`JqcsXD#xUwUA0p+pam`b%peze%Sv%KtCupQcX;31c6FJF7
zL9hL~S}Ak}4BUNzb+b?oF#`@#Mu~YN&yG3SWCf!83I@SZ>KQ)g*Tg|GE?
z{nSw2K*dbo`SaccIEZ}5n^5!Cm$<(F|K0y8pOT820oi@rfv|=$z*yjhZBe+GLG{3l
z5j59K9Wbn51I0f?E*IoMRe~CpJtnpvB8?pG8R}d%A<@si6@?W;(c3wZw#6fELXyg=Y&Kgk7IzT6%bQbERHO{!
zGgDgLGa>I$?oA+mjd(0r=e97lqxzA?Y|s?56GENVLOMYtm1!*PtcX0x#zB*bEAN2H
zNja&4QpIQt7nRx_qH5Qm>rYRO&drU&6NrDC
z0OJ9H$D#Zk4l?x)ElXQ?G>F{;vI90%MJRyPQ)`%|r=AKfW(vA09G)N)BQ9_SS1oUn
z?IX8<)kH9sLC9C6mHL{{5nzeR>aA)AoK&vr4))sTl;hGRl)j>?qP=rD(hd_Hf@Jb&
z5Z+A0J+dh+Q^TnV@8%|(qXj|pJIZs7$nL91iD^X8P_~D^pDu04MRF@S%%_Il4i?P&
z{ugy`0v~5p?+?#8=b0_ZOgfXjNs~$1CT)^#Nz;wercn0EQfaLSOedL1GR|gX@B4X|&*!CN
zp0hpYIm>VVon_1)s9NIoM~*%p@vjj9|JWs-W{(j&ZoTwUTtjwiT}Q{)q~))<(a-zH
z*tc0NJ1)f76=?;|WTI;I|HWN}UVPW=6DqgyW0V~~6?OLV699^qKeJANOPFxY0tORE
z(Wfnbyy}N5qu_$V|G*X#s66yg!IvJvyv_nzi6zv@;nCzM^D2kB!8-Bn_HU)y&%I+;
z|BjZ+o0m;3nZkM5-GN3h}!f8ME!tt#A6}a6Ix2_buuopF-pIo{$
zd4BUnyR?gCX8bh&?wIei@in5u@wqy--~W5}_Rv-|2^w<%Giz1_7kwo|SouZ!(+(t3?5i}
z${lB)|9nUz>RvjpJC1nTmaiOYS~RaQGJoaD`MdRE=96qIE60q!vsrwLc4M9z1wuCj
z?<>v4xY6z4!KV%$+#!Z(T1a89{WZtPW_VFfdZ5{+V@!))(oM>Lhr5bpr
zWQX8)xOQICa!fMNchL4a<06LXAzWH@FMpQl8JdNY_gnZo+xERvXB`5pd>WLSwt1jN
zk)V*Az^~Pb%}PoyD;N~B+9n1w7`DI9z*(&OAwJE60GUQ8?WLot_vn!wv$MKyz!He3`uL-^^NfAlU0Bv^{rhb1CUp_53%
zX^DDaiCL-v5IclCV&zc)9iOm}0AXRfhQ&}X*gzU3|Go!tHM(Y*fZ-jO^Fx|n8+fMu
z3v}v)v_ap`126~2{(t1IOCwWJUf^9#s9g^VFM$Ko;z@ZpE>9m$H7KxE8?6uA*yL|`
zD|#x_mZ}9j!N@F83Eb4=4|qpfLUO6kcMuM1#}|G?&kk?^(lqp2@?o$;o~5$@X#B(CK&eb33*ku$oOmIDu4a?{JEt(RrJ*b-WK#HL*A<3YHy9dRrxNf-=TjiGJvhe
z_2E+202=BO@^O&~{J7?iVix{`npX7X!c+?tk*QeHqankw7m!=>Omx_G%@)5oIOCk`
z)&Rfz@lHd>1uZ*{cNRK_kGzp*l^ueGRG(RZJMo$tc+k|^29F~=L2A*G@-XkCz_GA*
z>zipeXp^Ssy-?p=v6rXAz5vnfoITHIyCwjs$TNlzG4h|LBSlegh-c4IN`fK8NyJet
zrBrdpCK9Jz4UI4bG`1~*Td1YI8(%qj`DX4FA3y;d--$9G!y!tbIxRKJaP1k_{dBgr
zo0~P@BI@()-g0^(aXK!;wfjt2hyKkf?tNH{WuJBWYNI-U5BMW@cQ;`L0?0LJXv0D{
zaKx)mTiCd}uQ}1&xBFG0QzqM|mNfS@v@LJ^uY)U?x~KvE!chnH|@znkj`?ei(PT9L61}Fn)n^
z0C4ms~ov!Z^}L;{92P)85#Xn0D@9653X+S1Wk_T!lE
zkizmC{3oz$2p7_0ncji9-!Xl+eKS~d)j7Av3e*IKVC?hN8jI;JGz&31jB_n?8h(x*
z86Jk*b3&Pk)_&5ZqReDuC&*3EC><{4(eIbW>R+r;nboE)n!8*_!7=ckII!dJjvae;
z?&Pi{9ROrkj=x>4%9A?;O5>WU11yYUm`lo!90$TAJl!@eb`(qP
z)+<*u7aIA-$;UW{+&(x><{%qaXf3=wb-)cB!dGNgZxA2xJ*H)z%c1)n6sL(&{!Mr;gf>}5Lm2ZSf;gHG}$|Dn|Pi%B>6G2`J-$0yk6zpA
zUL6eYI&)KhP5)Zo^4eWzp47Xku3n$Z%{|YY?}f05F8qYU^`4Y0F2|O%$@SX(BH!?hw01jHNG=QD(#kn;8FbnQ*5SWas;enFJLu@{qj
zIws*)>h6rx)$7$TXYblRw0-@C3%Wdq_e3V=c{=MleDpRg*|B5&_6s%)_Gmwob3YpG
zAkdaOajkT{Q;bSXww0XG0#q@)iT5ip^w{e?2dJ~;IUG*IY|gt>xK=j
z_&Y-*W8X~AR;|gKgS-x$)H?s9ljgUczj+r{<_qk0vKPZ~bqi@vX`L!1HEnI8CB5DQ
zd-*0@UTh5N4gqio0?FS`PahcWt#7V}5Y<~-=847v)|r8Yt?QebI%^w)Y|FyGmOG_?
zs1+3Yj}6T;e3En(gVF#va(KsZ>6SLS9x8bUM?MhXzI?Tsh-)talLNO#}8^
z^<8otFCotNV&>#=orR_E(eKZ5p}_^5VLA3YkeY+X`vi8uAGdl)GYz-{5&P@_-#lTp
z1GGp(Is9@q?ix>t-A#!9cAN?uIoY@R`nV&$FEYJ55;=&!9g#?1BqFLC8iwiu4S~A8
znMfqE8yUNISNF}(>}$`zfjQ%y0Z>5Gw+($f{$nxzI3rXHu?rk}IXq0WkV*GIl$jMf
zho3s4eNu=HV*xb80^sTp=;*gVN6k&ja6}GuNc@j|m_gCwCh+%dzp|^Ne$UdAE(-*d
z)fhN>r#N_Y&l63NsT}@^dQYvyd4bs7iN7_v
z!XJ17fh)IP*5zH*(Yt5Y(%W-^qnEQvv3Y)@)!EoyRdb;)()P~k>)Wkob)7TNQ2EKIbsye^`tEyl?kzj2rKz0LR9B`7u|J
z_W^$xDNY5rGYmd7lK_ourF6|nvH_?44fpj$cH^jmvwI^pZ=S{sVJ~IhOpU*KIHDI?
z?DW^nU<`-R=ULDK%4&LL0QTvX+fH!i8CI}rqlVr+23_xPxyO`kboc>%Z?Lc#X_;0*
z2a%tCd;4We+n=%z89uCIchi_)n{Pm`E^=)b9~B?eG${-yZRR@2MP_3|W7u|(zzV`6
zXSC@+3~k*S+qyN4EeAVyrgrQQJ9g~2jw!LNqKau(?i!{@%Z#!%lIB77Su8W%c_bE6|EaQ8$yAW
zy70+~^r>jrN{=vn)2IQ*$>CY7tIN<;ySM_#t)VU2yv%*eUVLRS
z-oSx{;5Mee9zixQ>wz`_2s#o)B8(y>etnr>T-8gh_Nn5X9%9GbF
zl|jE>$m*KllKF$xRqHL)Iq<*t^pZsCUN_p8?SC979#-MXTxs%P!mo|Roq3%ouhWR+)m
z^ZfbE%c|TSe@|1BXl`0!WwCoxd8@5R#NzRDT4rnj&Ko-gWe%Ri4|In<3Mp|hXvp@<
zH}3x<{w~@mmTx?|PiqHj_HVrWkNCT2($Gn!_7x$4j!DX@z1g>en7Jl
zXwe5iv)W)U#ZG*f;et4)91a(Pa3hXuH_j^T^(*3n*Gz}Ft!|zO*W8MG-e4CKN}L5(
zXb6_EKJd$4Z29`Izuvm6rk2^F_E&VB&d7nb)eyp#47Hw5Cw{&Vz!ZnCoB6!I2xj7>^}`+XaF`ZujyT-V$^zoBzYZzB*AcA|9y!x*Z`
z(0qNw`pjZ%QpC>0eh72(yPLn)+!heZ)6vtr^eo&wuxEbgN6z`{lwGOc+ctanIXFSy
zA$^8DO!Ygkfet@M^zB864nr6$-Ob568qJB^-~xK@Lpc{H+UW>OWIz1lUS=*OCOhDS
z^0r1yFU-t4jvUcv&*&{cu%_b_*WTWP{DC8*#4kvTLFIYMJCBNoD%LZE1F3p~xM|$B
zX>G2n&yl1XFKk0pb<(b|s$bOS=XKJT!3lEa_
z0e(y0pkIN~2KIpR6Wz!8LTlhoU-G;qHEri5eb2KytpCft$g5J`=Oey5y{T6%@pdig
z^4@7`uLjTD0iNmDQH<{Ij2?&qB}}ck1INoP0ggprEz+IvFs0AK+~@xw^cF6)ndy6e
z^3Qwz3@;Iv;zZq04Ozm#ha?S$-fu(1)s0msMXngovXeA@={LuW^1
z4;rSo8*Ao*)5mSTv(}3=w#1H3w`T*
zmTlU!>@R)WE?lh#LQzZifI;YmmVkRH}{
zVy&##Q#7h>hWO;_zUZRvznZ;ahu3yrdP(=01E=fzTCq2b`gE_>XZrMdic?+ZV7}YE
z9`Hr*H4p}H(VlJq8U6&(+7ax!2ARE_W}x2o>(
zzPin7d$qTxTGfXdt2(Q~b(Zo}*SM8c7xs54y&d#T`u9C$;V+{OT*&8{=M7oCK2cRA
ze7*I(;l}DE)r}r0g!CYIf!_Wwtv8l}48ak%MyFt&r0WoP%MQ(3=ph5)1^yX%q0wnM
z?)(_;#Sr%%&=(jS!G&Z8IW3_#Ipjg}x?$Ky=(jeqKP=n0O9I>0c$h1n9+D{>Z0C%N
zcxcn6h5h{tBaubH;3GWscPGr)1E=G5gF~CT2CrVZc;T+dqPk^u59-r|Z`7yuqREhU
z9l6rz@y>dR9?aDW%#sTpIZ$L8%izrbK4@z-p324zuGZoot!LOejeCwD*v|HK&)>7B
ze>3(Og2>peEh2`{-es|4&mP|IbM%W$qjl3;=Jf2_ebjZ5ni3#HtDF*I=6*zQY@szb@Q94;KzCsNYt<;M&Lqdhx~XHGStq
zhKGkEci>K~mb%V)ZS}98U%zctC~&Qq)N28c{%y_K5sr-j#?3$no8dUPz^k!OovJW7
z2y9fv80^H@tWLI~r3ZbqjgE;U^FuzhO`=-4)>Gw)F~p>pz#$l7jhd%3#aLI1*yeFl
zaB_RrN*P0#2k?oQ@{!E>)EXhqVIpA6nJ>5YpX$}%O$$Mhy*%G~t&xi%DxxvC+@kvI
z+SeVv^`!pS_xJ6*vcIoy9hRf4%ignYgW2}GXW7Pn(bs?ZuKvE0&s)20-P(6&*KNc%
zN_ql(%3Pzdu%WmY@}ki0h}q+Mf7^!4jC~6U$I2y+NEbQ1FviIhaExJSI%tUDTybf~
zu}^ViaN)ZawLH)&yj5P$#bEOdSmM-xCy#y;Z#(+Ug##^z=6lrYqts^)ev7>RTMsk*
z@Lv3Ko}4iujOcR;y1GgGAS&ZmpCzD==^<=l%`1M}g&2m{dd=CJM(fYnBE*(+>PI)7
zy#_v+o>RiFzh27gUmxBw-POJ7%yoTOZriu+%vIf8OS}4l!C+q(q()84`asJLL;vAw
z_br6g#cwzIseZFOB)2mvmIyTg9s0;NS^w%8p*VfFFYKPYys0pr<7p_o^^$qRq4RrxI
zU$%5E8XR1-c*SD`s6!2UgVRji4$+9U`;Co_J&V@QTVHolU(LtsLcs+-U(>u5)wQQr
zTb(ua9lad^(SjbFIk@s}IN5P!)-V=;G~$6iQ{HrmUx{CVE``wVWz^1$!nd}E@+*Cy
zUfhL)tw=b45cr}IML#wR2z}R`c~#-$0!Z+geW!G=%1d+usvvaW7bL
zY3qz$2lbZOfZy%Dp|P)Tp>ltq^<-=oZVW7#AF8Sv8od<42OFMXK`g>pYR+#z4eM6Vh>3mXy7FD{sWMqdEPrM
zo%-M*eIYxYdt5gQ4_om>p>xjGM+&`p`Lgz9_W!RgKV9excEpiU_-Z$EQ
z+H2<~VDG|-vWjKd2ChY&1ihuNjNFL42ojF~L^+l1-~hi2T+|w{|tRH8l2a>}}W-9_pME2W)S~yw8fS>rrrlS9x)88N4BDJ$QhytsYzh
z$;itYC=6V^X=Q%Z6{}Wx8X^NHZ{93UAGu)NJ3FrL5L*W#4W3oV%CFpXHL^a5JfCOX
zBONzj@eO5fW9y?0o%4>-wXNu2_>f+VatNIQ$5w%}W7-}2`XF}5noV~*XLy?)4(ack
z*?GXM@rO67w^!fBMjQJuv-V_uR5ylOmQE3qo50jdyf*H|Ptc)mPIp
zNRJoV0$L5qu1!EV_Xl>ME|_Nk1xOrhTnrUPd&ZzCX~U3~XJG<@*3?+Ph!VJpgpW>vP9s#%BYyMB2A6a+@+08otBhVXH#gwnzJ@sqN7K@st-7N
zoj&8}FpkE1)y5S9vW8f((FvT>yx6;9^X3)a#m&ANA0pk&D>tpwu~vO@5{^*9|Ck2Q
zdD4PeTmD=^I`3#54(=_c90c&_G0FzzA#mEJ&iWn@!-e8fjeYheb8MUKXzR6>Sxa#q
zP|vazXV;6hht7x}+M)5z+^}<6Q)frt;?~y1eI5Ocm-h9o|7-m8GMYr)(>^N&gorB;
z_>c4tskp%CGwsXT+dGzZw0CU#%(nLSirAN#&XjiKp+tGi$e0cec&UH#R2#y6(LjQZ
z8l$c4yY4eCsDXtu3kN6R7{iup!g$Ltw+bKT_!+*$@gohx%8G`PcGfT$a&9)m>i*>$
zVNV|JGmE{&Ri}JOHqRr|ZmC%eb@L4xuDSBq(B@g}ly8S5+>O2e%U!3yE(yJ$RxDh*
zhP#jk^;RAo)!Tm&Z>l2_m;J$%T|{7#N=P3i#bkY_M=VwD2He%RRJ!Y4)8&y%@Rat`
zPb9|wSO41Fp*g&uu72JEYg%8mD6WpI@Yl39bS?J>TI)sUazyHdi5>l(@u1^aJbf;}
zAsm`BuhSjZceQc6xSreaamV*#uS3GRGkDgEKFe}D$KW9<*oU1xg514NUIxkz7UCLi
zn4CQFr7sD+CYb{&;LOx;ZRVOQoi1>_8Q-=sQJZ5ah~Y|S?29sXTF`j6ZFHsCmd4lO
za|og~q<&!goM*$Fl1{Yzh4S|VARb!etu+Cw=Cp>$(9+e-*Lzf;raI6#&=?S*CLHx1
zsBUVW`#fG6%giaavZ*={tZr&5EAAxW5cz;GitG-ba-Y^18LDf(&JsRfAmG~_^zynN
z^Ve4j2EA`{Qu(aN;arw%TeP;ga2Hk+?w~9xN7^;hvw5d=bjI4b*#e2e3_f^tr?@B(
z+p;AVsPb~Qyx*bw6MUi%YkMeTOJL?w(4kdWafx-ogFUowz+T5ayG9SBlDcrMa|auy
zQ7b}#AAtPA#k>|B^hr!^-n?c(Z|{OoW7E9O_I?~$x3PV3WYwa1O_3hGuj%OC`M9q1
zz~KWAZGK(fhTcfSywLofwyxItrtQHEi~9Q)4Mv(m?T^N!`vy
zdtE3kvD9>o>Ri@Xo)&A8braEM-XX3}O4SV|@!8pdBjQ4xPCGBLTi7&jL9f$DuKE$(
z-3u1B)i*iK913n&xN_y90W=WZ>0QwNRkU)L4J_$%=2T3Nx4UP}+D5GOfmQ++7pyOI
zG6;B~BE7y-Pd)nUt=Z1b?AGYEP)kF@wkVdccXc&IPd#<~l)=GMF1)tY<5s@bYqfmT
z2l?o*zCS@9oV5^JBfC~eZxhcVz*8Sduom0W8@1WjJh|Jfd#JN5^bPLo<=Ls&?LeuSNMW$
zKCfT;7YSJ-tp!WHmh!rV!~w=iM5Jo8taf+yI!j#;8d26;Puc30V?7()a^3b@{0*Y3
zF<9@ms=aMN0ppRi7$+F`YJF~~vY@vg(Y-_v>wQP;Fwd9$xE+>|%fD-fm20W!x5Jhz
zC<=DijqrQzFl1cuG$b!!`BCR?{F$(Tjr?{y%o9;&?XYrfckx_$mbYAOVy7L(iC^Ms
zJM4i~?PHKnGOny^zbofTy2f1-t^#zE0knD*EZ-6A1sZ2-N7Qfd1oXVDr-HnRMawOl~|8
z8CbqDvLRC06eO%$Hn0qxMA3g%`!QYEsr#BoZ`sEPKE`IQ!oP^iwKI{+Co}2Ds^u$J
ztwMZm&84APHg_zX_UVyYRvZ~ZDE|A$)#x1vj*gW5sBi032|Q2`DBZZb4`&_=zo-)M
z&AGN>pW^9&i!(7PXG6B#h8CTR9mD5?Gj4~Zdm*^;PS3{U8O#^0q79^tI2;k3qDw3ki^O8lEqcTfu@v{(^of4440_B8
zu~MuO17c9D7DHlS|O`1uD#TIdjI8|&Fr-{?W8RATFmN;9SBescG
ziF3ty;(T#|*e+fzE`+Uhr+AInC0;8o60Z{%i%Z0%A}V%^m>3bGA}$hQOpJ>OkraEd
zz%C`GL|SA-R$L}>A}UVc#AA?_6K6nBYtiMz$S#XaIZ;$HDy@jmf>
zaUXnN_lpmT2gHZOgRoOPBt9%Y0^j+^#K*-a#3#k4uz&qw@fq=m_^fzTd`^5`d_g=W
zz6isQxEW2e7q{yYRSN6$%xlAsXE96SKN)E_D
zxmpg%HFB+7C)djj@+7%Yo-8-X&2m_7k*CO0MC*LpclOK@x
z%MZ#2p
zR;wYkMy*xr)Oxi+ouoFZlhr1*Sq-Z#>J)XV+Nw@dr>is6nd&TcwmL^`Q?F9zs`J$O
z>H@W0y;@zUcBq}|HENf7t-46PPF<`nQJ1Qy+O1-0M2+H{zJwZ6<7z@B)gCpeQff-2
zRYqmiWh$rgs-ULTUbRnMuJ)@d)a%ui>J93R>P>1!y;)tQu2$Emx2S8?Th(>ydUb<(
zo4Qfmq;6JkSGTBJ)oto_^$vAF9aMLyJJmbYUFu!xZuM?;k9v=~SG`xgPrYB=r#_(W
zS07Xls1K*!FREGf
zCH1)avigeps`{Gxy84FtH}y^RE%k)@wt7;1M}1d)PkmqgyZV7Tte#T;p?;`-q<*Y^
zqMlYiRXvbj;eoHE=yR_QkG@8Esy23e3sv;vZ}2b3;r>y)|zL9tguyQ
z)k7d_w3@7DtHqjcwOVaB@TuMEup(Bc)nzTT7U6uSZmY*yVlB0Ltv;(CHh|^U3Tvgc
z${Mf+t<~0$wZ>X&t+Uo!8?2M8jn>K5CTp`bY;Cblu}-zNTBlj3TW45jT4z~jTjyBY
ztXEm*TIX5kTNhZ{tyfzYT05+r)@!U?)@!Yctk+o=TbEduT2X7a6|+XHQ7djGtTAic
zny`}A9&6G{SyNWp%2-+JGAn20t%5ae?X~t#f#x*7ep6*4wNbt(&Zyt+!jZShrfYS+`s7unt%Utvjqct#?{?S?{v$
zw%%>sW4*_^*Ltt@KI{F~ebxu8`>hXJ4_F_v9<&Zw4_P0!K4N{;`q*)KVAP$+{lvT7cd|&ok2qw$c&A-r!sq!iRx5xBp1u=kD^%tCp?nLOy-NRsp))j
zv?iU{7tJM7iC8|752iDPXfh4CFOe@K;_hs0I-jW1ztPcTZZwsMjwMs6M7+Kt%^r!W
zY$}HDVTksHb+F=Aq|_F`cN^zfs*bcPyiCCe^Jfi3~Q&5dJKQTQvmP+{ZlgVr}ow&RZ(4lNDu{W8S&U^CdOy-J&l`mwn
z{`~YvAz4VF$N6crcRC%9=J^}gn~WziQG2|6IyM@cs&N7w-|D@Y)bvy$8qe%YJK?G6
zLZZq}M-jgroX)C!i4i#w59BksLUaV9lgduSys>1CO|G)1Ji2OSF*vx=@1zd-P0x_+
zga%jo?6jd8!doE&c)+On$C4w7Tr|EnXNNGWf#TpY;E$mJg^%`6-q%N!5A^qPph+j8}d%m(&29*OTkaT6AML;+K&A43z3<@1Sx-R5jE9R|#%jmP)asn18$*9>eTU
z=YUW8V{AsSn4}wP0f;dfdLsF5wF#zTBZ-u61TddYSE3!kRyY??opPsccOuGaA!_
zM-VJU(q2%Ae2mRSOQIvwBO|FqU0K+nQ=W7Nt(bCWCx9~{elCWR(S6CWB)=4}TO8H-
zHBO98ttRn_8jud;fqj$ckS^AkFJQt)fkH&k6s9f@Dwk-eNam+iRWTtg?u##HZTb!Enl$4Qn7KbO9jLEr0&suiNvIr
zKd7xHj@k=}XdZE*%DyN7Dw9hV_SeKlM}Z*tZY(zyD39c;?HIYV&Am~k!4T&(x*Jd6
z%OpE{6Ll4-CM_@$D`05ZRbXv{69ew$#)&aw)2URAlgdc76Ui6iwN7|CJ(&h4@sFgY
z6NOBsFi}?wVWQJ%3|b-{E~Ukjc_MDStXLk@2XF%rAJO(761Zq=x{xW4Q+NoL@Y`B?7JWPChP
z$T!%yN7N`0lKS#ghmKSv<}w9MLTl}kPP{4)_7C9#y>13+C}1S>(foek(o{4a+aCum
zOf}?_<7x0qHj#F(=<4W5EY~+TwK!uK6(dQZJEm;o+_Hus*B_G%NU58f$WgV^G{x3m
zcWNx9L^&k2}i6Kjl
zIz6M75hXXkG&w!f;@1cu1ynCyk(i#If+(0Mwmut!K<{+rvT4XhP9O77LY!CggLh8|ShZe$e$Ir`=iKd+HgOVq5Y<1i}Hl5aQ=WCK_n4Q79KphJ~
zCt{=}$UQnQo};uw2&}bJK!h>LnY2>^0usiC&8fCakZ(D4gZ&pWRZzLc6Vbx{Y@$Y!
za9bqv>jueh
zVatMPx7K5-48~IQgshO6j@Rg4Thgd9kvQI@GCCbEa#%>NjDkp1n~;{{?U$#snhA40
ziZ4){NJLGJXADRJbT*GDSVbB=W0H}R;vpl6KqJ@(CeI9r-H_M$Q{ruqm8Y^q|LS-m
zKS|MQDmGf{gsBE+_T>?GxY0-kTCx=f3G-4thD1#TIS%xzwx3a|#gGqm$vP)sG*jJ^
zxoIQ@^>fN;auAx6IGqg_Q;ITLsSt!;s5zsPfnuQvs3En*SPpV$E?rZMLj(^NBU-%m
zq+lh;WJBfwY=HumpxRz#r(kS?cF-Og&lIpEx8FSlB{vb!B5)L7mdhmLH71tS55N8;
zzL-J`appb1d5pdnP)F+Q%O@bvLvr##TYvZfJF*6IDi(h)zt8RA)2$04~Wi03cvX7*WVTiCnGL+Bm^RM+{I}
z29ZHC3Yoko7AIBrLw$r?8%q~ziy>2@DmD$%f!22ekezZG6Ueu#b<7}|Plw4NO(0!U
zCJ?PEj|`MY2FoL>%OgYOku~L!wTNUOsYapwj6(uq;4tfi}qpNKK)}4nl2C
z7odOYbTEuuF`R*HVCW7oYIGW6N&#iZLltTJprS!P0CO<3x5}`nR3e=RvVlQ^@KaOG
zLYdLJLoSnwW}&kbny6J2W6?Me9Xz&>@Q>zzm#fx9*ZXv6Fgjo{U}8hj)h4huI%EP+
z;ms>!P@97Km7;bS&`~r3LUJr+Vve|0t@HA*apj=EBqm~LXgpDXT9UU&x@j`Yz+{sM
zIsp)u@#w0`=s;~TH=Dv3t(p@b2o&>ShRgUs(|`ccRZd{Q2@HBnmJ?s?1cscz8Yi&U
z39NGh>jQSZ>tU*x&Ij!nY#y*S?SmCYf3$q!YV0(yim90M_820Ge3hLw5eL0BZ_?%$
zvcm-=f#@d3U<@(2doi1YG^l*JL=}GX6Ivzkn;?MRXNS^I3zJzN#PN8N`~!hx8quWY
z3tkgQd+}vWE%Y;8#fO1CAA%?Xr6kI=^
zT5#E4ix9+U;8r3}obn-_pH8Ru*Ow7OSYR@fv~M(yHP?>2ufhY?s)v>}<&<@eE!Z|ll
z$l+BcG#VRA*k#asn1b^YO?FNO7Sc4x)F|c+8avz^x>9Hk5Nk|$j0`Sc&_;+cxr(#fNSR9LHU;wc{Vk4m4U_yd6(55hD
zn9{sm52$Ry83i!gNf1Z-gYW`C(;MAy7X;L$oxWs~S6mUZE2fRx?g;o^fdm+Bs5LEv
zi7Q
zQit*KAv+2=GV4Y&MibSb;R!egh=;Xw5XGi}s{qnOdfJ~yUJ3}DsMo^^5gtZ+167bs
zVZzOkt9SuP>_Itz=BCrymEklXKt5k=j#n#0Q!rQ;TWl*eHCJTdTY%__Z)M>Qa@tds
z*y~I<$yXv#V1qpd(A}hGX$!woEgGsZKQMt8U_1j-+HhjUve|r<_G3XgggwOsITBiX
zXq*$b8x$x@p3d4aP%=98AnE~RQ*P}nslo4FNNrJ4-hdO)q?gKDK2Z(vghB%RQ+X#0
z5g%+E3cs;k)s}*A9>5R5S*T(m!*QT^B1lH#%_Sg&5&wD+|d3jNT5x{ykC)3EP-ALAN29`(YUz)fUol
zJfkg$NB}DXSx*&$l}UbM7l0U83ndT`7uyROMI$KYnsGRF_S-?S^q`}JLh^viMNLiR
zE%r3%SQY_lAjl-?FszgsX6rV_PvE#?VdFIA03jd{leS7_s<0zd+DBwZv}ZK#g-MaR
zQ_xYO&@W7ef*%9w6s)J{K>|kb{pryFe}F5-tXrdF*<1$Qn?ja0LQZF4mrc|ZU{xSo
zkSYYoLABK-0qeSdF9iY;Id~yKA!ueP_~<5KLqnjz)ObL{U|fROS7<&a#Yi9V1r-_4
zy+NVl^b_W%f!y;ea#N5ch=UM`aBWM@Qmv&l5Xt=&HNXepRIKxgFZne*(2dtr3^@%49f_qU8}*6>
z5F{AL(^1$y=^TfFwj$Y1#7cx%&d(p11yf_yI%JI7)r0Y<79KHE0pq1gjsO}8`)d)y
zLIuz~IJ5w^jdS707|KWG6886*Q-Dn@#|j#ksZSP`%GFU
z?FBMvgAz811|bzQ7%T-LoyWD6#^k_nQ%Ho$vW;!5?wAye
zURfILXTVjM7byP2?F`mV9zC74DBJrS0UurhBk5zd=)QnJJr3E0a;SS`I+-Fz#yrG>
zK&dp$#S`E@5LC#T;Zw35w6+RKS17~;3gh6j1wEVdWIY#~wW
z1nlnk=vQXHVKM-j18*Y!nTTE>GKvVadREV30Vkp@OK|;yiDt7@^|9o^P!BrbEUq*N
z6z$O@X<^aWlt`qwGv14xfgqa#ozh&ymf!)Z+pMxhqQN?4dFW+P;;y-H>FLtfY)F8Qn>4JK2-
zP#;Ja20I?FvZUcAT5RcB!E+poUtT?1aOM_AYfxE
zKW?J5#pZkO)~A8
z7|S$d*2?laBM)RMea^fw5uZwydSue_T2v@C0xKYLoRna(Qaxj3Q5v#qi@7wwKoTv+
zwNz%2?E3fuj9L#u8iQC#qXT(_bh;{JM%{%BJm;|c!)pv4Oq!|*ZQdAoNc@ow93m6qO1e^v
z@@aK357JGM<_C%)Y77DN2=WZL0sJ00tnz|+2uKXHG*%;MCJ>W#G6s~+5iSMOHYBXy
zOT%)5FdAu!&~-#{3z9UFNq9_QP<?0Q=1n-tb5?sF@v~7As%j#Ivf!=|HxD%XZ_{}smvL)6U
z66+!*%q29R^J=Xa_FG3gwn&q0<%r^cwK3Z0WfMU=T!28ymg*IC_*|3?QD|}#m13fe9UqnBc79oqyG1lGQRv%(OdEp>KSTj#2DBemrc
z>tR7)_a$htf}RZQ8Ql*fl{Z7bIz;6x)|GHY7#4Y;8T%kzr?H5UYa_ApjT#jxKoDSr
z5rJx#ff$m5=L|l$ac$jX^&X?ARckMo7I1R35rYY5Qq_s+92Vg1pTf$5>T%9^I!`R9
zw@tMb$X{bqPR-Rrwq8n@hvgPSAGB9HVX7cW`*VhCzYRGU0`29t8w@Cuu28q#?*>QZ-spv`uJGfZ+_^YJ=lt-c7mW;z+&6aH#0?XV;9W~TixV(H`i#OhoG%l^UnAPsirreb
zxt_!>u3_w2`iS@?_TOy9UY2{XN90B9!`Os<57%O^!4v8Qtk&tA*Ylj`@7}O?iFcECyLZfc
zh4*If`@9c(zv+G2`+M(EU(na?TkhNBJJ)xyZ^Bpbz0r5Aybotb@Rs8i{I=rk)PO63
zU$!`)Q$qM%h`$hat_M+y`CW*2y2A2<=+lYbZ9y&dsJ9LO=IfCN>pJEkJtXhPxBj

zBF2UY~<*yK)hktFDhgLmLwR&b)BS$xY zcQ{k5yVLa(oPxpDEI^zkc%xApzQJ;w@gUwIbS^~AFQY{f{AV1iflQp9s4g zN4^(C8$0DoDDy8`Cf|oL|7wPQVz=aY%T#KQ8I$p0sBB7d#tz8sFq#$LWj zk7ucM1`-Kb9e~(B;(5Hjb2dWi^Joj<&p;TVwhN^Vj7=i-c)jj&EfCis&p-jj2jSva zxgs)E@m?MBJ2afLHNQX}dm6Ue#Bn3cbmFXyA6DcA&~DBT=}AOh3Eoj^3tHgRi2b!S zdK?L(4XA-QL)__hjUlJJjXcpK%s=^G$cdm1QX$R;C_q z+IZ_DVc6jm|ACxRpUT=Tp9621JFjh^SwFJV%_tCS8~GjCp?MLpCWyb{n2Kto6IPr) z5>p8`e*m{UW^UyKQcGh+ii7(U)%%cIYGorJj#TGkNIeFd<%@Q`rhWDZh#QgGrSXxR zl4FK?#o0(LwauYKd-Unth9cP|x+9qK+%(c-wfI@Db_UBKXhp<|ywyaW9s zMFzcfeH3xi&RTqLvHTt4z#o($Uk$mUh?gipw|W%udH4e9KA>A%uTS!9LLBXqzr{%w zPK)|=xE49{eapQ%@{hp->}`*vgR-8--=?rYZ1te4$2 z?k@K__qpy7_g?pP?z`L%xgT>s30dVg?w4>?!q3oiLO5gx^syT~;aG4k>d_xV$Lr8$ zj@d6TkE9#qCzK&NH6D^$5t9s#k!SqsxaEutIma98Dlu=)JDfFr zORuOsCWqfa8^pmA)Q0POPLSgh?>Iq@SKNMr9FMr|1UYVT>j`o!`6zO_Ff#_8OIX+` z?$qyaz6e9)--OJ1S6F=kIqaF?hkV-nRx~1~d_+kjyY+bgCvqz1!NzXUFz21(xQqHZ zIfim_er^W8a%SAoRyfjiNH70BXP^#kcn0Z}UuVlgq6xgEygcD)fe6e=CB(GIziaM9 z`I6jVfqoCCBk0r);2U|7BTql8xk)9(`XtgcM?={KIuw=*!6(A_*9w|Im<-FmA*BLK z#M?D@G0<&z+LcJDz;tm7xPdbspo;Ppa0Bx#%2MwH2k6jmG|2x!m^xbvu=Z1$Gf>uO zmVX6jFyGw?-taS=4zdm>Z`^>>Eq)ATdJ#@;*n^W39>6IEzr=|G5$LBEW2gO{*!lil zXsABy5Ff^_?aQ&3`XSK&XRN=u>)p%Ur@Jo&^}pGDKWP6`?%#Vno_5a~&opXCetBG6Sg`p)!S=-cf} z`7ZZe<-5^$(08xzL2*CMWg{P;yg*E(R6~wwBy4J`l-doAqn`RS%5$uF19U)Bq{Vnv z=oUJi)C1H@HUZ*UFCocjNBkNo{9J1G5K3xDK^`?sQeg4`r*_t3xXtZ=NyGhEOF8yc zv_rfH<%;wh^-$8Cr{|IKz3DNymwZs?wBnZ%0KuvO|hw2L&FW5gEO z?L^}hjSb{v18ZfCXS}p&9UydUAH`~c~I4Z>FZXDOcx!?`2W)8^X7C7Ug z-j5h<5yV6Gl{~_cXd~jah*75@Mjs<(s4pRp-{M$sPDrmfOCh9mtDE$@>M|JVNzC{XVI6k&3z=DZXc5 zi5Oyf7gldYo-+fKyGSefJ+UU|P!YCf6oxbIh;Ob(%hMY}s!k zG4w>-qw6Wb4c|d2@$O^NwM+rqVKv&yIxA_&XLS7rGYt(SUUrJ}kzN|Z zqNXZ7j&!G;5q%~lXFsegoo-}Ug9ipG-i-9)P&qXVK3KvE14Cig)kt+plj~_sTu@VF z?+7`Bbk0yjyb7tM782VDY46fCl*Wa%b-H*js#CAYBMo?q=DKEl2-h6H4 z7B?Q1(kz4nt{V9hJm>HQ^)94on8ACtTq@2(TB((WAHNA{rPdgV`c}=yIRgfoBjWu! z#q^r8e;Bt}_=;_-LMwdY!Lk$|QoQ1UvJ~E{;T8{-rD**{T!a)u&8P{G^1Kl#PKz{c z68AwuD5rSW+HsB^S}CWr#9*w3E$WLpMmr6q_s=56kuwl{m(H_iL>|>KN`inDagHrf z5K}*`!+JK9)M0HWl4*qPk(QU)E>wuaDterN7~vqm?*^XgAIZ zx&`O?JchG(o`<2o5$D=$!dWzNoF#J&wAlAqAG02_p0J*REb|-rX|#aUnxjE%Fj}Co zZq_1htblL%7p2(#PT7Z2BE)Urier@G}*-I8PLqFuh$F7q(T z{PVWA%C~?@lYRPf)bQr$XmNn!dNlq)wh4;S%JN2U2e*-txOfPSdnYT&(jWD68dsa)6LQb{9^8GEPsR)@G2yGM!4Bs09F zeOT%P74;BSXuk=IQ;<$brM6_GA4*4Ixd?Rom=PA&Ahpy7zCq}{PM2l7%sdh=7O49) zg2F0;La2#ykHvAhOploM$?vo%n~JM=V^*Q1EB z^^~_F%A-r#^Q83YpePd-mul(aB|{a zoMiYB@lBi;_&1#C*Ne0FcH*48t8lK}gRtB^iSy*1moGzi3qnTadHiNx2sMsmXC%@R z9pZdqq~*ua&hql46XY6%hVBwHh|eHpG2)cEsjsqTYC?oFLZ(|h{4Yvak~#tB%g8Q4 zgtZKaUu~ffpT%?THz-#)UwGu7=38iE@R;{G%Y;<6@5H&5cob#kej9U(p0>%0kw>mT zd?NKBRdBShrARZ{1?f?Rjwf@#6xM01kwzMV=BR3Px~b366kxfP`8nlDVKCC_Q%L8S zla53%E7YYRW6A4*8Kv}_u@Y3p@2Io_fzp*eb?6GoHIVpYlBE`HffN2AS2~ZQeMn z6z}jp1RKTE-j{q;uurV@9*}n^Pla%*q_D)>$myu z^FQqWhX05D=ln0>YRX{M{HjG&%f&K`7iW=uCj4*&IgW&5GTvq$8>`q zrX2G-al$fXNvAf$$BqBDysox6b(Qy_sM)yop_GyF4echCHy&?#XY4T-PiUy54raJA zX&hsu8r1*5(F7s)L~ZALfNepDD=$;tCUQd7vwTh&`V+MMNw~0 zsK<<|gI~wgqe3V_Uc;J5bIG|X=rl^uR#jPo6qet|NXTjA8~8R-lCkQNPcCMS;%j)@ zjFcHSY7Gv}U!)EoY#^0hBw}h%?(bX|_zx$|aL=>USnxAnWF`C<-X z)>!htj3*(5{iSz_^)zGbiTR!KZFmyu%$ReAjYnHcIpV8$r@Tish})c4Y`61RuaT~c zb~~K0&apMLW=@{qnwRp~wP#czx>@0XiH&bO9pZMaO6t4;+g;c;$ zIcOZ>3v&YVs$E}zL|E2;xeT*c zgduSuVns?Is9BZ9;BAl#`6m7DPa@3L0E)!-AtM@`{k#qVTU0GVw9ZjW>&JS`-LA`B zH@ohGR`mqdC%hYLUi~hdL7&CB^4GflOJf`L8OIt#KC_skr@o?Lnlgj& z9FUWOQ>#xSoz%x@tj4NBF7qcq6Vtd7hZ;QjMZ0RzspLPTS&MXkn&m9!-^fHvrIj({ z`aROPnu8LC-3Hv@L8~IyjF4Vp4UG7Aox(Sar=n9lh4C$wX;n>#Q=VYGTp#k7juXbU zEFif0#L!bhe8}}8;^w=CLp5t@oH=3jZiG4Gv^)&zFl~hq@dLXIsEGWc4(lFbeJ(wZ zkbKo0&~7O~G~ud+?YKGO7FhJY2H))~@J4pwj)AR^>*F}<{~Dav|32utkK$bYC&dpH zam?6fi!g$okakczLQ7OTxEsGl>x6+%jv4c5%ji~|gJbg@`+)I&bA?BF{krG4k%Kgv zd4V#0h;`nK9F75Ji**;*E{YhSft;4!9W(0GlIMvZVcy9Xj0VQJrUbfG<9)@~#g8$g zrljp{!!3rKLBlKYmTE)1%pWFp+xhl4)mITWwoJ}(t!uTNZ>W5|_z%R*7Yq#j5Lo8K zTjg@hpXPS7C0Pf#OGFGJzr05V@>xII-6~e;)|Gn9 z)cN?Q^%q5{>qOtb5qafbraWu+Sakuy>2;&p<;Wc8gBeHnPvVp23|d-MD`w zj#X`U!7h-{@X@X7A$K69(TIlkz`+6#2)l)ZVA#&L-$)n z3N_Bnrw&B83X3GhYHlw17+NnTHTA0;Il^nL7y<37cuRc^w1yaA@VON?*W3#&;qPJ* z?uto+GCYhsQI6s+l2hQvxfb_ud__Ksn=RU5&AAAx5O2dx4Bu4G;huwb+&!=pcLLmi z)A}E?erWv>YY-Q?H@Yu`Rp%P_-R@7hpK$-u{Wt8D==N;%Z1+rfuJqi3RVa^mp71>5 zdCBX8b!WME*n7Trw>RsZ@!o=UC?D~D9(yL9@;-}Q6R-FrE?jJsYeAKag^yah8Bt;{ zS7P#)qi$$8DMMIp0xXrvlm8eE)U10nSVEeB_vA?$pE#4O+w7WPeNI_wFr1-3;;+)m z$S;xul&y4>XJRf&UpAImQar=E%ojIQl;Ii{QaJMdB877Op;*SIStwzAV8xY?>K>h< z@di*uiQIU`$~j*eWeB6}1^FUpoRCOLL>$9vGkLBa(Q^CgoV6Wtuvmg)#8w)~swip8 zvnomybr;<)y<0$@T~UG~YI;l=K~r+n!4^3)QL2Ml0$MNEpj;`R<;c0IZ=eL)Se|R> zG%t<7-10H3^kHqSc$ajS?IKT{hwrXO3rlV0 zs#-#mmJ_7Wg-iYAd)&POX+pN+Jxz@Ir!pkIAzk0ITu8RIIZEZkU2cb^>$Ds)Av%HhL88# zIb0D|!N&k4&O5A+5KkQEh>(v3(L;^_@ujHgyPneTkdGLcF(b^DZ>gxw$fl$i5pf!F zSg*lfVxO6ZFl1nA$&8h^x>cTDOlQjo9X+Omka2sAB3#yq!aV> zDp5I%)Kcw)T3C$bP(>=o0$f>SkXnuh8pb78k(6p64TJqno>7r%TQ%i5kgQ8(Nj<}I zbw!$9pC$(?(l`_I<(rX4OeZyUtN_2&<+x&&5Yb7^O>zGLzhPjD(rByrO+`6vg%SU$ z>!Yp5%sorqj5J3srAHUKgm^}CPcvV722~GINU`dHS;nfj4E(eWBg)ak$ckU3)e;>umW~Et){)p0UOW#OjTnXA zh+pckZiQNmu#KnUS*R-O^W_+B0lN(=RzHthwtgvJ!M#^)xVLH}?w{HTFHj1S z!wl|%x()X|EyvgzYR6s>3pldII#0_2>C$3x1ZkWr&Sr^garO;%kV@Zv5zW11su7x?Urjadyq_%*!>NbL7|texXyW*J-Ag z`10lcBw<2!p(X>Ha z25dD{9{gG4Z2Tm=ZqQ*p%JOj=|3a=WA*4MQ@+O^z-pIWO5x%?RdC;~%gXN8QViz5D z5h>lC`M}`a@Z)~W^#j+-xLI;FZh1^&HTnJ6Q}i>ePpHT3foICyxU=sD+_m=*?#%lJ z?!o&8W`KN#HX!z@1hb?|q_Uh3jGFxVKP_33w!-qOD1YKQ?NO6o!&siUlpaZZlTdl0 z)=|rTQO^oTkW#d zmhsM(t-Fw4(Rs&hnfwIuD!*y8EZTVgfIM32%G*mQr0@AJx=u3f7UtWy{F{LnSSY*9BMrL<}1Ksqm#32NMmRx zT}Z)A-ytOqMJ&X~E-)Ja~VF@iWu3Lkd80~ldeO<;$M_`T+4 zPT%%xnCIIs1G4J?0pxtkal>ldb*1YTwGuPV-Wv-ce<|y-#q4v3dseUUPQksX@5B9~ zf5-aAbAe~~VCTjQxa)Bp?p3@7>mHv_FXGleu7J1__uS1|&$wOgh7y+^QH z<0VfR>mSbbq&(L_N1pXO?Rmvp?_G)24`a}c@A7`i`=s|b(1_>z2Cy^ZQk)@hgT40Q z$G#VQNB#5sUH;Ynt^N!Baeu*omH!t1J^lx={^4=|ll~uL1;iiyf3Nb$X7F=|e^Tz% zz9fti$B)oMosQ7Pr}mS}7HHr9`VC|E2Su0j|5xRUYvAPD?ed%p&L`Iv5ras1xh{ci z<1A1H!yK!1C{NnXlMOi2gi2!pAlEavPubMwP3k?!X9)+!489YKDpR%erqVEPEk_utN_N!%N6-BQjKT2MQui{#3J_GzyfuAjweqgAh#op*@|Dz z^Rc}sYn%KmN;y)D(S`|86?z`VFH@|GSZCyVBh9eioC8oyb+WGOU(B6atKqsB8eI%~; zBcBjM2r%@Kk}==Vd`8|jW%;G|&D=mwksn4qtj){|aj=YHyFP&2W4}%* zk~V=S^t%oxoC9O>I~6t0$yK!ZIQ=#nAfdsYNkw_v$t~*EdQ2j~i2IOxY^xmE;)BSg z49Y$l4+Uu%u@W3s{2kA^bjL_rAy=Iu|Kht`LB)A9oYkyjZIz!vE$q9IqbWBVOC2

>ZEe9(2a)H%nxT*EDQjsKd0mH@Uqd-)!=S}EB=(l&mFCXKxwJ)_I!pL# z>-plp?e|Q-nP0R>$y>|nbSN?LwFQ<)?!k4&wNn1%9^6t6A=Bu5A;~?s|EIBYkFl$$ z)&LkKaIKL7#!e&#oG@407hDZtN%_-k0jRcT>$dsb$}jCQm!c^7R)_l~RwdTy^W zp59D%XR@VO6`QH;shlZWDz65s9qpGePtd%6j0R6-(7%+RS!&r>7FD3pTJhvs(4Wx- zaV2R-zgsp;MH;*71j{%}>*^Or&juc_^*_$NA1yihgV#k#{$)XO=5M4J_isTuNA;(>r^jhVx_G;aHBQLw+~HX?iFBz*>y>gR%;9 z{ULuqZ!LS+zZl<=JPD`E$1-U#TJW57t9x6|Y0do8dQQ5}U#Mr+2Dy4)sw2tg_vzVo zIh+1>(MA<7hZo`#b9_=yjJBzf5@}sk+#xBkS2v)1Pv0S<(;`xxQX0OG>#T3G_S>bc zk5(5wk0-xDxWY!_AAFLj2^lSGJe-lr(W+*t8)w^jgpu$$wW6)Q}vY>6f|3_3PSiX$NUYdq^i~7wH`RBK3+%>-;Mk2{g{Au?5-7 zfUls-6!Fs7z$F~=YvHyPK~;^TSXS=8X}z=LpXoUET6c1ix0`%OC# z(PG3RqF&@Q|Ek`OG~SjKqrRpVS4aG&Xs>#(oH%rQ>ug|nD%OEUY>%|H^QT%duRZubPQDqGf*uQn0S z3nSG06|;N6+i#+$45y;Aqa`32LBy%ey~>v>uBgDsfO}3b?zTZ*5w9e=yIL}}+_txP zN)U7+lm#_U4d0P0LN2C^*>F~TM7k&A|_^P*hM z^V*l*=xJ29*yI?OtYvpYYzOCRrNaDJhHPQw&rj`4V!a@lVzU@4&W#c_(sejH$<>*~ zFQZ(=;=WEd<;<6)dD^|E(>C+vVL$H2!z9U;72%$+M`sGBPdY=tQU8E$LpVHnST`YT zkYD|)$#3XX!Kpo~dbagE+w%u`)Wh0uUNz+|oh5jA%HiH=I!Cay_j=uJu&Z~x_pQE0 z-^qQ8`quZ|rIQ1D`~K4RcK@{gllvF;ukF91|B?PZ{p0=r8t5OGJ#gy4;(@gTw+!4n z@WjC0f$@PiYQA=CZBA`|ZCP!7ZENk`+K+2{YA@IRUVE$FQ$MzTVtrnHLH*MDy87n& zw)*z^;~q^NbxJ7VB^Uy`6zyiVhtarMUn|&VT?_5VhY@&vEW;a(zSTdIgRA#D~<1l z)YFQE3-tezPaD^vj8&8LtT2@GkZL2fn4Rs6Vn5hyp(Esbd++PPQf+oaz($tE6KcE3 zs$Cv;+XHSXj*lY;*aF{!g)9RZ>il58-^#RF%Ib#3REh)rmW- z8WA=Ez8NwZF2_>BavF7ZLar^tyK%^q8XxNh=HNuu%IE@%6JBkEFRD&;OuSo^GRk=W z$5^L2Kg!U;(DaP3v`at9rc6>`R@kjNrAVtrh4ev>V^^5pe~s$E*X%f8&1U{8MQsAP za%JrRwup$)lUYBbT4Q*sbB#?RB#TINC0ksr%5>F1Px=mfe{C()`5kuBPnEO>16@wJ zX(Y|!J(ZX7Ug3hk33u$SR<4j*AgpuVOc@LH*dNq`R5p7a+%=V@iZ{s(v8;Wxe)WI6%w|}mvMN@S^!E>_Y)rM-?KN6$ zW#cJ3qKcPRyvg^-7b)WtsS)*;e^4d4#|~e~|4k*xV6d8K~4pzI;zF<4vyuVJ;Ind`tU_&Fi$i*;EcIM{rIhPx?3`5xKm ziMEOZg7U5QUJ~DZUr?w!+ddXed05c=HCB$AO}|o5xYnd`_QOJgGFHa3J z3Ubq$a2yuIeoAsQTp^2}S*BCm#Ui~EgR&jYhQCkm=GB9%sqSXImpIyO(mQLB?`E8$ zce5_cv;Mt&IK}{n&L}R>8(LymIWLWHZuIZyS2g?R0VmEA38G4CvLP<8aXe)$`vCq!=q_JwQ=KqxEYWt$2 z{vk;<)Acq3q-l&2gzF{Msu%(XF9~jZjYNWGuzW||*&T2^4`Q3mus+HJ!($tNoG&cn zk3NRNqvBYkE_4n~rT=|hFvEz+Wbu>lYrz;RX8{_1O5}Fd5xI))X!?%`)~1X&)^LMh z6)c8pOZDgx&-iOm3gmHX=6+BRv=!1S%8u~WScmmN)+uJ@xlg4%TE&sVvn>d%w06E% zoNX9n91WcU$nWgVH~l4gXVgdzc~gEs@9jQ&{IXadPZ60s%iSV=Z{gAr(MfXF*Wz=u zA^a*mrxsy@p4q36d~n|o&yq#i^sDtmjhTK(PuSCqaCIz|{0v_gCoB6bT&chMn!8P> zoIV!6Jf>`z#ma+P=Wh}XSS|o-Z`Oro=r>p3&@>T`gTg!b%(?uM@Zl%wvu(HjR^Nwi zhaGprG{uV_S60d?ikGwK!V8=6x&?l_rZs z-^GVPPnrIXc9G}2KAdD)7R$^y4oMcHyY5^1oBJRp&RR1Euq%5id{fW(O6&!)+Ysqi zHOx-FRZqEpFhNW`QE9OxmPLC>Pe5C{iQ(o_JG1=E!V}Mj8&$^KE$7K3mE~%Tcy1!j z-($YSqTtn0-dBq;yI&d6uesOV zVcoULw~UX_%kV?xzEyvWD48=mNMp0O7!zJe4G7> zu~Lv&SjDC%(IPDj`)R62Z{)$iwTc;d&QeEe6Be-ZM*Va_sKpIeSfpdhQA7*1a}z(R zcN-P-KdcN`w3z5f|KOk#^^RmuCrRAx(m82dA1qT=HG>^v_7I3>ErC zp>VfsBJ;tpW|3>iw5%;mvY0#!8tw7LDgJUv5%XZP_XY(BC*ixQ(NPk(OFBbXIj@m$ zLs_Bv)AjvhC3X8X?-N9WPEdYN>6Grjr*!R=o7owfq*{;YU->^;76-4ndsF=T>W}uN Hzx)3OGG_Ot diff --git a/esphome/dashboard/static/fonts/MaterialIcons-Regular.ttf b/esphome/dashboard/static/fonts/MaterialIcons-Regular.ttf deleted file mode 100644 index 7015564ad166a3e9d88c82f17829f0cc01ebe29a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 128180 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 ohyj3!`#ee%L;DTjx@8!5k5~VH0QmdE^#A|> 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