Compare commits

...
Author SHA1 Message Date
J. Nick Koston 439f79f0b4 Merge remote-tracking branch 'origin/dev' into esp32-log-v2-idf61
# Conflicts:
#	esphome/core/defines.h
2026-10-14 08:17:12 -10:00
91e0edc11d [pn532] Fix NDEF parsing greater than 255 bytes (#18188)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:15:48 -10:00
Jonathan Swoboda 9fe75e262f [ci] Add a host clang-tidy environment and job (#20747) 2026-10-14 08:09:53 -10:00
Kevin Ahrendt 0132f722a3 [audio] Bump esp-audio-libs to v4.1.0 (#20754) 2026-10-14 13:24:36 -04:00
Jonathan Swoboda 7d7a78308d [ci] Test tinyusb_keyboard in isolation (#20749) 2026-10-14 10:05:02 -04:00
J. Nick Koston 9c568525b0 [core] Add CodSpeed benchmarks for importing entity components (#20648) 2026-10-14 09:08:41 -04:00
J. Nick Koston 10f42e3e7c [shelly_dimmer] Build the STM32 firmware update path in CI (#20677) 2026-10-14 09:03:34 -04:00
J. Nick Koston 25d4b88605 [nfc] Add unit tests for the URI record prefix codes (#20724) 2026-10-14 09:01:59 -04:00
J. Nick Koston d6abf35138 [sx126x][sx127x] Stop the watchdog reset loop when a packet is sent before setup (#20727) 2026-10-14 09:01:10 -04:00
J. Nick Koston 66ea99ce65 [seeed_mr60fda2] Compile without binary sensors or selects (#20726) 2026-10-14 09:00:08 -04:00
J. Nick Koston d9298cb7b9 [seeed_mr60bha2] Compile without sensors or binary sensors (#20729) 2026-10-14 08:59:27 -04:00
J. Nick Koston 34e1cf96d5 [bmi270] Build the component on ESP8266 in CI (#20731) 2026-10-14 08:58:56 -04:00
56cc03c4d4 [haier] Haier Select component added (#16148)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Pavlo Dudnytskyi <Pavlo.Dudnytskyi@addsecure.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 23:27:18 -10:00
35b416c6c0 [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 22:19:06 -10:00
Petar Petrov 7f2c3ba3f5 [display] Let other components draw into a display (#20622) 2026-10-14 06:11:47 +00:00
Keith BurzinskiandClaude Opus 5.5 a97604d574 [esp32_ble] Rely on preferences for NVS init (#20708)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:51 -10:00
Petar Petrov 1146b77836 [socket] Add TcpClientLink::connecting() (#20722) 2026-10-13 19:19:17 -10:00
J. Nick Koston 1338902ac5 [heatpumpir] Link only the configured protocol classes (#20707) 2026-10-14 17:06:22 +13:00
60a1d70805 [core] Move deferred include processing to parse_yaml (#16344)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 17:46:54 -10:00
Ryan LudwigandCursor 887d4c3a83 [esp32_can] Add NO_ACK mode (#20706)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 17:36:15 -10:00
J. Nick Koston e217df8d41 Drop the temporary IDF 6.1 pins, guard the V2 block on the IDF version, cover V2 with a logger test pinned to 6.1.0 2026-10-02 08:31:10 -05:00
J. Nick Koston 003698970e Merge remote-tracking branch 'origin/dev' into esp32-log-v2-idf61 2026-10-02 08:14:41 -05:00
J. Nick Koston f8208b8fc2 Merge remote-tracking branch 'origin/dev' into esp32-log-v2-idf61
# Conflicts:
#	platformio.ini
2026-10-01 15:24:39 -05:00
J. Nick Koston 18ec9dd85b Merge remote-tracking branch 'upstream/dev' into HEAD 2026-09-17 08:49:35 -05:00
J. Nick Koston f12f5d3a1f [noise] Initialize reserved field for gcc 15
gcc 15 extends -Wmissing-field-initializers to designated
initializers; NoiseProtocolId's reserved array was omitted.
2026-08-26 21:59:10 -05:00
J. Nick Koston 33a9439d8f [esp32_ble_client] Value initialize conn params for IDF 6.1
gcc 15 on IDF 6.1 rejects the partial {{0}} initializer of
esp_ble_conn_update_params_t under -Werror=missing-field-initializers;
empty braces value initialize the whole struct. Same fix in
bluetooth_connection.
2026-08-26 21:51:44 -05:00
J. Nick Koston c0ed06215a TEMP: point IDF 6.1 PlatformIO builds at forked platform and framework
pioarduino has no 6.1 packaging, and only 6.1 has the Log V2 option,
so the PlatformIO toolchain uses a repack of espressif v6.1 plus a
platform fork carrying the 6.1 fixes: newer idf-component-manager and
esp-idf-kconfig, the bootloader ld include dir, dropping app --wrap
flags from the bootloader link, and linking the transitive
tf-psa-crypto sublibs. Revert with the 6.1 default before merge.
2026-08-26 21:44:37 -05:00
J. Nick Koston 7235c2a8d6 [logger] Move the Log V2 wrap into the logger component
The Log V2 enable block and the esp_log_format wrap belong to the
logger: the wrap routes IDF output through the logger's hook, and the
logger already owns the esp8266 ets_putc wrap and esp32 console
sdkconfig options. This also drops the CORE.config peek at the logger
key from the esp32 platform. The direct formatter now reuses LogBuffer
and the logger's level tables instead of hand rolled equivalents, uses
a smaller stack buffer, and the hot path tests the common case first.
2026-08-26 21:44:28 -05:00
J. Nick Koston 07b8bb53f8 TEMP: default to ESP-IDF 6.1.0 for CI testing of Log V2
Sets recommended/latest/dev to 6.1.0, maps it to the pioarduino
prep_IDF6 platform branch, and points the PlatformIO framework source
at a pioarduino style repack of espressif v6.1 hosted at
bdraco/esp-idf, since pioarduino has not published 6.1 yet. The native
toolchain needs no hosting; esphome-libs/esp-idf already has v6.1.
Revert this commit and restore 5.5.5 before merge.
2026-08-26 21:17:29 -05:00
J. Nick Koston 6d1cd3f62e [esp32] Intercept the Log V2 formatter with linker wrap
A plain strong definition of esp_log_format collides with liblog's copy
on IDF 6.1: log.c.obj references the symbol and the linker resolves it
from log_format_text.c.obj inside the same archive before reaching
ESPHome's archive, producing a duplicate definition error. Use
-Wl,--wrap=esp_log_format and define __wrap_esp_log_format instead,
which also lets the unused liblog formatter be dead stripped.
2026-08-26 21:17:21 -05:00
J. Nick Koston 5cbf994b28 Merge upstream/dev into esp32-log-v2-idf61 2026-08-26 21:00:30 -05:00
J. Nick Koston 6cf30012a5 Merge branch 'dev' into esp32-log-v2-idf61 2026-07-01 21:42:02 -05:00
J. Nick Koston beff1dec4f [esp32] Clamp header length in the Log V2 direct formatter 2026-07-01 20:26:55 -05:00
J. Nick Koston e5e92b1e22 [esp32] Simplify custom hook forwarding in the Log V2 override 2026-07-01 20:21:02 -05:00
J. Nick Koston f843cc81c6 [esp32] Drop IRAM_ATTR from the Log V2 esp_log_format override 2026-07-01 20:17:07 -05:00
J. Nick Koston e8e5f6366d [esp32] Only enable Log V2 when the logger component is configured 2026-07-01 20:06:03 -05:00
J. Nick Koston a00982af2a [esp32] Skip the Log V2 format override when the logger component is absent 2026-07-01 20:02:46 -05:00
J. Nick Koston e0e0e3cb8f [esp32] Route Log V2 messages to the logger with real tag and severity 2026-07-01 19:45:21 -05:00
J. Nick Koston 20bda75684 [esp32] Preserve IDF component tag on routed Log V2 messages 2026-07-01 19:28:22 -05:00
J. Nick Koston 8a4959eb6c [esp32] Fix clang-tidy identifier naming in log.cpp V2 override 2026-07-01 19:25:18 -05:00
J. Nick Koston d0bb1e5a98 [esp32] Enable ESP-IDF Log V2 on IDF 6.1+ 2026-07-01 18:52:48 -05:00
90 changed files with 1613 additions and 235 deletions
+20 -6
View File
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
determine-jobs:
name: Determine which jobs to run
@@ -415,8 +415,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -695,6 +695,10 @@ jobs:
name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2
pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy
name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
@@ -707,6 +711,16 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python
id: restore-python
@@ -1102,8 +1116,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+1
View File
@@ -240,6 +240,7 @@ esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
+1 -1
View File
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
data = _get_data()
@@ -382,7 +382,7 @@ void BluedroidGattClient::set_disconnecting_() {
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
+36
View File
@@ -250,6 +250,42 @@ def add_metadata(
)
KEY_EXTERNAL_WRITERS = "external_writers"
def _get_external_writer_list() -> list[tuple[ID, str]]:
"""Get the raw external writer list. Each entry is (display id, owner)."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
def register_external_writer(display_id: ID, owner: str) -> None:
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
resolves it in place before final validation.
"""
_get_external_writer_list().append((display_id, owner))
def get_external_writers(display_id: ID) -> list[str]:
"""Get the owners registered for a display. Only valid once IDs are resolved."""
return [
owner
for id_, owner in _get_external_writer_list()
if id_.id is not None and id_.id == display_id.id
]
def has_external_writer(display_id: ID) -> bool:
"""Tell a driver's final validation that something else draws into the display.
LVGL anywhere in the config also counts, as the drivers assumed before.
"""
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
+1 -6
View File
@@ -40,7 +40,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from . import models
@@ -198,15 +197,11 @@ def _final_validate(config) -> None:
"epaper_spi", require_miso=False, require_mosi=True
)(config)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if display.has_external_writer(config[CONF_ID]):
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
+1
View File
@@ -131,6 +131,7 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
// Components that need NVS check nvs_handle instead of initializing it again.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
+4 -12
View File
@@ -5,6 +5,7 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#ifndef CONFIG_BT_CONTROLLER_DISABLED
#include <esp_bt.h>
@@ -28,7 +29,6 @@ extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#ifdef USE_ARDUINO
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
@@ -77,8 +77,9 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
void ESP32BLE::setup() {
global_ble = this;
if (!ble_pre_setup_()) {
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
if (global_preferences->nvs_handle == 0) {
ESP_LOGE(TAG, "NVS unavailable");
this->mark_failed();
return;
}
@@ -185,15 +186,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
}
#endif
bool ESP32BLE::ble_pre_setup_() {
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
return false;
}
return true;
}
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr)
-1
View File
@@ -185,7 +185,6 @@ class ESP32BLE final : public Component {
}
this->ble_events_.get_and_reset_dropped_count();
}
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
#endif
@@ -10,7 +10,6 @@
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring>
#include "esphome/core/hal.h"
@@ -300,7 +300,7 @@ void BLEClientBase::log_warning_(const char *message) {
esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
@@ -7,7 +7,6 @@
#ifdef USE_ESP32
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
#ifndef CONFIG_BT_CONTROLLER_DISABLED
@@ -16,7 +16,6 @@
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cinttypes>
#ifdef USE_OTA
+1
View File
@@ -47,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
CAN_MODES = {
"NORMAL": CanMode.CAN_MODE_NORMAL,
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
}
# Currently the driver only supports a subset of the bit rates defined in canbus
+5 -2
View File
@@ -79,10 +79,13 @@ bool ESP32Can::setup_internal() {
}
// Select TWAI mode based on configuration
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
twai_mode = TWAI_MODE_LISTEN_ONLY;
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
} else if (this->mode_ == CAN_MODE_NO_ACK) {
twai_mode = TWAI_MODE_NO_ACK;
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
}
twai_general_config_t g_config =
+1
View File
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
enum CanMode : uint8_t {
CAN_MODE_NORMAL = 0,
CAN_MODE_LISTEN_ONLY = 1,
CAN_MODE_NO_ACK = 2,
};
class ESP32Can final : public canbus::Canbus {
+165 -11
View File
@@ -2,6 +2,8 @@
#include <string>
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/progmem.h"
#include "hon_climate.h"
@@ -12,6 +14,26 @@ using namespace esphome::uart;
namespace esphome::haier {
static bool vertical_axis_is_auto(climate::ClimateSwingMode swing_mode) {
return (swing_mode == climate::CLIMATE_SWING_VERTICAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
}
static bool horizontal_axis_is_auto(climate::ClimateSwingMode swing_mode) {
return (swing_mode == climate::CLIMATE_SWING_HORIZONTAL) || (swing_mode == climate::CLIMATE_SWING_BOTH);
}
static bool is_auto(hon_protocol::VerticalSwingMode mode) {
return (mode == hon_protocol::VerticalSwingMode::AUTO) || (mode == hon_protocol::VerticalSwingMode::AUTO_SPECIAL);
}
static bool is_auto(hon_protocol::HorizontalSwingMode mode) { return mode == hon_protocol::HorizontalSwingMode::AUTO; }
// A position the user just set wins over the appliance's echo of the old one for this long
constexpr uint32_t AIRFLOW_SELECT_DEBOUNCE_MS = 10000;
static bool in_debounce(uint32_t set_time, uint32_t now) {
return set_time != 0 && now - set_time < AIRFLOW_SELECT_DEBOUNCE_MS;
}
ESPHOME_LOG_TAG(TAG, "haier.climate");
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
@@ -89,21 +111,60 @@ bool HonClimate::get_quiet_mode_state() const {
}
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
if (!this->current_vertical_swing_.has_value()) {
return this->current_vertical_swing_;
}
// While the appliance swings, or right after the user chose a position, report the chosen position
if (is_auto(this->current_vertical_swing_.value()) ||
in_debounce(this->vertical_direction_set_time_, App.get_loop_component_start_time())) {
return this->settings_.last_vertiacal_swing;
}
return this->current_vertical_swing_;
};
}
void HonClimate::set_vertical_airflow(hon_protocol::VerticalSwingMode direction) {
if (is_auto(direction)) {
ESP_LOGW(TAG, "Vertical AUTO mode is not allowed for airflow position selector");
return;
}
if (this->settings_.last_vertiacal_swing != direction) {
this->settings_.last_vertiacal_swing = direction;
this->hon_rtc_.save(&this->settings_);
}
this->pending_vertical_direction_ = direction;
this->force_send_control_ = true;
this->vertical_direction_set_time_ = App.get_loop_component_start_time();
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
}
esphome::optional<hon_protocol::HorizontalSwingMode> HonClimate::get_horizontal_airflow() const {
if (!this->current_horizontal_swing_.has_value()) {
return this->current_horizontal_swing_;
}
if (is_auto(this->current_horizontal_swing_.value()) ||
in_debounce(this->horizontal_direction_set_time_, App.get_loop_component_start_time())) {
return this->settings_.last_horizontal_swing;
}
return this->current_horizontal_swing_;
}
void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction) {
if (is_auto(direction)) {
ESP_LOGW(TAG, "Horizontal AUTO mode is not allowed for airflow position selector");
return;
}
if (this->settings_.last_horizontal_swing != direction) {
this->settings_.last_horizontal_swing = direction;
this->hon_rtc_.save(&this->settings_);
}
this->pending_horizontal_direction_ = direction;
this->force_send_control_ = true;
this->horizontal_direction_set_time_ = App.get_loop_component_start_time();
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
}
const char *HonClimate::get_cleaning_status_text() const {
@@ -533,6 +594,18 @@ void HonClimate::initialization() {
} else {
this->settings_ = {hon_protocol::VerticalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::CENTER, true, false};
}
bool settings_fixed = false;
if (is_auto(this->settings_.last_vertiacal_swing)) {
this->settings_.last_vertiacal_swing = hon_protocol::VerticalSwingMode::CENTER;
settings_fixed = true;
}
if (is_auto(this->settings_.last_horizontal_swing)) {
this->settings_.last_horizontal_swing = hon_protocol::HorizontalSwingMode::CENTER;
settings_fixed = true;
}
if (settings_fixed) {
this->hon_rtc_.save(&this->settings_);
}
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
@@ -668,12 +741,21 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
}
}
}
const ClimateSwingMode target_swing_mode = this->current_hvac_settings_.swing_mode.value_or(this->swing_mode);
if (this->pending_vertical_direction_.has_value()) {
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
if (!vertical_axis_is_auto(target_swing_mode)) {
out_data->vertical_swing_mode = (uint8_t) this->pending_vertical_direction_.value();
} else {
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
}
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
if (!horizontal_axis_is_auto(target_swing_mode)) {
out_data->horizontal_swing_mode = (uint8_t) this->pending_horizontal_direction_.value();
} else {
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
}
this->pending_horizontal_direction_.reset();
}
{
@@ -840,6 +922,41 @@ void HonClimate::set_quiet_mode_switch(switch_::Switch *sw) {
}
#endif // USE_SWITCH
#ifdef USE_SELECT
template<typename Mode, size_t N>
static void publish_airflow_index(select::Select *sel, const std::array<Mode, N> &order, Mode mode) {
if (sel == nullptr) {
return;
}
// Saved settings never hold AUTO after initialization(); any other value outside the table shows as Center
size_t index = N;
size_t center = 0;
for (size_t i = 0; i < N; i++) {
const Mode entry = airflow_order_at(order, i);
if (entry == mode) {
index = i;
} else if (entry == Mode::CENTER) {
center = i;
}
}
if (index == N) {
index = center;
}
const auto active = sel->active_index();
if (active.has_value() && *active == index) {
return;
}
sel->publish_state(index);
}
void HonClimate::publish_airflow_selects_() {
publish_airflow_index(this->vertical_airflow_select_, VERTICAL_SWING_MODES_ORDER,
this->settings_.last_vertiacal_swing);
publish_airflow_index(this->horizontal_airflow_select_, HORIZONTAL_SWING_MODES_ORDER,
this->settings_.last_horizontal_swing);
}
#endif // USE_SELECT
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
size_t expected_size =
2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
@@ -1074,16 +1191,25 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
// Saving last known non auto mode for vertical and horizontal swing
this->current_vertical_swing_ = (hon_protocol::VerticalSwingMode) packet.control.vertical_swing_mode;
this->current_horizontal_swing_ = (hon_protocol::HorizontalSwingMode) packet.control.horizontal_swing_mode;
bool save_settings = ((this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO) &&
(this->current_vertical_swing_.value() != hon_protocol::VerticalSwingMode::AUTO_SPECIAL) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing)) ||
((this->current_horizontal_swing_.value() != hon_protocol::HorizontalSwingMode::AUTO) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing));
if (save_settings) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
const uint32_t now_ms = App.get_loop_component_start_time();
bool save_vertical = !in_debounce(this->vertical_direction_set_time_, now_ms) &&
!is_auto(this->current_vertical_swing_.value()) &&
(this->current_vertical_swing_.value() != this->settings_.last_vertiacal_swing);
bool save_horizontal = !in_debounce(this->horizontal_direction_set_time_, now_ms) &&
!is_auto(this->current_horizontal_swing_.value()) &&
(this->current_horizontal_swing_.value() != this->settings_.last_horizontal_swing);
if (save_vertical || save_horizontal) {
if (save_vertical) {
this->settings_.last_vertiacal_swing = this->current_vertical_swing_.value();
}
if (save_horizontal) {
this->settings_.last_horizontal_swing = this->current_horizontal_swing_.value();
}
this->hon_rtc_.save(&this->settings_);
}
#ifdef USE_SELECT
this->publish_airflow_selects_();
#endif
should_publish = should_publish || (old_swing_mode != this->swing_mode);
}
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
@@ -1259,8 +1385,10 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::SET_POINT,
buffer, 2);
}
ClimateSwingMode target_swing_mode = this->swing_mode;
// Vertical swing mode
if (climate_control.swing_mode.has_value()) {
target_swing_mode = climate_control.swing_mode.value();
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) hon_protocol::VerticalSwingMode::AUTO};
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) hon_protocol::HorizontalSwingMode::AUTO};
switch (climate_control.swing_mode.value()) {
@@ -1286,6 +1414,32 @@ void HonClimate::fill_control_messages_queue_() {
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2);
}
// A swing change above already sent both axes from settings_.last_*, which set_*_airflow() updated
const bool swing_sent = climate_control.swing_mode.has_value();
if (this->pending_vertical_direction_.has_value()) {
if (vertical_axis_is_auto(target_swing_mode)) {
ESP_LOGD(TAG, "Vertical swing is on; the position is kept for when it is turned off");
} else if (!swing_sent) {
uint8_t vertical_swing_buf[] = {0x00, (uint8_t) this->pending_vertical_direction_.value()};
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::VERTICAL_SWING_MODE,
vertical_swing_buf, 2);
}
this->pending_vertical_direction_.reset();
}
if (this->pending_horizontal_direction_.has_value()) {
if (horizontal_axis_is_auto(target_swing_mode)) {
ESP_LOGD(TAG, "Horizontal swing is on; the position is kept for when it is turned off");
} else if (!swing_sent) {
uint8_t horizontal_swing_buf[] = {0x00, (uint8_t) this->pending_horizontal_direction_.value()};
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HORIZONTAL_SWING_MODE,
horizontal_swing_buf, 2);
}
this->pending_horizontal_direction_.reset();
}
// Fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
+38
View File
@@ -1,5 +1,6 @@
#pragma once
#include <array>
#include <chrono>
#include <queue>
#ifdef USE_SENSOR
@@ -14,12 +15,36 @@
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#include "esphome/core/automation.h"
#include "esphome/core/hal.h"
#include "haier_base.h"
#include "hon_packet.h"
namespace esphome::haier {
#ifdef USE_SELECT
// Positions in the order the select platform lists them; in flash on ESP8266, so read with airflow_order_at()
inline constexpr std::array<hon_protocol::VerticalSwingMode, 7> VERTICAL_SWING_MODES_ORDER PROGMEM{
hon_protocol::VerticalSwingMode::HEALTH_UP, hon_protocol::VerticalSwingMode::MAX_UP,
hon_protocol::VerticalSwingMode::UP, hon_protocol::VerticalSwingMode::CENTER,
hon_protocol::VerticalSwingMode::DOWN, hon_protocol::VerticalSwingMode::MAX_DOWN,
hon_protocol::VerticalSwingMode::HEALTH_DOWN,
};
inline constexpr std::array<hon_protocol::HorizontalSwingMode, 5> HORIZONTAL_SWING_MODES_ORDER PROGMEM{
hon_protocol::HorizontalSwingMode::MAX_LEFT, hon_protocol::HorizontalSwingMode::LEFT,
hon_protocol::HorizontalSwingMode::CENTER, hon_protocol::HorizontalSwingMode::RIGHT,
hon_protocol::HorizontalSwingMode::MAX_RIGHT,
};
template<typename Mode, size_t N> Mode airflow_order_at(const std::array<Mode, N> &order, size_t index) {
static_assert(sizeof(Mode) == 1);
return static_cast<Mode>(progmem_read_byte(reinterpret_cast<const uint8_t *>(order.data() + index)));
}
#endif
enum class CleaningState : uint8_t {
NO_CLEANING = 0,
SELF_CLEAN = 1,
@@ -100,6 +125,16 @@ class HonClimate final : public HaierClimateBase {
protected:
switch_::Switch *beeper_switch_{nullptr};
switch_::Switch *quiet_mode_switch_{nullptr};
#endif
#ifdef USE_SELECT
public:
void set_vertical_airflow_select(select::Select *sel) { this->vertical_airflow_select_ = sel; }
void set_horizontal_airflow_select(select::Select *sel) { this->horizontal_airflow_select_ = sel; }
protected:
void publish_airflow_selects_();
select::Select *vertical_airflow_select_{nullptr};
select::Select *horizontal_airflow_select_{nullptr};
#endif
public:
HonClimate();
@@ -195,6 +230,9 @@ class HonClimate final : public HaierClimateBase {
uint8_t big_data_counter_{0};
esphome::optional<hon_protocol::VerticalSwingMode> current_vertical_swing_{};
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
// millis() of the last user-set position per axis, 0 when none
uint32_t vertical_direction_set_time_{0};
uint32_t horizontal_direction_set_time_{0};
HonSettings settings_{};
ESPPreferenceObject hon_rtc_;
SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF};
@@ -0,0 +1,67 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import (
CONF_HAIER_ID,
CONF_HORIZONTAL_AIRFLOW,
CONF_VERTICAL_AIRFLOW,
HonClimate,
haier_ns,
)
CODEOWNERS = ["@paveldn"]
VerticalAirflowSelect = haier_ns.class_("VerticalAirflowSelect", select.Select)
HorizontalAirflowSelect = haier_ns.class_("HorizontalAirflowSelect", select.Select)
# Same order as VERTICAL_SWING_MODES_ORDER and HORIZONTAL_SWING_MODES_ORDER in hon_climate.h
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = [
"Health Up",
"Max Up",
"Up",
"Center",
"Down",
"Max Down",
"Health Down",
]
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = [
"Max Left",
"Left",
"Center",
"Right",
"Max Right",
]
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_HAIER_ID): cv.use_id(HonClimate),
cv.Optional(CONF_VERTICAL_AIRFLOW): select.select_schema(
VerticalAirflowSelect,
icon="mdi:arrow-expand-vertical",
entity_category=ENTITY_CATEGORY_CONFIG,
),
cv.Optional(CONF_HORIZONTAL_AIRFLOW): select.select_schema(
HorizontalAirflowSelect,
icon="mdi:arrow-expand-horizontal",
entity_category=ENTITY_CATEGORY_CONFIG,
),
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_HAIER_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_VERTICAL_AIRFLOW,
hub.set_vertical_airflow_select,
options=AIRFLOW_VERTICAL_DIRECTION_OPTIONS,
)
await selects(
CONF_HORIZONTAL_AIRFLOW,
hub.set_horizontal_airflow_select,
options=AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS,
)
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class HorizontalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_horizontal_airflow(airflow_order_at(HORIZONTAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
@@ -0,0 +1,15 @@
#pragma once
#include "esphome/components/select/select.h"
#include "../hon_climate.h"
namespace esphome::haier {
class VerticalAirflowSelect : public select::Select, public Parented<HonClimate> {
protected:
void control(size_t index) override {
this->parent_->set_vertical_airflow(airflow_order_at(VERTICAL_SWING_MODES_ORDER, index));
}
};
} // namespace esphome::haier
+2 -2
View File
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
var = await climate_ir.new_climate_ir(config, tag())
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
+68 -64
View File
@@ -5,7 +5,6 @@
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <cmath>
#include <map>
#include <IRSender.h>
#include <HeatpumpIRFactory.h>
#include "esphome/components/remote_base/remote_base.h"
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
};
template<Protocol P> struct ProtocolClass;
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
return new typename ProtocolClass<P>::type(); // NOLINT
}
// Codegen calls only the configured protocols, so the linker drops the other classes
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
template<> struct ProtocolClass<protocol> { using type = cls; }; \
template HeatpumpIR *create_heatpump_ir<protocol>();
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
#undef HEATPUMPIR_PROTOCOL
void HeatpumpIRClimate::setup() {
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
ESP_LOGE(TAG, "Invalid protocol");
return;
}
this->heatpump_ir_ = protocol_constructor->second();
climate_ir::ClimateIR::setup();
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
+20 -12
View File
@@ -89,25 +89,21 @@ enum VerticalDirection {
VERTICAL_DIRECTION_DOWN = 5,
};
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
template<Protocol P> HeatpumpIR *create_heatpump_ir();
/// Selects the protocol at compile time so the enum and the library object always match.
template<Protocol P> struct ProtocolTag {};
// Temperature
const float TEMP_MIN = 0; // Celsius
const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
public:
HeatpumpIRClimate()
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
template<Protocol P>
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
void setup() override;
void set_protocol(Protocol protocol) {
this->protocol_ = protocol;
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
}
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
protected:
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
heatpump_ir_(heatpump_ir),
protocol_(protocol) {
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
HeatpumpIR *heatpump_ir_;
void control(const climate::ClimateCall &call) override;
/// Transmit via IR the state of this climate controller.
+2 -2
View File
@@ -59,8 +59,8 @@ namespace esphome::host {
char **get_argv() { return s_argv; }
const std::string &get_exe_path() {
static const std::string empty;
return s_exe_path != nullptr ? *s_exe_path : empty;
static const std::string EMPTY;
return s_exe_path != nullptr ? *s_exe_path : EMPTY;
}
void arm_reexec(const std::string &path) {
+1 -1
View File
@@ -5,7 +5,7 @@
#include "esphome/core/log.h"
#include "core.h"
#include <time.h>
#include <ctime>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
+2 -2
View File
@@ -39,8 +39,8 @@ bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock();
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
static const uint8_t esphome_host_mac_address[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
static constexpr uint8_t HOST_MAC_ADDRESS[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, HOST_MAC_ADDRESS, sizeof(HOST_MAC_ADDRESS));
}
} // namespace esphome
+4 -4
View File
@@ -2,6 +2,7 @@
#include <filesystem>
#include <fstream>
#include <utility>
#include "preferences.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -45,11 +46,10 @@ void HostPreferences::setup_() {
break;
if (fread(&len, sizeof(len), 1, fp) != 1)
break;
uint8_t data[len];
if (fread(data, sizeof(uint8_t), len, fp) != len)
std::vector<uint8_t> data(len);
if (fread(data.data(), sizeof(uint8_t), len, fp) != len)
break;
std::vector vec(data, data + len);
this->data_[key] = vec;
this->data_[key] = std::move(data);
}
fclose(fp);
}
@@ -53,7 +53,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
for (const auto &[name, value] : request_headers) {
h_headers.emplace(name, value);
}
httplib::Client client(scheme_host.c_str());
httplib::Client client(scheme_host);
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
return nullptr;
@@ -114,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
}
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
for (const auto &header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first); // NOLINT
if (should_collect_header(lower_case_collect_headers, lower_name)) {
+1 -5
View File
@@ -37,7 +37,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from esphome.types import ConfigType
AUTO_LOAD = ["split_buffer"]
@@ -348,11 +347,8 @@ def _final_validate(config: ConfigType) -> None:
config
)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if display.has_external_writer(config[CONF_ID]):
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
+11
View File
@@ -21,6 +21,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S31,
add_idf_sdkconfig_option,
get_esp32_variant,
idf_version,
require_usb_serial_jtag_secondary,
require_vfs_termios,
)
@@ -482,6 +483,16 @@ async def _late_logger_init(config: ConfigType) -> None:
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
# Log V2 formats centrally in esp_log() (~4KB flash, ~180B RAM). 6.1
# adds CONSTRAINED_ENV_SAFE=n, keeping early/DRAM logs on
# esp_rom_printf instead of pulling ~1.2KB esp_rom_vprintf into IRAM.
if idf_version() >= cv.Version(6, 1, 0):
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_1", False)
add_idf_sdkconfig_option("CONFIG_LOG_VERSION_2", True)
add_idf_sdkconfig_option("CONFIG_LOG_API_CONSTRAINED_ENV_SAFE", False)
cg.add_define("USE_ESP32_LOG_V2")
# --wrap: a strong definition collides; see logger_esp32.cpp
cg.add_build_flag("-Wl,--wrap=esp_log_format")
# Define platform support flags for components that need auto-detection
try:
uart_selection(USB_SERIAL_JTAG)
@@ -163,4 +163,66 @@ const LogString *Logger::get_uart_selection_() {
}
} // namespace esphome::logger
// The version guard keeps builds that define USE_ESP32_LOG_V2 unconditionally (static analysis) off
// IDF private headers that only 6.1 has
#if defined(USE_ESP32_LOG_V2) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 1, 0)
#include <esp_private/log_message.h>
#include <esp_log_write.h>
#include <esp_rom_sys.h>
namespace esphome::logger {
// IDF levels NONE=0 E=1 W=2 I=3 D=4 V=5; ESPHome inserts CONFIG at 4
static inline uint8_t idf_to_esphome_level(uint8_t idf_level) {
if (idf_level <= 3)
return idf_level;
return idf_level >= 5 ? ESPHOME_LOG_LEVEL_VERBOSE : ESPHOME_LOG_LEVEL_DEBUG;
}
// Console output without the logger hook: early boot and constrained env
// (fwrite locks crash during PHY init on USB JTAG). Cold path.
static void __attribute__((noinline)) esp_log_format_direct(esp_log_msg_t *message) {
// Constrained-env stacks are small; enough for IDF's own one-liners
char stack_buf[256];
LogBuffer buf{stack_buf, sizeof(stack_buf)};
buf.write_header(idf_to_esphome_level(message->config.opts.log_level), message->tag ? message->tag : "esp-idf", 0,
nullptr);
buf.format_body(message->format, message->args);
esp_rom_printf("%s\n", stack_buf);
}
} // namespace esphome::logger
extern "C" {
// Replaces liblog's formatter, which calls the vprintf hook 3x per message
// (header, body, newline). --wrap because a strong definition collides:
// liblog resolves the symbol within its own archive. Not IRAM_ATTR: cache-off
// callers bypass esp_log() under CONSTRAINED_ENV_SAFE=n.
void __wrap_esp_log_format(esp_log_msg_t *message) { // NOLINT
extern vprintf_like_t esp_log_vprint_func;
if (esp_log_vprint_func == &esphome::esp_idf_log_vprintf_) [[likely]] {
if (message->config.opts.constrained_env) [[unlikely]] {
esphome::logger::esp_log_format_direct(message);
return;
}
// Call the logger directly with V2's separate tag and severity so lines
// render as e.g. "[E][wifi]:"; the hook signature cannot carry them.
// global_logger is set before the hook is installed (pre_setup above).
esphome::logger::global_logger->log_vprintf_(esphome::logger::idf_to_esphome_level(message->config.opts.log_level),
message->tag ? message->tag : "esp-idf", 0, message->format,
message->args);
return;
}
extern int vprintf(const char *, __gnuc_va_list); // NOLINT
if (esp_log_vprint_func == &vprintf || message->config.opts.constrained_env) {
// No hook yet (early boot) or constrained env: direct console output
esphome::logger::esp_log_format_direct(message);
return;
}
// Custom hook via esp_log_set_vprintf: forward the body as-is
esp_log_vprint_func(message->format, message->args);
}
} // extern "C"
#endif // USE_ESP32_LOG_V2 && IDF >= 6.1
#endif
@@ -38,6 +38,7 @@ int NoiseResponderHandshake::init(const NoiseContext &ctx, const uint8_t *prolog
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
.reserved = {},
};
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
+17 -17
View File
@@ -32,7 +32,7 @@ ESPHOME_LOG_TAG(TAG, "ota");
constexpr size_t MAX_OTA_SIZE = 256u * 1024u * 1024u; // 256 MiB
constexpr size_t HEADER_PEEK_SIZE = 64;
ssize_t read_header_(const char *path, uint8_t *buf, size_t len) {
ssize_t read_header(const char *path, uint8_t *buf, size_t len) {
int fd = ::open(path, O_RDONLY);
if (fd < 0)
return -1;
@@ -50,7 +50,7 @@ struct ElfIdent {
uint16_t e_type;
};
ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
ElfIdent parse_elf(const uint8_t *buf, size_t len) {
ElfIdent out{};
if (len < EI_NIDENT + 4)
return out;
@@ -76,17 +76,17 @@ ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
return out;
}
bool validate_elf_(const char *staging_path, const std::string &exe_path) {
bool validate_elf(const char *staging_path, const std::string &exe_path) {
uint8_t new_buf[HEADER_PEEK_SIZE];
uint8_t cur_buf[HEADER_PEEK_SIZE];
ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
ssize_t new_n = read_header(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header(exe_path.c_str(), cur_buf, sizeof(cur_buf));
if (new_n < static_cast<ssize_t>(EI_NIDENT + 4) || cur_n < static_cast<ssize_t>(EI_NIDENT + 4)) {
ESP_LOGE(TAG, "ELF header read failed");
return false;
}
ElfIdent new_id = parse_elf_(new_buf, new_n);
ElfIdent cur_id = parse_elf_(cur_buf, cur_n);
ElfIdent new_id = parse_elf(new_buf, new_n);
ElfIdent cur_id = parse_elf(cur_buf, cur_n);
if (!new_id.valid) {
ESP_LOGE(TAG, "Uploaded payload is not a valid ELF");
return false;
@@ -122,7 +122,7 @@ struct MachOIdent {
uint32_t cpusubtype;
};
MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
MachOIdent parse_macho(const uint8_t *buf, size_t len) {
MachOIdent out{};
// mach_header is the common prefix of mach_header and mach_header_64;
// cputype/cpusubtype/filetype have identical offsets in both.
@@ -153,18 +153,18 @@ MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
return out;
}
bool validate_macho_(const char *staging_path, const std::string &exe_path) {
bool validate_macho(const char *staging_path, const std::string &exe_path) {
uint8_t new_buf[HEADER_PEEK_SIZE];
uint8_t cur_buf[HEADER_PEEK_SIZE];
ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
ssize_t new_n = read_header(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header(exe_path.c_str(), cur_buf, sizeof(cur_buf));
if (new_n < static_cast<ssize_t>(sizeof(struct mach_header)) ||
cur_n < static_cast<ssize_t>(sizeof(struct mach_header))) {
ESP_LOGE(TAG, "Mach-O header read failed");
return false;
}
MachOIdent new_id = parse_macho_(new_buf, new_n);
MachOIdent cur_id = parse_macho_(cur_buf, cur_n);
MachOIdent new_id = parse_macho(new_buf, new_n);
MachOIdent cur_id = parse_macho(cur_buf, cur_n);
if (!new_id.valid) {
ESP_LOGE(TAG, "Uploaded payload is not a valid thin Mach-O executable");
return false;
@@ -182,11 +182,11 @@ bool validate_macho_(const char *staging_path, const std::string &exe_path) {
}
#endif // __APPLE__
bool validate_executable_(const char *staging_path, const std::string &exe_path) {
bool validate_executable(const char *staging_path, const std::string &exe_path) {
#ifdef __linux__
return validate_elf_(staging_path, exe_path);
return validate_elf(staging_path, exe_path);
#elif defined(__APPLE__)
return validate_macho_(staging_path, exe_path);
return validate_macho(staging_path, exe_path);
#else
(void) staging_path;
(void) exe_path;
@@ -298,7 +298,7 @@ OTAResponseTypes HostOTABackend::end() {
::close(this->fd_);
this->fd_ = -1;
if (!validate_executable_(this->staging_path_.c_str(), this->final_path_)) {
if (!validate_executable(this->staging_path_.c_str(), this->final_path_)) {
::unlink(this->staging_path_.c_str());
this->staging_path_.clear();
return OTA_RESPONSE_ERROR_UPDATE_END;
+7 -4
View File
@@ -50,13 +50,15 @@ static constexpr size_t PN532_FRAME_MAX_DATA_SIZE = 254;
static constexpr size_t PN532_FRAME_MAX_SIZE = 1 + 6 + PN532_FRAME_MAX_DATA_SIZE + 2;
/// Holds one frame in either direction so bus traffic never allocates
using PN532Frame = StaticVector<uint8_t, PN532_FRAME_MAX_SIZE>;
/// Holds pages 3 to 6 (16 bytes) plus an NDEF message of up to 255 bytes and its TLV header, rounded up to whole reads
using UltralightReadBuffer = StaticVector<uint8_t, 272>;
/// Longest NDEF message accepted from a MIFARE Ultralight/NTAG tag (the user memory of an NTAG216)
static constexpr uint32_t MIFARE_ULTRALIGHT_MAX_NDEF_SIZE = 888;
/// Longest NDEF message accepted from a MIFARE Classic tag (the capacity of a 4K tag)
static constexpr uint32_t MIFARE_CLASSIC_MAX_NDEF_SIZE = 3440;
/// A MIFARE READ answers with 16 bytes: one Classic block or four Ultralight pages
static constexpr size_t MIFARE_READ_SIZE = 16;
using MifareReadData = std::array<uint8_t, MIFARE_READ_SIZE>;
/// Pages 3 to 6, read in one go: the capability container and the start of the NDEF TLV
using UltralightHeader = std::array<uint8_t, MIFARE_READ_SIZE>;
class PN532BinarySensor;
@@ -134,10 +136,11 @@ class PN532 : public PollingComponent {
bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, UltralightReadBuffer &data);
/// Fills `data` with the bytes from `start_page` onward
bool read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data);
bool is_mifare_ultralight_formatted_(std::span<const uint8_t> page_3_to_6);
uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(std::span<const uint8_t> page_3_to_6, uint8_t &message_length,
bool find_mifare_ultralight_ndef_(std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::span<const uint8_t> write_data);
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
@@ -1,5 +1,6 @@
#include <algorithm>
#include <array>
#include <cinttypes>
#include <memory>
#include "pn532.h"
@@ -10,10 +11,9 @@ namespace esphome::pn532 {
ESPHOME_LOG_TAG(TAG, "pn532.mifare_ultralight");
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
UltralightReadBuffer data;
UltralightHeader data;
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
data)) {
if (!this->read_mifare_ultralight_bytes_(3, data)) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
@@ -22,49 +22,68 @@ std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
uint8_t message_length;
uint32_t message_length;
uint8_t message_start_index;
if (!this->find_mifare_ultralight_ndef_(data, message_length, message_start_index)) {
ESP_LOGW(TAG, "Couldn't find NDEF message");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index);
ESP_LOGVV(TAG, "NDEF message length: %" PRIu32 ", start: %u", message_length, message_start_index);
if (message_length == 0) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
// we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages
const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0;
// data[2] is the CC byte read earlier as part of pages 3-6, same value used by read_mifare_ultralight_capacity_()
const uint32_t capacity = data[2] * 8U;
const uint32_t total_length = message_length + message_start_index;
if (total_length > capacity) {
ESP_LOGW(TAG, "NDEF message of %" PRIu32 " bytes at offset %u exceeds tag capacity %" PRIu32, message_length,
message_start_index, capacity);
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
if (message_length > MIFARE_ULTRALIGHT_MAX_NDEF_SIZE) {
ESP_LOGE(TAG, "NDEF message too long: %" PRIu32 " bytes", message_length);
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
// we already read pages 3-6 earlier, pick up where we left off so we're not re-reading pages
constexpr size_t header_message_bytes = MIFARE_READ_SIZE - nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // pages 4 to 6
const uint16_t read_length =
total_length > header_message_bytes ? static_cast<uint16_t>(total_length - header_message_bytes) : 0;
// A message up to the old 255 byte limit stays on the stack, a longer one borrows the heap for the read
SmallBufferWithHeapFallback<MIFARE_READ_SIZE + 256> buffer(data.size() + read_length);
std::span<uint8_t> message(buffer.get(), data.size() + read_length);
std::copy(data.begin(), data.end(), message.begin());
if (read_length) {
if (!read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data)) {
if (!this->read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3,
message.subspan(data.size()))) {
ESP_LOGE(TAG, "Error reading tag data");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
}
// skip page 3 as well as any bytes ahead of message_start_index
const size_t skip = message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
if (skip >= data.size()) {
if (skip >= message.size()) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2,
make_unique<nfc::NdefMessage>(std::span<const uint8_t>(data).subspan(skip)));
make_unique<nfc::NdefMessage>(std::span<const uint8_t>(message).subspan(skip)));
}
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, UltralightReadBuffer &data) {
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data) {
MifareReadData chunk;
for (uint8_t i = 0; i * MIFARE_READ_SIZE < num_bytes; i++) {
for (uint8_t i = 0; i * MIFARE_READ_SIZE < data.size(); i++) {
// a READ returns 4 pages (16 bytes)
if (!this->mifare_read_(uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), chunk)) {
return false;
}
// keep only the bytes still wanted from this read
const uint16_t remaining = num_bytes - i * MIFARE_READ_SIZE;
const size_t count = std::min<size_t>(MIFARE_READ_SIZE, remaining);
for (const uint8_t byte : std::span<const uint8_t>(chunk).subspan(0, count)) {
data.push_back(byte);
}
const size_t offset = i * MIFARE_READ_SIZE;
const size_t count = std::min(MIFARE_READ_SIZE, data.size() - offset);
std::copy_n(chunk.begin(), count, data.begin() + offset);
}
char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
@@ -82,15 +101,15 @@ bool PN532::is_mifare_ultralight_formatted_(const std::span<const uint8_t> page_
}
uint16_t PN532::read_mifare_ultralight_capacity_() {
UltralightReadBuffer data;
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) && data.size() > 2) {
std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> data;
if (this->read_mifare_ultralight_bytes_(3, data)) {
ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
return data[2] * 8U;
}
return 0;
}
bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint8_t &message_length,
bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
uint8_t &message_start_index) {
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
@@ -98,16 +117,31 @@ bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_t
return false;
}
uint8_t tlv_offset;
if (page_3_to_6[p4_offset + 0] == 0x03) {
message_length = page_3_to_6[p4_offset + 1];
message_start_index = 2;
return true;
tlv_offset = 0;
} else if (page_3_to_6[p4_offset + 5] == 0x03) {
message_length = page_3_to_6[p4_offset + 6];
message_start_index = 7;
return true;
tlv_offset = 5;
} else {
return false;
}
return false;
// Long-form TLV length: 0x03 0xFF LEN_HI LEN_LO, used whenever the message is >= 255 bytes.
// Short-form is just 0x03 LEN. See NFC Forum Type 2 Tag Operation spec section 2.3.
if (page_3_to_6[p4_offset + tlv_offset + 1] == 0xFF) {
if (page_3_to_6.size() <= p4_offset + tlv_offset + 3) {
// long-form marker present but the length bytes weren't read -- fail explicitly instead of
// misreporting a 255-byte short-form message
return false;
}
message_length =
(static_cast<uint32_t>(page_3_to_6[p4_offset + tlv_offset + 2]) << 8) | page_3_to_6[p4_offset + tlv_offset + 3];
message_start_index = tlv_offset + 4;
} else {
message_length = page_3_to_6[p4_offset + tlv_offset + 1];
message_start_index = tlv_offset + 2;
}
return true;
}
bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
@@ -1,5 +1,5 @@
#include "prometheus_handler.h"
#ifdef USE_NETWORK
#if defined(USE_NETWORK) && !defined(USE_HOST)
#include "esphome/core/application.h"
namespace esphome::prometheus {
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
#include <map>
#include <utility>
+3 -3
View File
@@ -157,7 +157,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
} else {
auto stride = x_offset + w + x_pad;
auto data = ptr + (stride * y_offset + x_offset) * 2;
const auto *data = ptr + (stride * y_offset + x_offset) * 2;
SDL_UpdateTexture(this->texture_, &rect, data, stride * 2);
}
this->redraw_(rect);
@@ -199,7 +199,7 @@ void Sdl::process_key(uint32_t keycode, bool down) {
callback->second(down);
}
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
Sdl *Sdl::instance_for_window(uint32_t window_id) {
SDL_Window *window = SDL_GetWindowFromID(window_id);
if (window == nullptr)
return nullptr;
@@ -293,7 +293,7 @@ void Sdl::loop() {
continue;
}
Sdl *target = instance_for_window_(window_id);
Sdl *target = instance_for_window(window_id);
if (target == nullptr) {
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
// input that stops working leaves no trace at all.
+2 -2
View File
@@ -42,7 +42,7 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void dump_config() override { LOG_DISPLAY("", "SDL", this); }
template<typename F> void add_key_listener(int32_t keycode, F &&callback) {
if (!this->key_callbacks_.count(keycode)) {
if (!this->key_callbacks_.contains(keycode)) {
this->key_callbacks_[keycode] = CallbackManager<void(bool)>();
}
this->key_callbacks_[keycode].add(std::forward<F>(callback));
@@ -66,7 +66,7 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
void handle_event_(const SDL_Event &event);
/// The display owning the given window, or nullptr if it is not one of ours.
static Sdl *instance_for_window_(uint32_t window_id);
static Sdl *instance_for_window(uint32_t window_id);
SDL_Renderer *renderer_{};
SDL_Window *window_{};
SDL_Texture *texture_{};
@@ -162,6 +162,7 @@ bool MR60BHA2Component::validate_message_() {
void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length) {
switch (frame_type) {
case BREATH_RATE_TYPE_BUFFER:
#ifdef USE_SENSOR
if (this->breath_rate_sensor_ != nullptr && length >= 4) {
uint32_t current_breath_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
if (current_breath_rate_int != 0) {
@@ -170,11 +171,14 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
this->breath_rate_sensor_->publish_state(breath_rate_float);
}
}
#endif
break;
case PEOPLE_EXIST_TYPE_BUFFER:
#ifdef USE_BINARY_SENSOR
if (this->has_target_binary_sensor_ != nullptr && length >= 2) {
uint16_t has_target_int = encode_uint16(data[1], data[0]);
this->has_target_binary_sensor_->publish_state(has_target_int);
#ifdef USE_SENSOR
if (has_target_int == 0) {
if (this->breath_rate_sensor_ != nullptr)
this->breath_rate_sensor_->publish_state(0.0);
@@ -185,9 +189,12 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
if (this->num_targets_sensor_ != nullptr)
this->num_targets_sensor_->publish_state(0);
}
#endif
}
#endif
break;
case HEART_RATE_TYPE_BUFFER:
#ifdef USE_SENSOR
if (this->heart_rate_sensor_ != nullptr && length >= 4) {
uint32_t current_heart_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
if (current_heart_rate_int != 0) {
@@ -196,8 +203,10 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
this->heart_rate_sensor_->publish_state(heart_rate_float);
}
}
#endif
break;
case DISTANCE_TYPE_BUFFER:
#ifdef USE_SENSOR
if (length >= 1 && data[0] != 0) {
if (this->distance_sensor_ != nullptr && length >= 8) {
uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]);
@@ -206,12 +215,15 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
this->distance_sensor_->publish_state(distance_float);
}
}
#endif
break;
case PRINT_CLOUD_BUFFER:
#ifdef USE_SENSOR
if (this->num_targets_sensor_ != nullptr && length >= 4) {
uint32_t current_num_targets_int = encode_uint32(data[3], data[2], data[1], data[0]);
this->num_targets_sensor_->publish_state(current_num_targets_int);
}
#endif
break;
default:
break;
@@ -96,6 +96,7 @@ static bool validate_checksum(const uint8_t *data, size_t len, uint8_t expected_
return calculate_checksum(data, len) == expected_checksum;
}
#ifdef USE_SELECT
static uint8_t find_nearest_index(float value, const float *arr, int size) {
int nearest_index = 0;
float min_diff = std::abs(value - arr[0]);
@@ -108,6 +109,7 @@ static uint8_t find_nearest_index(float value, const float *arr, int size) {
}
return nearest_index;
}
#endif
/**
* @brief Convert a float value to a byte array.
@@ -250,15 +252,19 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) {
void MR60FDA2Component::process_frame_() {
switch (this->current_frame_type_) {
case IS_FALL_TYPE_BUFFER:
#ifdef USE_BINARY_SENSOR
if (this->fall_detected_binary_sensor_ != nullptr) {
this->fall_detected_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
}
#endif
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
break;
case PEOPLE_EXIST_TYPE_BUFFER:
#ifdef USE_BINARY_SENSOR
if (this->people_exist_binary_sensor_ != nullptr)
this->people_exist_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
#endif
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
break;
@@ -293,6 +299,7 @@ void MR60FDA2Component::process_frame_() {
float install_height_float = 0;
float height_threshold_float = 0;
uint32_t current_sensitivity = 0;
#ifdef USE_SELECT
if (this->install_height_select_ != nullptr) {
uint32_t current_install_height_int =
encode_uint32(current_data_buf_[3], current_data_buf_[2], current_data_buf_[1], current_data_buf_[0]);
@@ -318,6 +325,7 @@ void MR60FDA2Component::process_frame_() {
uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3);
this->sensitivity_select_->publish_state(select_index);
}
#endif
ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float,
height_threshold_float, current_sensitivity);
+3 -3
View File
@@ -50,9 +50,9 @@ struct BitWriter {
uint32_t buffer{0};
unsigned count{0};
void put(uint32_t code, unsigned size) {
this->buffer |= code << this->count;
this->count += size;
void put(uint32_t value, unsigned bits) {
this->buffer |= value << this->count;
this->count += bits;
while (this->count >= 8) {
this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
this->buffer >>= 8;
+1 -1
View File
@@ -55,7 +55,7 @@ std::string sanitise_filename(const char *const name, const char *extension, boo
break;
}
char c = *p;
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' && c != '-') {
c = '_';
changed = true;
}
@@ -61,6 +61,8 @@ class TcpClientLink {
void close();
bool connected() const { return this->connected_; }
/// A connect attempt is in flight.
bool connecting() const { return this->sock_ != nullptr && !this->connected_; }
bool ready() const { return this->sock_ != nullptr && this->sock_->ready(); }
/// Shared retry clock, also usable for a listen socket.
void note_attempt() { this->last_attempt_ms_ = App.get_loop_component_start_time(); }
+7
View File
@@ -1,4 +1,5 @@
#include "sx126x.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -313,6 +314,11 @@ void SX126x::set_packet_params_(uint8_t payload_length) {
}
SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
// Callers such as a template number's on_value can run before setup() or after a failed setup
if (!this->is_ready()) {
ESP_LOGW(TAG, "Radio not ready, packet dropped");
return SX126xError::SX126X_ERROR_NOT_READY;
}
if (this->payload_length_ > 0 && this->payload_length_ != len) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX126xError::INVALID_PARAMS;
@@ -333,6 +339,7 @@ SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio1_pin_->digital_read()) {
App.feed_wdt();
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX126xError::TIMEOUT;
+1 -1
View File
@@ -48,7 +48,7 @@ enum SX126xBw : uint8_t {
SX126X_BW_500000,
};
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX126X_ERROR_NOT_READY };
class SX126xListener {
public:
+7
View File
@@ -1,4 +1,5 @@
#include "sx127x.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -256,6 +257,11 @@ size_t SX127x::get_max_packet_size() {
}
SX127xError SX127x::transmit_packet(const uint8_t *data, size_t len) {
// Callers such as a template number's on_value can run before setup() or after a failed setup
if (!this->is_ready()) {
ESP_LOGW(TAG, "Radio not ready, packet dropped");
return SX127xError::SX127X_ERROR_NOT_READY;
}
if (this->payload_length_ > 0 && this->payload_length_ != len) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX127xError::INVALID_PARAMS;
@@ -292,6 +298,7 @@ SX127xError SX127x::transmit_packet(const uint8_t *data, size_t len) {
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio0_pin_->digital_read()) {
App.feed_wdt();
if (millis() - start > 4000) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX127xError::TIMEOUT;
+1 -1
View File
@@ -36,7 +36,7 @@ enum SX127xBw : uint8_t {
SX127X_BW_500_0,
};
enum class SX127xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
enum class SX127xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX127X_ERROR_NOT_READY };
class SX127xListener {
public:
@@ -9,11 +9,11 @@
#error This HostUartComponent implementation is not supported on this host OS
#endif
#include <stdio.h>
#include <string.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
@@ -210,7 +210,6 @@ void HostUartComponent::write_array(const uint8_t *data, size_t len) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
return;
}
bool HostUartComponent::peek_byte(uint8_t *data) {
@@ -2,6 +2,8 @@
#ifdef USE_HOST
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "uart_component.h"
@@ -10,7 +12,7 @@ namespace esphome::uart {
class HostUartComponent final : public UARTComponent, public Component {
public:
virtual ~HostUartComponent();
~HostUartComponent();
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
@@ -19,7 +21,7 @@ class HostUartComponent final : public UARTComponent, public Component {
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
UARTFlushResult flush() override;
void set_name(std::string port_name) { port_name_ = port_name; };
void set_name(std::string port_name) { this->port_name_ = std::move(port_name); }
protected:
void update_error_(const std::string &error);
@@ -1,5 +1,5 @@
#include "web_server_base.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
namespace esphome::web_server_base {
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
#include <vector>
#include "esphome/core/progmem.h"
@@ -959,10 +959,39 @@ void AsyncEventSourceResponse::process_close_() {
}
}
void AsyncEventSourceResponse::refresh_lru_() {
// httpd only advances a session's LRU counter when a request arrives, and an event stream receives one
// request ever, so the purge that frees a socket for a new client would evict the live streams first.
// The HTTPD task owns the session table, so the refresh runs there.
// A stalled stream is the socket the purge should take, and a zeroed fd has no session left to refresh.
if (this->send_failure_started_ms_ != 0 || this->fd_.load() == 0)
return;
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_lru_refresh_ms_ < LRU_REFRESH_INTERVAL_MS ||
this->lru_work_queued_.load(std::memory_order_acquire)) {
return;
}
this->last_lru_refresh_ms_ = now; // a full queue also waits for the next interval
this->lru_work_queued_.store(true, std::memory_order_release);
if (httpd_queue_work(this->hd_, &AsyncEventSourceResponse::refresh_lru_work, this) != ESP_OK) {
this->lru_work_queued_.store(false, std::memory_order_release);
}
}
void AsyncEventSourceResponse::refresh_lru_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load();
if (response->owns_session_(fd)) {
httpd_sess_update_lru_counter(response->hd_, fd);
}
// Release self only after the HTTPD-task callback has finished every access.
response->lru_work_queued_.store(false, std::memory_order_release);
}
void AsyncEventSourceResponse::close_session_work(void *arg) {
auto *response = static_cast<AsyncEventSourceResponse *>(arg);
const int fd = response->fd_.load();
if (fd != 0 && httpd_sess_get_ctx(response->hd_, fd) == response) {
if (response->owns_session_(fd)) {
// The HTTPD task remains the session owner. Shutting the socket down makes
// its next select/recv path delete the session and invoke destroy().
shutdown(fd, SHUT_RDWR);
@@ -976,7 +1005,7 @@ ssize_t AsyncEventSourceResponse::send_(struct iovec *iov, int iovcnt) {
// httpd frees a session before closing its socket, so the fd may already be a new client's.
// Treated as would-block; the stall timer ends a session that never becomes ours again.
const int fd = this->fd_.load();
if (httpd_sess_get_ctx(this->hd_, fd) != this) {
if (!this->owns_session_(fd)) {
return 0;
}
struct msghdr msg {};
@@ -1085,6 +1114,7 @@ void AsyncEventSourceResponse::loop() {
this->process_close_();
return;
}
this->refresh_lru_();
drain_tail_();
process_deferred_queue_();
if (this->close_requested_)
@@ -339,10 +339,15 @@ class AsyncEventSourceResponse {
};
void process_close_();
static void close_session_work(void *arg);
void refresh_lru_();
static void refresh_lru_work(void *arg);
// httpd frees a session before closing its socket, so the fd may already belong to a new client
bool owns_session_(int fd) const { return fd != 0 && httpd_sess_get_ctx(this->hd_, fd) == this; }
// Deletable only after destroy() zeroed fd_ and no queued HTTPD close work still references this object.
// Deletable only after destroy() zeroed fd_ and no queued HTTPD work still references this object.
bool safe_to_delete_() const {
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire);
return this->fd_.load() == 0 && !this->close_work_queued_.load(std::memory_order_acquire) &&
!this->lru_work_queued_.load(std::memory_order_acquire);
}
static void destroy(void *p);
@@ -357,11 +362,13 @@ class AsyncEventSourceResponse {
RAMUniquePtr<uint8_t[]> tail_;
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
uint32_t next_close_attempt_ms_{0};
uint32_t last_lru_refresh_ms_{0};
uint16_t tail_cap_{0};
uint16_t tail_len_{0}; // Zero means nothing pending
uint16_t tail_sent_{0};
// Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done.
std::atomic<bool> close_work_queued_{false};
std::atomic<bool> lru_work_queued_{false};
// Main-loop only; the HTTPD task never reads or writes these flags.
bool close_requested_{false};
bool close_retry_warning_logged_{false};
@@ -383,6 +390,7 @@ class AsyncEventSourceResponse {
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
static constexpr uint32_t LRU_REFRESH_INTERVAL_MS = 1000;
};
using AsyncEventSourceClient = AsyncEventSourceResponse;
@@ -5,7 +5,6 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_check.h"
#include "nvs_flash.h"
#include "zigbee_attribute_esp32.h"
#include "zigbee_esp32.h"
#include "esphome/core/application.h"
+8 -1
View File
@@ -331,7 +331,7 @@
#define USE_NOISE
#define USE_NOISE_SPARE_EPHEMERAL
#define USE_SHA256
#ifndef USE_RP2 // no MQTT backend or esp_wireguard library on RP2
#if !defined(USE_RP2) && !defined(USE_HOST) // no MQTT backend or esp_wireguard library on RP2 and host
#define USE_MQTT
#define USE_MQTT_COVER_JSON
#define USE_WIREGUARD
@@ -420,7 +420,10 @@
#define USE_ESPNOW
#define USE_ESPNOW_MAX_PAYLOAD_SIZE 1470
#ifndef USE_HOST // web_server, captive_portal: no ESPAsyncWebServer on host
#define USE_CAPTIVE_PORTAL
#endif
#define USE_ESP32_LOG_V2
#define USE_WIFI_SCAN_RESULTS_LOCK
#define USE_ESP32_BLE
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
@@ -478,6 +481,7 @@
#define USE_SPI
#define USE_SPI_PSRAM_DMA
#define USE_VOICE_ASSISTANT
#ifndef USE_HOST
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_AUTH_DIGEST
@@ -489,6 +493,7 @@
#define USE_WEBSERVER_ALLOWED_ORIGINS
#define WEB_SERVER_DEFAULT_HEADERS_COUNT 1
#define USE_CAPTIVE_PORTAL_GZIP
#endif
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_FAST_CONNECT_IN_FLASH
@@ -691,6 +696,8 @@
#endif
#ifdef USE_HOST
#define USE_ESPHOME_HOST_MAC_ADDRESS \
{ 0x98, 0x35, 0x69, 0xab, 0xf6, 0x79 }
#define USE_HTTP_REQUEST_RESPONSE
// Host only: the uart arm would shadow the native logger UART arms in other envs
#define USE_IMPROV_SERIAL_UART
+1 -1
View File
@@ -4,7 +4,7 @@ dependencies:
esphome/dlms_parser:
version: 2.2.0
esphome/esp-audio-libs:
version: 4.0.1
version: 4.1.0
esphome/esp-micro-speech-features:
version: 1.2.3
esphome/micro-decoder:
+10 -4
View File
@@ -1120,8 +1120,12 @@ def _load_yaml_internal(fname: Path, *, track_document_range: bool = True) -> An
res = parse_yaml(fname, f_handle, track_document_range=track_document_range)
except (UnicodeDecodeError, OSError) as err:
raise EsphomeError(f"Error reading file {fname}: {err}") from err
# Top-level !include returns a deferred IncludeFile; resolve it so
# callers always receive the final content.
return res
def _resolve_include(res: Any) -> Any:
# A document that is just `!include` parses to a deferred IncludeFile; resolve it so
# every parse_yaml caller, including the VS Code and ACE loaders, gets the final content.
if isinstance(res, IncludeFile):
res = res.load() if res.should_load() else {}
return res
@@ -1147,7 +1151,7 @@ def parse_yaml(
# tracked mode; reject the combination instead of half-applying it.
raise ValueError("track_document_range=False requires the default yaml_loader")
try:
return _load_yaml_internal_with_type(
res = _load_yaml_internal_with_type(
ESPHomeLoader,
file_name,
file_handle,
@@ -1159,13 +1163,15 @@ def parse_yaml(
# readable exceptions
# Rewind the stream so we can try again
file_handle.seek(0, 0)
return _load_yaml_internal_with_type(
res = _load_yaml_internal_with_type(
ESPHomePurePythonLoader,
file_name,
file_handle,
yaml_loader,
track_document_range=track_document_range,
)
# Resolved after the fallback so a failed first parse never loads the include twice
return _resolve_include(res)
def _load_yaml_internal_with_type(
+17
View File
@@ -654,6 +654,23 @@ build_flags =
build_unflags =
${common.build_unflags}
[env:host-tidy]
extends = env:host
; every library a header under esphome/ may include: a missing one is a
; fatal error that breaks the all-include analysis for every later header
lib_deps =
${common.lib_deps}
${common:idf-component-libs.lib_deps}
${env:host.lib_deps}
build_flags =
${env:host.build_flags}
${flags:clangtidy.build_flags}
; analyze with the same language model as the embedded targets
-fno-exceptions
-fno-rtti
-I/usr/include/SDL2 ; sdl
-D__LINUX__ ; runtime_image: JPEGDEC's host detection, see its __init__.py
;;;;;;;; nRF52 ;;;;;;;;
[env:nrf52]
+1
View File
@@ -98,6 +98,7 @@ ISOLATED_COMPONENTS = {
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
"packages": "cannot merge packages",
"tinyusb": "Conflicts with usb_host component - cannot be used together",
"tinyusb_keyboard": "Depends on tinyusb which conflicts with usb_host",
"uart_mux": "Depends on tinyusb which conflicts with usb_host",
"usb_cdc_acm": "Depends on tinyusb which conflicts with usb_host",
}
+33 -9
View File
@@ -68,6 +68,9 @@ def clang_options(idedata, environment):
# extract target architecture from triplet in g++ filename
triplet = Path(idedata["cxx_path"]).name[:-4]
# env:host compiles with the system g++, so there is no triplet and the
# host's own target and headers are the right ones.
native = environment.startswith("host")
if triplet.startswith("xtensa-"):
# clang has an Xtensa frontend, but only a generic core -- the esp32 IDF
# toolchain headers (xtruntime, xtensa/config) need the GCC core config
@@ -83,6 +86,8 @@ def clang_options(idedata, environment):
cmd.append("-D__XTENSA__")
cmd.append("-D__XTENSA_EL__")
cmd.append("-D_LIBC")
elif native:
cmd.append("-Qunused-arguments")
else:
# RISC-V (and other non-Xtensa targets) have a real clang backend, so
# compile for the actual triplet. Espressif's RISC-V GCC -march adds
@@ -118,7 +123,7 @@ def clang_options(idedata, environment):
"-mfp16-format=ieee",
"--param=min-pagesize=0",
)
else:
elif not native:
cmd.extend(
[
# disable built-in include directories from the host
@@ -127,10 +132,11 @@ def clang_options(idedata, environment):
)
# set flags
if not native:
# disable built-in include directories from the host
cmd.append("-nostdinc")
cmd.extend(
[
# disable built-in include directories from the host
"-nostdinc",
# allow to condition code on the presence of clang-tidy
"-DCLANG_TIDY",
# (esp-idf) Fix __once_callable in some libstdc++ headers
@@ -314,11 +320,17 @@ def run_tidy(
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")):
# MMIO peripheral access on these bare-metal platforms is all
# fixed-address.
disabled_checks.append("-clang-analyzer-core.FixedAddressDereference")
if args.environment.startswith("host"):
# glibc's assert() expands to a call the check mistakes for a
# constant condition.
disabled_checks += ["-misc-static-assert", "-cert-dcl03-c"]
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx", "host")):
# bugprone-pointer-arithmetic-on-polymorphic-object (and its
# cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher
# recursion on lvgl_esphome.h under the RP2 defines.
# recursion on lvgl_esphome.h under the RP2 defines and on the
# all-include file under the host defines.
disabled_checks += [
"-clang-analyzer-core.FixedAddressDereference",
"-bugprone-pointer-arithmetic-on-polymorphic-object",
"-cert-ctr56-cpp",
]
@@ -327,6 +339,8 @@ def run_tidy(
if disabled_checks:
invocation.append("--checks=" + ",".join(disabled_checks))
invocation.append(f"--header-filter={Path(basepath).resolve()}/.*")
# Vendored third-party headers are kept as upstream wrote them.
invocation.append("--exclude-header-filter=.*/http_request/httplib\\.h")
invocation.append(str(Path(path).resolve()))
invocation.append("--")
invocation.extend(options)
@@ -448,6 +462,11 @@ def main():
if args.grep:
files = filter_grep(files, args.grep)
# Not every tidy environment carries googletest (LibreTiny does not),
# so a platform grep that matched a unit test would fail on
# <gtest/gtest.h>. The tests are analyzed by the full ESP32 IDF run,
# which has no --grep.
files = [f for f in files if not f.startswith("tests/")]
if args.exclude_grep:
excluded = set(filter_grep(files, args.exclude_grep))
@@ -464,11 +483,16 @@ def main():
# Add header file for checking (before early exit check)
if args.all_headers and args.split_at in (None, 1):
all_headers = [
os.path.relpath(p, cwd) for p in git_ls_files(["esphome/**/*.h"])
]
# The path filter applies to headers too: a component left out because
# it does not build in this environment must not come back via the
# all-include file.
if args.files:
all_headers = [p for p in all_headers if file_name_re.search(p)]
# When --changed is used, only include changed headers instead of all headers
if args.changed:
all_headers = [
os.path.relpath(p, cwd) for p in git_ls_files(["esphome/**/*.h"])
]
changed_headers = filter_changed(all_headers)
if changed_headers:
build_all_include(changed_headers)
@@ -476,7 +500,7 @@ def main():
else:
print("No changed headers to check")
else:
build_all_include()
build_all_include(all_headers)
files.insert(0, temp_header_file)
print(f"Added all-include header file, new total: {len(files)}")
@@ -0,0 +1,105 @@
"""CodSpeed benchmarks for importing entity components.
Every ``esphome config`` or ``esphome compile`` imports the Python module of
each component the YAML uses. A component that imports its optional
integrations (mqtt, web_server, zigbee) at module scope drags their whole
import chains in for every user, so the cost of a fresh import is what these
benchmarks pin down.
The core of esphome is imported once up front and stays loaded; each run
drops only the component modules from ``sys.modules`` and imports the
component again, so the import machinery and the module bodies of the
component and everything it pulls in run every time, as they do in a new
process, while a change to the core moves no number here. The modules and
registry entries the session had before a case are put back afterwards, so
later tests keep the objects they already hold.
Skipped when ``pytest-codspeed`` isn't installed so the regular unit-test
suite keeps working unchanged.
"""
from __future__ import annotations
from collections.abc import Generator, Iterable
import importlib
import sys
import pytest
pytest.importorskip("pytest_codspeed")
# Load the core before the first case so no case pays for it.
from esphome import automation, pins # noqa: E402
import esphome.codegen # noqa: E402, F401
import esphome.config_validation # noqa: E402, F401
COMPONENTS = ("binary_sensor", "sensor", "logger")
COMPONENT_PACKAGE = "esphome.components"
COMPONENT_PREFIX = COMPONENT_PACKAGE + "."
# Core registries that component module bodies write to when they are imported
REGISTRIES = (
automation.ACTION_REGISTRY,
automation.CONDITION_REGISTRY,
pins.PIN_SCHEMA_REGISTRY,
)
def _component_modules() -> dict[str, object]:
# The package itself goes too: ``from esphome.components import x`` is
# answered from its attributes, so a stale package would hide the import.
return {
n: m
for n, m in sys.modules.items()
if n == COMPONENT_PACKAGE or n.startswith(COMPONENT_PREFIX)
}
def _forget_component_modules() -> None:
for name in _component_modules():
del sys.modules[name]
def _import_component(name: str) -> object:
_forget_component_modules()
return importlib.import_module(COMPONENT_PREFIX + name)
_MISSING = object()
def _parent_bindings(names: Iterable[str]) -> list[tuple[object, str, object]]:
# An import binds the module to its parent package; a package may hold something else
# under that name (substitutions keeps a Jinja() instance as ``jinja``), so the value
# the parent has now is what goes back, not the module.
bindings = []
for name in names:
parent, _, child = name.rpartition(".")
bindings.append(
(sys.modules[parent], child, getattr(sys.modules[parent], child, _MISSING))
)
return bindings
@pytest.fixture
def restore_component_modules() -> Generator[None]:
saved = _component_modules()
saved_bindings = _parent_bindings(saved)
saved_registries = [dict(registry) for registry in REGISTRIES]
yield
_forget_component_modules()
sys.modules.update(saved)
for parent, child, value in saved_bindings:
if value is _MISSING:
delattr(parent, child)
else:
setattr(parent, child, value)
# The re-imported bodies registered their actions, conditions and pin schemas again
for registry, entries in zip(REGISTRIES, saved_registries, strict=True):
registry.clear()
registry.update(entries)
@pytest.mark.usefixtures("restore_component_modules")
@pytest.mark.parametrize("component", COMPONENTS)
def test_import_component(benchmark, component: str) -> None:
benchmark(_import_component, component)
@@ -0,0 +1,28 @@
esphome:
name: test
esp32:
board: esp32dev
external_components:
- source:
type: local
path: ../external_components
spi:
clk_pin: GPIO14
mosi_pin: GPIO12
miso_pin: GPIO13
display:
- platform: epaper_spi
model: ssd1677
dimensions:
width: 200
height: 200
cs_pin: GPIO5
dc_pin: GPIO21
busy_pin: GPIO22
reset_pin: GPIO25
fake_display_writer:
@@ -0,0 +1,21 @@
esphome:
name: test
esp32:
board: esp32dev
external_components:
- source:
type: local
path: ../external_components
spi:
clk_pin: GPIO14
mosi_pin: GPIO12
miso_pin: GPIO13
display:
- platform: it8951
model: m5stack-m5paper
fake_display_writer:
@@ -0,0 +1,25 @@
esphome:
name: test
esp32:
board: esp32dev
spi:
clk_pin: GPIO14
mosi_pin: GPIO12
miso_pin: GPIO13
display:
- platform: it8951
id: it8951_display
model: m5stack-m5paper
- platform: epaper_spi
id: epaper_display
model: ssd1677
dimensions:
width: 200
height: 200
cs_pin: GPIO5
dc_pin: GPIO21
busy_pin: GPIO22
reset_pin: GPIO25
@@ -0,0 +1,33 @@
esphome:
name: test
esp32:
board: esp32dev
external_components:
- source:
type: local
path: ../external_components
spi:
clk_pin: GPIO14
mosi_pin: GPIO12
miso_pin: GPIO13
display:
- platform: it8951
id: it8951_display
model: m5stack-m5paper
- platform: epaper_spi
id: epaper_display
model: ssd1677
dimensions:
width: 200
height: 200
cs_pin: GPIO5
dc_pin: GPIO21
busy_pin: GPIO22
reset_pin: GPIO25
fake_display_writer:
display_id: epaper_display
@@ -0,0 +1,20 @@
"""External component that draws into a display from outside its lambda or pages."""
from esphome.components import display
import esphome.config_validation as cv
from esphome.const import CONF_DISPLAY_ID
from esphome.types import ConfigType
DEPENDENCIES = ["display"]
DOMAIN = "fake_display_writer"
def _register_writer(config: ConfigType) -> ConfigType:
display.register_external_writer(config[CONF_DISPLAY_ID], DOMAIN)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema({cv.GenerateID(CONF_DISPLAY_ID): cv.use_id(display.Display)}),
_register_writer,
)
@@ -0,0 +1,98 @@
"""Tests for the display external writer registry."""
from collections.abc import Callable, Generator
from pathlib import Path
import sys
import pytest
from esphome import loader
from esphome.components.display import (
Display,
get_external_writers,
has_external_writer,
register_external_writer,
)
from esphome.core import ID
from esphome.final_validate import full_config
TEST_CARD = "show_test_card()"
UPDATE_NEVER = "set_update_interval(4294967295UL)"
@pytest.fixture
def unload_fake_writer() -> Generator[None]:
"""Drop the fake external component so later tests start clean."""
yield
loader._COMPONENT_CACHE.pop("fake_display_writer", None)
sys.modules.pop("esphome.components.fake_display_writer", None)
loader.clear_component_meta_finders()
def test_unresolved_id_matches_after_resolution() -> None:
"""An auto-picked ID registered during schema validation matches once resolved."""
pending = ID(None, is_declaration=False, type=Display)
register_external_writer(pending, "owner")
declared = ID("epd", is_declaration=True, type=Display)
assert get_external_writers(declared) == []
assert has_external_writer(declared) is False
pending.id = "epd"
assert get_external_writers(declared) == ["owner"]
assert has_external_writer(declared) is True
assert get_external_writers(ID("other", is_declaration=True, type=Display)) == []
def test_all_owners_returned() -> None:
register_external_writer(ID("epd"), "first")
register_external_writer(ID("epd"), "second")
register_external_writer(ID("other"), "third")
assert get_external_writers(ID("epd")) == ["first", "second"]
def test_lvgl_counts_as_external_writer() -> None:
assert has_external_writer(ID("epd")) is False
full_config.get()["lvgl"] = []
assert has_external_writer(ID("epd")) is True
def test_drivers_show_test_card_without_owner(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("no_owner.yaml"))
assert f"it8951_display->{TEST_CARD}" in main_cpp
assert f"epaper_display->{TEST_CARD}" in main_cpp
assert UPDATE_NEVER not in main_cpp
@pytest.mark.usefixtures("unload_fake_writer")
@pytest.mark.parametrize(
"config_file", ["it8951_owner_auto_id.yaml", "epaper_spi_owner_auto_id.yaml"]
)
def test_owned_display_has_no_test_card(
config_file: str,
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""The owner picks the display ID automatically, so it resolves after the schema runs."""
main_cpp = generate_main(component_config_path(config_file))
assert TEST_CARD not in main_cpp
assert UPDATE_NEVER in main_cpp
@pytest.mark.usefixtures("unload_fake_writer")
def test_only_owned_display_loses_test_card(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("owner_by_id.yaml"))
assert f"it8951_display->{TEST_CARD}" in main_cpp
assert f"epaper_display->{TEST_CARD}" not in main_cpp
assert f"epaper_display->{UPDATE_NEVER}" in main_cpp
assert f"it8951_display->{UPDATE_NEVER}" not in main_cpp
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
bmi270: !include common.yaml
+1 -1
View File
@@ -18,7 +18,7 @@ canbus:
tx_pin: ${tx_pin}
can_id: 4
bit_rate: 50kbps
mode: NORMAL
mode: NO_ACK
on_frame:
- can_id: 500
then:
+8
View File
@@ -121,3 +121,11 @@ switch:
name: Haier health mode
quiet_mode:
name: Haier quiet mode
select:
- platform: haier
haier_id: haier_ac
horizontal_airflow:
name: Haier airflow horizontal
vertical_airflow:
name: Haier airflow vertical
@@ -0,0 +1,8 @@
packages:
logger: !include common-default_uart.yaml
# IDF 6.1 is where Log V2 and the esp_log_format wrap are enabled; the default IDF has no V2
esp32:
framework:
type: esp-idf
version: 6.1.0
@@ -0,0 +1,96 @@
#include <gtest/gtest.h>
#include <iterator>
#include <string>
#include <vector>
#include "esphome/components/nfc/ndef_record_uri.h"
namespace esphome::nfc::testing {
static std::string decode(const std::vector<uint8_t> &payload) { return NdefRecordUri(payload).get_payload(); }
static uint8_t encoded_prefix(const std::string &uri) { return NdefRecordUri(uri).get_encoded_payload()[0]; }
TEST(NdefRecordUri, DecodeExpandsPrefixCode) {
EXPECT_EQ(decode({0x01, 'a', '.', 'b'}), "http://www.a.b");
EXPECT_EQ(decode({0x04, 'a', '.', 'b'}), "https://a.b");
EXPECT_EQ(decode({0x07, 'a'}), "ftp://anonymous:anonymous@a");
}
// URI identifier codes from the NFC Forum URI Record Type Definition, kept separate from the
// component's table so a reordered entry fails here
static constexpr const char *SPEC_PREFIXES[] = {"",
"http://www.",
"https://www.",
"http://",
"https://",
"tel:",
"mailto:",
"ftp://anonymous:anonymous@",
"ftp://ftp.",
"ftps://",
"sftp://",
"smb://",
"nfs://",
"ftp://",
"dav://",
"news:",
"telnet://",
"imap:",
"rtsp://",
"urn:",
"pop:",
"sip:",
"sips:",
"tftp:",
"btspp://",
"btl2cap://",
"btgoep://",
"tcpobex://",
"irdaobex://",
"file://",
"urn:epc:id:",
"urn:epc:tag:",
"urn:epc:pat:",
"urn:epc:raw:",
"urn:epc:",
"urn:nfc:"};
TEST(NdefRecordUri, DecodeEveryPrefixCode) {
for (size_t code = 1; code < std::size(SPEC_PREFIXES); code++)
EXPECT_EQ(decode({static_cast<uint8_t>(code), 'x'}), std::string(SPEC_PREFIXES[code]) + "x") << "code " << code;
}
TEST(NdefRecordUri, DecodeAcceptsLastPrefixCode) { EXPECT_EQ(decode({0x23, 'x'}), "urn:nfc:x"); }
TEST(NdefRecordUri, DecodeIgnoresUnknownPrefixCode) {
EXPECT_EQ(decode({0x00, 'x'}), "x");
EXPECT_EQ(decode({0x24, 'x'}), "x");
}
// Encode takes the first prefix in table order that matches, so codes 0x1E to 0x23 are never written;
// these tests pin that current behaviour, not the spec's longest match
TEST(NdefRecordUri, EncodePicksFirstMatchingPrefix) {
EXPECT_EQ(encoded_prefix("https://www.a.b"), 0x02);
EXPECT_EQ(encoded_prefix("http://a.b"), 0x03);
EXPECT_EQ(encoded_prefix("urn:epc:id:x"), 0x13);
}
TEST(NdefRecordUri, EncodeStopsBeforeLastPrefixCode) {
// urn:nfc: (0x23) is never chosen; the shorter urn: (0x13) matches first
EXPECT_EQ(encoded_prefix("urn:nfc:x"), 0x13);
}
TEST(NdefRecordUri, EncodeStripsPrefix) {
const std::vector<uint8_t> expected = {0x02, 'a', '.', 'b'};
EXPECT_EQ(NdefRecordUri("https://www.a.b").get_encoded_payload(), expected);
}
TEST(NdefRecordUri, EncodeWithoutPrefix) {
EXPECT_EQ(encoded_prefix("ht"), 0x00);
const std::vector<uint8_t> expected = {0x00, 'x', 'y'};
EXPECT_EQ(NdefRecordUri("xy").get_encoded_payload(), expected);
}
} // namespace esphome::nfc::testing
+5 -7
View File
@@ -163,9 +163,8 @@ TEST(PN532Mifare, UltralightReadTrimsLastChunk) {
pn532.responses.push_back(first);
pn532.responses.push_back(second);
UltralightReadBuffer data;
ASSERT_TRUE(pn532.read_mifare_ultralight_bytes_(4, 20, data));
ASSERT_EQ(data.size(), 20u);
std::array<uint8_t, 20> data{};
ASSERT_TRUE(pn532.read_mifare_ultralight_bytes_(4, data));
EXPECT_EQ(data[15], 15);
EXPECT_EQ(data[16], 0x10);
EXPECT_EQ(data[19], 0x13);
@@ -177,15 +176,14 @@ TEST(PN532Mifare, UltralightReadTrimsLastChunk) {
TEST(PN532Mifare, UltralightReadRejectsBadResponses) {
FakePN532 pn532;
UltralightReadBuffer data;
std::array<uint8_t, 16> data{};
pn532.responses.push_back({0x00, 0x01, 0x02}); // short response
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, 16, data));
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, data));
std::vector<uint8_t> failed(17, 0x00);
failed[0] = 0x01; // timeout status
pn532.responses.push_back(failed);
data.clear();
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, 16, data));
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, data));
}
} // namespace esphome::pn532
@@ -0,0 +1,273 @@
#include <gtest/gtest.h>
#include <memory>
#include <span>
#include <vector>
#include "esphome/components/nfc/ndef_message.h"
#include "esphome/components/nfc/ndef_record.h"
#include "esphome/components/nfc/nfc.h"
#include "esphome/components/pn532/pn532.h"
namespace esphome::pn532::testing {
/// Exposes the protected Ultralight helpers and stands in for the bus. Each MIFARE READ is answered with the
/// 16 bytes of `tag_memory` starting at the requested page, so the full read path can run against synthetic tags.
class TestablePN532 : public PN532 {
public:
using PN532::find_mifare_ultralight_ndef_;
using PN532::read_mifare_ultralight_tag_;
/// Tag memory starting at page 0 (4 bytes per page)
std::vector<uint8_t> tag_memory;
protected:
bool is_read_ready() override { return true; }
bool write_data(std::span<const uint8_t> data) override {
this->last_written_.assign(data.begin(), data.end());
return true;
}
// only used for ACK frames; index 0 is the I2C status byte
bool read_data(PN532Frame &data, size_t len) override {
data = {0x01, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00};
return true;
}
bool read_response(uint8_t command, PN532Frame &data) override {
// frame is preamble, start code (2), LEN, LCS, TFI, InDataExchange, Tg, READ, page, DCS, postamble
if (this->last_written_.size() < 3)
return false;
const size_t start = this->last_written_[this->last_written_.size() - 3] * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
data.clear();
data.push_back(0x00); // status: success
for (size_t i = start; i < start + MIFARE_READ_SIZE; i++) {
data.push_back(i < this->tag_memory.size() ? this->tag_memory[i] : 0x00);
}
return true;
}
std::vector<uint8_t> last_written_;
};
/// Builds Ultralight/NTAG tag memory holding `tlv` at page 4, with a capability container that reports
/// `capacity_bytes` of user memory.
std::vector<uint8_t> make_tag_memory(const std::vector<uint8_t> &tlv, uint16_t capacity_bytes) {
// pages 0-2: UID, lock bytes; page 3: CC (magic, version, size / 8, access)
std::vector<uint8_t> memory(12, 0x00);
memory.insert(memory.end(), {0xE1, 0x10, static_cast<uint8_t>(capacity_bytes / 8), 0x00});
memory.insert(memory.end(), tlv.begin(), tlv.end());
memory.resize(16 + capacity_bytes, 0x00);
return memory;
}
/// Wraps an encoded NDEF message in a Type 2 Tag TLV, using the long form when it is 255 bytes or more.
std::vector<uint8_t> wrap_tlv(const std::vector<uint8_t> &encoded) {
std::vector<uint8_t> tlv = {0x03};
if (encoded.size() < 255) {
tlv.push_back(static_cast<uint8_t>(encoded.size()));
} else {
tlv.insert(tlv.end(), {0xFF, static_cast<uint8_t>(encoded.size() >> 8), static_cast<uint8_t>(encoded.size())});
}
tlv.insert(tlv.end(), encoded.begin(), encoded.end());
tlv.push_back(0xFE);
return tlv;
}
// Builds a 16-byte page_3_to_6 buffer: 4 filler bytes for page 3 (unused by this function),
// followed by the given bytes starting at page 4, zero-padded out to the real 4-page read size.
std::vector<uint8_t> make_page_3_to_6(std::initializer_list<uint8_t> page4_onward) {
std::vector<uint8_t> data = {0x00, 0x00, 0x00, 0x00};
data.insert(data.end(), page4_onward);
data.resize(16, 0x00);
return data;
}
TEST(FindMifareUltralightNdef, ShortFormAtPage4Start) {
TestablePN532 pn532;
// 0x03 LEN -- message starts right after.
auto data = make_page_3_to_6({0x03, 0x20});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 0x20u);
EXPECT_EQ(message_start_index, 2);
}
TEST(FindMifareUltralightNdef, ShortFormAfterLeadingPadding) {
TestablePN532 pn532;
// A non-0x03 byte at page 4 (e.g. a lock/CC TLV) pushes the message TLV to offset 5.
auto data = make_page_3_to_6({0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 0x10u);
EXPECT_EQ(message_start_index, 7);
}
// Regression test: prior to this fix, a >=255 byte NDEF message's TLV length was always read as
// a single byte, so the 0xFF long-form marker was mistaken for a length of 255 and the two real
// length bytes were mistaken for message content. That misalignment is what produced the
// "Corrupt record encountered; NdefMessage constructor aborting" error for large tags
TEST(FindMifareUltralightNdef, LongFormAtPage4Start) {
TestablePN532 pn532;
// 0x03 FF 01 2C -> long-form length = 0x012C = 300
auto data = make_page_3_to_6({0x03, 0xFF, 0x01, 0x2C, 0xD1, 0x01, 0x0C, 0x55});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 300u);
EXPECT_EQ(message_start_index, 4);
}
TEST(FindMifareUltralightNdef, LongFormAfterLeadingPadding) {
TestablePN532 pn532;
auto data = make_page_3_to_6({0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x02, 0x00});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 0x0200u); // 512
EXPECT_EQ(message_start_index, 9);
}
TEST(FindMifareUltralightNdef, TooShortBufferIsRejected) {
TestablePN532 pn532;
std::vector<uint8_t> data = {0x00, 0x00, 0x00, 0x00, 0x03, 0x05}; // well under the size floor
uint32_t message_length = 0;
uint8_t message_start_index = 0;
EXPECT_FALSE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
}
TEST(FindMifareUltralightNdef, NoTlvTagIsRejected) {
TestablePN532 pn532;
auto data = make_page_3_to_6({0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
EXPECT_FALSE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
}
// Regression test: the entry size guard (page_3_to_6.size() > p4_offset + 6) is loose enough to admit
// buffers that are too short to hold the two long-form length bytes once the TLV is offset by leading
// padding (tlv_offset == 5). Before this fix, that case silently fell through to the short-form branch
// and reported a bogus 255-byte message instead of failing.
TEST(FindMifareUltralightNdef, LongFormMarkerWithMissingLengthBytesIsRejected) {
TestablePN532 pn532;
// Padding pushes the TLV to offset 5; 0x03 FF present, but the two length bytes are missing (size 11).
std::vector<uint8_t> data = {0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF};
ASSERT_EQ(data.size(), 11u);
uint32_t message_length = 0;
uint8_t message_start_index = 0;
EXPECT_FALSE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
}
/// End-to-end regression guard: builds a real 3-record NDEF message (URL, text, and a MIME
/// record) padded past 255 bytes and wraps it in a Type 2 Tag TLV the way a real tag stores it,
/// then runs the wrapper through find_mifare_ultralight_ndef_ to locate it, and feeds the located slice back
/// into NdefMessage to confirm all three records survive the round trip.
TEST(FindMifareUltralightNdef, LongFormMessageRoundTripsThroughNdefMessage) {
nfc::NdefMessage message;
message.add_uri_record("https://esphome.io/");
message.add_text_record("Hello, ESPHome!");
std::string json_payload = R"({"key":"value","pad":")" + std::string(255, 'x') + R"("})";
auto json_record = make_unique<nfc::NdefRecord>();
json_record->set_tnf(nfc::TNF_MIME_MEDIA);
json_record->set_type("application/json");
json_record->set_payload(json_payload);
message.add_record(std::move(json_record));
auto encoded = message.encode();
ASSERT_GE(encoded.size(), 255u) << "test fixture must exercise the long-form TLV path";
// Wrap exactly like write_mifare_ultralight_tag_ does: 0x03 FF LEN_HI LEN_LO ... 0xFE
std::vector<uint8_t> wrapped = {0x03, 0xFF, static_cast<uint8_t>((encoded.size() >> 8) & 0xFF),
static_cast<uint8_t>(encoded.size() & 0xFF)};
wrapped.insert(wrapped.end(), encoded.begin(), encoded.end());
wrapped.push_back(0xFE);
// Real tag memory is [page 3 (4 bytes, unused here)] [wrapped TLV ...].
std::vector<uint8_t> tag_memory = {0x00, 0x00, 0x00, 0x00};
tag_memory.insert(tag_memory.end(), wrapped.begin(), wrapped.end());
// find_mifare_ultralight_ndef_ only ever sees the first 16 bytes (pages 3-6) when locating the
// TLV header -- exactly what read_mifare_ultralight_tag_'s first, fixed-size read provides.
std::vector<uint8_t> page_3_to_6(tag_memory.begin(), tag_memory.begin() + 16);
TestablePN532 pn532;
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(page_3_to_6, message_length, message_start_index));
EXPECT_EQ(message_length, encoded.size());
EXPECT_EQ(message_start_index, 4);
// Reconstruct what read_mifare_ultralight_tag_ hands to NdefMessage: page 3 trimmed off, then
// sliced to [message_start_index, message_start_index + message_length).
std::vector<uint8_t> full_tag_data(tag_memory.begin() + 4, tag_memory.end());
std::vector<uint8_t> message_bytes(full_tag_data.begin() + message_start_index,
full_tag_data.begin() + message_start_index + message_length);
nfc::NdefMessage parsed(message_bytes);
const auto &records = parsed.get_records();
ASSERT_EQ(records.size(), 3u);
EXPECT_EQ(records[0]->get_type(), "U");
EXPECT_EQ(records[0]->get_payload(), "https://esphome.io/");
EXPECT_EQ(records[1]->get_type(), "T");
EXPECT_EQ(records[1]->get_payload(), "Hello, ESPHome!");
EXPECT_EQ(records[2]->get_type(), "application/json");
EXPECT_EQ(records[2]->get_payload(), json_payload);
}
/// End-to-end: a tag holding a message well past 255 bytes is read in full. The read buffer must grow to fit the
/// whole message; a fixed 255-byte buffer would cut it short and the records would fail to parse.
TEST(ReadMifareUltralightTag, LongFormMessageIsReadInFull) {
nfc::NdefMessage message;
message.add_uri_record("https://esphome.io/");
const std::string text(600, 'x');
message.add_text_record(text);
auto encoded = message.encode();
ASSERT_GT(encoded.size(), 600u);
TestablePN532 pn532;
pn532.tag_memory = make_tag_memory(wrap_tlv(encoded), 888); // NTAG216
nfc::NfcTagUid uid = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
auto tag = pn532.read_mifare_ultralight_tag_(uid);
ASSERT_TRUE(tag->has_ndef_message());
const auto &records = tag->get_ndef_message()->get_records();
ASSERT_EQ(records.size(), 2u);
EXPECT_EQ(records[0]->get_payload(), "https://esphome.io/");
EXPECT_EQ(records[1]->get_payload(), text);
}
TEST(ReadMifareUltralightTag, ShortFormMessageIsRead) {
nfc::NdefMessage message;
message.add_uri_record("https://esphome.io/");
auto encoded = message.encode();
TestablePN532 pn532;
pn532.tag_memory = make_tag_memory(wrap_tlv(encoded), 144); // NTAG213
nfc::NfcTagUid uid = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
auto tag = pn532.read_mifare_ultralight_tag_(uid);
ASSERT_TRUE(tag->has_ndef_message());
const auto &records = tag->get_ndef_message()->get_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_EQ(records[0]->get_payload(), "https://esphome.io/");
}
/// A length larger than the tag's capacity is rejected before any further pages are read.
TEST(ReadMifareUltralightTag, LengthBeyondCapacityIsRejected) {
TestablePN532 pn532;
pn532.tag_memory = make_tag_memory({0x03, 0xFF, 0x01, 0x2C}, 144); // claims 300 bytes on a 144-byte tag
nfc::NfcTagUid uid = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
auto tag = pn532.read_mifare_ultralight_tag_(uid);
EXPECT_FALSE(tag->has_ndef_message());
}
} // namespace esphome::pn532::testing
@@ -0,0 +1,9 @@
# The hub with only the presence binary sensor, which nests a USE_SENSOR guard in process_frame_()
seeed_mr60bha2:
id: my_seeed_mr60bha2_binary_sensor
binary_sensor:
- platform: seeed_mr60bha2
mr60bha2_id: my_seeed_mr60bha2_binary_sensor
has_target:
name: "Person Information Only"
@@ -0,0 +1,3 @@
# The hub alone, without the sensors and binary sensor that process_frame_() guards
seeed_mr60bha2:
id: my_seeed_mr60bha2_minimal
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60bha2: !include common-binary-sensor.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60bha2: !include common-minimal.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60bha2: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
seeed_mr60bha2: !include common.yaml
@@ -0,0 +1,3 @@
# The hub alone, without the binary sensors and selects that process_frame_() guards
seeed_mr60fda2:
id: my_seeed_mr60fda2_minimal
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60fda2: !include common-minimal.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60fda2: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
seeed_mr60fda2: !include common.yaml
@@ -0,0 +1,15 @@
light:
- platform: shelly_dimmer
name: Shelly Dimmer Light
power:
name: Shelly Dimmer Power
voltage:
name: Shelly Dimmer Voltage
current:
name: Shelly Dimmer Current
max_brightness: 500
firmware:
version: "51.6"
update: true
nrst_pin: 13
boot0_pin: 14
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
shelly_dimmer: !include common-update.yaml
@@ -102,6 +102,19 @@ TEST_F(TcpClientLinkClock, PendingConnectTimesOutIntoBackoff) {
EXPECT_TRUE(this->link_.in_backoff());
}
TEST_F(TcpClientLinkClock, ConnectingUntilThePendingAttemptEnds) {
this->link_.begin("link_test");
EXPECT_FALSE(this->link_.connecting());
this->set_pending_socket();
this->link_.note_attempt();
this->link_.poll();
EXPECT_TRUE(this->link_.connecting());
set_loop_time(1000 + 10000);
this->link_.poll();
EXPECT_FALSE(this->link_.connecting());
EXPECT_FALSE(this->link_.connected());
}
TEST_F(TcpClientLinkClock, ConnectTimeoutFollowsALongerInterval) {
this->link_.set_reconnect_interval(20000);
this->link_.begin("link_test");
@@ -177,6 +190,7 @@ TEST_F(TcpClientLinkClock, BackoffSpansAMillisWrap) {
TEST(TcpClientLink, AdoptedSocketFlushesQueuedBytes) {
LinkPeer p;
ASSERT_TRUE(p.link_.connected());
EXPECT_FALSE(p.link_.connecting());
EXPECT_EQ(p.link_.queue(reinterpret_cast<const uint8_t *>("ping"), 4), 4u);
EXPECT_TRUE(p.link_.flush_tx());
char buf[8];
+17
View File
@@ -954,6 +954,23 @@ def test_yaml_merge_chain_include_resolves() -> None:
assert config["base"]["existing"] == "value"
def test_parse_yaml_custom_loader_resolves_root_include() -> None:
"""A document that is only `!include` is resolved for a custom loader too."""
parent = Path("/fake/main.yaml")
loader_calls: list[Path] = []
def custom_loader(fname: Path) -> dict[str, int]:
loader_calls.append(fname)
return {"x": 1}
config = yaml_util.parse_yaml(
parent, io.StringIO("!include inner.yaml\n"), custom_loader
)
assert config == {"x": 1}
assert loader_calls == [Path("/fake/inner.yaml")]
def test_yaml_merge_chain_include_depth_exceeded() -> None:
"""Chain includes in merge keys exceeding depth limit raise a clear error."""
parent = Path("/fake/main.yaml")