From e731a2ffaa62848e43c4723680225392f5217e66 Mon Sep 17 00:00:00 2001 From: h2zero <32826625+h2zero@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:26:56 -0700 Subject: [PATCH 01/17] Add support X.509 client certificates for MQTT. (#5778) Co-authored-by: h2zero Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/mqtt/__init__.py | 11 +++++++++++ esphome/components/mqtt/mqtt_backend_esp32.cpp | 10 ++++++++++ esphome/components/mqtt/mqtt_backend_esp32.h | 4 ++++ esphome/components/mqtt/mqtt_client.h | 2 ++ esphome/const.py | 2 ++ 5 files changed, 29 insertions(+) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index baf32097fa..02184c8a39 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -10,6 +10,8 @@ from esphome.const import ( CONF_BIRTH_MESSAGE, CONF_BROKER, CONF_CERTIFICATE_AUTHORITY, + CONF_CLIENT_CERTIFICATE, + CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_ID, CONF_COMMAND_TOPIC, CONF_COMMAND_RETAIN, @@ -199,6 +201,12 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CERTIFICATE_AUTHORITY): cv.All( cv.string, cv.only_with_esp_idf ), + cv.Inclusive(CONF_CLIENT_CERTIFICATE, "cert-key-pair"): cv.All( + cv.string, cv.only_on_esp32 + ), + cv.Inclusive(CONF_CLIENT_CERTIFICATE_KEY, "cert-key-pair"): cv.All( + cv.string, cv.only_on_esp32 + ), cv.SplitDefault(CONF_SKIP_CERT_CN_CHECK, esp32_idf=False): cv.All( cv.boolean, cv.only_with_esp_idf ), @@ -378,6 +386,9 @@ async def to_code(config): if CONF_CERTIFICATE_AUTHORITY in config: cg.add(var.set_ca_certificate(config[CONF_CERTIFICATE_AUTHORITY])) cg.add(var.set_skip_cert_cn_check(config[CONF_SKIP_CERT_CN_CHECK])) + if CONF_CLIENT_CERTIFICATE in config: + cg.add(var.set_cl_certificate(config[CONF_CLIENT_CERTIFICATE])) + cg.add(var.set_cl_key(config[CONF_CLIENT_CERTIFICATE_KEY])) # prevent error -0x428e # See https://github.com/espressif/esp-idf/issues/139 diff --git a/esphome/components/mqtt/mqtt_backend_esp32.cpp b/esphome/components/mqtt/mqtt_backend_esp32.cpp index 2d4e6802f2..9c2e487ae7 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.cpp +++ b/esphome/components/mqtt/mqtt_backend_esp32.cpp @@ -45,6 +45,11 @@ bool MQTTBackendESP32::initialize_() { mqtt_cfg_.cert_pem = ca_certificate_.value().c_str(); mqtt_cfg_.skip_cert_common_name_check = skip_cert_cn_check_; mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_SSL; + + if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { + mqtt_cfg_.client_cert_pem = this->cl_certificate_.value().c_str(); + mqtt_cfg_.client_key_pem = this->cl_key_.value().c_str(); + } } else { mqtt_cfg_.transport = MQTT_TRANSPORT_OVER_TCP; } @@ -79,6 +84,11 @@ bool MQTTBackendESP32::initialize_() { mqtt_cfg_.broker.verification.certificate = ca_certificate_.value().c_str(); mqtt_cfg_.broker.verification.skip_cert_common_name_check = skip_cert_cn_check_; mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_SSL; + + if (this->cl_certificate_.has_value() && this->cl_key_.has_value()) { + mqtt_cfg_.credentials.authentication.certificate = this->cl_certificate_.value().c_str(); + mqtt_cfg_.credentials.authentication.key = this->cl_key_.value().c_str(); + } } else { mqtt_cfg_.broker.address.transport = MQTT_TRANSPORT_OVER_TCP; } diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index a4ee96ca59..b1f672da10 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -124,6 +124,8 @@ class MQTTBackendESP32 final : public MQTTBackend { void loop() final; void set_ca_certificate(const std::string &cert) { ca_certificate_ = cert; } + void set_cl_certificate(const std::string &cert) { cl_certificate_ = cert; } + void set_cl_key(const std::string &key) { cl_key_ = key; } void set_skip_cert_cn_check(bool skip_check) { skip_cert_cn_check_ = skip_check; } protected: @@ -154,6 +156,8 @@ class MQTTBackendESP32 final : public MQTTBackend { uint16_t keep_alive_; bool clean_session_; optional ca_certificate_; + optional cl_certificate_; + optional cl_key_; bool skip_cert_cn_check_{false}; // callbacks diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index bcb44ab4c2..454316aa87 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -146,6 +146,8 @@ class MQTTClientComponent : public Component { #endif #ifdef USE_ESP32 void set_ca_certificate(const char *cert) { this->mqtt_backend_.set_ca_certificate(cert); } + void set_cl_certificate(const char *cert) { this->mqtt_backend_.set_cl_certificate(cert); } + void set_cl_key(const char *key) { this->mqtt_backend_.set_cl_key(key); } void set_skip_cert_cn_check(bool skip_check) { this->mqtt_backend_.set_skip_cert_cn_check(skip_check); } #endif const Availability &get_availability(); diff --git a/esphome/const.py b/esphome/const.py index c35adc74ee..a95d1d5ac3 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -112,6 +112,8 @@ CONF_CHANNELS = "channels" CONF_CHARACTERISTIC_UUID = "characteristic_uuid" CONF_CHIPSET = "chipset" CONF_CLEAR_IMPEDANCE = "clear_impedance" +CONF_CLIENT_CERTIFICATE = "client_certificate" +CONF_CLIENT_CERTIFICATE_KEY = "client_certificate_key" CONF_CLIENT_ID = "client_id" CONF_CLK_PIN = "clk_pin" CONF_CLOCK_PIN = "clock_pin" From 39bd829236a5dca57f3fc21caf8847b4af89a9c8 Mon Sep 17 00:00:00 2001 From: mathieu-mp Date: Thu, 18 Jan 2024 00:40:30 +0100 Subject: [PATCH 02/17] Fix color observation for triangle outline in display component (#6107) --- esphome/components/display/display.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index 0c3631342e..e531c5cf5c 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -142,9 +142,9 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color) } while (dx <= 0); } void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) { - this->line(x1, y1, x2, y2); - this->line(x1, y1, x3, y3); - this->line(x2, y2, x3, y3); + this->line(x1, y1, x2, y2, color); + this->line(x1, y1, x3, y3, color); + this->line(x2, y2, x3, y3, color); } void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) { if (*y1 > *y2) { From c9c8d397784ac9f47d64cb15ed46ff9d82ad21f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Leforestier?= Date: Thu, 18 Jan 2024 01:56:56 +0100 Subject: [PATCH 03/17] Add support of Honeywell HumidIcon (I2C HIH series) Temperature & Humidity sensor (#5730) --- CODEOWNERS | 1 + .../components/honeywell_hih_i2c/__init__.py | 2 + .../honeywell_hih_i2c/honeywell_hih.cpp | 97 +++++++++++++++++++ .../honeywell_hih_i2c/honeywell_hih.h | 34 +++++++ .../components/honeywell_hih_i2c/sensor.py | 56 +++++++++++ tests/test1.yaml | 7 ++ 6 files changed, 197 insertions(+) create mode 100644 esphome/components/honeywell_hih_i2c/__init__.py create mode 100644 esphome/components/honeywell_hih_i2c/honeywell_hih.cpp create mode 100644 esphome/components/honeywell_hih_i2c/honeywell_hih.h create mode 100644 esphome/components/honeywell_hih_i2c/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index fea1e6215c..7e87679ad8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -137,6 +137,7 @@ esphome/components/heatpumpir/* @rob-deutsch esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hm3301/* @freekode esphome/components/homeassistant/* @OttoWinter +esphome/components/honeywell_hih_i2c/* @Benichou34 esphome/components/honeywellabp/* @RubyBailey esphome/components/honeywellabp2_i2c/* @jpfaff esphome/components/host/* @esphome/core diff --git a/esphome/components/honeywell_hih_i2c/__init__.py b/esphome/components/honeywell_hih_i2c/__init__.py new file mode 100644 index 0000000000..fbf67230f7 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/__init__.py @@ -0,0 +1,2 @@ +"""Support for Honeywell HumidIcon HIH""" +CODEOWNERS = ["@Benichou34"] diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp new file mode 100644 index 0000000000..64d5ddb541 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -0,0 +1,97 @@ +// Honeywell HumidIcon I2C Sensors +// https://prod-edam.honeywell.com/content/dam/honeywell-edam/sps/siot/en-us/products/sensors/humidity-with-temperature-sensors/common/documents/sps-siot-i2c-comms-humidicon-tn-009061-2-en-ciid-142171.pdf +// + +#include "honeywell_hih.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace honeywell_hih_i2c { + +static const char *const TAG = "honeywell_hih.i2c"; + +static const uint8_t REQUEST_CMD[1] = {0x00}; // Measurement Request Format +static const uint16_t MAX_COUNT = 0x3FFE; // 2^14 - 2 + +void HoneywellHIComponent::read_sensor_data_() { + uint8_t data[4]; + + if (this->read(data, sizeof(data)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return; + } + + const uint16_t raw_humidity = (static_cast(data[0] & 0x3F) << 8) | data[1]; + float humidity = (static_cast(raw_humidity) / MAX_COUNT) * 100; + + const uint16_t raw_temperature = (static_cast(data[2]) << 6) | (data[3] >> 2); + float temperature = (static_cast(raw_temperature) / MAX_COUNT) * 165 - 40; + + ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity); + if (this->temperature_sensor_ != nullptr) + this->temperature_sensor_->publish_state(temperature); + if (this->humidity_sensor_ != nullptr) + this->humidity_sensor_->publish_state(humidity); +} + +void HoneywellHIComponent::start_measurement_() { + if (this->write(REQUEST_CMD, sizeof(REQUEST_CMD)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return; + } + + this->measurement_running_ = true; +} + +bool HoneywellHIComponent::is_measurement_ready_() { + uint8_t data[1]; + + if (this->read(data, sizeof(data)) != i2c::ERROR_OK) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + this->mark_failed(); + return false; + } + + // Check status bits + return ((data[0] & 0xC0) == 0x00); +} + +void HoneywellHIComponent::measurement_timeout_() { + ESP_LOGE(TAG, "Honeywell HIH Timeout!"); + this->measurement_running_ = false; + this->mark_failed(); +} + +void HoneywellHIComponent::update() { + ESP_LOGV(TAG, "Update Honeywell HIH Sensor"); + + this->start_measurement_(); + // The measurement cycle duration is typically 36.65 ms for temperature and humidity readings. + this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); }); +} + +void HoneywellHIComponent::loop() { + if (this->measurement_running_ && this->is_measurement_ready_()) { + this->measurement_running_ = false; + this->cancel_timeout("meas_timeout"); + this->read_sensor_data_(); + } +} + +void HoneywellHIComponent::dump_config() { + ESP_LOGD(TAG, "Honeywell HIH:"); + LOG_I2C_DEVICE(this); + if (this->is_failed()) { + ESP_LOGE(TAG, "Communication with Honeywell HIH failed!"); + } + LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); + LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); + LOG_UPDATE_INTERVAL(this); +} + +float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } + +} // namespace honeywell_hih_i2c +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h new file mode 100644 index 0000000000..4457eab1da --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -0,0 +1,34 @@ +// Honeywell HumidIcon I2C Sensors +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome { +namespace honeywell_hih_i2c { + +class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { + public: + void dump_config() override; + float get_setup_priority() const override; + void loop() override; + void update() override; + + void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } + void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } + + protected: + bool measurement_running_{false}; + sensor::Sensor *temperature_sensor_{nullptr}; + sensor::Sensor *humidity_sensor_{nullptr}; + + private: + void read_sensor_data_(); + void start_measurement_(); + bool is_measurement_ready_(); + void measurement_timeout_(); +}; + +} // namespace honeywell_hih_i2c +} // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/sensor.py b/esphome/components/honeywell_hih_i2c/sensor.py new file mode 100644 index 0000000000..f5a6ad2398 --- /dev/null +++ b/esphome/components/honeywell_hih_i2c/sensor.py @@ -0,0 +1,56 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import i2c, sensor +from esphome.const import ( + CONF_HUMIDITY, + CONF_ID, + CONF_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + UNIT_CELSIUS, + UNIT_PERCENT, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_HUMIDITY, +) + +DEPENDENCIES = ["i2c"] + +honeywell_hih_ns = cg.esphome_ns.namespace("honeywell_hih_i2c") +HONEYWELLHIComponent = honeywell_hih_ns.class_( + "HoneywellHIComponent", cg.PollingComponent, i2c.I2CDevice +) + +CONFIG_SCHEMA = ( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(HONEYWELLHIComponent), + 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, + ), + 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.polling_component_schema("60s")) + .extend(i2c.i2c_device_schema(0x27)) +) + + +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)) + + if humidity_config := config.get(CONF_HUMIDITY): + sens = await sensor.new_sensor(humidity_config) + cg.add(var.set_humidity_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index afd199d264..038ac9c738 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -863,6 +863,13 @@ sensor: oversampling: 8x update_interval: 15s i2c_id: i2c_bus + - platform: honeywell_hih_i2c + temperature: + name: Living Room Temperature 7 + humidity: + name: Living Room Humidity 7 + update_interval: 15s + i2c_id: i2c_bus - platform: honeywellabp pressure: name: Honeywell pressure From c6f528583b2b527a4b9a91533d7aacf838c0a984 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 18 Jan 2024 08:13:40 +0100 Subject: [PATCH 04/17] Proposal: Test yaml for each component (#5398) * Test for each component. * When possible use commandline substitution. * Add wildcard support. * end file with new line. * Move component tests into subfolder. * Add component test to pipeline. * Remove trailing whitespace. * add restore python step. * Add `. venv/bin/activate` to pipeline. * step `changed-components` needs `common` step. * start `list-components-changed.py` different. * iterate on pipeline stage `list-components`. * Update `checkout` action. * Rename test folder from `tests` to `_test`. * validate file exists. * Move component test folder. * extend list-components to include child components. * File does not end with a newline * Handle empty list-components matrix. * list-components also check for changes in tests folder. * Improve `list-components.py`. * `*` is a forbidden character for filenames on windows. --------- Co-authored-by: Your Name Co-authored-by: Keith Burzinski --- .github/workflows/ci.yml | 57 +++++++ script/fulltest | 1 + script/list-components.py | 153 ++++++++++++++++++ script/test_build_components | 85 ++++++++++ tests/components/adc/test.esp32-c3.yaml | 5 + tests/components/adc/test.esp32-idf.yaml | 11 ++ tests/components/adc/test.esp32-s2.yaml | 5 + tests/components/adc/test.esp32-s3.yaml | 5 + tests/components/adc/test.esp32.yaml | 11 ++ tests/components/adc/test.esp8266.yaml | 4 + tests/components/adc/test.rp2040.yaml | 4 + .../mopeka_std_check/test.esp32.yaml | 16 ++ tests/components/template/test.all.yaml | 127 +++++++++++++++ .../build_components_base.esp32-ard.yaml | 20 +++ .../build_components_base.esp32-c3-ard.yaml | 20 +++ .../build_components_base.esp32-c3-idf.yaml | 20 +++ .../build_components_base.esp32-idf.yaml | 20 +++ .../build_components_base.esp32-s2-ard.yaml | 21 +++ .../build_components_base.esp32-s2-idf.yaml | 21 +++ .../build_components_base.esp32-s3-ard.yaml | 21 +++ .../build_components_base.esp32-s3-idf.yaml | 21 +++ .../build_components_base.esp8266.yaml | 18 +++ .../build_components_base.rp2040.yaml | 21 +++ 23 files changed, 687 insertions(+) create mode 100755 script/list-components.py create mode 100755 script/test_build_components create mode 100644 tests/components/adc/test.esp32-c3.yaml create mode 100644 tests/components/adc/test.esp32-idf.yaml create mode 100644 tests/components/adc/test.esp32-s2.yaml create mode 100644 tests/components/adc/test.esp32-s3.yaml create mode 100644 tests/components/adc/test.esp32.yaml create mode 100644 tests/components/adc/test.esp8266.yaml create mode 100644 tests/components/adc/test.rp2040.yaml create mode 100644 tests/components/mopeka_std_check/test.esp32.yaml create mode 100644 tests/components/template/test.all.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c3-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-c3-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s2-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s2-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s3-ard.yaml create mode 100644 tests/test_build_components/build_components_base.esp32-s3-idf.yaml create mode 100644 tests/test_build_components/build_components_base.esp8266.yaml create mode 100644 tests/test_build_components/build_components_base.rp2040.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ddc49b504..2187573709 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -392,6 +392,62 @@ jobs: # yamllint disable-line rule:line-length if: always() + list-components: + runs-on: ubuntu-latest + needs: + - common + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.1 + with: + # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works. + fetch-depth: 500 + - name: Fetch dev branch + run: | + git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev* + git merge-base refs/remotes/origin/dev HEAD + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: Find changed components + id: set-matrix + run: | + . venv/bin/activate + echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT + + test-build-components: + name: Component test ${{ matrix.file }} + runs-on: ubuntu-latest + needs: + - common + - list-components + if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }} + strategy: + fail-fast: false + max-parallel: 2 + matrix: + file: ${{ fromJson(needs.list-components.outputs.matrix) }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: Restore Python + uses: ./.github/actions/restore-python + with: + python-version: ${{ env.DEFAULT_PYTHON }} + cache-key: ${{ needs.common.outputs.cache-key }} + - name: test_build_components -e config -c ${{ matrix.file }} + run: | + . venv/bin/activate + ./script/test_build_components -e config -c ${{ matrix.file }} + - name: test_build_components -e compile -c ${{ matrix.file }} + run: | + . venv/bin/activate + ./script/test_build_components -e compile -c ${{ matrix.file }} + ci-status: name: CI Status runs-on: ubuntu-latest @@ -406,6 +462,7 @@ jobs: - pyupgrade - compile-tests - clang-tidy + - test-build-components if: always() steps: - name: Success diff --git a/script/fulltest b/script/fulltest index a605beebfe..6440401e97 100755 --- a/script/fulltest +++ b/script/fulltest @@ -12,3 +12,4 @@ script/lint-cpp script/unit_test script/component_test script/test +script/test_build_components diff --git a/script/list-components.py b/script/list-components.py new file mode 100755 index 0000000000..3e55c0e5f7 --- /dev/null +++ b/script/list-components.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +from pathlib import Path +import sys +import argparse + +from helpers import git_ls_files, changed_files +from esphome.loader import get_component, get_platform +from esphome.core import CORE +from esphome.const import ( + KEY_CORE, + KEY_TARGET_FRAMEWORK, + KEY_TARGET_PLATFORM, + PLATFORM_ESP32, + PLATFORM_ESP8266, +) + + +def filter_component_files(str): + return str.startswith("esphome/components/") | str.startswith("tests/components/") + + +def extract_component_names_array_from_files_array(files): + components = [] + for file in files: + file_parts = file.split("/") + if len(file_parts) >= 4: + component_name = file_parts[2] + if component_name not in components: + components.append(component_name) + return components + + +def add_item_to_components_graph(components_graph, parent, child): + if not parent.startswith("__") and parent != child: + if parent not in components_graph: + components_graph[parent] = [] + if child not in components_graph[parent]: + components_graph[parent].append(child) + + +def create_components_graph(): + # The root directory of the repo + root = Path(__file__).parent.parent + components_dir = root / "esphome" / "components" + # Fake some directory so that get_component works + CORE.config_path = str(root) + # Various configuration to capture different outcomes used by `AUTO_LOAD` function. + TARGET_CONFIGURATIONS = [ + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None}, + {KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32}, + ] + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + components_graph = {} + + for path in components_dir.iterdir(): + if not path.is_dir(): + continue + if not (path / "__init__.py").is_file(): + continue + name = path.name + comp = get_component(name) + if comp is None: + print( + f"Cannot find component {name}. Make sure current path is pip installed ESPHome" + ) + sys.exit(1) + + for dependency in comp.dependencies: + add_item_to_components_graph(components_graph, dependency, name) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for auto_load in comp.auto_load: + add_item_to_components_graph(components_graph, auto_load, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + for platform_path in path.iterdir(): + platform_name = platform_path.stem + platform = get_platform(platform_name, name) + if platform is None: + continue + + add_item_to_components_graph(components_graph, platform_name, name) + + for dependency in platform.dependencies: + add_item_to_components_graph(components_graph, dependency, name) + + for target_config in TARGET_CONFIGURATIONS: + CORE.data[KEY_CORE] = target_config + for auto_load in platform.auto_load: + add_item_to_components_graph(components_graph, auto_load, name) + # restore config + CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0] + + return components_graph + + +def find_children_of_component(components_graph, component_name, depth=0): + if component_name not in components_graph: + return [] + + children = [] + + for child in components_graph[component_name]: + children.append(child) + if depth < 10: + children.extend( + find_children_of_component(components_graph, child, depth + 1) + ) + # Remove duplicate values + return list(set(children)) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "-c", "--changed", action="store_true", help="Only run on changed files" + ) + args = parser.parse_args() + + files = git_ls_files() + files = filter(filter_component_files, files) + + if args.changed: + changed = changed_files() + files = [f for f in files if f in changed] + + components = extract_component_names_array_from_files_array(files) + + if args.changed: + components_graph = create_components_graph() + + all_changed_components = components.copy() + for c in components: + all_changed_components.extend( + find_children_of_component(components_graph, c) + ) + # Remove duplicate values + all_changed_components = list(set(all_changed_components)) + + for c in sorted(all_changed_components): + print(c) + else: + for c in sorted(components): + print(c) + + +if __name__ == "__main__": + main() diff --git a/script/test_build_components b/script/test_build_components new file mode 100755 index 0000000000..f951ba7545 --- /dev/null +++ b/script/test_build_components @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -e + +# Parse parameter: +# - `e` - Parameter for `esphome` command. Default `compile`. Common alternative is `config`. +# - `c` - Component folder name to test. Default `*`. +esphome_command="compile" +target_component="*" +while getopts e:c: flag +do + case $flag in + e) esphome_command=${OPTARG};; + c) target_component=${OPTARG};; + \?) echo "Usage: $0 [-e ] [-c ]" 1>&2; exit 1;; + esac +done + +cd "$(dirname "$0")/.." + +if ! [ -d "./tests/test_build_components/build" ]; then + mkdir ./tests/test_build_components/build +fi + +start_esphome() { + # create dynamic yaml file in `build` folder. + # `./tests/test_build_components/build/[target_component].[test_name].[target_platform].yaml` + component_test_file="./tests/test_build_components/build/$target_component.$test_name.$target_platform.yaml" + + cp $target_platform_file $component_test_file + sed -i "s!\$component_test_file!../../.$f!g" $component_test_file + + # Start esphome process + echo "> [$target_component] [$test_name] [$target_platform]" + echo "esphome -s component_name $target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file" + # TODO: Validate escape of Command line substitution value + esphome -s component_name $target_component -s test_name $test_name -s target_platform $target_platform $esphome_command $component_test_file +} + +# Find all test yaml files. +# - `./tests/components/[target_component]/[test_name].[target_platform].yaml` +# - `./tests/components/[target_component]/[test_name].all.yaml` +for f in ./tests/components/$target_component/*.*.yaml; do + [ -f "$f" ] || continue + IFS='/' read -r -a folder_name <<< "$f" + target_component="${folder_name[3]}" + + IFS='.' read -r -a file_name <<< "${folder_name[4]}" + test_name="${file_name[0]}" + target_platform="${file_name[1]}" + file_name_parts=${#file_name[@]} + + if [ "$target_platform" = "all" ] || [ $file_name_parts = 2 ]; then + # Test has *not* defined a specific target platform. Need to run tests for all possible target platforms. + + for target_platform_file in ./tests/test_build_components/build_components_base.*.yaml; do + IFS='/' read -r -a folder_name <<< "$target_platform_file" + IFS='.' read -r -a file_name <<< "${folder_name[3]}" + target_platform="${file_name[1]}" + + start_esphome + done + + else + # Test has defined a specific target platform. + + # Validate we have a base test yaml for selected platform. + # The target_platform is sourced from the following location. + # 1. `./tests/test_build_components/build_components_base.[target_platform].yaml` + # 2. `./tests/test_build_components/build_components_base.[target_platform]-ard.yaml` + target_platform_file="./tests/test_build_components/build_components_base.$target_platform.yaml" + if ! [ -f "$target_platform_file" ]; then + # Try find arduino test framework as platform. + target_platform_ard="$target_platform-ard" + target_platform_file="./tests/test_build_components/build_components_base.$target_platform_ard.yaml" + if ! [ -f "$target_platform_file" ]; then + echo "No base test file [./tests/test_build_components/build_components_base.$target_platform.yaml, ./tests/build_components_base.$target_platform_ard.yaml] for component test [$f] found." + exit 1 + fi + target_platform=$target_platform_ard + fi + + start_esphome + fi +done diff --git a/tests/components/adc/test.esp32-c3.yaml b/tests/components/adc/test.esp32-c3.yaml new file mode 100644 index 0000000000..18e5ab3561 --- /dev/null +++ b/tests/components/adc/test.esp32-c3.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 4 + attenuation: 11db diff --git a/tests/components/adc/test.esp32-idf.yaml b/tests/components/adc/test.esp32-idf.yaml new file mode 100644 index 0000000000..923fd0d706 --- /dev/null +++ b/tests/components/adc/test.esp32-idf.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + pin: A0 + name: Living Room Brightness + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp32-s2.yaml b/tests/components/adc/test.esp32-s2.yaml new file mode 100644 index 0000000000..0119ad5e4d --- /dev/null +++ b/tests/components/adc/test.esp32-s2.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db diff --git a/tests/components/adc/test.esp32-s3.yaml b/tests/components/adc/test.esp32-s3.yaml new file mode 100644 index 0000000000..0119ad5e4d --- /dev/null +++ b/tests/components/adc/test.esp32-s3.yaml @@ -0,0 +1,5 @@ +sensor: + - platform: adc + id: my_sensor + pin: 1 + attenuation: 11db diff --git a/tests/components/adc/test.esp32.yaml b/tests/components/adc/test.esp32.yaml new file mode 100644 index 0000000000..923fd0d706 --- /dev/null +++ b/tests/components/adc/test.esp32.yaml @@ -0,0 +1,11 @@ +sensor: + - platform: adc + pin: A0 + name: Living Room Brightness + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/adc/test.esp8266.yaml b/tests/components/adc/test.esp8266.yaml new file mode 100644 index 0000000000..1ef79c7ca1 --- /dev/null +++ b/tests/components/adc/test.esp8266.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + id: my_sensor + pin: VCC diff --git a/tests/components/adc/test.rp2040.yaml b/tests/components/adc/test.rp2040.yaml new file mode 100644 index 0000000000..200b802a4d --- /dev/null +++ b/tests/components/adc/test.rp2040.yaml @@ -0,0 +1,4 @@ +sensor: + - platform: adc + pin: VCC + name: VSYS diff --git a/tests/components/mopeka_std_check/test.esp32.yaml b/tests/components/mopeka_std_check/test.esp32.yaml new file mode 100644 index 0000000000..830adf952f --- /dev/null +++ b/tests/components/mopeka_std_check/test.esp32.yaml @@ -0,0 +1,16 @@ +esp32_ble_tracker: + +sensor: + # Example using 11kg 100% propane tank. + - platform: mopeka_std_check + mac_address: D3:75:F2:DC:16:91 + tank_type: Europe_11kg + temperature: + name: "Propane test temp" + level: + name: "Propane test level" + distance: + name: "Propane test distance" + battery_level: + name: "Propane test battery level" + diff --git a/tests/components/template/test.all.yaml b/tests/components/template/test.all.yaml new file mode 100644 index 0000000000..ad67b4e6ae --- /dev/null +++ b/tests/components/template/test.all.yaml @@ -0,0 +1,127 @@ +sensor: + - platform: template + name: "Template Sensor" + id: template_sens + lambda: |- + if (id(some_binary_sensor).state) { + return 42.0; + } else { + return 0.0; + } + update_interval: 60s + +esphome: + on_boot: + - sensor.template.publish: + id: template_sens + state: 42.0 + + # Templated + - sensor.template.publish: + id: template_sens + state: !lambda 'return 42.0;' + +binary_sensor: + - platform: template + id: some_binary_sensor + name: "Garage Door Open" + lambda: |- + if (id(template_sens).state > 30) { + // Garage Door is open. + return true; + } else { + // Garage Door is closed. + return false; + } + +output: + - platform: template + id: outputsplit + type: float + write_action: + - logger.log: "write_action" + +switch: + - platform: template + name: "Template Switch" + lambda: |- + if (id(some_binary_sensor).state) { + return true; + } else { + return false; + } + turn_on_action: + - logger.log: "turn_on_action" + turn_off_action: + - logger.log: "turn_off_action" + +button: + - platform: template + name: "Template Button" + on_press: + - logger.log: Button Pressed + +cover: + - platform: template + name: "Template Cover" + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } else { + return COVER_CLOSED; + } + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + +number: + - platform: template + name: "Template number" + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + +select: + - platform: template + name: "Template select" + optimistic: true + options: + - one + - two + - three + initial_option: two + +lock: + - platform: template + name: "Template Lock" + lambda: |- + if (id(some_binary_sensor).state) { + return LOCK_STATE_LOCKED; + } else { + return LOCK_STATE_UNLOCKED; + } + lock_action: + - logger.log: lock_action + unlock_action: + - logger.log: unlock_action + open_action: + - logger.log: open_action + +text: + - platform: template + name: "Template text" + optimistic: true + min_length: 0 + max_length: 100 + mode: text + +alarm_control_panel: + - platform: template + name: Alarm Panel + codes: + - "1234" diff --git a/tests/test_build_components/build_components_base.esp32-ard.yaml b/tests/test_build_components/build_components_base.esp32-ard.yaml new file mode 100644 index 0000000000..f460c57298 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-ard.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32ard + friendly_name: $component_name + +esp32: + board: nodemcu-32s + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-c3-ard.yaml b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml new file mode 100644 index 0000000000..8a52e0c916 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-ard.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c3ard + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-c3-idf.yaml b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml new file mode 100644 index 0000000000..6b4b61fe58 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c3-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c3idf + friendly_name: $component_name + +esp32: + board: lolin_c3_mini + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-idf.yaml b/tests/test_build_components/build_components_base.esp32-idf.yaml new file mode 100644 index 0000000000..ab1bda2a19 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32idf + friendly_name: $component_name + +esp32: + board: nodemcu-32s + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s2-ard.yaml b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml new file mode 100644 index 0000000000..ffb912d3d9 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-ard.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s2ard + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s2-idf.yaml b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml new file mode 100644 index 0000000000..4d1378b2b2 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s2-idf.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s2ard + friendly_name: $component_name + +esp32: + board: esp32-s2-saola-1 + variant: ESP32S2 + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s3-ard.yaml b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml new file mode 100644 index 0000000000..c850c9665f --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-ard.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s3ard + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: arduino + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp32-s3-idf.yaml b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml new file mode 100644 index 0000000000..a43a2a6736 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-s3-idf.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestesp32s3ard + friendly_name: $component_name + +esp32: + board: esp32s3box + variant: ESP32S3 + framework: + type: esp-idf + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.esp8266.yaml b/tests/test_build_components/build_components_base.esp8266.yaml new file mode 100644 index 0000000000..d7bdc03659 --- /dev/null +++ b/tests/test_build_components/build_components_base.esp8266.yaml @@ -0,0 +1,18 @@ +esphome: + name: componenttestesp8266 + friendly_name: $component_name + +esp8266: + board: d1_mini + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file diff --git a/tests/test_build_components/build_components_base.rp2040.yaml b/tests/test_build_components/build_components_base.rp2040.yaml new file mode 100644 index 0000000000..a02942ea35 --- /dev/null +++ b/tests/test_build_components/build_components_base.rp2040.yaml @@ -0,0 +1,21 @@ +esphome: + name: componenttestrp2040 + friendly_name: $component_name + +rp2040: + board: rpipicow + framework: + # Waiting for https://github.com/platformio/platform-raspberrypi/pull/36 + platform_version: https://github.com/maxgerhardt/platform-raspberrypi.git + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_name: $component_name + test_name: $test_name + target_platform: $target_platform + component_test_file: $component_test_file From e2f2feafdd912f5e249ccb1b47502e4201665cf2 Mon Sep 17 00:00:00 2001 From: Rene Guca <45061891+rguca@users.noreply.github.com> Date: Thu, 18 Jan 2024 08:30:58 +0100 Subject: [PATCH 05/17] WiFi fast_connect: save/load BSSID and channel for faster connect from sleep (#5931) --- esphome/components/wifi/wifi_component.cpp | 38 ++++++++++++++++++++++ esphome/components/wifi/wifi_component.h | 9 +++++ 2 files changed, 47 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 519489097a..05938d87a2 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -55,6 +55,9 @@ void WiFiComponent::start() { uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL; this->pref_ = global_preferences->make_preference(hash, true); + if (this->fast_connect_) { + this->fast_connect_pref_ = global_preferences->make_preference(hash, false); + } SavedWifiSettings save{}; if (this->pref_.load(&save)) { @@ -78,6 +81,7 @@ void WiFiComponent::start() { if (this->fast_connect_) { this->selected_ap_ = this->sta_[0]; + this->load_fast_connect_settings_(); this->start_connecting(this->selected_ap_, false); } else { this->start_scanning(); @@ -604,6 +608,11 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; + + if (this->fast_connect_) { + this->save_fast_connect_settings_(); + } + return; } @@ -705,6 +714,35 @@ bool WiFiComponent::is_esp32_improv_active_() { #endif } +void WiFiComponent::load_fast_connect_settings_() { + SavedWifiFastConnectSettings fast_connect_save{}; + + if (this->fast_connect_pref_.load(&fast_connect_save)) { + bssid_t bssid{}; + std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + this->selected_ap_.set_bssid(bssid); + this->selected_ap_.set_channel(fast_connect_save.channel); + + ESP_LOGD(TAG, "Loaded saved fast_connect wifi settings"); + } +} + +void WiFiComponent::save_fast_connect_settings_() { + bssid_t bssid = wifi_bssid(); + uint8_t channel = wifi_channel_(); + + if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { + SavedWifiFastConnectSettings fast_connect_save{}; + + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + + this->fast_connect_pref_.save(&fast_connect_save); + + ESP_LOGD(TAG, "Saved fast_connect wifi settings"); + } +} + void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; } void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; } void WiFiAP::set_bssid(optional bssid) { this->bssid_ = bssid; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6cbdc51caf..be5095105c 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -48,6 +48,11 @@ struct SavedWifiSettings { char password[65]; } PACKED; // NOLINT +struct SavedWifiFastConnectSettings { + uint8_t bssid[6]; + uint8_t channel; +} PACKED; // NOLINT + enum WiFiComponentState { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, @@ -334,6 +339,9 @@ class WiFiComponent : public Component { bool is_captive_portal_active_(); bool is_esp32_improv_active_(); + void load_fast_connect_settings_(); + void save_fast_connect_settings_(); + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -381,6 +389,7 @@ class WiFiComponent : public Component { optional output_power_; bool passive_scan_{false}; ESPPreferenceObject pref_; + ESPPreferenceObject fast_connect_pref_; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; From 45c0d10eb091c8717af55ef4651f22316f0ec4b4 Mon Sep 17 00:00:00 2001 From: "pofilo (vmerat)" Date: Thu, 18 Jan 2024 08:35:20 +0100 Subject: [PATCH 06/17] Fixes Waveshare 7.5in B V2 and V3 (#6079) --- esphome/components/waveshare_epaper/waveshare_epaper.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 0e9b129988..244b3b1ce2 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -1443,6 +1443,12 @@ void WaveshareEPaper7P5InBV2::initialize() { // COMMAND TCON SETTING this->command(0x60); this->data(0x22); + + this->command(0x82); + this->data(0x08); + this->command(0x30); + this->data(0x06); + // COMMAND RESOLUTION SETTING this->command(0x65); this->data(0x00); @@ -1472,6 +1478,7 @@ void HOT WaveshareEPaper7P5InBV2::display() { this->command(0x12); delay(100); // NOLINT this->wait_until_idle_(); + this->deep_sleep(); } int WaveshareEPaper7P5InBV2::get_width_internal() { return 800; } int WaveshareEPaper7P5InBV2::get_height_internal() { return 480; } @@ -1617,7 +1624,7 @@ void HOT WaveshareEPaper7P5InBV3::display() { this->command(0x13); // Start Transmission delay(2); for (uint32_t i = 0; i < buf_len; i++) { - this->data(this->buffer_[i]); + this->data(~this->buffer_[i]); } this->command(0x12); // Display Refresh From 045836c3fe4e088475c248256f7b2e537cbabe05 Mon Sep 17 00:00:00 2001 From: kahrendt Date: Thu, 18 Jan 2024 04:09:49 -0500 Subject: [PATCH 07/17] Add combination sensor and remove absorbed kalman_combinator component (#5438) --- CODEOWNERS | 2 +- esphome/components/combination/__init__.py | 0 .../components/combination/combination.cpp | 262 ++++++++++++++++++ esphome/components/combination/combination.h | 141 ++++++++++ esphome/components/combination/sensor.py | 176 ++++++++++++ .../components/kalman_combinator/__init__.py | 1 - .../kalman_combinator/kalman_combinator.cpp | 82 ------ .../kalman_combinator/kalman_combinator.h | 46 --- .../components/kalman_combinator/sensor.py | 92 +----- tests/test1.yaml | 54 +++- 10 files changed, 637 insertions(+), 219 deletions(-) create mode 100644 esphome/components/combination/__init__.py create mode 100644 esphome/components/combination/combination.cpp create mode 100644 esphome/components/combination/combination.h create mode 100644 esphome/components/combination/sensor.py delete mode 100644 esphome/components/kalman_combinator/kalman_combinator.cpp delete mode 100644 esphome/components/kalman_combinator/kalman_combinator.h diff --git a/CODEOWNERS b/CODEOWNERS index 7e87679ad8..95e3b35f56 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -71,6 +71,7 @@ esphome/components/cd74hc4067/* @asoehlke esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet esphome/components/color_temperature/* @jesserockz +esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/coolix/* @glmnet esphome/components/copy/* @OttoWinter esphome/components/cover/* @esphome/core @@ -161,7 +162,6 @@ esphome/components/integration/* @OttoWinter esphome/components/internal_temperature/* @Mat931 esphome/components/interval/* @esphome/core esphome/components/json/* @OttoWinter -esphome/components/kalman_combinator/* @Cat-Ion esphome/components/key_collector/* @ssieb esphome/components/key_provider/* @ssieb esphome/components/kuntze/* @ssieb diff --git a/esphome/components/combination/__init__.py b/esphome/components/combination/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/esphome/components/combination/combination.cpp b/esphome/components/combination/combination.cpp new file mode 100644 index 0000000000..716d270390 --- /dev/null +++ b/esphome/components/combination/combination.cpp @@ -0,0 +1,262 @@ +#include "combination.h" + +#include "esphome/core/log.h" +#include "esphome/core/hal.h" + +#include +#include +#include + +namespace esphome { +namespace combination { + +static const char *const TAG = "combination"; + +void CombinationComponent::log_config_(const LogString *combo_type) { + LOG_SENSOR("", "Combination Sensor:", this); + ESP_LOGCONFIG(TAG, " Combination Type: %s", LOG_STR_ARG(combo_type)); + this->log_source_sensors(); +} + +void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); } + +void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function const &stddev) { + this->sensor_pairs_.emplace_back(sensor, stddev); +} + +void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) { + this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); +} + +void CombinationNoParameterComponent::log_source_sensors() { + ESP_LOGCONFIG(TAG, " Source Sensors:"); + for (const auto &sensor : this->sensors_) { + ESP_LOGCONFIG(TAG, " - %s", sensor->get_name().c_str()); + } +} + +void CombinationOneParameterComponent::log_source_sensors() { + ESP_LOGCONFIG(TAG, " Source Sensors:"); + for (const auto &sensor : this->sensor_pairs_) { + auto &entity = *sensor.first; + ESP_LOGCONFIG(TAG, " - %s", entity.get_name().c_str()); + } +} + +void CombinationNoParameterComponent::setup() { + for (const auto &sensor : this->sensors_) { + // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result + // repeatedly in the same loop if multiple source senors update. + sensor->add_on_state_callback( + [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + } +} + +void KalmanCombinationComponent::dump_config() { + this->log_config_(LOG_STR("kalman")); + ESP_LOGCONFIG(TAG, " Update variance: %f per ms", this->update_variance_value_); + + if (this->std_dev_sensor_ != nullptr) { + LOG_SENSOR(" ", "Standard Deviation Sensor:", this->std_dev_sensor_); + } +} + +void KalmanCombinationComponent::setup() { + for (const auto &sensor : this->sensor_pairs_) { + const auto stddev = sensor.second; + sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); + } +} + +void KalmanCombinationComponent::update_variance_() { + uint32_t now = millis(); + + // Variance increases by update_variance_ each millisecond + auto dt = now - this->last_update_; + auto dv = this->update_variance_value_ * dt; + this->variance_ += dv; + this->last_update_ = now; +} + +void KalmanCombinationComponent::correct_(float value, float stddev) { + if (std::isnan(value) || std::isinf(stddev)) { + return; + } + + if (std::isnan(this->state_) || std::isinf(this->variance_)) { + this->state_ = value; + this->variance_ = stddev * stddev; + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(stddev); + } + return; + } + + this->update_variance_(); + + // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu + // Use the value with the smaller variance as mu1 to prevent precision errors + const bool this_first = this->variance_ < (stddev * stddev); + const float mu1 = this_first ? this->state_ : value; + const float mu2 = this_first ? value : this->state_; + + const float var1 = this_first ? this->variance_ : stddev * stddev; + const float var2 = this_first ? stddev * stddev : this->variance_; + + const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); + const float var = var1 - (var1 * var1) / (var1 + var2); + + // Update and publish state + this->state_ = mu; + this->variance_ = var; + + this->publish_state(mu); + if (this->std_dev_sensor_ != nullptr) { + this->std_dev_sensor_->publish_state(std::sqrt(var)); + } +} + +void LinearCombinationComponent::setup() { + for (const auto &sensor : this->sensor_pairs_) { + // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result + // repeatedly in the same loop if multiple source senors update. + sensor.first->add_on_state_callback( + [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); }); + } +} + +void LinearCombinationComponent::handle_new_value(float value) { + // Multiplies each sensor state by a configured coeffecient and then sums + + if (!std::isfinite(value)) + return; + + float sum = 0.0; + + for (const auto &sensor : this->sensor_pairs_) { + const float sensor_state = sensor.first->state; + if (std::isfinite(sensor_state)) { + sum += sensor_state * sensor.second(sensor_state); + } + } + + this->publish_state(sum); +}; + +void MaximumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float max_value = (-1) * std::numeric_limits::infinity(); // note x = max(x, -infinity) + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + max_value = std::max(max_value, sensor->state); + } + } + + this->publish_state(max_value); +} + +void MeanCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float sum = 0.0; + size_t count = 0.0; + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + ++count; + sum += sensor->state; + } + } + + float mean = sum / count; + + this->publish_state(mean); +} + +void MedianCombinationComponent::handle_new_value(float value) { + // Sorts sensor states in ascending order and determines the middle value + + if (!std::isfinite(value)) + return; + + std::vector sensor_states; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sensor_states.push_back(sensor->state); + } + } + + sort(sensor_states.begin(), sensor_states.end()); + size_t sensor_states_size = sensor_states.size(); + + float median = NAN; + + if (sensor_states_size) { + if (sensor_states_size % 2) { + // Odd number of measurements, use middle measurement + median = sensor_states[sensor_states_size / 2]; + } else { + // Even number of measurements, use the average of the two middle measurements + median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0; + } + } + + this->publish_state(median); +} + +void MinimumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float min_value = std::numeric_limits::infinity(); // note x = min(x, infinity) + + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + min_value = std::min(min_value, sensor->state); + } + } + + this->publish_state(min_value); +} + +void MostRecentCombinationComponent::handle_new_value(float value) { this->publish_state(value); } + +void RangeCombinationComponent::handle_new_value(float value) { + // Sorts sensor states then takes difference between largest and smallest states + + if (!std::isfinite(value)) + return; + + std::vector sensor_states; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sensor_states.push_back(sensor->state); + } + } + + sort(sensor_states.begin(), sensor_states.end()); + + float range = sensor_states.back() - sensor_states.front(); + this->publish_state(range); +} + +void SumCombinationComponent::handle_new_value(float value) { + if (!std::isfinite(value)) + return; + + float sum = 0.0; + for (const auto &sensor : this->sensors_) { + if (std::isfinite(sensor->state)) { + sum += sensor->state; + } + } + + this->publish_state(sum); +} + +} // namespace combination +} // namespace esphome diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h new file mode 100644 index 0000000000..901aeaf259 --- /dev/null +++ b/esphome/components/combination/combination.h @@ -0,0 +1,141 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" + +#include + +namespace esphome { +namespace combination { + +class CombinationComponent : public Component, public sensor::Sensor { + public: + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + + /// @brief Logs all source sensor's names + virtual void log_source_sensors() = 0; + + protected: + /// @brief Logs the sensor for use in dump_config + /// @param combo_type Name of the combination operation + void log_config_(const LogString *combo_type); +}; + +/// @brief Base class for operations that do not require an extra parameter to compute the combination +class CombinationNoParameterComponent : public CombinationComponent { + public: + /// @brief Adds a callback to each source sensor + void setup() override; + + void add_source(Sensor *sensor); + + /// @brief Computes the combination + /// @param value Newest sensor measurement + virtual void handle_new_value(float value) = 0; + + /// @brief Logs all source sensor's names in sensors_ + void log_source_sensors() override; + + protected: + std::vector sensors_; +}; + +// Base class for opertions that require one parameter to compute the combination +class CombinationOneParameterComponent : public CombinationComponent { + public: + void add_source(Sensor *sensor, std::function const &stddev); + void add_source(Sensor *sensor, float stddev); + + /// @brief Logs all source sensor's names in sensor_pairs_ + void log_source_sensors() override; + + protected: + std::vector>> sensor_pairs_; +}; + +class KalmanCombinationComponent : public CombinationOneParameterComponent { + public: + void dump_config() override; + void setup() override; + + void set_process_std_dev(float process_std_dev) { + this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; + } + void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } + + protected: + void update_variance_(); + void correct_(float value, float stddev); + + // Optional sensor for publishing the current error + sensor::Sensor *std_dev_sensor_{nullptr}; + + // Tick of the last update + uint32_t last_update_{0}; + // Change of the variance, per ms + float update_variance_value_{0.f}; + + // Best guess for the state and its variance + float state_{NAN}; + float variance_{INFINITY}; +}; + +class LinearCombinationComponent : public CombinationOneParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("linear")); } + void setup() override; + + void handle_new_value(float value); +}; + +class MaximumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("max")); } + + void handle_new_value(float value) override; +}; + +class MeanCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("mean")); } + + void handle_new_value(float value) override; +}; + +class MedianCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("median")); } + + void handle_new_value(float value) override; +}; + +class MinimumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("min")); } + + void handle_new_value(float value) override; +}; + +class MostRecentCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("most_recently_updated")); } + + void handle_new_value(float value) override; +}; + +class RangeCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("range")); } + + void handle_new_value(float value) override; +}; + +class SumCombinationComponent : public CombinationNoParameterComponent { + public: + void dump_config() override { this->log_config_(LOG_STR("sum")); } + + void handle_new_value(float value) override; +}; + +} // namespace combination +} // namespace esphome diff --git a/esphome/components/combination/sensor.py b/esphome/components/combination/sensor.py new file mode 100644 index 0000000000..fad0277061 --- /dev/null +++ b/esphome/components/combination/sensor.py @@ -0,0 +1,176 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_ID, + CONF_RANGE, + CONF_SOURCE, + CONF_SUM, + CONF_TYPE, + CONF_UNIT_OF_MEASUREMENT, +) +from esphome.core.entity_helpers import inherit_property_from + +CODEOWNERS = ["@Cat-Ion", "@kahrendt"] + +combination_ns = cg.esphome_ns.namespace("combination") + +KalmanCombinationComponent = combination_ns.class_( + "KalmanCombinationComponent", cg.Component, sensor.Sensor +) +LinearCombinationComponent = combination_ns.class_( + "LinearCombinationComponent", cg.Component, sensor.Sensor +) +MaximumCombinationComponent = combination_ns.class_( + "MaximumCombinationComponent", cg.Component, sensor.Sensor +) +MeanCombinationComponent = combination_ns.class_( + "MeanCombinationComponent", cg.Component, sensor.Sensor +) +MedianCombinationComponent = combination_ns.class_( + "MedianCombinationComponent", cg.Component, sensor.Sensor +) +MinimumCombinationComponent = combination_ns.class_( + "MinimumCombinationComponent", cg.Component, sensor.Sensor +) +MostRecentCombinationComponent = combination_ns.class_( + "MostRecentCombinationComponent", cg.Component, sensor.Sensor +) +RangeCombinationComponent = combination_ns.class_( + "RangeCombinationComponent", cg.Component, sensor.Sensor +) +SumCombinationComponent = combination_ns.class_( + "SumCombinationComponent", cg.Component, sensor.Sensor +) + +CONF_COEFFECIENT = "coeffecient" +CONF_ERROR = "error" +CONF_KALMAN = "kalman" +CONF_LINEAR = "linear" +CONF_MAX = "max" +CONF_MEAN = "mean" +CONF_MEDIAN = "median" +CONF_MIN = "min" +CONF_MOST_RECENTLY_UPDATED = "most_recently_updated" +CONF_PROCESS_STD_DEV = "process_std_dev" +CONF_SOURCES = "sources" +CONF_STD_DEV = "std_dev" + + +KALMAN_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), + } +) + +LINEAR_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_), + } +) + +SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema( + { + cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), + } +) + +CONFIG_SCHEMA = cv.typed_schema( + { + CONF_KALMAN: sensor.sensor_schema(KalmanCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend( + { + cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, + cv.Required(CONF_SOURCES): cv.ensure_list(KALMAN_SOURCE_SCHEMA), + cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), + } + ), + CONF_LINEAR: sensor.sensor_schema(LinearCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(LINEAR_SOURCE_SCHEMA)}), + CONF_MAX: sensor.sensor_schema(MaximumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MEAN: sensor.sensor_schema(MeanCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MEDIAN: sensor.sensor_schema(MedianCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MIN: sensor.sensor_schema(MinimumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_MOST_RECENTLY_UPDATED: sensor.sensor_schema(MostRecentCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_RANGE: sensor.sensor_schema(RangeCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + CONF_SUM: sensor.sensor_schema(SumCombinationComponent) + .extend(cv.COMPONENT_SCHEMA) + .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}), + } +) + + +# Inherit some sensor values from the first source, for both the state and the error value +# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" +properties_to_inherit = [ + CONF_ACCURACY_DECIMALS, + CONF_DEVICE_CLASS, + CONF_ENTITY_CATEGORY, + CONF_ICON, + CONF_UNIT_OF_MEASUREMENT, +] +inherit_schema_for_state = [ + inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] +inherit_schema_for_std_dev = [ + inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) + for property in properties_to_inherit +] + +FINAL_VALIDATE_SCHEMA = cv.All( + *inherit_schema_for_state, + *inherit_schema_for_std_dev, +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await sensor.register_sensor(var, config) + + if proces_std_dev := config.get(CONF_PROCESS_STD_DEV): + cg.add(var.set_process_std_dev(proces_std_dev)) + + for source_conf in config[CONF_SOURCES]: + source = await cg.get_variable(source_conf[CONF_SOURCE]) + if config[CONF_TYPE] == CONF_KALMAN: + error = await cg.templatable( + source_conf[CONF_ERROR], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, error)) + elif config[CONF_TYPE] == CONF_LINEAR: + coeffecient = await cg.templatable( + source_conf[CONF_COEFFECIENT], + [(float, "x")], + cg.float_, + ) + cg.add(var.add_source(source, coeffecient)) + else: + cg.add(var.add_source(source)) + + if CONF_STD_DEV in config: + sens = await sensor.new_sensor(config[CONF_STD_DEV]) + cg.add(var.set_std_dev_sensor(sens)) diff --git a/esphome/components/kalman_combinator/__init__.py b/esphome/components/kalman_combinator/__init__.py index 3356e61bb2..e69de29bb2 100644 --- a/esphome/components/kalman_combinator/__init__.py +++ b/esphome/components/kalman_combinator/__init__.py @@ -1 +0,0 @@ -CODEOWNERS = ["@Cat-Ion"] diff --git a/esphome/components/kalman_combinator/kalman_combinator.cpp b/esphome/components/kalman_combinator/kalman_combinator.cpp deleted file mode 100644 index 50d8f03a93..0000000000 --- a/esphome/components/kalman_combinator/kalman_combinator.cpp +++ /dev/null @@ -1,82 +0,0 @@ -#include "kalman_combinator.h" -#include "esphome/core/hal.h" -#include -#include - -namespace esphome { -namespace kalman_combinator { - -void KalmanCombinatorComponent::dump_config() { - ESP_LOGCONFIG("kalman_combinator", "Kalman Combinator:"); - ESP_LOGCONFIG("kalman_combinator", " Update variance: %f per ms", this->update_variance_value_); - ESP_LOGCONFIG("kalman_combinator", " Sensors:"); - for (const auto &sensor : this->sensors_) { - auto &entity = *sensor.first; - ESP_LOGCONFIG("kalman_combinator", " - %s", entity.get_name().c_str()); - } -} - -void KalmanCombinatorComponent::setup() { - for (const auto &sensor : this->sensors_) { - const auto stddev = sensor.second; - sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); }); - } -} - -void KalmanCombinatorComponent::add_source(Sensor *sensor, std::function const &stddev) { - this->sensors_.emplace_back(sensor, stddev); -} - -void KalmanCombinatorComponent::add_source(Sensor *sensor, float stddev) { - this->add_source(sensor, std::function{[stddev](float x) -> float { return stddev; }}); -} - -void KalmanCombinatorComponent::update_variance_() { - uint32_t now = millis(); - - // Variance increases by update_variance_ each millisecond - auto dt = now - this->last_update_; - auto dv = this->update_variance_value_ * dt; - this->variance_ += dv; - this->last_update_ = now; -} - -void KalmanCombinatorComponent::correct_(float value, float stddev) { - if (std::isnan(value) || std::isinf(stddev)) { - return; - } - - if (std::isnan(this->state_) || std::isinf(this->variance_)) { - this->state_ = value; - this->variance_ = stddev * stddev; - if (this->std_dev_sensor_ != nullptr) { - this->std_dev_sensor_->publish_state(stddev); - } - return; - } - - this->update_variance_(); - - // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu - // Use the value with the smaller variance as mu1 to prevent precision errors - const bool this_first = this->variance_ < (stddev * stddev); - const float mu1 = this_first ? this->state_ : value; - const float mu2 = this_first ? value : this->state_; - - const float var1 = this_first ? this->variance_ : stddev * stddev; - const float var2 = this_first ? stddev * stddev : this->variance_; - - const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2); - const float var = var1 - (var1 * var1) / (var1 + var2); - - // Update and publish state - this->state_ = mu; - this->variance_ = var; - - this->publish_state(mu); - if (this->std_dev_sensor_ != nullptr) { - this->std_dev_sensor_->publish_state(std::sqrt(var)); - } -} -} // namespace kalman_combinator -} // namespace esphome diff --git a/esphome/components/kalman_combinator/kalman_combinator.h b/esphome/components/kalman_combinator/kalman_combinator.h deleted file mode 100644 index afbe3ece92..0000000000 --- a/esphome/components/kalman_combinator/kalman_combinator.h +++ /dev/null @@ -1,46 +0,0 @@ -#pragma once -#include "esphome/core/component.h" -#include "esphome/components/sensor/sensor.h" -#include -#include - -namespace esphome { -namespace kalman_combinator { - -class KalmanCombinatorComponent : public Component, public sensor::Sensor { - public: - KalmanCombinatorComponent() = default; - - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - - void dump_config() override; - void setup() override; - - void add_source(Sensor *sensor, std::function const &stddev); - void add_source(Sensor *sensor, float stddev); - void set_process_std_dev(float process_std_dev) { - this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f; - } - void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; } - - private: - void update_variance_(); - void correct_(float value, float stddev); - - // Source sensors and their error functions - std::vector>> sensors_; - - // Optional sensor for publishing the current error - sensor::Sensor *std_dev_sensor_{nullptr}; - - // Tick of the last update - uint32_t last_update_{0}; - // Change of the variance, per ms - float update_variance_value_{0.f}; - - // Best guess for the state and its variance - float state_{NAN}; - float variance_{INFINITY}; -}; -} // namespace kalman_combinator -} // namespace esphome diff --git a/esphome/components/kalman_combinator/sensor.py b/esphome/components/kalman_combinator/sensor.py index 28b96077cc..eca1ba7b85 100644 --- a/esphome/components/kalman_combinator/sensor.py +++ b/esphome/components/kalman_combinator/sensor.py @@ -1,90 +1,6 @@ -import esphome.codegen as cg import esphome.config_validation as cv -from esphome.components import sensor -from esphome.const import ( - CONF_ID, - CONF_SOURCE, - CONF_ACCURACY_DECIMALS, - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_UNIT_OF_MEASUREMENT, + +CONFIG_SCHEMA = CONFIG_SCHEMA = cv.invalid( + "The kalman_combinator sensor has moved.\nPlease use the combination platform instead with type: kalman.\n" + "See https://esphome.io/components/sensor/combination.html" ) -from esphome.core.entity_helpers import inherit_property_from - -kalman_combinator_ns = cg.esphome_ns.namespace("kalman_combinator") -KalmanCombinatorComponent = kalman_combinator_ns.class_( - "KalmanCombinatorComponent", cg.Component, sensor.Sensor -) - -CONF_ERROR = "error" -CONF_SOURCES = "sources" -CONF_PROCESS_STD_DEV = "process_std_dev" -CONF_STD_DEV = "std_dev" - - -CONFIG_SCHEMA = ( - sensor.sensor_schema(KalmanCombinatorComponent) - .extend(cv.COMPONENT_SCHEMA) - .extend( - { - cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float, - cv.Required(CONF_SOURCES): cv.ensure_list( - cv.Schema( - { - cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor), - cv.Required(CONF_ERROR): cv.templatable(cv.positive_float), - } - ), - ), - cv.Optional(CONF_STD_DEV): sensor.sensor_schema(), - } - ) -) - -# Inherit some sensor values from the first source, for both the state and the error value -properties_to_inherit = [ - CONF_ACCURACY_DECIMALS, - CONF_DEVICE_CLASS, - CONF_ENTITY_CATEGORY, - CONF_ICON, - CONF_UNIT_OF_MEASUREMENT, - # CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing" -] -inherit_schema_for_state = [ - inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE]) - for property in properties_to_inherit -] -inherit_schema_for_std_dev = [ - inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE]) - for property in properties_to_inherit -] - -FINAL_VALIDATE_SCHEMA = cv.All( - CONFIG_SCHEMA.extend( - {cv.Required(CONF_ID): cv.use_id(KalmanCombinatorComponent)}, - extra=cv.ALLOW_EXTRA, - ), - *inherit_schema_for_state, - *inherit_schema_for_std_dev, -) - - -async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await sensor.register_sensor(var, config) - - cg.add(var.set_process_std_dev(config[CONF_PROCESS_STD_DEV])) - for source_conf in config[CONF_SOURCES]: - source = await cg.get_variable(source_conf[CONF_SOURCE]) - error = await cg.templatable( - source_conf[CONF_ERROR], - [(float, "x")], - cg.float_, - ) - cg.add(var.add_source(source, error)) - - if CONF_STD_DEV in config: - sens = await sensor.new_sensor(config[CONF_STD_DEV]) - cg.add(var.set_std_dev_sensor(sens)) diff --git a/tests/test1.yaml b/tests/test1.yaml index 038ac9c738..3558fa328e 100644 --- a/tests/test1.yaml +++ b/tests/test1.yaml @@ -971,7 +971,8 @@ sensor: name: Internal Ttemperature update_interval: 15s i2c_id: i2c_bus - - platform: kalman_combinator + - platform: combination + type: kalman name: Kalman-filtered temperature process_std_dev: 0.00139 sources: @@ -980,6 +981,57 @@ sensor: return 0.4 + std::abs(x - 25) * 0.023; - source: scd4x_temperature error: 1.5 + - platform: combination + type: linear + name: Linearly combined temperatures + sources: + - source: scd30_temperature + coeffecient: !lambda |- + return 0.4 + std::abs(x - 25) * 0.023; + - source: scd4x_temperature + coeffecient: 1.5 + - platform: combination + type: max + name: Max of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: mean + name: Mean of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: median + name: Median of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: min + name: Min of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: most_recently_updated + name: Most recently updated of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: range + name: Range of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature + - platform: combination + type: sum + name: Sum of combined temperatures + sources: + - source: scd30_temperature + - source: scd4x_temperature - platform: htu21d temperature: name: Living Room Temperature 6 From ea9de45d164e32227bf4ea8b61bb475fbf426518 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:07:50 +0900 Subject: [PATCH 08/17] Bump platformio from 6.1.11 to 6.1.13 (#6086) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- docker/Dockerfile | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 468124e3ed..b28ca2ba66 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -81,7 +81,7 @@ RUN \ fi; \ pip3 install \ --break-system-packages --no-cache-dir \ - platformio==6.1.11 \ + platformio==6.1.13 \ # Change some platformio settings && platformio settings set enable_telemetry No \ && platformio settings set check_platformio_interval 1000000 \ diff --git a/requirements.txt b/requirements.txt index 5281b64e66..18e0295fb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tornado==6.4 tzlocal==5.2 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.11 # When updating platformio, also update Dockerfile +platformio==6.1.13 # When updating platformio, also update Dockerfile esptool==4.7.0 click==8.1.7 esphome-dashboard==20231107.0 From 6a6a70f1e57055ff5de8cd065fb308927db327e4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 10:08:29 +0900 Subject: [PATCH 09/17] Bump actions/cache from 3.3.2 to 4.0.0 (#6110) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2187573709..2a108b34dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: venv # yamllint disable-line rule:line-length @@ -365,7 +365,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: ~/.platformio # yamllint disable-line rule:line-length From 8267b3274ce2c801000d16eca843a7c89561d811 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:10:23 +1100 Subject: [PATCH 10/17] Enable networking and some other components on host platform (#6114) --- esphome/components/host/__init__.py | 6 +++++- esphome/components/network/ip_address.h | 16 ++++++++++++++++ esphome/components/socket/bsd_sockets_impl.cpp | 2 +- esphome/core/component.cpp | 2 +- esphome/helpers.py | 5 +++++ platformio.ini | 1 + 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/esphome/components/host/__init__.py b/esphome/components/host/__init__.py index 14d2597866..eb44bcccd6 100644 --- a/esphome/components/host/__init__.py +++ b/esphome/components/host/__init__.py @@ -6,6 +6,7 @@ from esphome.const import ( PLATFORM_HOST, ) from esphome.core import CORE +from esphome.helpers import IS_MACOS import esphome.config_validation as cv import esphome.codegen as cg @@ -14,7 +15,6 @@ from .const import KEY_HOST # force import gpio to register pin schema from .gpio import host_pin_to_code # noqa - CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["network"] @@ -35,5 +35,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): cg.add_build_flag("-DUSE_HOST") + cg.add_build_flag("-std=c++17") + cg.add_build_flag("-lsodium") + if IS_MACOS: + cg.add_build_flag("-L/opt/homebrew/lib") cg.add_define("ESPHOME_BOARD", "host") cg.add_platformio_option("platform", "platformio/native") diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 709524c9d1..02e71790a7 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -14,6 +14,13 @@ #include #endif /* USE_ADRDUINO */ +#ifdef USE_HOST +#include +using ip_addr_t = in_addr; +using ip4_addr_t = in_addr; +#define ipaddr_aton(x, y) inet_aton((x), (y)) +#endif + #if USE_ESP32_FRAMEWORK_ARDUINO #define arduino_ns Arduino_h #elif USE_LIBRETINY @@ -32,6 +39,14 @@ namespace network { struct IPAddress { public: +#ifdef USE_HOST + IPAddress() { ip_addr_.s_addr = 0; } + IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { + this->ip_addr_.s_addr = htonl((first << 24) | (second << 16) | (third << 8) | fourth); + } + IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } + IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } +#else IPAddress() { ip_addr_set_zero(&ip_addr_); } IPAddress(uint8_t first, uint8_t second, uint8_t third, uint8_t fourth) { IP_ADDR4(&ip_addr_, first, second, third, fourth); @@ -107,6 +122,7 @@ struct IPAddress { } return *this; } +#endif protected: ip_addr_t ip_addr_; diff --git a/esphome/components/socket/bsd_sockets_impl.cpp b/esphome/components/socket/bsd_sockets_impl.cpp index 6c356106f3..f07f5c8f81 100644 --- a/esphome/components/socket/bsd_sockets_impl.cpp +++ b/esphome/components/socket/bsd_sockets_impl.cpp @@ -87,7 +87,7 @@ class BSDSocketImpl : public Socket { int listen(int backlog) override { return ::listen(fd_, backlog); } ssize_t read(void *buf, size_t len) override { return ::read(fd_, buf, len); } ssize_t recvfrom(void *buf, size_t len, sockaddr *addr, socklen_t *addr_len) override { -#if defined(USE_ESP32) +#if defined(USE_ESP32) || defined(USE_HOST) return ::recvfrom(this->fd_, buf, len, 0, addr, addr_len); #else return ::lwip_recvfrom(this->fd_, buf, len, 0, addr, addr_len); diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index e2f27f9828..b0406e6502 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -169,7 +169,7 @@ float Component::get_actual_setup_priority() const { void Component::set_setup_priority(float priority) { this->setup_priority_override_ = priority; } bool Component::has_overridden_loop() const { -#ifdef CLANG_TIDY +#if defined(USE_HOST) || defined(CLANG_TIDY) bool loop_overridden = true; bool call_loop_overridden = true; #else diff --git a/esphome/helpers.py b/esphome/helpers.py index 254c950b5d..4c8cb4e2cc 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -3,6 +3,7 @@ from contextlib import suppress import logging import os +import platform from pathlib import Path from typing import Union import tempfile @@ -11,6 +12,10 @@ import re _LOGGER = logging.getLogger(__name__) +IS_MACOS = platform.system() == "Darwin" +IS_WINDOWS = platform.system() == "Windows" +IS_LINUX = platform.system() == "Linux" + def ensure_unique_string(preferred_string, current_strings): test_string = preferred_string diff --git a/platformio.ini b/platformio.ini index f5f510244c..e47527fe98 100644 --- a/platformio.ini +++ b/platformio.ini @@ -388,3 +388,4 @@ lib_deps = build_flags = ${common.build_flags} -DUSE_HOST + -std=c++17 From 2283b3b443c024fa4bb5f35d1f7310e5fd1db72a Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 12:46:55 +1100 Subject: [PATCH 11/17] Fix time component for host platform (#6118) --- esphome/components/time/real_time_clock.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/time/real_time_clock.cpp b/esphome/components/time/real_time_clock.cpp index 0573c7de9d..9b903d098b 100644 --- a/esphome/components/time/real_time_clock.cpp +++ b/esphome/components/time/real_time_clock.cpp @@ -1,6 +1,10 @@ #include "real_time_clock.h" #include "esphome/core/log.h" +#ifdef USE_HOST +#include +#else #include "lwip/opt.h" +#endif #ifdef USE_ESP8266 #include "sys/time.h" #endif @@ -25,7 +29,7 @@ void RealTimeClock::synchronize_epoch_(uint32_t epoch) { .tv_sec = static_cast(epoch), .tv_usec = 0, }; ESP_LOGVV(TAG, "Got epoch %" PRIu32, epoch); - timezone tz = {0, 0}; + struct timezone tz = {0, 0}; int ret = settimeofday(&timev, &tz); if (ret == EINVAL) { // Some ESP8266 frameworks abort when timezone parameter is not NULL From 1fef769496ed89c0062d8e70f5964b8318ba4550 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 13:42:17 +1100 Subject: [PATCH 12/17] Add quad spi features (#5925) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/spi/__init__.py | 95 +++++++++++++++++++------- esphome/components/spi/spi.cpp | 6 +- esphome/components/spi/spi.h | 51 ++++++++++++-- esphome/components/spi/spi_arduino.cpp | 3 +- esphome/components/spi/spi_esp_idf.cpp | 83 ++++++++++++++++++++-- tests/test8.1.yaml | 9 +++ 6 files changed, 207 insertions(+), 40 deletions(-) diff --git a/esphome/components/spi/__init__.py b/esphome/components/spi/__init__.py index d116641373..10ea906a92 100644 --- a/esphome/components/spi/__init__.py +++ b/esphome/components/spi/__init__.py @@ -29,12 +29,15 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040, + CONF_ALLOW_OTHER_USES, + CONF_DATA_PINS, ) from esphome.core import coroutine_with_priority, CORE CODEOWNERS = ["@esphome/core", "@clydebarrow"] spi_ns = cg.esphome_ns.namespace("spi") SPIComponent = spi_ns.class_("SPIComponent", cg.Component) +QuadSPIComponent = spi_ns.class_("QuadSPIComponent", cg.Component) SPIDevice = spi_ns.class_("SPIDevice") SPIDataRate = spi_ns.enum("SPIDataRate") SPIMode = spi_ns.enum("SPIMode") @@ -190,12 +193,9 @@ def get_hw_spi(config, available): def validate_spi_config(config): available = list(range(len(get_hw_interface_list()))) for spi in config: + # map pin number to schema + spi[CONF_CLK_PIN] = pins.gpio_output_pin_schema(spi[CONF_CLK_PIN]) interface = spi[CONF_INTERFACE] - if spi[CONF_FORCE_SW]: - if interface == "any": - spi[CONF_INTERFACE] = interface = "software" - elif interface != "software": - raise cv.Invalid("force_sw is deprecated - use interface: software") if interface == "software": pass elif interface == "any": @@ -229,6 +229,8 @@ def validate_spi_config(config): spi, spi[CONF_INTERFACE_INDEX] ): raise cv.Invalid("Invalid pin selections for hardware SPI interface") + if CONF_DATA_PINS in spi and CONF_INTERFACE_INDEX not in spi: + raise cv.Invalid("Quad mode requires a hardware interface") return config @@ -249,14 +251,26 @@ def get_spi_interface(index): return "new SPIClass(HSPI)" +# Do not use a pin schema for the number, as that will trigger a pin reuse error due to duplication of the +# clock pin in the standard and quad schemas. +clk_pin_validator = cv.maybe_simple_value( + { + cv.Required(CONF_NUMBER): cv.Any(cv.int_, cv.string), + cv.Optional(CONF_ALLOW_OTHER_USES): cv.boolean, + }, + key=CONF_NUMBER, +) + SPI_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(SPIComponent), - cv.Required(CONF_CLK_PIN): pins.gpio_output_pin_schema, + cv.Required(CONF_CLK_PIN): clk_pin_validator, cv.Optional(CONF_MISO_PIN): pins.gpio_input_pin_schema, cv.Optional(CONF_MOSI_PIN): pins.gpio_output_pin_schema, - cv.Optional(CONF_FORCE_SW, default=False): cv.boolean, + cv.Optional(CONF_FORCE_SW): cv.invalid( + "force_sw is deprecated - use interface: software" + ), cv.Optional(CONF_INTERFACE, default="any"): cv.one_of( *sum(get_hw_interface_list(), ["software", "hardware", "any"]), lower=True, @@ -267,8 +281,34 @@ SPI_SCHEMA = cv.All( cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]), ) +SPI_QUAD_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(QuadSPIComponent), + cv.Required(CONF_CLK_PIN): clk_pin_validator, + cv.Required(CONF_DATA_PINS): cv.All( + cv.ensure_list(pins.internal_gpio_output_pin_number), + cv.Length(min=4, max=4), + ), + cv.Optional(CONF_INTERFACE, default="hardware"): cv.one_of( + *sum(get_hw_interface_list(), ["hardware"]), + lower=True, + ), + } + ), + cv.only_with_esp_idf, +) + CONFIG_SCHEMA = cv.All( - cv.ensure_list(SPI_SCHEMA), + # Order is important. SPI_SCHEMA is the default. + cv.ensure_list( + cv.Any( + SPI_SCHEMA, + SPI_QUAD_SCHEMA, + msg="Standard SPI requires mosi_pin and/or miso_pin; quad SPI requires data_pins only." + + " A clock pin is always required", + ), + ), validate_spi_config, ) @@ -277,43 +317,46 @@ CONFIG_SCHEMA = cv.All( async def to_code(configs): cg.add_define("USE_SPI") cg.add_global(spi_ns.using) + if CORE.using_arduino: + cg.add_library("SPI", None) for spi in configs: var = cg.new_Pvariable(spi[CONF_ID]) await cg.register_component(var, spi) - clk = await cg.gpio_pin_expression(spi[CONF_CLK_PIN]) cg.add(var.set_clk(clk)) - if CONF_MISO_PIN in spi: - miso = await cg.gpio_pin_expression(spi[CONF_MISO_PIN]) - cg.add(var.set_miso(miso)) - if CONF_MOSI_PIN in spi: - mosi = await cg.gpio_pin_expression(spi[CONF_MOSI_PIN]) - cg.add(var.set_mosi(mosi)) - if CONF_INTERFACE_INDEX in spi: - index = spi[CONF_INTERFACE_INDEX] - cg.add(var.set_interface(cg.RawExpression(get_spi_interface(index)))) + if miso := spi.get(CONF_MISO_PIN): + cg.add(var.set_miso(await cg.gpio_pin_expression(miso))) + if mosi := spi.get(CONF_MOSI_PIN): + cg.add(var.set_mosi(await cg.gpio_pin_expression(mosi))) + if data_pins := spi.get(CONF_DATA_PINS): + cg.add(var.set_data_pins(data_pins)) + if (index := spi.get(CONF_INTERFACE_INDEX)) is not None: + interface = get_spi_interface(index) + cg.add(var.set_interface(cg.RawExpression(interface))) cg.add( var.set_interface_name( - re.sub( - r"\W", "", get_spi_interface(index).replace("new SPIClass", "") - ) + re.sub(r"\W", "", interface.replace("new SPIClass", "")) ) ) - if CORE.using_arduino: - cg.add_library("SPI", None) - def spi_device_schema( - cs_pin_required=True, default_data_rate=cv.UNDEFINED, default_mode=cv.UNDEFINED + cs_pin_required=True, + default_data_rate=cv.UNDEFINED, + default_mode=cv.UNDEFINED, + quad=False, ): """Create a schema for an SPI device. :param cs_pin_required: If true, make the CS_PIN required in the config. :param default_data_rate: Optional data_rate to use as default + :param default_mode Optional. The default SPI mode to use. + :param quad If set, will require an SPI component configured as quad data bits. :return: The SPI device schema, `extend` this in your config schema. """ schema = { - cv.GenerateID(CONF_SPI_ID): cv.use_id(SPIComponent), + cv.GenerateID(CONF_SPI_ID): cv.use_id( + QuadSPIComponent if quad else SPIComponent + ), cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA, cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum( SPI_MODE_OPTIONS, upper=True diff --git a/esphome/components/spi/spi.cpp b/esphome/components/spi/spi.cpp index 9d06ac0e45..b13826c443 100644 --- a/esphome/components/spi/spi.cpp +++ b/esphome/components/spi/spi.cpp @@ -49,7 +49,8 @@ void SPIComponent::setup() { } if (this->using_hw_) { - this->spi_bus_ = SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_); + this->spi_bus_ = + SPIComponent::get_bus(this->interface_, this->clk_pin_, this->sdo_pin_, this->sdi_pin_, this->data_pins_); if (this->spi_bus_ == nullptr) { ESP_LOGE(TAG, "Unable to allocate SPI interface"); this->mark_failed(); @@ -68,6 +69,9 @@ void SPIComponent::dump_config() { LOG_PIN(" CLK Pin: ", this->clk_pin_) LOG_PIN(" SDI Pin: ", this->sdi_pin_) LOG_PIN(" SDO Pin: ", this->sdo_pin_) + for (size_t i = 0; i != this->data_pins_.size(); i++) { + ESP_LOGCONFIG(TAG, " Data pin %u: GPIO%d", i, this->data_pins_[i]); + } if (this->spi_bus_->is_hw()) { ESP_LOGCONFIG(TAG, " Using HW SPI: %s", this->interface_name_); } else { diff --git a/esphome/components/spi/spi.h b/esphome/components/spi/spi.h index 0eb4cd7eb6..f581dc3f56 100644 --- a/esphome/components/spi/spi.h +++ b/esphome/components/spi/spi.h @@ -1,11 +1,12 @@ #pragma once +#include "esphome/core/application.h" #include "esphome/core/component.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#include "esphome/core/application.h" -#include #include +#include +#include #ifdef USE_ARDUINO @@ -208,6 +209,10 @@ class SPIDelegate { esph_log_e("spi_device", "variable length write not implemented"); } + virtual void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, + const uint8_t *data, size_t length, uint8_t bus_width) { + esph_log_e("spi_device", "write_cmd_addr_data not implemented"); + } // write 16 bits virtual void write16(uint16_t data) { if (this->bit_order_ == BIT_ORDER_MSB_FIRST) { @@ -331,6 +336,7 @@ class SPIComponent : public Component { void set_miso(GPIOPin *sdi) { this->sdi_pin_ = sdi; } void set_mosi(GPIOPin *sdo) { this->sdo_pin_ = sdo; } + void set_data_pins(std::vector pins) { this->data_pins_ = std::move(pins); } void set_interface(SPIInterface interface) { this->interface_ = interface; @@ -348,15 +354,19 @@ class SPIComponent : public Component { GPIOPin *clk_pin_{nullptr}; GPIOPin *sdi_pin_{nullptr}; GPIOPin *sdo_pin_{nullptr}; + std::vector data_pins_{}; + SPIInterface interface_{}; bool using_hw_{false}; const char *interface_name_{nullptr}; SPIBus *spi_bus_{}; std::map devices_; - static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi); + static SPIBus *get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins); }; +using QuadSPIComponent = SPIComponent; /** * Base class for SPIDevice, un-templated. */ @@ -422,18 +432,49 @@ class SPIDevice : public SPIClient { void read_array(uint8_t *data, size_t length) { return this->delegate_->read_array(data, length); } + /** + * Write a single data item, up to 32 bits. + * @param data The data + * @param num_bits The number of bits to write. The lower num_bits of data will be sent. + */ void write(uint16_t data, size_t num_bits) { this->delegate_->write(data, num_bits); }; + /* Write command, address and data. Command and address will be written as single-bit SPI, + * data phase can be multiple bit (currently only 1 or 4) + * @param cmd_bits Number of bits to write in the command phase + * @param cmd The command value to write + * @param addr_bits Number of bits to write in addr phase + * @param address Address data + * @param data Plain data bytes + * @param length Number of data bytes + * @param bus_width The number of data lines to use for the data phase. + */ + void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data, + size_t length, uint8_t bus_width = 1) { + this->delegate_->write_cmd_addr_data(cmd_bits, cmd, addr_bits, address, data, length, bus_width); + } + void write_byte(uint8_t data) { this->delegate_->write_array(&data, 1); } + /** + * Write the array data, replace with received data. + * @param data + * @param length + */ void transfer_array(uint8_t *data, size_t length) { this->delegate_->transfer(data, length); } uint8_t transfer_byte(uint8_t data) { return this->delegate_->transfer(data); } - // the driver will byte-swap if required. + /** Write 16 bit data. The driver will byte-swap if required. + */ void write_byte16(uint16_t data) { this->delegate_->write16(data); } - // avoid use of this if possible. It's inefficient and ugly. + /** + * Write an array of data as 16 bit values, byte-swapping if required. Use of this should be avoided as + * it is horribly slow. + * @param data + * @param length + */ void write_array16(const uint16_t *data, size_t length) { this->delegate_->write_array16(data, length); } void enable() { this->delegate_->begin_transaction(); } diff --git a/esphome/components/spi/spi_arduino.cpp b/esphome/components/spi/spi_arduino.cpp index 4628486550..f7fe523a33 100644 --- a/esphome/components/spi/spi_arduino.cpp +++ b/esphome/components/spi/spi_arduino.cpp @@ -85,7 +85,8 @@ class SPIBusHw : public SPIBus { bool is_hw() override { return true; } }; -SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) { +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { return new SPIBusHw(clk, sdo, sdi, interface); } diff --git a/esphome/components/spi/spi_esp_idf.cpp b/esphome/components/spi/spi_esp_idf.cpp index 03ab298019..55680f72d3 100644 --- a/esphome/components/spi/spi_esp_idf.cpp +++ b/esphome/components/spi/spi_esp_idf.cpp @@ -104,6 +104,60 @@ class SPIDelegateHw : public SPIDelegate { } } + /** + * Write command, address and data + * @param cmd_bits Number of bits to write in the command phase + * @param cmd The command value to write + * @param addr_bits Number of bits to write in addr phase + * @param address Address data + * @param data Remaining data bytes + * @param length Number of data bytes + * @param bus_width The number of data lines to use + */ + void write_cmd_addr_data(size_t cmd_bits, uint32_t cmd, size_t addr_bits, uint32_t address, const uint8_t *data, + size_t length, uint8_t bus_width) override { + spi_transaction_ext_t desc = {}; + if (length == 0 && cmd_bits == 0 && addr_bits == 0) { + esph_log_w(TAG, "Nothing to transfer"); + return; + } + desc.base.flags = SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_DUMMY; + if (bus_width == 4) { + desc.base.flags |= SPI_TRANS_MODE_QIO; + } else if (bus_width == 8) { + desc.base.flags |= SPI_TRANS_MODE_OCT; + } + desc.command_bits = cmd_bits; + desc.address_bits = addr_bits; + desc.dummy_bits = 0; + desc.base.rxlength = 0; + desc.base.cmd = cmd; + desc.base.addr = address; + do { + size_t chunk_size = std::min(length, MAX_TRANSFER_SIZE); + if (data != nullptr && chunk_size != 0) { + desc.base.length = chunk_size * 8; + desc.base.tx_buffer = data; + length -= chunk_size; + data += chunk_size; + } else { + length = 0; + desc.base.length = 0; + } + esp_err_t err = spi_device_polling_start(this->handle_, (spi_transaction_t *) &desc, portMAX_DELAY); + if (err == ESP_OK) { + err = spi_device_polling_end(this->handle_, portMAX_DELAY); + } + if (err != ESP_OK) { + ESP_LOGE(TAG, "Transmit failed - err %X", err); + return; + } + // if more data is to be sent, skip the command and address phases. + desc.command_bits = 0; + desc.address_bits = 0; + } while (length != 0); + } + void transfer(uint8_t *ptr, size_t length) override { this->transfer(ptr, ptr, length); } uint8_t transfer(uint8_t data) override { @@ -142,13 +196,27 @@ class SPIDelegateHw : public SPIDelegate { class SPIBusHw : public SPIBus { public: - SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel) : SPIBus(clk, sdo, sdi), channel_(channel) { + SPIBusHw(GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, SPIInterface channel, std::vector data_pins) + : SPIBus(clk, sdo, sdi), channel_(channel) { spi_bus_config_t buscfg = {}; - buscfg.mosi_io_num = Utility::get_pin_no(sdo); - buscfg.miso_io_num = Utility::get_pin_no(sdi); buscfg.sclk_io_num = Utility::get_pin_no(clk); - buscfg.quadwp_io_num = -1; - buscfg.quadhd_io_num = -1; + buscfg.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK; + if (data_pins.empty()) { + buscfg.mosi_io_num = Utility::get_pin_no(sdo); + buscfg.miso_io_num = Utility::get_pin_no(sdi); + buscfg.quadwp_io_num = -1; + buscfg.quadhd_io_num = -1; + } else { + buscfg.data0_io_num = data_pins[0]; + buscfg.data1_io_num = data_pins[1]; + buscfg.data2_io_num = data_pins[2]; + buscfg.data3_io_num = data_pins[3]; + buscfg.data4_io_num = -1; + buscfg.data5_io_num = -1; + buscfg.data6_io_num = -1; + buscfg.data7_io_num = -1; + buscfg.flags |= SPICOMMON_BUSFLAG_QUAD; + } buscfg.max_transfer_sz = MAX_TRANSFER_SIZE; auto err = spi_bus_initialize(channel, &buscfg, SPI_DMA_CH_AUTO); if (err != ESP_OK) @@ -166,8 +234,9 @@ class SPIBusHw : public SPIBus { bool is_hw() override { return true; } }; -SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi) { - return new SPIBusHw(clk, sdo, sdi, interface); +SPIBus *SPIComponent::get_bus(SPIInterface interface, GPIOPin *clk, GPIOPin *sdo, GPIOPin *sdi, + const std::vector &data_pins) { + return new SPIBusHw(clk, sdo, sdi, interface, data_pins); } #endif diff --git a/tests/test8.1.yaml b/tests/test8.1.yaml index bc1d2e22a4..839b1f3e6e 100644 --- a/tests/test8.1.yaml +++ b/tests/test8.1.yaml @@ -28,6 +28,15 @@ spi: allow_other_uses: false mosi_pin: GPIO6 interface: any + - id: quad_spi + clk_pin: 47 + data_pins: + - + number: 40 + allow_other_uses: false + - 41 + - 42 + - 43 spi_device: id: spidev From 6561746f97c8473c5ec4a1e65bca337a4ea736d7 Mon Sep 17 00:00:00 2001 From: alexbuit Date: Fri, 19 Jan 2024 03:50:00 +0100 Subject: [PATCH 13/17] add AM2120 device type (#6115) --- esphome/components/dht/dht.cpp | 2 +- esphome/components/dht/dht.h | 2 ++ esphome/components/dht/sensor.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 07634cafdf..5112092073 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -91,7 +91,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r delayMicroseconds(40); } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { delayMicroseconds(2000); - } else if (this->model_ == DHT_MODEL_AM2302) { + } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { delayMicroseconds(1000); } else { delayMicroseconds(800); diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index f3a29f9ce9..327e8a4f5c 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -11,6 +11,7 @@ enum DHTModel { DHT_MODEL_AUTO_DETECT = 0, DHT_MODEL_DHT11, DHT_MODEL_DHT22, + DHT_MODEL_AM2120, DHT_MODEL_AM2302, DHT_MODEL_RHT03, DHT_MODEL_SI7021, @@ -27,6 +28,7 @@ class DHT : public PollingComponent { * - DHT_MODEL_AUTO_DETECT (default) * - DHT_MODEL_DHT11 * - DHT_MODEL_DHT22 + * - DHT_MODEL_AM2120 * - DHT_MODEL_AM2302 * - DHT_MODEL_RHT03 * - DHT_MODEL_SI7021 diff --git a/esphome/components/dht/sensor.py b/esphome/components/dht/sensor.py index cd1886728e..da92a97e1f 100644 --- a/esphome/components/dht/sensor.py +++ b/esphome/components/dht/sensor.py @@ -23,6 +23,7 @@ DHT_MODELS = { "AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT, "DHT11": DHTModel.DHT_MODEL_DHT11, "DHT22": DHTModel.DHT_MODEL_DHT22, + "AM2120": DHTModel.DHT_MODEL_AM2120, "AM2302": DHTModel.DHT_MODEL_AM2302, "RHT03": DHTModel.DHT_MODEL_RHT03, "SI7021": DHTModel.DHT_MODEL_SI7021, From ed771abc8aff491cae27bade58aa847a42acdd74 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:10:53 +1100 Subject: [PATCH 14/17] Add support for Waveshare EPD 2.13" V3 (#5363) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 1 + .../components/waveshare_epaper/__init__.py | 1 + .../components/waveshare_epaper/display.py | 4 + .../waveshare_epaper/waveshare_213v3.cpp | 186 ++++++++++++++++++ .../waveshare_epaper/waveshare_epaper.cpp | 24 ++- .../waveshare_epaper/waveshare_epaper.h | 37 +++- tests/test4.yaml | 19 +- 7 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 esphome/components/waveshare_epaper/waveshare_213v3.cpp diff --git a/CODEOWNERS b/CODEOWNERS index 95e3b35f56..db44317776 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -367,6 +367,7 @@ esphome/components/veml3235/* @kbx81 esphome/components/version/* @esphome/core esphome/components/voice_assistant/* @jesserockz esphome/components/wake_on_lan/* @willwill2will54 +esphome/components/waveshare_epaper/* @clydebarrow esphome/components/web_server_base/* @OttoWinter esphome/components/web_server_idf/* @dentra esphome/components/whirlpool/* @glmnet diff --git a/esphome/components/waveshare_epaper/__init__.py b/esphome/components/waveshare_epaper/__init__.py index e69de29bb2..c58ce8a01e 100644 --- a/esphome/components/waveshare_epaper/__init__.py +++ b/esphome/components/waveshare_epaper/__init__.py @@ -0,0 +1 @@ +CODEOWNERS = ["@clydebarrow"] diff --git a/esphome/components/waveshare_epaper/display.py b/esphome/components/waveshare_epaper/display.py index 1dd4b7fc54..1645ce0b1d 100644 --- a/esphome/components/waveshare_epaper/display.py +++ b/esphome/components/waveshare_epaper/display.py @@ -72,6 +72,9 @@ WaveshareEPaper7P5InHDB = waveshare_epaper_ns.class_( WaveshareEPaper2P13InDKE = waveshare_epaper_ns.class_( "WaveshareEPaper2P13InDKE", WaveshareEPaper ) +WaveshareEPaper2P13InV3 = waveshare_epaper_ns.class_( + "WaveshareEPaper2P13InV3", WaveshareEPaper +) GDEW0154M09 = waveshare_epaper_ns.class_("GDEW0154M09", WaveshareEPaper) WaveshareEPaperTypeAModel = waveshare_epaper_ns.enum("WaveshareEPaperTypeAModel") @@ -104,6 +107,7 @@ MODELS = { "7.50inv2alt": ("b", WaveshareEPaper7P5InV2alt), "7.50in-hd-b": ("b", WaveshareEPaper7P5InHDB), "2.13in-ttgo-dke": ("c", WaveshareEPaper2P13InDKE), + "2.13inv3": ("c", WaveshareEPaper2P13InV3), "1.54in-m5coreink-m09": ("c", GDEW0154M09), } diff --git a/esphome/components/waveshare_epaper/waveshare_213v3.cpp b/esphome/components/waveshare_epaper/waveshare_213v3.cpp new file mode 100644 index 0000000000..196aeed3f7 --- /dev/null +++ b/esphome/components/waveshare_epaper/waveshare_213v3.cpp @@ -0,0 +1,186 @@ +#include "waveshare_epaper.h" +#include "esphome/core/log.h" +#include "esphome/core/application.h" + +namespace esphome { +namespace waveshare_epaper { + +static const char *const TAG = "waveshare_2.13v3"; + +static const uint8_t PARTIAL_LUT[] = { + 0x32, // cmd + 0x0, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x40, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x80, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, +}; + +static const uint8_t FULL_LUT[] = { + 0x32, // CMD + 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x80, 0x4A, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x4A, 0x80, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF, 0x0, 0x0, 0xF, 0x0, 0x0, 0x2, 0xF, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x0, 0x0, 0x0, +}; + +static const uint8_t SW_RESET = 0x12; +static const uint8_t ACTIVATE = 0x20; +static const uint8_t WRITE_BUFFER = 0x24; +static const uint8_t WRITE_BASE = 0x26; + +static const uint8_t DRV_OUT_CTL[] = {0x01, 0x27, 0x01, 0x00}; // driver output control +static const uint8_t GATEV[] = {0x03, 0x17}; +static const uint8_t SRCV[] = {0x04, 0x41, 0x0C, 0x32}; +static const uint8_t SLEEP[] = {0x10, 0x01}; +static const uint8_t DATA_ENTRY[] = {0x11, 0x03}; // data entry mode +static const uint8_t TEMP_SENS[] = {0x18, 0x80}; // Temp sensor +static const uint8_t DISPLAY_UPDATE[] = {0x21, 0x00, 0x80}; // Display update control +static const uint8_t UPSEQ[] = {0x22, 0xC0}; +static const uint8_t ON_FULL[] = {0x22, 0xC7}; +static const uint8_t ON_PARTIAL[] = {0x22, 0x0F}; +static const uint8_t VCOM[] = {0x2C, 0x36}; +static const uint8_t CMD5[] = {0x37, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00}; +static const uint8_t BORDER_PART[] = {0x3C, 0x80}; // border waveform +static const uint8_t BORDER_FULL[] = {0x3C, 0x05}; // border waveform +static const uint8_t CMD1[] = {0x3F, 0x22}; +static const uint8_t RAM_X_START[] = {0x44, 0x00, 121 / 8}; // set ram_x_address_start_end +static const uint8_t RAM_Y_START[] = {0x45, 0x00, 0x00, 250 - 1, 0}; // set ram_y_address_start_end +static const uint8_t RAM_X_POS[] = {0x4E, 0x00}; // set ram_x_address_counter +// static const uint8_t RAM_Y_POS[] = {0x4F, 0x00, 0x00}; // set ram_y_address_counter +#define SEND(x) this->cmd_data(x, sizeof(x)) + +void WaveshareEPaper2P13InV3::write_lut_(const uint8_t *lut) { + this->wait_until_idle_(); + this->cmd_data(lut, sizeof(PARTIAL_LUT)); + SEND(CMD1); + SEND(GATEV); + SEND(SRCV); + SEND(VCOM); +} + +// write the buffer starting on line top, up to line bottom. +void WaveshareEPaper2P13InV3::write_buffer_(uint8_t cmd, int top, int bottom) { + this->wait_until_idle_(); + this->set_window_(top, bottom); + this->command(cmd); + this->start_data_(); + auto width_bytes = this->get_width_internal() / 8; + this->write_array(this->buffer_ + top * width_bytes, (bottom - top) * width_bytes); + this->end_data_(); +} + +void WaveshareEPaper2P13InV3::send_reset_() { + if (this->reset_pin_ != nullptr) { + this->reset_pin_->digital_write(false); + delay(2); + this->reset_pin_->digital_write(true); + } +} + +void WaveshareEPaper2P13InV3::setup() { + setup_pins_(); + delay(20); + this->send_reset_(); + // as a one-off delay this is not worth working around. + delay(100); // NOLINT + this->wait_until_idle_(); + this->command(SW_RESET); + this->wait_until_idle_(); + + SEND(DRV_OUT_CTL); + SEND(DATA_ENTRY); + SEND(CMD5); + this->set_window_(0, this->get_height_internal()); + SEND(BORDER_FULL); + SEND(DISPLAY_UPDATE); + SEND(TEMP_SENS); + this->wait_until_idle_(); + this->write_lut_(FULL_LUT); +} + +// t and b are y positions, i.e. line numbers. +void WaveshareEPaper2P13InV3::set_window_(int t, int b) { + uint8_t buffer[3]; + + SEND(RAM_X_START); + SEND(RAM_Y_START); + SEND(RAM_X_POS); + buffer[0] = 0x4F; + buffer[1] = (uint8_t) t; + buffer[2] = (uint8_t) (t >> 8); + SEND(buffer); +} + +// must implement, but we override setup to have more control +void WaveshareEPaper2P13InV3::initialize() {} + +void WaveshareEPaper2P13InV3::partial_update_() { + this->send_reset_(); + this->set_timeout(100, [this] { + this->write_lut_(PARTIAL_LUT); + SEND(BORDER_PART); + SEND(UPSEQ); + this->command(ACTIVATE); + this->set_timeout(100, [this] { + this->wait_until_idle_(); + this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal()); + SEND(ON_PARTIAL); + this->command(ACTIVATE); // Activate Display Update Sequence + this->is_busy_ = false; + }); + }); +} + +void WaveshareEPaper2P13InV3::full_update_() { + ESP_LOGI(TAG, "Performing full e-paper update."); + this->write_lut_(FULL_LUT); + this->write_buffer_(WRITE_BUFFER, 0, this->get_height_internal()); + this->write_buffer_(WRITE_BASE, 0, this->get_height_internal()); + SEND(ON_FULL); + this->command(ACTIVATE); // don't wait here + this->is_busy_ = false; +} + +void WaveshareEPaper2P13InV3::display() { + if (this->is_busy_ || (this->busy_pin_ != nullptr && this->busy_pin_->digital_read())) + return; + this->is_busy_ = true; + const bool partial = this->at_update_ != 0; + this->at_update_ = (this->at_update_ + 1) % this->full_update_every_; + if (partial) { + this->partial_update_(); + } else { + this->full_update_(); + } +} + +int WaveshareEPaper2P13InV3::get_width_internal() { return 128; } + +int WaveshareEPaper2P13InV3::get_height_internal() { return 250; } + +uint32_t WaveshareEPaper2P13InV3::idle_timeout_() { return 5000; } + +void WaveshareEPaper2P13InV3::dump_config() { + LOG_DISPLAY("", "Waveshare E-Paper", this) + ESP_LOGCONFIG(TAG, " Model: 2.13inV3"); + LOG_PIN(" CS Pin: ", this->cs_) + LOG_PIN(" Reset Pin: ", this->reset_pin_) + LOG_PIN(" DC Pin: ", this->dc_pin_) + LOG_PIN(" Busy Pin: ", this->busy_pin_) + LOG_UPDATE_INTERVAL(this) +} + +void WaveshareEPaper2P13InV3::set_full_update_every(uint32_t full_update_every) { + this->full_update_every_ = full_update_every; +} + +} // namespace waveshare_epaper +} // namespace esphome diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.cpp b/esphome/components/waveshare_epaper/waveshare_epaper.cpp index 244b3b1ce2..b0946ad9d0 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.cpp +++ b/esphome/components/waveshare_epaper/waveshare_epaper.cpp @@ -109,8 +109,20 @@ void WaveshareEPaper::data(uint8_t value) { this->write_byte(value); this->end_data_(); } + +// write a command followed by one or more bytes of data. +// The command is the first byte, length is the total including cmd. +void WaveshareEPaper::cmd_data(const uint8_t *c_data, size_t length) { + this->dc_pin_->digital_write(false); + this->enable(); + this->write_byte(c_data[0]); + this->dc_pin_->digital_write(true); + this->write_array(c_data + 1, length - 1); + this->disable(); +} + bool WaveshareEPaper::wait_until_idle_() { - if (this->busy_pin_ == nullptr) { + if (this->busy_pin_ == nullptr || !this->busy_pin_->digital_read()) { return true; } @@ -120,7 +132,7 @@ bool WaveshareEPaper::wait_until_idle_() { ESP_LOGE(TAG, "Timeout while displaying image!"); return false; } - delay(10); + delay(1); } return true; } @@ -2218,8 +2230,9 @@ void HOT WaveshareEPaper2P13InDKE::display() { } else { // set up partial update this->command(0x32); - for (uint8_t v : PART_UPDATE_LUT_TTGO_DKE) - this->data(v); + this->start_data_(); + this->write_array(PART_UPDATE_LUT_TTGO_DKE, sizeof(PART_UPDATE_LUT_TTGO_DKE)); + this->end_data_(); this->command(0x3F); this->data(0x22); @@ -2264,12 +2277,10 @@ void HOT WaveshareEPaper2P13InDKE::display() { this->wait_until_idle_(); // data must be sent again on partial update - delay(300); // NOLINT this->command(0x24); this->start_data_(); this->write_array(this->buffer_, this->get_buffer_length_()); this->end_data_(); - delay(300); // NOLINT } ESP_LOGI(TAG, "Completed e-paper update."); @@ -2281,6 +2292,7 @@ uint32_t WaveshareEPaper2P13InDKE::idle_timeout_() { return 5000; } void WaveshareEPaper2P13InDKE::dump_config() { LOG_DISPLAY("", "Waveshare E-Paper", this); ESP_LOGCONFIG(TAG, " Model: 2.13inDKE"); + LOG_PIN(" CS Pin: ", this->cs_); LOG_PIN(" Reset Pin: ", this->reset_pin_); LOG_PIN(" DC Pin: ", this->dc_pin_); LOG_PIN(" Busy Pin: ", this->busy_pin_); diff --git a/esphome/components/waveshare_epaper/waveshare_epaper.h b/esphome/components/waveshare_epaper/waveshare_epaper.h index ee9443e8be..0f1144ccba 100644 --- a/esphome/components/waveshare_epaper/waveshare_epaper.h +++ b/esphome/components/waveshare_epaper/waveshare_epaper.h @@ -19,6 +19,7 @@ class WaveshareEPaper : public display::DisplayBuffer, void command(uint8_t value); void data(uint8_t value); + void cmd_data(const uint8_t *data, size_t length); virtual void display() = 0; virtual void initialize() = 0; @@ -49,7 +50,7 @@ class WaveshareEPaper : public display::DisplayBuffer, this->reset_pin_->digital_write(false); delay(reset_duration_); // NOLINT this->reset_pin_->digital_write(true); - delay(200); // NOLINT + delay(20); } } @@ -614,5 +615,39 @@ class WaveshareEPaper2P13InDKE : public WaveshareEPaper { uint32_t at_update_{0}; }; +class WaveshareEPaper2P13InV3 : public WaveshareEPaper { + public: + void display() override; + + void dump_config() override; + + void deep_sleep() override { + // COMMAND POWER DOWN + this->command(0x10); + this->data(0x01); + // cannot wait until idle here, the device no longer responds + } + + void set_full_update_every(uint32_t full_update_every); + + void setup() override; + void initialize() override; + + protected: + int get_width_internal() override; + int get_height_internal() override; + uint32_t idle_timeout_() override; + + void write_buffer_(uint8_t cmd, int top, int bottom); + void set_window_(int t, int b); + void send_reset_(); + void partial_update_(); + void full_update_(); + + uint32_t full_update_every_{30}; + uint32_t at_update_{0}; + bool is_busy_{false}; + void write_lut_(const uint8_t *lut); +}; } // namespace waveshare_epaper } // namespace esphome diff --git a/tests/test4.yaml b/tests/test4.yaml index 089caf073b..65068871dd 100644 --- a/tests/test4.yaml +++ b/tests/test4.yaml @@ -693,7 +693,6 @@ display: greyscale: false partial_updating: false update_interval: 60s - display_data_1_pin: number: GPIO5 allow_other_uses: true @@ -742,6 +741,24 @@ display: vcom_pin: number: GPIO1 allow_other_uses: true + - platform: waveshare_epaper + spi_id: spi_id_1 + cs_pin: + number: GPIO23 + allow_other_uses: true + dc_pin: + number: GPIO23 + allow_other_uses: true + busy_pin: + number: GPIO23 + allow_other_uses: true + reset_pin: + number: GPIO23 + allow_other_uses: true + model: 2.13inv3 + full_update_every: 30 + lambda: |- + it.rectangle(0, 0, it.get_width(), it.get_height()); number: - platform: tuya From 6a8da17ea39146e5aedacdcc051fca1fd7c1cd51 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Fri, 19 Jan 2024 05:18:06 +0100 Subject: [PATCH 15/17] OTA 2 which confirm each written chunk (#6066) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- esphome/components/ota/__init__.py | 3 ++ esphome/components/ota/ota_component.cpp | 16 ++++++-- esphome/components/ota/ota_component.h | 47 +++++++++++----------- esphome/core/defines.h | 1 + esphome/espota2.py | 51 +++++++++++++----------- tests/test3.1.yaml | 1 + 6 files changed, 70 insertions(+), 49 deletions(-) diff --git a/esphome/components/ota/__init__.py b/esphome/components/ota/__init__.py index 039596d897..3c845490dc 100644 --- a/esphome/components/ota/__init__.py +++ b/esphome/components/ota/__init__.py @@ -12,6 +12,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_OTA, KEY_PAST_SAFE_MODE, + CONF_VERSION, ) from esphome.core import CORE, coroutine_with_priority @@ -41,6 +42,7 @@ CONFIG_SCHEMA = cv.Schema( { cv.GenerateID(): cv.declare_id(OTAComponent), cv.Optional(CONF_SAFE_MODE, default=True): cv.boolean, + cv.Optional(CONF_VERSION, default=2): cv.one_of(1, 2, int=True), cv.SplitDefault( CONF_PORT, esp8266=8266, @@ -93,6 +95,7 @@ async def to_code(config): if CONF_PASSWORD in config: cg.add(var.set_auth_password(config[CONF_PASSWORD])) cg.add_define("USE_OTA_PASSWORD") + cg.add_define("USE_OTA_VERSION", config[CONF_VERSION]) await cg.register_component(var, config) diff --git a/esphome/components/ota/ota_component.cpp b/esphome/components/ota/ota_component.cpp index 41cf333be9..15af14ff1a 100644 --- a/esphome/components/ota/ota_component.cpp +++ b/esphome/components/ota/ota_component.cpp @@ -20,8 +20,7 @@ namespace esphome { namespace ota { static const char *const TAG = "ota"; - -static const uint8_t OTA_VERSION_1_0 = 1; +static constexpr u_int16_t OTA_BLOCK_SIZE = 8192; OTAComponent *global_ota_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) @@ -101,6 +100,7 @@ void OTAComponent::dump_config() { ESP_LOGCONFIG(TAG, " Using Password."); } #endif + ESP_LOGCONFIG(TAG, " OTA version: %d.", USE_OTA_VERSION); if (this->has_safe_mode_ && this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != esphome::ota::OTAComponent::ENTER_SAFE_MODE_MAGIC) { ESP_LOGW(TAG, "Last Boot was an unhandled reset, will proceed to safe mode in %" PRIu32 " restarts", @@ -132,6 +132,9 @@ void OTAComponent::handle_() { uint8_t ota_features; std::unique_ptr backend; (void) ota_features; +#if USE_OTA_VERSION == 2 + size_t size_acknowledged = 0; +#endif if (client_ == nullptr) { struct sockaddr_storage source_addr; @@ -168,7 +171,7 @@ void OTAComponent::handle_() { // Send OK and version - 2 bytes buf[0] = OTA_RESPONSE_OK; - buf[1] = OTA_VERSION_1_0; + buf[1] = USE_OTA_VERSION; this->writeall_(buf, 2); backend = make_ota_backend(); @@ -312,6 +315,13 @@ void OTAComponent::handle_() { goto error; // NOLINT(cppcoreguidelines-avoid-goto) } total += read; +#if USE_OTA_VERSION == 2 + while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { + buf[0] = OTA_RESPONSE_CHUNK_OK; + this->writeall_(buf, 1); + size_acknowledged += OTA_BLOCK_SIZE; + } +#endif uint32_t now = millis(); if (now - last_progress > 1000) { diff --git a/esphome/components/ota/ota_component.h b/esphome/components/ota/ota_component.h index 50d095be6c..c20f4f0709 100644 --- a/esphome/components/ota/ota_component.h +++ b/esphome/components/ota/ota_component.h @@ -10,31 +10,32 @@ namespace esphome { namespace ota { enum OTAResponseTypes { - OTA_RESPONSE_OK = 0, - OTA_RESPONSE_REQUEST_AUTH = 1, + OTA_RESPONSE_OK = 0x00, + OTA_RESPONSE_REQUEST_AUTH = 0x01, - OTA_RESPONSE_HEADER_OK = 64, - OTA_RESPONSE_AUTH_OK = 65, - OTA_RESPONSE_UPDATE_PREPARE_OK = 66, - OTA_RESPONSE_BIN_MD5_OK = 67, - OTA_RESPONSE_RECEIVE_OK = 68, - OTA_RESPONSE_UPDATE_END_OK = 69, - OTA_RESPONSE_SUPPORTS_COMPRESSION = 70, + OTA_RESPONSE_HEADER_OK = 0x40, + OTA_RESPONSE_AUTH_OK = 0x41, + OTA_RESPONSE_UPDATE_PREPARE_OK = 0x42, + OTA_RESPONSE_BIN_MD5_OK = 0x43, + OTA_RESPONSE_RECEIVE_OK = 0x44, + OTA_RESPONSE_UPDATE_END_OK = 0x45, + OTA_RESPONSE_SUPPORTS_COMPRESSION = 0x46, + OTA_RESPONSE_CHUNK_OK = 0x47, - OTA_RESPONSE_ERROR_MAGIC = 128, - OTA_RESPONSE_ERROR_UPDATE_PREPARE = 129, - OTA_RESPONSE_ERROR_AUTH_INVALID = 130, - OTA_RESPONSE_ERROR_WRITING_FLASH = 131, - OTA_RESPONSE_ERROR_UPDATE_END = 132, - OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133, - OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134, - OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135, - OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136, - OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137, - OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 138, - OTA_RESPONSE_ERROR_MD5_MISMATCH = 139, - OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 140, - OTA_RESPONSE_ERROR_UNKNOWN = 255, + OTA_RESPONSE_ERROR_MAGIC = 0x80, + OTA_RESPONSE_ERROR_UPDATE_PREPARE = 0x81, + OTA_RESPONSE_ERROR_AUTH_INVALID = 0x82, + OTA_RESPONSE_ERROR_WRITING_FLASH = 0x83, + OTA_RESPONSE_ERROR_UPDATE_END = 0x84, + OTA_RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85, + OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86, + OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87, + OTA_RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88, + OTA_RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89, + OTA_RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A, + OTA_RESPONSE_ERROR_MD5_MISMATCH = 0x8B, + OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE = 0x8C, + OTA_RESPONSE_ERROR_UNKNOWN = 0xFF, }; enum OTAState { OTA_COMPLETED = 0, OTA_STARTED, OTA_IN_PROGRESS, OTA_ERROR }; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e75abdb88f..75ed24ddfe 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -37,6 +37,7 @@ #define USE_OTA #define USE_OTA_PASSWORD #define USE_OTA_STATE_CALLBACK +#define USE_OTA_VERSION 1 #define USE_OUTPUT #define USE_POWER_SUPPLY #define USE_QR_CODE diff --git a/esphome/espota2.py b/esphome/espota2.py index dbf48a989a..cdf6d7df32 100644 --- a/esphome/espota2.py +++ b/esphome/espota2.py @@ -12,32 +12,34 @@ import time from esphome.core import EsphomeError from esphome.helpers import is_ip_address, resolve_ip_address -RESPONSE_OK = 0 -RESPONSE_REQUEST_AUTH = 1 +RESPONSE_OK = 0x00 +RESPONSE_REQUEST_AUTH = 0x01 -RESPONSE_HEADER_OK = 64 -RESPONSE_AUTH_OK = 65 -RESPONSE_UPDATE_PREPARE_OK = 66 -RESPONSE_BIN_MD5_OK = 67 -RESPONSE_RECEIVE_OK = 68 -RESPONSE_UPDATE_END_OK = 69 -RESPONSE_SUPPORTS_COMPRESSION = 70 +RESPONSE_HEADER_OK = 0x40 +RESPONSE_AUTH_OK = 0x41 +RESPONSE_UPDATE_PREPARE_OK = 0x42 +RESPONSE_BIN_MD5_OK = 0x43 +RESPONSE_RECEIVE_OK = 0x44 +RESPONSE_UPDATE_END_OK = 0x45 +RESPONSE_SUPPORTS_COMPRESSION = 0x46 +RESPONSE_CHUNK_OK = 0x47 -RESPONSE_ERROR_MAGIC = 128 -RESPONSE_ERROR_UPDATE_PREPARE = 129 -RESPONSE_ERROR_AUTH_INVALID = 130 -RESPONSE_ERROR_WRITING_FLASH = 131 -RESPONSE_ERROR_UPDATE_END = 132 -RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 133 -RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 134 -RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 135 -RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 136 -RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 137 -RESPONSE_ERROR_NO_UPDATE_PARTITION = 138 -RESPONSE_ERROR_MD5_MISMATCH = 139 -RESPONSE_ERROR_UNKNOWN = 255 +RESPONSE_ERROR_MAGIC = 0x80 +RESPONSE_ERROR_UPDATE_PREPARE = 0x81 +RESPONSE_ERROR_AUTH_INVALID = 0x82 +RESPONSE_ERROR_WRITING_FLASH = 0x83 +RESPONSE_ERROR_UPDATE_END = 0x84 +RESPONSE_ERROR_INVALID_BOOTSTRAPPING = 0x85 +RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG = 0x86 +RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG = 0x87 +RESPONSE_ERROR_ESP8266_NOT_ENOUGH_SPACE = 0x88 +RESPONSE_ERROR_ESP32_NOT_ENOUGH_SPACE = 0x89 +RESPONSE_ERROR_NO_UPDATE_PARTITION = 0x8A +RESPONSE_ERROR_MD5_MISMATCH = 0x8B +RESPONSE_ERROR_UNKNOWN = 0xFF OTA_VERSION_1_0 = 1 +OTA_VERSION_2_0 = 2 MAGIC_BYTES = [0x6C, 0x26, 0xF7, 0x5C, 0x45] @@ -203,7 +205,8 @@ def perform_ota( send_check(sock, MAGIC_BYTES, "magic bytes") _, version = receive_exactly(sock, 2, "version", RESPONSE_OK) - if version != OTA_VERSION_1_0: + _LOGGER.debug("Device support OTA version: %s", version) + if version not in (OTA_VERSION_1_0, OTA_VERSION_2_0): raise OTAError(f"Unsupported OTA version {version}") # Features @@ -279,6 +282,8 @@ def perform_ota( try: sock.sendall(chunk) + if version >= OTA_VERSION_2_0: + receive_exactly(sock, 1, "chunk OK", RESPONSE_CHUNK_OK) except OSError as err: sys.stderr.write("\n") raise OTAError(f"Error sending data: {err}") from err diff --git a/tests/test3.1.yaml b/tests/test3.1.yaml index b5428abbfa..5cbdca91c1 100644 --- a/tests/test3.1.yaml +++ b/tests/test3.1.yaml @@ -49,6 +49,7 @@ spi: number: GPIO14 ota: + version: 2 logger: From 2f09624c07d8b24c174b58ea27230f459b65df08 Mon Sep 17 00:00:00 2001 From: Stefan Rado <628587+kroimon@users.noreply.github.com> Date: Fri, 19 Jan 2024 14:30:57 +0100 Subject: [PATCH 16/17] Remove optional<> for pointer types (#6120) --- esphome/components/bedjet/bedjet_hub.cpp | 12 +++++------- esphome/components/bedjet/bedjet_hub.h | 2 +- esphome/components/tuya/tuya.cpp | 20 ++++++++------------ esphome/components/tuya/tuya.h | 4 ++-- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/esphome/components/bedjet/bedjet_hub.cpp b/esphome/components/bedjet/bedjet_hub.cpp index 7933a35a97..6404298697 100644 --- a/esphome/components/bedjet/bedjet_hub.cpp +++ b/esphome/components/bedjet/bedjet_hub.cpp @@ -242,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga this->set_notify_(true); #ifdef USE_TIME - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time(); } #endif @@ -441,9 +441,8 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { #ifdef USE_TIME void BedJetHub::send_local_time() { - if (this->time_id_.has_value()) { - auto *time_id = *this->time_id_; - ESPTime now = time_id->now(); + if (this->time_id_ != nullptr) { + ESPTime now = this->time_id_->now(); if (now.is_valid()) { this->set_clock(now.hour, now.minute); ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); @@ -454,10 +453,9 @@ void BedJetHub::send_local_time() { } void BedJetHub::setup_time_() { - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time(); - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); + this->time_id_->add_on_time_sync_callback([this] { this->send_local_time(); }); } else { ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); } diff --git a/esphome/components/bedjet/bedjet_hub.h b/esphome/components/bedjet/bedjet_hub.h index bb1349b2ac..6258795b02 100644 --- a/esphome/components/bedjet/bedjet_hub.h +++ b/esphome/components/bedjet/bedjet_hub.h @@ -141,7 +141,7 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo #ifdef USE_TIME /** Initializes time sync callbacks to support syncing current time to the BedJet. */ void setup_time_(); - optional time_id_{}; + time::RealTimeClock *time_id_{nullptr}; #endif uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index da03e3faad..1cc9681d09 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -23,8 +23,8 @@ static const int MAX_RETRIES = 5; void Tuya::setup() { this->set_interval("heartbeat", 15000, [this] { this->send_empty_command_(TuyaCommandType::HEARTBEAT); }); - if (this->status_pin_.has_value()) { - this->status_pin_.value()->digital_write(false); + if (this->status_pin_ != nullptr) { + this->status_pin_->digital_write(false); } } @@ -70,9 +70,7 @@ void Tuya::dump_config() { ESP_LOGCONFIG(TAG, " GPIO Configuration: status: pin %d, reset: pin %d", this->status_pin_reported_, this->reset_pin_reported_); } - if (this->status_pin_.has_value()) { - LOG_PIN(" Status Pin: ", this->status_pin_.value()); - } + LOG_PIN(" Status Pin: ", this->status_pin_); ESP_LOGCONFIG(TAG, " Product: '%s'", this->product_.c_str()); } @@ -194,7 +192,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff this->init_state_ = TuyaInitState::INIT_DATAPOINT; this->send_empty_command_(TuyaCommandType::DATAPOINT_QUERY); bool is_pin_equals = - this->status_pin_.has_value() && this->status_pin_.value()->get_pin() == this->status_pin_reported_; + this->status_pin_ != nullptr && this->status_pin_->get_pin() == this->status_pin_reported_; // Configure status pin toggling (if reported and configured) or WIFI_STATE periodic send if (is_pin_equals) { ESP_LOGV(TAG, "Configured status pin %i", this->status_pin_reported_); @@ -244,13 +242,12 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff break; case TuyaCommandType::LOCAL_TIME_QUERY: #ifdef USE_TIME - if (this->time_id_.has_value()) { + if (this->time_id_ != nullptr) { this->send_local_time_(); if (!this->time_sync_callback_registered_) { // tuya mcu supports time, so we let them know when our time changed - auto *time_id = *this->time_id_; - time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); + this->time_id_->add_on_time_sync_callback([this] { this->send_local_time_(); }); this->time_sync_callback_registered_ = true; } } else @@ -463,7 +460,7 @@ void Tuya::send_empty_command_(TuyaCommandType command) { void Tuya::set_status_pin_() { bool is_network_ready = network::is_connected() && remote_is_connected(); - this->status_pin_.value()->digital_write(is_network_ready); + this->status_pin_->digital_write(is_network_ready); } uint8_t Tuya::get_wifi_status_code_() { @@ -511,8 +508,7 @@ void Tuya::send_wifi_status_() { #ifdef USE_TIME void Tuya::send_local_time_() { std::vector payload; - auto *time_id = *this->time_id_; - ESPTime now = time_id->now(); + ESPTime now = this->time_id_->now(); if (now.is_valid()) { uint8_t year = now.year - 2000; uint8_t month = now.month; diff --git a/esphome/components/tuya/tuya.h b/esphome/components/tuya/tuya.h index 27a97c3dc9..7dc405e3dd 100644 --- a/esphome/components/tuya/tuya.h +++ b/esphome/components/tuya/tuya.h @@ -130,14 +130,14 @@ class Tuya : public Component, public uart::UARTDevice { #ifdef USE_TIME void send_local_time_(); - optional time_id_{}; + time::RealTimeClock *time_id_{nullptr}; bool time_sync_callback_registered_{false}; #endif TuyaInitState init_state_ = TuyaInitState::INIT_HEARTBEAT; bool init_failed_{false}; int init_retries_{0}; uint8_t protocol_version_ = -1; - optional status_pin_{}; + InternalGPIOPin *status_pin_{nullptr}; int status_pin_reported_ = -1; int reset_pin_reported_ = -1; uint32_t last_command_timestamp_ = 0; From 0cbc06a9b91fd16e6dd9a48e75569b409039a957 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Sat, 20 Jan 2024 03:38:37 +1300 Subject: [PATCH 17/17] Fix some Voice Assistant bugs (#6121) --- esphome/components/voice_assistant/voice_assistant.cpp | 10 ++++++---- esphome/core/ring_buffer.cpp | 9 +++++---- esphome/core/ring_buffer.h | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 299e624f5f..9094b93c02 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -17,7 +17,7 @@ static const char *const TAG = "voice_assistant"; static const size_t SAMPLE_RATE_HZ = 16000; static const size_t INPUT_BUFFER_SIZE = 32 * SAMPLE_RATE_HZ / 1000; // 32ms * 16kHz / 1000ms -static const size_t BUFFER_SIZE = 1000 * SAMPLE_RATE_HZ / 1000; // 1s +static const size_t BUFFER_SIZE = 1024 * SAMPLE_RATE_HZ / 1000; static const size_t SEND_BUFFER_SIZE = INPUT_BUFFER_SIZE * sizeof(int16_t); static const size_t RECEIVE_SIZE = 1024; static const size_t SPEAKER_BUFFER_SIZE = 16 * RECEIVE_SIZE; @@ -231,10 +231,12 @@ void VoiceAssistant::loop() { } case State::STREAMING_MICROPHONE: { this->read_microphone_(); - if (this->ring_buffer_->available() >= SEND_BUFFER_SIZE) { - this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); - this->socket_->sendto(this->send_buffer_, SEND_BUFFER_SIZE, 0, (struct sockaddr *) &this->dest_addr_, + size_t available = this->ring_buffer_->available(); + while (available >= SEND_BUFFER_SIZE) { + size_t read_bytes = this->ring_buffer_->read((void *) this->send_buffer_, SEND_BUFFER_SIZE, 0); + this->socket_->sendto(this->send_buffer_, read_bytes, 0, (struct sockaddr *) &this->dest_addr_, sizeof(this->dest_addr_)); + available = this->ring_buffer_->available(); } break; diff --git a/esphome/core/ring_buffer.cpp b/esphome/core/ring_buffer.cpp index d9c56d84c5..9bd3d9d853 100644 --- a/esphome/core/ring_buffer.cpp +++ b/esphome/core/ring_buffer.cpp @@ -15,17 +15,18 @@ std::unique_ptr RingBuffer::create(size_t len) { std::unique_ptr rb = make_unique(); ExternalRAMAllocator allocator(ExternalRAMAllocator::ALLOW_FAILURE); - rb->storage_ = allocator.allocate(len); + rb->storage_ = allocator.allocate(len + 1); if (rb->storage_ == nullptr) { return nullptr; } - rb->handle_ = xStreamBufferCreateStatic(len, 0, rb->storage_, &rb->structure_); + rb->handle_ = xStreamBufferCreateStatic(len + 1, 0, rb->storage_, &rb->structure_); + ESP_LOGD(TAG, "Created ring buffer with size %u", len); return rb; } -size_t RingBuffer::read(void *data, size_t size, TickType_t ticks_to_wait) { - return xStreamBufferReceive(this->handle_, data, size, ticks_to_wait); +size_t RingBuffer::read(void *data, size_t len, TickType_t ticks_to_wait) { + return xStreamBufferReceive(this->handle_, data, len, ticks_to_wait); } size_t RingBuffer::write(void *data, size_t len) { diff --git a/esphome/core/ring_buffer.h b/esphome/core/ring_buffer.h index 6c6d04117a..e602068844 100644 --- a/esphome/core/ring_buffer.h +++ b/esphome/core/ring_buffer.h @@ -12,7 +12,7 @@ namespace esphome { class RingBuffer { public: - size_t read(void *data, size_t size, TickType_t ticks_to_wait = 0); + size_t read(void *data, size_t len, TickType_t ticks_to_wait = 0); size_t write(void *data, size_t len);