From 46356cbc4afbb2e38380e4702919927d40ea2192 Mon Sep 17 00:00:00 2001 From: buxtronix Date: Tue, 23 Mar 2021 16:21:04 +1100 Subject: [PATCH 01/62] Fix BLE UUID matching (#1637) Co-authored-by: Ben Buxton --- esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index a8185a8c67..b2d303da15 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -241,7 +241,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const { } bool ESPBTUUID::contains(uint8_t data1, uint8_t data2) const { if (this->uuid_.len == ESP_UUID_LEN_16) { - return (this->uuid_.uuid.uuid16 >> 8) == data2 || (this->uuid_.uuid.uuid16 & 0xFF) == data1; + return (this->uuid_.uuid.uuid16 >> 8) == data2 && (this->uuid_.uuid.uuid16 & 0xFF) == data1; } else if (this->uuid_.len == ESP_UUID_LEN_32) { for (uint8_t i = 0; i < 3; i++) { bool a = ((this->uuid_.uuid.uuid32 >> i * 8) & 0xFF) == data1; From a6255c31feaa511896cbcc00326028080d22b906 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diego=20Elio=20Petten=C3=B2?= Date: Thu, 25 Mar 2021 18:33:06 +0000 Subject: [PATCH 02/62] Add optional bindkey support for CGG1. (#1407) --- esphome/components/xiaomi_ble/xiaomi_ble.cpp | 7 +++++-- esphome/components/xiaomi_cgg1/sensor.py | 4 ++++ esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp | 18 ++++++++++++++++-- esphome/components/xiaomi_cgg1/xiaomi_cgg1.h | 2 ++ 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/esphome/components/xiaomi_ble/xiaomi_ble.cpp b/esphome/components/xiaomi_ble/xiaomi_ble.cpp index 033269f66c..29a34be81c 100644 --- a/esphome/components/xiaomi_ble/xiaomi_ble.cpp +++ b/esphome/components/xiaomi_ble/xiaomi_ble.cpp @@ -103,7 +103,7 @@ bool parse_xiaomi_message(const std::vector &message, XiaomiParseResult return false; } - while (payload_length > 0) { + while (payload_length > 3) { if (payload[payload_offset + 1] != 0x10) { ESP_LOGVV(TAG, "parse_xiaomi_message(): fixed byte not found, stop parsing residual data."); break; @@ -171,7 +171,10 @@ optional parse_xiaomi_header(const esp32_ble_tracker::Service result.type = XiaomiParseResult::TYPE_MUE4094RT; result.name = "MUE4094RT"; result.raw_offset -= 6; - } else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // round body, e-ink display + } else if ((raw[2] == 0x47) && (raw[3] == 0x03)) { // ClearGrass-branded, round body, e-ink display + result.type = XiaomiParseResult::TYPE_CGG1; + result.name = "CGG1"; + } else if ((raw[2] == 0x48) && (raw[3] == 0x0B)) { // Qingping-branded, round body, e-ink display — with bindkeys result.type = XiaomiParseResult::TYPE_CGG1; result.name = "CGG1"; } else if ((raw[2] == 0xbc) && (raw[3] == 0x03)) { // VegTrug Grow Care Garden diff --git a/esphome/components/xiaomi_cgg1/sensor.py b/esphome/components/xiaomi_cgg1/sensor.py index 6201df61b8..cedf04d8a6 100644 --- a/esphome/components/xiaomi_cgg1/sensor.py +++ b/esphome/components/xiaomi_cgg1/sensor.py @@ -3,6 +3,7 @@ import esphome.config_validation as cv from esphome.components import sensor, esp32_ble_tracker from esphome.const import ( CONF_BATTERY_LEVEL, + CONF_BINDKEY, CONF_HUMIDITY, CONF_MAC_ADDRESS, CONF_TEMPERATURE, @@ -27,6 +28,7 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(XiaomiCGG1), + cv.Optional(CONF_BINDKEY): cv.bind_key, cv.Required(CONF_MAC_ADDRESS): cv.mac_address, cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE @@ -50,6 +52,8 @@ def to_code(config): yield esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if CONF_BINDKEY in config: + cg.add(var.set_bindkey(config[CONF_BINDKEY])) if CONF_TEMPERATURE in config: sens = yield sensor.new_sensor(config[CONF_TEMPERATURE]) diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp index a7c94fafad..86725956e0 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.cpp @@ -10,6 +10,7 @@ static const char *TAG = "xiaomi_cgg1"; void XiaomiCGG1::dump_config() { ESP_LOGCONFIG(TAG, "Xiaomi CGG1"); + ESP_LOGCONFIG(TAG, " Bindkey: %s", hexencode(this->bindkey_, 16).c_str()); LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -31,8 +32,9 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { if (res->is_duplicate) { continue; } - if (res->has_encryption) { - ESP_LOGVV(TAG, "parse_device(): payload decryption is currently not supported on this device."); + if (res->has_encryption && + (!(xiaomi_ble::decrypt_xiaomi_payload(const_cast &>(service_data.data), this->bindkey_, + this->address_)))) { continue; } if (!(xiaomi_ble::parse_xiaomi_message(service_data.data, *res))) { @@ -57,6 +59,18 @@ bool XiaomiCGG1::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { return true; } +void XiaomiCGG1::set_bindkey(const std::string &bindkey) { + memset(bindkey_, 0, 16); + if (bindkey.size() != 32) { + return; + } + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(bindkey.c_str()[i * 2]), 2); + bindkey_[i] = std::strtoul(temp, NULL, 16); + } +} + } // namespace xiaomi_cgg1 } // namespace esphome diff --git a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h index 57f883405c..e1d812e929 100644 --- a/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h +++ b/esphome/components/xiaomi_cgg1/xiaomi_cgg1.h @@ -13,6 +13,7 @@ namespace xiaomi_cgg1 { class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListener { public: void set_address(uint64_t address) { address_ = address; } + void set_bindkey(const std::string &bindkey); bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; @@ -24,6 +25,7 @@ class XiaomiCGG1 : public Component, public esp32_ble_tracker::ESPBTDeviceListen protected: uint64_t address_; + uint8_t bindkey_[16]; sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; sensor::Sensor *battery_level_{nullptr}; From 13e0d6b9a1e0e5335bf416b46d333720994c1ed5 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 26 Mar 2021 11:39:29 -0700 Subject: [PATCH 03/62] Update FUNDING.yml --- .github/FUNDING.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 52ac3648b0..864586fe6b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,8 +1,3 @@ # These are supported funding model platforms -github: -patreon: ottowinter -open_collective: -ko_fi: -tidelift: -custom: https://esphome.io/guides/supporters.html +custom: https://www.nabucasa.com From 2028362fd5d743dd03eb37ca9583a4e06f439f6a Mon Sep 17 00:00:00 2001 From: SenexCrenshaw <35600301+SenexCrenshaw@users.noreply.github.com> Date: Fri, 26 Mar 2021 18:01:37 -0400 Subject: [PATCH 04/62] Buffer allocation and TRUEFALSE templates (#1644) --- esphome/core/helpers.h | 15 +++++++++++++++ esphome/core/log.h | 1 + 2 files changed, 16 insertions(+) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 63706e8a19..5f9ab1fdd1 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -319,3 +319,18 @@ template class Parented { uint32_t fnv1_hash(const std::string &str); } // namespace esphome + +template T *new_buffer(size_t length) { + T *buffer; +#ifdef ARDUINO_ARCH_ESP32 + if (psramFound()) { + buffer = (T *) ps_malloc(length); + } else { + buffer = new T[length]; + } +#else + buffer = new T[length]; +#endif + + return buffer; +} // namespace esphome diff --git a/esphome/core/log.h b/esphome/core/log.h index 361fbe1182..0eec28101f 100644 --- a/esphome/core/log.h +++ b/esphome/core/log.h @@ -160,5 +160,6 @@ int esp_idf_log_vprintf_(const char *format, va_list args); // NOLINT ((byte) &0x08 ? '1' : '0'), ((byte) &0x04 ? '1' : '0'), ((byte) &0x02 ? '1' : '0'), ((byte) &0x01 ? '1' : '0') #define YESNO(b) ((b) ? "YES" : "NO") #define ONOFF(b) ((b) ? "ON" : "OFF") +#define TRUEFALSE(b) ((b) ? "TRUE" : "FALSE") } // namespace esphome From ad76312f6678da0c542147e31f9dc0a81e8dcc34 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Sun, 28 Mar 2021 22:35:39 -0300 Subject: [PATCH 05/62] fix servo not reattaching with same target (#1649) --- esphome/components/servo/servo.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/servo/servo.cpp b/esphome/components/servo/servo.cpp index 6935c34653..57baf4aecf 100644 --- a/esphome/components/servo/servo.cpp +++ b/esphome/components/servo/servo.cpp @@ -52,6 +52,8 @@ void Servo::loop() { void Servo::write(float value) { value = clamp(value, -1.0f, 1.0f); + if (this->target_value_ == value) + this->internal_write(value); this->target_value_ = value; this->source_value_ = this->current_value_; this->state_ = STATE_ATTACHED; From 9e23987db896ecadf67ce3467ebf7e3f7084a5cb Mon Sep 17 00:00:00 2001 From: Andreas Hergert <36455093+andreashergert1984@users.noreply.github.com> Date: Mon, 29 Mar 2021 21:50:30 +0200 Subject: [PATCH 06/62] Add I2CMultiplexer in generel and the TCA9548A in special (#1410) * Added I2CMultiplexer in generel and the TCA9548A in special * cleanup * tidy * tidy * tidy * tidy * Update CODEOWNERS * Update CODEOWNERS * added CODEOWNERS * Fix CODEOWNERS * protected function * fixed scan * fixed style * added to test1.yaml * Update esphome/components/tca9548a/__init__.py * Update esphome/components/i2c/__init__.py Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/i2c/i2c.cpp Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> * Update esphome/components/i2c/__init__.py * Update esphome/components/i2c/__init__.py Co-authored-by: Guillermo Ruffino * Update esphome/components/i2c/i2c.cpp Co-authored-by: Guillermo Ruffino * added define statements for I2C Multiplexer * fix * try to tidy * bug fix * tidy * override fix * only change channel if different * tidy * added test * testfix * added defines * tidy * fix dep * like recommended Co-authored-by: Andreas Hergert Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Guillermo Ruffino --- CODEOWNERS | 1 + esphome/components/i2c/__init__.py | 16 +++++++++ esphome/components/i2c/i2c.cpp | 39 ++++++++++++++++++++++ esphome/components/i2c/i2c.h | 20 ++++++++++-- esphome/components/tca9548a/__init__.py | 30 +++++++++++++++++ esphome/components/tca9548a/tca9548a.cpp | 41 ++++++++++++++++++++++++ esphome/components/tca9548a/tca9548a.h | 22 +++++++++++++ tests/test1.yaml | 12 +++++++ 8 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 esphome/components/tca9548a/__init__.py create mode 100644 esphome/components/tca9548a/tca9548a.cpp create mode 100644 esphome/components/tca9548a/tca9548a.h diff --git a/CODEOWNERS b/CODEOWNERS index 0a1f2b3ed2..a726f85cf1 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -95,6 +95,7 @@ esphome/components/st7789v/* @kbx81 esphome/components/substitutions/* @esphome/core esphome/components/sun/* @OttoWinter esphome/components/switch/* @esphome/core +esphome/components/tca9548a/* @andreashergert1984 esphome/components/tcl112/* @glmnet esphome/components/teleinfo/* @0hax esphome/components/thermostat/* @kbx81 diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 1446835904..59f90842e1 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import pins from esphome.const import ( + CONF_CHANNEL, CONF_FREQUENCY, CONF_ID, CONF_SCAN, @@ -9,6 +10,7 @@ from esphome.const import ( CONF_SDA, CONF_ADDRESS, CONF_I2C_ID, + CONF_MULTIPLEXER, ) from esphome.core import coroutine, coroutine_with_priority @@ -16,6 +18,7 @@ CODEOWNERS = ["@esphome/core"] i2c_ns = cg.esphome_ns.namespace("i2c") I2CComponent = i2c_ns.class_("I2CComponent", cg.Component) I2CDevice = i2c_ns.class_("I2CDevice") +I2CMultiplexer = i2c_ns.class_("I2CMultiplexer", I2CDevice) MULTI_CONF = True CONFIG_SCHEMA = cv.Schema( @@ -30,6 +33,13 @@ CONFIG_SCHEMA = cv.Schema( } ).extend(cv.COMPONENT_SCHEMA) +I2CMULTIPLEXER_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(I2CMultiplexer), + cv.Required(CONF_CHANNEL): cv.uint8_t, + } +) + @coroutine_with_priority(1.0) def to_code(config): @@ -53,6 +63,7 @@ def i2c_device_schema(default_address): """ schema = { cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CComponent), + cv.Optional(CONF_MULTIPLEXER): I2CMULTIPLEXER_SCHEMA, } if default_address is None: schema[cv.Required(CONF_ADDRESS)] = cv.i2c_address @@ -72,3 +83,8 @@ def register_i2c_device(var, config): parent = yield cg.get_variable(config[CONF_I2C_ID]) cg.add(var.set_i2c_parent(parent)) cg.add(var.set_i2c_address(config[CONF_ADDRESS])) + if CONF_MULTIPLEXER in config: + multiplexer = yield cg.get_variable(config[CONF_MULTIPLEXER][CONF_ID]) + cg.add( + var.set_i2c_multiplexer(multiplexer, config[CONF_MULTIPLEXER][CONF_CHANNEL]) + ) diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index 8e6d9f32fa..e9a7e72932 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -178,28 +178,67 @@ bool I2CComponent::write_byte_16(uint8_t address, uint8_t a_register, uint16_t d } void I2CDevice::set_i2c_address(uint8_t address) { this->address_ = address; } +#ifdef USE_I2C_MULTIPLEXER +void I2CDevice::set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel) { + ESP_LOGVV(TAG, " Setting Multiplexer %p for channel %d", multiplexer, channel); + this->multiplexer_ = multiplexer; + this->channel_ = channel; +} + +void I2CDevice::check_multiplexer_() { + if (this->multiplexer_ != nullptr) { + ESP_LOGVV(TAG, "Multiplexer setting channel to %d", this->channel_); + this->multiplexer_->set_channel(this->channel_); + } +} +#endif + bool I2CDevice::read_bytes(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_bytes(this->address_, a_register, data, len, conversion); } bool I2CDevice::read_byte(uint8_t a_register, uint8_t *data, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_byte(this->address_, a_register, data, conversion); } bool I2CDevice::write_bytes(uint8_t a_register, const uint8_t *data, uint8_t len) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_bytes(this->address_, a_register, data, len); } bool I2CDevice::write_byte(uint8_t a_register, uint8_t data) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_byte(this->address_, a_register, data); } bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_bytes_16(this->address_, a_register, data, len, conversion); } bool I2CDevice::read_byte_16(uint8_t a_register, uint16_t *data, uint32_t conversion) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->read_byte_16(this->address_, a_register, data, conversion); } bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_bytes_16(this->address_, a_register, data, len); } bool I2CDevice::write_byte_16(uint8_t a_register, uint16_t data) { // NOLINT +#ifdef USE_I2C_MULTIPLEXER + this->check_multiplexer_(); +#endif return this->parent_->write_byte_16(this->address_, a_register, data); } void I2CDevice::set_i2c_parent(I2CComponent *parent) { this->parent_ = parent; } diff --git a/esphome/components/i2c/i2c.h b/esphome/components/i2c/i2c.h index 72777f8eb0..56da64c218 100644 --- a/esphome/components/i2c/i2c.h +++ b/esphome/components/i2c/i2c.h @@ -1,6 +1,7 @@ #pragma once #include +#include "esphome/core/defines.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" @@ -135,7 +136,7 @@ extern uint8_t next_i2c_bus_num_; #endif class I2CDevice; - +class I2CMultiplexer; class I2CRegister { public: I2CRegister(I2CDevice *parent, uint8_t a_register) : parent_(parent), register_(a_register) {} @@ -167,7 +168,10 @@ class I2CDevice { /// Manually set the i2c address of this device. void set_i2c_address(uint8_t address); - +#ifdef USE_I2C_MULTIPLEXER + /// Manually set the i2c multiplexer of this device. + void set_i2c_multiplexer(I2CMultiplexer *multiplexer, uint8_t channel); +#endif /// Manually set the parent i2c bus for this device. void set_i2c_parent(I2CComponent *parent); @@ -280,9 +284,19 @@ class I2CDevice { bool write_byte_16(uint8_t a_register, uint16_t data); protected: + // Checks for multiplexer set and set channel + void check_multiplexer_(); uint8_t address_{0x00}; I2CComponent *parent_{nullptr}; +#ifdef USE_I2C_MULTIPLEXER + I2CMultiplexer *multiplexer_{nullptr}; + uint8_t channel_; +#endif +}; +class I2CMultiplexer : public I2CDevice { + public: + I2CMultiplexer() = default; + virtual void set_channel(uint8_t channelno); }; - } // namespace i2c } // namespace esphome diff --git a/esphome/components/tca9548a/__init__.py b/esphome/components/tca9548a/__init__.py new file mode 100644 index 0000000000..aedd751086 --- /dev/null +++ b/esphome/components/tca9548a/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID, CONF_SCAN + +CODEOWNERS = ["@andreashergert1984"] + +DEPENDENCIES = ["i2c"] + +tca9548a_ns = cg.esphome_ns.namespace("tca9548a") +TCA9548AComponent = tca9548a_ns.class_( + "TCA9548AComponent", cg.PollingComponent, i2c.I2CMultiplexer +) + +MULTI_CONF = True + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(TCA9548AComponent), + cv.Optional(CONF_SCAN, default=True): cv.boolean, + } +).extend(i2c.i2c_device_schema(0x70)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + cg.add_define("USE_I2C_MULTIPLEXER") + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + cg.add(var.set_scan(config[CONF_SCAN])) diff --git a/esphome/components/tca9548a/tca9548a.cpp b/esphome/components/tca9548a/tca9548a.cpp new file mode 100644 index 0000000000..0df60d6dd2 --- /dev/null +++ b/esphome/components/tca9548a/tca9548a.cpp @@ -0,0 +1,41 @@ +#include "tca9548a.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace tca9548a { + +static const char *TAG = "tca9548a"; + +void TCA9548AComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up TCA9548A..."); + uint8_t status = 0; + if (!this->read_byte(0x00, &status)) { + ESP_LOGI(TAG, "TCA9548A failed"); + return; + } + // out of range to make sure on first set_channel a new one will be set + this->current_channelno_ = 8; + ESP_LOGCONFIG(TAG, "Channels currently open: %d", status); +} +void TCA9548AComponent::dump_config() { + ESP_LOGCONFIG(TAG, "TCA9548A:"); + LOG_I2C_DEVICE(this); + if (this->scan_) { + for (uint8_t i = 0; i < 8; i++) { + ESP_LOGCONFIG(TAG, "Activating channel: %d", i); + this->set_channel(i); + this->parent_->dump_config(); + } + } +} + +void TCA9548AComponent::set_channel(uint8_t channelno) { + if (this->current_channelno_ != channelno) { + this->current_channelno_ = channelno; + uint8_t channelbyte = 1 << channelno; + this->write_byte(0x70, channelbyte); + } +} + +} // namespace tca9548a +} // namespace esphome diff --git a/esphome/components/tca9548a/tca9548a.h b/esphome/components/tca9548a/tca9548a.h new file mode 100644 index 0000000000..50b1eb8b56 --- /dev/null +++ b/esphome/components/tca9548a/tca9548a.h @@ -0,0 +1,22 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tca9548a { + +class TCA9548AComponent : public Component, public i2c::I2CMultiplexer { + public: + void set_scan(bool scan) { scan_ = scan; } + void setup() override; + void dump_config() override; + void update(); + void set_channel(uint8_t channelno) override; + + protected: + bool scan_; + uint8_t current_channelno_; +}; +} // namespace tca9548a +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index bcd4460d3c..e9b4cdf67c 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1993,6 +1993,18 @@ cover: debug: +tca9548a: + - address: 0x70 + id: multiplex0 + scan: True + - address: 0x71 + id: multiplex1 + scan: True + multiplexer: + id: multiplex0 + channel: 0 + + pcf8574: - id: 'pcf8574_hub' address: 0x21 From 808e3be324590425ad831c7d7e7c80d3680686bf Mon Sep 17 00:00:00 2001 From: Stanislav Meduna Date: Sat, 3 Apr 2021 04:00:41 +0200 Subject: [PATCH 07/62] Add the display.is_displaying_page condition (#1646) * display: add the display.is_displaying_page condition * use maybe_simple_value for page_id * add test --- esphome/components/display/__init__.py | 25 ++++++++++++++++++++- esphome/components/display/display_buffer.h | 14 ++++++++++++ tests/test1.yaml | 7 ++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index f72ec88fae..fe8bc9bd7c 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -2,7 +2,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 +from esphome.const import CONF_ID, CONF_LAMBDA, CONF_PAGES, CONF_PAGE_ID, CONF_ROTATION from esphome.core import coroutine, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -19,6 +19,9 @@ DisplayPageShowNextAction = display_ns.class_( DisplayPageShowPrevAction = display_ns.class_( "DisplayPageShowPrevAction", automation.Action ) +DisplayIsDisplayingPageCondition = display_ns.class_( + "DisplayIsDisplayingPageCondition", automation.Condition +) DISPLAY_ROTATIONS = { 0: display_ns.DISPLAY_ROTATION_0_DEGREES, @@ -125,6 +128,26 @@ def display_page_show_previous_to_code(config, action_id, template_arg, args): yield cg.new_Pvariable(action_id, template_arg, paren) +@automation.register_condition( + "display.is_displaying_page", + DisplayIsDisplayingPageCondition, + cv.maybe_simple_value( + { + cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer), + cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage), + }, + key=CONF_PAGE_ID, + ), +) +def display_is_displaying_page_to_code(config, condition_id, template_arg, args): + paren = yield cg.get_variable(config[CONF_ID]) + page = yield cg.get_variable(config[CONF_PAGE_ID]) + var = cg.new_Pvariable(condition_id, template_arg, paren) + cg.add(var.set_page(page)) + + yield var + + @coroutine_with_priority(100.0) def to_code(config): cg.add_global(display_ns.using) diff --git a/esphome/components/display/display_buffer.h b/esphome/components/display/display_buffer.h index 5a63441e2d..71a6189990 100644 --- a/esphome/components/display/display_buffer.h +++ b/esphome/components/display/display_buffer.h @@ -296,6 +296,8 @@ class DisplayBuffer { void set_pages(std::vector pages); + const DisplayPage *get_active_page() const { return this->page_; } + /// Internal method to set the display rotation with. void set_rotation(DisplayRotation rotation); @@ -448,5 +450,17 @@ template class DisplayPageShowPrevAction : public Action DisplayBuffer *buffer_; }; +template class DisplayIsDisplayingPageCondition : public Condition { + public: + DisplayIsDisplayingPageCondition(DisplayBuffer *parent) : parent_(parent) {} + + void set_page(DisplayPage *page) { this->page_ = page; } + bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; } + + protected: + DisplayBuffer *parent_; + DisplayPage *page_; +}; + } // namespace display } // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index e9b4cdf67c..41f688e4e8 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -1760,6 +1760,13 @@ interval: btn_left_state = ((uint32_t) id(btn_left)->get_value() + 63 * (uint32_t)btn_left_state) >> 6; id(btn_left)->set_threshold(btn_left_state * 0.9); + - if: + condition: + display.is_displaying_page: + id: display1 + page_id: page1 + then: + - logger.log: 'Seeing page 1' color: - id: kbx_red From 2d618768d53bf6ae52e494341e3c19d86cbf930f Mon Sep 17 00:00:00 2001 From: Trevor North Date: Tue, 6 Apr 2021 11:19:56 +0100 Subject: [PATCH 08/62] Add BME680 via BSEC integration (#1313) --- CODEOWNERS | 1 + esphome/components/bme680_bsec/__init__.py | 64 +++ .../components/bme680_bsec/bme680_bsec.cpp | 398 ++++++++++++++++++ esphome/components/bme680_bsec/bme680_bsec.h | 106 +++++ esphome/components/bme680_bsec/sensor.py | 91 ++++ esphome/components/bme680_bsec/text_sensor.py | 41 ++ 6 files changed, 701 insertions(+) create mode 100644 esphome/components/bme680_bsec/__init__.py create mode 100644 esphome/components/bme680_bsec/bme680_bsec.cpp create mode 100644 esphome/components/bme680_bsec/bme680_bsec.h create mode 100644 esphome/components/bme680_bsec/sensor.py create mode 100644 esphome/components/bme680_bsec/text_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index a726f85cf1..ef2af83fa7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -20,6 +20,7 @@ esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter esphome/components/climate/* @esphome/core diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py new file mode 100644 index 0000000000..f5028a90a3 --- /dev/null +++ b/esphome/components/bme680_bsec/__init__.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c +from esphome.const import CONF_ID + +CODEOWNERS = ["@trvrnrth"] +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_BME680_BSEC_ID = "bme680_bsec_id" +CONF_TEMPERATURE_OFFSET = "temperature_offset" +CONF_IAQ_MODE = "iaq_mode" +CONF_SAMPLE_RATE = "sample_rate" +CONF_STATE_SAVE_INTERVAL = "state_save_interval" + +bme680_bsec_ns = cg.esphome_ns.namespace("bme680_bsec") + +IAQMode = bme680_bsec_ns.enum("IAQMode") +IAQ_MODE_OPTIONS = { + "STATIC": IAQMode.IAQ_MODE_STATIC, + "MOBILE": IAQMode.IAQ_MODE_MOBILE, +} + +SampleRate = bme680_bsec_ns.enum("SampleRate") +SAMPLE_RATE_OPTIONS = { + "LP": SampleRate.SAMPLE_RATE_LP, + "ULP": SampleRate.SAMPLE_RATE_ULP, +} + +BME680BSECComponent = bme680_bsec_ns.class_( + "BME680BSECComponent", cg.Component, i2c.I2CDevice +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature, + cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum( + IAQ_MODE_OPTIONS, upper=True + ), + cv.Optional(CONF_SAMPLE_RATE, default="LP"): cv.enum( + SAMPLE_RATE_OPTIONS, upper=True + ), + cv.Optional( + CONF_STATE_SAVE_INTERVAL, default="6hours" + ): cv.positive_time_period_minutes, + } +).extend(i2c.i2c_device_schema(0x76)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_temperature_offset(config[CONF_TEMPERATURE_OFFSET])) + cg.add(var.set_iaq_mode(config[CONF_IAQ_MODE])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) + cg.add( + var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) + ) + + cg.add_build_flag("-DUSING_BSEC") + cg.add_library("BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp new file mode 100644 index 0000000000..0efe4083ef --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -0,0 +1,398 @@ +#ifdef USING_BSEC + +#include "bme680_bsec.h" +#include "esphome/core/log.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome { +namespace bme680_bsec { + +static const char *TAG = "bme680_bsec.sensor"; + +static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; + +BME680BSECComponent *BME680BSECComponent::instance; + +void BME680BSECComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up BME680 via BSEC..."); + BME680BSECComponent::instance = this; + + this->bsec_status_ = bsec_init(); + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->bme680_.dev_id = this->address_; + this->bme680_.intf = BME680_I2C_INTF; + this->bme680_.read = BME680BSECComponent::read_bytes_wrapper; + this->bme680_.write = BME680BSECComponent::write_bytes_wrapper; + this->bme680_.delay_ms = BME680BSECComponent::delay_ms; + this->bme680_.amb_temp = 25; + this->bme680_.power_mode = BME680_FORCED_MODE; + + this->bme680_status_ = bme680_init(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + this->mark_failed(); + return; + } + + if (this->sample_rate_ == SAMPLE_RATE_ULP) { + const uint8_t bsec_config[] = { +#include "config/generic_33v_300s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + this->update_subscription_(BSEC_SAMPLE_RATE_ULP); + } else { + const uint8_t bsec_config[] = { +#include "config/generic_33v_3s_28d/bsec_iaq.txt" + }; + this->set_config_(bsec_config); + this->update_subscription_(BSEC_SAMPLE_RATE_LP); + } + if (this->bsec_status_ != BSEC_OK) { + this->mark_failed(); + return; + } + + this->load_state_(); +} + +void BME680BSECComponent::set_config_(const uint8_t *config) { + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_configuration(config, BSEC_MAX_PROPERTY_BLOB_SIZE, work_buffer, sizeof(work_buffer)); +} + +void BME680BSECComponent::update_subscription_(float sample_rate) { + bsec_sensor_configuration_t virtual_sensors[BSEC_NUMBER_OUTPUTS]; + int num_virtual_sensors = 0; + + if (this->iaq_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = + this->iaq_mode_ == IAQ_MODE_STATIC ? BSEC_OUTPUT_STATIC_IAQ : BSEC_OUTPUT_IAQ; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->co2_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_CO2_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->breath_voc_equivalent_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_BREATH_VOC_EQUIVALENT; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->pressure_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_PRESSURE; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->gas_resistance_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_RAW_GAS; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->temperature_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + if (this->humidity_sensor_) { + virtual_sensors[num_virtual_sensors].sensor_id = BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY; + virtual_sensors[num_virtual_sensors].sample_rate = sample_rate; + num_virtual_sensors++; + } + + bsec_sensor_configuration_t sensor_settings[BSEC_MAX_PHYSICAL_SENSOR]; + uint8_t num_sensor_settings = BSEC_MAX_PHYSICAL_SENSOR; + this->bsec_status_ = + bsec_update_subscription(virtual_sensors, num_virtual_sensors, sensor_settings, &num_sensor_settings); +} + +void BME680BSECComponent::dump_config() { + ESP_LOGCONFIG(TAG, "BME680 via BSEC:"); + + bsec_version_t version; + bsec_get_version(&version); + ESP_LOGCONFIG(TAG, " BSEC Version: %d.%d.%d.%d", version.major, version.minor, version.major_bugfix, + version.minor_bugfix); + + LOG_I2C_DEVICE(this); + + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication failed (BSEC Status: %d, BME680 Status: %d)", this->bsec_status_, + this->bme680_status_); + } + + ESP_LOGCONFIG(TAG, " Temperature Offset: %.2f", this->temperature_offset_); + ESP_LOGCONFIG(TAG, " IAQ Mode: %s", this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile"); + ESP_LOGCONFIG(TAG, " Sample Rate: %s", this->sample_rate_ == SAMPLE_RATE_ULP ? "ULP" : "LP"); + ESP_LOGCONFIG(TAG, " State Save Interval: %ims", this->state_save_interval_ms_); + + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_SENSOR(" ", "Gas Resistance", this->gas_resistance_sensor_); + LOG_SENSOR(" ", "IAQ", this->iaq_sensor_); + LOG_SENSOR(" ", "Numeric IAQ Accuracy", this->iaq_accuracy_sensor_); + LOG_TEXT_SENSOR(" ", "IAQ Accuracy", this->iaq_accuracy_text_sensor_); + LOG_SENSOR(" ", "CO2 Equivalent", this->co2_equivalent_sensor_); + LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); +} + +float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } + +void BME680BSECComponent::loop() { + this->run_(); + + if (this->bsec_status_ < BSEC_OK || this->bme680_status_ < BME680_OK) { + this->status_set_error(); + } else { + this->status_clear_error(); + } + if (this->bsec_status_ > BSEC_OK || this->bme680_status_ > BME680_OK) { + this->status_set_warning(); + } else { + this->status_clear_warning(); + } +} + +void BME680BSECComponent::run_() { + int64_t curr_time_ns = this->get_time_ns_(); + if (curr_time_ns < this->next_call_ns_) { + return; + } + + ESP_LOGV(TAG, "Performing sensor run"); + + bsec_bme_settings_t bme680_settings; + this->bsec_status_ = bsec_sensor_control(curr_time_ns, &bme680_settings); + if (this->bsec_status_ < BSEC_OK) { + ESP_LOGW(TAG, "Failed to fetch sensor control settings (BSEC Error Code %d)", this->bsec_status_); + return; + } + this->next_call_ns_ = bme680_settings.next_call; + + this->bme680_.gas_sett.run_gas = bme680_settings.run_gas; + this->bme680_.tph_sett.os_hum = bme680_settings.humidity_oversampling; + this->bme680_.tph_sett.os_temp = bme680_settings.temperature_oversampling; + this->bme680_.tph_sett.os_pres = bme680_settings.pressure_oversampling; + this->bme680_.gas_sett.heatr_temp = bme680_settings.heater_temperature; + this->bme680_.gas_sett.heatr_dur = bme680_settings.heating_duration; + uint16_t desired_settings = + BME680_OST_SEL | BME680_OSP_SEL | BME680_OSH_SEL | BME680_FILTER_SEL | BME680_GAS_SENSOR_SEL; + this->bme680_status_ = bme680_set_sensor_settings(desired_settings, &this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor settings (BME680 Error Code %d)", this->bme680_status_); + return; + } + + this->bme680_status_ = bme680_set_sensor_mode(&this->bme680_); + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to set sensor mode (BME680 Error Code %d)", this->bme680_status_); + return; + } + + uint16_t meas_dur = 0; + bme680_get_profile_dur(&meas_dur, &this->bme680_); + ESP_LOGV(TAG, "Queueing read in %ums", meas_dur); + this->set_timeout("read", meas_dur, [this, bme680_settings]() { this->read_(bme680_settings); }); +} + +void BME680BSECComponent::read_(bsec_bme_settings_t bme680_settings) { + ESP_LOGV(TAG, "Reading data"); + struct bme680_field_data data; + this->bme680_status_ = bme680_get_sensor_data(&data, &this->bme680_); + + if (this->bme680_status_ != BME680_OK) { + ESP_LOGW(TAG, "Failed to get sensor data (BME680 Error Code %d)", this->bme680_status_); + return; + } + if (!(data.status & BME680_NEW_DATA_MSK)) { + ESP_LOGD(TAG, "BME680 did not report new data"); + return; + } + + bsec_input_t inputs[BSEC_MAX_PHYSICAL_SENSOR]; // Temperature, Pressure, Humidity & Gas Resistance + uint8_t num_inputs = 0; + int64_t curr_time_ns = this->get_time_ns_(); + + if (bme680_settings.process_data & BSEC_PROCESS_TEMPERATURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_TEMPERATURE; + inputs[num_inputs].signal = data.temperature / 100.0f; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + + // Temperature offset from the real temperature due to external heat sources + inputs[num_inputs].sensor_id = BSEC_INPUT_HEATSOURCE; + inputs[num_inputs].signal = this->temperature_offset_; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_HUMIDITY) { + inputs[num_inputs].sensor_id = BSEC_INPUT_HUMIDITY; + inputs[num_inputs].signal = data.humidity / 1000.0f; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_PRESSURE) { + inputs[num_inputs].sensor_id = BSEC_INPUT_PRESSURE; + inputs[num_inputs].signal = data.pressure; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (bme680_settings.process_data & BSEC_PROCESS_GAS) { + inputs[num_inputs].sensor_id = BSEC_INPUT_GASRESISTOR; + inputs[num_inputs].signal = data.gas_resistance; + inputs[num_inputs].time_stamp = curr_time_ns; + num_inputs++; + } + if (num_inputs < 1) { + ESP_LOGD(TAG, "No signal inputs available for BSEC"); + return; + } + + bsec_output_t outputs[BSEC_NUMBER_OUTPUTS]; + uint8_t num_outputs = BSEC_NUMBER_OUTPUTS; + this->bsec_status_ = bsec_do_steps(inputs, num_inputs, outputs, &num_outputs); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "BSEC failed to process signals (BSEC Error Code %d)", this->bsec_status_); + return; + } + if (num_outputs < 1) { + ESP_LOGD(TAG, "No signal outputs provided by BSEC"); + return; + } + + this->publish_(outputs, num_outputs); +} + +void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_outputs) { + ESP_LOGV(TAG, "Publishing sensor states"); + for (uint8_t i = 0; i < num_outputs; i++) { + switch (outputs[i].sensor_id) { + case BSEC_OUTPUT_IAQ: + case BSEC_OUTPUT_STATIC_IAQ: + uint8_t accuracy; + accuracy = outputs[i].accuracy; + this->publish_sensor_state_(this->iaq_sensor_, outputs[i].signal); + this->publish_sensor_state_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[accuracy]); + this->publish_sensor_state_(this->iaq_accuracy_sensor_, accuracy, true); + + // Queue up an opportunity to save state + this->defer("save_state", [this, accuracy]() { this->save_state_(accuracy); }); + break; + case BSEC_OUTPUT_CO2_EQUIVALENT: + this->publish_sensor_state_(this->co2_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT: + this->publish_sensor_state_(this->breath_voc_equivalent_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_RAW_PRESSURE: + this->publish_sensor_state_(this->pressure_sensor_, outputs[i].signal / 100.0f); + break; + case BSEC_OUTPUT_RAW_GAS: + this->publish_sensor_state_(this->gas_resistance_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE: + this->publish_sensor_state_(this->temperature_sensor_, outputs[i].signal); + break; + case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY: + this->publish_sensor_state_(this->humidity_sensor_, outputs[i].signal); + break; + } + } +} + +int64_t BME680BSECComponent::get_time_ns_() { + int64_t time_ms = millis(); + if (this->last_time_ms_ > time_ms) { + this->millis_overflow_counter_++; + } + this->last_time_ms_ = time_ms; + + return (time_ms + ((int64_t) this->millis_overflow_counter_ << 32)) * INT64_C(1000000); +} + +void BME680BSECComponent::publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only) { + if (!sensor || (change_only && sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +void BME680BSECComponent::publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value) { + if (!sensor || (sensor->has_state() && sensor->state == value)) { + return; + } + sensor->publish_state(value); +} + +int8_t BME680BSECComponent::read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->read_bytes(a_register, data, len) ? 0 : -1; +} + +int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len) { + return BME680BSECComponent::instance->write_bytes(a_register, data, len) ? 0 : -1; +} + +void BME680BSECComponent::delay_ms(uint32_t period) { + ESP_LOGV(TAG, "Delaying for %ums", period); + delay(period); +} + +void BME680BSECComponent::load_state_() { + uint32_t hash = fnv1_hash("bme680_bsec_state_" + to_string(this->address_)); + this->bsec_state_ = global_preferences.make_preference(hash, true); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + if (this->bsec_state_.load(&state)) { + ESP_LOGV(TAG, "Loading state"); + uint8_t work_buffer[BSEC_MAX_WORKBUFFER_SIZE]; + this->bsec_status_ = bsec_set_state(state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, sizeof(work_buffer)); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed to load state (BSEC Error Code %d)", this->bsec_status_); + } + ESP_LOGI(TAG, "Loaded state"); + } +} + +void BME680BSECComponent::save_state_(uint8_t accuracy) { + if (accuracy < 3 || (millis() - this->last_state_save_ms_ < this->state_save_interval_ms_)) { + return; + } + + ESP_LOGV(TAG, "Saving state"); + + uint8_t state[BSEC_MAX_STATE_BLOB_SIZE]; + uint8_t work_buffer[BSEC_MAX_STATE_BLOB_SIZE]; + uint32_t num_serialized_state = BSEC_MAX_STATE_BLOB_SIZE; + + this->bsec_status_ = + bsec_get_state(0, state, BSEC_MAX_STATE_BLOB_SIZE, work_buffer, BSEC_MAX_STATE_BLOB_SIZE, &num_serialized_state); + if (this->bsec_status_ != BSEC_OK) { + ESP_LOGW(TAG, "Failed fetch state for save (BSEC Error Code %d)", this->bsec_status_); + return; + } + + if (!this->bsec_state_.save(&state)) { + ESP_LOGW(TAG, "Failed to save state"); + return; + } + this->last_state_save_ms_ = millis(); + + ESP_LOGI(TAG, "Saved state"); +} + +} // namespace bme680_bsec +} // namespace esphome + +#endif diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h new file mode 100644 index 0000000000..ce35e21c9a --- /dev/null +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -0,0 +1,106 @@ +#ifdef USING_BSEC + +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/preferences.h" +#include +#include + +namespace esphome { +namespace bme680_bsec { + +enum IAQMode { + IAQ_MODE_STATIC = 0, + IAQ_MODE_MOBILE = 1, +}; + +enum SampleRate { + SAMPLE_RATE_LP = 0, + SAMPLE_RATE_ULP = 1, +}; + +class BME680BSECComponent : public Component, public i2c::I2CDevice { + public: + void set_temperature_offset(float offset) { this->temperature_offset_ = offset; } + void set_iaq_mode(IAQMode iaq_mode) { this->iaq_mode_ = iaq_mode; } + void set_sample_rate(SampleRate sample_rate) { this->sample_rate_ = sample_rate; } + void set_state_save_interval(uint32_t interval) { this->state_save_interval_ms_ = interval; } + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } + void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } + void set_gas_resistance_sensor(sensor::Sensor *gas_resistance_sensor) { + gas_resistance_sensor_ = gas_resistance_sensor; + } + void set_iaq_sensor(sensor::Sensor *iaq_sensor) { iaq_sensor_ = iaq_sensor; } + void set_iaq_accuracy_text_sensor(text_sensor::TextSensor *iaq_accuracy_text_sensor) { + iaq_accuracy_text_sensor_ = iaq_accuracy_text_sensor; + } + void set_iaq_accuracy_sensor(sensor::Sensor *iaq_accuracy_sensor) { iaq_accuracy_sensor_ = iaq_accuracy_sensor; } + void set_co2_equivalent_sensor(sensor::Sensor *co2_equivalent_sensor) { + co2_equivalent_sensor_ = co2_equivalent_sensor; + } + void set_breath_voc_equivalent_sensor(sensor::Sensor *breath_voc_equivalent_sensor) { + breath_voc_equivalent_sensor_ = breath_voc_equivalent_sensor; + } + + static BME680BSECComponent *instance; + static int8_t read_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static int8_t write_bytes_wrapper(uint8_t address, uint8_t a_register, uint8_t *data, uint16_t len); + static void delay_ms(uint32_t period); + + void setup() override; + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + + protected: + void set_config_(const uint8_t *config); + void update_subscription_(float sample_rate); + + void run_(); + void read_(bsec_bme_settings_t bme680_settings); + void publish_(const bsec_output_t *outputs, uint8_t num_outputs); + int64_t get_time_ns_(); + + void publish_sensor_state_(sensor::Sensor *sensor, float value, bool change_only = false); + void publish_sensor_state_(text_sensor::TextSensor *sensor, std::string value); + + void load_state_(); + void save_state_(uint8_t accuracy); + + struct bme680_dev bme680_; + bsec_library_return_t bsec_status_{BSEC_OK}; + int8_t bme680_status_{BME680_OK}; + + int64_t last_time_ms_{0}; + uint32_t millis_overflow_counter_{0}; + int64_t next_call_ns_{0}; + + ESPPreferenceObject bsec_state_; + uint32_t state_save_interval_ms_{21600000}; // 6 hours - 4 times a day + uint32_t last_state_save_ms_ = 0; + + float temperature_offset_{0}; + IAQMode iaq_mode_{IAQ_MODE_STATIC}; + SampleRate sample_rate_{SAMPLE_RATE_LP}; + + sensor::Sensor *temperature_sensor_; + sensor::Sensor *pressure_sensor_; + sensor::Sensor *humidity_sensor_; + sensor::Sensor *gas_resistance_sensor_; + sensor::Sensor *iaq_sensor_; + text_sensor::TextSensor *iaq_accuracy_text_sensor_; + sensor::Sensor *iaq_accuracy_sensor_; + sensor::Sensor *co2_equivalent_sensor_; + sensor::Sensor *breath_voc_equivalent_sensor_; +}; + +} // namespace bme680_bsec +} // namespace esphome + +#endif diff --git a/esphome/components/bme680_bsec/sensor.py b/esphome/components/bme680_bsec/sensor.py new file mode 100644 index 0000000000..b27b267c11 --- /dev/null +++ b/esphome/components/bme680_bsec/sensor.py @@ -0,0 +1,91 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_GAS_RESISTANCE, + CONF_HUMIDITY, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + UNIT_CELSIUS, + UNIT_EMPTY, + UNIT_HECTOPASCAL, + UNIT_OHM, + UNIT_PARTS_PER_MILLION, + UNIT_PERCENT, + ICON_GAS_CYLINDER, + ICON_GAUGE, + ICON_THERMOMETER, + ICON_WATER_PERCENT, +) +from esphome.core import coroutine +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ = "iaq" +CONF_IAQ_ACCURACY = "iaq_accuracy" +CONF_CO2_EQUIVALENT = "co2_equivalent" +CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent" +UNIT_IAQ = "IAQ" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" +ICON_TEST_TUBE = "mdi:test-tube" + +TYPES = { + CONF_TEMPERATURE: "set_temperature_sensor", + CONF_PRESSURE: "set_pressure_sensor", + CONF_HUMIDITY: "set_humidity_sensor", + CONF_GAS_RESISTANCE: "set_gas_resistance_sensor", + CONF_IAQ: "set_iaq_sensor", + CONF_IAQ_ACCURACY: "set_iaq_accuracy_sensor", + CONF_CO2_EQUIVALENT: "set_co2_equivalent_sensor", + CONF_BREATH_VOC_EQUIVALENT: "set_breath_voc_equivalent_sensor", +} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + UNIT_HECTOPASCAL, ICON_GAUGE, 1, DEVICE_CLASS_PRESSURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_WATER_PERCENT, 1, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_GAS_RESISTANCE): sensor.sensor_schema( + UNIT_OHM, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_IAQ): sensor.sensor_schema( + UNIT_IAQ, ICON_GAUGE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_IAQ_ACCURACY): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCURACY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_CO2_EQUIVALENT): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_BREATH_VOC_EQUIVALENT): sensor.sensor_schema( + UNIT_PARTS_PER_MILLION, ICON_TEST_TUBE, 1, DEVICE_CLASS_EMPTY + ), + } +) + + +@coroutine +def setup_conf(config, key, hub, funcName): + if key in config: + conf = config[key] + var = yield sensor.new_sensor(conf) + func = getattr(hub, funcName) + cg.add(func(var)) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key, funcName in TYPES.items(): + yield setup_conf(config, key, hub, funcName) diff --git a/esphome/components/bme680_bsec/text_sensor.py b/esphome/components/bme680_bsec/text_sensor.py new file mode 100644 index 0000000000..992e2989c9 --- /dev/null +++ b/esphome/components/bme680_bsec/text_sensor.py @@ -0,0 +1,41 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import CONF_ID, CONF_ICON +from esphome.core import coroutine +from . import BME680BSECComponent, CONF_BME680_BSEC_ID + +DEPENDENCIES = ["bme680_bsec"] + +CONF_IAQ_ACCURACY = "iaq_accuracy" +ICON_ACCURACY = "mdi:checkbox-marked-circle-outline" + +TYPES = {CONF_IAQ_ACCURACY: "set_iaq_accuracy_text_sensor"} + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_BME680_BSEC_ID): cv.use_id(BME680BSECComponent), + cv.Optional(CONF_IAQ_ACCURACY): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + cv.Optional(CONF_ICON, default=ICON_ACCURACY): cv.icon, + } + ), + } +) + + +@coroutine +def setup_conf(config, key, hub, funcName): + if key in config: + conf = config[key] + var = cg.new_Pvariable(conf[CONF_ID]) + yield text_sensor.register_text_sensor(var, conf) + func = getattr(hub, funcName) + cg.add(func(var)) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_BME680_BSEC_ID]) + for key, funcName in TYPES.items(): + yield setup_conf(config, key, hub, funcName) From 5c359856ff3159341f8602f5cd7188b07a7f6524 Mon Sep 17 00:00:00 2001 From: Anthony Uk <1492471+dataway@users.noreply.github.com> Date: Tue, 6 Apr 2021 13:50:31 +0200 Subject: [PATCH 09/62] Fixed CustomComponentConstructor::get_component() (#1653) Co-authored-by: Guillermo Ruffino --- esphome/components/custom_component/custom_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/custom_component/custom_component.h b/esphome/components/custom_component/custom_component.h index 6b009ba549..3f5760e4cf 100644 --- a/esphome/components/custom_component/custom_component.h +++ b/esphome/components/custom_component/custom_component.h @@ -16,7 +16,7 @@ class CustomComponentConstructor { } } - Component *get_component(int i) { return this->components_[i]; } + Component *get_component(int i) const { return this->components_[i]; } protected: std::vector components_; From 3ecae3f16ff13e7427b8fecf9848f454b4399687 Mon Sep 17 00:00:00 2001 From: Vegetto Date: Tue, 6 Apr 2021 14:31:38 +0200 Subject: [PATCH 10/62] fixes #858 - esphome crashes with neolightbus and RMT (#1667) Co-authored-by: Otto Winter --- esphome/components/neopixelbus/neopixelbus_light.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/neopixelbus/neopixelbus_light.h b/esphome/components/neopixelbus/neopixelbus_light.h index 46601d8345..2f279e1c9b 100644 --- a/esphome/components/neopixelbus/neopixelbus_light.h +++ b/esphome/components/neopixelbus/neopixelbus_light.h @@ -68,7 +68,8 @@ class NeoPixelBusLightOutputBase : public light::AddressableLight { void add_leds(uint16_t count_pixels) { this->add_leds(new NeoPixelBus(count_pixels)); } void add_leds(NeoPixelBus *controller) { this->controller_ = controller; - this->controller_->Begin(); + // controller gets initialised in setup() - avoid calling twice (crashes with RMT) + // this->controller_->Begin(); } // ========== INTERNAL METHODS ========== From 26407e001ba107233dcb0e8388836cf50adf5006 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 14:42:50 +0200 Subject: [PATCH 11/62] Bump pytest from 6.2.2 to 6.2.3 (#1663) Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.2 to 6.2.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.2...6.2.3) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index f811626cc5..fdd1e91b20 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ pexpect==4.8.0 pre-commit # Unit tests -pytest==6.2.2 +pytest==6.2.3 pytest-cov==2.11.1 pytest-mock==3.5.1 asyncmock==0.4.2 From e0c5b456942a696bb3f9741736a122c0112ea393 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Apr 2021 14:43:10 +0200 Subject: [PATCH 12/62] Bump protobuf from 3.15.6 to 3.15.7 (#1662) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Otto Winter --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9ab99e0bb4..aed9f97d8a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ paho-mqtt==1.5.1 colorama==0.4.4 colorlog==4.7.2 tornado==6.1 -protobuf==3.15.6 +protobuf==3.15.7 tzlocal==2.1 pytz==2021.1 pyserial==3.5 From 7964b724ed24dbefa02df8aacae33e9b6a13d081 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Wed, 7 Apr 2021 12:16:36 +0200 Subject: [PATCH 13/62] Rewrite sun component calculations (#1661) --- esphome/components/sun/__init__.py | 50 +++- esphome/components/sun/sun.cpp | 443 +++++++++++++++++++---------- esphome/components/sun/sun.h | 113 ++++---- script/ci-custom.py | 1 + 4 files changed, 391 insertions(+), 216 deletions(-) diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index 5241f1bb55..b24d62f32f 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -1,3 +1,5 @@ +import re + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation @@ -22,7 +24,7 @@ CONF_ON_SUNSET = "on_sunset" # Default sun elevation is a bit below horizon because sunset # means time when the entire sun disk is below the horizon -DEFAULT_ELEVATION = -0.883 +DEFAULT_ELEVATION = -0.83333 ELEVATION_MAP = { "sunrise": 0.0, @@ -45,12 +47,54 @@ def elevation(value): return cv.float_range(min=-180, max=180)(value) +# Parses sexagesimal values like 22°57′7″S +LAT_LON_REGEX = re.compile( + r"([+\-])?\s*" + r"(?:([0-9]+)\s*°)?\s*" + r"(?:([0-9]+)\s*[′\'])?\s*" + r'(?:([0-9]+)\s*[″"])?\s*' + r"([NESW])?" +) + + +def parse_latlon(value): + if isinstance(value, str) and value.endswith("°"): + # strip trailing degree character + value = value[:-1] + try: + return cv.float_(value) + except cv.Invalid: + pass + + value = cv.string_strict(value) + m = LAT_LON_REGEX.match(value) + + if m is None: + raise cv.Invalid("Invalid format for latitude/longitude") + sign = m.group(1) + deg = m.group(2) + minute = m.group(3) + second = m.group(4) + d = m.group(5) + + val = float(deg or 0) + float(minute or 0) / 60 + float(second or 0) / 3600 + if sign == "-": + val *= -1 + if d and d in "SW": + val *= -1 + return val + + CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(Sun), cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), - cv.Required(CONF_LATITUDE): cv.float_range(min=-90, max=90), - cv.Required(CONF_LONGITUDE): cv.float_range(min=-180, max=180), + cv.Required(CONF_LATITUDE): cv.All( + parse_latlon, cv.float_range(min=-90, max=90) + ), + cv.Required(CONF_LONGITUDE): cv.All( + parse_latlon, cv.float_range(min=-180, max=180) + ), cv.Optional(CONF_ON_SUNRISE): automation.validate_automation( { cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SunTrigger), diff --git a/esphome/components/sun/sun.cpp b/esphome/components/sun/sun.cpp index 6744a418c5..0722e66e67 100644 --- a/esphome/components/sun/sun.cpp +++ b/esphome/components/sun/sun.cpp @@ -1,176 +1,319 @@ #include "sun.h" #include "esphome/core/log.h" +/* +The formulas/algorithms in this module are based on the book +"Astronomical algorithms" by Jean Meeus (2nd edition) + +The target accuracy of this implementation is ~1min for sunrise/sunset calculations, +and 6 arcminutes for elevation/azimuth. As such, some of the advanced correction factors +like exact nutation are not included. But in some testing the accuracy appears to be within range +for random spots around the globe. +*/ + namespace esphome { namespace sun { +using namespace esphome::sun::internal; + static const char *TAG = "sun"; #undef PI +#undef degrees +#undef radians +#undef sq -/* Usually, ESPHome uses single-precision floating point values - * because those tend to be accurate enough and are more efficient. - * - * However, some of the data in this class has to be quite accurate, so double is - * used everywhere. - */ -static const double PI = 3.141592653589793; -static const double TAU = 6.283185307179586; -static const double TO_RADIANS = PI / 180.0; -static const double TO_DEGREES = 180.0 / PI; -static const double EARTH_TILT = 23.44 * TO_RADIANS; +static const num_t PI = 3.141592653589793; +inline num_t degrees(num_t rad) { return rad * 180 / PI; } +inline num_t radians(num_t deg) { return deg * PI / 180; } +inline num_t arcdeg(num_t deg, num_t minutes, num_t seconds) { return deg + minutes / 60 + seconds / 3600; } +inline num_t sq(num_t x) { return x * x; } +inline num_t cb(num_t x) { return x * x * x; } -optional Sun::sunrise(double elevation) { - auto time = this->time_->now(); - if (!time.is_valid()) - return {}; - double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, true); - if (isnan(sun_time)) - return {}; - uint32_t epoch = this->calc_epoch_(time, sun_time); - return time::ESPTime::from_epoch_local(epoch); -} -optional Sun::sunset(double elevation) { - auto time = this->time_->now(); - if (!time.is_valid()) - return {}; - double sun_time = this->sun_time_for_elevation_(time.day_of_year, elevation, false); - if (isnan(sun_time)) - return {}; - uint32_t epoch = this->calc_epoch_(time, sun_time); - return time::ESPTime::from_epoch_local(epoch); -} -double Sun::elevation() { - auto time = this->current_sun_time_(); - if (isnan(time)) - return NAN; - return this->elevation_(time); -} -double Sun::azimuth() { - auto time = this->current_sun_time_(); - if (isnan(time)) - return NAN; - return this->azimuth_(time); -} -// like clamp, but with doubles -double clampd(double val, double min, double max) { - if (val < min) - return min; - if (val > max) - return max; - return val; -} -double Sun::sun_declination_(double sun_time) { - double n = sun_time - 1.0; - // maximum declination - const double tot = -sin(EARTH_TILT); +num_t GeoLocation::latitude_rad() const { return radians(latitude); } +num_t GeoLocation::longitude_rad() const { return radians(longitude); } +num_t EquatorialCoordinate::right_ascension_rad() const { return radians(right_ascension); } +num_t EquatorialCoordinate::declination_rad() const { return radians(declination); } +num_t HorizontalCoordinate::elevation_rad() const { return radians(elevation); } +num_t HorizontalCoordinate::azimuth_rad() const { return radians(azimuth); } - // eccentricity of the earth's orbit (ellipse) - double eccentricity = 0.0167; - - // days since perihelion (January 3rd) - double days_since_perihelion = n - 2; - // days since december solstice (december 22) - double days_since_december_solstice = n + 10; - const double c = TAU / 365.24; - double v = cos(c * days_since_december_solstice + 2 * eccentricity * sin(c * days_since_perihelion)); - // Make sure value is in range (double error may lead to results slightly larger than 1) - double x = clampd(tot * v, -1.0, 1.0); - return asin(x); +num_t julian_day(time::ESPTime moment) { + // p. 59 + // UT -> JD, TT -> JDE + int y = moment.year; + int m = moment.month; + num_t d = moment.day_of_month; + d += moment.hour / 24.0; + d += moment.minute / (24.0 * 60); + d += moment.second / (24.0 * 60 * 60); + if (m <= 2) { + y -= 1; + m += 12; + } + int a = y / 100; + int b = 2 - a + a / 4; + return ((int) (365.25 * (y + 4716))) + ((int) (30.6001 * (m + 1))) + d + b - 1524.5; } -double Sun::elevation_ratio_(double sun_time) { - double decl = this->sun_declination_(sun_time); - double hangle = this->hour_angle_(sun_time); - double a = sin(this->latitude_rad_()) * sin(decl); - double b = cos(this->latitude_rad_()) * cos(decl) * cos(hangle); - double val = clampd(a + b, -1.0, 1.0); - return val; +num_t delta_t(time::ESPTime moment) { + // approximation for 2005-2050 from NASA (https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html) + int t = moment.year - 2000; + return 62.92 + t * (0.32217 + t * 0.005589); } -double Sun::latitude_rad_() { return this->latitude_ * TO_RADIANS; } -double Sun::hour_angle_(double sun_time) { - double time_of_day = fmod(sun_time, 1.0) * 24.0; - return -PI * (time_of_day - 12) / 12; +// Perform a fractional module operation where the result will always be positive (wrapping around) +num_t wmod(num_t x, num_t y) { + num_t res = fmod(x, y); + if (res < 0) + res += y; + return res; } -double Sun::elevation_(double sun_time) { return this->elevation_rad_(sun_time) * TO_DEGREES; } -double Sun::elevation_rad_(double sun_time) { return asin(this->elevation_ratio_(sun_time)); } -double Sun::zenith_rad_(double sun_time) { return acos(this->elevation_ratio_(sun_time)); } -double Sun::azimuth_rad_(double sun_time) { - double hangle = -this->hour_angle_(sun_time); - double decl = this->sun_declination_(sun_time); - double zen = this->zenith_rad_(sun_time); - double nom = cos(zen) * sin(this->latitude_rad_()) - sin(decl); - double denom = sin(zen) * cos(this->latitude_rad_()); - double v = clampd(nom / denom, -1.0, 1.0); - double az = PI - acos(v); - if (hangle > 0) - az = -az; - if (az < 0) - az += TAU; - return az; + +num_t internal::Moment::jd() const { return julian_day(dt); } + +num_t internal::Moment::jde() const { + // dt is in UT1, but JDE is based on TT + // so add deltaT factor + return jd() + delta_t(dt) / (60 * 60 * 24); } -double Sun::azimuth_(double sun_time) { return this->azimuth_rad_(sun_time) * TO_DEGREES; } -double Sun::calc_sun_time_(const time::ESPTime &time) { - // Time as seen at 0° longitude - if (!time.is_valid()) - return NAN; - double base = (time.day_of_year + time.hour / 24.0 + time.minute / 24.0 / 60.0 + time.second / 24.0 / 60.0 / 60.0); - // Add longitude correction - double add = this->longitude_ / 360.0; - return base + add; -} -uint32_t Sun::calc_epoch_(time::ESPTime base, double sun_time) { - sun_time -= this->longitude_ / 360.0; - base.day_of_year = uint32_t(floor(sun_time)); +struct SunAtTime { + num_t jde; + num_t t; - sun_time = (sun_time - base.day_of_year) * 24.0; - base.hour = uint32_t(floor(sun_time)); - - sun_time = (sun_time - base.hour) * 60.0; - base.minute = uint32_t(floor(sun_time)); - - sun_time = (sun_time - base.minute) * 60.0; - base.second = uint32_t(floor(sun_time)); - - base.recalc_timestamp_utc(true); - return base.timestamp; -} -double Sun::sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising) { - // Use binary search, newton's method would be better but binary search already - // converges quite well (19 cycles) and much simpler. Function is guaranteed to be - // monotonous. - double lo, hi; - if (rising) { - lo = day_of_year + 0.0; - hi = day_of_year + 0.5; - } else { - lo = day_of_year + 1.0; - hi = day_of_year + 0.5; + SunAtTime(num_t jde) : jde(jde) { + // eq 25.1, p. 163; julian centuries from the epoch J2000.0 + t = (jde - 2451545) / 36525.0; } - double min_elevation = this->elevation_(lo); - double max_elevation = this->elevation_(hi); - if (elevation < min_elevation || elevation > max_elevation) - return NAN; - - // Accuracy: 0.1s - const double accuracy = 1.0 / (24.0 * 60.0 * 60.0 * 10.0); - - while (fabs(hi - lo) > accuracy) { - double mid = (lo + hi) / 2.0; - double value = this->elevation_(mid) - elevation; - if (value < 0) { - lo = mid; - } else if (value > 0) { - hi = mid; - } else { - lo = hi = mid; - break; - } + num_t mean_obliquity() const { + // eq. 22.2, p. 147; mean obliquity of the ecliptic + num_t epsilon_0 = (+arcdeg(23, 26, 21.448) - arcdeg(0, 0, 46.8150) * t - arcdeg(0, 0, 0.00059) * sq(t) + + arcdeg(0, 0, 0.001813) * cb(t)); + return epsilon_0; } - return (lo + hi) / 2.0; + num_t omega() const { + // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic + // in degrees + num_t omega = 125.05 - 1934.136 * t; + return omega; + } + + num_t true_obliquity() const { + // eq. 25.8, p. 165; correction factor for obliquity of the ecliptic + num_t delta_epsilon = 0.00256 * cos(radians(omega())); + num_t epsilon = mean_obliquity() + delta_epsilon; + return epsilon; + } + + num_t mean_longitude() const { + // eq 25.2, p. 163; geometric mean longitude = mean equinox of the date in degrees + num_t l0 = 280.46646 + 36000.76983 * t + 0.0003032 * sq(t); + return wmod(l0, 360); + } + + num_t eccentricity() const { + // eq 25.4, p. 163; eccentricity of earth's orbit + num_t e = 0.016708634 - 0.000042037 * t - 0.0000001267 * sq(t); + return e; + } + + num_t mean_anomaly() const { + // eq 25.3, p. 163; mean anomaly of the sun in degrees + num_t m = 357.52911 + 35999.05029 * t - 0.0001537 * sq(t); + return wmod(m, 360); + } + + num_t equation_of_center() const { + // p. 164; sun's equation of the center c in degrees + num_t m_rad = radians(mean_anomaly()); + num_t c = ((1.914602 - 0.004817 * t - 0.000014 * sq(t)) * sin(m_rad) + (0.019993 - 0.000101 * t) * sin(2 * m_rad) + + 0.000289 * sin(3 * m_rad)); + return wmod(c, 360); + } + + num_t true_longitude() const { + // p. 164; sun's true longitude in degrees + num_t x = mean_longitude() + equation_of_center(); + return wmod(x, 360); + } + + num_t true_anomaly() const { + // p. 164; sun's true anomaly in degrees + num_t x = mean_anomaly() + equation_of_center(); + return wmod(x, 360); + } + + num_t apparent_longitude() const { + // p. 164; sun's apparent longitude = true equinox in degrees + num_t x = true_longitude() - 0.00569 - 0.00478 * sin(radians(omega())); + return wmod(x, 360); + } + + EquatorialCoordinate equatorial_coordinate() const { + num_t epsilon_rad = radians(true_obliquity()); + // eq. 25.6; p. 165; sun's right ascension alpha + num_t app_lon_rad = radians(apparent_longitude()); + num_t right_ascension_rad = atan2(cos(epsilon_rad) * sin(app_lon_rad), cos(app_lon_rad)); + num_t declination_rad = asin(sin(epsilon_rad) * sin(app_lon_rad)); + return EquatorialCoordinate{degrees(right_ascension_rad), degrees(declination_rad)}; + } + + num_t equation_of_time() const { + // chapter 28, p. 185 + num_t epsilon_half = radians(true_obliquity() / 2); + num_t y = sq(tan(epsilon_half)); + num_t l2 = 2 * mean_longitude(); + num_t l2_rad = radians(l2); + num_t e = eccentricity(); + num_t m = mean_anomaly(); + num_t m_rad = radians(m); + num_t sin_m = sin(m_rad); + num_t eot = (y * sin(l2_rad) - 2 * e * sin_m + 4 * e * y * sin_m * cos(l2_rad) - 1 / 2.0 * sq(y) * sin(2 * l2_rad) - + 5 / 4.0 * sq(e) * sin(2 * m_rad)); + return degrees(eot); + } + + void debug() const { + // debug output like in example 25.a, p. 165 + ESP_LOGV(TAG, "jde: %f", jde); + ESP_LOGV(TAG, "T: %f", t); + ESP_LOGV(TAG, "L_0: %f", mean_longitude()); + ESP_LOGV(TAG, "M: %f", mean_anomaly()); + ESP_LOGV(TAG, "e: %f", eccentricity()); + ESP_LOGV(TAG, "C: %f", equation_of_center()); + ESP_LOGV(TAG, "Odot: %f", true_longitude()); + ESP_LOGV(TAG, "Omega: %f", omega()); + ESP_LOGV(TAG, "lambda: %f", apparent_longitude()); + ESP_LOGV(TAG, "epsilon_0: %f", mean_obliquity()); + ESP_LOGV(TAG, "epsilon: %f", true_obliquity()); + ESP_LOGV(TAG, "v: %f", true_anomaly()); + auto eq = equatorial_coordinate(); + ESP_LOGV(TAG, "right_ascension: %f", eq.right_ascension); + ESP_LOGV(TAG, "declination: %f", eq.declination); + } +}; + +struct SunAtLocation { + GeoLocation location; + + num_t greenwich_sidereal_time(Moment moment) const { + // Return the greenwich mean sidereal time for this instant in degrees + // see chapter 12, p. 87 + num_t jd = moment.jd(); + // eq 12.1, p.87; jd for 0h UT of this date + time::ESPTime moment_0h = moment.dt; + moment_0h.hour = moment_0h.minute = moment_0h.second = 0; + num_t jd0 = Moment{moment_0h}.jd(); + num_t t = (jd0 - 2451545) / 36525; + // eq. 12.4, p.88 + num_t gmst = (+280.46061837 + 360.98564736629 * (jd - 2451545) + 0.000387933 * sq(t) - (1 / 38710000.0) * cb(t)); + return wmod(gmst, 360); + } + + HorizontalCoordinate true_coordinate(Moment moment) const { + auto eq = SunAtTime(moment.jde()).equatorial_coordinate(); + num_t gmst = greenwich_sidereal_time(moment); + // do not apply any nutation correction (not important for our target accuracy) + num_t nutation_corr = 0; + + num_t ra = eq.right_ascension; + num_t alpha = gmst + nutation_corr + location.longitude - ra; + alpha = wmod(alpha, 360); + num_t alpha_rad = radians(alpha); + + num_t sin_lat = sin(location.latitude_rad()); + num_t cos_lat = cos(location.latitude_rad()); + num_t sin_elevation = (+sin_lat * sin(eq.declination_rad()) + cos_lat * cos(eq.declination_rad()) * cos(alpha_rad)); + num_t elevation_rad = asin(sin_elevation); + num_t azimuth_rad = atan2(sin(alpha_rad), cos(alpha_rad) * sin_lat - tan(eq.declination_rad()) * cos_lat); + return HorizontalCoordinate{degrees(elevation_rad), degrees(azimuth_rad) + 180}; + } + + optional sunrise(time::ESPTime date, num_t zenith) const { return event(true, date, zenith); } + optional sunset(time::ESPTime date, num_t zenith) const { return event(false, date, zenith); } + optional event(bool rise, time::ESPTime date, num_t zenith) const { + // couldn't get the method described in chapter 15 to work, + // so instead this is based on the algorithm in time4j + // https://github.com/MenoData/Time4J/blob/master/base/src/main/java/net/time4j/calendar/astro/StdSolarCalculator.java + auto m = local_event_(date, 12); // noon + num_t jde = julian_day(m); + num_t new_h = 0, old_h; + do { + old_h = new_h; + auto x = local_hour_angle_(jde + old_h / 86400, rise, zenith); + if (!x.has_value()) + return {}; + new_h = *x; + } while (std::abs(new_h - old_h) >= 15); + time_t new_timestamp = m.timestamp + (time_t) new_h; + return time::ESPTime::from_epoch_local(new_timestamp); + } + + protected: + optional local_hour_angle_(num_t jde, bool rise, num_t zenith) const { + auto pos = SunAtTime(jde).equatorial_coordinate(); + num_t dec_rad = pos.declination_rad(); + num_t lat_rad = location.latitude_rad(); + num_t num = cos(radians(zenith)) - (sin(dec_rad) * sin(lat_rad)); + num_t denom = cos(dec_rad) * cos(lat_rad); + num_t cos_h = num / denom; + if (cos_h > 1 || cos_h < -1) + return {}; + num_t hour_angle = degrees(acos(cos_h)) * 240; + if (rise) + hour_angle *= -1; + return hour_angle; + } + + time::ESPTime local_event_(time::ESPTime date, int hour) const { + // input date should be in UTC, and hour/minute/second fields 0 + num_t added_d = hour / 24.0 - location.longitude / 360; + num_t jd = julian_day(date) + added_d; + + num_t eot = SunAtTime(jd).equation_of_time() * 240; + time_t new_timestamp = (time_t)(date.timestamp + added_d * 86400 - eot); + return time::ESPTime::from_epoch_utc(new_timestamp); + } +}; + +HorizontalCoordinate Sun::calc_coords_() { + SunAtLocation sun{location_}; + Moment m{time_->utcnow()}; + if (!m.dt.is_valid()) + return HorizontalCoordinate{NAN, NAN}; + + // uncomment to print some debug output + /* + SunAtTime st{m.jde()}; + st.debug(); + */ + return sun.true_coordinate(m); } +optional Sun::calc_event_(bool rising, double zenith) { + SunAtLocation sun{location_}; + auto now = this->time_->utcnow(); + if (!now.is_valid()) + return {}; + // Calculate UT1 timestamp at 0h + auto today = now; + today.hour = today.minute = today.second = 0; + today.recalc_timestamp_utc(); + + auto it = sun.event(rising, today, zenith); + if (it.has_value() && it->timestamp < now.timestamp) { + // We're calculating *next* sunrise/sunset, but calculated event + // is today, so try again tomorrow + time_t new_timestamp = today.timestamp + 24 * 60 * 60; + today = time::ESPTime::from_epoch_utc(new_timestamp); + it = sun.event(rising, today, zenith); + } + return it; +} + +optional Sun::sunrise(double elevation) { return this->calc_event_(true, 90 - elevation); } +optional Sun::sunset(double elevation) { return this->calc_event_(false, 90 - elevation); } +double Sun::elevation() { return this->calc_coords_().elevation; } +double Sun::azimuth() { return this->calc_coords_().azimuth; } } // namespace sun } // namespace esphome diff --git a/esphome/components/sun/sun.h b/esphome/components/sun/sun.h index 501d122da0..6a8364a5f0 100644 --- a/esphome/components/sun/sun.h +++ b/esphome/components/sun/sun.h @@ -8,85 +8,72 @@ namespace esphome { namespace sun { +namespace internal { + +/* Usually, ESPHome uses single-precision floating point values + * because those tend to be accurate enough and are more efficient. + * + * However, some of the data in this class has to be quite accurate, so double is + * used everywhere. + */ +using num_t = double; +struct GeoLocation { + num_t latitude; + num_t longitude; + + num_t latitude_rad() const; + num_t longitude_rad() const; +}; + +struct Moment { + time::ESPTime dt; + + num_t jd() const; + num_t jde() const; +}; + +struct EquatorialCoordinate { + num_t right_ascension; + num_t declination; + + num_t right_ascension_rad() const; + num_t declination_rad() const; +}; + +struct HorizontalCoordinate { + num_t elevation; + num_t azimuth; + + num_t elevation_rad() const; + num_t azimuth_rad() const; +}; + +} // namespace internal + class Sun { public: void set_time(time::RealTimeClock *time) { time_ = time; } time::RealTimeClock *get_time() const { return time_; } - void set_latitude(double latitude) { latitude_ = latitude; } - void set_longitude(double longitude) { longitude_ = longitude; } + void set_latitude(double latitude) { location_.latitude = latitude; } + void set_longitude(double longitude) { location_.longitude = longitude; } - optional sunrise(double elevation = 0.0); - optional sunset(double elevation = 0.0); + optional sunrise(double elevation); + optional sunset(double elevation); double elevation(); double azimuth(); protected: - double current_sun_time_() { return this->calc_sun_time_(this->time_->utcnow()); } - - /** Calculate the declination of the sun in rad. - * - * See https://en.wikipedia.org/wiki/Position_of_the_Sun#Declination_of_the_Sun_as_seen_from_Earth - * - * Accuracy: ±0.2° - * - * @param sun_time The day of the year, 1 means January 1st. See calc_sun_time_. - * @return Sun declination in degrees - */ - double sun_declination_(double sun_time); - - double elevation_ratio_(double sun_time); - - /** Calculate the hour angle based on the sun time of day in hours. - * - * Positive in morning, 0 at noon, negative in afternoon. - * - * @param sun_time Sun time, see calc_sun_time_. - * @return Hour angle in rad. - */ - double hour_angle_(double sun_time); - - double elevation_(double sun_time); - - double elevation_rad_(double sun_time); - - double zenith_rad_(double sun_time); - - double azimuth_rad_(double sun_time); - - double azimuth_(double sun_time); - - /** Return the sun time given by the time_ object. - * - * Sun time is defined as doubleing point day of year. - * Integer part encodes the day of the year (1=January 1st) - * Decimal part encodes time of day (1/24 = 1 hour) - */ - double calc_sun_time_(const time::ESPTime &time); - - uint32_t calc_epoch_(time::ESPTime base, double sun_time); - - /** Calculate the sun time of day - * - * @param day_of_year - * @param elevation - * @param rising - * @return - */ - double sun_time_for_elevation_(int32_t day_of_year, double elevation, bool rising); - - double latitude_rad_(); + internal::HorizontalCoordinate calc_coords_(); + optional calc_event_(bool rising, double zenith); time::RealTimeClock *time_; - /// Latitude in degrees, range: -90 to 90. - double latitude_; - /// Longitude in degrees, range: -180 to 180. - double longitude_; + internal::GeoLocation location_; }; class SunTrigger : public Trigger<>, public PollingComponent, public Parented { public: - SunTrigger() : PollingComponent(1000) {} + SunTrigger() : PollingComponent(60000) {} void set_sunrise(bool sunrise) { sunrise_ = sunrise; } void set_elevation(double elevation) { elevation_ = elevation; } diff --git a/script/ci-custom.py b/script/ci-custom.py index 3106cabeff..4ec7c664a4 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -405,6 +405,7 @@ ARDUINO_FORBIDDEN_RE = r"[^\w\d](" + r"|".join(ARDUINO_FORBIDDEN) + r")\(.*" include=cpp_include, exclude=[ "esphome/components/mqtt/custom_mqtt_device.h", + "esphome/components/sun/sun.cpp", "esphome/core/esphal.*", ], ) From d92c8ccadf0bcf30db550bb94838c96bbe101c9c Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 8 Apr 2021 13:57:29 +0200 Subject: [PATCH 14/62] Raise minimum python version to 3.7 (#1673) --- esphome/__main__.py | 6 +++--- setup.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 20cb44d11c..a41fce56d8 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -638,10 +638,10 @@ def run_esphome(argv): _LOGGER.error("Missing configuration parameter, see esphome --help.") return 1 - if sys.version_info < (3, 6, 0): + if sys.version_info < (3, 7, 0): _LOGGER.error( - "You're running ESPHome with Python <3.6. ESPHome is no longer compatible " - "with this Python version. Please reinstall ESPHome with Python 3.6+" + "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " + "with this Python version. Please reinstall ESPHome with Python 3.7+" ) return 1 diff --git a/setup.py b/setup.py index 3b4b77d61e..6e4d4b06be 100755 --- a/setup.py +++ b/setup.py @@ -74,7 +74,7 @@ setup( zip_safe=False, platforms="any", test_suite="tests", - python_requires=">=3.6,<4.0", + python_requires=">=3.7,<4.0", install_requires=REQUIRES, keywords=["home", "automation"], entry_points={"console_scripts": ["esphome = esphome.__main__:main"]}, From 99f14e03d4f33c27a26ce373ad7e6cfe5460947b Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 8 Apr 2021 13:58:01 +0200 Subject: [PATCH 15/62] Fix colorlog removing colors and refactor color code (#1671) --- esphome/__main__.py | 57 +++++------------------------- esphome/api/client.py | 5 +-- esphome/config.py | 31 +++++++++------- esphome/helpers.py | 11 ------ esphome/log.py | 82 +++++++++++++++++++++++++++++++++++++++++++ esphome/mqtt.py | 4 +-- esphome/wizard.py | 69 +++++++++++++++++++----------------- requirements.txt | 1 - 8 files changed, 151 insertions(+), 109 deletions(-) create mode 100644 esphome/log.py diff --git a/esphome/__main__.py b/esphome/__main__.py index a41fce56d8..1ec72d9255 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -19,7 +19,7 @@ from esphome.const import ( CONF_PLATFORMIO_OPTIONS, ) from esphome.core import CORE, EsphomeError, coroutine, coroutine_with_priority -from esphome.helpers import color, indent +from esphome.helpers import indent from esphome.util import ( run_external_command, run_external_process, @@ -27,6 +27,7 @@ from esphome.util import ( list_yaml_files, get_serial_ports, ) +from esphome.log import color, setup_log, Fore _LOGGER = logging.getLogger(__name__) @@ -57,7 +58,7 @@ def choose_prompt(options): raise ValueError break except ValueError: - safe_print(color("red", f"Invalid option: '{opt}'")) + safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) return options[opt - 1][1] @@ -263,46 +264,6 @@ def clean_mqtt(config, args): ) -def setup_log(debug=False, quiet=False): - if debug: - log_level = logging.DEBUG - CORE.verbose = True - elif quiet: - log_level = logging.CRITICAL - else: - log_level = logging.INFO - logging.basicConfig(level=log_level) - fmt = "%(levelname)s %(message)s" - colorfmt = f"%(log_color)s{fmt}%(reset)s" - datefmt = "%H:%M:%S" - - logging.getLogger("urllib3").setLevel(logging.WARNING) - - try: - import colorama - - colorama.init(strip=True) - - from colorlog import ColoredFormatter - - logging.getLogger().handlers[0].setFormatter( - ColoredFormatter( - colorfmt, - datefmt=datefmt, - reset=True, - log_colors={ - "DEBUG": "cyan", - "INFO": "green", - "WARNING": "yellow", - "ERROR": "red", - "CRITICAL": "red", - }, - ) - ) - except ImportError: - pass - - def command_wizard(args): from esphome import wizard @@ -442,30 +403,30 @@ def command_update_all(args): click.echo(f"{half_line}{middle_text}{half_line}") for f in files: - print("Updating {}".format(color("cyan", f))) + print("Updating {}".format(color(Fore.CYAN, f))) print("-" * twidth) print() rc = run_external_process( "esphome", "--dashboard", f, "run", "--no-logs", "--upload-port", "OTA" ) if rc == 0: - print_bar("[{}] {}".format(color("bold_green", "SUCCESS"), f)) + print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) success[f] = True else: - print_bar("[{}] {}".format(color("bold_red", "ERROR"), f)) + print_bar("[{}] {}".format(color(Fore.BOLD_RED, "ERROR"), f)) success[f] = False print() print() print() - print_bar("[{}]".format(color("bold_white", "SUMMARY"))) + print_bar("[{}]".format(color(Fore.BOLD_WHITE, "SUMMARY"))) failed = 0 for f in files: if success[f]: - print(" - {}: {}".format(f, color("green", "SUCCESS"))) + print(" - {}: {}".format(f, color(Fore.GREEN, "SUCCESS"))) else: - print(" - {}: {}".format(f, color("bold_red", "FAILED"))) + print(" - {}: {}".format(f, color(Fore.BOLD_RED, "FAILED"))) failed += 1 return failed diff --git a/esphome/api/client.py b/esphome/api/client.py index 84c9890fe0..dd11f79922 100644 --- a/esphome/api/client.py +++ b/esphome/api/client.py @@ -13,7 +13,8 @@ from esphome import const import esphome.api.api_pb2 as pb from esphome.const import CONF_PASSWORD, CONF_PORT from esphome.core import EsphomeError -from esphome.helpers import resolve_ip_address, indent, color +from esphome.helpers import resolve_ip_address, indent +from esphome.log import color, Fore from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -488,7 +489,7 @@ def run_logs(config, address): text = msg.message if msg.send_failed: text = color( - "white", + Fore.WHITE, "(Message skipped because it was too big to fit in " "TCP buffer - This is only cosmetic)", ) diff --git a/esphome/config.py b/esphome/config.py index 3317196965..0c8e51fdce 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -19,13 +19,14 @@ from esphome.const import ( CONF_SUBSTITUTIONS, ) from esphome.core import CORE, EsphomeError # noqa -from esphome.helpers import color, indent +from esphome.helpers import indent from esphome.util import safe_print, OrderedDict from typing import List, Optional, Tuple, Union # noqa from esphome.core import ConfigType # noqa from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid +from esphome.log import color, Fore _LOGGER = logging.getLogger(__name__) @@ -790,7 +791,7 @@ def line_info(config, path, highlight=True): if obj: mark = obj.start_mark source = "[source {}:{}]".format(mark.document, mark.line + 1) - return color("cyan", source) + return color(Fore.CYAN, source) return "None" @@ -813,7 +814,9 @@ def dump_dict(config, path, at_root=True): if at_root: error = config.get_error_for_path(path) if error is not None: - ret += "\n" + color("bold_red", _format_vol_invalid(error, config)) + "\n" + ret += ( + "\n" + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) + "\n" + ) if isinstance(conf, (list, tuple)): multiline = True @@ -826,12 +829,14 @@ def dump_dict(config, path, at_root=True): error = config.get_error_for_path(path_) if error is not None: ret += ( - "\n" + color("bold_red", _format_vol_invalid(error, config)) + "\n" + "\n" + + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) + + "\n" ) sep = "- " if config.is_in_error_path(path_): - sep = color("red", sep) + sep = color(Fore.RED, sep) msg, _ = dump_dict(config, path_, at_root=False) msg = indent(msg) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) @@ -851,12 +856,14 @@ def dump_dict(config, path, at_root=True): error = config.get_error_for_path(path_) if error is not None: ret += ( - "\n" + color("bold_red", _format_vol_invalid(error, config)) + "\n" + "\n" + + color(Fore.BOLD_RED, _format_vol_invalid(error, config)) + + "\n" ) st = f"{k}: " if config.is_in_error_path(path_): - st = color("red", st) + st = color(Fore.RED, st) msg, m = dump_dict(config, path_, at_root=False) inf = line_info(config, path_, highlight=config.is_in_error_path(path_)) @@ -878,7 +885,7 @@ def dump_dict(config, path, at_root=True): if len(conf) > 80: conf = "|-\n" + indent(conf) error = config.get_error_for_path(path) - col = "bold_red" if error else "white" + col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, str(conf)) elif isinstance(conf, core.Lambda): if is_secret(conf): @@ -886,13 +893,13 @@ def dump_dict(config, path, at_root=True): conf = "!lambda |-\n" + indent(str(conf.value)) error = config.get_error_for_path(path) - col = "bold_red" if error else "white" + col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, conf) elif conf is None: pass else: error = config.get_error_for_path(path) - col = "bold_red" if error else "white" + col = Fore.BOLD_RED if error else Fore.KEEP ret += color(col, str(conf)) multiline = "\n" in ret @@ -934,13 +941,13 @@ def read_config(command_line_substitutions): if not CORE.verbose: res = strip_default_ids(res) - safe_print(color("bold_red", "Failed config")) + safe_print(color(Fore.BOLD_RED, "Failed config")) safe_print("") for path, domain in res.output_paths: if not res.is_in_error_path(path): continue - errstr = color("bold_red", f"{domain}:") + errstr = color(Fore.BOLD_RED, f"{domain}:") errline = line_info(res, path) if errline: errstr += " " + errline diff --git a/esphome/helpers.py b/esphome/helpers.py index 780a2aa88e..b80d338eef 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -57,17 +57,6 @@ def cpp_string_escape(string, encoding="utf-8"): return '"' + result + '"' -def color(the_color, message=""): - from colorlog.escape_codes import escape_codes, parse_colors - - if not message: - res = parse_colors(the_color) - else: - res = parse_colors(the_color) + message + escape_codes["reset"] - - return res - - def run_system_command(*args): import subprocess diff --git a/esphome/log.py b/esphome/log.py new file mode 100644 index 0000000000..fa79efa833 --- /dev/null +++ b/esphome/log.py @@ -0,0 +1,82 @@ +import logging + +from esphome.core import CORE + + +class AnsiFore: + KEEP = "" + BLACK = "\033[30m" + RED = "\033[31m" + GREEN = "\033[32m" + YELLOW = "\033[33m" + BLUE = "\033[34m" + MAGENTA = "\033[35m" + CYAN = "\033[36m" + WHITE = "\033[37m" + RESET = "\033[39m" + + BOLD_BLACK = "\033[1;30m" + BOLD_RED = "\033[1;31m" + BOLD_GREEN = "\033[1;32m" + BOLD_YELLOW = "\033[1;33m" + BOLD_BLUE = "\033[1;34m" + BOLD_MAGENTA = "\033[1;35m" + BOLD_CYAN = "\033[1;36m" + BOLD_WHITE = "\033[1;37m" + BOLD_RESET = "\033[1;39m" + + +class AnsiStyle: + BRIGHT = "\033[1m" + BOLD = "\033[1m" + DIM = "\033[2m" + THIN = "\033[2m" + NORMAL = "\033[22m" + RESET_ALL = "\033[0m" + + +Fore = AnsiFore() +Style = AnsiStyle() + + +def color(col: str, msg: str, reset: bool = True) -> bool: + if col and not col.startswith("\033["): + raise ValueError("Color must be value from esphome.log.Fore") + s = str(col) + msg + if reset and col: + s += str(Style.RESET_ALL) + return s + + +class ESPHomeLogFormatter(logging.Formatter): + def __init__(self): + super().__init__(fmt="%(levelname)s %(message)s", datefmt="%H:%M:%S", style="%") + + def format(self, record): + formatted = super().format(record) + prefix = { + "DEBUG": Fore.CYAN, + "INFO": Fore.GREEN, + "WARNING": Fore.YELLOW, + "ERROR": Fore.RED, + "CRITICAL": Fore.RED, + }.get(record.levelname, "") + return f"{prefix}{formatted}{Style.RESET_ALL}" + + +def setup_log(debug=False, quiet=False): + import colorama + + if debug: + log_level = logging.DEBUG + CORE.verbose = True + elif quiet: + log_level = logging.CRITICAL + else: + log_level = logging.INFO + logging.basicConfig(level=log_level) + + logging.getLogger("urllib3").setLevel(logging.WARNING) + + colorama.init() + logging.getLogger().handlers[0].setFormatter(ESPHomeLogFormatter()) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index 86937ba37e..9be87b5c5d 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -22,7 +22,7 @@ from esphome.const import ( CONF_USERNAME, ) from esphome.core import CORE, EsphomeError -from esphome.helpers import color +from esphome.log import color, Fore from esphome.util import safe_print _LOGGER = logging.getLogger(__name__) @@ -158,7 +158,7 @@ def get_fingerprint(config): sha1 = hashlib.sha1(cert_der).hexdigest() - safe_print("SHA1 Fingerprint: " + color("cyan", sha1)) + safe_print("SHA1 Fingerprint: " + color(Fore.CYAN, sha1)) safe_print( "Copy the string above into mqtt.ssl_fingerprints section of {}" "".format(CORE.config_path) diff --git a/esphome/wizard.py b/esphome/wizard.py index 620ceb9b59..4ad5c63216 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -6,7 +6,8 @@ import unicodedata import voluptuous as vol import esphome.config_validation as cv -from esphome.helpers import color, get_bool_env, write_file +from esphome.helpers import get_bool_env, write_file +from esphome.log import color, Fore # pylint: disable=anomalous-backslash-in-string from esphome.pins import ESP32_BOARD_PINS, ESP8266_BOARD_PINS @@ -148,13 +149,13 @@ def wizard(path): if not path.endswith(".yaml") and not path.endswith(".yml"): safe_print( "Please make your configuration file {} have the extension .yaml or .yml" - "".format(color("cyan", path)) + "".format(color(Fore.CYAN, path)) ) return 1 if os.path.exists(path): safe_print( "Uh oh, it seems like {} already exists, please delete that file first " - "or chose another configuration file.".format(color("cyan", path)) + "or chose another configuration file.".format(color(Fore.CYAN, path)) ) return 2 safe_print("Hi there!") @@ -171,7 +172,7 @@ def wizard(path): safe_print() safe_print_step(1, CORE_BIG) safe_print( - "First up, please choose a " + color("green", "name") + " for your node." + "First up, please choose a " + color(Fore.GREEN, "name") + " for your node." ) safe_print( "It should be a unique name that can be used to identify the device later." @@ -179,12 +180,12 @@ def wizard(path): sleep(1) safe_print( "For example, I like calling the node in my living room {}.".format( - color("bold_white", "livingroom") + color(Fore.BOLD_WHITE, "livingroom") ) ) safe_print() sleep(1) - name = input(color("bold_white", "(name): ")) + name = input(color(Fore.BOLD_WHITE, "(name): ")) while True: try: @@ -193,7 +194,7 @@ def wizard(path): except vol.Invalid: safe_print( color( - "red", + Fore.RED, f'Oh noes, "{name}" isn\'t a valid name. Names can only ' f"include numbers, lower-case letters, underscores and " f"hyphens.", @@ -202,12 +203,12 @@ def wizard(path): name = strip_accents(name).lower().replace(" ", "_") name = "".join(c for c in name if c in ALLOWED_NAME_CHARS) safe_print( - 'Shall I use "{}" as the name instead?'.format(color("cyan", name)) + 'Shall I use "{}" as the name instead?'.format(color(Fore.CYAN, name)) ) sleep(0.5) name = default_input("(name [{}]): ", name) - safe_print('Great! Your node is now called "{}".'.format(color("cyan", name))) + safe_print('Great! Your node is now called "{}".'.format(color(Fore.CYAN, name))) sleep(1) safe_print_step(2, ESP_BIG) safe_print( @@ -216,16 +217,16 @@ def wizard(path): ) safe_print( "Are you using an " - + color("green", "ESP32") + + color(Fore.GREEN, "ESP32") + " or " - + color("green", "ESP8266") + + color(Fore.GREEN, "ESP8266") + " platform? (Choose ESP8266 for Sonoff devices)" ) while True: sleep(0.5) safe_print() safe_print("Please enter either ESP32 or ESP8266.") - platform = input(color("bold_white", "(ESP32/ESP8266): ")) + platform = input(color(Fore.BOLD_WHITE, "(ESP32/ESP8266): ")) try: platform = vol.All(vol.Upper, vol.Any("ESP32", "ESP8266"))(platform) break @@ -235,7 +236,7 @@ def wizard(path): '"{}". Please try again.'.format(platform) ) safe_print( - "Thanks! You've chosen {} as your platform.".format(color("cyan", platform)) + "Thanks! You've chosen {} as your platform.".format(color(Fore.CYAN, platform)) ) safe_print() sleep(1) @@ -250,37 +251,39 @@ def wizard(path): ) safe_print( - "Next, I need to know what " + color("green", "board") + " you're using." + "Next, I need to know what " + color(Fore.GREEN, "board") + " you're using." ) sleep(0.5) - safe_print("Please go to {} and choose a board.".format(color("green", board_link))) + safe_print( + "Please go to {} and choose a board.".format(color(Fore.GREEN, board_link)) + ) if platform == "ESP32": - safe_print("(Type " + color("green", "esp01_1m") + " for Sonoff devices)") + safe_print("(Type " + color(Fore.GREEN, "esp01_1m") + " for Sonoff devices)") safe_print() # Don't sleep because user needs to copy link if platform == "ESP32": - safe_print('For example "{}".'.format(color("bold_white", "nodemcu-32s"))) + safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "nodemcu-32s"))) boards = list(ESP32_BOARD_PINS.keys()) else: - safe_print('For example "{}".'.format(color("bold_white", "nodemcuv2"))) + safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "nodemcuv2"))) boards = list(ESP8266_BOARD_PINS.keys()) safe_print("Options: {}".format(", ".join(sorted(boards)))) while True: - board = input(color("bold_white", "(board): ")) + board = input(color(Fore.BOLD_WHITE, "(board): ")) try: board = vol.All(vol.Lower, vol.Any(*boards))(board) break except vol.Invalid: safe_print( - color("red", f'Sorry, I don\'t think the board "{board}" exists.') + color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.') ) safe_print() sleep(0.25) safe_print() safe_print( - "Way to go! You've chosen {} as your board.".format(color("cyan", board)) + "Way to go! You've chosen {} as your board.".format(color(Fore.CYAN, board)) ) safe_print() sleep(1) @@ -291,20 +294,20 @@ def wizard(path): sleep(1) safe_print( "First, what's the " - + color("green", "SSID") + + color(Fore.GREEN, "SSID") + f" (the name) of the WiFi network {name} I should connect to?" ) sleep(1.5) - safe_print('For example "{}".'.format(color("bold_white", "Abraham Linksys"))) + safe_print('For example "{}".'.format(color(Fore.BOLD_WHITE, "Abraham Linksys"))) while True: - ssid = input(color("bold_white", "(ssid): ")) + ssid = input(color(Fore.BOLD_WHITE, "(ssid): ")) try: ssid = cv.ssid(ssid) break except vol.Invalid: safe_print( color( - "red", + Fore.RED, 'Unfortunately, "{}" doesn\'t seem to be a valid SSID. ' "Please try again.".format(ssid), ) @@ -314,20 +317,20 @@ def wizard(path): safe_print( 'Thank you very much! You\'ve just chosen "{}" as your SSID.' - "".format(color("cyan", ssid)) + "".format(color(Fore.CYAN, ssid)) ) safe_print() sleep(0.75) safe_print( "Now please state the " - + color("green", "password") + + color(Fore.GREEN, "password") + " of the WiFi network so that I can connect to it (Leave empty for no password)" ) safe_print() - safe_print('For example "{}"'.format(color("bold_white", "PASSWORD42"))) + safe_print('For example "{}"'.format(color(Fore.BOLD_WHITE, "PASSWORD42"))) sleep(0.5) - psk = input(color("bold_white", "(PSK): ")) + psk = input(color(Fore.BOLD_WHITE, "(PSK): ")) safe_print( "Perfect! WiFi is now set up (you can create static IPs and so on later)." ) @@ -340,12 +343,12 @@ def wizard(path): ) safe_print( "This can be insecure if you do not trust the WiFi network. Do you want to set " - "a " + color("green", "password") + " for connecting to this ESP?" + "a " + color(Fore.GREEN, "password") + " for connecting to this ESP?" ) safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = input(color("bold_white", "(password): ")) + password = input(color(Fore.BOLD_WHITE, "(password): ")) wizard_write( path=path, @@ -359,8 +362,8 @@ def wizard(path): safe_print() safe_print( - color("cyan", "DONE! I've now written a new configuration file to ") - + color("bold_cyan", path) + color(Fore.CYAN, "DONE! I've now written a new configuration file to ") + + color(Fore.BOLD_CYAN, path) ) safe_print() safe_print("Next steps:") diff --git a/requirements.txt b/requirements.txt index aed9f97d8a..2fe8909e16 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ voluptuous==0.12.1 PyYAML==5.4.1 paho-mqtt==1.5.1 colorama==0.4.4 -colorlog==4.7.2 tornado==6.1 protobuf==3.15.7 tzlocal==2.1 From bf01c22e1ffdd30144db23a0249dce90334b57e2 Mon Sep 17 00:00:00 2001 From: rbaron Date: Thu, 8 Apr 2021 13:59:30 +0200 Subject: [PATCH 16/62] Adds support for b-parasite soil moisture sensor (#1666) --- CODEOWNERS | 1 + esphome/components/b_parasite/__init__.py | 0 esphome/components/b_parasite/b_parasite.cpp | 82 ++++++++++++++++++++ esphome/components/b_parasite/b_parasite.h | 40 ++++++++++ esphome/components/b_parasite/sensor.py | 68 ++++++++++++++++ tests/test2.yaml | 10 +++ 6 files changed, 201 insertions(+) create mode 100644 esphome/components/b_parasite/__init__.py create mode 100644 esphome/components/b_parasite/b_parasite.cpp create mode 100644 esphome/components/b_parasite/b_parasite.h create mode 100644 esphome/components/b_parasite/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ef2af83fa7..aad7cc3a19 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -18,6 +18,7 @@ esphome/components/animation/* @syndlex esphome/components/api/* @OttoWinter esphome/components/async_tcp/* @OttoWinter esphome/components/atc_mithermometer/* @ahpohl +esphome/components/b_parasite/* @rbaron esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core esphome/components/bme680_bsec/* @trvrnrth diff --git a/esphome/components/b_parasite/__init__.py b/esphome/components/b_parasite/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/b_parasite/b_parasite.cpp b/esphome/components/b_parasite/b_parasite.cpp new file mode 100644 index 0000000000..828e392993 --- /dev/null +++ b/esphome/components/b_parasite/b_parasite.cpp @@ -0,0 +1,82 @@ +#include "b_parasite.h" +#include "esphome/core/log.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace b_parasite { + +static const char* TAG = "b_parasite"; + +void BParasite::dump_config() { + ESP_LOGCONFIG(TAG, "b_parasite"); + LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_); + LOG_SENSOR(" ", "Temperature", this->temperature_); + LOG_SENSOR(" ", "Humidity", this->humidity_); + LOG_SENSOR(" ", "Soil Moisture", this->soil_moisture_); +} + +bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice& device) { + if (device.address_uint64() != address_) { + ESP_LOGVV(TAG, "parse_device(): unknown MAC address."); + return false; + } + ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str()); + const auto& service_datas = device.get_service_datas(); + if (service_datas.size() != 1) { + ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size()); + return false; + } + const auto& service_data = service_datas[0]; + + ESP_LOGVV(TAG, "Service data:"); + for (const uint8_t byte : service_data.data) { + ESP_LOGVV(TAG, "0x%02x", byte); + } + + const auto& data = service_data.data; + + // Counter for deduplicating messages. + uint8_t counter = data[1] & 0x0f; + if (last_processed_counter_ == counter) { + ESP_LOGVV(TAG, "Skipping already processed counter (%u)", counter); + return false; + } + + // Battery voltage in millivolts. + uint16_t battery_millivolt = data[2] << 8 | data[3]; + float battery_voltage = battery_millivolt / 1000.0f; + + // Temperature in 1000 * Celcius. + uint16_t temp_millicelcius = data[4] << 8 | data[5]; + float temp_celcius = temp_millicelcius / 1000.0f; + + // Relative air humidity in the range [0, 2^16). + uint16_t humidity = data[6] << 8 | data[7]; + float humidity_percent = (100.0f * humidity) / (1 << 16); + + // Relative soil moisture in [0 - 2^16). + uint16_t soil_moisture = data[8] << 8 | data[9]; + float moisture_percent = (100.0f * soil_moisture) / (1 << 16); + + if (battery_voltage_ != nullptr) { + battery_voltage_->publish_state(battery_voltage); + } + if (temperature_ != nullptr) { + temperature_->publish_state(temp_celcius); + } + if (humidity_ != nullptr) { + humidity_->publish_state(humidity_percent); + } + if (soil_moisture_ != nullptr) { + soil_moisture_->publish_state(moisture_percent); + } + + last_processed_counter_ = counter; + return true; +} + +} // namespace b_parasite +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/b_parasite/b_parasite.h b/esphome/components/b_parasite/b_parasite.h new file mode 100644 index 0000000000..04f648ab63 --- /dev/null +++ b/esphome/components/b_parasite/b_parasite.h @@ -0,0 +1,40 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace b_parasite { + +class BParasite : public Component, public esp32_ble_tracker::ESPBTDeviceListener { + public: + void set_address(uint64_t address) { address_ = address; }; + void set_bindkey(const std::string &bindkey); + + bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + + void set_battery_voltage(sensor::Sensor *battery_voltage) { battery_voltage_ = battery_voltage; } + void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; } + void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; } + void set_soil_moisture(sensor::Sensor *soil_moisture) { soil_moisture_ = soil_moisture; } + + protected: + // The received advertisement packet contains an unsigned 4 bits wrap-around counter + // for deduplicating messages. + int8_t last_processed_counter_ = -1; + uint64_t address_; + sensor::Sensor *battery_voltage_{nullptr}; + sensor::Sensor *temperature_{nullptr}; + sensor::Sensor *humidity_{nullptr}; + sensor::Sensor *soil_moisture_{nullptr}; +}; + +} // namespace b_parasite +} // namespace esphome + +#endif // ARDUINO_ARCH_ESP32 diff --git a/esphome/components/b_parasite/sensor.py b/esphome/components/b_parasite/sensor.py new file mode 100644 index 0000000000..d90ea84cd3 --- /dev/null +++ b/esphome/components/b_parasite/sensor.py @@ -0,0 +1,68 @@ +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_VOLTAGE, + CONF_HUMIDITY, + CONF_ID, + CONF_MOISTURE, + CONF_MAC_ADDRESS, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + UNIT_CELSIUS, + UNIT_PERCENT, + UNIT_VOLT, +) + +CODEOWNERS = ["@rbaron"] + +DEPENDENCIES = ["esp32_ble_tracker"] + +b_parasite_ns = cg.esphome_ns.namespace("b_parasite") +BParasite = b_parasite_ns.class_( + "BParasite", esp32_ble_tracker.ESPBTDeviceListener, cg.Component +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BParasite), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_EMPTY, 1, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE + ), + cv.Optional(CONF_MOISTURE): sensor.sensor_schema( + UNIT_PERCENT, ICON_EMPTY, 1, DEVICE_CLASS_HUMIDITY + ), + } + ) + .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)) + + for (config_key, setter) in [ + (CONF_TEMPERATURE, var.set_temperature), + (CONF_HUMIDITY, var.set_humidity), + (CONF_BATTERY_VOLTAGE, var.set_battery_voltage), + (CONF_MOISTURE, var.set_soil_moisture), + ]: + if config_key in config: + sens = yield sensor.new_sensor(config[config_key]) + cg.add(setter(sens)) diff --git a/tests/test2.yaml b/tests/test2.yaml index 616fe32139..04514cbef2 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -70,6 +70,16 @@ sensor: - platform: ble_rssi service_uuid: '11223344-5566-7788-99aa-bbccddeeff00' name: 'BLE Test Service 128' + - platform: b_parasite + mac_address: F0:CA:F0:CA:01:01 + humidity: + name: 'b-parasite Air Humidity' + temperature: + name: 'b-parasite Air Temperature' + moisture: + name: 'b-parasite Soil Moisture' + battery_voltage: + name: 'b-parasite Battery Voltage' - platform: senseair id: senseair0 co2: From 76a6c39f25f187d9b9209c857df2c6a5881d761e Mon Sep 17 00:00:00 2001 From: Anthony Uk <1492471+dataway@users.noreply.github.com> Date: Thu, 8 Apr 2021 14:21:42 +0200 Subject: [PATCH 17/62] mqtt_client: Added MQTTClientComponent::unsubscribe() (#1672) Co-authored-by: Otto Winter --- esphome/components/mqtt/mqtt_client.cpp | 20 ++++++++++++++++++++ esphome/components/mqtt/mqtt_client.h | 9 +++++++++ 2 files changed, 29 insertions(+) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 2eb1c52153..190df0b2f3 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -347,6 +347,26 @@ void MQTTClientComponent::subscribe_json(const std::string &topic, mqtt_json_cal this->subscriptions_.push_back(subscription); } +void MQTTClientComponent::unsubscribe(const std::string &topic) { + uint16_t ret = this->mqtt_client_.unsubscribe(topic.c_str()); + yield(); + if (ret != 0) { + ESP_LOGV(TAG, "unsubscribe(topic='%s')", topic.c_str()); + } else { + delay(5); + ESP_LOGV(TAG, "Unsubscribe failed for topic='%s'.", topic.c_str()); + this->status_momentary_warning("unsubscribe", 1000); + } + + auto it = subscriptions_.begin(); + while (it != subscriptions_.end()) { + if (it->topic == topic) + it = subscriptions_.erase(it); + else + ++it; + } +} + // Publish bool MQTTClientComponent::publish(const std::string &topic, const std::string &payload, uint8_t qos, bool retain) { return this->publish(topic, payload.data(), payload.size(), qos, retain); diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 2bbebff845..885f4a9a96 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -159,6 +159,15 @@ class MQTTClientComponent : public Component { */ void subscribe_json(const std::string &topic, mqtt_json_callback_t callback, uint8_t qos = 0); + /** Unsubscribe from an MQTT topic. + * + * If multiple existing subscriptions to the same topic exist, all of them will be removed. + * + * @param topic The topic to unsubscribe from. + * Must match the topic in the original subscribe or subscribe_json call exactly. + */ + void unsubscribe(const std::string &topic); + /** Publish a MQTTMessage * * @param message The message. From 2e50e1f50671d53aec57e78ad0f3e3c50c363ba4 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Thu, 8 Apr 2021 09:22:30 -0300 Subject: [PATCH 18/62] Fix servo detach chopped PWM (#1650) --- esphome/components/esp8266_pwm/esp8266_pwm.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esphome/components/esp8266_pwm/esp8266_pwm.cpp b/esphome/components/esp8266_pwm/esp8266_pwm.cpp index 96290871e0..b3fd2398f3 100644 --- a/esphome/components/esp8266_pwm/esp8266_pwm.cpp +++ b/esphome/components/esp8266_pwm/esp8266_pwm.cpp @@ -37,6 +37,11 @@ void HOT ESP8266PWM::write_state(float state) { uint32_t duty_off = total_time_us - duty_on; if (duty_on == 0) { + // This is a hacky fix for servos: Servo PWM high time is maximum 2.4ms by default + // The frequency check is to affect this fix for servos mostly as the frequency is usually 50-300 hz + if (this->pin_->digital_read() && 50 <= this->frequency_ && this->frequency_ <= 300) { + delay(3); + } stopWaveform(this->pin_->get_pin()); this->pin_->digital_write(this->pin_->is_inverted()); } else if (duty_off == 0) { From eaf9735edabfa4eba24fd3175743a2268f9989ed Mon Sep 17 00:00:00 2001 From: Peter Kuehne Date: Thu, 8 Apr 2021 13:26:01 +0100 Subject: [PATCH 19/62] Disallow _ in node name (#1632) --- esphome/const.py | 3 ++- esphome/dashboard/static/js/esphome.js | 4 ++++ esphome/dashboard/templates/index.html | 4 ++-- esphome/wizard.py | 6 +++--- tests/unit_tests/test_config_validation.py | 2 +- tests/unit_tests/test_wizard.py | 11 ++++++----- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/esphome/const.py b/esphome/const.py index f33b4ee406..35d4b016ce 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -10,7 +10,8 @@ ESP_PLATFORM_ESP32 = "ESP32" ESP_PLATFORM_ESP8266 = "ESP8266" ESP_PLATFORMS = [ESP_PLATFORM_ESP32, ESP_PLATFORM_ESP8266] -ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_-" +ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" + # Lookup table from ESP32 arduino framework version to latest platformio # package with that version # See also https://github.com/platformio/platform-espressif32/releases diff --git a/esphome/dashboard/static/js/esphome.js b/esphome/dashboard/static/js/esphome.js index 52dda446ce..e861f169df 100644 --- a/esphome/dashboard/static/js/esphome.js +++ b/esphome/dashboard/static/js/esphome.js @@ -1012,6 +1012,10 @@ jQuery.validator.addMethod("nospaces", (value, element) => { return value.indexOf(' ') < 0; }, "Name cannot contain any spaces!"); +jQuery.validator.addMethod("nounderscores", (value, element) => { + return value.indexOf('_') < 0; +}, "Name cannot contain underscores!"); + jQuery.validator.addMethod("lowercase", (value, element) => { return value === value.toLowerCase(); }, "Name must be all lower case!"); diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index 142bc2cd6f..dee10bfc27 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -359,12 +359,12 @@

Names must be all lowercase and must not contain any spaces! Characters that are allowed are: a-z, - 0-9, _ and -. + 0-9 and -.

+ data-rule-lowercase="true" data-rule-nounderscores="true" required>
diff --git a/esphome/wizard.py b/esphome/wizard.py index 4ad5c63216..3550e39392 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -196,11 +196,11 @@ def wizard(path): color( Fore.RED, f'Oh noes, "{name}" isn\'t a valid name. Names can only ' - f"include numbers, lower-case letters, underscores and " - f"hyphens.", + f"include numbers, lower-case letters and hyphens. ", ) ) - name = strip_accents(name).lower().replace(" ", "_") + name = strip_accents(name).lower().replace(" ", "-") + name = strip_accents(name).lower().replace("_", "-") name = "".join(c for c in name if c in ALLOWED_NAME_CHARS) safe_print( 'Shall I use "{}" as the name instead?'.format(color(Fore.CYAN, name)) diff --git a/tests/unit_tests/test_config_validation.py b/tests/unit_tests/test_config_validation.py index 949d4251ee..16cfb16e94 100644 --- a/tests/unit_tests/test_config_validation.py +++ b/tests/unit_tests/test_config_validation.py @@ -27,7 +27,7 @@ def test_alphanumeric__invalid(value): config_validation.alphanumeric(value) -@given(value=text(alphabet=string.ascii_lowercase + string.digits + "_-")) +@given(value=text(alphabet=string.ascii_lowercase + string.digits + "-_")) def test_valid_name__valid(value): actual = config_validation.valid_name(value) diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index 6c952608d4..0ca7c83e1b 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -9,7 +9,7 @@ from mock import MagicMock @pytest.fixture def default_config(): return { - "name": "test_name", + "name": "test-name", "platform": "test_platform", "board": "test_board", "ssid": "test_ssid", @@ -21,7 +21,7 @@ def default_config(): @pytest.fixture def wizard_answers(): return [ - "test_node", # Name of the node + "test-node", # Name of the node "ESP8266", # platform "nodemcuv2", # board "SSID", # ssid @@ -305,13 +305,14 @@ def test_wizard_offers_better_node_name(tmpdir, monkeypatch, wizard_answers): """ When the node name does not conform, a better alternative is offered * Removes special chars - * Replaces spaces with underscores + * Replaces spaces with hyphens + * Replaces underscores with hyphens * Converts all uppercase letters to lowercase """ # Given - wizard_answers[0] = "Küche #2" - expected_name = "kuche_2" + wizard_answers[0] = "Küche_Unten #2" + expected_name = "kuche-unten-2" monkeypatch.setattr( wz, "default_input", MagicMock(side_effect=lambda _, default: default) ) From e6f8e73705dc310cb8b188fd509b543fe3ad7cf9 Mon Sep 17 00:00:00 2001 From: Gareth Cooper Date: Thu, 8 Apr 2021 13:26:34 +0100 Subject: [PATCH 20/62] Receive long MQTT payload (#1590) --- esphome/components/mqtt/mqtt_client.cpp | 14 +++++++++++--- esphome/components/mqtt/mqtt_client.h | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 190df0b2f3..b8743fc142 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -25,9 +25,17 @@ void MQTTClientComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up MQTT..."); this->mqtt_client_.onMessage([this](char *topic, char *payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { - std::string payload_s(payload, len); - std::string topic_s(topic); - this->on_message(topic_s, payload_s); + if (index == 0) + this->payload_buffer_.reserve(total); + + // append new payload, may contain incomplete MQTT message + this->payload_buffer_.append(payload, len); + + // MQTT fully received + if (len + index == total) { + this->on_message(topic, this->payload_buffer_); + this->payload_buffer_.clear(); + } }); this->mqtt_client_.onDisconnect([this](AsyncMqttClientDisconnectReason reason) { this->state_ = MQTT_CLIENT_DISCONNECTED; diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 885f4a9a96..e4f7c479b2 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -259,6 +259,7 @@ class MQTTClientComponent : public Component { }; std::string topic_prefix_{}; MQTTMessage log_message_; + std::string payload_buffer_; int log_level_{ESPHOME_LOG_LEVEL}; std::vector subscriptions_; From 586e36906d204978087648851cffdb31e1c659fd Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 8 Apr 2021 14:37:55 +0200 Subject: [PATCH 21/62] Fix sensor.sensor_schema interface changed (#1659) --- esphome/components/sensor/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index a10c5d7326..c5df0ca97c 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -1,4 +1,5 @@ import math +from typing import Optional import esphome.codegen as cg import esphome.config_validation as cv @@ -180,8 +181,12 @@ SENSOR_SCHEMA = cv.MQTT_COMPONENT_SCHEMA.extend( ) -def sensor_schema(unit_of_measurement_, icon_, accuracy_decimals_, device_class_): - # type: (str, str, int, str) -> cv.Schema +def sensor_schema( + unit_of_measurement_: str, + icon_: str, + accuracy_decimals_: int, + device_class_: Optional[str] = DEVICE_CLASS_EMPTY, +) -> cv.Schema: schema = SENSOR_SCHEMA if unit_of_measurement_ != UNIT_EMPTY: schema = schema.extend( From b53f9f2a81322b4d700e12fd1b8b3d96174f4bf6 Mon Sep 17 00:00:00 2001 From: Dan Gentry Date: Thu, 8 Apr 2021 11:04:40 -0400 Subject: [PATCH 22/62] Daylight Saving Time spelling fix (#1677) --- esphome/components/time/__init__.py | 2 +- esphome/components/time/real_time_clock.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/time/__init__.py b/esphome/components/time/__init__.py index b1c938c18e..8fbc2dcaf6 100644 --- a/esphome/components/time/__init__.py +++ b/esphome/components/time/__init__.py @@ -138,7 +138,7 @@ def convert_tz(pytz_obj): _tz_dst_str(dst_ends_local), ) _LOGGER.info( - "Detected timezone '%s' with UTC offset %s and daylight savings time from " + "Detected timezone '%s' with UTC offset %s and daylight saving time from " "%s to %s", tzname_off, _tz_timedelta(utcoffset_off), diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index a809401c33..c591482729 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -30,7 +30,7 @@ struct ESPTime { uint8_t month; /// year uint16_t year; - /// daylight savings time flag + /// daylight saving time flag bool is_dst; union { ESPDEPRECATED(".time is deprecated, use .timestamp instead") time_t time; From a39bb7b92f685e99343a2f99bc69fb293b5c4644 Mon Sep 17 00:00:00 2001 From: SenexCrenshaw <35600301+SenexCrenshaw@users.noreply.github.com> Date: Thu, 8 Apr 2021 11:06:54 -0400 Subject: [PATCH 23/62] Support custom build_flags for bme680_bsec (#1678) --- esphome/components/bme680_bsec/__init__.py | 2 +- esphome/components/bme680_bsec/bme680_bsec.cpp | 8 +++----- esphome/components/bme680_bsec/bme680_bsec.h | 12 +++++++----- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index f5028a90a3..8286029c3b 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -60,5 +60,5 @@ def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - cg.add_build_flag("-DUSING_BSEC") + cg.add_define("USING_BSEC") cg.add_library("BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index 0efe4083ef..a463ff78c4 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -1,4 +1,4 @@ -#ifdef USING_BSEC + #include "bme680_bsec.h" #include "esphome/core/log.h" @@ -7,7 +7,7 @@ namespace esphome { namespace bme680_bsec { - +#ifdef USING_BSEC static const char *TAG = "bme680_bsec.sensor"; static const std::string IAQ_ACCURACY_STATES[4] = {"Stabilizing", "Uncertain", "Calibrating", "Calibrated"}; @@ -391,8 +391,6 @@ void BME680BSECComponent::save_state_(uint8_t accuracy) { ESP_LOGI(TAG, "Saved state"); } - +#endif } // namespace bme680_bsec } // namespace esphome - -#endif diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index ce35e21c9a..4a71e1d23b 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -1,4 +1,4 @@ -#ifdef USING_BSEC + #pragma once @@ -7,11 +7,15 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/i2c/i2c.h" #include "esphome/core/preferences.h" -#include #include +#ifdef USING_BSEC +#include +#endif + namespace esphome { namespace bme680_bsec { +#ifdef USING_BSEC enum IAQMode { IAQ_MODE_STATIC = 0, @@ -99,8 +103,6 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { sensor::Sensor *co2_equivalent_sensor_; sensor::Sensor *breath_voc_equivalent_sensor_; }; - +#endif } // namespace bme680_bsec } // namespace esphome - -#endif From 30893afd673e8a266d72e54cb0defb16a18ca48e Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 8 Apr 2021 17:07:19 +0200 Subject: [PATCH 24/62] Add Arduino ESP32 version mapping (#1679) See also https://github.com/platformio/platform-espressif32/releases/tag/v3.2.0 --- esphome/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/const.py b/esphome/const.py index 35d4b016ce..48bab31c6c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -17,6 +17,7 @@ ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" # See also https://github.com/platformio/platform-espressif32/releases ARDUINO_VERSION_ESP32 = { "dev": "https://github.com/platformio/platform-espressif32.git", + "1.0.6": "platformio/espressif32@3.2.0", "1.0.5": "platformio/espressif32@3.1.1", "1.0.4": "platformio/espressif32@3.0.0", "1.0.3": "platformio/espressif32@1.10.0", From 5e239d3d882aa2b6d2d8556b7c08ea54f238c91a Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Thu, 8 Apr 2021 17:08:11 +0200 Subject: [PATCH 25/62] Automate building and publishing of esphome-lint docker image (#1680) --- .github/workflows/docker-lint-build.yml | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/docker-lint-build.yml diff --git a/.github/workflows/docker-lint-build.yml b/.github/workflows/docker-lint-build.yml new file mode 100644 index 0000000000..f148d98d65 --- /dev/null +++ b/.github/workflows/docker-lint-build.yml @@ -0,0 +1,36 @@ +name: Build and publish lint docker image + +# Only run when docker paths change +on: + push: + branches: [dev] + paths: + - 'docker/Dockerfile.lint' + - 'requirements.txt' + - 'requirements_test.txt' + - 'platformio.ini' + - '.github/workflows/docker-lint-build.yml' + +jobs: + publish-docker-lint-iage: + name: Build docker containers + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Pull for cache + run: | + docker pull "esphome/esphome-lint:latest" || true + - name: Build + run: | + docker build \ + --cache-from "esphome/esphome-lint:latest" \ + --file "docker/Dockerfile.lint" \ + --tag "esphome/esphome-lint:latest" \ + . + - name: Log in to docker hub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" + - run: | + docker push "esphome/esphome-lint:latest" From 2033ac34e5ad3e79484d5f7458ab3c6402c95704 Mon Sep 17 00:00:00 2001 From: SenexCrenshaw <35600301+SenexCrenshaw@users.noreply.github.com> Date: Thu, 8 Apr 2021 21:40:19 -0400 Subject: [PATCH 26/62] Sgp40 (#1513) * Start of SGP40 dev * Clean up * Initial Commit * VOC is working * Fixed up sensor config * Lint Fixes Added in save/restore baseline Noted original repo in header * Lint Fixes Added to test * Lint Fixes * Added additional check on restoring * Removed double check * Changed defines to static const double * Changed defines to const Do not send voc index until sensor stabilizes * Fixed sensor stabilization message * Fixup according to PR * samples_read increment fix * Fixed missing device class * Choose a SENSOR device class * Moved some sensors for tests Co-authored-by: Guillermo Ruffino --- CODEOWNERS | 1 + esphome/components/sgp40/__init__.py | 0 .../sgp40/sensirion_voc_algorithm.cpp | 629 ++++++++++++++++++ .../sgp40/sensirion_voc_algorithm.h | 147 ++++ esphome/components/sgp40/sensor.py | 57 ++ esphome/components/sgp40/sgp40.cpp | 314 +++++++++ esphome/components/sgp40/sgp40.h | 92 +++ tests/test1.yaml | 18 +- tests/test2.yaml | 21 +- 9 files changed, 1261 insertions(+), 18 deletions(-) create mode 100644 esphome/components/sgp40/__init__.py create mode 100644 esphome/components/sgp40/sensirion_voc_algorithm.cpp create mode 100644 esphome/components/sgp40/sensirion_voc_algorithm.h create mode 100644 esphome/components/sgp40/sensor.py create mode 100644 esphome/components/sgp40/sgp40.cpp create mode 100644 esphome/components/sgp40/sgp40.h diff --git a/CODEOWNERS b/CODEOWNERS index aad7cc3a19..472e304fb0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,6 +78,7 @@ esphome/components/rf_bridge/* @jesserockz esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sensor/* @esphome/core +esphome/components/sgp40/* @SenexCrenshaw esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet esphome/components/spi/* @esphome/core diff --git a/esphome/components/sgp40/__init__.py b/esphome/components/sgp40/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.cpp b/esphome/components/sgp40/sensirion_voc_algorithm.cpp new file mode 100644 index 0000000000..c82db2301a --- /dev/null +++ b/esphome/components/sgp40/sensirion_voc_algorithm.cpp @@ -0,0 +1,629 @@ + +#include "sensirion_voc_algorithm.h" + +namespace esphome { +namespace sgp40 { + +/* The VOC code were originally created by + * https://github.com/Sensirion/embedded-sgp + * The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +/*!< the maximum value of fix16_t */ +#define FIX16_MAXIMUM 0x7FFFFFFF +/*!< the minimum value of fix16_t */ +static const uint32_t FIX16_MINIMUM = 0x80000000; +/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not + * specified */ +static const uint32_t FIX16_OVERFLOW = 0x80000000; +/*!< fix16_t value of 1 */ +const uint32_t FIX16_ONE = 0x00010000; + +inline fix16_t fix16_from_int(int32_t a) { return a * FIX16_ONE; } + +inline int32_t fix16_cast_to_int(fix16_t a) { return (a >> 16); } + +/*! Multiplies the two given fix16_t's and returns the result. */ +static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1); + +/*! Divides the first given fix16_t by the second and returns the result. */ +static fix16_t fix16_div(fix16_t a, fix16_t b); + +/*! Returns the square root of the given fix16_t. */ +static fix16_t fix16_sqrt(fix16_t in_value); + +/*! Returns the exponent (e^) of the given fix16_t. */ +static fix16_t fix16_exp(fix16_t in_value); + +static fix16_t fix16_mul(fix16_t in_arg0, fix16_t in_arg1) { + // Each argument is divided to 16-bit parts. + // AB + // * CD + // ----------- + // BD 16 * 16 -> 32 bit products + // CB + // AD + // AC + // |----| 64 bit product + int32_t a = (in_arg0 >> 16), c = (in_arg1 >> 16); + uint32_t b = (in_arg0 & 0xFFFF), d = (in_arg1 & 0xFFFF); + + int32_t ac = a * c; + int32_t ad_cb = a * d + c * b; + uint32_t bd = b * d; + + int32_t product_hi = ac + (ad_cb >> 16); // NOLINT + + // Handle carry from lower 32 bits to upper part of result. + uint32_t ad_cb_temp = ad_cb << 16; // NOLINT + uint32_t product_lo = bd + ad_cb_temp; + if (product_lo < bd) + product_hi++; + +#ifndef FIXMATH_NO_OVERFLOW + // The upper 17 bits should all be the same (the sign). + if (product_hi >> 31 != product_hi >> 15) + return FIX16_OVERFLOW; +#endif + +#ifdef FIXMATH_NO_ROUNDING + return (product_hi << 16) | (product_lo >> 16); +#else + // Subtracting 0x8000 (= 0.5) and then using signed right shift + // achieves proper rounding to result-1, except in the corner + // case of negative numbers and lowest word = 0x8000. + // To handle that, we also have to subtract 1 for negative numbers. + uint32_t product_lo_tmp = product_lo; + product_lo -= 0x8000; + product_lo -= (uint32_t) product_hi >> 31; + if (product_lo > product_lo_tmp) + product_hi--; + + // Discard the lowest 16 bits. Note that this is not exactly the same + // as dividing by 0x10000. For example if product = -1, result will + // also be -1 and not 0. This is compensated by adding +1 to the result + // and compensating this in turn in the rounding above. + fix16_t result = (product_hi << 16) | (product_lo >> 16); // NOLINT + result += 1; + return result; +#endif +} + +static fix16_t fix16_div(fix16_t a, fix16_t b) { + // This uses the basic binary restoring division algorithm. + // It appears to be faster to do the whole division manually than + // trying to compose a 64-bit divide out of 32-bit divisions on + // platforms without hardware divide. + + if (b == 0) + return FIX16_MINIMUM; + + uint32_t remainder = (a >= 0) ? a : (-a); + uint32_t divider = (b >= 0) ? b : (-b); + + uint32_t quotient = 0; + uint32_t bit = 0x10000; + + /* The algorithm requires D >= R */ + while (divider < remainder) { + divider <<= 1; + bit <<= 1; + } + +#ifndef FIXMATH_NO_OVERFLOW + if (!bit) + return FIX16_OVERFLOW; +#endif + + if (divider & 0x80000000) { + // Perform one step manually to avoid overflows later. + // We know that divider's bottom bit is 0 here. + if (remainder >= divider) { + quotient |= bit; + remainder -= divider; + } + divider >>= 1; + bit >>= 1; + } + + /* Main division loop */ + while (bit && remainder) { + if (remainder >= divider) { + quotient |= bit; + remainder -= divider; + } + + remainder <<= 1; + bit >>= 1; + } + +#ifndef FIXMATH_NO_ROUNDING + if (remainder >= divider) { + quotient++; + } +#endif + + fix16_t result = quotient; + + /* Figure out the sign of result */ + if ((a ^ b) & 0x80000000) { +#ifndef FIXMATH_NO_OVERFLOW + if (result == FIX16_MINIMUM) + return FIX16_OVERFLOW; +#endif + + result = -result; + } + + return result; +} + +static fix16_t fix16_sqrt(fix16_t in_value) { + // It is assumed that x is not negative + + uint32_t num = in_value; + uint32_t result = 0; + uint32_t bit; + uint8_t n; + + bit = (uint32_t) 1 << 30; + while (bit > num) + bit >>= 2; + + // The main part is executed twice, in order to avoid + // using 64 bit values in computations. + for (n = 0; n < 2; n++) { + // First we get the top 24 bits of the answer. + while (bit) { + if (num >= result + bit) { + num -= result + bit; + result = (result >> 1) + bit; + } else { + result = (result >> 1); + } + bit >>= 2; + } + + if (n == 0) { + // Then process it again to get the lowest 8 bits. + if (num > 65535) { + // The remainder 'num' is too large to be shifted left + // by 16, so we have to add 1 to result manually and + // adjust 'num' accordingly. + // num = a - (result + 0.5)^2 + // = num + result^2 - (result + 0.5)^2 + // = num - result - 0.5 + num -= result; + num = (num << 16) - 0x8000; + result = (result << 16) + 0x8000; + } else { + num <<= 16; + result <<= 16; + } + + bit = 1 << 14; + } + } + +#ifndef FIXMATH_NO_ROUNDING + // Finally, if next bit would have been 1, round the result upwards. + if (num > result) { + result++; + } +#endif + + return (fix16_t) result; +} + +static fix16_t fix16_exp(fix16_t in_value) { + // Function to approximate exp(); optimized more for code size than speed + + // exp(x) for x = +/- {1, 1/8, 1/64, 1/512} + fix16_t x = in_value; + static const uint8_t NUM_EXP_VALUES = 4; + static const fix16_t EXP_POS_VALUES[4] = {F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550)}; + static const fix16_t EXP_NEG_VALUES[4] = {F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488)}; + const fix16_t* exp_values; + + fix16_t res, arg; + uint16_t i; + + if (x >= F16(10.3972)) + return FIX16_MAXIMUM; + if (x <= F16(-11.7835)) + return 0; + + if (x < 0) { + x = -x; + exp_values = EXP_NEG_VALUES; + } else { + exp_values = EXP_POS_VALUES; + } + + res = FIX16_ONE; + arg = FIX16_ONE; + for (i = 0; i < NUM_EXP_VALUES; i++) { + while (x >= arg) { + res = fix16_mul(res, exp_values[i]); + x -= arg; + } + arg >>= 3; + } + return res; +} + +static void voc_algorithm_init_instances(VocAlgorithmParams* params); +static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams* params); +static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams* params); +static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams* params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, + fix16_t gating_max_duration_minutes); +static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams* params, fix16_t mean, fix16_t std, + fix16_t uptime_gamma); +static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams* params); +static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams* params); +static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams* params, + fix16_t voc_index_from_prior); +static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams* params, fix16_t sraw, + fix16_t voc_index_from_prior); +static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams* params); +static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams* params, fix16_t l, + fix16_t x0, fix16_t k); +static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams* params, fix16_t sample); +static void voc_algorithm_mox_model_init(VocAlgorithmParams* params); +static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams* params, fix16_t sraw_std, fix16_t sraw_mean); +static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams* params, fix16_t sraw); +static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams* params); +static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams* params, fix16_t offset); +static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams* params, fix16_t sample); +static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams* params); +static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams* params); +static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams* params, fix16_t sample); + +void voc_algorithm_init(VocAlgorithmParams* params) { + params->mVoc_Index_Offset = F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT); + params->mTau_Mean_Variance_Hours = F16(VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS); + params->mGating_Max_Duration_Minutes = F16(VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES); + params->mSraw_Std_Initial = F16(VOC_ALGORITHM_SRAW_STD_INITIAL); + params->mUptime = F16(0.); + params->mSraw = F16(0.); + params->mVoc_Index = 0; + voc_algorithm_init_instances(params); +} + +static void voc_algorithm_init_instances(VocAlgorithmParams* params) { + voc_algorithm_mean_variance_estimator_init(params); + voc_algorithm_mean_variance_estimator_set_parameters( + params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, params->mGating_Max_Duration_Minutes); + voc_algorithm_mox_model_init(params); + voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), + voc_algorithm_mean_variance_estimator_get_mean(params)); + voc_algorithm_sigmoid_scaled_init(params); + voc_algorithm_sigmoid_scaled_set_parameters(params, params->mVoc_Index_Offset); + voc_algorithm_adaptive_lowpass_init(params); + voc_algorithm_adaptive_lowpass_set_parameters(params); +} + +void voc_algorithm_get_states(VocAlgorithmParams* params, int32_t* state0, int32_t* state1) { + *state0 = voc_algorithm_mean_variance_estimator_get_mean(params); + *state1 = voc_algorithm_mean_variance_estimator_get_std(params); +} + +void voc_algorithm_set_states(VocAlgorithmParams* params, int32_t state0, int32_t state1) { + voc_algorithm_mean_variance_estimator_set_states(params, state0, state1, F16(VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA)); + params->mSraw = state0; +} + +void voc_algorithm_set_tuning_parameters(VocAlgorithmParams* params, int32_t voc_index_offset, + int32_t learning_time_hours, int32_t gating_max_duration_minutes, + int32_t std_initial) { + params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset)); + params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours)); + params->mGating_Max_Duration_Minutes = (fix16_from_int(gating_max_duration_minutes)); + params->mSraw_Std_Initial = (fix16_from_int(std_initial)); + voc_algorithm_init_instances(params); +} + +void voc_algorithm_process(VocAlgorithmParams* params, int32_t sraw, int32_t* voc_index) { + if ((params->mUptime <= F16(VOC_ALGORITHM_INITIAL_BLACKOUT))) { + params->mUptime = (params->mUptime + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); + } else { + if (((sraw > 0) && (sraw < 65000))) { + if ((sraw < 20001)) { + sraw = 20001; + } else if ((sraw > 52767)) { + sraw = 52767; + } + params->mSraw = (fix16_from_int((sraw - 20000))); + } + params->mVoc_Index = voc_algorithm_mox_model_process(params, params->mSraw); + params->mVoc_Index = voc_algorithm_sigmoid_scaled_process(params, params->mVoc_Index); + params->mVoc_Index = voc_algorithm_adaptive_lowpass_process(params, params->mVoc_Index); + if ((params->mVoc_Index < F16(0.5))) { + params->mVoc_Index = F16(0.5); + } + if ((params->mSraw > F16(0.))) { + voc_algorithm_mean_variance_estimator_process(params, params->mSraw, params->mVoc_Index); + voc_algorithm_mox_model_set_parameters(params, voc_algorithm_mean_variance_estimator_get_std(params), + voc_algorithm_mean_variance_estimator_get_mean(params)); + } + } + *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5)))); +} + +static void voc_algorithm_mean_variance_estimator_init(VocAlgorithmParams* params) { + voc_algorithm_mean_variance_estimator_set_parameters(params, F16(0.), F16(0.), F16(0.)); + voc_algorithm_mean_variance_estimator_init_instances(params); +} + +static void voc_algorithm_mean_variance_estimator_init_instances(VocAlgorithmParams* params) { + voc_algorithm_mean_variance_estimator_sigmoid_init(params); +} + +static void voc_algorithm_mean_variance_estimator_set_parameters(VocAlgorithmParams* params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, + fix16_t gating_max_duration_minutes) { + params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes = gating_max_duration_minutes; + params->m_Mean_Variance_Estimator___Initialized = false; + params->m_Mean_Variance_Estimator___Mean = F16(0.); + params->m_Mean_Variance_Estimator___Sraw_Offset = F16(0.); + params->m_Mean_Variance_Estimator___Std = std_initial; + params->m_Mean_Variance_Estimator___Gamma = + (fix16_div(F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * (VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))), + (tau_mean_variance_hours + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 3600.))))); + params->m_Mean_Variance_Estimator___Gamma_Initial_Mean = + F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / + (VOC_ALGORITHM_TAU_INITIAL_MEAN + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator___Gamma_Initial_Variance = + F16(((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING * VOC_ALGORITHM_SAMPLING_INTERVAL) / + (VOC_ALGORITHM_TAU_INITIAL_VARIANCE + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator__Gamma_Mean = F16(0.); + params->m_Mean_Variance_Estimator__Gamma_Variance = F16(0.); + params->m_Mean_Variance_Estimator___Uptime_Gamma = F16(0.); + params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.); +} + +static void voc_algorithm_mean_variance_estimator_set_states(VocAlgorithmParams* params, fix16_t mean, fix16_t std, + fix16_t uptime_gamma) { + params->m_Mean_Variance_Estimator___Mean = mean; + params->m_Mean_Variance_Estimator___Std = std; + params->m_Mean_Variance_Estimator___Uptime_Gamma = uptime_gamma; + params->m_Mean_Variance_Estimator___Initialized = true; +} + +static fix16_t voc_algorithm_mean_variance_estimator_get_std(VocAlgorithmParams* params) { + return params->m_Mean_Variance_Estimator___Std; +} + +static fix16_t voc_algorithm_mean_variance_estimator_get_mean(VocAlgorithmParams* params) { + return (params->m_Mean_Variance_Estimator___Mean + params->m_Mean_Variance_Estimator___Sraw_Offset); +} + +static void voc_algorithm_mean_variance_estimator_calculate_gamma(VocAlgorithmParams* params, + fix16_t voc_index_from_prior) { + fix16_t uptime_limit; + fix16_t sigmoid_gamma_mean; + fix16_t gamma_mean; + fix16_t gating_threshold_mean; + fix16_t sigmoid_gating_mean; + fix16_t sigmoid_gamma_variance; + fix16_t gamma_variance; + fix16_t gating_threshold_variance; + fix16_t sigmoid_gating_variance; + + uptime_limit = F16((VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX - VOC_ALGORITHM_SAMPLING_INTERVAL)); + if ((params->m_Mean_Variance_Estimator___Uptime_Gamma < uptime_limit)) { + params->m_Mean_Variance_Estimator___Uptime_Gamma = + (params->m_Mean_Variance_Estimator___Uptime_Gamma + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); + } + if ((params->m_Mean_Variance_Estimator___Uptime_Gating < uptime_limit)) { + params->m_Mean_Variance_Estimator___Uptime_Gating = + (params->m_Mean_Variance_Estimator___Uptime_Gating + F16(VOC_ALGORITHM_SAMPLING_INTERVAL)); + } + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_MEAN), + F16(VOC_ALGORITHM_INIT_TRANSITION_MEAN)); + sigmoid_gamma_mean = + voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_mean = + (params->m_Mean_Variance_Estimator___Gamma + + (fix16_mul((params->m_Mean_Variance_Estimator___Gamma_Initial_Mean - params->m_Mean_Variance_Estimator___Gamma), + sigmoid_gamma_mean))); + gating_threshold_mean = (F16(VOC_ALGORITHM_GATING_THRESHOLD) + + (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), + voc_algorithm_mean_variance_estimator_sigmoid_process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating)))); + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_mean, + F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_mean = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); + params->m_Mean_Variance_Estimator__Gamma_Mean = (fix16_mul(sigmoid_gating_mean, gamma_mean)); + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters( + params, F16(1.), F16(VOC_ALGORITHM_INIT_DURATION_VARIANCE), F16(VOC_ALGORITHM_INIT_TRANSITION_VARIANCE)); + sigmoid_gamma_variance = + voc_algorithm_mean_variance_estimator_sigmoid_process(params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_variance = (params->m_Mean_Variance_Estimator___Gamma + + (fix16_mul((params->m_Mean_Variance_Estimator___Gamma_Initial_Variance - + params->m_Mean_Variance_Estimator___Gamma), + (sigmoid_gamma_variance - sigmoid_gamma_mean)))); + gating_threshold_variance = + (F16(VOC_ALGORITHM_GATING_THRESHOLD) + + (fix16_mul(F16((VOC_ALGORITHM_GATING_THRESHOLD_INITIAL - VOC_ALGORITHM_GATING_THRESHOLD)), + voc_algorithm_mean_variance_estimator_sigmoid_process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating)))); + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(1.), gating_threshold_variance, + F16(VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_variance = voc_algorithm_mean_variance_estimator_sigmoid_process(params, voc_index_from_prior); + params->m_Mean_Variance_Estimator__Gamma_Variance = (fix16_mul(sigmoid_gating_variance, gamma_variance)); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = + (params->m_Mean_Variance_Estimator___Gating_Duration_Minutes + + (fix16_mul(F16((VOC_ALGORITHM_SAMPLING_INTERVAL / 60.)), + ((fix16_mul((F16(1.) - sigmoid_gating_mean), F16((1. + VOC_ALGORITHM_GATING_MAX_RATIO)))) - + F16(VOC_ALGORITHM_GATING_MAX_RATIO))))); + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes < F16(0.))) { + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.); + } + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes > + params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes)) { + params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.); + } +} + +static void voc_algorithm_mean_variance_estimator_process(VocAlgorithmParams* params, fix16_t sraw, + fix16_t voc_index_from_prior) { + fix16_t delta_sgp; + fix16_t c; + fix16_t additional_scaling; + + if ((!params->m_Mean_Variance_Estimator___Initialized)) { + params->m_Mean_Variance_Estimator___Initialized = true; + params->m_Mean_Variance_Estimator___Sraw_Offset = sraw; + params->m_Mean_Variance_Estimator___Mean = F16(0.); + } else { + if (((params->m_Mean_Variance_Estimator___Mean >= F16(100.)) || + (params->m_Mean_Variance_Estimator___Mean <= F16(-100.)))) { + params->m_Mean_Variance_Estimator___Sraw_Offset = + (params->m_Mean_Variance_Estimator___Sraw_Offset + params->m_Mean_Variance_Estimator___Mean); + params->m_Mean_Variance_Estimator___Mean = F16(0.); + } + sraw = (sraw - params->m_Mean_Variance_Estimator___Sraw_Offset); + voc_algorithm_mean_variance_estimator_calculate_gamma(params, voc_index_from_prior); + delta_sgp = (fix16_div((sraw - params->m_Mean_Variance_Estimator___Mean), + F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING))); + if ((delta_sgp < F16(0.))) { + c = (params->m_Mean_Variance_Estimator___Std - delta_sgp); + } else { + c = (params->m_Mean_Variance_Estimator___Std + delta_sgp); + } + additional_scaling = F16(1.); + if ((c > F16(1440.))) { + additional_scaling = F16(4.); + } + params->m_Mean_Variance_Estimator___Std = (fix16_mul( + fix16_sqrt((fix16_mul(additional_scaling, (F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING) - + params->m_Mean_Variance_Estimator__Gamma_Variance)))), + fix16_sqrt(((fix16_mul(params->m_Mean_Variance_Estimator___Std, + (fix16_div(params->m_Mean_Variance_Estimator___Std, + (fix16_mul(F16(VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING), + additional_scaling)))))) + + (fix16_mul((fix16_div((fix16_mul(params->m_Mean_Variance_Estimator__Gamma_Variance, delta_sgp)), + additional_scaling)), + delta_sgp)))))); + params->m_Mean_Variance_Estimator___Mean = (params->m_Mean_Variance_Estimator___Mean + + (fix16_mul(params->m_Mean_Variance_Estimator__Gamma_Mean, delta_sgp))); + } +} + +static void voc_algorithm_mean_variance_estimator_sigmoid_init(VocAlgorithmParams* params) { + voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(params, F16(0.), F16(0.), F16(0.)); +} + +static void voc_algorithm_mean_variance_estimator_sigmoid_set_parameters(VocAlgorithmParams* params, fix16_t l, + fix16_t x0, fix16_t k) { + params->m_Mean_Variance_Estimator___Sigmoid__L = l; + params->m_Mean_Variance_Estimator___Sigmoid__K = k; + params->m_Mean_Variance_Estimator___Sigmoid__X0 = x0; +} + +static fix16_t voc_algorithm_mean_variance_estimator_sigmoid_process(VocAlgorithmParams* params, fix16_t sample) { + fix16_t x; + + x = (fix16_mul(params->m_Mean_Variance_Estimator___Sigmoid__K, + (sample - params->m_Mean_Variance_Estimator___Sigmoid__X0))); + if ((x < F16(-50.))) { + return params->m_Mean_Variance_Estimator___Sigmoid__L; + } else if ((x > F16(50.))) { + return F16(0.); + } else { + return (fix16_div(params->m_Mean_Variance_Estimator___Sigmoid__L, (F16(1.) + fix16_exp(x)))); + } +} + +static void voc_algorithm_mox_model_init(VocAlgorithmParams* params) { + voc_algorithm_mox_model_set_parameters(params, F16(1.), F16(0.)); +} + +static void voc_algorithm_mox_model_set_parameters(VocAlgorithmParams* params, fix16_t sraw_std, fix16_t sraw_mean) { + params->m_Mox_Model__Sraw_Std = sraw_std; + params->m_Mox_Model__Sraw_Mean = sraw_mean; +} + +static fix16_t voc_algorithm_mox_model_process(VocAlgorithmParams* params, fix16_t sraw) { + return (fix16_mul((fix16_div((sraw - params->m_Mox_Model__Sraw_Mean), + (-(params->m_Mox_Model__Sraw_Std + F16(VOC_ALGORITHM_SRAW_STD_BONUS))))), + F16(VOC_ALGORITHM_VOC_INDEX_GAIN))); +} + +static void voc_algorithm_sigmoid_scaled_init(VocAlgorithmParams* params) { + voc_algorithm_sigmoid_scaled_set_parameters(params, F16(0.)); +} + +static void voc_algorithm_sigmoid_scaled_set_parameters(VocAlgorithmParams* params, fix16_t offset) { + params->m_Sigmoid_Scaled__Offset = offset; +} + +static fix16_t voc_algorithm_sigmoid_scaled_process(VocAlgorithmParams* params, fix16_t sample) { + fix16_t x; + fix16_t shift; + + x = (fix16_mul(F16(VOC_ALGORITHM_SIGMOID_K), (sample - F16(VOC_ALGORITHM_SIGMOID_X0)))); + if ((x < F16(-50.))) { + return F16(VOC_ALGORITHM_SIGMOID_L); + } else if ((x > F16(50.))) { + return F16(0.); + } else { + if ((sample >= F16(0.))) { + shift = + (fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) - (fix16_mul(F16(5.), params->m_Sigmoid_Scaled__Offset))), F16(4.))); + return ((fix16_div((F16(VOC_ALGORITHM_SIGMOID_L) + shift), (F16(1.) + fix16_exp(x)))) - shift); + } else { + return (fix16_mul((fix16_div(params->m_Sigmoid_Scaled__Offset, F16(VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT))), + (fix16_div(F16(VOC_ALGORITHM_SIGMOID_L), (F16(1.) + fix16_exp(x)))))); + } + } +} + +static void voc_algorithm_adaptive_lowpass_init(VocAlgorithmParams* params) { + voc_algorithm_adaptive_lowpass_set_parameters(params); +} + +static void voc_algorithm_adaptive_lowpass_set_parameters(VocAlgorithmParams* params) { + params->m_Adaptive_Lowpass__A1 = + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_FAST + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass__A2 = + F16((VOC_ALGORITHM_SAMPLING_INTERVAL / (VOC_ALGORITHM_LP_TAU_SLOW + VOC_ALGORITHM_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass___Initialized = false; +} + +static fix16_t voc_algorithm_adaptive_lowpass_process(VocAlgorithmParams* params, fix16_t sample) { + fix16_t abs_delta; + fix16_t f1; + fix16_t tau_a; + fix16_t a3; + + if ((!params->m_Adaptive_Lowpass___Initialized)) { + params->m_Adaptive_Lowpass___X1 = sample; + params->m_Adaptive_Lowpass___X2 = sample; + params->m_Adaptive_Lowpass___X3 = sample; + params->m_Adaptive_Lowpass___Initialized = true; + } + params->m_Adaptive_Lowpass___X1 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A1), params->m_Adaptive_Lowpass___X1)) + + (fix16_mul(params->m_Adaptive_Lowpass__A1, sample))); + params->m_Adaptive_Lowpass___X2 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A2), params->m_Adaptive_Lowpass___X2)) + + (fix16_mul(params->m_Adaptive_Lowpass__A2, sample))); + abs_delta = (params->m_Adaptive_Lowpass___X1 - params->m_Adaptive_Lowpass___X2); + if ((abs_delta < F16(0.))) { + abs_delta = (-abs_delta); + } + f1 = fix16_exp((fix16_mul(F16(VOC_ALGORITHM_LP_ALPHA), abs_delta))); + tau_a = + ((fix16_mul(F16((VOC_ALGORITHM_LP_TAU_SLOW - VOC_ALGORITHM_LP_TAU_FAST)), f1)) + F16(VOC_ALGORITHM_LP_TAU_FAST)); + a3 = (fix16_div(F16(VOC_ALGORITHM_SAMPLING_INTERVAL), (F16(VOC_ALGORITHM_SAMPLING_INTERVAL) + tau_a))); + params->m_Adaptive_Lowpass___X3 = + ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass___X3)) + (fix16_mul(a3, sample))); + return params->m_Adaptive_Lowpass___X3; +} +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sensirion_voc_algorithm.h b/esphome/components/sgp40/sensirion_voc_algorithm.h new file mode 100644 index 0000000000..05431635ad --- /dev/null +++ b/esphome/components/sgp40/sensirion_voc_algorithm.h @@ -0,0 +1,147 @@ +#pragma once +#include +namespace esphome { +namespace sgp40 { + +/* The VOC code were originally created by + * https://github.com/Sensirion/embedded-sgp + * The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +using fix16_t = int32_t; + +#define F16(x) ((fix16_t)(((x) >= 0) ? ((x) *65536.0 + 0.5) : ((x) *65536.0 - 0.5))) + +static const float VOC_ALGORITHM_SAMPLING_INTERVAL(1.); +static const float VOC_ALGORITHM_INITIAL_BLACKOUT(45.); +static const float VOC_ALGORITHM_VOC_INDEX_GAIN(230.); +static const float VOC_ALGORITHM_SRAW_STD_INITIAL(50.); +static const float VOC_ALGORITHM_SRAW_STD_BONUS(220.); +static const float VOC_ALGORITHM_TAU_MEAN_VARIANCE_HOURS(12.); +static const float VOC_ALGORITHM_TAU_INITIAL_MEAN(20.); +static const float VOC_ALGORITHM_INIT_DURATION_MEAN((3600. * 0.75)); +static const float VOC_ALGORITHM_INIT_TRANSITION_MEAN(0.01); +static const float VOC_ALGORITHM_TAU_INITIAL_VARIANCE(2500.); +static const float VOC_ALGORITHM_INIT_DURATION_VARIANCE((3600. * 1.45)); +static const float VOC_ALGORITHM_INIT_TRANSITION_VARIANCE(0.01); +static const float VOC_ALGORITHM_GATING_THRESHOLD(340.); +static const float VOC_ALGORITHM_GATING_THRESHOLD_INITIAL(510.); +static const float VOC_ALGORITHM_GATING_THRESHOLD_TRANSITION(0.09); +static const float VOC_ALGORITHM_GATING_MAX_DURATION_MINUTES((60. * 3.)); +static const float VOC_ALGORITHM_GATING_MAX_RATIO(0.3); +static const float VOC_ALGORITHM_SIGMOID_L(500.); +static const float VOC_ALGORITHM_SIGMOID_K(-0.0065); +static const float VOC_ALGORITHM_SIGMOID_X0(213.); +static const float VOC_ALGORITHM_VOC_INDEX_OFFSET_DEFAULT(100.); +static const float VOC_ALGORITHM_LP_TAU_FAST(20.0); +static const float VOC_ALGORITHM_LP_TAU_SLOW(500.0); +static const float VOC_ALGORITHM_LP_ALPHA(-0.2); +static const float VOC_ALGORITHM_PERSISTENCE_UPTIME_GAMMA((3. * 3600.)); +static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_GAMMA_SCALING(64.); +static const float VOC_ALGORITHM_MEAN_VARIANCE_ESTIMATOR_FI_X16_MAX(32767.); + +/** + * Struct to hold all the states of the VOC algorithm. + */ +struct VocAlgorithmParams { + fix16_t mVoc_Index_Offset; + fix16_t mTau_Mean_Variance_Hours; + fix16_t mGating_Max_Duration_Minutes; + fix16_t mSraw_Std_Initial; + fix16_t mUptime; + fix16_t mSraw; + fix16_t mVoc_Index; + fix16_t m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes; + bool m_Mean_Variance_Estimator___Initialized; + fix16_t m_Mean_Variance_Estimator___Mean; + fix16_t m_Mean_Variance_Estimator___Sraw_Offset; + fix16_t m_Mean_Variance_Estimator___Std; + fix16_t m_Mean_Variance_Estimator___Gamma; + fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Mean; + fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Variance; + fix16_t m_Mean_Variance_Estimator__Gamma_Mean; + fix16_t m_Mean_Variance_Estimator__Gamma_Variance; + fix16_t m_Mean_Variance_Estimator___Uptime_Gamma; + fix16_t m_Mean_Variance_Estimator___Uptime_Gating; + fix16_t m_Mean_Variance_Estimator___Gating_Duration_Minutes; + fix16_t m_Mean_Variance_Estimator___Sigmoid__L; + fix16_t m_Mean_Variance_Estimator___Sigmoid__K; + fix16_t m_Mean_Variance_Estimator___Sigmoid__X0; + fix16_t m_Mox_Model__Sraw_Std; + fix16_t m_Mox_Model__Sraw_Mean; + fix16_t m_Sigmoid_Scaled__Offset; + fix16_t m_Adaptive_Lowpass__A1; + fix16_t m_Adaptive_Lowpass__A2; + bool m_Adaptive_Lowpass___Initialized; + fix16_t m_Adaptive_Lowpass___X1; + fix16_t m_Adaptive_Lowpass___X2; + fix16_t m_Adaptive_Lowpass___X3; +}; + +/** + * Initialize the VOC algorithm parameters. Call this once at the beginning or + * whenever the sensor stopped measurements. + * @param params Pointer to the VocAlgorithmParams struct + */ +void voc_algorithm_init(VocAlgorithmParams *params); + +/** + * Get current algorithm states. Retrieved values can be used in + * voc_algorithm_set_states() to resume operation after a short interruption, + * skipping initial learning phase. This feature can only be used after at least + * 3 hours of continuous operation. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be stored + * @param state1 State1 to be stored + */ +void voc_algorithm_get_states(VocAlgorithmParams *params, int32_t *state0, int32_t *state1); + +/** + * Set previously retrieved algorithm states to resume operation after a short + * interruption, skipping initial learning phase. This feature should not be + * used after inerruptions of more than 10 minutes. Call this once after + * voc_algorithm_init() and the optional voc_algorithm_set_tuning_parameters(), if + * desired. Otherwise, the algorithm will start with initial learning phase. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be restored + * @param state1 State1 to be restored + */ +void voc_algorithm_set_states(VocAlgorithmParams *params, int32_t state0, int32_t state1); + +/** + * Set parameters to customize the VOC algorithm. Call this once after + * voc_algorithm_init(), if desired. Otherwise, the default values will be used. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param voc_index_offset VOC index representing typical (average) + * conditions. Range 1..250, default 100 + * @param learning_time_hours Time constant of long-term estimator. + * Past events will be forgotten after about + * twice the learning time. + * Range 1..72 [hours], default 12 [hours] + * @param gating_max_duration_minutes Maximum duration of gating (freeze of + * estimator during high VOC index signal). + * 0 (no gating) or range 1..720 [minutes], + * default 180 [minutes] + * @param std_initial Initial estimate for standard deviation. + * Lower value boosts events during initial + * learning period, but may result in larger + * device-to-device variations. + * Range 10..500, default 50 + */ +void voc_algorithm_set_tuning_parameters(VocAlgorithmParams *params, int32_t voc_index_offset, + int32_t learning_time_hours, int32_t gating_max_duration_minutes, + int32_t std_initial); + +/** + * Calculate the VOC index value from the raw sensor value. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param sraw Raw value from the SGP40 sensor + * @param voc_index Calculated VOC index value from the raw sensor value. Zero + * during initial blackout period and 1..500 afterwards + */ +void voc_algorithm_process(VocAlgorithmParams *params, int32_t sraw, int32_t *voc_index); +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sensor.py b/esphome/components/sgp40/sensor.py new file mode 100644 index 0000000000..40bc07389b --- /dev/null +++ b/esphome/components/sgp40/sensor.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, DEVICE_CLASS_EMPTY, ICON_RADIATOR, UNIT_EMPTY + +DEPENDENCIES = ["i2c"] + +CODEOWNERS = ["@SenexCrenshaw"] + +sgp40_ns = cg.esphome_ns.namespace("sgp40") +SGP40Component = sgp40_ns.class_( + "SGP40Component", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONF_COMPENSATION = "compensation" +CONF_HUMIDITY_SOURCE = "humidity_source" +CONF_TEMPERATURE_SOURCE = "temperature_source" +CONF_STORE_BASELINE = "store_baseline" +CONF_VOC_BASELINE = "voc_baseline" + +CONFIG_SCHEMA = ( + sensor.sensor_schema(UNIT_EMPTY, ICON_RADIATOR, 0, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(SGP40Component), + cv.Optional(CONF_STORE_BASELINE, default=True): cv.boolean, + cv.Optional(CONF_VOC_BASELINE): cv.hex_uint16_t, + cv.Optional(CONF_COMPENSATION): cv.Schema( + { + cv.Required(CONF_HUMIDITY_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_TEMPERATURE_SOURCE): cv.use_id(sensor.Sensor), + }, + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x59)) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + yield sensor.register_sensor(var, config) + + if CONF_COMPENSATION in config: + compensation_config = config[CONF_COMPENSATION] + sens = yield cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE]) + cg.add(var.set_humidity_sensor(sens)) + sens = yield cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE]) + cg.add(var.set_temperature_sensor(sens)) + + cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE])) + + if CONF_VOC_BASELINE in config: + cg.add(var.set_voc_baseline(CONF_VOC_BASELINE)) diff --git a/esphome/components/sgp40/sgp40.cpp b/esphome/components/sgp40/sgp40.cpp new file mode 100644 index 0000000000..3e9f2b96cf --- /dev/null +++ b/esphome/components/sgp40/sgp40.cpp @@ -0,0 +1,314 @@ +#include "esphome/core/log.h" +#include "sgp40.h" + +namespace esphome { +namespace sgp40 { + +static const char *TAG = "sgp40"; + +void SGP40Component::setup() { + ESP_LOGCONFIG(TAG, "Setting up SGP40..."); + + // Serial Number identification + if (!this->write_command_(SGP40_CMD_GET_SERIAL_ID)) { + this->error_code_ = COMMUNICATION_FAILED; + this->mark_failed(); + return; + } + uint16_t raw_serial_number[3]; + + if (!this->read_data_(raw_serial_number, 3)) { + this->mark_failed(); + return; + } + this->serial_number_ = (uint64_t(raw_serial_number[0]) << 24) | (uint64_t(raw_serial_number[1]) << 16) | + (uint64_t(raw_serial_number[2])); + ESP_LOGD(TAG, "Serial Number: %llu", this->serial_number_); + + // Featureset identification for future use + if (!this->write_command_(SGP40_CMD_GET_FEATURESET)) { + ESP_LOGD(TAG, "raw_featureset write_command_ failed"); + this->mark_failed(); + return; + } + uint16_t raw_featureset[1]; + if (!this->read_data_(raw_featureset, 1)) { + ESP_LOGD(TAG, "raw_featureset read_data_ failed"); + this->mark_failed(); + return; + } + + this->featureset_ = raw_featureset[0]; + if ((this->featureset_ & 0x1FF) != SGP40_FEATURESET) { + ESP_LOGD(TAG, "Product feature set failed 0x%0X , expecting 0x%0X", uint16_t(this->featureset_ & 0x1FF), + SGP40_FEATURESET); + this->mark_failed(); + return; + } + + ESP_LOGD(TAG, "Product version: 0x%0X", uint16_t(this->featureset_ & 0x1FF)); + + voc_algorithm_init(&this->voc_algorithm_params_); + + if (this->store_baseline_) { + // Hash with compilation time + // This ensures the baseline storage is cleared after OTA + uint32_t hash = fnv1_hash(App.get_compilation_time()); + this->pref_ = global_preferences.make_preference(hash, true); + + if (this->pref_.load(&this->baselines_storage_)) { + this->state0_ = this->baselines_storage_.state0; + this->state1_ = this->baselines_storage_.state1; + ESP_LOGI(TAG, "Loaded VOC baseline state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, + baselines_storage_.state1); + } + + // Initialize storage timestamp + this->seconds_since_last_store_ = 0; + + if (this->baselines_storage_.state0 > 0 && this->baselines_storage_.state1 > 0) { + ESP_LOGI(TAG, "Setting VOC baseline from save state0: 0x%04X, state1: 0x%04X", this->baselines_storage_.state0, + baselines_storage_.state1); + voc_algorithm_set_states(&this->voc_algorithm_params_, this->baselines_storage_.state0, + this->baselines_storage_.state1); + } + } + + this->self_test_(); +} + +void SGP40Component::self_test_() { + ESP_LOGD(TAG, "selfTest started"); + if (!this->write_command_(SGP40_CMD_SELF_TEST)) { + this->error_code_ = COMMUNICATION_FAILED; + ESP_LOGD(TAG, "selfTest communicatin failed"); + this->mark_failed(); + } + + this->set_timeout(250, [this]() { + uint16_t reply[1]; + if (!this->read_data_(reply, 1)) { + ESP_LOGD(TAG, "selfTest read_data_ failed"); + this->mark_failed(); + return; + } + + if (reply[0] == 0xD400) { + ESP_LOGD(TAG, "selfTest completed"); + return; + } + + ESP_LOGD(TAG, "selfTest failed"); + this->mark_failed(); + }); +} + +/** + * @brief Combined the measured gasses, temperature, and humidity + * to calculate the VOC Index + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return int32_t The VOC Index + */ +int32_t SGP40Component::measure_voc_index_() { + int32_t voc_index; + + uint16_t sraw = measure_raw_(); + + if (sraw == UINT16_MAX) + return UINT16_MAX; + + this->status_clear_warning(); + + voc_algorithm_process(&voc_algorithm_params_, sraw, &voc_index); + + // Store baselines after defined interval or if the difference between current and stored baseline becomes too + // much + if (this->store_baseline_ && this->seconds_since_last_store_ > SHORTEST_BASELINE_STORE_INTERVAL) { + voc_algorithm_get_states(&voc_algorithm_params_, &this->state0_, &this->state1_); + if (abs(this->baselines_storage_.state0 - this->state0_) > MAXIMUM_STORAGE_DIFF || + abs(this->baselines_storage_.state1 - this->state1_) > MAXIMUM_STORAGE_DIFF) { + this->seconds_since_last_store_ = 0; + this->baselines_storage_.state0 = this->state0_; + this->baselines_storage_.state1 = this->state1_; + + if (this->pref_.save(&this->baselines_storage_)) { + ESP_LOGI(TAG, "Stored VOC baseline state0: 0x%04X ,state1: 0x%04X", this->baselines_storage_.state0, + baselines_storage_.state1); + } else { + ESP_LOGW(TAG, "Could not store VOC baselines"); + } + } + } + + return voc_index; +} + +/** + * @brief Return the raw gas measurement + * + * @param temperature The measured temperature in degrees C + * @param humidity The measured relative humidity in % rH + * @return uint16_t The current raw gas measurement + */ +uint16_t SGP40Component::measure_raw_() { + float humidity = NAN; + if (this->humidity_sensor_ != nullptr) { + humidity = this->humidity_sensor_->state; + } + if (isnan(humidity) || humidity < 0.0f || humidity > 100.0f) { + humidity = 50; + } + + float temperature = NAN; + if (this->temperature_sensor_ != nullptr) { + temperature = float(this->temperature_sensor_->state); + } + if (isnan(temperature) || temperature < -40.0f || temperature > 85.0f) { + temperature = 25; + } + + uint8_t command[8]; + + command[0] = 0x26; + command[1] = 0x0F; + + uint16_t rhticks = llround((uint16_t)((humidity * 65535) / 100)); + command[2] = rhticks >> 8; + command[3] = rhticks & 0xFF; + command[4] = generate_crc_(command + 2, 2); + uint16_t tempticks = (uint16_t)(((temperature + 45) * 65535) / 175); + command[5] = tempticks >> 8; + command[6] = tempticks & 0xFF; + command[7] = generate_crc_(command + 5, 2); + + if (!this->write_bytes_raw(command, 8)) { + this->status_set_warning(); + ESP_LOGD(TAG, "write_bytes_raw error"); + return UINT16_MAX; + } + delay(250); // NOLINT + uint16_t raw_data[1]; + + if (!this->read_data_(raw_data, 1)) { + this->status_set_warning(); + ESP_LOGD(TAG, "read_data_ error"); + return UINT16_MAX; + } + return raw_data[0]; +} + +uint8_t SGP40Component::generate_crc_(const uint8_t *data, uint8_t datalen) { + // calculates 8-Bit checksum with given polynomial + uint8_t crc = SGP40_CRC8_INIT; + + for (uint8_t i = 0; i < datalen; i++) { + crc ^= data[i]; + for (uint8_t b = 0; b < 8; b++) { + if (crc & 0x80) + crc = (crc << 1) ^ SGP40_CRC8_POLYNOMIAL; + else + crc <<= 1; + } + } + return crc; +} + +void SGP40Component::update() { + this->seconds_since_last_store_ += this->update_interval_ / 1000; + + uint32_t voc_index = this->measure_voc_index_(); + + if (this->samples_read_++ < this->samples_to_stabalize_) { + ESP_LOGD(TAG, "Sensor has not collected enough samples yet. (%d/%d) VOC index is: %u", this->samples_read_, + this->samples_to_stabalize_, voc_index); + return; + } + + if (voc_index != UINT16_MAX) { + this->status_clear_warning(); + this->publish_state(voc_index); + } else { + this->status_set_warning(); + } +} + +void SGP40Component::dump_config() { + ESP_LOGCONFIG(TAG, "SGP40:"); + 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; + default: + ESP_LOGW(TAG, "Unknown setup error!"); + break; + } + } else { + ESP_LOGCONFIG(TAG, " Serial number: %llu", this->serial_number_); + ESP_LOGCONFIG(TAG, " Minimum Samples: %f", VOC_ALGORITHM_INITIAL_BLACKOUT); + } + LOG_UPDATE_INTERVAL(this); + + if (this->humidity_sensor_ != nullptr && this->temperature_sensor_ != nullptr) { + ESP_LOGCONFIG(TAG, " Compensation:"); + LOG_SENSOR(" ", "Temperature Source:", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity Source:", this->humidity_sensor_); + } else { + ESP_LOGCONFIG(TAG, " Compensation: No source configured"); + } +} + +bool SGP40Component::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 SGP40Component::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 SGP40Component::read_data_(uint16_t *data, uint8_t len) { + const uint8_t num_bytes = len * 3; + std::vector buf(num_bytes); + + if (!this->parent_->raw_receive(this->address_, buf.data(), num_bytes)) { + 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); + return false; + } + data[i] = (buf[j] << 8) | buf[j + 1]; + } + + return true; +} + +} // namespace sgp40 +} // namespace esphome diff --git a/esphome/components/sgp40/sgp40.h b/esphome/components/sgp40/sgp40.h new file mode 100644 index 0000000000..d448b5e45c --- /dev/null +++ b/esphome/components/sgp40/sgp40.h @@ -0,0 +1,92 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" +#include "esphome/core/application.h" +#include "esphome/core/preferences.h" +#include "sensirion_voc_algorithm.h" + +#include + +namespace esphome { +namespace sgp40 { + +struct SGP40Baselines { + int32_t state0; + int32_t state1; +} PACKED; // NOLINT + +// commands and constants +static const uint8_t SGP40_FEATURESET = 0x0020; ///< The required set for this library +static const uint8_t SGP40_CRC8_POLYNOMIAL = 0x31; ///< Seed for SGP40's CRC polynomial +static const uint8_t SGP40_CRC8_INIT = 0xFF; ///< Init value for CRC +static const uint8_t SGP40_WORD_LEN = 2; ///< 2 bytes per word + +// Commands + +static const uint16_t SGP40_CMD_GET_SERIAL_ID = 0x3682; +static const uint16_t SGP40_CMD_GET_FEATURESET = 0x202f; +static const uint16_t SGP40_CMD_SELF_TEST = 0x280e; + +// Shortest time interval of 3H for storing baseline values. +// Prevents wear of the flash because of too many write operations +const long SHORTEST_BASELINE_STORE_INTERVAL = 10800; + +// Store anyway if the baseline difference exceeds the max storage diff value +const long MAXIMUM_STORAGE_DIFF = 50; + +class SGP40Component; + +/// This class implements support for the Sensirion sgp40 i2c GAS (VOC) sensors. +class SGP40Component : public PollingComponent, public sensor::Sensor, public i2c::I2CDevice { + public: + 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; } + void set_store_baseline(bool store_baseline) { store_baseline_ = store_baseline; } + + protected: + /// Input sensor for humidity and temperature compensation. + sensor::Sensor *humidity_sensor_{nullptr}; + sensor::Sensor *temperature_sensor_{nullptr}; + bool write_command_(uint16_t command); + bool read_data_(uint16_t *data, uint8_t len); + int16_t sensirion_init_sensors_(); + int16_t sgp40_probe_(); + uint8_t sht_crc_(uint8_t data1, uint8_t data2); + uint64_t serial_number_; + uint16_t featureset_; + int32_t measure_voc_index_(); + uint8_t generate_crc_(const uint8_t *data, uint8_t datalen); + uint16_t measure_raw_(); + ESPPreferenceObject pref_; + long seconds_since_last_store_; + SGP40Baselines baselines_storage_; + VocAlgorithmParams voc_algorithm_params_; + bool store_baseline_; + int32_t state0_; + int32_t state1_; + uint8_t samples_read_ = 0; + uint8_t samples_to_stabalize_ = static_cast(VOC_ALGORITHM_INITIAL_BLACKOUT) * 2; + + /** + * @brief Request the sensor to perform a self-test, returning the result + * + * @return true: success false:failure + */ + void self_test_(); + enum ErrorCode { + COMMUNICATION_FAILED, + MEASUREMENT_INIT_FAILED, + INVALID_ID, + UNSUPPORTED_ID, + UNKNOWN + } error_code_{UNKNOWN}; +}; +} // namespace sgp40 +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 41f688e4e8..8742387a7d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -235,10 +235,6 @@ wled: adalight: -mcp3008: - - id: 'mcp3008_hub' - cs_pin: GPIO12 - mcp23s08: - id: 'mcp23s08_hub' cs_pin: GPIO12 @@ -877,12 +873,6 @@ sensor: id: ph_ezo address: 99 unit_of_measurement: 'pH' - - platform: mcp3008 - update_interval: 5s - mcp3008_id: 'mcp3008_hub' - id: freezer_temp_source - reference_voltage: 3.19 - number: 0 esp32_touch: setup_mode: False @@ -1488,14 +1478,14 @@ climate: min_temperature: 18 °C max_temperature: 25 °C temperature_step: 0.1 °C - name: "Electrolux EACS" + name: 'Electrolux EACS' beeper: true outdoor_temperature: - name: "Temp" + name: 'Temp' power_usage: - name: "Power" + name: 'Power' humidity_setpoint: - name: "Hum" + name: 'Hum' midea_dongle: uart_id: uart0 diff --git a/tests/test2.yaml b/tests/test2.yaml index 04514cbef2..fa21c1044a 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -54,6 +54,10 @@ deep_sleep: as3935_i2c: irq_pin: GPIO12 +mcp3008: + - id: 'mcp3008_hub' + cs_pin: GPIO12 + sensor: - platform: homeassistant entity_id: sensor.hello_world @@ -212,12 +216,21 @@ sensor: - platform: inkbird_ibsth1_mini mac_address: 38:81:D7:0A:9C:11 temperature: - name: 'Inkbird IBS-TH1 Temperature' + name: 'Inkbird IBS-TH1 Temperature' humidity: - name: 'Inkbird IBS-TH1 Humidity' + name: 'Inkbird IBS-TH1 Humidity' battery_level: - name: 'Inkbird IBS-TH1 Battery Level' - + name: 'Inkbird IBS-TH1 Battery Level' + - platform: sgp40 + name: 'Workshop VOC' + update_interval: 5s + store_baseline: 'true' + - platform: mcp3008 + update_interval: 5s + mcp3008_id: 'mcp3008_hub' + id: freezer_temp_source + reference_voltage: 3.19 + number: 0 time: - platform: homeassistant on_time: From af616473aa520136606f807d97b33f06dccfb5b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 9 Apr 2021 10:14:38 +0200 Subject: [PATCH 27/62] Bump protobuf from 3.15.7 to 3.15.8 (#1682) Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 3.15.7 to 3.15.8. - [Release notes](https://github.com/protocolbuffers/protobuf/releases) - [Changelog](https://github.com/protocolbuffers/protobuf/blob/master/generate_changelog.py) - [Commits](https://github.com/protocolbuffers/protobuf/compare/v3.15.7...v3.15.8) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2fe8909e16..f749358001 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ PyYAML==5.4.1 paho-mqtt==1.5.1 colorama==0.4.4 tornado==6.1 -protobuf==3.15.7 +protobuf==3.15.8 tzlocal==2.1 pytz==2021.1 pyserial==3.5 From d8e4f5d56be9526d99d7cc88db85d7d6eee27280 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 9 Apr 2021 10:27:18 +0200 Subject: [PATCH 28/62] Sensor Average Filter Fix Floating Pointer Error Accumulating (#1624) --- esphome/components/sensor/filter.cpp | 18 +++++++++++++----- esphome/components/sensor/filter.h | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 6dfb11b9c9..57ffe9b482 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -148,10 +148,10 @@ void SlidingWindowMovingAverageFilter::set_window_size(size_t window_size) { thi optional SlidingWindowMovingAverageFilter::new_value(float value) { if (!isnan(value)) { if (this->queue_.size() == this->window_size_) { - this->sum_ -= this->queue_.front(); - this->queue_.pop(); + this->sum_ -= this->queue_[0]; + this->queue_.pop_front(); } - this->queue_.push(value); + this->queue_.push_back(value); this->sum_ += value; } float average; @@ -161,8 +161,16 @@ optional SlidingWindowMovingAverageFilter::new_value(float value) { average = this->sum_ / this->queue_.size(); ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) -> %f", this, value, average); - if (++this->send_at_ >= this->send_every_) { - this->send_at_ = 0; + if (++this->send_at_ % this->send_every_ == 0) { + if (this->send_at_ >= 10000) { + // Recalculate to prevent floating point error accumulating + this->sum_ = 0; + for (auto v : this->queue_) + this->sum_ += v; + average = this->sum_ / this->queue_.size(); + this->send_at_ = 0; + } + ESP_LOGVV(TAG, "SlidingWindowMovingAverageFilter(%p)::new_value(%f) SENDING", this, value); return average; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 651d2a8986..5b06d002fa 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -162,7 +162,7 @@ class SlidingWindowMovingAverageFilter : public Filter { protected: float sum_{0.0}; - std::queue queue_; + std::deque queue_; size_t send_every_; size_t send_at_; size_t window_size_; From bf1885af3f5045a43dce38339485326678fc4b44 Mon Sep 17 00:00:00 2001 From: John Coggeshall Date: Sun, 11 Apr 2021 13:15:23 -0400 Subject: [PATCH 29/62] Implementing the remainder of GPS data for the GPS component. (#1676) --- CODEOWNERS | 1 + esphome/components/gps/__init__.py | 64 +++++++++++++++++++++++++++++- esphome/components/gps/gps.cpp | 43 +++++++++++++++----- esphome/components/gps/gps.h | 27 ++++++++++++- esphome/components/sun/__init__.py | 10 +++-- esphome/const.py | 5 +++ 6 files changed, 134 insertions(+), 16 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 472e304fb0..24b4bd362d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -38,6 +38,7 @@ esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core +esphome/components/gps/* @coogle esphome/components/homeassistant/* @OttoWinter esphome/components/i2c/* @esphome/core esphome/components/inkbird_ibsth1_mini/* @fkirill diff --git a/esphome/components/gps/__init__.py b/esphome/components/gps/__init__.py index 60dcc2002c..c09a49315c 100644 --- a/esphome/components/gps/__init__.py +++ b/esphome/components/gps/__init__.py @@ -1,9 +1,27 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import uart -from esphome.const import CONF_ID +from esphome.components import sensor +from esphome.const import ( + CONF_ID, + CONF_LATITUDE, + CONF_LONGITUDE, + CONF_SPEED, + CONF_COURSE, + CONF_ALTITUDE, + CONF_SATELLITES, + UNIT_DEGREES, + UNIT_KILOMETER_PER_HOUR, + UNIT_METER, + UNIT_EMPTY, + ICON_EMPTY, + DEVICE_CLASS_EMPTY, +) DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor"] + +CODEOWNERS = ["@coogle"] gps_ns = cg.esphome_ns.namespace("gps") GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) @@ -15,9 +33,27 @@ CONFIG_SCHEMA = ( cv.Schema( { cv.GenerateID(): cv.declare_id(GPS), + cv.Optional(CONF_LATITUDE): sensor.sensor_schema( + UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( + UNIT_DEGREES, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_SPEED): sensor.sensor_schema( + UNIT_KILOMETER_PER_HOUR, ICON_EMPTY, 6, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_COURSE): sensor.sensor_schema( + UNIT_DEGREES, ICON_EMPTY, 2, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( + UNIT_METER, ICON_EMPTY, 1, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_SATELLITES): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY + ), } ) - .extend(cv.COMPONENT_SCHEMA) + .extend(cv.polling_component_schema("20s")) .extend(uart.UART_DEVICE_SCHEMA) ) @@ -27,5 +63,29 @@ def to_code(config): yield cg.register_component(var, config) yield uart.register_uart_device(var, config) + if CONF_LATITUDE in config: + sens = yield sensor.new_sensor(config[CONF_LATITUDE]) + cg.add(var.set_latitude_sensor(sens)) + + if CONF_LONGITUDE in config: + sens = yield sensor.new_sensor(config[CONF_LONGITUDE]) + cg.add(var.set_longitude_sensor(sens)) + + if CONF_SPEED in config: + sens = yield sensor.new_sensor(config[CONF_SPEED]) + cg.add(var.set_speed_sensor(sens)) + + if CONF_COURSE in config: + sens = yield sensor.new_sensor(config[CONF_COURSE]) + cg.add(var.set_course_sensor(sens)) + + if CONF_ALTITUDE in config: + sens = yield sensor.new_sensor(config[CONF_ALTITUDE]) + cg.add(var.set_altitude_sensor(sens)) + + if CONF_SATELLITES in config: + sens = yield sensor.new_sensor(config[CONF_SATELLITES]) + cg.add(var.set_satellites_sensor(sens)) + # https://platformio.org/lib/show/1655/TinyGPSPlus cg.add_library("1655", "1.0.2") # TinyGPSPlus, has name conflict diff --git a/esphome/components/gps/gps.cpp b/esphome/components/gps/gps.cpp index 26371565f3..ba0afdf7cc 100644 --- a/esphome/components/gps/gps.cpp +++ b/esphome/components/gps/gps.cpp @@ -8,34 +8,57 @@ static const char *TAG = "gps"; TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } +void GPS::update() { + if (this->latitude_sensor_ != nullptr) + this->latitude_sensor_->publish_state(this->latitude_); + + if (this->longitude_sensor_ != nullptr) + this->longitude_sensor_->publish_state(this->longitude_); + + if (this->speed_sensor_ != nullptr) + this->speed_sensor_->publish_state(this->speed_); + + if (this->course_sensor_ != nullptr) + this->course_sensor_->publish_state(this->course_); + + if (this->altitude_sensor_ != nullptr) + this->altitude_sensor_->publish_state(this->altitude_); + + if (this->satellites_sensor_ != nullptr) + this->satellites_sensor_->publish_state(this->satellites_); +} + void GPS::loop() { while (this->available() && !this->has_time_) { if (this->tiny_gps_.encode(this->read())) { if (tiny_gps_.location.isUpdated()) { + this->latitude_ = tiny_gps_.location.lat(); + this->longitude_ = tiny_gps_.location.lng(); + ESP_LOGD(TAG, "Location:"); - ESP_LOGD(TAG, " Lat: %f", tiny_gps_.location.lat()); - ESP_LOGD(TAG, " Lon: %f", tiny_gps_.location.lng()); + ESP_LOGD(TAG, " Lat: %f", this->latitude_); + ESP_LOGD(TAG, " Lon: %f", this->longitude_); } if (tiny_gps_.speed.isUpdated()) { + this->speed_ = tiny_gps_.speed.kmph(); ESP_LOGD(TAG, "Speed:"); - ESP_LOGD(TAG, " %f km/h", tiny_gps_.speed.kmph()); + ESP_LOGD(TAG, " %f km/h", this->speed_); } if (tiny_gps_.course.isUpdated()) { + this->course_ = tiny_gps_.course.deg(); ESP_LOGD(TAG, "Course:"); - ESP_LOGD(TAG, " %f °", tiny_gps_.course.deg()); + ESP_LOGD(TAG, " %f °", this->course_); } if (tiny_gps_.altitude.isUpdated()) { + this->altitude_ = tiny_gps_.altitude.meters(); ESP_LOGD(TAG, "Altitude:"); - ESP_LOGD(TAG, " %f m", tiny_gps_.altitude.meters()); + ESP_LOGD(TAG, " %f m", this->altitude_); } if (tiny_gps_.satellites.isUpdated()) { + this->satellites_ = tiny_gps_.satellites.value(); 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()); + ESP_LOGD(TAG, " %d", this->satellites_); } for (auto *listener : this->listeners_) diff --git a/esphome/components/gps/gps.h b/esphome/components/gps/gps.h index 84a9248bc6..50dd476ae3 100644 --- a/esphome/components/gps/gps.h +++ b/esphome/components/gps/gps.h @@ -2,6 +2,7 @@ #include "esphome/core/component.h" #include "esphome/components/uart/uart.h" +#include "esphome/components/sensor/sensor.h" #include namespace esphome { @@ -20,17 +21,41 @@ class GPSListener { GPS *parent_; }; -class GPS : public Component, public uart::UARTDevice { +class GPS : public PollingComponent, public uart::UARTDevice { public: + void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; } + void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; } + void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; } + void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } + void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } + void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } + void register_listener(GPSListener *listener) { listener->parent_ = this; this->listeners_.push_back(listener); } float get_setup_priority() const override { return setup_priority::HARDWARE; } + void loop() override; + void update() override; + TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } protected: + float latitude_ = -1; + float longitude_ = -1; + float speed_ = -1; + float course_ = -1; + float altitude_ = -1; + int satellites_ = -1; + + sensor::Sensor *latitude_sensor_{nullptr}; + sensor::Sensor *longitude_sensor_{nullptr}; + sensor::Sensor *speed_sensor_{nullptr}; + sensor::Sensor *course_sensor_{nullptr}; + sensor::Sensor *altitude_sensor_{nullptr}; + sensor::Sensor *satellites_sensor_{nullptr}; + bool has_time_{false}; TinyGPSPlus tiny_gps_; std::vector listeners_{}; diff --git a/esphome/components/sun/__init__.py b/esphome/components/sun/__init__.py index b24d62f32f..8cc911529b 100644 --- a/esphome/components/sun/__init__.py +++ b/esphome/components/sun/__init__.py @@ -4,7 +4,13 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import time -from esphome.const import CONF_TIME_ID, CONF_ID, CONF_TRIGGER_ID +from esphome.const import ( + CONF_TIME_ID, + CONF_ID, + CONF_TRIGGER_ID, + CONF_LATITUDE, + CONF_LONGITUDE, +) CODEOWNERS = ["@OttoWinter"] sun_ns = cg.esphome_ns.namespace("sun") @@ -16,8 +22,6 @@ SunTrigger = sun_ns.class_( SunCondition = sun_ns.class_("SunCondition", automation.Condition) CONF_SUN_ID = "sun_id" -CONF_LATITUDE = "latitude" -CONF_LONGITUDE = "longitude" CONF_ELEVATION = "elevation" CONF_ON_SUNRISE = "on_sunrise" CONF_ON_SUNSET = "on_sunset" diff --git a/esphome/const.py b/esphome/const.py index 48bab31c6c..629495bdd1 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -58,6 +58,7 @@ CONF_ACTION_ID = "action_id" CONF_ADDRESS = "address" CONF_ADDRESSABLE_LIGHT_ID = "addressable_light_id" CONF_ALPHA = "alpha" +CONF_ALTITUDE = "altitude" CONF_AND = "and" CONF_AP = "ap" CONF_ARDUINO_VERSION = "arduino_version" @@ -130,6 +131,7 @@ CONF_CONTRAST = "contrast" CONF_COOL_ACTION = "cool_action" CONF_COOL_MODE = "cool_mode" CONF_COUNT_MODE = "count_mode" +CONF_COURSE = "course" CONF_CRON = "cron" CONF_CS_PIN = "cs_pin" CONF_CSS_INCLUDE = "css_include" @@ -272,6 +274,7 @@ CONF_KEEP_ON_TIME = "keep_on_time" CONF_KEEPALIVE = "keepalive" CONF_KEY = "key" CONF_LAMBDA = "lambda" +CONF_LATITUDE = "latitude" CONF_LENGTH = "length" CONF_LEVEL = "level" CONF_LG = "lg" @@ -284,6 +287,7 @@ CONF_LOCAL = "local" CONF_LOG_TOPIC = "log_topic" CONF_LOGGER = "logger" CONF_LOGS = "logs" +CONF_LONGITUDE = "longitude" CONF_LOW = "low" CONF_LOW_VOLTAGE_REFERENCE = "low_voltage_reference" CONF_MAC_ADDRESS = "mac_address" @@ -458,6 +462,7 @@ CONF_RX_ONLY = "rx_only" CONF_RX_PIN = "rx_pin" CONF_SAFE_MODE = "safe_mode" CONF_SAMSUNG = "samsung" +CONF_SATELLITES = "satellites" CONF_SCAN = "scan" CONF_SCL = "scl" CONF_SCL_PIN = "scl_pin" From aed6f2b1ea860d9db73f53e99cd6982b38b5593c Mon Sep 17 00:00:00 2001 From: Maurice Makaay Date: Tue, 20 Apr 2021 01:15:37 +0200 Subject: [PATCH 30/62] Bump AsyncTCP-esphome to 1.2.1. (#1693) Co-authored-by: Maurice Makaay --- esphome/components/async_tcp/__init__.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index b07db9ed7c..c6c0a9de4d 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -9,7 +9,7 @@ CODEOWNERS = ["@OttoWinter"] def to_code(config): if CORE.is_esp32: # https://github.com/OttoWinter/AsyncTCP/blob/master/library.json - cg.add_library("AsyncTCP-esphome", "1.1.1") + cg.add_library("AsyncTCP-esphome", "1.2.1") elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP cg.add_library("ESPAsyncTCP-esphome", "1.2.3") diff --git a/platformio.ini b/platformio.ini index 1dbc88cfde..e9fedb0a1e 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ include_dir = include [common] lib_deps = - AsyncTCP-esphome@1.1.1 + AsyncTCP-esphome@1.2.1 AsyncMqttClient-esphome@0.8.4 ArduinoJson-esphomelib@5.13.3 ESPAsyncWebServer-esphome@1.2.7 From 820dedbcd2f3b9687604f3c3b985b06d7e20a4e7 Mon Sep 17 00:00:00 2001 From: Richard Klingler Date: Wed, 21 Apr 2021 12:10:45 +0200 Subject: [PATCH 31/62] Added / to default glyphs (#1691) Co-authored-by: Charlie Root --- esphome/components/font/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index e79d311dab..c414d37c40 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -72,7 +72,7 @@ def validate_truetype_file(value): DEFAULT_GLYPHS = ( - ' !"%()+,-.:0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' + ' !"%()+,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) CONF_RAW_DATA_ID = "raw_data_id" From bb3d0706d3de6571d17d0d1452605b677eb4dee7 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 21 Apr 2021 10:33:59 -0300 Subject: [PATCH 32/62] Revert "Bump AsyncTCP-esphome to 1.2.1. (#1693)" (#1709) This reverts commit aed6f2b1ea860d9db73f53e99cd6982b38b5593c. --- esphome/components/async_tcp/__init__.py | 2 +- platformio.ini | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/async_tcp/__init__.py b/esphome/components/async_tcp/__init__.py index c6c0a9de4d..b07db9ed7c 100644 --- a/esphome/components/async_tcp/__init__.py +++ b/esphome/components/async_tcp/__init__.py @@ -9,7 +9,7 @@ CODEOWNERS = ["@OttoWinter"] def to_code(config): if CORE.is_esp32: # https://github.com/OttoWinter/AsyncTCP/blob/master/library.json - cg.add_library("AsyncTCP-esphome", "1.2.1") + cg.add_library("AsyncTCP-esphome", "1.1.1") elif CORE.is_esp8266: # https://github.com/OttoWinter/ESPAsyncTCP cg.add_library("ESPAsyncTCP-esphome", "1.2.3") diff --git a/platformio.ini b/platformio.ini index e9fedb0a1e..1dbc88cfde 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,7 +10,7 @@ include_dir = include [common] lib_deps = - AsyncTCP-esphome@1.2.1 + AsyncTCP-esphome@1.1.1 AsyncMqttClient-esphome@0.8.4 ArduinoJson-esphomelib@5.13.3 ESPAsyncWebServer-esphome@1.2.7 From 6d3ccf4df569469aab60065c1d0f7b15efe2114c Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Mon, 26 Apr 2021 22:41:16 -0300 Subject: [PATCH 33/62] Change mac add mac suffix from underscore to dash (#1702) --- esphome/core/application.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/application.h b/esphome/core/application.h index e9c8638f60..aeda245161 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -39,7 +39,7 @@ class Application { public: void pre_setup(const std::string &name, const char *compilation_time, bool name_add_mac_suffix) { if (name_add_mac_suffix) { - this->name_ = name + "_" + get_mac_address().substr(6); + this->name_ = name + "-" + get_mac_address().substr(6); } else { this->name_ = name; } From cc43e01e348b6e8640e3a085f89d2d1fa0a2fbb2 Mon Sep 17 00:00:00 2001 From: Christian Ferbar <5595808+ferbar@users.noreply.github.com> Date: Wed, 28 Apr 2021 05:05:04 +0200 Subject: [PATCH 34/62] Add monochromatic effects: Pulse, Random (#1616) --- esphome/components/light/base_light_effects.h | 46 +++++++++++++++++-- esphome/components/light/effects.py | 23 +++++++++- esphome/components/light/types.py | 1 + 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index d6d930e9d4..775ee363af 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -11,6 +11,40 @@ inline static float random_cubic_float() { return r * r * r; } +/// Pulse effect. +class PulseLightEffect : public LightEffect { + public: + explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} + + void apply() override { + const uint32_t now = millis(); + if (now - this->last_color_change_ < this->update_interval_) { + return; + } + auto call = this->state_->turn_on(); + float out = this->on_ ? 1.0 : 0.0; + call.set_brightness_if_supported(out); + this->on_ = !this->on_; + call.set_transition_length_if_supported(this->transition_length_); + // don't tell HA every change + call.set_publish(false); + call.set_save(false); + call.perform(); + + this->last_color_change_ = now; + } + + void set_transition_length(uint32_t transition_length) { this->transition_length_ = transition_length; } + + void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } + + protected: + bool on_ = false; + uint32_t last_color_change_{0}; + uint32_t transition_length_{}; + uint32_t update_interval_{}; +}; + /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. class RandomLightEffect : public LightEffect { public: @@ -22,10 +56,14 @@ class RandomLightEffect : public LightEffect { return; } auto call = this->state_->turn_on(); - call.set_red_if_supported(random_float()); - call.set_green_if_supported(random_float()); - call.set_blue_if_supported(random_float()); - call.set_white_if_supported(random_float()); + if (this->state_->get_traits().get_supports_rgb()) { + call.set_red_if_supported(random_float()); + call.set_green_if_supported(random_float()); + call.set_blue_if_supported(random_float()); + call.set_white_if_supported(random_float()); + } else { + call.set_brightness_if_supported(random_float()); + } call.set_color_temperature_if_supported(random_float()); call.set_transition_length_if_supported(this->transition_length_); call.set_publish(true); diff --git a/esphome/components/light/effects.py b/esphome/components/light/effects.py index 9f017de98b..08a78d90ed 100644 --- a/esphome/components/light/effects.py +++ b/esphome/components/light/effects.py @@ -26,6 +26,7 @@ from esphome.const import ( from esphome.util import Registry from .types import ( LambdaLightEffect, + PulseLightEffect, RandomLightEffect, StrobeLightEffect, StrobeLightEffectColor, @@ -152,7 +153,27 @@ def automation_effect_to_code(config, effect_id): yield var -@register_rgb_effect( +@register_monochromatic_effect( + "pulse", + PulseLightEffect, + "Pulse", + { + cv.Optional( + CONF_TRANSITION_LENGTH, default="1s" + ): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_UPDATE_INTERVAL, default="1s" + ): cv.positive_time_period_milliseconds, + }, +) +def pulse_effect_to_code(config, effect_id): + effect = cg.new_Pvariable(effect_id, config[CONF_NAME]) + cg.add(effect.set_transition_length(config[CONF_TRANSITION_LENGTH])) + cg.add(effect.set_update_interval(config[CONF_UPDATE_INTERVAL])) + yield effect + + +@register_monochromatic_effect( "random", RandomLightEffect, "Random", diff --git a/esphome/components/light/types.py b/esphome/components/light/types.py index 4bca266b67..7c96cda7b1 100644 --- a/esphome/components/light/types.py +++ b/esphome/components/light/types.py @@ -31,6 +31,7 @@ LightTurnOffTrigger = light_ns.class_( # Effects LightEffect = light_ns.class_("LightEffect") +PulseLightEffect = light_ns.class_("PulseLightEffect", LightEffect) RandomLightEffect = light_ns.class_("RandomLightEffect", LightEffect) LambdaLightEffect = light_ns.class_("LambdaLightEffect", LightEffect) AutomationLightEffect = light_ns.class_("AutomationLightEffect", LightEffect) From 7fb116d87df3a9cfc2d0c910a094b98601934dcd Mon Sep 17 00:00:00 2001 From: Stephen Tierney Date: Wed, 28 Apr 2021 18:22:46 +1000 Subject: [PATCH 35/62] Add support for SHT4X (#1512) --- CODEOWNERS | 1 + esphome/components/sht4x/__init__.py | 0 esphome/components/sht4x/sensor.py | 92 ++++++++++++++++++++++++++++ esphome/components/sht4x/sht4x.cpp | 89 +++++++++++++++++++++++++++ esphome/components/sht4x/sht4x.h | 45 ++++++++++++++ tests/test1.yaml | 7 +++ 6 files changed, 234 insertions(+) create mode 100644 esphome/components/sht4x/__init__.py create mode 100644 esphome/components/sht4x/sensor.py create mode 100644 esphome/components/sht4x/sht4x.cpp create mode 100644 esphome/components/sht4x/sht4x.h diff --git a/CODEOWNERS b/CODEOWNERS index 24b4bd362d..08189a13b3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -80,6 +80,7 @@ esphome/components/rtttl/* @glmnet esphome/components/script/* @esphome/core esphome/components/sensor/* @esphome/core esphome/components/sgp40/* @SenexCrenshaw +esphome/components/sht4x/* @sjtrny esphome/components/shutdown/* @esphome/core esphome/components/sim800l/* @glmnet esphome/components/spi/* @esphome/core diff --git a/esphome/components/sht4x/__init__.py b/esphome/components/sht4x/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/sht4x/sensor.py b/esphome/components/sht4x/sensor.py new file mode 100644 index 0000000000..bd3e5c108c --- /dev/null +++ b/esphome/components/sht4x/sensor.py @@ -0,0 +1,92 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_ID, + CONF_TEMPERATURE, + CONF_HUMIDITY, + UNIT_CELSIUS, + UNIT_PERCENT, + ICON_THERMOMETER, + ICON_WATER_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + +CODEOWNERS = ["@sjtrny"] +DEPENDENCIES = ["i2c"] + +sht4x_ns = cg.esphome_ns.namespace("sht4x") + +SHT4XComponent = sht4x_ns.class_("SHT4XComponent", cg.PollingComponent, i2c.I2CDevice) + +CONF_PRECISION = "precision" +SHT4XPRECISION = sht4x_ns.enum("SHT4XPRECISION") +PRECISION_OPTIONS = { + "High": SHT4XPRECISION.SHT4X_PRECISION_HIGH, + "Med": SHT4XPRECISION.SHT4X_PRECISION_MED, + "Low": SHT4XPRECISION.SHT4X_PRECISION_LOW, +} + +CONF_HEATER_POWER = "heater_power" +SHT4XHEATERPOWER = sht4x_ns.enum("SHT4XHEATERPOWER") +HEATER_POWER_OPTIONS = { + "High": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_HIGH, + "Med": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_MED, + "Low": SHT4XHEATERPOWER.SHT4X_HEATERPOWER_LOW, +} + +CONF_HEATER_TIME = "heater_time" +SHT4XHEATERTIME = sht4x_ns.enum("SHT4XHEATERTIME") +HEATER_TIME_OPTIONS = { + "Long": SHT4XHEATERTIME.SHT4X_HEATERTIME_LONG, + "Short": SHT4XHEATERTIME.SHT4X_HEATERTIME_SHORT, +} + +CONF_HEATER_MAX_DUTY = "heater_max_duty" + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(SHT4XComponent), + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + UNIT_CELSIUS, ICON_THERMOMETER, 2, DEVICE_CLASS_TEMPERATURE + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + UNIT_PERCENT, ICON_WATER_PERCENT, 2, DEVICE_CLASS_HUMIDITY + ), + cv.Optional(CONF_PRECISION, default="High"): cv.enum(PRECISION_OPTIONS), + cv.Optional(CONF_HEATER_POWER, default="High"): cv.enum( + HEATER_POWER_OPTIONS + ), + cv.Optional(CONF_HEATER_TIME, default="Long"): cv.enum(HEATER_TIME_OPTIONS), + cv.Optional(CONF_HEATER_MAX_DUTY, default=0.0): cv.float_range( + min=0.0, max=0.05 + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x44)) +) + +TYPES = { + CONF_TEMPERATURE: "set_temp_sensor", + CONF_HUMIDITY: "set_humidity_sensor", +} + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield i2c.register_i2c_device(var, config) + + cg.add(var.set_precision_value(config[CONF_PRECISION])) + cg.add(var.set_heater_power_value(config[CONF_HEATER_POWER])) + cg.add(var.set_heater_time_value(config[CONF_HEATER_TIME])) + cg.add(var.set_heater_duty_value(config[CONF_HEATER_MAX_DUTY])) + + for key, funcName in TYPES.items(): + + if key in config: + sens = yield sensor.new_sensor(config[key]) + cg.add(getattr(var, funcName)(sens)) diff --git a/esphome/components/sht4x/sht4x.cpp b/esphome/components/sht4x/sht4x.cpp new file mode 100644 index 0000000000..a4b315940d --- /dev/null +++ b/esphome/components/sht4x/sht4x.cpp @@ -0,0 +1,89 @@ +#include "sht4x.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace sht4x { + +static const char *TAG = "sht4x"; + +static const uint8_t MEASURECOMMANDS[] = {0xFD, 0xF6, 0xE0}; + +void SHT4XComponent::start_heater_() { + uint8_t cmd[] = {MEASURECOMMANDS[this->heater_command_]}; + + ESP_LOGD(TAG, "Heater turning on"); + this->write_bytes_raw(cmd, 1); +} + +void SHT4XComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up sht4x..."); + + if (this->duty_cycle_ > 0.0) { + uint32_t heater_interval = (uint32_t)(this->heater_time_ / this->duty_cycle_); + ESP_LOGD(TAG, "Heater interval: %i", heater_interval); + + if (this->heater_power_ == SHT4X_HEATERPOWER_HIGH) { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x39; + } else { + this->heater_command_ = 0x32; + } + } else if (this->heater_power_ == SHT4X_HEATERPOWER_MED) { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x2F; + } else { + this->heater_command_ = 0x24; + } + } else { + if (this->heater_time_ == SHT4X_HEATERTIME_LONG) { + this->heater_command_ = 0x1E; + } else { + this->heater_command_ = 0x15; + } + } + ESP_LOGD(TAG, "Heater command: %x", this->heater_command_); + + this->set_interval(heater_interval, std::bind(&SHT4XComponent::start_heater_, this)); + } +} + +void SHT4XComponent::dump_config() { LOG_I2C_DEVICE(this); } + +void SHT4XComponent::update() { + uint8_t cmd[] = {MEASURECOMMANDS[this->precision_]}; + + // Send command + this->write_bytes_raw(cmd, 1); + + this->set_timeout(10, [this]() { + const uint8_t num_bytes = 6; + uint8_t buffer[num_bytes]; + + // Read measurement + bool read_status = this->read_bytes_raw(buffer, num_bytes); + + if (read_status) { + // Evaluate and publish measurements + if (this->temp_sensor_ != nullptr) { + // Temp is contained in the first 16 bits + float sensor_value_temp = (buffer[0] << 8) + buffer[1]; + float temp = -45 + 175 * sensor_value_temp / 65535; + + this->temp_sensor_->publish_state(temp); + } + + if (this->humidity_sensor_ != nullptr) { + // Relative humidity is in the last 16 bits + float sensor_value_rh = (buffer[3] << 8) + buffer[4]; + float rh = -6 + 125 * sensor_value_rh / 65535; + + this->humidity_sensor_->publish_state(rh); + } + } else { + ESP_LOGD(TAG, "Sensor read failed"); + } + }); +} + +} // namespace sht4x +} // namespace esphome diff --git a/esphome/components/sht4x/sht4x.h b/esphome/components/sht4x/sht4x.h new file mode 100644 index 0000000000..8694bd9879 --- /dev/null +++ b/esphome/components/sht4x/sht4x.h @@ -0,0 +1,45 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace sht4x { + +enum SHT4XPRECISION { SHT4X_PRECISION_HIGH = 0, SHT4X_PRECISION_MED, SHT4X_PRECISION_LOW }; + +enum SHT4XHEATERPOWER { SHT4X_HEATERPOWER_HIGH, SHT4X_HEATERPOWER_MED, SHT4X_HEATERPOWER_LOW }; + +enum SHT4XHEATERTIME { SHT4X_HEATERTIME_LONG = 1100, SHT4X_HEATERTIME_SHORT = 110 }; + +class SHT4XComponent : public PollingComponent, public i2c::I2CDevice { + public: + float get_setup_priority() const override { return setup_priority::DATA; } + void setup() override; + void dump_config() override; + void update() override; + + void set_precision_value(SHT4XPRECISION precision) { this->precision_ = precision; }; + void set_heater_power_value(SHT4XHEATERPOWER heater_power) { this->heater_power_ = heater_power; }; + void set_heater_time_value(SHT4XHEATERTIME heater_time) { this->heater_time_ = heater_time; }; + void set_heater_duty_value(float duty_cycle) { this->duty_cycle_ = duty_cycle; }; + + void set_temp_sensor(sensor::Sensor *temp_sensor) { this->temp_sensor_ = temp_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + SHT4XPRECISION precision_; + SHT4XHEATERPOWER heater_power_; + SHT4XHEATERTIME heater_time_; + float duty_cycle_; + + void start_heater_(); + uint8_t heater_command_; + + sensor::Sensor *temp_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; +}; + +} // namespace sht4x +} // namespace esphome diff --git a/tests/test1.yaml b/tests/test1.yaml index 8742387a7d..8a93de09b1 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -741,6 +741,13 @@ sensor: id: 'workshop_PMC_10_0' address: 0x69 update_interval: 10s + - platform: sht4x + temperature: + name: 'SHT4X Temperature' + humidity: + name: 'SHT4X Humidity' + address: 0x44 + update_interval: 15s - platform: shtcx temperature: name: 'Living Room Temperature 10' From cc6d1e85ccc9bb38d51b95ea9fa7a833168ac664 Mon Sep 17 00:00:00 2001 From: elyorkhakimov Date: Wed, 28 Apr 2021 12:15:50 -0700 Subject: [PATCH 36/62] Addition of forward and reverse active energy counters to ATM90E32 sensor component (#1271) Co-authored-by: Elyor Khakimov --- esphome/components/atm90e32/atm90e32.cpp | 48 ++++++++++++++++++++++++ esphome/components/atm90e32/atm90e32.h | 14 +++++++ esphome/components/atm90e32/sensor.py | 16 ++++++++ esphome/const.py | 2 + 4 files changed, 80 insertions(+) diff --git a/esphome/components/atm90e32/atm90e32.cpp b/esphome/components/atm90e32/atm90e32.cpp index 85e38fce3e..d732212cdd 100644 --- a/esphome/components/atm90e32/atm90e32.cpp +++ b/esphome/components/atm90e32/atm90e32.cpp @@ -58,6 +58,24 @@ void ATM90E32Component::update() { if (this->phase_[2].power_factor_sensor_ != nullptr) { this->phase_[2].power_factor_sensor_->publish_state(this->get_power_factor_c_()); } + if (this->phase_[0].forward_active_energy_sensor_ != nullptr) { + this->phase_[0].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_a_()); + } + if (this->phase_[1].forward_active_energy_sensor_ != nullptr) { + this->phase_[1].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_b_()); + } + if (this->phase_[2].forward_active_energy_sensor_ != nullptr) { + this->phase_[2].forward_active_energy_sensor_->publish_state(this->get_forward_active_energy_c_()); + } + if (this->phase_[0].reverse_active_energy_sensor_ != nullptr) { + this->phase_[0].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_a_()); + } + if (this->phase_[1].reverse_active_energy_sensor_ != nullptr) { + this->phase_[1].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_b_()); + } + if (this->phase_[2].reverse_active_energy_sensor_ != nullptr) { + this->phase_[2].reverse_active_energy_sensor_->publish_state(this->get_reverse_active_energy_c_()); + } if (this->freq_sensor_ != nullptr) { this->freq_sensor_->publish_state(this->get_frequency_()); } @@ -119,16 +137,22 @@ void ATM90E32Component::dump_config() { LOG_SENSOR(" ", "Power A", this->phase_[0].power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[0].reactive_power_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[0].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[0].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[0].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Voltage B", this->phase_[1].voltage_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[1].current_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[1].power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[1].reactive_power_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[1].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[1].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[1].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[2].voltage_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[2].current_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[2].power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[2].reactive_power_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[2].power_factor_sensor_); + LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[2].forward_active_energy_sensor_); + LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[2].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); } @@ -239,6 +263,30 @@ float ATM90E32Component::get_power_factor_c_() { int16_t pf = this->read16_(ATM90E32_REGISTER_PFMEANC); return (float) pf / 1000; } +float ATM90E32Component::get_forward_active_energy_a_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYA); + return (float) val * 10 / 3200; // convert register value to WattHours +} +float ATM90E32Component::get_forward_active_energy_b_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYB); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_forward_active_energy_c_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_APENERGYC); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_reverse_active_energy_a_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYA); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_reverse_active_energy_b_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYB); + return (float) val * 10 / 3200; +} +float ATM90E32Component::get_reverse_active_energy_c_() { + uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGYC); + return (float) val * 10 / 3200; +} float ATM90E32Component::get_frequency_() { uint16_t freq = this->read16_(ATM90E32_REGISTER_FREQ); return (float) freq / 100; diff --git a/esphome/components/atm90e32/atm90e32.h b/esphome/components/atm90e32/atm90e32.h index eb5de3878c..89d62adaf6 100644 --- a/esphome/components/atm90e32/atm90e32.h +++ b/esphome/components/atm90e32/atm90e32.h @@ -20,6 +20,12 @@ class ATM90E32Component : public PollingComponent, void set_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].current_sensor_ = obj; } void set_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_sensor_ = obj; } void set_reactive_power_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].reactive_power_sensor_ = obj; } + void set_forward_active_energy_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].forward_active_energy_sensor_ = obj; + } + void set_reverse_active_energy_sensor(int phase, sensor::Sensor *obj) { + this->phase_[phase].reverse_active_energy_sensor_ = obj; + } void set_power_factor_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].power_factor_sensor_ = obj; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].volt_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } @@ -52,6 +58,12 @@ class ATM90E32Component : public PollingComponent, float get_power_factor_a_(); float get_power_factor_b_(); float get_power_factor_c_(); + float get_forward_active_energy_a_(); + float get_forward_active_energy_b_(); + float get_forward_active_energy_c_(); + float get_reverse_active_energy_a_(); + float get_reverse_active_energy_b_(); + float get_reverse_active_energy_c_(); float get_frequency_(); float get_chip_temperature_(); @@ -63,6 +75,8 @@ class ATM90E32Component : public PollingComponent, sensor::Sensor *power_sensor_{nullptr}; sensor::Sensor *reactive_power_sensor_{nullptr}; sensor::Sensor *power_factor_sensor_{nullptr}; + sensor::Sensor *forward_active_energy_sensor_{nullptr}; + sensor::Sensor *reverse_active_energy_sensor_{nullptr}; } phase_[3]; sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr}; diff --git a/esphome/components/atm90e32/sensor.py b/esphome/components/atm90e32/sensor.py index d0813cfa52..4a9100d9d6 100644 --- a/esphome/components/atm90e32/sensor.py +++ b/esphome/components/atm90e32/sensor.py @@ -8,8 +8,11 @@ from esphome.const import ( CONF_POWER, CONF_POWER_FACTOR, CONF_FREQUENCY, + CONF_FORWARD_ACTIVE_ENERGY, + CONF_REVERSE_ACTIVE_ENERGY, DEVICE_CLASS_CURRENT, DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_POWER_FACTOR, DEVICE_CLASS_TEMPERATURE, @@ -24,6 +27,7 @@ from esphome.const import ( UNIT_EMPTY, UNIT_CELSIUS, UNIT_VOLT_AMPS_REACTIVE, + UNIT_WATT_HOURS, ) CONF_PHASE_A = "phase_a" @@ -73,6 +77,12 @@ ATM90E32_PHASE_SCHEMA = cv.Schema( cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema( UNIT_EMPTY, ICON_EMPTY, 2, DEVICE_CLASS_POWER_FACTOR ), + cv.Optional(CONF_FORWARD_ACTIVE_ENERGY): sensor.sensor_schema( + UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), + cv.Optional(CONF_REVERSE_ACTIVE_ENERGY): sensor.sensor_schema( + UNIT_WATT_HOURS, ICON_EMPTY, 2, DEVICE_CLASS_ENERGY + ), cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, } @@ -129,6 +139,12 @@ def to_code(config): if CONF_POWER_FACTOR in conf: sens = yield sensor.new_sensor(conf[CONF_POWER_FACTOR]) cg.add(var.set_power_factor_sensor(i, sens)) + if CONF_FORWARD_ACTIVE_ENERGY in conf: + sens = yield sensor.new_sensor(conf[CONF_FORWARD_ACTIVE_ENERGY]) + cg.add(var.set_forward_active_energy_sensor(i, sens)) + if CONF_REVERSE_ACTIVE_ENERGY in conf: + sens = yield sensor.new_sensor(conf[CONF_REVERSE_ACTIVE_ENERGY]) + cg.add(var.set_reverse_active_energy_sensor(i, sens)) if CONF_FREQUENCY in config: sens = yield sensor.new_sensor(config[CONF_FREQUENCY]) cg.add(var.set_freq_sensor(sens)) diff --git a/esphome/const.py b/esphome/const.py index 629495bdd1..618c9fd922 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -213,6 +213,7 @@ CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMAT = "format" +CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" CONF_FREQUENCY = "frequency" CONF_FROM = "from" CONF_FULL_UPDATE_EVERY = "full_update_every" @@ -447,6 +448,7 @@ CONF_RESTORE_MODE = "restore_mode" CONF_RESTORE_STATE = "restore_state" CONF_RESTORE_VALUE = "restore_value" CONF_RETAIN = "retain" +CONF_REVERSE_ACTIVE_ENERGY = "reverse_active_energy" CONF_RGB_ORDER = "rgb_order" CONF_RGBW = "rgbw" CONF_RISING_EDGE = "rising_edge" From 4d7c1ae143200b5ba21329505dcd8b17514fec14 Mon Sep 17 00:00:00 2001 From: Barry Loong Date: Thu, 29 Apr 2021 06:08:27 +0800 Subject: [PATCH 37/62] Add Grow Fingerprint Reader (#1356) --- CODEOWNERS | 1 + .../components/fingerprint_grow/__init__.py | 293 ++++++++++++ .../fingerprint_grow/binary_sensor.py | 20 + .../fingerprint_grow/fingerprint_grow.cpp | 434 ++++++++++++++++++ .../fingerprint_grow/fingerprint_grow.h | 276 +++++++++++ esphome/components/fingerprint_grow/sensor.py | 64 +++ esphome/const.py | 23 + tests/test3.yaml | 65 +++ 8 files changed, 1176 insertions(+) create mode 100644 esphome/components/fingerprint_grow/__init__.py create mode 100644 esphome/components/fingerprint_grow/binary_sensor.py create mode 100644 esphome/components/fingerprint_grow/fingerprint_grow.cpp create mode 100644 esphome/components/fingerprint_grow/fingerprint_grow.h create mode 100644 esphome/components/fingerprint_grow/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 08189a13b3..1a6a54a00c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -36,6 +36,7 @@ esphome/components/ds1307/* @badbadc0ffee esphome/components/exposure_notifications/* @OttoWinter esphome/components/ezo/* @ssieb esphome/components/fastled_base/* @OttoWinter +esphome/components/fingerprint_grow/* @OnFreund @loongyh esphome/components/globals/* @esphome/core esphome/components/gpio/* @esphome/core esphome/components/gps/* @coogle diff --git a/esphome/components/fingerprint_grow/__init__.py b/esphome/components/fingerprint_grow/__init__.py new file mode 100644 index 0000000000..6fbaa4e6c9 --- /dev/null +++ b/esphome/components/fingerprint_grow/__init__.py @@ -0,0 +1,293 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome import automation +from esphome import pins +from esphome.components import uart +from esphome.const import ( + CONF_COLOR, + CONF_COUNT, + CONF_FINGER_ID, + CONF_ID, + CONF_NEW_PASSWORD, + CONF_NUM_SCANS, + CONF_ON_ENROLLMENT_DONE, + CONF_ON_ENROLLMENT_FAILED, + CONF_ON_ENROLLMENT_SCAN, + CONF_ON_FINGER_SCAN_MATCHED, + CONF_ON_FINGER_SCAN_UNMATCHED, + CONF_PASSWORD, + CONF_SENSING_PIN, + CONF_SPEED, + CONF_STATE, + CONF_TRIGGER_ID, +) + +CODEOWNERS = ["@OnFreund", "@loongyh"] +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["binary_sensor", "sensor"] +MULTI_CONF = True + +CONF_FINGERPRINT_GROW_ID = "fingerprint_grow_id" + +fingerprint_grow_ns = cg.esphome_ns.namespace("fingerprint_grow") +FingerprintGrowComponent = fingerprint_grow_ns.class_( + "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice +) + +FingerScanMatchedTrigger = fingerprint_grow_ns.class_( + "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16) +) + +FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_( + "FingerScanUnmatchedTrigger", automation.Trigger.template() +) + +EnrollmentScanTrigger = fingerprint_grow_ns.class_( + "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16) +) + +EnrollmentDoneTrigger = fingerprint_grow_ns.class_( + "EnrollmentDoneTrigger", automation.Trigger.template(cg.uint16) +) + +EnrollmentFailedTrigger = fingerprint_grow_ns.class_( + "EnrollmentFailedTrigger", automation.Trigger.template(cg.uint16) +) + +EnrollmentAction = fingerprint_grow_ns.class_("EnrollmentAction", automation.Action) +CancelEnrollmentAction = fingerprint_grow_ns.class_( + "CancelEnrollmentAction", automation.Action +) +DeleteAction = fingerprint_grow_ns.class_("DeleteAction", automation.Action) +DeleteAllAction = fingerprint_grow_ns.class_("DeleteAllAction", automation.Action) +LEDControlAction = fingerprint_grow_ns.class_("LEDControlAction", automation.Action) +AuraLEDControlAction = fingerprint_grow_ns.class_( + "AuraLEDControlAction", automation.Action +) + +AuraLEDState = fingerprint_grow_ns.enum("GrowAuraLEDState", True) +AURA_LED_STATES = { + "BREATHING": AuraLEDState.BREATHING, + "FLASHING": AuraLEDState.FLASHING, + "ALWAYS_ON": AuraLEDState.ALWAYS_ON, + "ALWAYS_OFF": AuraLEDState.ALWAYS_OFF, + "GRADUAL_ON": AuraLEDState.GRADUAL_ON, + "GRADUAL_OFF": AuraLEDState.GRADUAL_OFF, +} +validate_aura_led_states = cv.enum(AURA_LED_STATES, upper=True) +AuraLEDColor = fingerprint_grow_ns.enum("GrowAuraLEDColor", True) +AURA_LED_COLORS = { + "RED": AuraLEDColor.RED, + "BLUE": AuraLEDColor.BLUE, + "PURPLE": AuraLEDColor.PURPLE, +} +validate_aura_led_colors = cv.enum(AURA_LED_COLORS, upper=True) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(FingerprintGrowComponent), + cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema, + cv.Optional(CONF_PASSWORD): cv.uint32_t, + cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t, + cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanMatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_FINGER_SCAN_UNMATCHED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + FingerScanUnmatchedTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentScanTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_DONE): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentDoneTrigger + ), + } + ), + cv.Optional(CONF_ON_ENROLLMENT_FAILED): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + EnrollmentFailedTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("500ms")) + .extend(uart.UART_DEVICE_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + if CONF_PASSWORD in config: + password = config[CONF_PASSWORD] + cg.add(var.set_password(password)) + yield uart.register_uart_device(var, config) + + if CONF_NEW_PASSWORD in config: + new_password = config[CONF_NEW_PASSWORD] + cg.add(var.set_new_password(new_password)) + + if CONF_SENSING_PIN in config: + sensing_pin = yield cg.gpio_pin_expression(config[CONF_SENSING_PIN]) + cg.add(var.set_sensing_pin(sensing_pin)) + + for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation( + trigger, [(cg.uint16, "finger_id"), (cg.uint16, "confidence")], conf + ) + + for conf in config.get(CONF_ON_FINGER_SCAN_UNMATCHED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + + for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation( + trigger, [(cg.uint8, "scan_num"), (cg.uint16, "finger_id")], conf + ) + + for conf in config.get(CONF_ON_ENROLLMENT_DONE, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + + for conf in config.get(CONF_ON_ENROLLMENT_FAILED, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [(cg.uint16, "finger_id")], conf) + + +@automation.register_action( + "fingerprint_grow.enroll", + EnrollmentAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t), + cv.Optional(CONF_NUM_SCANS): cv.templatable(cv.uint8_t), + }, + key=CONF_FINGER_ID, + ), +) +def fingerprint_grow_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16) + cg.add(var.set_finger_id(template_)) + if CONF_NUM_SCANS in config: + template_ = yield cg.templatable(config[CONF_NUM_SCANS], args, cg.uint8) + cg.add(var.set_num_scans(template_)) + yield var + + +@automation.register_action( + "fingerprint_grow.cancel_enroll", + CancelEnrollmentAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + } + ), +) +def fingerprint_grow_cancel_enroll_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +@automation.register_action( + "fingerprint_grow.delete", + DeleteAction, + cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_FINGER_ID): cv.templatable(cv.uint16_t), + }, + key=CONF_FINGER_ID, + ), +) +def fingerprint_grow_delete_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + template_ = yield cg.templatable(config[CONF_FINGER_ID], args, cg.uint16) + cg.add(var.set_finger_id(template_)) + yield var + + +@automation.register_action( + "fingerprint_grow.delete_all", + DeleteAllAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + } + ), +) +def fingerprint_grow_delete_all_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + yield var + + +FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA = cv.maybe_simple_value( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_STATE): cv.templatable(cv.boolean), + }, + key=CONF_STATE, +) + + +@automation.register_action( + "fingerprint_grow.led_control", + LEDControlAction, + FINGERPRINT_GROW_LED_CONTROL_ACTION_SCHEMA, +) +def fingerprint_grow_led_control_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + template_ = yield cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) + yield var + + +@automation.register_action( + "fingerprint_grow.aura_led_control", + AuraLEDControlAction, + cv.Schema( + { + cv.GenerateID(): cv.use_id(FingerprintGrowComponent), + cv.Required(CONF_STATE): cv.templatable(validate_aura_led_states), + cv.Required(CONF_SPEED): cv.templatable(cv.uint8_t), + cv.Required(CONF_COLOR): cv.templatable(validate_aura_led_colors), + cv.Required(CONF_COUNT): cv.templatable(cv.uint8_t), + } + ), +) +def fingerprint_grow_aura_led_control_to_code(config, action_id, template_arg, args): + var = cg.new_Pvariable(action_id, template_arg) + yield cg.register_parented(var, config[CONF_ID]) + + for key in [CONF_STATE, CONF_SPEED, CONF_COLOR, CONF_COUNT]: + template_ = yield cg.templatable(config[key], args, cg.uint8) + cg.add(getattr(var, f"set_{key}")(template_)) + yield var diff --git a/esphome/components/fingerprint_grow/binary_sensor.py b/esphome/components/fingerprint_grow/binary_sensor.py new file mode 100644 index 0000000000..4f49841f15 --- /dev/null +++ b/esphome/components/fingerprint_grow/binary_sensor.py @@ -0,0 +1,20 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import binary_sensor +from esphome.const import CONF_ICON, ICON_KEY_PLUS +from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent + +DEPENDENCIES = ["fingerprint_grow"] + +CONFIG_SCHEMA = binary_sensor.BINARY_SENSOR_SCHEMA.extend( + { + cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), + cv.Optional(CONF_ICON, default=ICON_KEY_PLUS): cv.icon, + } +) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID]) + var = yield binary_sensor.new_binary_sensor(config) + cg.add(hub.set_enrolling_binary_sensor(var)) diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.cpp b/esphome/components/fingerprint_grow/fingerprint_grow.cpp new file mode 100644 index 0000000000..77ddf8ec37 --- /dev/null +++ b/esphome/components/fingerprint_grow/fingerprint_grow.cpp @@ -0,0 +1,434 @@ +#include "fingerprint_grow.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace fingerprint_grow { + +static const char* TAG = "fingerprint_grow"; + +// Based on Adafruit's library: https://github.com/adafruit/Adafruit-Fingerprint-Sensor-Library + +void FingerprintGrowComponent::update() { + if (this->enrollment_image_ > this->enrollment_buffers_) { + this->finish_enrollment(this->save_fingerprint_()); + return; + } + + if (this->sensing_pin_ != nullptr) { + if (this->sensing_pin_->digital_read() == HIGH) { + ESP_LOGV(TAG, "No touch sensing"); + this->waiting_removal_ = false; + return; + } + } + + if (this->waiting_removal_) { + if (this->scan_image_(1) == NO_FINGER) { + ESP_LOGD(TAG, "Finger removed"); + this->waiting_removal_ = false; + } + return; + } + + if (this->enrollment_image_ == 0) { + this->scan_and_match_(); + return; + } + + uint8_t result = this->scan_image_(this->enrollment_image_); + if (result == NO_FINGER) { + return; + } + this->waiting_removal_ = true; + if (result != OK) { + this->finish_enrollment(result); + return; + } + this->enrollment_scan_callback_.call(this->enrollment_image_, this->enrollment_slot_); + ++this->enrollment_image_; +} + +void FingerprintGrowComponent::setup() { + ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); + if (this->check_password_()) { + if (this->new_password_ != nullptr) { + if (this->set_password_()) + return; + } else { + if (this->get_parameters_()) + return; + } + } + this->mark_failed(); +} + +void FingerprintGrowComponent::enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers) { + ESP_LOGI(TAG, "Starting enrollment in slot %d", finger_id); + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(true); + } + this->enrollment_slot_ = finger_id; + this->enrollment_buffers_ = num_buffers; + this->enrollment_image_ = 1; +} + +void FingerprintGrowComponent::finish_enrollment(uint8_t result) { + if (result == OK) { + this->enrollment_done_callback_.call(this->enrollment_slot_); + } else { + this->enrollment_failed_callback_.call(this->enrollment_slot_); + } + this->enrollment_image_ = 0; + this->enrollment_slot_ = 0; + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(false); + } + ESP_LOGI(TAG, "Finished enrollment"); +} + +void FingerprintGrowComponent::scan_and_match_() { + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "Scan and match"); + } else { + ESP_LOGV(TAG, "Scan and match"); + } + if (this->scan_image_(1) == OK) { + this->waiting_removal_ = true; + this->data_ = {SEARCH, 0x01, 0x00, 0x00, (uint8_t)(this->capacity_ >> 8), (uint8_t)(this->capacity_ & 0xFF)}; + switch (this->send_command_()) { + case OK: { + ESP_LOGD(TAG, "Fingerprint matched"); + uint16_t finger_id = ((uint16_t) this->data_[1] << 8) | this->data_[2]; + uint16_t confidence = ((uint16_t) this->data_[3] << 8) | this->data_[4]; + if (this->last_finger_id_sensor_ != nullptr) { + this->last_finger_id_sensor_->publish_state(finger_id); + } + if (this->last_confidence_sensor_ != nullptr) { + this->last_confidence_sensor_->publish_state(confidence); + } + this->finger_scan_matched_callback_.call(finger_id, confidence); + break; + } + case NOT_FOUND: + ESP_LOGD(TAG, "Fingerprint not matched to any saved slots"); + this->finger_scan_unmatched_callback_.call(); + break; + } + } +} + +uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) { + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "Getting image %d", buffer); + } else { + ESP_LOGV(TAG, "Getting image %d", buffer); + } + this->data_ = {GET_IMAGE}; + switch (this->send_command_()) { + case OK: + break; + case NO_FINGER: + if (this->sensing_pin_ != nullptr) { + ESP_LOGD(TAG, "No finger"); + } else { + ESP_LOGV(TAG, "No finger"); + } + return this->data_[0]; + case IMAGE_FAIL: + ESP_LOGE(TAG, "Imaging error"); + default: + return this->data_[0]; + } + + ESP_LOGD(TAG, "Processing image %d", buffer); + this->data_ = {IMAGE_2_TZ, buffer}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Processed image %d", buffer); + break; + case IMAGE_MESS: + ESP_LOGE(TAG, "Image too messy"); + break; + case FEATURE_FAIL: + case INVALID_IMAGE: + ESP_LOGE(TAG, "Could not find fingerprint features"); + break; + } + return this->data_[0]; +} + +uint8_t FingerprintGrowComponent::save_fingerprint_() { + ESP_LOGI(TAG, "Creating model"); + this->data_ = {REG_MODEL}; + switch (this->send_command_()) { + case OK: + break; + case ENROLL_MISMATCH: + ESP_LOGE(TAG, "Scans do not match"); + default: + return this->data_[0]; + } + + ESP_LOGI(TAG, "Storing model"); + this->data_ = {STORE, 0x01, (uint8_t)(this->enrollment_slot_ >> 8), (uint8_t)(this->enrollment_slot_ & 0xFF)}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Stored model"); + break; + case BAD_LOCATION: + ESP_LOGE(TAG, "Invalid slot"); + break; + case FLASH_ERR: + ESP_LOGE(TAG, "Error writing to flash"); + break; + } + return this->data_[0]; +} + +bool FingerprintGrowComponent::check_password_() { + ESP_LOGD(TAG, "Checking password"); + this->data_ = {VERIFY_PASSWORD, (uint8_t)(this->password_ >> 24), (uint8_t)(this->password_ >> 16), + (uint8_t)(this->password_ >> 8), (uint8_t)(this->password_ & 0xFF)}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "Password verified"); + return true; + case PASSWORD_FAIL: + ESP_LOGE(TAG, "Wrong password"); + break; + } + return false; +} + +bool FingerprintGrowComponent::set_password_() { + ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_); + this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16), + (uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)}; + if (this->send_command_() == OK) { + ESP_LOGI(TAG, "New password successfully set"); + ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); + ESP_LOGW(TAG, "!!!Forgetting the password will render your device unusable!!!"); + return true; + } + return false; +} + +bool FingerprintGrowComponent::get_parameters_() { + ESP_LOGD(TAG, "Getting parameters"); + this->data_ = {READ_SYS_PARAM}; + if (this->send_command_() == OK) { + ESP_LOGD(TAG, "Got parameters"); + if (this->status_sensor_ != nullptr) { + this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); + } + this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6]; + if (this->capacity_sensor_ != nullptr) { + this->capacity_sensor_->publish_state(this->capacity_); + } + if (this->security_level_sensor_ != nullptr) { + this->security_level_sensor_->publish_state(((uint16_t) this->data_[7] << 8) | this->data_[8]); + } + if (this->enrolling_binary_sensor_ != nullptr) { + this->enrolling_binary_sensor_->publish_state(false); + } + this->get_fingerprint_count_(); + return true; + } + return false; +} + +void FingerprintGrowComponent::get_fingerprint_count_() { + ESP_LOGD(TAG, "Getting fingerprint count"); + this->data_ = {TEMPLATE_COUNT}; + if (this->send_command_() == OK) { + ESP_LOGD(TAG, "Got fingerprint count"); + if (this->fingerprint_count_sensor_ != nullptr) + this->fingerprint_count_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]); + } +} + +void FingerprintGrowComponent::delete_fingerprint(uint16_t finger_id) { + ESP_LOGI(TAG, "Deleting fingerprint in slot %d", finger_id); + this->data_ = {DELETE, (uint8_t)(finger_id >> 8), (uint8_t)(finger_id & 0xFF), 0x00, 0x01}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Deleted fingerprint"); + this->get_fingerprint_count_(); + break; + case DELETE_FAIL: + ESP_LOGE(TAG, "Reader failed to delete fingerprint"); + break; + } +} + +void FingerprintGrowComponent::delete_all_fingerprints() { + ESP_LOGI(TAG, "Deleting all stored fingerprints"); + this->data_ = {EMPTY}; + switch (this->send_command_()) { + case OK: + ESP_LOGI(TAG, "Deleted all fingerprints"); + this->get_fingerprint_count_(); + break; + case DB_CLEAR_FAIL: + ESP_LOGE(TAG, "Reader failed to clear fingerprint library"); + break; + } +} + +void FingerprintGrowComponent::led_control(bool state) { + ESP_LOGD(TAG, "Setting LED"); + if (state) + this->data_ = {LED_ON}; + else + this->data_ = {LED_OFF}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "LED set"); + break; + case PACKET_RCV_ERR: + case TIMEOUT: + break; + default: + ESP_LOGE(TAG, "Try aura_led_control instead"); + break; + } +} + +void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count) { + const uint32_t now = millis(); + const uint32_t elapsed = now - this->last_aura_led_control_; + if (elapsed < this->last_aura_led_duration_) { + delay(this->last_aura_led_duration_ - elapsed); + } + ESP_LOGD(TAG, "Setting Aura LED"); + this->data_ = {AURA_CONFIG, state, speed, color, count}; + switch (this->send_command_()) { + case OK: + ESP_LOGD(TAG, "Aura LED set"); + this->last_aura_led_control_ = millis(); + this->last_aura_led_duration_ = 10 * speed * count; + break; + case PACKET_RCV_ERR: + case TIMEOUT: + break; + default: + ESP_LOGE(TAG, "Try led_control instead"); + break; + } +} + +uint8_t FingerprintGrowComponent::send_command_() { + this->write((uint8_t)(START_CODE >> 8)); + this->write((uint8_t)(START_CODE & 0xFF)); + this->write(this->address_[0]); + this->write(this->address_[1]); + this->write(this->address_[2]); + this->write(this->address_[3]); + this->write(COMMAND); + + uint16_t wire_length = this->data_.size() + 2; + this->write((uint8_t)(wire_length >> 8)); + this->write((uint8_t)(wire_length & 0xFF)); + + uint16_t sum = ((wire_length) >> 8) + ((wire_length) &0xFF) + COMMAND; + for (auto data : this->data_) { + this->write(data); + sum += data; + } + + this->write((uint8_t)(sum >> 8)); + this->write((uint8_t)(sum & 0xFF)); + + this->data_.clear(); + + uint8_t byte; + uint16_t idx = 0, length = 0; + + for (uint16_t timer = 0; timer < 1000; timer++) { + if (this->available() == 0) { + delay(1); + continue; + } + byte = this->read(); + switch (idx) { + case 0: + if (byte != (uint8_t)(START_CODE >> 8)) + continue; + break; + case 1: + if (byte != (uint8_t)(START_CODE & 0xFF)) { + idx = 0; + continue; + } + break; + case 2: + case 3: + case 4: + case 5: + if (byte != this->address_[idx - 2]) { + idx = 0; + continue; + } + break; + case 6: + if (byte != ACK) { + idx = 0; + continue; + } + break; + case 7: + length = (uint16_t) byte << 8; + break; + case 8: + length |= byte; + break; + default: + this->data_.push_back(byte); + if ((idx - 8) == length) { + switch (this->data_[0]) { + case OK: + case NO_FINGER: + case IMAGE_FAIL: + case IMAGE_MESS: + case FEATURE_FAIL: + case NO_MATCH: + case NOT_FOUND: + case ENROLL_MISMATCH: + case BAD_LOCATION: + case DELETE_FAIL: + case DB_CLEAR_FAIL: + case PASSWORD_FAIL: + case INVALID_IMAGE: + case FLASH_ERR: + break; + case PACKET_RCV_ERR: + ESP_LOGE(TAG, "Reader failed to process request"); + break; + default: + ESP_LOGE(TAG, "Unknown response received from reader: %d", this->data_[0]); + break; + } + return this->data_[0]; + } + break; + } + idx++; + } + ESP_LOGE(TAG, "No response received from reader"); + this->data_[0] = TIMEOUT; + return TIMEOUT; +} + +void FingerprintGrowComponent::dump_config() { + ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:"); + LOG_UPDATE_INTERVAL(this); + LOG_SENSOR(" ", "Fingerprint Count", this->fingerprint_count_sensor_); + LOG_SENSOR(" ", "Status", this->status_sensor_); + LOG_SENSOR(" ", "Capacity", this->capacity_sensor_); + LOG_SENSOR(" ", "Security Level", this->security_level_sensor_); + LOG_SENSOR(" ", "Last Finger ID", this->last_finger_id_sensor_); + LOG_SENSOR(" ", "Last Confidence", this->last_confidence_sensor_); +} + +} // namespace fingerprint_grow +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/fingerprint_grow.h b/esphome/components/fingerprint_grow/fingerprint_grow.h new file mode 100644 index 0000000000..e7d734777a --- /dev/null +++ b/esphome/components/fingerprint_grow/fingerprint_grow.h @@ -0,0 +1,276 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/automation.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/uart/uart.h" + +namespace esphome { +namespace fingerprint_grow { + +static const uint16_t START_CODE = 0xEF01; + +enum GrowPacketType { + COMMAND = 0x01, + DATA = 0x02, + ACK = 0x07, + END_DATA = 0x08, +}; + +enum GrowCommand { + GET_IMAGE = 0x01, + IMAGE_2_TZ = 0x02, + SEARCH = 0x04, + REG_MODEL = 0x05, + STORE = 0x06, + LOAD = 0x07, + UPLOAD = 0x08, + DELETE = 0x0C, + EMPTY = 0x0D, + READ_SYS_PARAM = 0x0F, + SET_PASSWORD = 0x12, + VERIFY_PASSWORD = 0x13, + HI_SPEED_SEARCH = 0x1B, + TEMPLATE_COUNT = 0x1D, + AURA_CONFIG = 0x35, + LED_ON = 0x50, + LED_OFF = 0x51, +}; + +enum GrowResponse { + OK = 0x00, + PACKET_RCV_ERR = 0x01, + NO_FINGER = 0x02, + IMAGE_FAIL = 0x03, + IMAGE_MESS = 0x06, + FEATURE_FAIL = 0x07, + NO_MATCH = 0x08, + NOT_FOUND = 0x09, + ENROLL_MISMATCH = 0x0A, + BAD_LOCATION = 0x0B, + DB_RANGE_FAIL = 0x0C, + UPLOAD_FEATURE_FAIL = 0x0D, + PACKET_RESPONSE_FAIL = 0x0E, + UPLOAD_FAIL = 0x0F, + DELETE_FAIL = 0x10, + DB_CLEAR_FAIL = 0x11, + PASSWORD_FAIL = 0x13, + INVALID_IMAGE = 0x15, + FLASH_ERR = 0x18, + INVALID_REG = 0x1A, + BAD_PACKET = 0xFE, + TIMEOUT = 0xFF, +}; + +enum GrowAuraLEDState { + BREATHING = 0x01, + FLASHING = 0x02, + ALWAYS_ON = 0x03, + ALWAYS_OFF = 0x04, + GRADUAL_ON = 0x05, + GRADUAL_OFF = 0x06, +}; + +enum GrowAuraLEDColor { + RED = 0x01, + BLUE = 0x02, + PURPLE = 0x03, +}; + +class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevice { + public: + void update() override; + void setup() override; + void dump_config() override; + + void set_address(uint32_t address) { + this->address_[0] = (uint8_t)(address >> 24); + this->address_[1] = (uint8_t)(address >> 16); + this->address_[2] = (uint8_t)(address >> 8); + this->address_[3] = (uint8_t)(address & 0xFF); + } + void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } + void set_password(uint32_t password) { this->password_ = password; } + void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; } + void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { + this->fingerprint_count_sensor_ = fingerprint_count_sensor; + } + void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; } + void set_capacity_sensor(sensor::Sensor *capacity_sensor) { this->capacity_sensor_ = capacity_sensor; } + void set_security_level_sensor(sensor::Sensor *security_level_sensor) { + this->security_level_sensor_ = security_level_sensor; + } + void set_last_finger_id_sensor(sensor::Sensor *last_finger_id_sensor) { + this->last_finger_id_sensor_ = last_finger_id_sensor; + } + void set_last_confidence_sensor(sensor::Sensor *last_confidence_sensor) { + this->last_confidence_sensor_ = last_confidence_sensor; + } + void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) { + this->enrolling_binary_sensor_ = enrolling_binary_sensor; + } + void add_on_finger_scan_matched_callback(std::function callback) { + this->finger_scan_matched_callback_.add(std::move(callback)); + } + void add_on_finger_scan_unmatched_callback(std::function callback) { + this->finger_scan_unmatched_callback_.add(std::move(callback)); + } + void add_on_enrollment_scan_callback(std::function callback) { + this->enrollment_scan_callback_.add(std::move(callback)); + } + void add_on_enrollment_done_callback(std::function callback) { + this->enrollment_done_callback_.add(std::move(callback)); + } + + void add_on_enrollment_failed_callback(std::function callback) { + this->enrollment_failed_callback_.add(std::move(callback)); + } + + void enroll_fingerprint(uint16_t finger_id, uint8_t num_buffers); + void finish_enrollment(uint8_t result); + void delete_fingerprint(uint16_t finger_id); + void delete_all_fingerprints(); + + void led_control(bool state); + void aura_led_control(uint8_t state, uint8_t speed, uint8_t color, uint8_t count); + + protected: + void scan_and_match_(); + uint8_t scan_image_(uint8_t buffer); + uint8_t save_fingerprint_(); + bool check_password_(); + bool set_password_(); + bool get_parameters_(); + void get_fingerprint_count_(); + uint8_t send_command_(); + + std::vector data_ = {}; + uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; + uint16_t capacity_ = 64; + uint32_t password_ = 0x0; + uint32_t *new_password_{nullptr}; + GPIOPin *sensing_pin_{nullptr}; + uint8_t enrollment_image_ = 0; + uint16_t enrollment_slot_ = 0; + uint8_t enrollment_buffers_ = 5; + bool waiting_removal_ = false; + uint32_t last_aura_led_control_ = 0; + uint16_t last_aura_led_duration_ = 0; + sensor::Sensor *fingerprint_count_sensor_{nullptr}; + sensor::Sensor *status_sensor_{nullptr}; + sensor::Sensor *capacity_sensor_{nullptr}; + sensor::Sensor *security_level_sensor_{nullptr}; + sensor::Sensor *last_finger_id_sensor_{nullptr}; + sensor::Sensor *last_confidence_sensor_{nullptr}; + binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr}; + CallbackManager finger_scan_matched_callback_; + CallbackManager finger_scan_unmatched_callback_; + CallbackManager enrollment_scan_callback_; + CallbackManager enrollment_done_callback_; + CallbackManager enrollment_failed_callback_; +}; + +class FingerScanMatchedTrigger : public Trigger { + public: + explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_matched_callback( + [this](uint16_t finger_id, uint16_t confidence) { this->trigger(finger_id, confidence); }); + } +}; + +class FingerScanUnmatchedTrigger : public Trigger<> { + public: + explicit FingerScanUnmatchedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_finger_scan_unmatched_callback([this]() { this->trigger(); }); + } +}; + +class EnrollmentScanTrigger : public Trigger { + public: + explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_scan_callback( + [this](uint8_t scan_num, uint16_t finger_id) { this->trigger(scan_num, finger_id); }); + } +}; + +class EnrollmentDoneTrigger : public Trigger { + public: + explicit EnrollmentDoneTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_done_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); + } +}; + +class EnrollmentFailedTrigger : public Trigger { + public: + explicit EnrollmentFailedTrigger(FingerprintGrowComponent *parent) { + parent->add_on_enrollment_failed_callback([this](uint16_t finger_id) { this->trigger(finger_id); }); + } +}; + +template class EnrollmentAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, finger_id) + TEMPLATABLE_VALUE(uint8_t, num_scans) + + void play(Ts... x) override { + auto finger_id = this->finger_id_.value(x...); + auto num_scans = this->num_scans_.value(x...); + if (num_scans) { + this->parent_->enroll_fingerprint(finger_id, num_scans); + } else { + this->parent_->enroll_fingerprint(finger_id, 2); + } + } +}; + +template +class CancelEnrollmentAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->finish_enrollment(1); } +}; + +template class DeleteAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint16_t, finger_id) + + void play(Ts... x) override { + auto finger_id = this->finger_id_.value(x...); + this->parent_->delete_fingerprint(finger_id); + } +}; + +template class DeleteAllAction : public Action, public Parented { + public: + void play(Ts... x) override { this->parent_->delete_all_fingerprints(); } +}; + +template class LEDControlAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(bool, state) + + void play(Ts... x) override { + auto state = this->state_.value(x...); + this->parent_->led_control(state); + } +}; + +template class AuraLEDControlAction : public Action, public Parented { + public: + TEMPLATABLE_VALUE(uint8_t, state) + TEMPLATABLE_VALUE(uint8_t, speed) + TEMPLATABLE_VALUE(uint8_t, color) + TEMPLATABLE_VALUE(uint8_t, count) + + void play(Ts... x) override { + auto state = this->state_.value(x...); + auto speed = this->speed_.value(x...); + auto color = this->color_.value(x...); + auto count = this->count_.value(x...); + + this->parent_->aura_led_control(state, speed, color, count); + } +}; + +} // namespace fingerprint_grow +} // namespace esphome diff --git a/esphome/components/fingerprint_grow/sensor.py b/esphome/components/fingerprint_grow/sensor.py new file mode 100644 index 0000000000..c76c898727 --- /dev/null +++ b/esphome/components/fingerprint_grow/sensor.py @@ -0,0 +1,64 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_CAPACITY, + CONF_FINGERPRINT_COUNT, + CONF_LAST_CONFIDENCE, + CONF_LAST_FINGER_ID, + CONF_SECURITY_LEVEL, + CONF_STATUS, + DEVICE_CLASS_EMPTY, + ICON_ACCOUNT, + ICON_ACCOUNT_CHECK, + ICON_DATABASE, + ICON_EMPTY, + ICON_FINGERPRINT, + ICON_SECURITY, + UNIT_EMPTY, +) +from . import CONF_FINGERPRINT_GROW_ID, FingerprintGrowComponent + +DEPENDENCIES = ["fingerprint_grow"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_FINGERPRINT_GROW_ID): cv.use_id(FingerprintGrowComponent), + cv.Optional(CONF_FINGERPRINT_COUNT): sensor.sensor_schema( + UNIT_EMPTY, ICON_FINGERPRINT, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_STATUS): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_CAPACITY): sensor.sensor_schema( + UNIT_EMPTY, ICON_DATABASE, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_SECURITY_LEVEL): sensor.sensor_schema( + UNIT_EMPTY, ICON_SECURITY, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_LAST_FINGER_ID): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCOUNT, 0, DEVICE_CLASS_EMPTY + ), + cv.Optional(CONF_LAST_CONFIDENCE): sensor.sensor_schema( + UNIT_EMPTY, ICON_ACCOUNT_CHECK, 0, DEVICE_CLASS_EMPTY + ), + } +) + + +def to_code(config): + hub = yield cg.get_variable(config[CONF_FINGERPRINT_GROW_ID]) + + for key in [ + CONF_FINGERPRINT_COUNT, + CONF_STATUS, + CONF_CAPACITY, + CONF_SECURITY_LEVEL, + CONF_LAST_FINGER_ID, + CONF_LAST_CONFIDENCE, + ]: + if key not in config: + continue + conf = config[key] + sens = yield sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) diff --git a/esphome/const.py b/esphome/const.py index 618c9fd922..eef9252a5c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -97,6 +97,7 @@ CONF_BUSY_PIN = "busy_pin" CONF_CALIBRATE_LINEAR = "calibrate_linear" CONF_CALIBRATION = "calibration" CONF_CAPACITANCE = "capacitance" +CONF_CAPACITY = "capacity" CONF_CARRIER_DUTY_PERCENT = "carrier_duty_percent" CONF_CARRIER_FREQUENCY = "carrier_frequency" CONF_CERTIFICATE = "certificate" @@ -115,6 +116,7 @@ CONF_CO2 = "co2" CONF_CODE = "code" CONF_COLD_WHITE = "cold_white" CONF_COLD_WHITE_COLOR_TEMPERATURE = "cold_white_color_temperature" +CONF_COLOR = "color" CONF_COLOR_CORRECT = "color_correct" CONF_COLOR_TEMPERATURE = "color_temperature" CONF_COLORS = "colors" @@ -130,6 +132,7 @@ CONF_CONDUCTIVITY = "conductivity" CONF_CONTRAST = "contrast" CONF_COOL_ACTION = "cool_action" CONF_COOL_MODE = "cool_mode" +CONF_COUNT = "count" CONF_COUNT_MODE = "count_mode" CONF_COURSE = "course" CONF_CRON = "cron" @@ -208,6 +211,8 @@ CONF_FILE = "file" CONF_FILTER = "filter" CONF_FILTER_OUT = "filter_out" CONF_FILTERS = "filters" +CONF_FINGER_ID = "finger_id" +CONF_FINGERPRINT_COUNT = "fingerprint_count" CONF_FLASH_LENGTH = "flash_length" CONF_FOR = "for" CONF_FORCE_UPDATE = "force_update" @@ -275,6 +280,8 @@ CONF_KEEP_ON_TIME = "keep_on_time" CONF_KEEPALIVE = "keepalive" CONF_KEY = "key" CONF_LAMBDA = "lambda" +CONF_LAST_CONFIDENCE = "last_confidence" +CONF_LAST_FINGER_ID = "last_finger_id" CONF_LATITUDE = "latitude" CONF_LENGTH = "length" CONF_LEVEL = "level" @@ -335,11 +342,13 @@ CONF_NAME = "name" CONF_NBITS = "nbits" CONF_NEC = "nec" CONF_NETWORKS = "networks" +CONF_NEW_PASSWORD = "new_password" CONF_NOISE_LEVEL = "noise_level" CONF_NUM_ATTEMPTS = "num_attempts" CONF_NUM_CHANNELS = "num_channels" CONF_NUM_CHIPS = "num_chips" CONF_NUM_LEDS = "num_leds" +CONF_NUM_SCANS = "num_scans" CONF_NUMBER = "number" CONF_OFF_MODE = "off_mode" CONF_OFFSET = "offset" @@ -350,6 +359,11 @@ CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" CONF_ON_DOUBLE_CLICK = "on_double_click" +CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" +CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" +CONF_ON_ENROLLMENT_SCAN = "on_enrollment_scan" +CONF_ON_FINGER_SCAN_MATCHED = "on_finger_scan_matched" +CONF_ON_FINGER_SCAN_UNMATCHED = "on_finger_scan_unmatched" CONF_ON_JSON_MESSAGE = "on_json_message" CONF_ON_LOOP = "on_loop" CONF_ON_MESSAGE = "on_message" @@ -472,10 +486,12 @@ CONF_SDA = "sda" CONF_SDO_PIN = "sdo_pin" CONF_SECOND = "second" CONF_SECONDS = "seconds" +CONF_SECURITY_LEVEL = "security_level" CONF_SEGMENTS = "segments" CONF_SEL_PIN = "sel_pin" CONF_SEND_EVERY = "send_every" CONF_SEND_FIRST_AT = "send_first_at" +CONF_SENSING_PIN = "sensing_pin" CONF_SENSOR = "sensor" CONF_SENSOR_ID = "sensor_id" CONF_SENSORS = "sensors" @@ -505,6 +521,7 @@ CONF_SSL_FINGERPRINTS = "ssl_fingerprints" CONF_STATE = "state" CONF_STATE_TOPIC = "state_topic" CONF_STATIC_IP = "static_ip" +CONF_STATUS = "status" CONF_STEP_MODE = "step_mode" CONF_STEP_PIN = "step_pin" CONF_STOP = "stop" @@ -599,6 +616,8 @@ ICON_ACCELERATION = "mdi:axis-arrow" ICON_ACCELERATION_X = "mdi:axis-x-arrow" ICON_ACCELERATION_Y = "mdi:axis-y-arrow" ICON_ACCELERATION_Z = "mdi:axis-z-arrow" +ICON_ACCOUNT = "mdi:account" +ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" @@ -608,7 +627,9 @@ ICON_CHECK_CIRCLE_OUTLINE = "mdi:check-circle-outline" ICON_CHEMICAL_WEAPON = "mdi:chemical-weapon" ICON_COUNTER = "mdi:counter" ICON_CURRENT_AC = "mdi:current-ac" +ICON_DATABASE = "mdi:database" ICON_EMPTY = "" +ICON_FINGERPRINT = "mdi:fingerprint" ICON_FLASH = "mdi:flash" ICON_FLASK = "mdi:flask" ICON_FLASK_OUTLINE = "mdi:flask-outline" @@ -616,6 +637,7 @@ ICON_FLOWER = "mdi:flower" ICON_GAS_CYLINDER = "mdi:gas-cylinder" ICON_GAUGE = "mdi:gauge" ICON_GRAIN = "mdi:grain" +ICON_KEY_PLUS = "mdi:key-plus" ICON_LIGHTBULB = "mdi:lightbulb" ICON_MAGNET = "mdi:magnet" ICON_MOLECULE_CO2 = "mdi:molecule-co2" @@ -632,6 +654,7 @@ ICON_RULER = "mdi:ruler" ICON_SCALE = "mdi:scale" ICON_SCALE_BATHROOM = "mdi:scale-bathroom" ICON_SCREEN_ROTATION = "mdi:screen-rotation" +ICON_SECURITY = "mdi:security" ICON_SIGN_DIRECTION = "mdi:sign-direction" ICON_SIGNAL = "mdi:signal-distance-variant" ICON_SIGNAL_DISTANCE_VARIANT = "mdi:signal" diff --git a/tests/test3.yaml b/tests/test3.yaml index ee95e9d35f..165a36c015 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -177,6 +177,26 @@ api: kp: 1.0 kd: 1.0 ki: 1.0 + - service: fingerprint_grow_enroll + variables: + finger_id: int + num_scans: int + then: + - fingerprint_grow.enroll: + finger_id: !lambda 'return finger_id;' + num_scans: !lambda 'return num_scans;' + - service: fingerprint_grow_cancel_enroll + then: + - fingerprint_grow.cancel_enroll: + - service: fingerprint_grow_delete + variables: + finger_id: int + then: + - fingerprint_grow.delete: + finger_id: !lambda 'return finger_id;' + - service: fingerprint_grow_delete_all + then: + - fingerprint_grow.delete_all: wifi: ssid: 'MySSID' @@ -424,6 +444,19 @@ sensor: id: ph_ezo address: 99 unit_of_measurement: 'pH' + - platform: fingerprint_grow + fingerprint_count: + name: "Fingerprint Count" + status: + name: "Fingerprint Status" + capacity: + name: "Fingerprint Capacity" + security_level: + name: "Fingerprint Security Level" + last_finger_id: + name: "Fingerprint Last Finger ID" + last_confidence: + name: "Fingerprint Last Confidence" time: - platform: homeassistant @@ -486,6 +519,8 @@ binary_sensor: - platform: ttp229_bsf channel: 1 name: TTP229 BSF Test + - platform: fingerprint_grow + name: "Fingerprint Enrolling" - platform: custom lambda: |- auto s = new CustomBinarySensor(); @@ -919,3 +954,33 @@ display: http_request: useragent: esphome/device timeout: 10s + +fingerprint_grow: + sensing_pin: 4 + password: 0x12FE37DC + new_password: 0xA65B9840 + on_finger_scan_matched: + - homeassistant.event: + event: esphome.${devicename}_fingerprint_grow_finger_scan_matched + data: + finger_id: !lambda 'return finger_id;' + confidence: !lambda 'return confidence;' + on_finger_scan_unmatched: + - homeassistant.event: + event: esphome.${devicename}_fingerprint_grow_finger_scan_unmatched + on_enrollment_scan: + - homeassistant.event: + event: esphome.${devicename}_fingerprint_grow_enrollment_scan + data: + finger_id: !lambda 'return finger_id;' + scan_num: !lambda 'return scan_num;' + on_enrollment_done: + - homeassistant.event: + event: esphome.${devicename}_fingerprint_grow_node_enrollment_done + data: + finger_id: !lambda 'return finger_id;' + on_enrollment_failed: + - homeassistant.event: + event: esphome.${devicename}_fingerprint_grow_enrollment_failed + data: + finger_id: !lambda 'return finger_id;' From a7c648b60b27baa475f548148210ad3f27762a54 Mon Sep 17 00:00:00 2001 From: Guillermo Ruffino Date: Wed, 28 Apr 2021 19:50:24 -0300 Subject: [PATCH 38/62] RC522 fixes (#1479) --- esphome/components/rc522/rc522.cpp | 792 +++++++-------------- esphome/components/rc522/rc522.h | 109 ++- esphome/components/rc522_i2c/rc522_i2c.cpp | 30 - esphome/components/rc522_spi/rc522_spi.cpp | 19 +- 4 files changed, 330 insertions(+), 620 deletions(-) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 8182c1e8c9..ced58760ad 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -7,22 +7,38 @@ namespace esphome { namespace rc522 { +static const uint8_t WAIT_I_RQ = 0x30; // RxIRq and IdleIRq + static const char *TAG = "rc522"; static const uint8_t RESET_COUNT = 5; -void format_uid(char *buf, const uint8_t *uid, uint8_t uid_length) { +std::string format_buffer(uint8_t *b, uint8_t len) { + char buf[32]; int offset = 0; - for (uint8_t i = 0; i < uid_length; i++) { + for (uint8_t i = 0; i < len; i++) { const char *format = "%02X"; - if (i + 1 < uid_length) + if (i + 1 < len) + format = "%02X-"; + offset += sprintf(buf + offset, format, b[i]); + } + return std::string(buf); +} + +std::string format_uid(std::vector &uid) { + char buf[32]; + int offset = 0; + for (uint8_t i = 0; i < uid.size(); i++) { + const char *format = "%02X"; + if (i + 1 < uid.size()) format = "%02X-"; offset += sprintf(buf + offset, format, uid[i]); } + return std::string(buf); } void RC522::setup() { - initialize_pending_ = true; + state_ = STATE_SETUP; // Pull device out of power down / reset state. // First set the resetPowerDownPin as digital input, to check the MFRC522 power down mode. @@ -48,7 +64,7 @@ void RC522::setup() { } void RC522::initialize_() { - // Per originall code, wait 50 ms + // Per original code, wait 50 ms if (millis() - reset_timeout_ < 50) return; @@ -75,9 +91,8 @@ void RC522::initialize_() { pcd_write_register(TX_ASK_REG, 0x40); pcd_write_register(MODE_REG, 0x3D); // Default 0x3F. Set the preset value for the CRC coprocessor for the CalcCRC // command to 0x6363 (ISO 14443-3 part 6.2.4) - pcd_antenna_on_(); // Enable the antenna driver pins TX1 and TX2 (they were disabled by the reset) - initialize_pending_ = false; + state_ = STATE_INIT; } void RC522::dump_config() { @@ -99,76 +114,163 @@ void RC522::dump_config() { } } +void RC522::update() { + if (state_ == STATE_INIT) { + pcd_antenna_on_(); + pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared. + buffer_[0] = PICC_CMD_REQA; + pcd_transceive_data_(1); + state_ = STATE_PICC_REQUEST_A; + } else { + ESP_LOGW(TAG, "Communication takes longer than update interval: %d", state_); + } +} + void RC522::loop() { // First check reset is needed if (reset_count_ > 0) { pcd_reset_(); return; } - if (initialize_pending_) { + if (state_ == STATE_SETUP) { initialize_(); return; } - if (millis() - update_wait_ < this->update_interval_) - return; + StatusCode status = STATUS_ERROR; // For lint passing. TODO: refactor this + if (awaiting_comm_) { + if (state_ == STATE_SELECT_SERIAL_DONE) + status = await_crc_(); + else + status = await_transceive_(); - auto status = picc_is_new_card_present_(); - - static StatusCode LAST_STATUS = StatusCode::STATUS_OK; - - if (status != LAST_STATUS) { - ESP_LOGD(TAG, "Status is now: %d", status); - LAST_STATUS = status; - } - - if (status == STATUS_ERROR) // No card - { - // ESP_LOGE(TAG, "Error"); - // mark_failed(); - return; - } - - if (status != STATUS_OK) // We can receive STATUS_TIMEOUT when no card, or unexpected status. - return; - - // Try process card - if (!picc_read_card_serial_()) { - ESP_LOGW(TAG, "Requesting tag read failed!"); - return; - }; - - if (uid_.size < 4) { - return; - ESP_LOGW(TAG, "Read serial size: %d", uid_.size); - } - - update_wait_ = millis(); - - bool report = true; - // 1. Go through all triggers - for (auto *trigger : this->triggers_) - trigger->process(uid_.uiduint8_t, uid_.size); - - // 2. Find a binary sensor - for (auto *tag : this->binary_sensors_) { - if (tag->process(uid_.uiduint8_t, uid_.size)) { - // 2.1 if found, do not dump - report = false; + if (status == STATUS_WAITING) { + return; } + awaiting_comm_ = false; + ESP_LOGV(TAG, "finished communication status: %d, state: %d", status, state_); } - if (report) { - char buf[32]; - format_uid(buf, uid_.uiduint8_t, uid_.size); - ESP_LOGD(TAG, "Found new tag '%s'", buf); - } -} + switch (state_) { + case STATE_PICC_REQUEST_A: { + if (status == STATUS_TIMEOUT) { // no tag present + for (auto *obj : this->binary_sensors_) + obj->on_scan_end(); // reset the binary sensors + ESP_LOGV(TAG, "CMD_REQA -> TIMEOUT (no tag present) %d", status); + state_ = STATE_DONE; + } else if (status != STATUS_OK) { + ESP_LOGW(TAG, "CMD_REQA -> Not OK %d", status); + state_ = STATE_DONE; + } else if (back_length_ != 2) { // || *valid_bits_ != 0) { // ATQA must be exactly 16 bits. + ESP_LOGW(TAG, "CMD_REQA -> OK, but unexpacted back_length_ of %d", back_length_); + state_ = STATE_DONE; + } else { + state_ = STATE_READ_SERIAL; + } + if (state_ == STATE_DONE) { + // Don't wait another loop cycle + pcd_antenna_off_(); + } + break; + } + case STATE_READ_SERIAL: { + ESP_LOGV(TAG, "STATE_READ_SERIAL (%d)", status); + switch (uid_idx_) { + case 0: + buffer_[0] = PICC_CMD_SEL_CL1; + break; + case 3: + buffer_[0] = PICC_CMD_SEL_CL2; + break; + case 6: + buffer_[0] = PICC_CMD_SEL_CL3; + break; + default: + ESP_LOGE(TAG, "uid_idx_ invalid, uid_idx_ = %d", uid_idx_); + state_ = STATE_DONE; + } + buffer_[1] = 32; + pcd_transceive_data_(2); + state_ = STATE_SELECT_SERIAL; + break; + } + case STATE_SELECT_SERIAL: { + buffer_[1] = 0x70; // select + // todo: set CRC + buffer_[6] = buffer_[2] ^ buffer_[3] ^ buffer_[4] ^ buffer_[5]; + pcd_calculate_crc_(buffer_, 7); + state_ = STATE_SELECT_SERIAL_DONE; + break; + } + case STATE_SELECT_SERIAL_DONE: { + send_len_ = 6; + pcd_transceive_data_(9); + state_ = STATE_READ_SERIAL_DONE; + break; + } + case STATE_READ_SERIAL_DONE: { + if (status != STATUS_OK || back_length_ != 3) { + if (status == STATUS_TIMEOUT) + ESP_LOGV(TAG, "STATE_READ_SERIAL_DONE -> TIMEOUT (no tag present) %d", status); + else + ESP_LOGW(TAG, "Unexpected response. Read status is %d. Read bytes: %d (%s)", status, back_length_, + format_buffer(buffer_, 9).c_str()); -void RC522::update() { - for (auto *obj : this->binary_sensors_) - obj->on_scan_end(); -} + state_ = STATE_DONE; + uid_idx_ = 0; + + pcd_antenna_off_(); + return; + } + + // copy the uid + bool cascade = buffer_[2] == PICC_CMD_CT; // todo: should be determined based on select response (buffer[6]) + for (uint8_t i = 2 + cascade; i < 6; i++) + uid_buffer_[uid_idx_++] = buffer_[i]; + ESP_LOGVV(TAG, "copied uid to idx %d last byte is 0x%x, cascade is %d", uid_idx_, uid_buffer_[uid_idx_ - 1], + cascade); + + if (cascade) { // there is more bytes in the UID + state_ = STATE_READ_SERIAL; + return; + } + + std::vector rfid_uid(std::begin(uid_buffer_), std::begin(uid_buffer_) + uid_idx_); + uid_idx_ = 0; + // ESP_LOGD(TAG, "Processing '%s'", format_uid(rfid_uid).c_str()); + pcd_antenna_off_(); + state_ = STATE_INIT; // scan again on next update + bool report = true; + + for (auto *tag : this->binary_sensors_) { + if (tag->process(rfid_uid)) { + report = false; + } + } + + if (this->current_uid_ == rfid_uid) { + return; + } + + this->current_uid_ = rfid_uid; + + for (auto *trigger : this->triggers_) + trigger->process(rfid_uid); + + if (report) { + ESP_LOGD(TAG, "Found new tag '%s'", format_uid(rfid_uid).c_str()); + } + break; + } + case STATE_DONE: { + this->current_uid_ = {}; + state_ = STATE_INIT; + break; + } + default: + break; + } +} // namespace rc522 /** * Performs a soft reset on the MFRC522 chip and waits for it to be ready again. @@ -176,14 +278,14 @@ void RC522::update() { void RC522::pcd_reset_() { // The datasheet does not mention how long the SoftRest command takes to complete. // But the MFRC522 might have been in soft power-down mode (triggered by bit 4 of CommandReg) - // Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs. Let - // us be generous: 50ms. + // Section 8.8.2 in the datasheet says the oscillator start-up time is the start up time of the crystal + 37,74μs. + // Let us be generous: 50ms. if (millis() - reset_timeout_ < 50) return; if (reset_count_ == RESET_COUNT) { - ESP_LOGV(TAG, "Soft reset..."); + ESP_LOGI(TAG, "Soft reset..."); // Issue the SoftReset command. pcd_write_register(COMMAND_REG, PCD_SOFT_RESET); } @@ -199,6 +301,7 @@ void RC522::pcd_reset_() { if (--reset_count_ == 0) { ESP_LOGE(TAG, "Unable to reset RC522."); + this->error_code_ = RESET_FAILED; mark_failed(); } } @@ -215,49 +318,13 @@ void RC522::pcd_antenna_on_() { } /** - * Transmits a REQuest command, Type A. Invites PICCs in state IDLE to go to READY and prepare for anticollision or - * selection. 7 bit frame. Beware: When two PICCs are in the field at the same time I often get STATUS_TIMEOUT - - * probably due do bad antenna design. - * - * @return STATUS_OK on success, STATUS_??? otherwise. + * Turns the antenna off by disabling pins TX1 and TX2. */ -RC522::StatusCode RC522::picc_request_a_( - uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in - uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. -) { - return picc_reqa_or_wupa_(PICC_CMD_REQA, buffer_atqa, buffer_size); -} - -/** - * Transmits REQA or WUPA commands. - * Beware: When two PICCs are in the field at the same time I often get STATUS_TIMEOUT - probably due do bad antenna - * design. - * - * @return STATUS_OK on success, STATUS_??? otherwise. - */ -RC522::StatusCode RC522::picc_reqa_or_wupa_( - uint8_t command, ///< The command to send - PICC_CMD_REQA or PICC_CMD_WUPA - uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in - uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. -) { - uint8_t valid_bits; - RC522::StatusCode status; - - if (buffer_atqa == nullptr || *buffer_size < 2) { // The ATQA response is 2 uint8_ts long. - return STATUS_NO_ROOM; +void RC522::pcd_antenna_off_() { + uint8_t value = pcd_read_register(TX_CONTROL_REG); + if ((value & 0x03) != 0x00) { + pcd_write_register(TX_CONTROL_REG, value & ~0x03); } - pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared. - valid_bits = 7; // For REQA and WUPA we need the short frame format - transmit only 7 bits of the last (and only) - // uint8_t. TxLastBits = BitFramingReg[2..0] - status = pcd_transceive_data_(&command, 1, buffer_atqa, buffer_size, &valid_bits); - if (status != STATUS_OK) - return status; - if (*buffer_size != 2 || valid_bits != 0) { // ATQA must be exactly 16 bits. - ESP_LOGVV(TAG, "picc_reqa_or_wupa_() -> STATUS_ERROR"); - return STATUS_ERROR; - } - - return STATUS_OK; } /** @@ -280,140 +347,86 @@ void RC522::pcd_clear_register_bit_mask_(PcdRegister reg, ///< The register to pcd_write_register(reg, tmp & (~mask)); // clear bit mask } -/** - * Executes the Transceive command. - * CRC validation can only be done if backData and backLen are specified. - * - * @return STATUS_OK on success, STATUS_??? otherwise. - */ -RC522::StatusCode RC522::pcd_transceive_data_( - uint8_t *send_data, ///< Pointer to the data to transfer to the FIFO. - uint8_t send_len, ///< Number of uint8_ts to transfer to the FIFO. - uint8_t *back_data, ///< nullptr or pointer to buffer if data should be read back after executing the command. - uint8_t *back_len, ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned. - uint8_t - *valid_bits, ///< In/Out: The number of valid bits in the last uint8_t. 0 for 8 valid bits. Default nullptr. - uint8_t rx_align, ///< In: Defines the bit position in backData[0] for the first bit received. Default 0. - bool check_crc ///< In: True => The last two uint8_ts of the response is assumed to be a CRC_A that must be - ///< validated. -) { - uint8_t wait_i_rq = 0x30; // RxIRq and IdleIRq - auto ret = pcd_communicate_with_picc_(PCD_TRANSCEIVE, wait_i_rq, send_data, send_len, back_data, back_len, valid_bits, - rx_align, check_crc); - - if (ret == STATUS_OK && *back_len == 5) - ESP_LOGVV(TAG, "pcd_transceive_data_(..., %d, ) -> %d [%x, %x, %x, %x, %x]", send_len, ret, back_data[0], - back_data[1], back_data[2], back_data[3], back_data[4]); - else - ESP_LOGVV(TAG, "pcd_transceive_data_(..., %d, ... ) -> %d", send_len, ret); - return ret; -} - /** * Transfers data to the MFRC522 FIFO, executes a command, waits for completion and transfers data back from the FIFO. * CRC validation can only be done if backData and backLen are specified. * * @return STATUS_OK on success, STATUS_??? otherwise. */ -RC522::StatusCode RC522::pcd_communicate_with_picc_( - uint8_t command, ///< The command to execute. One of the PCD_Command enums. - uint8_t wait_i_rq, ///< The bits in the ComIrqReg register that signals successful completion of the command. - uint8_t *send_data, ///< Pointer to the data to transfer to the FIFO. - uint8_t send_len, ///< Number of uint8_ts to transfer to the FIFO. - uint8_t *back_data, ///< nullptr or pointer to buffer if data should be read back after executing the command. - uint8_t *back_len, ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned. - uint8_t *valid_bits, ///< In/Out: The number of valid bits in the last uint8_t. 0 for 8 valid bits. - uint8_t rx_align, ///< In: Defines the bit position in backData[0] for the first bit received. Default 0. - bool check_crc ///< In: True => The last two uint8_ts of the response is assumed to be a CRC_A that must be - ///< validated. -) { - ESP_LOGVV(TAG, "pcd_communicate_with_picc_(%d, %d,... %d)", command, wait_i_rq, check_crc); - +void RC522::pcd_transceive_data_(uint8_t send_len) { + ESP_LOGV(TAG, "PCD TRANSCEIVE: RX: %s", format_buffer(buffer_, send_len).c_str()); + delayMicroseconds(1000); // we need 1 ms delay between antenna on and those communication commands + send_len_ = send_len; // Prepare values for BitFramingReg - uint8_t tx_last_bits = valid_bits ? *valid_bits : 0; - uint8_t bit_framing = - (rx_align << 4) + tx_last_bits; // RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0] + // For REQA and WUPA we need the short frame format - transmit only 7 bits of the last (and only) + // uint8_t. TxLastBits = BitFramingReg[2..0] + uint8_t bit_framing = (buffer_[0] == PICC_CMD_REQA) ? 7 : 0; - pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. - pcd_write_register(COM_IRQ_REG, 0x7F); // Clear all seven interrupt request bits - pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization - pcd_write_register(FIFO_DATA_REG, send_len, send_data); // Write sendData to the FIFO - pcd_write_register(BIT_FRAMING_REG, bit_framing); // Bit adjustments - pcd_write_register(COMMAND_REG, command); // Execute the command - if (command == PCD_TRANSCEIVE) { - pcd_set_register_bit_mask_(BIT_FRAMING_REG, 0x80); // StartSend=1, transmission of data starts - } + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. + pcd_write_register(COM_IRQ_REG, 0x7F); // Clear all seven interrupt request bits + pcd_write_register(FIFO_LEVEL_REG, 0x80); // FlushBuffer = 1, FIFO initialization + pcd_write_register(FIFO_DATA_REG, send_len_, buffer_); // Write sendData to the FIFO + pcd_write_register(BIT_FRAMING_REG, bit_framing); // Bit adjustments + pcd_write_register(COMMAND_REG, PCD_TRANSCEIVE); // Execute the command + pcd_set_register_bit_mask_(BIT_FRAMING_REG, 0x80); // StartSend=1, transmission of data starts + awaiting_comm_ = true; + awaiting_comm_time_ = millis(); +} - // Wait for the command to complete. - // In PCD_Init() we set the TAuto flag in TModeReg. This means the timer automatically starts when the PCD stops - // transmitting. Each iteration of the do-while-loop takes 17.86μs. - // TODO check/modify for other architectures than Arduino Uno 16bit - uint16_t i; - for (i = 2000; i > 0; i--) { - uint8_t n = pcd_read_register( - COM_IRQ_REG); // ComIrqReg[7..0] bits are: Set1 TxIRq RxIRq IdleIRq HiAlertIRq LoAlertIRq ErrIRq TimerIRq - if (n & wait_i_rq) { // One of the interrupts that signal success has been set. - break; - } - if (n & 0x01) { // Timer interrupt - nothing received in 25ms - return STATUS_TIMEOUT; - } - } - // 35.7ms and nothing happend. Communication with the MFRC522 might be down. - if (i == 0) { +RC522::StatusCode RC522::await_transceive_() { + if (millis() - awaiting_comm_time_ < 2) // wait at least 2 ms + return STATUS_WAITING; + uint8_t n = pcd_read_register( + COM_IRQ_REG); // ComIrqReg[7..0] bits are: Set1 TxIRq RxIRq IdleIRq HiAlertIRq LoAlertIRq ErrIRq TimerIRq + if (n & 0x01) { // Timer interrupt - nothing received in 25ms + back_length_ = 0; + error_counter_ = 0; // reset the error counter return STATUS_TIMEOUT; } + if (!(n & WAIT_I_RQ)) { // None of the interrupts that signal success has been set. + // Wait for the command to complete. + if (millis() - awaiting_comm_time_ < 40) + return STATUS_WAITING; + back_length_ = 0; + ESP_LOGW(TAG, "Communication with the MFRC522 might be down, reset in %d", + 10 - error_counter_); // todo: trigger reset? + if (error_counter_++ > 10) + setup(); + return STATUS_TIMEOUT; + } // Stop now if any errors except collisions were detected. uint8_t error_reg_value = pcd_read_register( ERROR_REG); // ErrorReg[7..0] bits are: WrErr TempErr reserved BufferOvfl CollErr CRCErr ParityErr ProtocolErr if (error_reg_value & 0x13) { // BufferOvfl ParityErr ProtocolErr return STATUS_ERROR; } + error_counter_ = 0; // reset the error counter - uint8_t valid_bits_local = 0; - - // If the caller wants data back, get it from the MFRC522. - if (back_data && back_len) { - uint8_t n = pcd_read_register(FIFO_LEVEL_REG); // Number of uint8_ts in the FIFO - if (n > *back_len) { - return STATUS_NO_ROOM; - } - *back_len = n; // Number of uint8_ts returned - pcd_read_register(FIFO_DATA_REG, n, back_data, rx_align); // Get received data from FIFO - valid_bits_local = - pcd_read_register(CONTROL_REG) & 0x07; // RxLastBits[2:0] indicates the number of valid bits in the last - // received uint8_t. If this value is 000b, the whole uint8_t is valid. - if (valid_bits) { - *valid_bits = valid_bits_local; - } - } + n = pcd_read_register(FIFO_LEVEL_REG); // Number of uint8_ts in the FIFO + if (n > sizeof(buffer_)) + return STATUS_NO_ROOM; + if (n > sizeof(buffer_) - send_len_) + send_len_ = sizeof(buffer_) - n; // simply overwrite the sent values + back_length_ = n; // Number of uint8_ts returned + pcd_read_register(FIFO_DATA_REG, n, buffer_ + send_len_, rx_align_); // Get received data from FIFO + uint8_t valid_bits_local = + pcd_read_register(CONTROL_REG) & 0x07; // RxLastBits[2:0] indicates the number of valid bits in the last + // received uint8_t. If this value is 000b, the whole uint8_t is valid. // Tell about collisions if (error_reg_value & 0x08) { // CollErr + ESP_LOGW(TAG, "collision error, received %d bytes + %d bits (but anticollision not implemented)", + back_length_ - (valid_bits_local > 0), valid_bits_local); return STATUS_COLLISION; } - - // Perform CRC_A validation if requested. - if (back_data && back_len && check_crc) { - // In this case a MIFARE Classic NAK is not OK. - if (*back_len == 1 && valid_bits_local == 4) { - return STATUS_MIFARE_NACK; - } - // We need at least the CRC_A value and all 8 bits of the last uint8_t must be received. - if (*back_len < 2 || valid_bits_local != 0) { - return STATUS_CRC_WRONG; - } - // Verify CRC_A - do our own calculation and store the control in controlBuffer. - uint8_t control_buffer[2]; - RC522::StatusCode status = pcd_calculate_crc_(&back_data[0], *back_len - 2, &control_buffer[0]); - if (status != STATUS_OK) { - return status; - } - if ((back_data[*back_len - 2] != control_buffer[0]) || (back_data[*back_len - 1] != control_buffer[1])) { - return STATUS_CRC_WRONG; - } + // Tell about collisions + if (valid_bits_local) { + ESP_LOGW(TAG, "only %d valid bits received, tag distance to high? Error code is 0x%x", valid_bits_local, + error_reg_value); // TODO: is this always due to collissions? + return STATUS_ERROR; } + ESP_LOGV(TAG, "received %d bytes: %s", back_length_, format_buffer(buffer_ + send_len_, back_length_).c_str()); return STATUS_OK; } @@ -424,10 +437,8 @@ RC522::StatusCode RC522::pcd_communicate_with_picc_( * @return STATUS_OK on success, STATUS_??? otherwise. */ -RC522::StatusCode RC522::pcd_calculate_crc_( - uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. - uint8_t length, ///< In: The number of uint8_ts to transfer. - uint8_t *result ///< Out: Pointer to result buffer. Result is written to result[0..1], low uint8_t first. +void RC522::pcd_calculate_crc_(uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. + uint8_t length ///< In: The number of uint8_ts to transfer. ) { ESP_LOGVV(TAG, "pcd_calculate_crc_(..., %d, ...)", length); pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop any active command. @@ -436,323 +447,50 @@ RC522::StatusCode RC522::pcd_calculate_crc_( pcd_write_register(FIFO_DATA_REG, length, data); // Write data to the FIFO pcd_write_register(COMMAND_REG, PCD_CALC_CRC); // Start the calculation - // Wait for the CRC calculation to complete. Each iteration of the while-loop takes 17.73μs. - // TODO check/modify for other architectures than Arduino Uno 16bit + awaiting_comm_ = true; + awaiting_comm_time_ = millis(); +} - // Wait for the CRC calculation to complete. Each iteration of the while-loop takes 17.73us. - for (uint16_t i = 5000; i > 0; i--) { - // DivIrqReg[7..0] bits are: Set2 reserved reserved MfinActIRq reserved CRCIRq reserved reserved - uint8_t n = pcd_read_register(DIV_IRQ_REG); - if (n & 0x04) { // CRCIRq bit set - calculation done - pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop calculating CRC for new content in the FIFO. - // Transfer the result from the registers to the result buffer - result[0] = pcd_read_register(CRC_RESULT_REG_L); - result[1] = pcd_read_register(CRC_RESULT_REG_H); +RC522::StatusCode RC522::await_crc_() { + if (millis() - awaiting_comm_time_ < 2) // wait at least 2 ms + return STATUS_WAITING; - ESP_LOGVV(TAG, "pcd_calculate_crc_() STATUS_OK"); - return STATUS_OK; - } + // DivIrqReg[7..0] bits are: Set2 reserved reserved MfinActIRq reserved CRCIRq reserved reserved + uint8_t n = pcd_read_register(DIV_IRQ_REG); + if (n & 0x04) { // CRCIRq bit set - calculation done + pcd_write_register(COMMAND_REG, PCD_IDLE); // Stop calculating CRC for new content in the FIFO. + // Transfer the result from the registers to the result buffer + buffer_[7] = pcd_read_register(CRC_RESULT_REG_L); + buffer_[8] = pcd_read_register(CRC_RESULT_REG_H); + + ESP_LOGVV(TAG, "pcd_calculate_crc_() STATUS_OK"); + return STATUS_OK; } - ESP_LOGVV(TAG, "pcd_calculate_crc_() TIMEOUT"); + if (millis() - awaiting_comm_time_ < 89) + return STATUS_WAITING; + + ESP_LOGD(TAG, "pcd_calculate_crc_() TIMEOUT"); // 89ms passed and nothing happend. Communication with the MFRC522 might be down. return STATUS_TIMEOUT; } -/** - * Returns STATUS_OK if a PICC responds to PICC_CMD_REQA. - * Only "new" cards in state IDLE are invited. Sleeping cards in state HALT are ignored. - * - * @return STATUS_OK on success, STATUS_??? otherwise. - */ -RC522::StatusCode RC522::picc_is_new_card_present_() { - uint8_t buffer_atqa[2]; - uint8_t buffer_size = sizeof(buffer_atqa); - - // Reset baud rates - pcd_write_register(TX_MODE_REG, 0x00); - pcd_write_register(RX_MODE_REG, 0x00); - // Reset ModWidthReg - pcd_write_register(MOD_WIDTH_REG, 0x26); - - auto result = picc_request_a_(buffer_atqa, &buffer_size); - - ESP_LOGV(TAG, "picc_is_new_card_present_() -> %d", result); +bool RC522BinarySensor::process(std::vector &data) { + bool result = true; + if (data.size() != this->uid_.size()) + result = false; + else { + for (uint8_t i = 0; i < data.size(); i++) { + if (data[i] != this->uid_[i]) { + result = false; + break; + } + } + } + this->publish_state(result); + this->found_ = result; return result; } - -/** - * Simple wrapper around PICC_Select. - * Returns true if a UID could be read. - * Remember to call PICC_IsNewCardPresent(), PICC_RequestA() or PICC_WakeupA() first. - * The read UID is available in the class variable uid. - * - * @return bool - */ -bool RC522::picc_read_card_serial_() { - RC522::StatusCode result = picc_select_(&this->uid_); - ESP_LOGVV(TAG, "picc_select_(...) -> %d", result); - return (result == STATUS_OK); -} - -/** - * Transmits SELECT/ANTICOLLISION commands to select a single PICC. - * Before calling this function the PICCs must be placed in the READY(*) state by calling PICC_RequestA() or - * PICC_WakeupA(). On success: - * - The chosen PICC is in state ACTIVE(*) and all other PICCs have returned to state IDLE/HALT. (Figure 7 of the - * ISO/IEC 14443-3 draft.) - * - The UID size and value of the chosen PICC is returned in *uid along with the SAK. - * - * A PICC UID consists of 4, 7 or 10 uint8_ts. - * Only 4 uint8_ts can be specified in a SELECT command, so for the longer UIDs two or three iterations are used: - * UID size Number of UID uint8_ts Cascade levels Example of PICC - * ======== =================== ============== =============== - * single 4 1 MIFARE Classic - * double 7 2 MIFARE Ultralight - * triple 10 3 Not currently in use? - * - * @return STATUS_OK on success, STATUS_??? otherwise. - */ -RC522::StatusCode RC522::picc_select_( - Uid *uid, ///< Pointer to Uid struct. Normally output, but can also be used to supply a known UID. - uint8_t valid_bits ///< The number of known UID bits supplied in *uid. Normally 0. If set you must also supply - ///< uid->size. -) { - bool uid_complete; - bool select_done; - bool use_cascade_tag; - uint8_t cascade_level = 1; - RC522::StatusCode result; - uint8_t count; - uint8_t check_bit; - uint8_t index; - uint8_t uid_index; // The first index in uid->uiduint8_t[] that is used in the current Cascade Level. - int8_t current_level_known_bits; // The number of known UID bits in the current Cascade Level. - uint8_t buffer[9]; // The SELECT/ANTICOLLISION commands uses a 7 uint8_t standard frame + 2 uint8_ts CRC_A - uint8_t buffer_used; // The number of uint8_ts used in the buffer, ie the number of uint8_ts to transfer to the FIFO. - uint8_t rx_align; // Used in BitFramingReg. Defines the bit position for the first bit received. - uint8_t tx_last_bits; // Used in BitFramingReg. The number of valid bits in the last transmitted uint8_t. - uint8_t *response_buffer; - uint8_t response_length; - - // Description of buffer structure: - // uint8_t 0: SEL Indicates the Cascade Level: PICC_CMD_SEL_CL1, PICC_CMD_SEL_CL2 or PICC_CMD_SEL_CL3 - // uint8_t 1: NVB Number of Valid Bits (in complete command, not just the UID): High nibble: complete - // uint8_ts, - // Low nibble: Extra bits. uint8_t 2: UID-data or CT See explanation below. CT means Cascade Tag. uint8_t - // 3: UID-data uint8_t 4: UID-data uint8_t 5: UID-data uint8_t 6: BCC Block Check Character - XOR of - // uint8_ts 2-5 uint8_t 7: CRC_A uint8_t 8: CRC_A The BCC and CRC_A are only transmitted if we know all the UID bits - // of the current Cascade Level. - // - // Description of uint8_ts 2-5: (Section 6.5.4 of the ISO/IEC 14443-3 draft: UID contents and cascade levels) - // UID size Cascade level uint8_t2 uint8_t3 uint8_t4 uint8_t5 - // ======== ============= ===== ===== ===== ===== - // 4 uint8_ts 1 uid0 uid1 uid2 uid3 - // 7 uint8_ts 1 CT uid0 uid1 uid2 - // 2 uid3 uid4 uid5 uid6 - // 10 uint8_ts 1 CT uid0 uid1 uid2 - // 2 CT uid3 uid4 uid5 - // 3 uid6 uid7 uid8 uid9 - - // Sanity checks - if (valid_bits > 80) { - return STATUS_INVALID; - } - - ESP_LOGVV(TAG, "picc_select_(&, %d)", valid_bits); - - // Prepare MFRC522 - pcd_clear_register_bit_mask_(COLL_REG, 0x80); // ValuesAfterColl=1 => Bits received after collision are cleared. - - // Repeat Cascade Level loop until we have a complete UID. - uid_complete = false; - while (!uid_complete) { - // Set the Cascade Level in the SEL uint8_t, find out if we need to use the Cascade Tag in uint8_t 2. - switch (cascade_level) { - case 1: - buffer[0] = PICC_CMD_SEL_CL1; - uid_index = 0; - use_cascade_tag = valid_bits && uid->size > 4; // When we know that the UID has more than 4 uint8_ts - break; - - case 2: - buffer[0] = PICC_CMD_SEL_CL2; - uid_index = 3; - use_cascade_tag = valid_bits && uid->size > 7; // When we know that the UID has more than 7 uint8_ts - break; - - case 3: - buffer[0] = PICC_CMD_SEL_CL3; - uid_index = 6; - use_cascade_tag = false; // Never used in CL3. - break; - - default: - return STATUS_INTERNAL_ERROR; - break; - } - - // How many UID bits are known in this Cascade Level? - current_level_known_bits = valid_bits - (8 * uid_index); - if (current_level_known_bits < 0) { - current_level_known_bits = 0; - } - // Copy the known bits from uid->uiduint8_t[] to buffer[] - index = 2; // destination index in buffer[] - if (use_cascade_tag) { - buffer[index++] = PICC_CMD_CT; - } - uint8_t uint8_ts_to_copy = current_level_known_bits / 8 + - (current_level_known_bits % 8 - ? 1 - : 0); // The number of uint8_ts needed to represent the known bits for this level. - if (uint8_ts_to_copy) { - uint8_t maxuint8_ts = - use_cascade_tag ? 3 : 4; // Max 4 uint8_ts in each Cascade Level. Only 3 left if we use the Cascade Tag - if (uint8_ts_to_copy > maxuint8_ts) { - uint8_ts_to_copy = maxuint8_ts; - } - for (count = 0; count < uint8_ts_to_copy; count++) { - buffer[index++] = uid->uiduint8_t[uid_index + count]; - } - } - // Now that the data has been copied we need to include the 8 bits in CT in currentLevelKnownBits - if (use_cascade_tag) { - current_level_known_bits += 8; - } - - // Repeat anti collision loop until we can transmit all UID bits + BCC and receive a SAK - max 32 iterations. - select_done = false; - while (!select_done) { - // Find out how many bits and uint8_ts to send and receive. - if (current_level_known_bits >= 32) { // All UID bits in this Cascade Level are known. This is a SELECT. - - if (response_length < 4) { - ESP_LOGW(TAG, "Not enough data received."); - return STATUS_INVALID; - } - - // Serial.print(F("SELECT: currentLevelKnownBits=")); Serial.println(currentLevelKnownBits, DEC); - buffer[1] = 0x70; // NVB - Number of Valid Bits: Seven whole uint8_ts - // Calculate BCC - Block Check Character - buffer[6] = buffer[2] ^ buffer[3] ^ buffer[4] ^ buffer[5]; - // Calculate CRC_A - result = pcd_calculate_crc_(buffer, 7, &buffer[7]); - if (result != STATUS_OK) { - return result; - } - tx_last_bits = 0; // 0 => All 8 bits are valid. - buffer_used = 9; - // Store response in the last 3 uint8_ts of buffer (BCC and CRC_A - not needed after tx) - response_buffer = &buffer[6]; - response_length = 3; - } else { // This is an ANTICOLLISION. - // Serial.print(F("ANTICOLLISION: currentLevelKnownBits=")); Serial.println(currentLevelKnownBits, DEC); - tx_last_bits = current_level_known_bits % 8; - count = current_level_known_bits / 8; // Number of whole uint8_ts in the UID part. - index = 2 + count; // Number of whole uint8_ts: SEL + NVB + UIDs - buffer[1] = (index << 4) + tx_last_bits; // NVB - Number of Valid Bits - buffer_used = index + (tx_last_bits ? 1 : 0); - // Store response in the unused part of buffer - response_buffer = &buffer[index]; - response_length = sizeof(buffer) - index; - } - - // Set bit adjustments - rx_align = tx_last_bits; // Having a separate variable is overkill. But it makes the next line easier to read. - pcd_write_register( - BIT_FRAMING_REG, - (rx_align << 4) + tx_last_bits); // RxAlign = BitFramingReg[6..4]. TxLastBits = BitFramingReg[2..0] - - // Transmit the buffer and receive the response. - result = pcd_transceive_data_(buffer, buffer_used, response_buffer, &response_length, &tx_last_bits, rx_align); - if (result == STATUS_COLLISION) { // More than one PICC in the field => collision. - uint8_t value_of_coll_reg = pcd_read_register( - COLL_REG); // CollReg[7..0] bits are: ValuesAfterColl reserved CollPosNotValid CollPos[4:0] - if (value_of_coll_reg & 0x20) { // CollPosNotValid - return STATUS_COLLISION; // Without a valid collision position we cannot continue - } - uint8_t collision_pos = value_of_coll_reg & 0x1F; // Values 0-31, 0 means bit 32. - if (collision_pos == 0) { - collision_pos = 32; - } - if (collision_pos <= current_level_known_bits) { // No progress - should not happen - return STATUS_INTERNAL_ERROR; - } - // Choose the PICC with the bit set. - current_level_known_bits = collision_pos; - count = current_level_known_bits % 8; // The bit to modify - check_bit = (current_level_known_bits - 1) % 8; - index = 1 + (current_level_known_bits / 8) + (count ? 1 : 0); // First uint8_t is index 0. - if (response_length > 2) // Note: Otherwise buffer[index] might be not initialized - buffer[index] |= (1 << check_bit); - } else if (result != STATUS_OK) { - return result; - } else { // STATUS_OK - if (current_level_known_bits >= 32) { // This was a SELECT. - select_done = true; // No more anticollision - // We continue below outside the while. - } else { // This was an ANTICOLLISION. - // We now have all 32 bits of the UID in this Cascade Level - current_level_known_bits = 32; - // Run loop again to do the SELECT. - } - } - } // End of while (!selectDone) - - // We do not check the CBB - it was constructed by us above. - - // Copy the found UID uint8_ts from buffer[] to uid->uiduint8_t[] - index = (buffer[2] == PICC_CMD_CT) ? 3 : 2; // source index in buffer[] - uint8_ts_to_copy = (buffer[2] == PICC_CMD_CT) ? 3 : 4; - for (count = 0; count < uint8_ts_to_copy; count++) { - uid->uiduint8_t[uid_index + count] = buffer[index++]; - } - - // Check response SAK (Select Acknowledge) - if (response_length != 3 || tx_last_bits != 0) { // SAK must be exactly 24 bits (1 uint8_t + CRC_A). - return STATUS_ERROR; - } - // Verify CRC_A - do our own calculation and store the control in buffer[2..3] - those uint8_ts are not needed - // anymore. - result = pcd_calculate_crc_(response_buffer, 1, &buffer[2]); - if (result != STATUS_OK) { - return result; - } - if ((buffer[2] != response_buffer[1]) || (buffer[3] != response_buffer[2])) { - return STATUS_CRC_WRONG; - } - if (response_buffer[0] & 0x04) { // Cascade bit set - UID not complete yes - cascade_level++; - } else { - uid_complete = true; - uid->sak = response_buffer[0]; - } - } // End of while (!uidComplete) - - // Set correct uid->size - uid->size = 3 * cascade_level + 1; - - return STATUS_OK; -} - -bool RC522BinarySensor::process(const uint8_t *data, uint8_t len) { - if (len != this->uid_.size()) - return false; - - for (uint8_t i = 0; i < len; i++) { - if (data[i] != this->uid_[i]) - return false; - } - - this->publish_state(true); - this->found_ = true; - return true; -} -void RC522Trigger::process(const uint8_t *uid, uint8_t uid_length) { - char buf[32]; - format_uid(buf, uid, uid_length); - this->trigger(std::string(buf)); -} +void RC522Trigger::process(std::vector &data) { this->trigger(format_uid(data)); } } // namespace rc522 } // namespace esphome diff --git a/esphome/components/rc522/rc522.h b/esphome/components/rc522/rc522.h index cabcf8db0b..7fb49e97fd 100644 --- a/esphome/components/rc522/rc522.h +++ b/esphome/components/rc522/rc522.h @@ -26,6 +26,33 @@ class RC522 : public PollingComponent { void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } protected: + // Return codes from the functions in this class. Remember to update GetStatusCodeName() if you add more. + // last value set to 0xff, then compiler uses less ram, it seems some optimisations are triggered + enum StatusCode : uint8_t { + STATUS_OK, // Success + STATUS_WAITING, // Waiting result from RC522 chip + STATUS_ERROR, // Error in communication + STATUS_COLLISION, // Collission detected + STATUS_TIMEOUT, // Timeout in communication. + STATUS_NO_ROOM, // A buffer is not big enough. + STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-) + STATUS_INVALID, // Invalid argument. + STATUS_CRC_WRONG, // The CRC_A does not match + STATUS_MIFARE_NACK = 0xff // A MIFARE PICC responded with NAK. + }; + + enum State { + STATE_NONE = 0, + STATE_SETUP, + STATE_INIT, + STATE_PICC_REQUEST_A, + STATE_READ_SERIAL, + STATE_SELECT_SERIAL, + STATE_SELECT_SERIAL_DONE, + STATE_READ_SERIAL_DONE, + STATE_DONE, + } state_{STATE_NONE}; + enum PcdRegister : uint8_t { // Page 0: Command and status // 0x00 // reserved for future use @@ -150,33 +177,11 @@ class RC522 : public PollingComponent { PICC_CMD_UL_WRITE = 0xA2 // Writes one 4 uint8_t page to the PICC. }; - // Return codes from the functions in this class. Remember to update GetStatusCodeName() if you add more. - // last value set to 0xff, then compiler uses less ram, it seems some optimisations are triggered - enum StatusCode : uint8_t { - STATUS_OK, // Success - STATUS_ERROR, // Error in communication - STATUS_COLLISION, // Collission detected - STATUS_TIMEOUT, // Timeout in communication. - STATUS_NO_ROOM, // A buffer is not big enough. - STATUS_INTERNAL_ERROR, // Internal error in the code. Should not happen ;-) - STATUS_INVALID, // Invalid argument. - STATUS_CRC_WRONG, // The CRC_A does not match - STATUS_MIFARE_NACK = 0xff // A MIFARE PICC responded with NAK. - }; - - // A struct used for passing the UID of a PICC. - using Uid = struct { - uint8_t size; // Number of uint8_ts in the UID. 4, 7 or 10. - uint8_t uiduint8_t[10]; - uint8_t sak; // The SAK (Select acknowledge) uint8_t returned from the PICC after successful selection. - }; - - Uid uid_; - uint32_t update_wait_{0}; - void pcd_reset_(); void initialize_(); void pcd_antenna_on_(); + void pcd_antenna_off_(); + virtual uint8_t pcd_read_register(PcdRegister reg ///< The register to read from. One of the PCD_Register enums. ) = 0; @@ -202,15 +207,6 @@ class RC522 : public PollingComponent { uint8_t *values ///< The values to write. uint8_t array. ) = 0; - StatusCode picc_request_a_( - uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in - uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. - ); - StatusCode picc_reqa_or_wupa_( - uint8_t command, ///< The command to send - PICC_CMD_REQA or PICC_CMD_WUPA - uint8_t *buffer_atqa, ///< The buffer to store the ATQA (Answer to request) in - uint8_t *buffer_size ///< Buffer size, at least two uint8_ts. Also number of uint8_ts returned if STATUS_OK. - ); void pcd_set_register_bit_mask_(PcdRegister reg, ///< The register to update. One of the PCD_Register enums. uint8_t mask ///< The bits to set. ); @@ -218,38 +214,33 @@ class RC522 : public PollingComponent { uint8_t mask ///< The bits to clear. ); - StatusCode pcd_transceive_data_(uint8_t *send_data, uint8_t send_len, uint8_t *back_data, uint8_t *back_len, - uint8_t *valid_bits = nullptr, uint8_t rx_align = 0, bool check_crc = false); - StatusCode pcd_communicate_with_picc_(uint8_t command, uint8_t wait_i_rq, uint8_t *send_data, uint8_t send_len, - uint8_t *back_data = nullptr, uint8_t *back_len = nullptr, - uint8_t *valid_bits = nullptr, uint8_t rx_align = 0, bool check_crc = false); - StatusCode pcd_calculate_crc_( - uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. - uint8_t length, ///< In: The number of uint8_ts to transfer. - uint8_t *result ///< Out: Pointer to result buffer. Result is written to result[0..1], low uint8_t first. - ); - RC522::StatusCode picc_is_new_card_present_(); - bool picc_read_card_serial_(); - StatusCode picc_select_( - Uid *uid, ///< Pointer to Uid struct. Normally output, but can also be used to supply a known UID. - uint8_t valid_bits = 0 ///< The number of known UID bits supplied in *uid. Normally 0. If set you must also - ///< supply uid->size. + void pcd_transceive_data_(uint8_t send_len); + + void pcd_calculate_crc_(uint8_t *data, ///< In: Pointer to the data to transfer to the FIFO for CRC calculation. + uint8_t length ///< In: The number of uint8_ts to transfer. ); - /** Read a data frame from the RC522 and return the result as a vector. - * - * Note that is_ready needs to be checked first before requesting this method. - * - * On failure, an empty vector is returned. - */ - std::vector r_c522_read_data_(); + bool awaiting_comm_; + uint32_t awaiting_comm_time_; + StatusCode await_transceive_(); + StatusCode await_crc_(); + + uint8_t buffer_[9]; ///< buffer for communication, the first bits [0..back_idx-1] are for tx , + ///< [back_idx..back_idx+back_len] for rx + uint8_t send_len_; // index of first byte for RX + uint8_t back_length_; ///< In: Max number of uint8_ts to write to *backData. Out: The number of uint8_ts returned. + uint8_t uid_buffer_[10]; // buffer to construct the uid (for 7 and 10 bit uids) + uint8_t uid_idx_ = 0; // number of read uid bytes e.g. index of the next available position in uid_buffer + uint8_t error_counter_ = 0; // to reset if unresponsive + uint8_t rx_align_; + uint8_t *valid_bits_; GPIOPin *reset_pin_{nullptr}; uint8_t reset_count_{0}; uint32_t reset_timeout_{0}; - bool initialize_pending_{false}; std::vector binary_sensors_; std::vector triggers_; + std::vector current_uid_; enum RC522Error { NONE = 0, @@ -261,7 +252,7 @@ class RC522BinarySensor : public binary_sensor::BinarySensor { public: void set_uid(const std::vector &uid) { uid_ = uid; } - bool process(const uint8_t *data, uint8_t len); + bool process(std::vector &data); void on_scan_end() { if (!this->found_) { @@ -277,7 +268,7 @@ class RC522BinarySensor : public binary_sensor::BinarySensor { class RC522Trigger : public Trigger { public: - void process(const uint8_t *uid, uint8_t uid_length); + void process(std::vector &data); }; } // namespace rc522 diff --git a/esphome/components/rc522_i2c/rc522_i2c.cpp b/esphome/components/rc522_i2c/rc522_i2c.cpp index 8248e79b50..fe88f567c0 100644 --- a/esphome/components/rc522_i2c/rc522_i2c.cpp +++ b/esphome/components/rc522_i2c/rc522_i2c.cpp @@ -36,10 +36,6 @@ void RC522I2C::pcd_read_register(PcdRegister reg, ///< The register to read fro return; } - std::string buf; - buf = "Rx"; - char cstrb[20]; - uint8_t b = values[0]; read_bytes(reg >> 1, values, count); @@ -69,31 +65,5 @@ void RC522I2C::pcd_write_register(PcdRegister reg, ///< The register to write t write_bytes(reg >> 1, values, count); } -// bool RC522I2C::write_data(const std::vector &data) { -// return this->write_bytes_raw(data.data(), data.size()); } - -// bool RC522I2C::read_data(std::vector &data, uint8_t len) { -// delay(5); - -// std::vector ready; -// ready.resize(1); -// uint32_t start_time = millis(); -// while (true) { -// if (this->read_bytes_raw(ready.data(), 1)) { -// if (ready[0] == 0x01) -// break; -// } - -// if (millis() - start_time > 100) { -// ESP_LOGV(TAG, "Timed out waiting for readiness from RC522!"); -// return false; -// } -// } - -// data.resize(len + 1); -// this->read_bytes_raw(data.data(), len + 1); -// return true; -// } - } // namespace rc522_i2c } // namespace esphome diff --git a/esphome/components/rc522_spi/rc522_spi.cpp b/esphome/components/rc522_spi/rc522_spi.cpp index 61236393e4..1865b36da6 100644 --- a/esphome/components/rc522_spi/rc522_spi.cpp +++ b/esphome/components/rc522_spi/rc522_spi.cpp @@ -32,7 +32,7 @@ uint8_t RC522Spi::pcd_read_register(PcdRegister reg ///< The register to read f transfer_byte(0x80 | reg); value = read_byte(); disable(); - ESP_LOGV(TAG, "read_register_(%x) -> %x", reg, value); + ESP_LOGVV(TAG, "read_register_(%d) -> %d", reg, value); return value; } @@ -45,9 +45,11 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro uint8_t *values, ///< uint8_t array to store the values in. uint8_t rx_align ///< Only bit positions rxAlign..7 in values[0] are updated. ) { +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE std::string buf; buf = "Rx"; char cstrb[20]; +#endif if (count == 0) { return; } @@ -68,25 +70,30 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro values[0] = (values[0] & ~mask) | (value & mask); index++; +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE sprintf(cstrb, " %x", values[0]); buf.append(cstrb); +#endif } while (index < count) { values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again. +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE sprintf(cstrb, " %x", values[index]); buf.append(cstrb); +#endif index++; } values[index] = transfer_byte(0); // Read the final uint8_t. Send 0 to stop reading. +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE buf = buf + " "; sprintf(cstrb, "%x", values[index]); buf.append(cstrb); ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str()); - +#endif disable(); } @@ -108,21 +115,25 @@ void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write t uint8_t count, ///< The number of uint8_ts to write to the register uint8_t *values ///< The values to write. uint8_t array. ) { +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE std::string buf; buf = "Tx"; + char cstrb[20]; +#endif enable(); transfer_byte(reg); - char cstrb[20]; for (uint8_t index = 0; index < count; index++) { transfer_byte(values[index]); +#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE sprintf(cstrb, " %x", values[index]); buf.append(cstrb); +#endif } disable(); - ESP_LOGVV(TAG, "write_register_(%x, %d) -> %s", reg, count, buf.c_str()); + ESP_LOGVV(TAG, "write_register_(%d, %d) -> %s", reg, count, buf.c_str()); } } // namespace rc522_spi From 4ae4a4ee88d985e53af4e5aaf07950476e2893e2 Mon Sep 17 00:00:00 2001 From: Wojtek Strzalka Date: Thu, 29 Apr 2021 01:49:46 +0200 Subject: [PATCH 39/62] Support for TOF10120 distance sensor (#1375) --- CODEOWNERS | 1 + esphome/components/tof10120/__init__.py | 0 esphome/components/tof10120/sensor.py | 26 +++++++++ .../components/tof10120/tof10120_sensor.cpp | 53 +++++++++++++++++++ esphome/components/tof10120/tof10120_sensor.h | 19 +++++++ tests/test3.yaml | 5 +- 6 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 esphome/components/tof10120/__init__.py create mode 100644 esphome/components/tof10120/sensor.py create mode 100644 esphome/components/tof10120/tof10120_sensor.cpp create mode 100644 esphome/components/tof10120/tof10120_sensor.h diff --git a/CODEOWNERS b/CODEOWNERS index 1a6a54a00c..891b24f179 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -108,6 +108,7 @@ esphome/components/thermostat/* @kbx81 esphome/components/time/* @OttoWinter esphome/components/tm1637/* @glmnet esphome/components/tmp102/* @timsavage +esphome/components/tof10120/* @wstrzalka esphome/components/tuya/binary_sensor/* @jesserockz esphome/components/tuya/climate/* @jesserockz esphome/components/tuya/sensor/* @jesserockz diff --git a/esphome/components/tof10120/__init__.py b/esphome/components/tof10120/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/tof10120/sensor.py b/esphome/components/tof10120/sensor.py new file mode 100644 index 0000000000..91a15960b4 --- /dev/null +++ b/esphome/components/tof10120/sensor.py @@ -0,0 +1,26 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import CONF_ID, UNIT_METER, ICON_ARROW_EXPAND_VERTICAL + +CODEOWNERS = ["@wstrzalka"] +DEPENDENCIES = ["i2c"] + +tof10120_ns = cg.esphome_ns.namespace("tof10120") +TOF10120Sensor = tof10120_ns.class_( + "TOF10120Sensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + sensor.sensor_schema(UNIT_METER, ICON_ARROW_EXPAND_VERTICAL, 3) + .extend({cv.GenerateID(): cv.declare_id(TOF10120Sensor)}) + .extend(cv.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x52)) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield sensor.register_sensor(var, config) + yield i2c.register_i2c_device(var, config) diff --git a/esphome/components/tof10120/tof10120_sensor.cpp b/esphome/components/tof10120/tof10120_sensor.cpp new file mode 100644 index 0000000000..4e2732bb08 --- /dev/null +++ b/esphome/components/tof10120/tof10120_sensor.cpp @@ -0,0 +1,53 @@ +#include "tof10120_sensor.h" +#include "esphome/core/log.h" + +// Very basic support for TOF10120 distance sensor + +namespace esphome { +namespace tof10120 { + +static const char *TAG = "tof10120"; +static const uint8_t TOF10120_READ_DISTANCE_CMD[] = {0x00}; +static const uint8_t TOF10120_DEFAULT_DELAY = 30; + +static const uint8_t TOF10120_DIR_SEND_REGISTER = 0x0e; +static const uint8_t TOF10120_DISTANCE_REGISTER = 0x00; + +static const uint16_t TOF10120_OUT_OF_RANGE_VALUE = 2000; + +void TOF10120Sensor::dump_config() { + LOG_SENSOR("", "TOF10120", this); + LOG_UPDATE_INTERVAL(this); + LOG_I2C_DEVICE(this); +} + +void TOF10120Sensor::setup() {} + +void TOF10120Sensor::update() { + if (!this->write_bytes(TOF10120_DISTANCE_REGISTER, TOF10120_READ_DISTANCE_CMD, sizeof(TOF10120_READ_DISTANCE_CMD))) { + ESP_LOGE(TAG, "Communication with TOF10120 failed on write"); + this->status_set_warning(); + return; + } + + uint8_t data[2]; + if (!this->read_bytes(TOF10120_DISTANCE_REGISTER, data, 2, TOF10120_DEFAULT_DELAY)) { + ESP_LOGE(TAG, "Communication with TOF10120 failed on read"); + this->status_set_warning(); + return; + } + + uint32_t distance_mm = (data[0] << 8) | data[1]; + ESP_LOGI(TAG, "Data read: %dmm", distance_mm); + + if (distance_mm == TOF10120_OUT_OF_RANGE_VALUE) { + ESP_LOGW(TAG, "Distance measurement out of range"); + this->publish_state(NAN); + } else { + this->publish_state(distance_mm / 1000.0); + } + this->status_clear_warning(); +} + +} // namespace tof10120 +} // namespace esphome diff --git a/esphome/components/tof10120/tof10120_sensor.h b/esphome/components/tof10120/tof10120_sensor.h new file mode 100644 index 0000000000..90bad8ed07 --- /dev/null +++ b/esphome/components/tof10120/tof10120_sensor.h @@ -0,0 +1,19 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace tof10120 { + +class TOF10120Sensor : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice { + public: + void setup() override; + + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void update() override; +}; +} // namespace tof10120 +} // namespace esphome diff --git a/tests/test3.yaml b/tests/test3.yaml index 165a36c015..8781c935a1 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -443,7 +443,10 @@ sensor: - platform: ezo id: ph_ezo address: 99 - unit_of_measurement: 'pH' + unit_of_measurement: 'pH' + - platform: tof10120 + name: "Distance sensor" + update_interval: 5s - platform: fingerprint_grow fingerprint_count: name: "Fingerprint Count" From 07db9319ad6b602de4cdb2d754cad717c21c6f62 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Apr 2021 11:53:44 +1200 Subject: [PATCH 40/62] Swap fan and swing fields for Fujitu ACs (#1635) --- .../fujitsu_general/fujitsu_general.cpp | 20 +++++------ .../fujitsu_general/fujitsu_general.h | 36 +++++++++++++++++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/esphome/components/fujitsu_general/fujitsu_general.cpp b/esphome/components/fujitsu_general/fujitsu_general.cpp index 75ee3f708b..2676609d9b 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.cpp +++ b/esphome/components/fujitsu_general/fujitsu_general.cpp @@ -41,33 +41,33 @@ const uint8_t FUJITSU_GENERAL_TEMPERATURE_NIBBLE = 16; // Power on const uint8_t FUJITSU_GENERAL_POWER_ON_NIBBLE = 17; -const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01; const uint8_t FUJITSU_GENERAL_POWER_OFF = 0x00; +const uint8_t FUJITSU_GENERAL_POWER_ON = 0x01; // Mode const uint8_t FUJITSU_GENERAL_MODE_NIBBLE = 19; const uint8_t FUJITSU_GENERAL_MODE_AUTO = 0x00; -const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04; const uint8_t FUJITSU_GENERAL_MODE_COOL = 0x01; const uint8_t FUJITSU_GENERAL_MODE_DRY = 0x02; const uint8_t FUJITSU_GENERAL_MODE_FAN = 0x03; +const uint8_t FUJITSU_GENERAL_MODE_HEAT = 0x04; // const uint8_t FUJITSU_GENERAL_MODE_10C = 0x0B; // Swing -const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 20; +const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 20; +const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00; +const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01; +const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02; +const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03; + +// Fan +const uint8_t FUJITSU_GENERAL_FAN_NIBBLE = 21; const uint8_t FUJITSU_GENERAL_FAN_AUTO = 0x00; const uint8_t FUJITSU_GENERAL_FAN_HIGH = 0x01; const uint8_t FUJITSU_GENERAL_FAN_MEDIUM = 0x02; const uint8_t FUJITSU_GENERAL_FAN_LOW = 0x03; const uint8_t FUJITSU_GENERAL_FAN_SILENT = 0x04; -// Fan speed -const uint8_t FUJITSU_GENERAL_SWING_NIBBLE = 21; -const uint8_t FUJITSU_GENERAL_SWING_NONE = 0x00; -const uint8_t FUJITSU_GENERAL_SWING_VERTICAL = 0x01; -const uint8_t FUJITSU_GENERAL_SWING_HORIZONTAL = 0x02; -const uint8_t FUJITSU_GENERAL_SWING_BOTH = 0x03; - // TODO Outdoor Unit Low Noise // const uint8_t FUJITSU_GENERAL_OUTDOOR_UNIT_LOW_NOISE_BYTE14 = 0xA0; // const uint8_t FUJITSU_GENERAL_STATE_BYTE14 = 0x20; diff --git a/esphome/components/fujitsu_general/fujitsu_general.h b/esphome/components/fujitsu_general/fujitsu_general.h index 8154d7a1d2..e97615f739 100644 --- a/esphome/components/fujitsu_general/fujitsu_general.h +++ b/esphome/components/fujitsu_general/fujitsu_general.h @@ -11,6 +11,42 @@ namespace fujitsu_general { const uint8_t FUJITSU_GENERAL_TEMP_MIN = 16; // Celsius // TODO 16 for heating, 18 for cooling, unsupported in ESPH const uint8_t FUJITSU_GENERAL_TEMP_MAX = 30; // Celsius +// clang-format off +/** + * ``` + * turn + * on temp mode fan swing + * * | | | | | | * + * + * temperatures 1 1248 124 124 1 + * auto auto 18 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000100 00000000 00000000 00000000 00000000 00000000 00000100 11110001 + * auto auto 19 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10001100 00000000 00000000 00000000 00000000 00000000 00000100 11111110 + * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 + * + * on flag: + * on at 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000000 00100000 00000000 00000000 00000000 00000000 00000100 11010101 + * down to 16 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000000 00100000 00000000 00000000 00000000 00000000 00000100 00110101 + * + * mode options: + * auto auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00000000 00000000 00000000 00000000 00000000 00000100 11110011 + * cool auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 10000000 00000000 00000000 00000000 00000000 00000100 01110011 + * dry auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 01000000 00000000 00000000 00000000 00000000 00000100 10110011 + * fan (auto) (30) 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 11000000 00000000 00000000 00000000 00000000 00000100 00110011 + * heat auto 30 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 00000000 00000000 00000000 00000000 00000100 11010011 + * + * fan options: + * heat 30 high 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 10000111 00100000 10000000 00000000 00000000 00000000 00000100 01010011 + * heat 30 med 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 01000000 00000000 00000000 00000000 00000100 01010011 + * heat 30 low 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 11000000 00000000 00000000 00000000 00000100 10010011 + * heat 30 quiet 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 + * + * swing options: + * heat 30 swing vert 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00101000 00000000 00000000 00000000 00000100 00011101 + * heat 30 noswing 00101000 11000110 00000000 00001000 00001000 01111111 10010000 00001100 00000111 00100000 00100000 00000000 00000000 00000000 00000100 00010011 + * ``` + */ +// clang-format on + class FujitsuGeneralClimate : public climate_ir::ClimateIR { public: FujitsuGeneralClimate() From 02aa75f68c149df50b2d3cdd23c539ccd715e10b Mon Sep 17 00:00:00 2001 From: buxtronix Date: Mon, 3 May 2021 09:10:50 +1000 Subject: [PATCH 41/62] BLE client support on ESP32 (#1177) Co-authored-by: Ben Buxton Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + esphome/components/ble_client/__init__.py | 87 ++++ esphome/components/ble_client/automation.h | 37 ++ esphome/components/ble_client/ble_client.cpp | 380 ++++++++++++++++++ esphome/components/ble_client/ble_client.h | 140 +++++++ .../components/ble_client/sensor/__init__.py | 115 ++++++ .../components/ble_client/sensor/automation.h | 37 ++ .../ble_client/sensor/ble_sensor.cpp | 129 ++++++ .../components/ble_client/sensor/ble_sensor.h | 46 +++ .../components/ble_client/switch/__init__.py | 30 ++ .../ble_client/switch/ble_switch.cpp | 39 ++ .../components/ble_client/switch/ble_switch.h | 30 ++ .../components/esp32_ble_tracker/__init__.py | 8 + .../esp32_ble_tracker/esp32_ble_tracker.cpp | 86 +++- .../esp32_ble_tracker/esp32_ble_tracker.h | 44 ++ esphome/components/esp32_ble_tracker/queue.h | 96 +++++ esphome/config.py | 13 +- esphome/const.py | 3 + tests/test1.yaml | 29 ++ 19 files changed, 1342 insertions(+), 8 deletions(-) create mode 100644 esphome/components/ble_client/__init__.py create mode 100644 esphome/components/ble_client/automation.h create mode 100644 esphome/components/ble_client/ble_client.cpp create mode 100644 esphome/components/ble_client/ble_client.h create mode 100644 esphome/components/ble_client/sensor/__init__.py create mode 100644 esphome/components/ble_client/sensor/automation.h create mode 100644 esphome/components/ble_client/sensor/ble_sensor.cpp create mode 100644 esphome/components/ble_client/sensor/ble_sensor.h create mode 100644 esphome/components/ble_client/switch/__init__.py create mode 100644 esphome/components/ble_client/switch/ble_switch.cpp create mode 100644 esphome/components/ble_client/switch/ble_switch.h create mode 100644 esphome/components/esp32_ble_tracker/queue.h diff --git a/CODEOWNERS b/CODEOWNERS index 891b24f179..e15de721b9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -21,6 +21,7 @@ esphome/components/atc_mithermometer/* @ahpohl esphome/components/b_parasite/* @rbaron esphome/components/bang_bang/* @OttoWinter esphome/components/binary_sensor/* @esphome/core +esphome/components/ble_client/* @buxtronix esphome/components/bme680_bsec/* @trvrnrth esphome/components/canbus/* @danielschramm @mvturnho esphome/components/captive_portal/* @OttoWinter diff --git a/esphome/components/ble_client/__init__.py b/esphome/components/ble_client/__init__.py new file mode 100644 index 0000000000..d3b287574b --- /dev/null +++ b/esphome/components/ble_client/__init__.py @@ -0,0 +1,87 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import esp32_ble_tracker +from esphome.const import ( + CONF_ID, + CONF_MAC_ADDRESS, + CONF_NAME, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, + CONF_TRIGGER_ID, +) +from esphome.core import coroutine +from esphome import automation + +CODEOWNERS = ["@buxtronix"] +DEPENDENCIES = ["esp32_ble_tracker"] + +ble_client_ns = cg.esphome_ns.namespace("ble_client") +BLEClient = ble_client_ns.class_( + "BLEClient", cg.Component, esp32_ble_tracker.ESPBTClient +) +BLEClientNode = ble_client_ns.class_("BLEClientNode") +BLEClientNodeConstRef = BLEClientNode.operator("ref").operator("const") +# Triggers +BLEClientConnectTrigger = ble_client_ns.class_( + "BLEClientConnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) +BLEClientDisconnectTrigger = ble_client_ns.class_( + "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) +) + +# Espressif platformio framework is built with MAX_BLE_CONN to 3, so +# enforce this in yaml checks. +MULTI_CONF = 3 + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(BLEClient), + cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_NAME): cv.string, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientConnectTrigger + ), + } + ), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLEClientDisconnectTrigger + ), + } + ), + } + ) + .extend(cv.COMPONENT_SCHEMA) + .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) +) + +CONF_BLE_CLIENT_ID = "ble_client_id" + +BLE_CLIENT_SCHEMA = cv.Schema( + { + cv.Required(CONF_BLE_CLIENT_ID): cv.use_id(BLEClient), + } +) + + +@coroutine +def register_ble_node(var, config): + parent = yield cg.get_variable(config[CONF_BLE_CLIENT_ID]) + cg.add(parent.register_ble_node(var)) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield esp32_ble_tracker.register_client(var, config) + cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + for conf in config.get(CONF_ON_CONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) + for conf in config.get(CONF_ON_DISCONNECT, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ble_client/automation.h b/esphome/components/ble_client/automation.h new file mode 100644 index 0000000000..2db609de55 --- /dev/null +++ b/esphome/components/ble_client/automation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/ble_client.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { +class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_OPEN_EVT && param->open.status == ESP_GATT_OK) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::Established; + } +}; + +class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { + public: + explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); } + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_DISCONNECT_EVT && memcmp(param->disconnect.remote_bda, this->parent_->remote_bda, 6) == 0) + this->trigger(); + if (event == ESP_GATTC_SEARCH_CMPL_EVT) + this->node_state = espbt::ClientState::Established; + } +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp new file mode 100644 index 0000000000..391f6425e2 --- /dev/null +++ b/esphome/components/ble_client/ble_client.cpp @@ -0,0 +1,380 @@ +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "ble_client.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_client"; + +void BLEClient::setup() { + auto ret = esp_ble_gattc_app_register(this->app_id); + if (ret) { + ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret); + this->mark_failed(); + } + this->set_states(espbt::ClientState::Idle); + this->enabled = true; +} + +void BLEClient::loop() { + if (this->state() == espbt::ClientState::Discovered) { + this->connect(); + } + for (auto *node : this->nodes_) + node->loop(); +} + +void BLEClient::dump_config() { + ESP_LOGCONFIG(TAG, "BLE Client:"); + ESP_LOGCONFIG(TAG, " Address: %s", this->address_str().c_str()); +} + +bool BLEClient::parse_device(const espbt::ESPBTDevice &device) { + if (!this->enabled) + return false; + if (device.address_uint64() != this->address) + return false; + if (this->state() != espbt::ClientState::Idle) + return false; + + ESP_LOGD(TAG, "Found device at MAC address [%s]", device.address_str().c_str()); + this->set_states(espbt::ClientState::Discovered); + + auto addr = device.address_uint64(); + this->remote_bda[0] = (addr >> 40) & 0xFF; + this->remote_bda[1] = (addr >> 32) & 0xFF; + this->remote_bda[2] = (addr >> 24) & 0xFF; + this->remote_bda[3] = (addr >> 16) & 0xFF; + this->remote_bda[4] = (addr >> 8) & 0xFF; + this->remote_bda[5] = (addr >> 0) & 0xFF; + return true; +} + +std::string BLEClient::address_str() const { + char buf[20]; + sprintf(buf, "%02x:%02x:%02x:%02x:%02x:%02x", (uint8_t)(this->address >> 40) & 0xff, + (uint8_t)(this->address >> 32) & 0xff, (uint8_t)(this->address >> 24) & 0xff, + (uint8_t)(this->address >> 16) & 0xff, (uint8_t)(this->address >> 8) & 0xff, + (uint8_t)(this->address >> 0) & 0xff); + std::string ret; + ret = buf; + return ret; +} + +void BLEClient::set_enabled(bool enabled) { + if (enabled == this->enabled) + return; + if (!enabled && this->state() != espbt::ClientState::Idle) { + ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str()); + auto ret = esp_ble_gattc_close(this->gattc_if, this->conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret); + } + } + this->enabled = enabled; +} + +void BLEClient::connect() { + ESP_LOGI(TAG, "Attempting BLE connection to %s", this->address_str().c_str()); + auto ret = esp_ble_gattc_open(this->gattc_if, this->remote_bda, BLE_ADDR_TYPE_PUBLIC, true); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_open error, address=%s status=%d", this->address_str().c_str(), ret); + this->set_states(espbt::ClientState::Idle); + } else { + this->set_states(espbt::ClientState::Connecting); + } +} + +void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if, + esp_ble_gattc_cb_param_t *param) { + if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id) + return; + if (event != ESP_GATTC_REG_EVT && esp_gattc_if != ESP_GATT_IF_NONE && gattc_if != this->gattc_if) + return; + + bool all_established = this->all_nodes_established(); + + switch (event) { + case ESP_GATTC_REG_EVT: { + if (param->reg.status == ESP_GATT_OK) { + ESP_LOGI(TAG, "gattc registered app id %d", this->app_id); + this->gattc_if = esp_gattc_if; + } else { + ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); + } + break; + } + case ESP_GATTC_OPEN_EVT: { + ESP_LOGI(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); + if (param->open.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); + this->set_states(espbt::ClientState::Idle); + break; + } + this->conn_id = param->open.conn_id; + auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->open.conn_id); + if (ret) { + ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%d", ret); + } + break; + } + case ESP_GATTC_CFG_MTU_EVT: { + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "cfg_mtu to %s failed, status %d", this->address_str().c_str(), param->cfg_mtu.status); + this->set_states(espbt::ClientState::Idle); + break; + } + ESP_LOGV(TAG, "cfg_mtu status %d, mtu %d", param->cfg_mtu.status, param->cfg_mtu.mtu); + esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, NULL); + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { + return; + } + ESP_LOGI(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) + delete svc; + this->services_.clear(); + this->set_states(espbt::ClientState::Idle); + break; + } + case ESP_GATTC_SEARCH_RES_EVT: { + BLEService *ble_service = new BLEService(); + ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid); + ble_service->start_handle = param->search_res.start_handle; + ble_service->end_handle = param->search_res.end_handle; + ble_service->client = this; + this->services_.push_back(ble_service); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + ESP_LOGV(TAG, "[%s] ESP_GATTC_SEARCH_CMPL_EVT", this->address_str().c_str()); + for (auto &svc : this->services_) { + ESP_LOGI(TAG, "Service UUID: %s", svc->uuid.to_string().c_str()); + ESP_LOGI(TAG, " start_handle: 0x%x end_handle: 0x%x", svc->start_handle, svc->end_handle); + svc->parse_characteristics(); + } + this->set_states(espbt::ClientState::Connected); + this->set_state(espbt::ClientState::Established); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + auto descr = this->get_config_descriptor(param->reg_for_notify.handle); + if (descr == nullptr) { + ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", param->reg_for_notify.handle); + break; + } + if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || + descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGW(TAG, "Handle 0x%x (uuid %s) is not a client config char uuid", param->reg_for_notify.handle, + descr->uuid.to_string().c_str()); + break; + } + uint8_t notify_en = 1; + auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), + ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); + } + break; + } + + default: + break; + } + for (auto *node : this->nodes_) + node->gattc_event_handler(event, esp_gattc_if, param); + + // Delete characteristics after clients have used them to save RAM. + if (!all_established && this->all_nodes_established()) { + for (auto &svc : this->services_) + delete svc; + this->services_.clear(); + } +} + +// Parse GATT values into a float for a sensor. +// Ref: https://www.bluetooth.com/specifications/assigned-numbers/format-types/ +float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { + // A length of one means a single octet value. + if (length == 0) + return 0; + if (length == 1) + return (float) ((uint8_t) value[0]); + + switch (value[0]) { + case 0x1: // boolean. + case 0x2: // 2bit. + case 0x3: // nibble. + case 0x4: // uint8. + return (float) ((uint8_t) value[1]); + case 0x5: // uint12. + case 0x6: // uint16. + return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); + case 0x7: // uint24. + return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); + case 0x8: // uint32. + return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + + (uint32_t)(value[4])); + case 0xC: // int8. + return (float) ((int8_t) value[1]); + case 0xD: // int12. + case 0xE: // int16. + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + case 0xF: // int24. + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + case 0x10: // int32. + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } + ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x", value[0]); + return NAN; +} + +BLEService *BLEClient::get_service(espbt::ESPBTUUID uuid) { + for (auto svc : this->services_) + if (svc->uuid == uuid) + return svc; + return nullptr; +} + +BLEService *BLEClient::get_service(uint16_t uuid) { return this->get_service(espbt::ESPBTUUID::from_uint16(uuid)); } + +BLECharacteristic *BLEClient::get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + return svc->get_characteristic(chr); +} + +BLECharacteristic *BLEClient::get_characteristic(uint16_t service, uint16_t chr) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr)); +} + +BLEDescriptor *BLEClient::get_config_descriptor(uint16_t handle) { + for (auto &svc : this->services_) + for (auto &chr : svc->characteristics) + if (chr->handle == handle) + for (auto &desc : chr->descriptors) + if (desc->uuid == espbt::ESPBTUUID::from_uint16(0x2902)) + return desc; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(espbt::ESPBTUUID uuid) { + for (auto &chr : this->characteristics) + if (chr->uuid == uuid) + return chr; + return nullptr; +} + +BLECharacteristic *BLEService::get_characteristic(uint16_t uuid) { + return this->get_characteristic(espbt::ESPBTUUID::from_uint16(uuid)); +} + +BLEDescriptor *BLEClient::get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr) { + auto svc = this->get_service(service); + if (svc == nullptr) + return nullptr; + auto ch = svc->get_characteristic(chr); + if (ch == nullptr) + return nullptr; + return ch->get_descriptor(descr); +} + +BLEDescriptor *BLEClient::get_descriptor(uint16_t service, uint16_t chr, uint16_t descr) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(service), espbt::ESPBTUUID::from_uint16(chr), + espbt::ESPBTUUID::from_uint16(descr)); +} + +BLEService::~BLEService() { + for (auto &chr : this->characteristics) + delete chr; +} + +void BLEService::parse_characteristics() { + uint16_t offset = 0; + esp_gattc_char_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_char( + this->client->gattc_if, this->client->conn_id, this->start_handle, this->end_handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_char error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLECharacteristic *characteristic = new BLECharacteristic(); + characteristic->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + characteristic->properties = result.properties; + characteristic->handle = result.char_handle; + characteristic->service = this; + this->characteristics.push_back(characteristic); + ESP_LOGI(TAG, " characteristic %s, handle 0x%x, properties 0x%x", characteristic->uuid.to_string().c_str(), + characteristic->handle, characteristic->properties); + characteristic->parse_descriptors(); + offset++; + } +} + +BLECharacteristic::~BLECharacteristic() { + for (auto &desc : this->descriptors) + delete desc; +} + +void BLECharacteristic::parse_descriptors() { + uint16_t offset = 0; + esp_gattc_descr_elem_t result; + + while (true) { + uint16_t count = 1; + esp_gatt_status_t status = esp_ble_gattc_get_all_descr( + this->service->client->gattc_if, this->service->client->conn_id, this->handle, &result, &count, offset); + if (status == ESP_GATT_INVALID_OFFSET || status == ESP_GATT_NOT_FOUND) { + break; + } + if (status != ESP_GATT_OK) { + ESP_LOGW(TAG, "esp_ble_gattc_get_all_descr error, status=%d", status); + break; + } + if (count == 0) { + break; + } + + BLEDescriptor *desc = new BLEDescriptor(); + desc->uuid = espbt::ESPBTUUID::from_uuid(result.uuid); + desc->handle = result.handle; + desc->characteristic = this; + this->descriptors.push_back(desc); + ESP_LOGI(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); + offset++; + } +} + +BLEDescriptor *BLECharacteristic::get_descriptor(espbt::ESPBTUUID uuid) { + for (auto &desc : this->descriptors) + if (desc->uuid == uuid) + return desc; + return nullptr; +} +BLEDescriptor *BLECharacteristic::get_descriptor(uint16_t uuid) { + return this->get_descriptor(espbt::ESPBTUUID::from_uint16(uuid)); +} + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h new file mode 100644 index 0000000000..d99753e9da --- /dev/null +++ b/esphome/components/ble_client/ble_client.h @@ -0,0 +1,140 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include +#include +#include +#include + +namespace espbt = esphome::esp32_ble_tracker; + +namespace esphome { +namespace ble_client { + +class BLEClient; +class BLEService; +class BLECharacteristic; + +class BLEClientNode { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void loop() = 0; + void set_address(uint64_t address) { address_ = address; } + espbt::ESPBTClient *client; + // This should be transitioned to Established once the node no longer needs + // the services/descriptors/characteristics of the parent client. This will + // allow some memory to be freed. + espbt::ClientState node_state; + + BLEClient *parent() { return this->parent_; } + void set_ble_client_parent(BLEClient *parent) { this->parent_ = parent; } + + protected: + BLEClient *parent_; + uint64_t address_; +}; + +class BLEDescriptor { + public: + espbt::ESPBTUUID uuid; + uint16_t handle; + + BLECharacteristic *characteristic; +}; + +class BLECharacteristic { + public: + ~BLECharacteristic(); + espbt::ESPBTUUID uuid; + uint16_t handle; + esp_gatt_char_prop_t properties; + std::vector descriptors; + void parse_descriptors(); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID uuid); + BLEDescriptor *get_descriptor(uint16_t uuid); + + BLEService *service; +}; + +class BLEService { + public: + ~BLEService(); + espbt::ESPBTUUID uuid; + uint16_t start_handle; + uint16_t end_handle; + std::vector characteristics; + BLEClient *client; + void parse_characteristics(); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID uuid); + BLECharacteristic *get_characteristic(uint16_t uuid); +}; + +class BLEClient : public espbt::ESPBTClient, public Component { + public: + void setup() override; + void dump_config() override; + void loop() override; + + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + bool parse_device(const espbt::ESPBTDevice &device) override; + void on_scan_end() override {} + void connect(); + + void set_address(uint64_t address) { this->address = address; } + + void set_enabled(bool enabled); + + void register_ble_node(BLEClientNode *node) { + node->client = this; + node->set_ble_client_parent(this); + this->nodes_.push_back(node); + } + + BLEService *get_service(espbt::ESPBTUUID uuid); + BLEService *get_service(uint16_t uuid); + BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr); + BLECharacteristic *get_characteristic(uint16_t service, uint16_t chr); + BLEDescriptor *get_descriptor(espbt::ESPBTUUID service, espbt::ESPBTUUID chr, espbt::ESPBTUUID descr); + BLEDescriptor *get_descriptor(uint16_t service, uint16_t chr, uint16_t descr); + // Get the configuration descriptor for the given characteristic handle. + BLEDescriptor *get_config_descriptor(uint16_t handle); + + float parse_char_value(uint8_t *value, uint16_t length); + + int gattc_if; + esp_bd_addr_t remote_bda; + uint16_t conn_id; + uint64_t address; + bool enabled; + std::string address_str() const; + + protected: + void set_states(espbt::ClientState st) { + this->set_state(st); + for (auto &node : nodes_) + node->node_state = st; + } + bool all_nodes_established() { + if (this->state() != espbt::ClientState::Established) + return false; + for (auto &node : nodes_) + if (node->node_state != espbt::ClientState::Established) + return false; + return true; + } + + std::vector nodes_; + std::vector services_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/__init__.py b/esphome/components/ble_client/sensor/__init__.py new file mode 100644 index 0000000000..27eca87a37 --- /dev/null +++ b/esphome/components/ble_client/sensor/__init__.py @@ -0,0 +1,115 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor, ble_client, esp32_ble_tracker +from esphome.const import ( + DEVICE_CLASS_EMPTY, + CONF_ID, + UNIT_EMPTY, + ICON_EMPTY, + CONF_TRIGGER_ID, + CONF_SERVICE_UUID, +) +from esphome import automation +from .. import ble_client_ns + +DEPENDENCIES = ["ble_client"] + +CONF_CHARACTERISTIC_UUID = "characteristic_uuid" +CONF_DESCRIPTOR_UUID = "descriptor_uuid" + +CONF_NOTIFY = "notify" +CONF_ON_NOTIFY = "on_notify" + +BLESensor = ble_client_ns.class_( + "BLESensor", sensor.Sensor, cg.PollingComponent, ble_client.BLEClientNode +) +BLESensorNotifyTrigger = ble_client_ns.class_( + "BLESensorNotifyTrigger", automation.Trigger.template(cg.float_) +) + +CONFIG_SCHEMA = cv.All( + sensor.sensor_schema(UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY) + .extend( + { + cv.GenerateID(): cv.declare_id(BLESensor), + cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, + cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, + cv.Optional(CONF_NOTIFY, default=False): cv.boolean, + cv.Optional(CONF_ON_NOTIFY): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + BLESensorNotifyTrigger + ), + } + ), + } + ) + .extend(cv.polling_component_schema("60s")) + .extend(ble_client.BLE_CLIENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid32_format): + cg.add( + var.set_service_uuid32(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) + ) + elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_SERVICE_UUID]) + cg.add(var.set_service_uuid128(uuid128)) + + if len(config[CONF_CHARACTERISTIC_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_char_uuid16( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_char_uuid32( + esp32_ble_tracker.as_hex(config[CONF_CHARACTERISTIC_UUID]) + ) + ) + elif len(config[CONF_CHARACTERISTIC_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_CHARACTERISTIC_UUID]) + cg.add(var.set_char_uuid128(uuid128)) + + if CONF_DESCRIPTOR_UUID in config: + if len(config[CONF_DESCRIPTOR_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): + cg.add( + var.set_descr_uuid16( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid32_format + ): + cg.add( + var.set_descr_uuid32( + esp32_ble_tracker.as_hex(config[CONF_DESCRIPTOR_UUID]) + ) + ) + elif len(config[CONF_DESCRIPTOR_UUID]) == len( + esp32_ble_tracker.bt_uuid128_format + ): + uuid128 = esp32_ble_tracker.as_hex_array(config[CONF_DESCRIPTOR_UUID]) + cg.add(var.set_descr_uuid128(uuid128)) + + yield cg.register_component(var, config) + yield ble_client.register_ble_node(var, config) + cg.add(var.set_enable_notify(config[CONF_NOTIFY])) + yield sensor.register_sensor(var, config) + for conf in config.get(CONF_ON_NOTIFY, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + yield ble_client.register_ble_node(trigger, config) + yield automation.build_automation(trigger, [(float, "x")], conf) diff --git a/esphome/components/ble_client/sensor/automation.h b/esphome/components/ble_client/sensor/automation.h new file mode 100644 index 0000000000..a528493947 --- /dev/null +++ b/esphome/components/ble_client/sensor/automation.h @@ -0,0 +1,37 @@ +#pragma once + +#include "esphome/core/automation.h" +#include "esphome/components/ble_client/sensor/ble_sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +class BLESensorNotifyTrigger : public Trigger, public BLESensor { + public: + explicit BLESensorNotifyTrigger(BLESensor *sensor) { sensor_ = sensor; } + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->sensor_->node_state = espbt::ClientState::Established; + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->sensor_->parent()->conn_id || param->notify.handle != this->sensor_->handle) + break; + this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len)); + } + default: + break; + } + } + + protected: + BLESensor *sensor_; +}; + +} // namespace ble_client +} // namespace esphome + +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp new file mode 100644 index 0000000000..d1b4801021 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -0,0 +1,129 @@ +#include "ble_sensor.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_sensor"; + +uint32_t BLESensor::hash_base() { return 343459825UL; } + +void BLESensor::loop() {} + +void BLESensor::dump_config() { + LOG_SENSOR("", "BLE Sensor", this); + ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str()); + ESP_LOGCONFIG(TAG, " Service UUID : %s", this->service_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Characteristic UUID: %s", this->char_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Descriptor UUID : %s", this->descr_uuid_.to_string().c_str()); + ESP_LOGCONFIG(TAG, " Notifications : %s", YESNO(this->notify_)); + LOG_UPDATE_INTERVAL(this); +} + +void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_OPEN_EVT: { + if (param->open.status == ESP_GATT_OK) { + ESP_LOGW(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + break; + } + break; + } + case ESP_GATTC_DISCONNECT_EVT: { + ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str()); + this->status_set_warning(); + this->publish_state(NAN); + break; + } + case ESP_GATTC_SEARCH_CMPL_EVT: { + this->handle = 0; + auto chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); + if (chr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), + this->char_uuid_.to_string().c_str()); + break; + } + this->handle = chr->handle; + if (this->descr_uuid_.get_uuid().len > 0) { + auto descr = chr->get_descriptor(this->descr_uuid_); + if (descr == nullptr) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s", + this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(), + this->descr_uuid_.to_string().c_str()); + break; + } + this->handle = descr->handle; + } + if (this->notify_) { + auto status = + esp_ble_gattc_register_for_notify(this->parent()->gattc_if, this->parent()->remote_bda, chr->handle); + if (status) { + ESP_LOGW(TAG, "esp_ble_gattc_register_for_notify failed, status=%d", status); + } + } else { + this->node_state = espbt::ClientState::Established; + } + break; + } + case ESP_GATTC_READ_CHAR_EVT: { + if (param->read.conn_id != this->parent()->conn_id) + break; + if (param->read.status != ESP_GATT_OK) { + ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); + break; + } + if (param->read.handle == this->handle) { + this->status_clear_warning(); + this->publish_state((float) param->read.value[0]); + } + break; + } + case ESP_GATTC_NOTIFY_EVT: { + if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) + break; + ESP_LOGI(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + param->notify.handle, param->notify.value[0]); + this->publish_state((float) param->notify.value[0]); + break; + } + case ESP_GATTC_REG_FOR_NOTIFY_EVT: { + this->node_state = espbt::ClientState::Established; + break; + } + default: + break; + } +} + +void BLESensor::update() { + if (this->node_state != espbt::ClientState::Established) { + ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str()); + return; + } + if (this->handle == 0) { + ESP_LOGW(TAG, "[%s] Cannot poll, no service or characteristic found", this->get_name().c_str()); + return; + } + + auto status = + esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle, ESP_GATT_AUTH_REQ_NONE); + if (status) { + this->status_set_warning(); + this->publish_state(NAN); + ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status); + } +} + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/sensor/ble_sensor.h b/esphome/components/ble_client/sensor/ble_sensor.h new file mode 100644 index 0000000000..e3a8a8ea25 --- /dev/null +++ b/esphome/components/ble_client/sensor/ble_sensor.h @@ -0,0 +1,46 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/sensor/sensor.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode { + public: + void loop() override; + void update() override; + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::DATA; } + void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_char_uuid16(uint16_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); } + void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); } + void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } + void set_enable_notify(bool notify) { this->notify_ = notify; } + uint16_t handle; + + protected: + uint32_t hash_base() override; + bool notify_; + espbt::ESPBTUUID service_uuid_; + espbt::ESPBTUUID char_uuid_; + espbt::ESPBTUUID descr_uuid_; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/__init__.py b/esphome/components/ble_client/switch/__init__.py new file mode 100644 index 0000000000..acc8683407 --- /dev/null +++ b/esphome/components/ble_client/switch/__init__.py @@ -0,0 +1,30 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import switch, ble_client +from esphome.const import CONF_ICON, CONF_ID, CONF_INVERTED, ICON_BLUETOOTH +from .. import ble_client_ns + +BLEClientSwitch = ble_client_ns.class_( + "BLEClientSwitch", switch.Switch, cg.Component, ble_client.BLEClientNode +) + +CONFIG_SCHEMA = ( + switch.SWITCH_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(BLEClientSwitch), + cv.Optional(CONF_INVERTED): cv.invalid( + "BLE client switches do not support inverted mode!" + ), + cv.Optional(CONF_ICON, default=ICON_BLUETOOTH): switch.icon, + } + ) + .extend(ble_client.BLE_CLIENT_SCHEMA) + .extend(cv.COMPONENT_SCHEMA) +) + + +def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + yield cg.register_component(var, config) + yield switch.register_switch(var, config) + yield ble_client.register_ble_node(var, config) diff --git a/esphome/components/ble_client/switch/ble_switch.cpp b/esphome/components/ble_client/switch/ble_switch.cpp new file mode 100644 index 0000000000..ef7b64be66 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.cpp @@ -0,0 +1,39 @@ +#include "ble_switch.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +#ifdef ARDUINO_ARCH_ESP32 + +namespace esphome { +namespace ble_client { + +static const char *TAG = "ble_switch"; + +void BLEClientSwitch::write_state(bool state) { + this->parent_->set_enabled(state); + this->publish_state(state); +} + +void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + switch (event) { + case ESP_GATTC_REG_EVT: + this->publish_state(this->parent_->enabled); + break; + case ESP_GATTC_OPEN_EVT: + this->node_state = espbt::ClientState::Established; + break; + case ESP_GATTC_DISCONNECT_EVT: + this->node_state = espbt::ClientState::Idle; + this->publish_state(this->parent_->enabled); + break; + default: + break; + } +} + +void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); } + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/ble_client/switch/ble_switch.h b/esphome/components/ble_client/switch/ble_switch.h new file mode 100644 index 0000000000..f91af533f1 --- /dev/null +++ b/esphome/components/ble_client/switch/ble_switch.h @@ -0,0 +1,30 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/ble_client/ble_client.h" +#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" +#include "esphome/components/switch/switch.h" + +#ifdef ARDUINO_ARCH_ESP32 +#include + +namespace esphome { +namespace ble_client { + +namespace espbt = esphome::esp32_ble_tracker; + +class BLEClientSwitch : public switch_::Switch, public Component, public BLEClientNode { + public: + void dump_config() override; + void loop() override {} + void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) override; + float get_setup_priority() const override { return setup_priority::DATA; } + + protected: + void write_state(bool state) override; +}; + +} // namespace ble_client +} // namespace esphome +#endif diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2e567b49bc..8726ab3e8d 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -26,6 +26,7 @@ CONF_WINDOW = "window" CONF_ACTIVE = "active" esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) +ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") ESPBTDeviceListener = esp32_ble_tracker_ns.class_("ESPBTDeviceListener") ESPBTDevice = esp32_ble_tracker_ns.class_("ESPBTDevice") ESPBTDeviceConstRef = ESPBTDevice.operator("ref").operator("const") @@ -220,3 +221,10 @@ def register_ble_device(var, config): paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) cg.add(paren.register_listener(var)) yield var + + +@coroutine +def register_client(var, config): + paren = yield cg.get_variable(config[CONF_ESP32_BLE_ID]) + cg.add(paren.register_client(var)) + yield var diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp index b2d303da15..7a5b023387 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.cpp @@ -48,7 +48,23 @@ void ESP32BLETracker::setup() { } void ESP32BLETracker::loop() { - if (xSemaphoreTake(this->scan_end_lock_, 0L)) { + BLEEvent *ble_event = this->ble_events_.pop(); + while (ble_event != nullptr) { + if (ble_event->type_) + this->real_gattc_event_handler(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, + &ble_event->event_.gattc.gattc_param); + else + this->real_gap_event_handler(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); + delete ble_event; + ble_event = this->ble_events_.pop(); + } + + bool connecting = false; + for (auto *client : this->clients_) { + if (client->state() == ClientState::Connecting || client->state() == ClientState::Discovered) + connecting = true; + } + if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { xSemaphoreGive(this->scan_end_lock_); global_esp32_ble_tracker->start_scan(false); } @@ -69,6 +85,17 @@ void ESP32BLETracker::loop() { if (listener->parse_device(device)) found = true; + for (auto *client : this->clients_) + if (client->parse_device(device)) { + found = true; + if (client->state() == ClientState::Discovered) { + esp_ble_gap_stop_scanning(); + if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { + xSemaphoreGive(this->scan_end_lock_); + } + } + } + if (!found) { this->print_bt_device_info(device); } @@ -122,6 +149,11 @@ bool ESP32BLETracker::ble_setup() { ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err); return false; } + err = esp_ble_gattc_register_callback(ESP32BLETracker::gattc_event_handler); + if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err); + return false; + } // Empty name esp_ble_gap_set_device_name(""); @@ -166,7 +198,17 @@ void ESP32BLETracker::start_scan(bool first) { }); } +void ESP32BLETracker::register_client(ESPBTClient *client) { + client->app_id = ++this->app_id_; + this->clients_.push_back(client); +} + void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { + BLEEvent *gap_event = new BLEEvent(event, param); + global_esp32_ble_tracker->ble_events_.push(gap_event); +} + +void ESP32BLETracker::real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { switch (event) { case ESP_GAP_BLE_SCAN_RESULT_EVT: global_esp32_ble_tracker->gap_scan_result(param->scan_rst); @@ -177,6 +219,9 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: global_esp32_ble_tracker->gap_scan_start_complete(param->scan_start_cmpl); break; + case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: + global_esp32_ble_tracker->gap_scan_stop_complete(param->scan_stop_cmpl); + break; default: break; } @@ -190,6 +235,10 @@ void ESP32BLETracker::gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_ this->scan_start_failed_ = param.status; } +void ESP32BLETracker::gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) { + xSemaphoreGive(this->scan_end_lock_); +} + void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { if (xSemaphoreTake(this->scan_result_lock_, 0L)) { @@ -203,6 +252,19 @@ void ESP32BLETracker::gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_res } } +void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + BLEEvent *gattc_event = new BLEEvent(event, gattc_if, param); + global_esp32_ble_tracker->ble_events_.push(gattc_event); +} + +void ESP32BLETracker::real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) { + for (auto *client : global_esp32_ble_tracker->clients_) { + client->gattc_event_handler(event, gattc_if, param); + } +} + ESPBTUUID::ESPBTUUID() : uuid_() {} ESPBTUUID ESPBTUUID::from_uint16(uint16_t uuid) { ESPBTUUID ret; @@ -223,6 +285,15 @@ ESPBTUUID ESPBTUUID::from_raw(const uint8_t *data) { ret.uuid_.uuid.uuid128[i] = data[i]; return ret; } +ESPBTUUID ESPBTUUID::from_uuid(esp_bt_uuid_t uuid) { + ESPBTUUID ret; + ret.uuid_.len = uuid.len; + ret.uuid_.uuid.uuid16 = uuid.uuid.uuid16; + ret.uuid_.uuid.uuid32 = uuid.uuid.uuid32; + for (size_t i = 0; i < ESP_UUID_LEN_128; i++) + ret.uuid_.uuid.uuid128[i] = uuid.uuid.uuid128[i]; + return ret; +} ESPBTUUID ESPBTUUID::as_128bit() const { if (this->uuid_.len == ESP_UUID_LEN_128) { return *this; @@ -289,16 +360,21 @@ std::string ESPBTUUID::to_string() { char sbuf[64]; switch (this->uuid_.len) { case ESP_UUID_LEN_16: - sprintf(sbuf, "%02X:%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); + sprintf(sbuf, "0x%02X%02X", this->uuid_.uuid.uuid16 >> 8, this->uuid_.uuid.uuid16 & 0xff); break; case ESP_UUID_LEN_32: - sprintf(sbuf, "%02X:%02X:%02X:%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), + sprintf(sbuf, "0x%02X%02X%02X%02X", this->uuid_.uuid.uuid32 >> 24, (this->uuid_.uuid.uuid32 >> 16 & 0xff), (this->uuid_.uuid.uuid32 >> 8 & 0xff), this->uuid_.uuid.uuid32 & 0xff); break; default: case ESP_UUID_LEN_128: - for (uint8_t i = 0; i < 16; i++) - sprintf(sbuf + i * 3, "%02X:", this->uuid_.uuid.uuid128[i]); + char *bpos = sbuf; + for (int8_t i = 15; i >= 0; i--) { + sprintf(bpos, "%02X", this->uuid_.uuid.uuid128[i]); + bpos += 2; + if (i == 3 || i == 5 || i == 7 || i == 9) + sprintf(bpos++, "-"); + } sbuf[47] = '\0'; break; } diff --git a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h index eef7930b78..6f0c28a73c 100644 --- a/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h +++ b/esphome/components/esp32_ble_tracker/esp32_ble_tracker.h @@ -2,12 +2,14 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" +#include "queue.h" #ifdef ARDUINO_ARCH_ESP32 #include #include #include +#include #include namespace esphome { @@ -23,6 +25,8 @@ class ESPBTUUID { static ESPBTUUID from_raw(const uint8_t *data); + static ESPBTUUID from_uuid(esp_bt_uuid_t uuid); + ESPBTUUID as_128bit() const; bool contains(uint8_t data1, uint8_t data2) const; @@ -135,6 +139,32 @@ class ESPBTDeviceListener { ESP32BLETracker *parent_{nullptr}; }; +enum class ClientState { + // Connection is idle, no device detected. + Idle, + // Device advertisement found. + Discovered, + // Connection in progress. + Connecting, + // Initial connection established. + Connected, + // The client and sub-clients have completed setup. + Established, +}; + +class ESPBTClient : public ESPBTDeviceListener { + public: + virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, + esp_ble_gattc_cb_param_t *param) = 0; + virtual void connect() = 0; + void set_state(ClientState st) { this->state_ = st; } + ClientState state() const { return state_; } + int app_id; + + protected: + ClientState state_; +}; + class ESP32BLETracker : public Component { public: void set_scan_duration(uint32_t scan_duration) { scan_duration_ = scan_duration; } @@ -153,6 +183,8 @@ class ESP32BLETracker : public Component { this->listeners_.push_back(listener); } + void register_client(ESPBTClient *client); + void print_bt_device_info(const ESPBTDevice &device); protected: @@ -162,16 +194,26 @@ class ESP32BLETracker : public Component { void start_scan(bool first); /// Callback that will handle all GAP events and redistribute them to other callbacks. static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); + void real_gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); /// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received. void gap_scan_result(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received. void gap_scan_set_param_complete(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m); /// Called when a `ESP_GAP_BLE_SCAN_START_COMPLETE_EVT` event is received. void gap_scan_start_complete(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m); + /// Called when a `ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT` event is received. + void gap_scan_stop_complete(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m); + + int app_id_; + /// Callback that will handle all GATTC events and redistribute them to other callbacks. + static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); + void real_gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); /// Vector of addresses that have already been printed in print_bt_device_info std::vector already_discovered_; std::vector listeners_; + /// Client parameters. + std::vector clients_; /// A structure holding the ESP BLE scan parameters. esp_ble_scan_params_t scan_params_; /// The interval in seconds to perform scans. @@ -185,6 +227,8 @@ class ESP32BLETracker : public Component { esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_buffer_[16]; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; + + Queue ble_events_; }; extern ESP32BLETracker *global_esp32_ble_tracker; diff --git a/esphome/components/esp32_ble_tracker/queue.h b/esphome/components/esp32_ble_tracker/queue.h new file mode 100644 index 0000000000..6f36cf874d --- /dev/null +++ b/esphome/components/esp32_ble_tracker/queue.h @@ -0,0 +1,96 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" + +#include +#include + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +/* + * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather + * than trying to deal wth various locking strategies, all incoming GAP and GATT + * events will simply be placed on a semaphore guarded queue. The next time the + * component runs loop(), these events are popped off the queue and handed at + * this safer time. + */ + +namespace esphome { +namespace esp32_ble_tracker { + +template class Queue { + public: + Queue() { m = xSemaphoreCreateMutex(); } + + void push(T *element) { + if (element == nullptr) + return; + if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { + q.push(element); + xSemaphoreGive(m); + } + } + + T *pop() { + T *element = nullptr; + + if (xSemaphoreTake(m, 5L / portTICK_PERIOD_MS)) { + if (!q.empty()) { + element = q.front(); + q.pop(); + } + xSemaphoreGive(m); + } + return element; + } + + protected: + std::queue q; + SemaphoreHandle_t m; +}; + +// Received GAP and GATTC events are only queued, and get processed in the main loop(). +// This class stores each event in a single type. +class BLEEvent { + public: + BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { + this->event_.gap.gap_event = e; + memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); + this->type_ = 0; + }; + + BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { + this->event_.gattc.gattc_event = e; + this->event_.gattc.gattc_if = i; + memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); + // Need to also make a copy of notify event data. + if (e == ESP_GATTC_NOTIFY_EVT) { + memcpy(this->event_.gattc.notify_data, p->notify.value, p->notify.value_len); + this->event_.gattc.gattc_param.notify.value = this->event_.gattc.notify_data; + } + this->type_ = 1; + }; + + union { + struct gap_event { + esp_gap_ble_cb_event_t gap_event; + esp_ble_gap_cb_param_t gap_param; + } gap; + + struct gattc_event { + esp_gattc_cb_event_t gattc_event; + esp_gatt_if_t gattc_if; + esp_ble_gattc_cb_param_t gattc_param; + uint8_t notify_data[64]; + } gattc; + } event_; + uint8_t type_; // 0=gap 1=gattc +}; + +} // namespace esp32_ble_tracker +} // namespace esphome + +#endif diff --git a/esphome/config.py b/esphome/config.py index 0c8e51fdce..5a135b3e37 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -49,7 +49,7 @@ class ComponentManifest: return getattr(self.module, "CONFIG_SCHEMA", None) @property - def is_multi_conf(self): + def multi_conf(self): return getattr(self.module, "MULTI_CONF", False) @property @@ -194,7 +194,7 @@ _COMPONENT_CACHE["esphome"] = ComponentManifest( def iter_components(config): for domain, conf in config.items(): component = get_component(domain) - if component.is_multi_conf: + if component.multi_conf: for conf_ in conf: yield domain, component, conf_ else: @@ -651,9 +651,16 @@ def validate_config(config, command_line_substitutions): ) continue - if comp.is_multi_conf: + if comp.multi_conf: if not isinstance(conf, list): result[domain] = conf = [conf] + if not isinstance(comp.multi_conf, bool) and len(conf) > comp.multi_conf: + result.add_str_error( + "Component {} supports a maximum of {} " + "entries ({} found).".format(domain, comp.multi_conf, len(conf)), + path, + ) + continue for i, part_conf in enumerate(conf): validate_queue.append((path + [i], part_conf, comp)) continue diff --git a/esphome/const.py b/esphome/const.py index eef9252a5c..1d992d12e9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -358,6 +358,8 @@ CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE = "on_ble_manufacturer_data_advertise" CONF_ON_BLE_SERVICE_DATA_ADVERTISE = "on_ble_service_data_advertise" CONF_ON_BOOT = "on_boot" CONF_ON_CLICK = "on_click" +CONF_ON_CONNECT = "on_connect" +CONF_ON_DISCONNECT = "on_disconnect" CONF_ON_DOUBLE_CLICK = "on_double_click" CONF_ON_ENROLLMENT_DONE = "on_enrollment_done" CONF_ON_ENROLLMENT_FAILED = "on_enrollment_failed" @@ -620,6 +622,7 @@ ICON_ACCOUNT = "mdi:account" ICON_ACCOUNT_CHECK = "mdi:account-check" ICON_ARROW_EXPAND_VERTICAL = "mdi:arrow-expand-vertical" ICON_BATTERY = "mdi:battery" +ICON_BLUETOOTH = "mdi:bluetooth" ICON_BRIEFCASE_DOWNLOAD = "mdi:briefcase-download" ICON_BRIGHTNESS_5 = "mdi:brightness-5" ICON_BUG = "mdi:bug" diff --git a/tests/test1.yaml b/tests/test1.yaml index 8a93de09b1..167c0b4902 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -9,6 +9,8 @@ esphome: name_add_mac_suffix: true platform: ESP32 board: nodemcu-32s + platformio_options: + board_build.partitions: huge_app.csv on_boot: priority: 150.0 then: @@ -235,6 +237,19 @@ wled: adalight: +esp32_ble_tracker: + +ble_client: + - mac_address: AA:BB:CC:DD:EE:FF + id: ble_foo + - mac_address: 11:22:33:44:55:66 + id: ble_blah + on_connect: + then: + - switch.turn_on: ble1_status + on_disconnect: + then: + - switch.turn_on: ble1_status mcp23s08: - id: 'mcp23s08_hub' cs_pin: GPIO12 @@ -246,6 +261,18 @@ mcp23s17: deviceaddress: 1 sensor: + - platform: ble_client + ble_client_id: ble_foo + name: "Green iTag btn" + service_uuid: 'ffe0' + characteristic_uuid: 'ffe1' + descriptor_uuid: 'ffe2' + notify: true + update_interval: never + on_notify: + then: + - lambda: |- + ESP_LOGD("green_btn", "Button was pressed, val%f", x); - platform: adc pin: A0 name: 'Living Room Brightness' @@ -1722,6 +1749,8 @@ switch: # Use pin number 0 number: 0 inverted: False + - platform: template + id: ble1_status fan: - platform: binary From 07b3327102f7457f960940a4f5ceae8abb4686cd Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 3 May 2021 11:51:10 +1200 Subject: [PATCH 42/62] Update email addresses (#1733) --- CODE_OF_CONDUCT.md | 22 +++++++++++----------- setup.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index c5be227278..b91a3b4f83 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo Examples of behavior that contributes to creating a positive environment include: -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members Examples of unacceptable behavior by participants include: -* The use of sexualized language or imagery and unwelcome sexual attention or advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities @@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@otto-winter.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esphome@nabucasa.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/setup.py b/setup.py index 6e4d4b06be..44a5965887 100755 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ PROJECT_LICENSE = "MIT" PROJECT_AUTHOR = "ESPHome" PROJECT_COPYRIGHT = "2019, ESPHome" PROJECT_URL = "https://esphome.io/" -PROJECT_EMAIL = "contact@esphome.io" +PROJECT_EMAIL = "esphome@nabucasa.com" PROJECT_GITHUB_USERNAME = "esphome" PROJECT_GITHUB_REPOSITORY = "esphome" From dd4fb85170ca1063cd9070249574ec0d7cf38eea Mon Sep 17 00:00:00 2001 From: buxtronix Date: Tue, 4 May 2021 06:51:03 +1000 Subject: [PATCH 43/62] Ble client fixes (#1739) Co-authored-by: Ben Buxton --- esphome/components/ble_client/ble_client.cpp | 38 ++++++++++++------- esphome/components/ble_client/ble_client.h | 4 +- .../ble_client/sensor/ble_sensor.cpp | 4 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/esphome/components/ble_client/ble_client.cpp b/esphome/components/ble_client/ble_client.cpp index 391f6425e2..3819a6e560 100644 --- a/esphome/components/ble_client/ble_client.cpp +++ b/esphome/components/ble_client/ble_client.cpp @@ -102,7 +102,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es switch (event) { case ESP_GATTC_REG_EVT: { if (param->reg.status == ESP_GATT_OK) { - ESP_LOGI(TAG, "gattc registered app id %d", this->app_id); + ESP_LOGV(TAG, "gattc registered app id %d", this->app_id); this->gattc_if = esp_gattc_if; } else { ESP_LOGE(TAG, "gattc app registration failed id=%d code=%d", param->reg.app_id, param->reg.status); @@ -110,7 +110,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es break; } case ESP_GATTC_OPEN_EVT: { - ESP_LOGI(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); + ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); if (param->open.status != ESP_GATT_OK) { ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); this->set_states(espbt::ClientState::Idle); @@ -137,7 +137,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es if (memcmp(param->disconnect.remote_bda, this->remote_bda, 6) != 0) { return; } - ESP_LOGI(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); + ESP_LOGV(TAG, "[%s] ESP_GATTC_DISCONNECT_EVT", this->address_str().c_str()); for (auto &svc : this->services_) delete svc; this->services_.clear(); @@ -216,24 +216,36 @@ float BLEClient::parse_char_value(uint8_t *value, uint16_t length) { return (float) ((uint8_t) value[1]); case 0x5: // uint12. case 0x6: // uint16. - return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); + if (length > 2) { + return (float) ((uint16_t)(value[1] << 8) + (uint16_t) value[2]); + } case 0x7: // uint24. - return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); + if (length > 3) { + return (float) ((uint32_t)(value[1] << 16) + (uint32_t)(value[2] << 8) + (uint32_t)(value[3])); + } case 0x8: // uint32. - return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + - (uint32_t)(value[4])); + if (length > 4) { + return (float) ((uint32_t)(value[1] << 24) + (uint32_t)(value[2] << 16) + (uint32_t)(value[3] << 8) + + (uint32_t)(value[4])); + } case 0xC: // int8. return (float) ((int8_t) value[1]); case 0xD: // int12. case 0xE: // int16. - return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + if (length > 2) { + return (float) ((int16_t)(value[1] << 8) + (int16_t) value[2]); + } case 0xF: // int24. - return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + if (length > 3) { + return (float) ((int32_t)(value[1] << 16) + (int32_t)(value[2] << 8) + (int32_t)(value[3])); + } case 0x10: // int32. - return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + - (int32_t)(value[4])); + if (length > 4) { + return (float) ((int32_t)(value[1] << 24) + (int32_t)(value[2] << 16) + (int32_t)(value[3] << 8) + + (int32_t)(value[4])); + } } - ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x", value[0]); + ESP_LOGW(TAG, "Cannot parse characteristic value of type 0x%x length %d", value[0], length); return NAN; } @@ -359,7 +371,7 @@ void BLECharacteristic::parse_descriptors() { desc->handle = result.handle; desc->characteristic = this; this->descriptors.push_back(desc); - ESP_LOGI(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); + ESP_LOGV(TAG, " descriptor %s, handle 0x%x", desc->uuid.to_string().c_str(), desc->handle); offset++; } } diff --git a/esphome/components/ble_client/ble_client.h b/esphome/components/ble_client/ble_client.h index d99753e9da..203acc181f 100644 --- a/esphome/components/ble_client/ble_client.h +++ b/esphome/components/ble_client/ble_client.h @@ -12,11 +12,11 @@ #include #include -namespace espbt = esphome::esp32_ble_tracker; - namespace esphome { namespace ble_client { +namespace espbt = esphome::esp32_ble_tracker; + class BLEClient; class BLEService; class BLECharacteristic; diff --git a/esphome/components/ble_client/sensor/ble_sensor.cpp b/esphome/components/ble_client/sensor/ble_sensor.cpp index d1b4801021..ef1d6c120f 100644 --- a/esphome/components/ble_client/sensor/ble_sensor.cpp +++ b/esphome/components/ble_client/sensor/ble_sensor.cpp @@ -30,7 +30,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga switch (event) { case ESP_GATTC_OPEN_EVT: { if (param->open.status == ESP_GATT_OK) { - ESP_LOGW(TAG, "[%s] Connected successfully!", this->get_name().c_str()); + ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str()); break; } break; @@ -91,7 +91,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga case ESP_GATTC_NOTIFY_EVT: { if (param->notify.conn_id != this->parent()->conn_id || param->notify.handle != this->handle) break; - ESP_LOGI(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), + ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(), param->notify.handle, param->notify.value[0]); this->publish_state((float) param->notify.value[0]); break; From 98f0d75180e7eedb67a41b481427e7beef0472e7 Mon Sep 17 00:00:00 2001 From: 0x0a11c0de <77504775+0x0a11c0de@users.noreply.github.com> Date: Mon, 3 May 2021 19:11:24 -0700 Subject: [PATCH 44/62] Fix #1940: Implement speed_count in TuyaFan (#1654) Co-authored-by: Frank Riley --- esphome/components/tuya/fan/__init__.py | 15 ++++++++------- esphome/components/tuya/fan/tuya_fan.cpp | 12 +++++------- esphome/components/tuya/fan/tuya_fan.h | 4 ++-- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index 8615f3ae85..b5e3e579be 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -1,7 +1,7 @@ from esphome.components import fan import esphome.config_validation as cv import esphome.codegen as cg -from esphome.const import CONF_OUTPUT_ID, CONF_SWITCH_DATAPOINT +from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT from .. import tuya_ns, CONF_TUYA_ID, Tuya DEPENDENCIES = ["tuya"] @@ -19,6 +19,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), } ).extend(cv.COMPONENT_SCHEMA), cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT), @@ -26,13 +27,13 @@ CONFIG_SCHEMA = cv.All( def to_code(config): - var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) - yield cg.register_component(var, config) + parent = yield cg.get_variable(config[CONF_TUYA_ID]) + state = yield fan.create_fan_state(config) - paren = yield cg.get_variable(config[CONF_TUYA_ID]) - fan_ = yield fan.create_fan_state(config) - cg.add(var.set_tuya_parent(paren)) - cg.add(var.set_fan(fan_)) + var = cg.new_Pvariable( + config[CONF_OUTPUT_ID], parent, state, config[CONF_SPEED_COUNT] + ) + yield cg.register_component(var, config) if CONF_SPEED_DATAPOINT in config: cg.add(var.set_speed_id(config[CONF_SPEED_DATAPOINT])) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 718f292f5b..26e5de253d 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -8,18 +8,15 @@ namespace tuya { static const char *TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false, 3); + auto traits = + fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false, this->speed_count_); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { this->parent_->register_listener(*this->speed_id_, [this](TuyaDatapoint datapoint) { auto call = this->fan_->make_call(); - if (datapoint.value_enum == 0x0) - call.set_speed(1); - else if (datapoint.value_enum == 0x1) - call.set_speed(2); - else if (datapoint.value_enum == 0x2) - call.set_speed(3); + if (datapoint.value_enum < this->speed_count_) + call.set_speed(datapoint.value_enum + 1); else ESP_LOGCONFIG(TAG, "Speed has invalid value %d", datapoint.value_enum); ESP_LOGD(TAG, "MCU reported speed of: %d", datapoint.value_enum); @@ -47,6 +44,7 @@ void TuyaFan::setup() { void TuyaFan::dump_config() { ESP_LOGCONFIG(TAG, "Tuya Fan:"); + ESP_LOGCONFIG(TAG, " Speed count %d", this->speed_count_); if (this->speed_id_.has_value()) ESP_LOGCONFIG(TAG, " Speed has datapoint ID %u", *this->speed_id_); if (this->switch_id_.has_value()) diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index d31d490e1a..ae31df087c 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -9,13 +9,12 @@ namespace tuya { class TuyaFan : public Component { public: + TuyaFan(Tuya *parent, fan::FanState *fan, int speed_count) : parent_(parent), fan_(fan), speed_count_(speed_count) {} void setup() override; void dump_config() override; void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } - void set_fan(fan::FanState *fan) { this->fan_ = fan; } - void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } void write_state(); protected: @@ -28,6 +27,7 @@ class TuyaFan : public Component { optional switch_id_{}; optional oscillation_id_{}; fan::FanState *fan_; + int speed_count_{}; }; } // namespace tuya From 28ed42d8798ae53e0c54e24404c9189d03abba31 Mon Sep 17 00:00:00 2001 From: d-two <32079446+d-two@users.noreply.github.com> Date: Wed, 5 May 2021 04:09:38 +0200 Subject: [PATCH 45/62] Add Hyperion Support (#1339) --- esphome/components/wled/wled_light_effect.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/esphome/components/wled/wled_light_effect.cpp b/esphome/components/wled/wled_light_effect.cpp index 3c26beeed4..f3d8fbd082 100644 --- a/esphome/components/wled/wled_light_effect.cpp +++ b/esphome/components/wled/wled_light_effect.cpp @@ -92,8 +92,14 @@ bool WLEDLightEffect::parse_frame_(light::AddressableLight &it, const uint8_t *p switch (protocol) { case WLED_NOTIFIER: - if (!parse_notifier_frame_(it, payload, size)) - return false; + // Hyperion Port + if (port_ == 19446) { + if (!parse_drgb_frame_(it, payload, size)) + return false; + } else { + if (!parse_notifier_frame_(it, payload, size)) + return false; + } break; case WARLS: From b103be99e849d4d9a6ef4bfee6ec7b4e2ce38926 Mon Sep 17 00:00:00 2001 From: Alex <33379584+alexyao2015@users.noreply.github.com> Date: Tue, 4 May 2021 21:31:38 -0500 Subject: [PATCH 46/62] Do not call component update on failed components (#1392) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/core/base_automation.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/core/base_automation.h b/esphome/core/base_automation.h index d2656290bc..fa49786d1d 100644 --- a/esphome/core/base_automation.h +++ b/esphome/core/base_automation.h @@ -266,7 +266,11 @@ template class UpdateComponentAction : public Action { public: UpdateComponentAction(PollingComponent *component) : component_(component) {} - void play(Ts... x) override { this->component_->update(); } + void play(Ts... x) override { + if (this->component_->is_failed()) + return; + this->component_->update(); + } protected: PollingComponent *component_; From 06b8e4df274dbc4adf045882b0753fdc1e75f06f Mon Sep 17 00:00:00 2001 From: Alex Konradi Date: Tue, 4 May 2021 22:32:15 -0400 Subject: [PATCH 47/62] Call Stepper::should_step_ every loop iteration (#1373) --- esphome/components/uln2003/uln2003.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/uln2003/uln2003.cpp b/esphome/components/uln2003/uln2003.cpp index 38eadc9dc8..9bf34490ec 100644 --- a/esphome/components/uln2003/uln2003.cpp +++ b/esphome/components/uln2003/uln2003.cpp @@ -14,8 +14,8 @@ void ULN2003::setup() { this->loop(); } void ULN2003::loop() { - bool at_target = this->has_reached_target(); - if (at_target) { + int dir = this->should_step_(); + if (dir == 0 && this->has_reached_target()) { this->high_freq_.stop(); if (this->sleep_when_done_) { @@ -28,8 +28,6 @@ void ULN2003::loop() { } } else { this->high_freq_.start(); - - int dir = this->should_step_(); this->current_uln_pos_ += dir; } From ffc6fe9ca0a3653e22d64c7c51e697590ad26191 Mon Sep 17 00:00:00 2001 From: Ryan Mounce Date: Wed, 5 May 2021 13:54:31 +0930 Subject: [PATCH 48/62] Add support for controlling Tuya fan direction (#1409) --- esphome/components/tuya/fan/__init__.py | 4 ++++ esphome/components/tuya/fan/tuya_fan.cpp | 24 ++++++++++++++++++++++-- esphome/components/tuya/fan/tuya_fan.h | 3 +++ 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/esphome/components/tuya/fan/__init__.py b/esphome/components/tuya/fan/__init__.py index b5e3e579be..5d3345a5c4 100644 --- a/esphome/components/tuya/fan/__init__.py +++ b/esphome/components/tuya/fan/__init__.py @@ -8,6 +8,7 @@ DEPENDENCIES = ["tuya"] CONF_SPEED_DATAPOINT = "speed_datapoint" CONF_OSCILLATION_DATAPOINT = "oscillation_datapoint" +CONF_DIRECTION_DATAPOINT = "direction_datapoint" TuyaFan = tuya_ns.class_("TuyaFan", cg.Component) @@ -19,6 +20,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, + cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t, cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256), } ).extend(cv.COMPONENT_SCHEMA), @@ -41,3 +43,5 @@ def to_code(config): cg.add(var.set_switch_id(config[CONF_SWITCH_DATAPOINT])) if CONF_OSCILLATION_DATAPOINT in config: cg.add(var.set_oscillation_id(config[CONF_OSCILLATION_DATAPOINT])) + if CONF_DIRECTION_DATAPOINT in config: + cg.add(var.set_direction_id(config[CONF_DIRECTION_DATAPOINT])) diff --git a/esphome/components/tuya/fan/tuya_fan.cpp b/esphome/components/tuya/fan/tuya_fan.cpp index 26e5de253d..62f4a78db7 100644 --- a/esphome/components/tuya/fan/tuya_fan.cpp +++ b/esphome/components/tuya/fan/tuya_fan.cpp @@ -8,8 +8,8 @@ namespace tuya { static const char *TAG = "tuya.fan"; void TuyaFan::setup() { - auto traits = - fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), false, this->speed_count_); + auto traits = fan::FanTraits(this->oscillation_id_.has_value(), this->speed_id_.has_value(), + this->direction_id_.has_value(), this->speed_count_); this->fan_->set_traits(traits); if (this->speed_id_.has_value()) { @@ -39,6 +39,15 @@ void TuyaFan::setup() { ESP_LOGD(TAG, "MCU reported oscillation is: %s", ONOFF(datapoint.value_bool)); }); } + if (this->direction_id_.has_value()) { + this->parent_->register_listener(*this->direction_id_, [this](TuyaDatapoint datapoint) { + auto call = this->fan_->make_call(); + call.set_direction(datapoint.value_bool ? fan::FAN_DIRECTION_REVERSE : fan::FAN_DIRECTION_FORWARD); + call.perform(); + ESP_LOGD(TAG, "MCU reported reverse direction is: %s", ONOFF(datapoint.value_bool)); + }); + } + this->fan_->add_on_state_callback([this]() { this->write_state(); }); } @@ -51,6 +60,8 @@ void TuyaFan::dump_config() { ESP_LOGCONFIG(TAG, " Switch has datapoint ID %u", *this->switch_id_); if (this->oscillation_id_.has_value()) ESP_LOGCONFIG(TAG, " Oscillation has datapoint ID %u", *this->oscillation_id_); + if (this->direction_id_.has_value()) + ESP_LOGCONFIG(TAG, " Direction has datapoint ID %u", *this->direction_id_); } void TuyaFan::write_state() { @@ -70,6 +81,15 @@ void TuyaFan::write_state() { this->parent_->set_datapoint_value(datapoint); ESP_LOGD(TAG, "Setting oscillating: %s", ONOFF(this->fan_->oscillating)); } + if (this->direction_id_.has_value()) { + TuyaDatapoint datapoint{}; + datapoint.id = *this->direction_id_; + datapoint.type = TuyaDatapointType::BOOLEAN; + bool enable = this->fan_->direction == fan::FAN_DIRECTION_REVERSE; + datapoint.value_bool = enable; + this->parent_->set_datapoint_value(datapoint); + ESP_LOGD(TAG, "Setting reverse direction: %s", ONOFF(enable)); + } if (this->speed_id_.has_value()) { TuyaDatapoint datapoint{}; datapoint.id = *this->speed_id_; diff --git a/esphome/components/tuya/fan/tuya_fan.h b/esphome/components/tuya/fan/tuya_fan.h index ae31df087c..a24e7a218e 100644 --- a/esphome/components/tuya/fan/tuya_fan.h +++ b/esphome/components/tuya/fan/tuya_fan.h @@ -15,17 +15,20 @@ class TuyaFan : public Component { void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; } void set_switch_id(uint8_t switch_id) { this->switch_id_ = switch_id; } void set_oscillation_id(uint8_t oscillation_id) { this->oscillation_id_ = oscillation_id; } + void set_direction_id(uint8_t direction_id) { this->direction_id_ = direction_id; } void write_state(); protected: void update_speed_(uint32_t value); void update_switch_(uint32_t value); void update_oscillation_(uint32_t value); + void update_direction_(uint32_t value); Tuya *parent_; optional speed_id_{}; optional switch_id_{}; optional oscillation_id_{}; + optional direction_id_{}; fan::FanState *fan_; int speed_count_{}; }; From f7232b199a33dca9e863f88134ae8b7849bb1b93 Mon Sep 17 00:00:00 2001 From: mbo18 Date: Wed, 5 May 2021 10:13:14 +0200 Subject: [PATCH 49/62] Change wifi signal strength unit to dBm (#1734) --- esphome/components/wifi_signal/sensor.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi_signal/sensor.py b/esphome/components/wifi_signal/sensor.py index f2a9f5408c..891d95d33e 100644 --- a/esphome/components/wifi_signal/sensor.py +++ b/esphome/components/wifi_signal/sensor.py @@ -5,7 +5,7 @@ from esphome.const import ( CONF_ID, DEVICE_CLASS_SIGNAL_STRENGTH, ICON_EMPTY, - UNIT_DECIBEL, + UNIT_DECIBEL_MILLIWATT, ) DEPENDENCIES = ["wifi"] @@ -15,7 +15,9 @@ WiFiSignalSensor = wifi_signal_ns.class_( ) CONFIG_SCHEMA = ( - sensor.sensor_schema(UNIT_DECIBEL, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH) + sensor.sensor_schema( + UNIT_DECIBEL_MILLIWATT, ICON_EMPTY, 0, DEVICE_CLASS_SIGNAL_STRENGTH + ) .extend( { cv.GenerateID(): cv.declare_id(WiFiSignalSensor), From 2225594ee80fffe4e7265f20344074b372f15b0b Mon Sep 17 00:00:00 2001 From: Farzad E <372719+dnetguru@users.noreply.github.com> Date: Thu, 6 May 2021 08:31:42 -0700 Subject: [PATCH 50/62] Added an option to disable mDNS (#1716) * Added an option to disable mDNS * Fixed linter issues * Moved the enable_mdns option to WiFi and Ethernet components * extracted common method for add mdns library * lint Co-authored-by: Guillermo Ruffino --- esphome/components/ethernet/__init__.py | 6 ++++++ esphome/components/ethernet/ethernet_component.cpp | 2 ++ esphome/components/network/__init__.py | 11 +++++++++++ esphome/components/wifi/__init__.py | 6 ++++++ esphome/components/wifi/wifi_component.cpp | 6 +++--- esphome/const.py | 2 ++ esphome/core.py | 5 +++-- esphome/core/util.cpp | 9 +++++++-- esphome/core/util.h | 1 + esphome/core_config.py | 6 ------ tests/test1.yaml | 1 + tests/test2.yaml | 1 + tests/unit_tests/test_core.py | 4 ++-- 13 files changed, 45 insertions(+), 15 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ca00f33359..82366eeac2 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.config_validation as cv import esphome.codegen as cg +from esphome.components.network import add_mdns_library from esphome.const import ( CONF_DOMAIN, CONF_ID, @@ -9,6 +10,7 @@ from esphome.const import ( CONF_TYPE, CONF_USE_ADDRESS, ESP_PLATFORM_ESP32, + CONF_ENABLE_MDNS, CONF_GATEWAY, CONF_SUBNET, CONF_DNS1, @@ -80,6 +82,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31), cv.Optional(CONF_POWER_PIN): pins.gpio_output_pin_schema, cv.Optional(CONF_MANUAL_IP): MANUAL_IP_SCHEMA, + cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional("hostname"): cv.invalid( @@ -122,3 +125,6 @@ def to_code(config): cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP]))) cg.add_define("USE_ETHERNET") + + if config[CONF_ENABLE_MDNS]: + add_mdns_library() diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 0553d66273..005712420f 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -33,7 +33,9 @@ void EthernetComponent::setup() { this->start_connect_(); +#ifdef USE_MDNS network_setup_mdns(); +#endif } void EthernetComponent::loop() { const uint32_t now = millis(); diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 46713d3ffe..5581a2b8d3 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -1,2 +1,13 @@ # Dummy package to allow components to depend on network +import esphome.codegen as cg +from esphome.core import CORE + CODEOWNERS = ["@esphome/core"] + + +def add_mdns_library(): + cg.add_define("USE_MDNS") + if CORE.is_esp32: + cg.add_library("ESPmDNS", None) + elif CORE.is_esp8266: + cg.add_library("ESP8266mDNS", None) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index f5b7340ad6..421797eb28 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.automation import Condition +from esphome.components.network import add_mdns_library from esphome.const import ( CONF_AP, CONF_BSSID, @@ -22,6 +23,7 @@ from esphome.const import ( CONF_STATIC_IP, CONF_SUBNET, CONF_USE_ADDRESS, + CONF_ENABLE_MDNS, CONF_PRIORITY, CONF_IDENTITY, CONF_CERTIFICATE_AUTHORITY, @@ -187,6 +189,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, cv.Optional(CONF_EAP): EAP_AUTH_SCHEMA, cv.Optional(CONF_AP): WIFI_NETWORK_AP, + cv.Optional(CONF_ENABLE_MDNS, default=True): cv.boolean, cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional( CONF_REBOOT_TIMEOUT, default="15min" @@ -298,6 +301,9 @@ def to_code(config): cg.add_define("USE_WIFI") + if config[CONF_ENABLE_MDNS]: + add_mdns_library() + # Register at end for OTA safe mode yield cg.register_component(var, config) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index df80c5b109..0a6607852d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -62,7 +62,7 @@ void WiFiComponent::setup() { } this->wifi_apply_hostname_(); -#ifdef ARDUINO_ARCH_ESP32 +#if defined(ARDUINO_ARCH_ESP32) && defined(USE_MDNS) network_setup_mdns(); #endif } @@ -171,7 +171,7 @@ void WiFiComponent::setup_ap_config_() { this->ap_setup_ = this->wifi_start_ap_(this->ap_); ESP_LOGCONFIG(TAG, " IP Address: %s", this->wifi_soft_ap_ip().toString().c_str()); -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) network_setup_mdns(this->wifi_soft_ap_ip(), 1); #endif @@ -466,7 +466,7 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGD(TAG, "Disabling AP..."); this->wifi_mode_({}, false); } -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) network_setup_mdns(this->wifi_sta_ip_(), 0); #endif this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; diff --git a/esphome/const.py b/esphome/const.py index 1d992d12e9..cece8df4e3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -183,12 +183,14 @@ CONF_ECHO_PIN = "echo_pin" CONF_EFFECT = "effect" CONF_EFFECTS = "effects" CONF_ELSE = "else" +CONF_ENABLE_MDNS = "enable_mdns" CONF_ENABLE_PIN = "enable_pin" CONF_ENABLE_TIME = "enable_time" CONF_ENERGY = "energy" CONF_ENTITY_ID = "entity_id" CONF_ESP8266_RESTORE_FROM_FLASH = "esp8266_restore_from_flash" CONF_ESPHOME = "esphome" +CONF_ETHERNET = "ethernet" CONF_EVENT = "event" CONF_EXPIRE_AFTER = "expire_after" CONF_EXTERNAL_VCC = "external_vcc" diff --git a/esphome/core.py b/esphome/core.py index cf2a07d35f..ce7eaad6fb 100644 --- a/esphome/core.py +++ b/esphome/core.py @@ -16,6 +16,7 @@ from esphome.const import ( CONF_COMMENT, CONF_ESPHOME, CONF_USE_ADDRESS, + CONF_ETHERNET, CONF_WIFI, ) from esphome.helpers import ensure_unique_string, is_hassio @@ -580,8 +581,8 @@ class EsphomeCore: if "wifi" in self.config: return self.config[CONF_WIFI][CONF_USE_ADDRESS] - if "ethernet" in self.config: - return self.config["ethernet"][CONF_USE_ADDRESS] + if CONF_ETHERNET in self.config: + return self.config[CONF_ETHERNET][CONF_USE_ADDRESS] return None diff --git a/esphome/core/util.cpp b/esphome/core/util.cpp index a701b8013c..4e15d142be 100644 --- a/esphome/core/util.cpp +++ b/esphome/core/util.cpp @@ -16,12 +16,14 @@ #include "esphome/components/ethernet/ethernet_component.h" #endif +#ifdef USE_MDNS #ifdef ARDUINO_ARCH_ESP32 #include #endif #ifdef ARDUINO_ARCH_ESP8266 #include #endif +#endif namespace esphome { @@ -39,7 +41,7 @@ bool network_is_connected() { return false; } -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) bool mdns_setup; #endif @@ -47,6 +49,7 @@ bool mdns_setup; static const uint8_t WEBSERVER_PORT = 80; #endif +#ifdef USE_MDNS #ifdef ARDUINO_ARCH_ESP8266 void network_setup_mdns(IPAddress address, int interface) { // Latest arduino framework breaks mDNS for AP interface @@ -80,8 +83,10 @@ void network_setup_mdns(IPAddress address, int interface) { MDNS.addService("prometheus-http", "tcp", WEBSERVER_PORT); #endif } +#endif + void network_tick_mdns() { -#ifdef ARDUINO_ARCH_ESP8266 +#if defined(ARDUINO_ARCH_ESP8266) && defined(USE_MDNS) if (mdns_setup) MDNS.update(); #endif diff --git a/esphome/core/util.h b/esphome/core/util.h index 0e121ef382..8e30211be6 100644 --- a/esphome/core/util.h +++ b/esphome/core/util.h @@ -17,6 +17,7 @@ void network_setup_mdns(IPAddress address, int interface); #ifdef ARDUINO_ARCH_ESP32 void network_setup_mdns(); #endif + void network_tick_mdns(); } // namespace esphome diff --git a/esphome/core_config.py b/esphome/core_config.py index 55d219d86b..1dbe2ec33a 100644 --- a/esphome/core_config.py +++ b/esphome/core_config.py @@ -308,12 +308,6 @@ def to_code(config): cg.add_build_flag("-fno-exceptions") # Libraries - if CORE.is_esp32: - cg.add_library("ESPmDNS", None) - elif CORE.is_esp8266: - cg.add_library("ESP8266WiFi", None) - cg.add_library("ESP8266mDNS", None) - for lib in config[CONF_LIBRARIES]: if "@" in lib: name, vers = lib.split("@", 1) diff --git a/tests/test1.yaml b/tests/test1.yaml index 167c0b4902..c5c39fb52d 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -71,6 +71,7 @@ wifi: password: '' channel: 14 bssid: 'A1:63:95:47:D3:1D' + enable_mdns: true manual_ip: static_ip: 192.168.178.230 gateway: 192.168.178.1 diff --git a/tests/test2.yaml b/tests/test2.yaml index fa21c1044a..34724ee955 100644 --- a/tests/test2.yaml +++ b/tests/test2.yaml @@ -14,6 +14,7 @@ ethernet: clk_mode: GPIO0_IN phy_addr: 0 power_pin: GPIO25 + enable_mdns: false manual_ip: static_ip: 192.168.178.56 gateway: 192.168.178.1 diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index fd3f171275..37a4920224 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -503,13 +503,13 @@ class TestEsphomeCore: def test_address__wifi(self, target): target.config = {} target.config[const.CONF_WIFI] = {const.CONF_USE_ADDRESS: "1.2.3.4"} - target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} + target.config[const.CONF_ETHERNET] = {const.CONF_USE_ADDRESS: "4.3.2.1"} assert target.address == "1.2.3.4" def test_address__ethernet(self, target): target.config = {} - target.config["ethernet"] = {const.CONF_USE_ADDRESS: "4.3.2.1"} + target.config[const.CONF_ETHERNET] = {const.CONF_USE_ADDRESS: "4.3.2.1"} assert target.address == "4.3.2.1" From 229bf719a2bc40f1ef297efb88684c97f47a458f Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 7 May 2021 20:02:17 +0200 Subject: [PATCH 51/62] Implement external custom components installing from YAML (#1630) * Move components import loading to importlib MetaPathFinder and importlib.resources * Add external_components component * Fix * Fix * fix cv.url return * fix validate shorthand git * implement git refresh * Use finders from sys.path_hooks instead of looking for __init__.py * use github:// schema * error handling * add test * fix handling git output * revert file check handling * fix test * allow full component path be specified for local * fix test * fix path handling * lint Co-authored-by: Guillermo Ruffino --- .github/workflows/ci.yml | 2 +- .github/workflows/release-dev.yml | 2 +- .github/workflows/release.yml | 2 +- .../external_components/__init__.py | 197 ++++++++++++++++++ esphome/components/http_request/__init__.py | 2 +- esphome/config.py | 190 ++--------------- esphome/config_validation.py | 14 ++ esphome/const.py | 3 + esphome/{core.py => core/__init__.py} | 2 +- esphome/{core_config.py => core/config.py} | 0 esphome/helpers.py | 4 +- esphome/loader.py | 179 ++++++++++++++++ esphome/writer.py | 38 ++-- script/build_jsonschema.py | 2 +- tests/test4.yaml | 6 + 15 files changed, 451 insertions(+), 192 deletions(-) create mode 100644 esphome/components/external_components/__init__.py rename esphome/{core.py => core/__init__.py} (99%) rename esphome/{core_config.py => core/config.py} (100%) create mode 100644 esphome/loader.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed4343202c..d2230b3da7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,7 +115,7 @@ jobs: uses: actions/cache@v1 with: path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - name: Set up environment diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index b4c4a8f17d..f0dc4bd0c0 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -112,7 +112,7 @@ jobs: uses: actions/cache@v1 with: path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - name: Set up environment diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1fd2dba24..1eca3be269 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -111,7 +111,7 @@ jobs: uses: actions/cache@v1 with: path: ~/.platformio - key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core_config.py') }} + key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} restore-keys: | test-home-platformio-${{ matrix.test }}- - name: Set up environment diff --git a/esphome/components/external_components/__init__.py b/esphome/components/external_components/__init__.py new file mode 100644 index 0000000000..272812adcf --- /dev/null +++ b/esphome/components/external_components/__init__.py @@ -0,0 +1,197 @@ +import re +import logging +from pathlib import Path +import subprocess +import hashlib +import datetime + +import esphome.config_validation as cv +from esphome.const import ( + CONF_COMPONENTS, + CONF_SOURCE, + CONF_URL, + CONF_TYPE, + CONF_EXTERNAL_COMPONENTS, + CONF_PATH, +) +from esphome.core import CORE +from esphome import loader + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = CONF_EXTERNAL_COMPONENTS + +TYPE_GIT = "git" +TYPE_LOCAL = "local" +CONF_REFRESH = "refresh" +CONF_REF = "ref" + + +def validate_git_ref(value): + if re.match(r"[a-zA-Z0-9\-_.\./]+", value) is None: + raise cv.Invalid("Not a valid git ref") + return value + + +GIT_SCHEMA = { + cv.Required(CONF_URL): cv.url, + cv.Optional(CONF_REF): validate_git_ref, +} +LOCAL_SCHEMA = { + cv.Required(CONF_PATH): cv.directory, +} + + +def validate_source_shorthand(value): + if not isinstance(value, str): + raise cv.Invalid("Shorthand only for strings") + try: + return SOURCE_SCHEMA({CONF_TYPE: TYPE_LOCAL, CONF_PATH: value}) + except cv.Invalid: + pass + # Regex for GitHub repo name with optional branch/tag + # Note: git allows other branch/tag names as well, but never seen them used before + m = re.match( + r"github://([a-zA-Z0-9\-]+)/([a-zA-Z0-9\-\._]+)(?:@([a-zA-Z0-9\-_.\./]+))?", + value, + ) + if m is None: + raise cv.Invalid( + "Source is not a file system path or in expected github://username/name[@branch-or-tag] format!" + ) + conf = { + CONF_TYPE: TYPE_GIT, + CONF_URL: f"https://github.com/{m.group(1)}/{m.group(2)}.git", + } + if m.group(3): + conf[CONF_REF] = m.group(3) + return SOURCE_SCHEMA(conf) + + +def validate_refresh(value: str): + if value.lower() == "always": + return validate_refresh("0s") + if value.lower() == "never": + return validate_refresh("1000y") + return cv.positive_time_period_seconds(value) + + +SOURCE_SCHEMA = cv.Any( + validate_source_shorthand, + cv.typed_schema( + { + TYPE_GIT: cv.Schema(GIT_SCHEMA), + TYPE_LOCAL: cv.Schema(LOCAL_SCHEMA), + } + ), +) + + +CONFIG_SCHEMA = cv.ensure_list( + { + cv.Required(CONF_SOURCE): SOURCE_SCHEMA, + cv.Optional(CONF_REFRESH, default="1d"): cv.All(cv.string, validate_refresh), + cv.Optional(CONF_COMPONENTS, default="all"): cv.Any( + "all", cv.ensure_list(cv.string) + ), + } +) + + +def to_code(config): + pass + + +def _compute_destination_path(key: str) -> Path: + base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN + h = hashlib.new("sha256") + h.update(key.encode()) + return base_dir / h.hexdigest()[:8] + + +def _handle_git_response(ret): + if ret.returncode != 0 and ret.stderr: + err_str = ret.stderr.decode("utf-8") + lines = [x.strip() for x in err_str.splitlines()] + if lines[-1].startswith("fatal:"): + raise cv.Invalid(lines[-1][len("fatal: ") :]) + raise cv.Invalid(err_str) + + +def _process_single_config(config: dict): + conf = config[CONF_SOURCE] + if conf[CONF_TYPE] == TYPE_GIT: + key = f"{conf[CONF_URL]}@{conf.get(CONF_REF)}" + repo_dir = _compute_destination_path(key) + if not repo_dir.is_dir(): + cmd = ["git", "clone", "--depth=1"] + if CONF_REF in conf: + cmd += ["--branch", conf[CONF_REF]] + cmd += [conf[CONF_URL], str(repo_dir)] + ret = subprocess.run(cmd, capture_output=True, check=False) + _handle_git_response(ret) + + else: + # Check refresh needed + file_timestamp = Path(repo_dir / ".git" / "FETCH_HEAD") + # On first clone, FETCH_HEAD does not exists + if not file_timestamp.exists(): + file_timestamp = Path(repo_dir / ".git" / "HEAD") + age = datetime.datetime.now() - datetime.datetime.fromtimestamp( + file_timestamp.stat().st_mtime + ) + if age.seconds > config[CONF_REFRESH].total_seconds: + _LOGGER.info("Executing git pull %s", key) + cmd = ["git", "pull"] + ret = subprocess.run( + cmd, cwd=repo_dir, capture_output=True, check=False + ) + _handle_git_response(ret) + + if (repo_dir / "esphome" / "components").is_dir(): + components_dir = repo_dir / "esphome" / "components" + elif (repo_dir / "components").is_dir(): + components_dir = repo_dir / "components" + else: + raise cv.Invalid( + "Could not find components folder for source. Please check the source contains a 'components' or 'esphome/components' folder", + [CONF_SOURCE], + ) + + elif conf[CONF_TYPE] == TYPE_LOCAL: + components_dir = Path(CORE.relative_config_path(conf[CONF_PATH])) + else: + raise NotImplementedError() + + if config[CONF_COMPONENTS] == "all": + num_components = len(list(components_dir.glob("*/__init__.py"))) + if num_components > 100: + # Prevent accidentally including all components from an esphome fork/branch + # In this case force the user to manually specify which components they want to include + raise cv.Invalid( + "This source is an ESPHome fork or branch. Please manually specify the components you want to import using the 'components' key", + [CONF_COMPONENTS], + ) + allowed_components = None + else: + for i, name in enumerate(config[CONF_COMPONENTS]): + expected = components_dir / name / "__init__.py" + if not expected.is_file(): + raise cv.Invalid( + f"Could not find __init__.py file for component {name}. Please check the component is defined by this source (search path: {expected})", + [CONF_COMPONENTS, i], + ) + allowed_components = config[CONF_COMPONENTS] + + loader.install_meta_finder(components_dir, allowed_components=allowed_components) + + +def do_external_components_pass(config: dict) -> None: + conf = config.get(DOMAIN) + if conf is None: + return + with cv.prepend_path(DOMAIN): + conf = CONFIG_SCHEMA(conf) + for i, c in enumerate(conf): + with cv.prepend_path(i): + _process_single_config(c) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index dee3fe8f77..ef664a9d35 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -14,7 +14,7 @@ from esphome.const import ( CONF_URL, ) from esphome.core import CORE, Lambda -from esphome.core_config import PLATFORMIO_ESP8266_LUT +from esphome.core.config import PLATFORMIO_ESP8266_LUT DEPENDENCIES = ["network"] AUTO_LOAD = ["json"] diff --git a/esphome/config.py b/esphome/config.py index 5a135b3e37..a1fc07a21f 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -1,195 +1,34 @@ import collections -import importlib import logging import re -import os.path # pylint: disable=unused-import, wrong-import-order -import sys from contextlib import contextmanager import voluptuous as vol -from esphome import core, core_config, yaml_util +from esphome import core, yaml_util, loader +import esphome.core.config as core_config from esphome.const import ( CONF_ESPHOME, CONF_PLATFORM, - ESP_PLATFORMS, CONF_PACKAGES, CONF_SUBSTITUTIONS, + CONF_EXTERNAL_COMPONENTS, ) -from esphome.core import CORE, EsphomeError # noqa +from esphome.core import CORE, EsphomeError from esphome.helpers import indent from esphome.util import safe_print, OrderedDict -from typing import List, Optional, Tuple, Union # noqa -from esphome.core import ConfigType # noqa +from typing import List, Optional, Tuple, Union +from esphome.core import ConfigType +from esphome.loader import get_component, get_platform, ComponentManifest from esphome.yaml_util import is_secret, ESPHomeDataBase, ESPForceValue from esphome.voluptuous_schema import ExtraKeysInvalid from esphome.log import color, Fore _LOGGER = logging.getLogger(__name__) -_COMPONENT_CACHE = {} - - -class ComponentManifest: - def __init__(self, module, base_components_path, is_core=False, is_platform=False): - self.module = module - self._is_core = is_core - self.is_platform = is_platform - self.base_components_path = base_components_path - - @property - def is_platform_component(self): - return getattr(self.module, "IS_PLATFORM_COMPONENT", False) - - @property - def config_schema(self): - return getattr(self.module, "CONFIG_SCHEMA", None) - - @property - def multi_conf(self): - return getattr(self.module, "MULTI_CONF", False) - - @property - def to_code(self): - return getattr(self.module, "to_code", None) - - @property - def esp_platforms(self): - return getattr(self.module, "ESP_PLATFORMS", ESP_PLATFORMS) - - @property - def dependencies(self): - return getattr(self.module, "DEPENDENCIES", []) - - @property - def conflicts_with(self): - return getattr(self.module, "CONFLICTS_WITH", []) - - @property - def auto_load(self): - return getattr(self.module, "AUTO_LOAD", []) - - @property - def codeowners(self) -> List[str]: - return getattr(self.module, "CODEOWNERS", []) - - def _get_flags_set(self, name, config): - if not hasattr(self.module, name): - return set() - obj = getattr(self.module, name) - if callable(obj): - obj = obj(config) - if obj is None: - return set() - if not isinstance(obj, (list, tuple, set)): - obj = [obj] - return set(obj) - - @property - def source_files(self): - if self._is_core: - core_p = os.path.abspath(os.path.join(os.path.dirname(__file__), "core")) - source_files = core.find_source_files(os.path.join(core_p, "dummy")) - ret = {} - for f in source_files: - ret[f"esphome/core/{f}"] = os.path.join(core_p, f) - return ret - - source_files = core.find_source_files(self.module.__file__) - ret = {} - # Make paths absolute - directory = os.path.abspath(os.path.dirname(self.module.__file__)) - for x in source_files: - full_file = os.path.join(directory, x) - rel = os.path.relpath(full_file, self.base_components_path) - # Always use / for C++ include names - rel = rel.replace(os.sep, "/") - target_file = f"esphome/components/{rel}" - ret[target_file] = full_file - return ret - - -CORE_COMPONENTS_PATH = os.path.abspath( - os.path.join(os.path.dirname(__file__), "components") -) -_UNDEF = object() -CUSTOM_COMPONENTS_PATH = _UNDEF - - -def _mount_config_dir(): - global CUSTOM_COMPONENTS_PATH - if CUSTOM_COMPONENTS_PATH is not _UNDEF: - return - custom_path = os.path.abspath(os.path.join(CORE.config_dir, "custom_components")) - if not os.path.isdir(custom_path): - CUSTOM_COMPONENTS_PATH = None - return - if CORE.config_dir not in sys.path: - sys.path.insert(0, CORE.config_dir) - CUSTOM_COMPONENTS_PATH = custom_path - - -def _lookup_module(domain, is_platform): - if domain in _COMPONENT_CACHE: - return _COMPONENT_CACHE[domain] - - _mount_config_dir() - # First look for custom_components - try: - module = importlib.import_module(f"custom_components.{domain}") - except ImportError as e: - # ImportError when no such module - if "No module named" not in str(e): - _LOGGER.warning( - "Unable to import custom component %s:", domain, exc_info=True - ) - except Exception: # pylint: disable=broad-except - # Other error means component has an issue - _LOGGER.error("Unable to load custom component %s:", domain, exc_info=True) - return None - else: - # Found in custom components - manif = ComponentManifest( - module, CUSTOM_COMPONENTS_PATH, is_platform=is_platform - ) - _COMPONENT_CACHE[domain] = manif - return manif - - try: - module = importlib.import_module(f"esphome.components.{domain}") - except ImportError as e: - if "No module named" not in str(e): - _LOGGER.error("Unable to import component %s:", domain, exc_info=True) - return None - except Exception: # pylint: disable=broad-except - _LOGGER.error("Unable to load component %s:", domain, exc_info=True) - return None - else: - manif = ComponentManifest(module, CORE_COMPONENTS_PATH, is_platform=is_platform) - _COMPONENT_CACHE[domain] = manif - return manif - - -def get_component(domain): - assert "." not in domain - return _lookup_module(domain, False) - - -def get_platform(domain, platform): - full = f"{platform}.{domain}" - return _lookup_module(full, True) - - -_COMPONENT_CACHE["esphome"] = ComponentManifest( - core_config, - CORE_COMPONENTS_PATH, - is_core=True, - is_platform=False, -) - def iter_components(config): for domain, conf in config.items(): @@ -453,6 +292,9 @@ def recursive_check_replaceme(value): def validate_config(config, command_line_substitutions): result = Config() + loader.clear_component_meta_finders() + loader.install_custom_components_meta_finder() + # 0. Load packages if CONF_PACKAGES in config: from esphome.components.packages import do_packages_pass @@ -486,6 +328,18 @@ def validate_config(config, command_line_substitutions): except vol.Invalid as err: result.add_error(err) + # 1.2. Load external_components + if CONF_EXTERNAL_COMPONENTS in config: + from esphome.components.external_components import do_external_components_pass + + result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS) + try: + do_external_components_pass(config) + except vol.Invalid as err: + result.update(config) + result.add_error(err) + return result + if "esphomeyaml" in config: _LOGGER.warning( "The esphomeyaml section has been renamed to esphome in 1.11.0. " diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 4a65c59379..24c86e6713 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1556,3 +1556,17 @@ def polling_component_schema(default_update_interval): ): update_interval, } ) + + +def url(value): + import urllib.parse + + value = string_strict(value) + try: + parsed = urllib.parse.urlparse(value) + except ValueError as e: + raise Invalid("Not a valid URL") from e + + if not parsed.scheme or not parsed.netloc: + raise Invalid("Expected a URL scheme and host") + return parsed.geturl() diff --git a/esphome/const.py b/esphome/const.py index cece8df4e3..a8f8aa81f5 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -193,6 +193,7 @@ CONF_ESPHOME = "esphome" CONF_ETHERNET = "ethernet" CONF_EVENT = "event" CONF_EXPIRE_AFTER = "expire_after" +CONF_EXTERNAL_COMPONENTS = "external_components" CONF_EXTERNAL_VCC = "external_vcc" CONF_FALLING_EDGE = "falling_edge" CONF_FAMILY = "family" @@ -405,6 +406,7 @@ CONF_PAGE_ID = "page_id" CONF_PAGES = "pages" CONF_PANASONIC = "panasonic" CONF_PASSWORD = "password" +CONF_PATH = "path" CONF_PAYLOAD = "payload" CONF_PAYLOAD_AVAILABLE = "payload_available" CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" @@ -514,6 +516,7 @@ CONF_SLEEP_DURATION = "sleep_duration" CONF_SLEEP_PIN = "sleep_pin" CONF_SLEEP_WHEN_DONE = "sleep_when_done" CONF_SONY = "sony" +CONF_SOURCE = "source" CONF_SPEED = "speed" CONF_SPEED_COMMAND_TOPIC = "speed_command_topic" CONF_SPEED_COUNT = "speed_count" diff --git a/esphome/core.py b/esphome/core/__init__.py similarity index 99% rename from esphome/core.py rename to esphome/core/__init__.py index ce7eaad6fb..47048478ef 100644 --- a/esphome/core.py +++ b/esphome/core/__init__.py @@ -23,7 +23,7 @@ from esphome.helpers import ensure_unique_string, is_hassio from esphome.util import OrderedDict if TYPE_CHECKING: - from .cpp_generator import MockObj, MockObjClass, Statement + from ..cpp_generator import MockObj, MockObjClass, Statement _LOGGER = logging.getLogger(__name__) diff --git a/esphome/core_config.py b/esphome/core/config.py similarity index 100% rename from esphome/core_config.py rename to esphome/core/config.py diff --git a/esphome/helpers.py b/esphome/helpers.py index b80d338eef..d9730f96a7 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -222,7 +222,7 @@ def write_file_if_changed(path: Union[Path, str], text: str): write_file(path, text) -def copy_file_if_changed(src, dst): +def copy_file_if_changed(src: os.PathLike, dst: os.PathLike) -> None: import shutil if file_compare(src, dst): @@ -240,7 +240,7 @@ def list_starts_with(list_, sub): return len(sub) <= len(list_) and all(list_[i] == x for i, x in enumerate(sub)) -def file_compare(path1, path2): +def file_compare(path1: os.PathLike, path2: os.PathLike) -> bool: """Return True if the files path1 and path2 have the same contents.""" import stat diff --git a/esphome/loader.py b/esphome/loader.py new file mode 100644 index 0000000000..c418008453 --- /dev/null +++ b/esphome/loader.py @@ -0,0 +1,179 @@ +import logging +import typing +from typing import Callable, List, Optional, Dict, Any, ContextManager +from types import ModuleType +import importlib +import importlib.util +import importlib.resources +import importlib.abc +import sys +from pathlib import Path + +from esphome.const import ESP_PLATFORMS, SOURCE_FILE_EXTENSIONS +import esphome.core.config +from esphome.core import CORE + +_LOGGER = logging.getLogger(__name__) + + +class SourceFile: + def __init__( + self, + package: importlib.resources.Package, + resource: importlib.resources.Resource, + ) -> None: + self._package = package + self._resource = resource + + def open_binary(self) -> typing.BinaryIO: + return importlib.resources.open_binary(self._package, self._resource) + + def path(self) -> ContextManager[Path]: + return importlib.resources.path(self._package, self._resource) + + +class ComponentManifest: + def __init__(self, module: ModuleType): + self.module = module + + @property + def package(self) -> str: + return self.module.__package__ + + @property + def is_platform(self) -> bool: + return len(self.module.__name__.split(".")) == 4 + + @property + def is_platform_component(self) -> bool: + return getattr(self.module, "IS_PLATFORM_COMPONENT", False) + + @property + def config_schema(self) -> Optional[Any]: + return getattr(self.module, "CONFIG_SCHEMA", None) + + @property + def multi_conf(self) -> bool: + return getattr(self.module, "MULTI_CONF", False) + + @property + def to_code(self) -> Optional[Callable[[Any], None]]: + return getattr(self.module, "to_code", None) + + @property + def esp_platforms(self) -> List[str]: + return getattr(self.module, "ESP_PLATFORMS", ESP_PLATFORMS) + + @property + def dependencies(self) -> List[str]: + return getattr(self.module, "DEPENDENCIES", []) + + @property + def conflicts_with(self) -> List[str]: + return getattr(self.module, "CONFLICTS_WITH", []) + + @property + def auto_load(self) -> List[str]: + return getattr(self.module, "AUTO_LOAD", []) + + @property + def codeowners(self) -> List[str]: + return getattr(self.module, "CODEOWNERS", []) + + @property + def source_files(self) -> Dict[Path, SourceFile]: + ret = {} + for resource in importlib.resources.contents(self.package): + if Path(resource).suffix not in SOURCE_FILE_EXTENSIONS: + continue + if not importlib.resources.is_resource(self.package, resource): + # Not a resource = this is a directory (yeah this is confusing) + continue + # Always use / for C++ include names + target_path = Path(*self.package.split(".")) / resource + ret[target_path] = SourceFile(self.package, resource) + return ret + + +class ComponentMetaFinder(importlib.abc.MetaPathFinder): + def __init__( + self, components_path: Path, allowed_components: Optional[List[str]] = None + ) -> None: + self._allowed_components = allowed_components + self._finders = [] + for hook in sys.path_hooks: + try: + finder = hook(str(components_path)) + except ImportError: + continue + self._finders.append(finder) + + def find_spec(self, fullname: str, path: Optional[List[str]], target=None): + if not fullname.startswith("esphome.components."): + return None + parts = fullname.split(".") + if len(parts) != 3: + # only handle direct components, not platforms + # platforms are handled automatically when parent is imported + return None + component = parts[2] + if ( + self._allowed_components is not None + and component not in self._allowed_components + ): + return None + + for finder in self._finders: + spec = finder.find_spec(fullname, target=target) + if spec is not None: + return spec + return None + + +def clear_component_meta_finders(): + sys.meta_path = [x for x in sys.meta_path if not isinstance(x, ComponentMetaFinder)] + + +def install_meta_finder( + components_path: Path, allowed_components: Optional[List[str]] = None +): + sys.meta_path.insert(0, ComponentMetaFinder(components_path, allowed_components)) + + +def install_custom_components_meta_finder(): + custom_components_dir = (Path(CORE.config_dir) / "custom_components").resolve() + install_meta_finder(custom_components_dir) + + +def _lookup_module(domain): + if domain in _COMPONENT_CACHE: + return _COMPONENT_CACHE[domain] + + try: + module = importlib.import_module(f"esphome.components.{domain}") + except ImportError as e: + if "No module named" not in str(e): + _LOGGER.error("Unable to import component %s:", domain, exc_info=True) + return None + except Exception: # pylint: disable=broad-except + _LOGGER.error("Unable to load component %s:", domain, exc_info=True) + return None + else: + manif = ComponentManifest(module) + _COMPONENT_CACHE[domain] = manif + return manif + + +def get_component(domain): + assert "." not in domain + return _lookup_module(domain) + + +def get_platform(domain, platform): + full = f"{platform}.{domain}" + return _lookup_module(full) + + +_COMPONENT_CACHE = {} +CORE_COMPONENTS_PATH = (Path(__file__).parent / "components").resolve() +_COMPONENT_CACHE["esphome"] = ComponentManifest(esphome.core.config) diff --git a/esphome/writer.py b/esphome/writer.py index ec772b5127..57698f8c25 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,6 +1,8 @@ import logging import os import re +from pathlib import Path +from typing import Dict from esphome.config import iter_components from esphome.const import ( @@ -24,6 +26,7 @@ from esphome.helpers import ( ) from esphome.storage_json import StorageJSON, storage_path from esphome.pins import ESP8266_FLASH_SIZES, ESP8266_LD_SCRIPTS +from esphome import loader _LOGGER = logging.getLogger(__name__) @@ -355,7 +358,7 @@ or use the custom_components folder. def copy_src_tree(): - source_files = {} + source_files: Dict[Path, loader.SourceFile] = {} for _, component, _ in iter_components(CORE.config): source_files.update(component.source_files) @@ -365,37 +368,40 @@ def copy_src_tree(): # Build #include list for esphome.h include_l = [] - for target, path in source_files_l: - if os.path.splitext(path)[1] in HEADER_FILE_EXTENSIONS: + for target, _ in source_files_l: + if target.suffix in HEADER_FILE_EXTENSIONS: include_l.append(f'#include "{target}"') include_l.append("") include_s = "\n".join(include_l) source_files_copy = source_files.copy() - source_files_copy.pop(DEFINES_H_TARGET) + ignore_targets = [Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET)] + for t in ignore_targets: + source_files_copy.pop(t) - for path in walk_files(CORE.relative_src_path("esphome")): - if os.path.splitext(path)[1] not in SOURCE_FILE_EXTENSIONS: + for fname in walk_files(CORE.relative_src_path("esphome")): + p = Path(fname) + if p.suffix not in SOURCE_FILE_EXTENSIONS: # Not a source file, ignore continue # Transform path to target path name - target = os.path.relpath(path, CORE.relative_src_path()).replace( - os.path.sep, "/" - ) - if target in (DEFINES_H_TARGET, VERSION_H_TARGET): + target = p.relative_to(CORE.relative_src_path()) + if target in ignore_targets: # Ignore defines.h, will be dealt with later continue if target not in source_files_copy: # Source file removed, delete target - os.remove(path) + p.unlink() else: - src_path = source_files_copy.pop(target) - copy_file_if_changed(src_path, path) + src_file = source_files_copy.pop(target) + with src_file.path() as src_path: + copy_file_if_changed(src_path, p) # Now copy new files - for target, src_path in source_files_copy.items(): - dst_path = CORE.relative_src_path(*target.split("/")) - copy_file_if_changed(src_path, dst_path) + for target, src_file in source_files_copy.items(): + dst_path = CORE.relative_src_path(*target.parts) + with src_file.path() as src_path: + copy_file_if_changed(src_path, dst_path) # Finally copy defines write_file_if_changed( diff --git a/script/build_jsonschema.py b/script/build_jsonschema.py index 2c9534b861..6d19e25e29 100644 --- a/script/build_jsonschema.py +++ b/script/build_jsonschema.py @@ -62,7 +62,7 @@ def add_definition_array_or_single_object(ref): def add_core(): - from esphome.core_config import CONFIG_SCHEMA + from esphome.core.config import CONFIG_SCHEMA base_props["esphome"] = get_jschema("esphome", CONFIG_SCHEMA.schema) diff --git a/tests/test4.yaml b/tests/test4.yaml index ed63a1ac14..e85cdfbc19 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -160,3 +160,9 @@ display: lambda: |- it.rectangle(0, 0, it.get_width(), it.get_height()); +external_components: + - source: github://esphome/esphome@dev + refresh: 1d + components: ["bh1750"] + - source: ../esphome/components + components: ["sntp"] From cafdcaec292478a1b9aa2f67a25798d45c4f6168 Mon Sep 17 00:00:00 2001 From: Mauricio Bonani Date: Sun, 9 May 2021 15:40:06 -0400 Subject: [PATCH 52/62] Update copyright year (#1760) --- esphome/dashboard/templates/index.html | 2 +- esphome/dashboard/templates/login.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/dashboard/templates/index.html b/esphome/dashboard/templates/index.html index dee10bfc27..b10901f5d4 100644 --- a/esphome/dashboard/templates/index.html +++ b/esphome/dashboard/templates/index.html @@ -649,7 +649,7 @@