From 4202fe65b522f03c8bddbad301381d4685f015fa Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Tue, 9 Jan 2024 00:05:52 +0100 Subject: [PATCH 01/19] fix compilation error for libretiny (#6064) --- esphome/components/libretiny/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index e36c08d522..7dca370eff 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -309,7 +309,7 @@ async def component_to_code(config): lt_options["LT_UART_SILENT_ENABLED"] = 0 lt_options["LT_UART_SILENT_ALL"] = 0 # set default UART port - if uart_port := framework.get(CONF_UART_PORT, None) is not None: + if (uart_port := framework.get(CONF_UART_PORT, None)) is not None: lt_options["LT_UART_DEFAULT_PORT"] = uart_port # add custom options lt_options.update(framework[CONF_OPTIONS]) From 14bffaf8a7726ceaa6611fbd101c6dc4549b3b84 Mon Sep 17 00:00:00 2001 From: Ruben van Dijk <15885455+RubenNL@users.noreply.github.com> Date: Tue, 9 Jan 2024 00:12:28 +0100 Subject: [PATCH 02/19] Add questionmark to default glyphs. (#6053) --- 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 22a5f6b2c5..a803c7567b 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema) DEFAULT_GLYPHS = ( - ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' + ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°' ) CONF_RAW_GLYPH_ID = "raw_glyph_id" From 696bfe6a87124f7cbde34180b05dc3e60def252f Mon Sep 17 00:00:00 2001 From: functionpointer Date: Tue, 9 Jan 2024 00:26:13 +0100 Subject: [PATCH 03/19] pylontech: Fix parsing error with US2000 (#6061) --- esphome/components/pylontech/__init__.py | 2 +- esphome/components/pylontech/pylontech.cpp | 57 +++++++++++++--------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/esphome/components/pylontech/__init__.py b/esphome/components/pylontech/__init__.py index 56fac92e89..197f7e7904 100644 --- a/esphome/components/pylontech/__init__.py +++ b/esphome/components/pylontech/__init__.py @@ -19,7 +19,7 @@ PylontechComponent = pylontech_ns.class_( ) PylontechBattery = pylontech_ns.class_("PylontechBattery") -CV_NUM_BATTERIES = cv.int_range(1, 6) +CV_NUM_BATTERIES = cv.int_range(1, 16) PYLONTECH_COMPONENT_SCHEMA = cv.Schema( { diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 4bfa876110..b33f4d4874 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -1,5 +1,6 @@ #include "pylontech.h" #include "esphome/core/log.h" +#include "esphome/core/helpers.h" namespace esphome { namespace pylontech { @@ -34,26 +35,30 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - uint8_t data; - - // pylontech sends a lot of data very suddenly - // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow - while (this->available() > 0) { - if (this->read_byte(&data)) { - buffer_[buffer_index_write_] += (char) data; - if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || - buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { - // complete line received - buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + if (this->available() > 0) { + // pylontech sends a lot of data very suddenly + // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow + uint8_t data; + int recv = 0; + while (this->available() > 0) { + if (this->read_byte(&data)) { + buffer_[buffer_index_write_] += (char) data; + recv++; + if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || + buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + // complete line received + buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; + } } } - } - - // only process one line per call of loop() to not block esphome for too long - if (buffer_index_read_ != buffer_index_write_) { - this->process_line_(buffer_[buffer_index_read_]); - buffer_[buffer_index_read_].clear(); - buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + ESP_LOGV(TAG, "received %d bytes", recv); + } else { + // only process one line per call of loop() to not block esphome for too long + if (buffer_index_read_ != buffer_index_write_) { + this->process_line_(buffer_[buffer_index_read_]); + buffer_[buffer_index_read_].clear(); + buffer_index_read_ = (buffer_index_read_ + 1) % NUM_BUFFERS; + } } } @@ -66,10 +71,11 @@ void PylontechComponent::process_line_(std::string &buffer) { // clang-format on PylontechListener::LineContents l{}; - const int parsed = sscanf( // NOLINT - buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %d %*s", // NOLINT - &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT - l.curr_st, l.temp_st, &l.coulomb, &l.mostempr); // NOLINT + char mostempr_s[6]; + const int parsed = sscanf( // NOLINT + buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT + &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT + l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT if (l.bat_num <= 0) { ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); @@ -79,6 +85,13 @@ void PylontechComponent::process_line_(std::string &buffer) { ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); return; } + auto mostempr_parsed = parse_number(mostempr_s); + if (mostempr_parsed.has_value()) { + l.mostempr = mostempr_parsed.value(); + } else { + l.mostempr = -300; + ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num); + } for (PylontechListener *listener : this->listeners_) { listener->on_line_read(&l); From 9bdb9dc1a37c0a73258c8756b264874dcc90ed75 Mon Sep 17 00:00:00 2001 From: functionpointer Date: Tue, 9 Jan 2024 00:30:37 +0100 Subject: [PATCH 04/19] pylontech: fix voltage_low and voltage_high wrong unit (#6060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: だから <82636574+Dackara@users.noreply.github.com> --- esphome/components/pylontech/sensor/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/pylontech/sensor/__init__.py b/esphome/components/pylontech/sensor/__init__.py index 0423f3370c..a1477c627f 100644 --- a/esphome/components/pylontech/sensor/__init__.py +++ b/esphome/components/pylontech/sensor/__init__.py @@ -59,14 +59,14 @@ TYPES: dict[str, cv.Schema] = { device_class=DEVICE_CLASS_TEMPERATURE, ), CONF_VOLTAGE_LOW: sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_VOLTAGE_HIGH: sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_TEMPERATURE, + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_VOLTAGE, ), CONF_COULOMB: sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, From 886d1a2d00d77e1a572fdeda6341593c8557c119 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:38:50 +0900 Subject: [PATCH 05/19] Bump flake8 from 6.1.0 to 7.0.0 (#6058) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 401f9cb30f..0348ef6cb2 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,5 +1,5 @@ pylint==3.0.3 -flake8==6.1.0 # also change in .pre-commit-config.yaml when updating +flake8==7.0.0 # also change in .pre-commit-config.yaml when updating black==23.12.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From 6061699eff0a651643e03e1130b6b6bd45d2a4e1 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:41:34 +0100 Subject: [PATCH 06/19] Nextion enable upload from https when using esp-idf (#6051) --- esphome/components/nextion/display.py | 6 ++++++ .../components/nextion/nextion_upload_idf.cpp | 20 +++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index fd61dfa2be..27f2030f0d 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -2,6 +2,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation from esphome.components import display, uart +from esphome.components import esp32 from esphome.const import ( CONF_ID, CONF_LAMBDA, @@ -96,6 +97,11 @@ async def to_code(config): if CORE.is_esp32 and CORE.using_arduino: cg.add_library("WiFiClientSecure", None) cg.add_library("HTTPClient", None) + elif CORE.is_esp32 and CORE.using_esp_idf: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True + ) elif CORE.is_esp8266 and CORE.using_arduino: cg.add_library("ESP8266HTTPClient", None) diff --git a/esphome/components/nextion/nextion_upload_idf.cpp b/esphome/components/nextion/nextion_upload_idf.cpp index 57bb9c45e8..709ff65b12 100644 --- a/esphome/components/nextion/nextion_upload_idf.cpp +++ b/esphome/components/nextion/nextion_upload_idf.cpp @@ -24,7 +24,7 @@ int Nextion::upload_range(const std::string &url, int range_start) { ESP_LOGVV(TAG, "url: %s", url.c_str()); uint range_size = this->tft_size_ - range_start; ESP_LOGVV(TAG, "tft_size_: %i", this->tft_size_); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int range_end = (range_start == 0) ? std::min(this->tft_size_, 16383) : this->tft_size_; if (range_size <= 0 or range_end <= range_start) { ESP_LOGE(TAG, "Invalid range"); @@ -37,6 +37,8 @@ int Nextion::upload_range(const std::string &url, int range_start) { esp_http_client_config_t config = { .url = url.c_str(), .cert_pem = nullptr, + .disable_auto_redirect = false, + .max_redirection_count = 10, }; esp_http_client_handle_t client = esp_http_client_init(&config); @@ -44,7 +46,7 @@ int Nextion::upload_range(const std::string &url, int range_start) { sprintf(range_header, "bytes=%d-%d", range_start, range_end); ESP_LOGV(TAG, "Requesting range: %s", range_header); esp_http_client_set_header(client, "Range", range_header); - ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); ESP_LOGV(TAG, "Opening http connetion"); esp_err_t err; @@ -70,13 +72,13 @@ int Nextion::upload_range(const std::string &url, int range_start) { std::string recv_string; if (buffer == nullptr) { ESP_LOGE(TAG, "Failed to allocate memory for buffer"); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); } else { ESP_LOGV(TAG, "Memory for buffer allocated successfully"); while (true) { App.feed_wdt(); - ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); int read_len = esp_http_client_read(client, reinterpret_cast(buffer), 4096); ESP_LOGVV(TAG, "Read %d bytes from HTTP client, writing to UART", read_len); if (read_len > 0) { @@ -145,17 +147,19 @@ bool Nextion::upload_tft() { // Define the configuration for the HTTP client ESP_LOGV(TAG, "Establishing connection to HTTP server"); - ESP_LOGVV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGVV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_config_t config = { .url = this->tft_url_.c_str(), .cert_pem = nullptr, .method = HTTP_METHOD_HEAD, .timeout_ms = 15000, + .disable_auto_redirect = false, + .max_redirection_count = 10, }; // Initialize the HTTP client with the configuration ESP_LOGV(TAG, "Initializing HTTP client"); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_http_client_handle_t http = esp_http_client_init(&config); if (!http) { ESP_LOGE(TAG, "Failed to initialize HTTP client."); @@ -164,7 +168,7 @@ bool Nextion::upload_tft() { // Perform the HTTP request ESP_LOGV(TAG, "Check if the client could connect"); - ESP_LOGV(TAG, "Available heap: %u", esp_get_free_heap_size()); + ESP_LOGV(TAG, "Available heap: %" PRIu32, esp_get_free_heap_size()); esp_err_t err = esp_http_client_perform(http); if (err != ESP_OK) { ESP_LOGE(TAG, "HTTP request failed: %s", esp_err_to_name(err)); @@ -256,7 +260,7 @@ bool Nextion::upload_end(bool successful) { this->soft_reset(); vTaskDelay(pdMS_TO_TICKS(1500)); // NOLINT if (successful) { - ESP_LOGD(TAG, "Restarting esphome"); + ESP_LOGD(TAG, "Restarting ESPHome"); esp_restart(); // NOLINT(readability-static-accessed-through-instance) } return successful; From e3d146ee44e133d14cf797ab3bea42b7ff72d8c0 Mon Sep 17 00:00:00 2001 From: Robert Paskowitz Date: Mon, 8 Jan 2024 16:44:08 -0800 Subject: [PATCH 07/19] Support full (>460 char) dumps of Pronto IR commands (#6040) Co-authored-by: Rob Paskowitz --- .../remote_base/pronto_protocol.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/esphome/components/remote_base/pronto_protocol.cpp b/esphome/components/remote_base/pronto_protocol.cpp index 4b6977e1a2..ccae64449a 100644 --- a/esphome/components/remote_base/pronto_protocol.cpp +++ b/esphome/components/remote_base/pronto_protocol.cpp @@ -227,16 +227,17 @@ optional ProntoProtocol::decode(RemoteReceiveData src) { } void ProntoProtocol::dump(const ProntoData &data) { - std::string first, rest; - if (data.data.size() < 230) { - first = data.data; - } else { - first = data.data.substr(0, 229); - rest = data.data.substr(230); - } - ESP_LOGI(TAG, "Received Pronto: data=%s", first.c_str()); - if (!rest.empty()) { - ESP_LOGI(TAG, "%s", rest.c_str()); + std::string rest; + + rest = data.data; + ESP_LOGI(TAG, "Received Pronto: data="); + while (true) { + ESP_LOGI(TAG, "%s", rest.substr(0, 230).c_str()); + if (rest.size() > 230) { + rest = rest.substr(230); + } else { + break; + } } } From 2bb5343d2787f2d1d120aa81b808944247aa2672 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:45:46 +0100 Subject: [PATCH 08/19] Extends UART change at runtime to ESP8266 (#6019) --- esphome/components/uart/uart_component.h | 4 ++-- .../uart/uart_component_esp8266.cpp | 20 +++++++++++++++++-- .../components/uart/uart_component_esp8266.h | 15 ++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index 6f27f36bcb..a57910c1a1 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -122,7 +122,7 @@ class UARTComponent { // @return Baud rate in bits per second. uint32_t get_baud_rate() const { return baud_rate_; } -#ifdef USE_ESP32 +#if defined(USE_ESP8266) || defined(USE_ESP32) /** * Load the UART settings. * @param dump_config If true (default), output the new settings to logs; otherwise, change settings quietly. @@ -147,7 +147,7 @@ class UARTComponent { * This will load the current UART interface with the latest settings (baud_rate, parity, etc). */ virtual void load_settings(){}; -#endif // USE_ESP32 +#endif // USE_ESP8266 || USE_ESP32 #ifdef USE_UART_DEBUGGER void add_debug_callback(std::function &&callback) { diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 529108f439..fa8dc3fb17 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -98,10 +98,26 @@ void ESP8266UartComponent::setup() { } } +void ESP8266UartComponent::load_settings(bool dump_config) { + ESP_LOGCONFIG(TAG, "Loading UART bus settings..."); + if (this->hw_serial_ != nullptr) { + SerialConfig config = static_cast(get_config()); + this->hw_serial_->begin(this->baud_rate_, config); + this->hw_serial_->setRxBufferSize(this->rx_buffer_size_); + } else { + this->sw_serial_->setup(this->tx_pin_, this->rx_pin_, this->baud_rate_, this->stop_bits_, this->data_bits_, + this->parity_, this->rx_buffer_size_); + } + if (dump_config) { + ESP_LOGCONFIG(TAG, "UART bus was reloaded."); + this->dump_config(); + } +} + void ESP8266UartComponent::dump_config() { ESP_LOGCONFIG(TAG, "UART Bus:"); - LOG_PIN(" TX Pin: ", tx_pin_); - LOG_PIN(" RX Pin: ", rx_pin_); + LOG_PIN(" TX Pin: ", this->tx_pin_); + LOG_PIN(" RX Pin: ", this->rx_pin_); if (this->rx_pin_ != nullptr) { ESP_LOGCONFIG(TAG, " RX Buffer Size: %u", this->rx_buffer_size_); // NOLINT } diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index eed14f3265..749dd4c61e 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -63,6 +63,21 @@ class ESP8266UartComponent : public UARTComponent, public Component { uint32_t get_config(); + /** + * Load the UART with the current settings. + * @param dump_config (Optional, default `true`): True for displaying new settings or + * false to change it quitely + * + * Example: + * ```cpp + * id(uart1).load_settings(); + * ``` + * + * This will load the current UART interface with the latest settings (baud_rate, parity, etc). + */ + void load_settings(bool dump_config) override; + void load_settings() override { this->load_settings(true); } + protected: void check_logger_conflict() override; From 869cdf122de7beb9a77618d3fee9e020057eadf1 Mon Sep 17 00:00:00 2001 From: Edward Firmo <94725493+edwardtfn@users.noreply.github.com> Date: Tue, 9 Jan 2024 01:47:48 +0100 Subject: [PATCH 09/19] Nextion draw QR code at runtime (#6027) --- esphome/components/nextion/nextion.h | 44 +++++++++++++++++++ .../components/nextion/nextion_commands.cpp | 13 ++++++ 2 files changed, 57 insertions(+) diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index 2f52a032c4..eef2c61638 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -750,6 +750,50 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe */ void filled_circle(int center_x, int center_y, int radius, Color color); + /** + * Draws a QR code in the screen + * @param x1 The top left x coordinate to start the QR code. + * @param y1 The top left y coordinate to start the QR code. + * @param content The content of the QR code (as a plain text - Nextion will generate the QR code). + * @param size The size (in pixels) for the QR code. Defaults to 200px. + * @param background_color The background color to draw with (as rgb565 integer). Defaults to 65535 (white). + * @param foreground_color The foreground color to draw with (as rgb565 integer). Defaults to 0 (black). + * @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo). + * @param border_width The border width (in pixels) for the QR code. Defaults to 8px. + * + * Example: + * ```cpp + * it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;"); + * ``` + * + * Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25). + */ + void qrcode(int x1, int y1, const char *content, int size = 200, uint16_t background_color = 65535, + uint16_t foreground_color = 0, int logo_pic = -1, uint8_t border_width = 8); + /** + * Draws a QR code in the screen + * @param x1 The top left x coordinate to start the QR code. + * @param y1 The top left y coordinate to start the QR code. + * @param content The content of the QR code (as a plain text - Nextion will generate the QR code). + * @param size The size (in pixels) for the QR code. Defaults to 200px. + * @param background_color The background color to draw with (as Color). Defaults to 65535 (white). + * @param foreground_color The foreground color to draw with (as Color). Defaults to 0 (black). + * @param logo_pic The picture id for the logo in the center of the QR code. Defaults to -1 (no logo). + * @param border_width The border width (in pixels) for the QR code. Defaults to 8px. + * + * Example: + * ```cpp + * auto blue = Color(0, 0, 255); + * auto red = Color(255, 0, 0); + * it.qrcode(25, 25, "WIFI:S:MySSID;T:WPA;P:MyPassW0rd;;", 150, blue, red); + * ``` + * + * Draws a QR code with a Wi-Fi network credentials starting at the given coordinates (25,25) with size of 150px in + * red on a blue background. + */ + void qrcode(int x1, int y1, const char *content, int size, Color background_color = Color(255, 255, 255), + Color foreground_color = Color(0, 0, 0), int logo_pic = -1, uint8_t border_width = 8); + /** Set the brightness of the backlight. * * @param brightness The brightness percentage from 0 to 1.0. diff --git a/esphome/components/nextion/nextion_commands.cpp b/esphome/components/nextion/nextion_commands.cpp index 8512ea5573..c4849d6050 100644 --- a/esphome/components/nextion/nextion_commands.cpp +++ b/esphome/components/nextion/nextion_commands.cpp @@ -294,6 +294,19 @@ void Nextion::filled_circle(int center_x, int center_y, int radius, Color color) display::ColorUtil::color_to_565(color)); } +void Nextion::qrcode(int x1, int y1, const char *content, int size, uint16_t background_color, + uint16_t foreground_color, int logo_pic, uint8_t border_width) { + this->add_no_result_to_queue_with_printf_("qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size, + background_color, foreground_color, logo_pic, border_width, content); +} + +void Nextion::qrcode(int x1, int y1, const char *content, int size, Color background_color, Color foreground_color, + int logo_pic, uint8_t border_width) { + this->add_no_result_to_queue_with_printf_( + "qrcode", "qrcode %d,%d,%d,%d,%d,%d,%d,\"%s\"", x1, y1, size, display::ColorUtil::color_to_565(background_color), + display::ColorUtil::color_to_565(foreground_color), logo_pic, border_width, content); +} + void Nextion::set_nextion_rtc_time(ESPTime time) { this->add_no_result_to_queue_with_printf_("rtc0", "rtc0=%u", time.year); this->add_no_result_to_queue_with_printf_("rtc1", "rtc1=%u", time.month); From 79d00ec9136a03dcd86b1714edbeb9cba4d6eb4a Mon Sep 17 00:00:00 2001 From: Dusan Cervenka Date: Tue, 9 Jan 2024 02:07:21 +0100 Subject: [PATCH 10/19] Extend i2s config options (#6056) --- esphome/components/i2s_audio/microphone/__init__.py | 6 ++++++ .../i2s_audio/microphone/i2s_audio_microphone.cpp | 4 ++-- .../components/i2s_audio/microphone/i2s_audio_microphone.h | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/__init__.py b/esphome/components/i2s_audio/microphone/__init__.py index b917da3045..5ee359dc26 100644 --- a/esphome/components/i2s_audio/microphone/__init__.py +++ b/esphome/components/i2s_audio/microphone/__init__.py @@ -20,7 +20,9 @@ DEPENDENCIES = ["i2s_audio"] CONF_ADC_PIN = "adc_pin" CONF_ADC_TYPE = "adc_type" CONF_PDM = "pdm" +CONF_SAMPLE_RATE = "sample_rate" CONF_BITS_PER_SAMPLE = "bits_per_sample" +CONF_USE_APLL = "use_apll" I2SAudioMicrophone = i2s_audio_ns.class_( "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component @@ -62,9 +64,11 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), + cv.Optional(CONF_SAMPLE_RATE, default=16000): cv.int_range(min=1), cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( _validate_bits, cv.enum(BITS_PER_SAMPLE) ), + cv.Optional(CONF_USE_APLL, default=False): cv.boolean, } ).extend(cv.COMPONENT_SCHEMA) @@ -105,6 +109,8 @@ async def to_code(config): cg.add(var.set_pdm(config[CONF_PDM])) cg.add(var.set_channel(config[CONF_CHANNEL])) + cg.add(var.set_sample_rate(config[CONF_SAMPLE_RATE])) cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) + cg.add(var.set_use_apll(config[CONF_USE_APLL])) await microphone.register_microphone(var, config) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index ec2fe258c9..602d537bcb 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -47,14 +47,14 @@ void I2SAudioMicrophone::start_() { } i2s_driver_config_t config = { .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = 16000, + .sample_rate = this->sample_rate_, .bits_per_sample = this->bits_per_sample_, .channel_format = this->channel_, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 4, .dma_buf_len = 256, - .use_apll = false, + .use_apll = this->use_apll_, .tx_desc_auto_clear = false, .fixed_mclk = 0, .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index dc6b70047a..68b9a94fbd 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -31,7 +31,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #endif void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } + void set_sample_rate(uint32_t sample_rate) { this->sample_rate_ = sample_rate; } void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } + void set_use_apll(uint32_t use_apll) { this->use_apll_ = use_apll; } protected: void start_(); @@ -45,7 +47,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub #endif bool pdm_{false}; i2s_channel_fmt_t channel_; + uint32_t sample_rate_; i2s_bits_per_sample_t bits_per_sample_; + bool use_apll_; HighFrequencyLoopRequester high_freq_; }; From 65e6f9cba98803fc75ee035a225213dbf47932cd Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:07:45 +1100 Subject: [PATCH 11/19] Add getter for image data_start (#6036) --- esphome/components/image/image.h | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/image/image.h b/esphome/components/image/image.h index 4e869f5204..5f1f50a134 100644 --- a/esphome/components/image/image.h +++ b/esphome/components/image/image.h @@ -37,6 +37,7 @@ class Image : public display::BaseImage { Color get_pixel(int x, int y, Color color_on = display::COLOR_ON, Color color_off = display::COLOR_OFF) const; int get_width() const override; int get_height() const override; + const uint8_t *get_data_start() { return this->data_start_; } ImageType get_type() const; void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; From d9def0cb3a757750df4bf2c2a73af761bd559a51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 15:08:50 -1000 Subject: [PATCH 12/19] Bump hypothesis to 6.92.1 (#6011) --- requirements_test.txt | 2 +- tests/unit_tests/test_core.py | 2 +- tests/unit_tests/test_helpers.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_test.txt b/requirements_test.txt index 0348ef6cb2..9015152794 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -10,4 +10,4 @@ pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.23.2 asyncmock==0.4.2 -hypothesis==5.49.0 +hypothesis==6.92.1 diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index efa9ff5677..2860486efe 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip_addresses +from hypothesis.strategies import ip_addresses from strategies import mac_addr_strings from esphome import core, const diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index fc6bdbcdec..26ebdcf6af 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -1,7 +1,7 @@ import pytest from hypothesis import given -from hypothesis.provisional import ip_addresses +from hypothesis.strategies import ip_addresses from esphome import helpers From 2be19c4e458c6e86f9357b241792ba2fd0889d2b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 15:13:18 -1000 Subject: [PATCH 13/19] Bump recommended ESP32 IDF to 4.4.6 (#6048) --- esphome/components/esp32/__init__.py | 6 +++--- .../components/esp32_ble_tracker/__init__.py | 20 ++++++++++++------- platformio.ini | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 5d17633975..50d6d229f9 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) # The default/recommended esp-idf framework version # - https://github.com/espressif/esp-idf/releases # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf -RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5) +RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 6) # The platformio/espressif32 version to use for esp-idf frameworks # - https://github.com/platformio/platform-espressif32/releases # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 @@ -271,8 +271,8 @@ def _arduino_check_versions(value): def _esp_idf_check_versions(value): value = value.copy() lookups = { - "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"), - "latest": (cv.Version(5, 1, 0), None), + "dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"), + "latest": (cv.Version(5, 1, 2), None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), } diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index 2ead59c025..1edeaadbfd 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -1,23 +1,26 @@ import re + import esphome.codegen as cg import esphome.config_validation as cv from esphome import automation +from esphome.components import esp32_ble +from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.const import ( CONF_ACTIVE, + CONF_DURATION, CONF_ID, CONF_INTERVAL, - CONF_DURATION, - CONF_TRIGGER_ID, CONF_MAC_ADDRESS, - CONF_SERVICE_UUID, CONF_MANUFACTURER_ID, CONF_ON_BLE_ADVERTISE, - CONF_ON_BLE_SERVICE_DATA_ADVERTISE, CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, + CONF_ON_BLE_SERVICE_DATA_ADVERTISE, + CONF_SERVICE_UUID, + CONF_TRIGGER_ID, + KEY_CORE, + KEY_FRAMEWORK_VERSION, ) -from esphome.components import esp32_ble from esphome.core import CORE -from esphome.components.esp32 import add_idf_sdkconfig_option AUTO_LOAD = ["esp32_ble"] DEPENDENCIES = ["esp32"] @@ -263,7 +266,10 @@ async def to_code(config): # https://github.com/espressif/esp-idf/issues/2503 # Match arduino CONFIG_BTU_TASK_STACK_SIZE # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866 - add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) + if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6): + add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192) + else: + add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192) add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9) cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts diff --git a/platformio.ini b/platformio.ini index 2dfaa79a52..f5f510244c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -136,7 +136,7 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script extends = common:idf platform = platformio/espressif32@5.4.0 platform_packages = - platformio/framework-espidf@~3.40405.0 + platformio/framework-espidf@~3.40406.0 framework = espidf lib_deps = From 87301a2e766435f97ece93deb200bc3bd69ed3e7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 15:33:50 -1000 Subject: [PATCH 14/19] Bump pytest from 7.4.3 to 7.4.4 (#6046) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index 9015152794..bf7103f025 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -5,7 +5,7 @@ pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit # Unit tests -pytest==7.4.3 +pytest==7.4.4 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-asyncio==0.23.2 From 6dfdcff66caf3f62de6442f7ecb2f194d1232c11 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 15:35:43 -1000 Subject: [PATCH 15/19] dashboard: refactor ping implementation to be more efficient (#6002) --- esphome/dashboard/const.py | 2 + esphome/dashboard/core.py | 5 +- esphome/dashboard/dashboard.py | 99 ++++++++++++++++++++++++++++++++ esphome/dashboard/dns.py | 43 ++++++++++++++ esphome/dashboard/settings.py | 15 +++++ esphome/dashboard/status/ping.py | 85 ++++++++++++++++++++++----- esphome/dashboard/web_server.py | 25 ++++++-- requirements.txt | 2 + 8 files changed, 255 insertions(+), 21 deletions(-) create mode 100644 esphome/dashboard/dns.py diff --git a/esphome/dashboard/const.py b/esphome/dashboard/const.py index ed2b81d3e8..190d6c4a9a 100644 --- a/esphome/dashboard/const.py +++ b/esphome/dashboard/const.py @@ -4,5 +4,7 @@ EVENT_ENTRY_ADDED = "entry_added" EVENT_ENTRY_REMOVED = "entry_removed" EVENT_ENTRY_UPDATED = "entry_updated" EVENT_ENTRY_STATE_CHANGED = "entry_state_changed" +MAX_EXECUTOR_WORKERS = 48 + SENTINEL = object() diff --git a/esphome/dashboard/core.py b/esphome/dashboard/core.py index ffec9784e8..e22d95fba9 100644 --- a/esphome/dashboard/core.py +++ b/esphome/dashboard/core.py @@ -8,6 +8,7 @@ from functools import partial from typing import TYPE_CHECKING, Any, Callable from ..zeroconf import DiscoveredImport +from .dns import DNSCache from .entries import DashboardEntries from .settings import DashboardSettings @@ -69,6 +70,7 @@ class ESPHomeDashboard: "mqtt_ping_request", "mdns_status", "settings", + "dns_cache", ) def __init__(self) -> None: @@ -81,7 +83,8 @@ class ESPHomeDashboard: self.ping_request: asyncio.Event | None = None self.mqtt_ping_request = threading.Event() self.mdns_status: MDNSStatus | None = None - self.settings: DashboardSettings = DashboardSettings() + self.settings = DashboardSettings() + self.dns_cache = DNSCache() async def async_setup(self) -> None: """Setup the dashboard.""" diff --git a/esphome/dashboard/dashboard.py b/esphome/dashboard/dashboard.py index 789b14653c..2be98ab3e4 100644 --- a/esphome/dashboard/dashboard.py +++ b/esphome/dashboard/dashboard.py @@ -1,11 +1,19 @@ from __future__ import annotations import asyncio +import logging import os import socket +import threading +import traceback +from asyncio import events +from concurrent.futures import ThreadPoolExecutor +from time import monotonic +from typing import Any from esphome.storage_json import EsphomeStorageJSON, esphome_storage_path +from .const import MAX_EXECUTOR_WORKERS from .core import DASHBOARD from .web_server import make_app, start_web_server @@ -14,6 +22,95 @@ ENV_DEV = "ESPHOME_DASHBOARD_DEV" settings = DASHBOARD.settings +def can_use_pidfd() -> bool: + """Check if pidfd_open is available. + + Back ported from cpython 3.12 + """ + if not hasattr(os, "pidfd_open"): + return False + try: + pid = os.getpid() + os.close(os.pidfd_open(pid, 0)) + except OSError: + # blocked by security policy like SECCOMP + return False + return True + + +class DashboardEventLoopPolicy(asyncio.DefaultEventLoopPolicy): + """Event loop policy for Home Assistant.""" + + def __init__(self, debug: bool) -> None: + """Init the event loop policy.""" + super().__init__() + self.debug = debug + self._watcher: asyncio.AbstractChildWatcher | None = None + + def _init_watcher(self) -> None: + """Initialize the watcher for child processes. + + Back ported from cpython 3.12 + """ + with events._lock: # type: ignore[attr-defined] # pylint: disable=protected-access + if self._watcher is None: # pragma: no branch + if can_use_pidfd(): + self._watcher = asyncio.PidfdChildWatcher() + else: + self._watcher = asyncio.ThreadedChildWatcher() + if threading.current_thread() is threading.main_thread(): + self._watcher.attach_loop( + self._local._loop # type: ignore[attr-defined] # pylint: disable=protected-access + ) + + @property + def loop_name(self) -> str: + """Return name of the loop.""" + return self._loop_factory.__name__ # type: ignore[no-any-return,attr-defined] + + def new_event_loop(self) -> asyncio.AbstractEventLoop: + """Get the event loop.""" + loop: asyncio.AbstractEventLoop = super().new_event_loop() + loop.set_exception_handler(_async_loop_exception_handler) + + if self.debug: + loop.set_debug(True) + + executor = ThreadPoolExecutor( + thread_name_prefix="SyncWorker", max_workers=MAX_EXECUTOR_WORKERS + ) + loop.set_default_executor(executor) + # bind the built-in time.monotonic directly as loop.time to avoid the + # overhead of the additional method call since its the most called loop + # method and its roughly 10%+ of all the call time in base_events.py + loop.time = monotonic # type: ignore[method-assign] + return loop + + +def _async_loop_exception_handler(_: Any, context: dict[str, Any]) -> None: + """Handle all exception inside the core loop.""" + kwargs = {} + if exception := context.get("exception"): + kwargs["exc_info"] = (type(exception), exception, exception.__traceback__) + + logger = logging.getLogger(__package__) + if source_traceback := context.get("source_traceback"): + stack_summary = "".join(traceback.format_list(source_traceback)) + logger.error( + "Error doing job: %s: %s", + context["message"], + stack_summary, + **kwargs, # type: ignore[arg-type] + ) + return + + logger.error( + "Error doing job: %s", + context["message"], + **kwargs, # type: ignore[arg-type] + ) + + def start_dashboard(args) -> None: """Start the dashboard.""" settings.parse_args(args) @@ -26,6 +123,8 @@ def start_dashboard(args) -> None: storage.save(path) settings.cookie_secret = storage.cookie_secret + asyncio.set_event_loop_policy(DashboardEventLoopPolicy(settings.verbose)) + try: asyncio.run(async_start(args)) except KeyboardInterrupt: diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py new file mode 100644 index 0000000000..b78a909220 --- /dev/null +++ b/esphome/dashboard/dns.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +import asyncio +import sys + +from icmplib import NameLookupError, async_resolve + +if sys.version_info >= (3, 11): + from asyncio import timeout as async_timeout +else: + from async_timeout import timeout as async_timeout + + +async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: + """Wrap the icmplib async_resolve function.""" + try: + async with async_timeout(2): + return await async_resolve(hostname) + except (asyncio.TimeoutError, NameLookupError, UnicodeError) as ex: + return ex + + +class DNSCache: + """DNS cache for the dashboard.""" + + def __init__(self, ttl: int | None = 120) -> None: + """Initialize the DNSCache.""" + self._cache: dict[str, tuple[float, list[str] | Exception]] = {} + self._ttl = ttl + + async def async_resolve( + self, hostname: str, now_monotonic: float + ) -> list[str] | Exception: + """Resolve a hostname to a list of IP address.""" + if expire_time_addresses := self._cache.get(hostname): + expire_time, addresses = expire_time_addresses + if expire_time > now_monotonic: + return addresses + + expires = now_monotonic + self._ttl + addresses = await _async_resolve_wrapper(hostname) + self._cache[hostname] = (expires, addresses) + return addresses diff --git a/esphome/dashboard/settings.py b/esphome/dashboard/settings.py index 1a5b1620e8..1f05abab4c 100644 --- a/esphome/dashboard/settings.py +++ b/esphome/dashboard/settings.py @@ -14,7 +14,19 @@ from .util.password import password_hash class DashboardSettings: """Settings for the dashboard.""" + __slots__ = ( + "config_dir", + "password_hash", + "username", + "using_password", + "on_ha_addon", + "cookie_secret", + "absolute_config_dir", + "verbose", + ) + def __init__(self) -> None: + """Initialize the dashboard settings.""" self.config_dir: str = "" self.password_hash: str = "" self.username: str = "" @@ -22,8 +34,10 @@ class DashboardSettings: self.on_ha_addon: bool = False self.cookie_secret: str | None = None self.absolute_config_dir: Path | None = None + self.verbose: bool = False def parse_args(self, args: Any) -> None: + """Parse the arguments.""" self.on_ha_addon: bool = args.ha_addon password = args.password or os.getenv("PASSWORD") or "" if not self.on_ha_addon: @@ -33,6 +47,7 @@ class DashboardSettings: self.password_hash = password_hash(password) self.config_dir = args.configuration self.absolute_config_dir = Path(self.config_dir).resolve() + self.verbose = args.verbose CORE.config_path = os.path.join(self.config_dir, ".") @property diff --git a/esphome/dashboard/status/ping.py b/esphome/dashboard/status/ping.py index 989cd1570f..6630f03c9d 100644 --- a/esphome/dashboard/status/ping.py +++ b/esphome/dashboard/status/ping.py @@ -1,20 +1,20 @@ from __future__ import annotations import asyncio -import os +import logging +import time from typing import cast +from icmplib import Host, SocketPermissionError, async_ping + +from ..const import MAX_EXECUTOR_WORKERS from ..core import DASHBOARD -from ..entries import DashboardEntry, bool_to_entry_state +from ..entries import DashboardEntry, EntryState, bool_to_entry_state from ..util.itertools import chunked -from ..util.subprocess import async_system_command_status +_LOGGER = logging.getLogger(__name__) -async def _async_ping_host(host: str) -> bool: - """Ping a host.""" - return await async_system_command_status( - ["ping", "-n" if os.name == "nt" else "-c", "1", host] - ) +GROUP_SIZE = int(MAX_EXECUTOR_WORKERS / 2) class PingStatus: @@ -27,6 +27,10 @@ class PingStatus: """Run the ping status.""" dashboard = DASHBOARD entries = dashboard.entries + privileged = await _can_use_icmp_lib_with_privilege() + if privileged is None: + _LOGGER.warning("Cannot use icmplib because privileges are insufficient") + return while not dashboard.stop_event.is_set(): # Only ping if the dashboard is open @@ -36,15 +40,68 @@ class PingStatus: to_ping: list[DashboardEntry] = [ entry for entry in current_entries if entry.address is not None ] - for ping_group in chunked(to_ping, 16): + + # Resolve DNS for all entries + entries_with_addresses: dict[DashboardEntry, list[str]] = {} + for ping_group in chunked(to_ping, GROUP_SIZE): ping_group = cast(list[DashboardEntry], ping_group) - results = await asyncio.gather( - *(_async_ping_host(entry.address) for entry in ping_group), + now_monotonic = time.monotonic() + dns_results = await asyncio.gather( + *( + dashboard.dns_cache.async_resolve(entry.address, now_monotonic) + for entry in ping_group + ), return_exceptions=True, ) - for entry, result in zip(ping_group, results): + + for entry, result in zip(ping_group, dns_results): if isinstance(result, Exception): - result = False + entries.async_set_state(entry, EntryState.UNKNOWN) + continue + if isinstance(result, BaseException): + raise result + entries_with_addresses[entry] = result + + # Ping all entries with valid addresses + for ping_group in chunked(entries_with_addresses.items(), GROUP_SIZE): + entry_addresses = cast(tuple[DashboardEntry, list[str]], ping_group) + + results = await asyncio.gather( + *( + async_ping(addresses[0], privileged=privileged) + for _, addresses in entry_addresses + ), + return_exceptions=True, + ) + + for entry_addresses, result in zip(entry_addresses, results): + if isinstance(result, Exception): + ping_result = False elif isinstance(result, BaseException): raise result - entries.async_set_state(entry, bool_to_entry_state(result)) + else: + host: Host = result + ping_result = host.is_alive + entry, _ = entry_addresses + entries.async_set_state(entry, bool_to_entry_state(ping_result)) + + +async def _can_use_icmp_lib_with_privilege() -> None | bool: + """Verify we can create a raw socket.""" + try: + await async_ping("127.0.0.1", count=0, timeout=0, privileged=True) + except SocketPermissionError: + try: + await async_ping("127.0.0.1", count=0, timeout=0, privileged=False) + except SocketPermissionError: + _LOGGER.debug( + "Cannot use icmplib because privileges are insufficient to create the" + " socket" + ) + return None + + _LOGGER.debug("Using icmplib in privileged=False mode") + return False + + _LOGGER.debug("Using icmplib in privileged=True mode") + return True diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 6a80865906..c16461d174 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -9,6 +9,7 @@ import hashlib import json import logging import os +import time import secrets import shutil import subprocess @@ -302,16 +303,28 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket): port = json_message["port"] if ( port == "OTA" # pylint: disable=too-many-boolean-expressions - and (mdns := dashboard.mdns_status) and (entry := entries.get(config_file)) and entry.loaded_integrations and "api" in entry.loaded_integrations - and (address := await mdns.async_resolve_host(entry.name)) ): - # Use the IP address if available but only - # if the API is loaded and the device is online - # since MQTT logging will not work otherwise - port = address + if (mdns := dashboard.mdns_status) and ( + address := await mdns.async_resolve_host(entry.name) + ): + # Use the IP address if available but only + # if the API is loaded and the device is online + # since MQTT logging will not work otherwise + port = address + elif ( + entry.address + and ( + address_list := await dashboard.dns_cache.async_resolve( + entry.address, time.monotonic() + ) + ) + and not isinstance(address_list, Exception) + ): + # If mdns is not available, try to use the DNS cache + port = address_list[0] return [ *DASHBOARD_COMMAND, diff --git a/requirements.txt b/requirements.txt index 115f85de3e..5281b64e66 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ +async_timeout==4.0.3; python_version <= "3.10" voluptuous==0.14.1 PyYAML==6.0.1 paho-mqtt==1.6.1 colorama==0.4.6 +icmplib==3.0.4 tornado==6.4 tzlocal==5.2 # from time tzdata>=2021.1 # from time From 97be209aec830789184df9e2174bee5427a24afd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:11:48 -1000 Subject: [PATCH 16/19] Bump pytest-asyncio from 0.23.2 to 0.23.3 (#6047) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index bf7103f025..35f48e767f 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -8,6 +8,6 @@ pre-commit pytest==7.4.4 pytest-cov==4.1.0 pytest-mock==3.12.0 -pytest-asyncio==0.23.2 +pytest-asyncio==0.23.3 asyncmock==0.4.2 hypothesis==6.92.1 From aa8a533da6eb35f0ab6ffb64423a6bffe99c2f03 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:12:08 -1000 Subject: [PATCH 17/19] Bump black from 23.12.0 to 23.12.1 (#6018) --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36ec1894d8..b2f44d088f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.12.0 + rev: 23.12.1 hooks: - id: black args: diff --git a/requirements_test.txt b/requirements_test.txt index 35f48e767f..74d66f5b25 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==3.0.3 flake8==7.0.0 # also change in .pre-commit-config.yaml when updating -black==23.12.0 # also change in .pre-commit-config.yaml when updating +black==23.12.1 # also change in .pre-commit-config.yaml when updating pyupgrade==3.15.0 # also change in .pre-commit-config.yaml when updating pre-commit From fcd549e5b6d5a776551acde2e38c59d04535b2b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Jan 2024 17:18:13 -1000 Subject: [PATCH 18/19] Run python tests on windows and macos (#6010) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 13 +++++-- .github/workflows/ci.yml | 44 +++++++++++++++++++++-- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 18a2485dbb..3c1a5e2b04 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -28,11 +28,20 @@ runs: # yamllint disable-line rule:line-length key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' + if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows' shell: bash run: | python -m venv venv - . venv/bin/activate + source venv/bin/activate + python --version + pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt + pip install -e . + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows' + shell: bash + run: | + python -m venv venv + ./venv/Scripts/activate python --version pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt pip install -e . diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8182f92f94..1ddc49b504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -166,7 +166,35 @@ jobs: pytest: name: Run pytest - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + os: + - ubuntu-latest + - macOS-latest + - windows-latest + exclude: + # Minimize CI resource usage + # by only running the Python version + # version used for docker images on Windows and macOS + - python-version: "3.12" + os: windows-latest + - python-version: "3.10" + os: windows-latest + - python-version: "3.9" + os: windows-latest + - python-version: "3.12" + os: macOS-latest + - python-version: "3.10" + os: macOS-latest + - python-version: "3.9" + os: macOS-latest + runs-on: ${{ matrix.os }} needs: - common steps: @@ -175,14 +203,24 @@ jobs: - name: Restore Python uses: ./.github/actions/restore-python with: - python-version: ${{ env.DEFAULT_PYTHON }} + python-version: ${{ matrix.python-version }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Register matcher run: echo "::add-matcher::.github/workflows/matchers/pytest.json" - name: Run pytest + if: matrix.os == 'windows-latest' + run: | + ./venv/Scripts/activate + pytest -vv --cov-report=xml --tb=native tests + - name: Run pytest + if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' run: | . venv/bin/activate - pytest -vv --tb=native tests + pytest -vv --cov-report=xml --tb=native tests + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} clang-format: name: Check clang-format From 4b783c03723f50df6969e78ef26729b4b05fa70b Mon Sep 17 00:00:00 2001 From: Andrey Bodrov Date: Wed, 10 Jan 2024 07:31:38 +0300 Subject: [PATCH 19/19] BME280 SPI (#5538) * bme spi finally * linter * CO * tidy * lint * tidy [2] * tidy[-1] * final solution * Update test1.yaml remove failed test * Update test1.1.yaml add test to another file with free GPIO5 pin * fix spi read bytes * fix tests * rename bme280 to bme280_i2c --- CODEOWNERS | 2 + esphome/components/bme280/sensor.py | 116 ------------------ esphome/components/bme280_base/__init__.py | 1 + .../bme280_base.cpp} | 68 +++++----- .../bme280.h => bme280_base/bme280_base.h} | 14 ++- esphome/components/bme280_base/sensor.py | 106 ++++++++++++++++ .../{bme280 => bme280_i2c}/__init__.py | 0 esphome/components/bme280_i2c/bme280_i2c.cpp | 30 +++++ esphome/components/bme280_i2c/bme280_i2c.h | 20 +++ esphome/components/bme280_i2c/sensor.py | 19 +++ esphome/components/bme280_spi/__init__.py | 1 + esphome/components/bme280_spi/bme280_spi.cpp | 66 ++++++++++ esphome/components/bme280_spi/bme280_spi.h | 20 +++ esphome/components/bme280_spi/sensor.py | 24 ++++ tests/test1.yaml | 17 ++- 15 files changed, 350 insertions(+), 154 deletions(-) delete mode 100644 esphome/components/bme280/sensor.py create mode 100644 esphome/components/bme280_base/__init__.py rename esphome/components/{bme280/bme280.cpp => bme280_base/bme280_base.cpp} (93%) rename esphome/components/{bme280/bme280.h => bme280_base/bme280_base.h} (90%) create mode 100644 esphome/components/bme280_base/sensor.py rename esphome/components/{bme280 => bme280_i2c}/__init__.py (100%) create mode 100644 esphome/components/bme280_i2c/bme280_i2c.cpp create mode 100644 esphome/components/bme280_i2c/bme280_i2c.h create mode 100644 esphome/components/bme280_i2c/sensor.py create mode 100644 esphome/components/bme280_spi/__init__.py create mode 100644 esphome/components/bme280_spi/bme280_spi.cpp create mode 100644 esphome/components/bme280_spi/bme280_spi.h create mode 100644 esphome/components/bme280_spi/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index c655f94a1b..0ff5ce4508 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -54,6 +54,8 @@ esphome/components/bl0940/* @tobias- esphome/components/bl0942/* @dbuezas esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/bluetooth_proxy/* @jesserockz +esphome/components/bme280_base/* @esphome/core +esphome/components/bme280_spi/* @apbodrov esphome/components/bme680_bsec/* @trvrnrth esphome/components/bmi160/* @flaviut esphome/components/bmp3xx/* @martgras diff --git a/esphome/components/bme280/sensor.py b/esphome/components/bme280/sensor.py deleted file mode 100644 index 35744a436d..0000000000 --- a/esphome/components/bme280/sensor.py +++ /dev/null @@ -1,116 +0,0 @@ -import esphome.codegen as cg -import esphome.config_validation as cv -from esphome.components import i2c, sensor -from esphome.const import ( - CONF_HUMIDITY, - CONF_ID, - CONF_IIR_FILTER, - CONF_OVERSAMPLING, - CONF_PRESSURE, - CONF_TEMPERATURE, - DEVICE_CLASS_HUMIDITY, - DEVICE_CLASS_PRESSURE, - DEVICE_CLASS_TEMPERATURE, - STATE_CLASS_MEASUREMENT, - UNIT_CELSIUS, - UNIT_HECTOPASCAL, - UNIT_PERCENT, -) - -DEPENDENCIES = ["i2c"] - -bme280_ns = cg.esphome_ns.namespace("bme280") -BME280Oversampling = bme280_ns.enum("BME280Oversampling") -OVERSAMPLING_OPTIONS = { - "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, - "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, - "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, - "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, - "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, - "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, -} - -BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") -IIR_FILTER_OPTIONS = { - "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, - "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, - "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, - "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, - "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, -} - -BME280Component = bme280_ns.class_( - "BME280Component", cg.PollingComponent, i2c.I2CDevice -) - -CONFIG_SCHEMA = ( - cv.Schema( - { - cv.GenerateID(): cv.declare_id(BME280Component), - cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( - unit_of_measurement=UNIT_CELSIUS, - accuracy_decimals=1, - device_class=DEVICE_CLASS_TEMPERATURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_PRESSURE): sensor.sensor_schema( - unit_of_measurement=UNIT_HECTOPASCAL, - accuracy_decimals=1, - device_class=DEVICE_CLASS_PRESSURE, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( - unit_of_measurement=UNIT_PERCENT, - accuracy_decimals=1, - device_class=DEVICE_CLASS_HUMIDITY, - state_class=STATE_CLASS_MEASUREMENT, - ).extend( - { - cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( - OVERSAMPLING_OPTIONS, upper=True - ), - } - ), - cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( - IIR_FILTER_OPTIONS, upper=True - ), - } - ) - .extend(cv.polling_component_schema("60s")) - .extend(i2c.i2c_device_schema(0x77)) -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await i2c.register_i2c_device(var, config) - - if temperature_config := config.get(CONF_TEMPERATURE): - sens = await sensor.new_sensor(temperature_config) - cg.add(var.set_temperature_sensor(sens)) - cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) - - if pressure_config := config.get(CONF_PRESSURE): - sens = await sensor.new_sensor(pressure_config) - cg.add(var.set_pressure_sensor(sens)) - cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) - - if humidity_config := config.get(CONF_HUMIDITY): - sens = await sensor.new_sensor(humidity_config) - cg.add(var.set_humidity_sensor(sens)) - cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING])) - - cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/esphome/components/bme280_base/__init__.py b/esphome/components/bme280_base/__init__.py new file mode 100644 index 0000000000..f70ffa9520 --- /dev/null +++ b/esphome/components/bme280_base/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@esphome/core"] diff --git a/esphome/components/bme280/bme280.cpp b/esphome/components/bme280_base/bme280_base.cpp similarity index 93% rename from esphome/components/bme280/bme280.cpp rename to esphome/components/bme280_base/bme280_base.cpp index 786fc01d28..3c6e15cbca 100644 --- a/esphome/components/bme280/bme280.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -1,9 +1,14 @@ -#include "bme280.h" +#include +#include + +#include "bme280_base.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include +#include namespace esphome { -namespace bme280 { +namespace bme280_base { static const char *const TAG = "bme280.sensor"; @@ -46,7 +51,24 @@ static const uint8_t BME280_STATUS_IM_UPDATE = 0b01; inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } -static const char *oversampling_to_str(BME280Oversampling oversampling) { +const char *iir_filter_to_str(BME280IIRFilter filter) { // NOLINT + switch (filter) { + case BME280_IIR_FILTER_OFF: + return "OFF"; + case BME280_IIR_FILTER_2X: + return "2x"; + case BME280_IIR_FILTER_4X: + return "4x"; + case BME280_IIR_FILTER_8X: + return "8x"; + case BME280_IIR_FILTER_16X: + return "16x"; + default: + return "UNKNOWN"; + } +} + +const char *oversampling_to_str(BME280Oversampling oversampling) { // NOLINT switch (oversampling) { case BME280_OVERSAMPLING_NONE: return "None"; @@ -65,23 +87,6 @@ static const char *oversampling_to_str(BME280Oversampling oversampling) { } } -static const char *iir_filter_to_str(BME280IIRFilter filter) { - switch (filter) { - case BME280_IIR_FILTER_OFF: - return "OFF"; - case BME280_IIR_FILTER_2X: - return "2x"; - case BME280_IIR_FILTER_4X: - return "4x"; - case BME280_IIR_FILTER_8X: - return "8x"; - case BME280_IIR_FILTER_16X: - return "16x"; - default: - return "UNKNOWN"; - } -} - void BME280Component::setup() { ESP_LOGCONFIG(TAG, "Setting up BME280..."); uint8_t chip_id = 0; @@ -112,7 +117,7 @@ void BME280Component::setup() { // Wait until the NVM data has finished loading. uint8_t status; uint8_t retry = 5; - do { + do { // NOLINT delay(2); if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { ESP_LOGW(TAG, "Error reading status register."); @@ -175,7 +180,6 @@ void BME280Component::setup() { } void BME280Component::dump_config() { ESP_LOGCONFIG(TAG, "BME280:"); - LOG_I2C_DEVICE(this); switch (this->error_code_) { case COMMUNICATION_FAILED: ESP_LOGE(TAG, "Communication with BME280 failed!"); @@ -226,14 +230,14 @@ void BME280Component::update() { return; } int32_t t_fine = 0; - float temperature = this->read_temperature_(data, &t_fine); + float const temperature = this->read_temperature_(data, &t_fine); if (std::isnan(temperature)) { ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values."); this->status_set_warning(); return; } - float pressure = this->read_pressure_(data, t_fine); - float humidity = this->read_humidity_(data, t_fine); + float const pressure = this->read_pressure_(data, t_fine); + float const humidity = this->read_humidity_(data, t_fine); ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity); if (this->temperature_sensor_ != nullptr) @@ -257,11 +261,11 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) { const int32_t t2 = this->calibration_.t2; const int32_t t3 = this->calibration_.t3; - int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; - int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; + int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11; + int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14; *t_fine = var1 + var2; - float temperature = (*t_fine * 5 + 128) >> 8; + float const temperature = (*t_fine * 5 + 128) >> 8; return temperature / 100.0f; } @@ -303,11 +307,11 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) { } float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { - uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); + uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF); if (raw_adc == 0x8000) return NAN; - int32_t adc = raw_adc; + int32_t const adc = raw_adc; const int32_t h1 = this->calibration_.h1; const int32_t h2 = this->calibration_.h2; @@ -325,7 +329,7 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) { v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r; v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r; - float h = v_x1_u32r >> 12; + float const h = v_x1_u32r >> 12; return h / 1024.0f; } @@ -351,5 +355,5 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) { } int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); } -} // namespace bme280 +} // namespace bme280_base } // namespace esphome diff --git a/esphome/components/bme280/bme280.h b/esphome/components/bme280_base/bme280_base.h similarity index 90% rename from esphome/components/bme280/bme280.h rename to esphome/components/bme280_base/bme280_base.h index 50d398c40f..0f55ad0101 100644 --- a/esphome/components/bme280/bme280.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -2,10 +2,9 @@ #include "esphome/core/component.h" #include "esphome/components/sensor/sensor.h" -#include "esphome/components/i2c/i2c.h" namespace esphome { -namespace bme280 { +namespace bme280_base { /// Internal struct storing the calibration values of an BME280. struct BME280CalibrationData { @@ -57,8 +56,8 @@ enum BME280IIRFilter { BME280_IIR_FILTER_16X = 0b100, }; -/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor. -class BME280Component : public PollingComponent, public i2c::I2CDevice { +/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor. +class BME280Component : public PollingComponent { public: void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } @@ -91,6 +90,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice { uint16_t read_u16_le_(uint8_t a_register); int16_t read_s16_le_(uint8_t a_register); + virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0; + virtual bool write_byte(uint8_t a_register, uint8_t data) = 0; + virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0; + virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0; + BME280CalibrationData calibration_; BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X}; BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X}; @@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice { } error_code_{NONE}; }; -} // namespace bme280 +} // namespace bme280_base } // namespace esphome diff --git a/esphome/components/bme280_base/sensor.py b/esphome/components/bme280_base/sensor.py new file mode 100644 index 0000000000..3a745ed348 --- /dev/null +++ b/esphome/components/bme280_base/sensor.py @@ -0,0 +1,106 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_IIR_FILTER, + CONF_OVERSAMPLING, + CONF_PRESSURE, + CONF_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_HECTOPASCAL, + UNIT_PERCENT, +) + +bme280_ns = cg.esphome_ns.namespace("bme280_base") +BME280Oversampling = bme280_ns.enum("BME280Oversampling") +OVERSAMPLING_OPTIONS = { + "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE, + "1X": BME280Oversampling.BME280_OVERSAMPLING_1X, + "2X": BME280Oversampling.BME280_OVERSAMPLING_2X, + "4X": BME280Oversampling.BME280_OVERSAMPLING_4X, + "8X": BME280Oversampling.BME280_OVERSAMPLING_8X, + "16X": BME280Oversampling.BME280_OVERSAMPLING_16X, +} + +BME280IIRFilter = bme280_ns.enum("BME280IIRFilter") +IIR_FILTER_OPTIONS = { + "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF, + "2X": BME280IIRFilter.BME280_IIR_FILTER_2X, + "4X": BME280IIRFilter.BME280_IIR_FILTER_4X, + "8X": BME280IIRFilter.BME280_IIR_FILTER_8X, + "16X": BME280IIRFilter.BME280_IIR_FILTER_16X, +} + +CONFIG_SCHEMA_BASE = cv.Schema( + { + cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_PRESSURE): sensor.sensor_schema( + unit_of_measurement=UNIT_HECTOPASCAL, + accuracy_decimals=1, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum( + OVERSAMPLING_OPTIONS, upper=True + ), + } + ), + cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum( + IIR_FILTER_OPTIONS, upper=True + ), + } +).extend(cv.polling_component_schema("60s")) + + +async def to_code(config, func=None): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + if func is not None: + await func(var, config) + + if temperature_config := config.get(CONF_TEMPERATURE): + sens = await sensor.new_sensor(temperature_config) + cg.add(var.set_temperature_sensor(sens)) + cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING])) + + if pressure_config := config.get(CONF_PRESSURE): + sens = await sensor.new_sensor(pressure_config) + cg.add(var.set_pressure_sensor(sens)) + cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING])) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) + cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING])) + + cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) diff --git a/esphome/components/bme280/__init__.py b/esphome/components/bme280_i2c/__init__.py similarity index 100% rename from esphome/components/bme280/__init__.py rename to esphome/components/bme280_i2c/__init__.py diff --git a/esphome/components/bme280_i2c/bme280_i2c.cpp b/esphome/components/bme280_i2c/bme280_i2c.cpp new file mode 100644 index 0000000000..e29675b5b7 --- /dev/null +++ b/esphome/components/bme280_i2c/bme280_i2c.cpp @@ -0,0 +1,30 @@ +#include +#include + +#include "bme280_i2c.h" +#include "esphome/components/i2c/i2c.h" +#include "../bme280_base/bme280_base.h" + +namespace esphome { +namespace bme280_i2c { + +bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) { + return I2CDevice::read_byte(a_register, data); +}; +bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) { + return I2CDevice::write_byte(a_register, data); +}; +bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + return I2CDevice::read_bytes(a_register, data, len); +}; +bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + return I2CDevice::read_byte_16(a_register, data); +}; + +void BME280I2CComponent::dump_config() { + LOG_I2C_DEVICE(this); + BME280Component::dump_config(); +} + +} // namespace bme280_i2c +} // namespace esphome diff --git a/esphome/components/bme280_i2c/bme280_i2c.h b/esphome/components/bme280_i2c/bme280_i2c.h new file mode 100644 index 0000000000..c5e2f7e342 --- /dev/null +++ b/esphome/components/bme280_i2c/bme280_i2c.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bme280_base/bme280_base.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace bme280_i2c { + +static const char *const TAG = "bme280_i2c.sensor"; + +class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice { + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; + void dump_config() override; +}; + +} // namespace bme280_i2c +} // namespace esphome diff --git a/esphome/components/bme280_i2c/sensor.py b/esphome/components/bme280_i2c/sensor.py new file mode 100644 index 0000000000..489c52969d --- /dev/null +++ b/esphome/components/bme280_i2c/sensor.py @@ -0,0 +1,19 @@ +import esphome.codegen as cg +from esphome.components import i2c +from ..bme280_base.sensor import to_code as to_code_base, cv, CONFIG_SCHEMA_BASE + +DEPENDENCIES = ["i2c"] +AUTO_LOAD = ["bme280_base"] + +bme280_ns = cg.esphome_ns.namespace("bme280_i2c") +BME280I2CComponent = bme280_ns.class_( + "BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend( + i2c.i2c_device_schema(default_address=0x77) +).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)}) + + +async def to_code(config): + await to_code_base(config, func=i2c.register_i2c_device) diff --git a/esphome/components/bme280_spi/__init__.py b/esphome/components/bme280_spi/__init__.py new file mode 100644 index 0000000000..a1d33e4d7a --- /dev/null +++ b/esphome/components/bme280_spi/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@apbodrov"] diff --git a/esphome/components/bme280_spi/bme280_spi.cpp b/esphome/components/bme280_spi/bme280_spi.cpp new file mode 100644 index 0000000000..921128c8f5 --- /dev/null +++ b/esphome/components/bme280_spi/bme280_spi.cpp @@ -0,0 +1,66 @@ +#include +#include + +#include "bme280_spi.h" +#include + +int set_bit(uint8_t num, int position) { + int mask = 1 << position; + return num | mask; +} + +int clear_bit(uint8_t num, int position) { + int mask = 1 << position; + return num & ~mask; +} + +namespace esphome { +namespace bme280_spi { + +void BME280SPIComponent::setup() { + this->spi_setup(); + BME280Component::setup(); +}; + +// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used +// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read). +// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte +// 0x77 is transferred, for read access, the byte 0xF7 is transferred. +// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf + +bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) { + this->enable(); + // cause: *data = this->delegate_->transfer(tmp) doesnt work + this->delegate_->transfer(set_bit(a_register, 7)); + *data = this->delegate_->transfer(0); + this->disable(); + return true; +} + +bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) { + this->enable(); + this->delegate_->transfer(clear_bit(a_register, 7)); + this->delegate_->transfer(data); + this->disable(); + return true; +} + +bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) { + this->enable(); + this->delegate_->transfer(set_bit(a_register, 7)); + this->delegate_->read_array(data, len); + this->disable(); + return true; +} + +bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) { + this->enable(); + this->delegate_->transfer(set_bit(a_register, 7)); + ((uint8_t *) data)[1] = this->delegate_->transfer(0); + ((uint8_t *) data)[0] = this->delegate_->transfer(0); + this->disable(); + return true; +} + +} // namespace bme280_spi +} // namespace esphome diff --git a/esphome/components/bme280_spi/bme280_spi.h b/esphome/components/bme280_spi/bme280_spi.h new file mode 100644 index 0000000000..b6b8997fa7 --- /dev/null +++ b/esphome/components/bme280_spi/bme280_spi.h @@ -0,0 +1,20 @@ +#pragma once + +#include "esphome/components/bme280_base/bme280_base.h" +#include "esphome/components/spi/spi.h" + +namespace esphome { +namespace bme280_spi { + +class BME280SPIComponent : public esphome::bme280_base::BME280Component, + public spi::SPIDevice { + void setup() override; + bool read_byte(uint8_t a_register, uint8_t *data) override; + bool write_byte(uint8_t a_register, uint8_t data) override; + bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override; + bool read_byte_16(uint8_t a_register, uint16_t *data) override; +}; + +} // namespace bme280_spi +} // namespace esphome diff --git a/esphome/components/bme280_spi/sensor.py b/esphome/components/bme280_spi/sensor.py new file mode 100644 index 0000000000..3cfe1b3cdd --- /dev/null +++ b/esphome/components/bme280_spi/sensor.py @@ -0,0 +1,24 @@ +import esphome.codegen as cg +from esphome.components import spi +from esphome.components.bme280_base.sensor import ( + to_code as to_code_base, + cv, + CONFIG_SCHEMA_BASE, +) + +DEPENDENCIES = ["spi"] +AUTO_LOAD = ["bme280_base"] + + +bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi") +BME280SPIComponent = bme280_spi_ns.class_( + "BME280SPIComponent", cg.PollingComponent, spi.SPIDevice +) + +CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend( + {cv.GenerateID(): cv.declare_id(BME280SPIComponent)} +) + + +async def to_code(config): + await to_code_base(config, func=spi.register_spi_device) diff --git a/tests/test1.yaml b/tests/test1.yaml index bc7a94bc5a..3ca6faca8a 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -690,7 +690,7 @@ sensor: update_interval: 30s mode: low_power i2c_id: i2c_bus - - platform: bme280 + - platform: bme280_i2c temperature: name: Outside Temperature oversampling: 16x @@ -704,6 +704,21 @@ sensor: iir_filter: 16x update_interval: 15s i2c_id: i2c_bus + - platform: bme280_spi + temperature: + name: Outside Temperature + oversampling: 16x + pressure: + name: Outside Pressure + oversampling: none + humidity: + name: Outside Humidity + oversampling: 8x + cs_pin: + allow_other_uses: true + number: GPIO23 + iir_filter: 16x + update_interval: 15s - platform: bme680 temperature: name: Outside Temperature