Compare commits

...
Author SHA1 Message Date
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 c1fa73470e [display] Walk the blit fallback in frame buffer order and resolve the bitness once (#20651) 2026-10-13 17:31:13 -10:00
J. Nick Koston e41d16ba7e [haier] Keep the alarm message table in flash (#20641) 2026-10-14 03:23:28 +00:00
93 changed files with 1783 additions and 332 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()
+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)
+53 -27
View File
@@ -5,6 +5,7 @@
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
this->line(x1, y1, x2, y2, color);
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
// -Os stops inlining it once there are two call sites, which costs a call and
// spills per pixel. Without PSRAM there is one call site and it changes
// nothing, and draw_pixel_at() stays a virtual call either way.
template<ColorBitness BITNESS>
static void __attribute__((flatten))
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
bool big_endian, int x_offset, int y_offset, int x_pad) {
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
// BITNESS is a constant here, so to_color()'s three per pixel divisions
// become a multiply and shift and its bitness switch folds.
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
display->draw_pixel_at(
x + x_start, y + y_start,
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
};
// The source and frame buffers are row-major, so walking rows keeps the read
// and the write sequential. When the display swaps the axes before writing,
// walking columns keeps the write sequential instead; with the frame buffer
// in PSRAM a strided write costs more than a strided read.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
size_t column_idx = y_offset * line_stride + x_offset;
for (int x = 0; x != w; x++, column_idx++) {
size_t source_idx = column_idx;
for (int y = 0; y != h; y++, source_idx += line_stride) {
draw_source_pixel(x, y, source_idx);
}
}
return;
}
#endif
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
draw_source_pixel(x, y, source_idx);
}
}
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
switch (bitness) {
case COLOR_BITNESS_565:
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
case COLOR_BITNESS_888:
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
default:
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
}
}
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = x; i < x + width; i++)
+11
View File
@@ -139,6 +139,11 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
/// Whether a rotation swaps the x and y axes.
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
}
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
@@ -717,6 +722,12 @@ class Display : public PollingComponent {
virtual DisplayRotation get_rotation() const { return this->rotation_; }
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
/// neighbours along x land a row apart in the frame buffer. Not the same as
/// get_width()/get_height() swapping: a display that rotates in hardware
/// reports swapped dimensions but writes unswapped pixels.
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
@@ -2,6 +2,7 @@
#include <array>
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
namespace esphome::display {
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
@@ -10,6 +11,27 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
class ColorUtil {
public:
/// Reads one packed pixel of the given bitness from a source buffer into the
/// value to_color() takes. BITNESS is a template argument so the byte layout
/// folds; pass it on to to_color() as a constant too, and its channel scaling
/// divides by a constant, which the compiler turns into a multiply and shift.
template<ColorBitness BITNESS>
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
if constexpr (BITNESS == COLOR_BITNESS_565) {
const size_t idx = index * 2;
if (big_endian)
return (ptr[idx] << 8) + ptr[idx + 1];
return ptr[idx] + (ptr[idx + 1] << 8);
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
const size_t idx = index * 3;
if (big_endian)
return (ptr[idx + 0] << 16) + (ptr[idx + 1] << 8) + ptr[idx + 2];
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
} else {
return ptr[index];
}
}
static Color to_color(uint32_t colorcode, ColorOrder color_order,
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
uint8_t first_color, second_color, third_color;
+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")
@@ -106,6 +106,7 @@ class EPaperBase : public Display,
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
void draw_pixel_at(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
+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"
@@ -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 {
+198 -14
View File
@@ -2,7 +2,10 @@
#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"
#include "hon_packet.h"
@@ -11,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;
@@ -20,6 +43,27 @@ constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
const uint8_t ONE_BUF[] = {0x00, 0x01};
const uint8_t ZERO_BUF[] = {0x00, 0x00};
// Alarm texts indexed by alarm code; the last entry is the fallback for unknown codes
PROGMEM_STRING_TABLE(
HonAlarmMessages, "Outdoor module failure", "Outdoor defrost sensor failure",
"Outdoor compressor exhaust sensor failure", "Outdoor EEPROM abnormality", "Indoor coil sensor failure",
"Indoor-outdoor communication failure", "Power supply overvoltage protection",
"Communication failure between panel and indoor unit", "Outdoor compressor overheat protection",
"Outdoor environmental sensor abnormality", "Full water protection", "Indoor EEPROM failure",
"Outdoor out air sensor failure", "CBD and module communication failure", "Indoor DC fan failure",
"Outdoor DC fan failure", "Door switch failure", "Dust filter needs cleaning reminder", "Water shortage protection",
"Humidity sensor failure", "Indoor temperature sensor failure", "Manipulator limit failure",
"Indoor PM2.5 sensor failure", "Outdoor PM2.5 sensor failure", "Indoor heating overload/high load alarm",
"Outdoor AC current protection", "Outdoor compressor operation abnormality", "Outdoor DC current protection",
"Outdoor no-load failure", "CT current abnormality", "Indoor cooling freeze protection",
"High and low pressure protection", "Compressor out air temperature is too high",
"Outdoor evaporator sensor failure", "Outdoor cooling overload", "Water pump drainage failure",
"Three-phase power supply failure", "Four-way valve failure", "External alarm/scraper flow switch failure",
"Temperature cutoff protection alarm", "Different mode operation failure", "Electronic expansion valve failure",
"Dual heat source sensor Tw failure", "Communication failure with the wired controller",
"Indoor unit address duplication failure", "50Hz zero crossing failure", "Outdoor unit failure",
"Formaldehyde sensor failure", "VOC sensor failure", "CO2 sensor failure", "Firewall failure", "Unknown");
HonClimate::HonClimate()
: cleaning_status_(CleaningState::NO_CLEANING), got_valid_outdoor_temp_(false), active_alarms_{0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
@@ -67,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 {
@@ -511,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;
@@ -646,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();
}
{
@@ -687,9 +791,17 @@ void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, boo
if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
: "Unknown";
#ifdef USE_ESP8266
// Static so a delayed on_alarm automation can still read it, but only until the next
// alarm change, including a later one in this packet; `code` stays correct
static char alarm_message[HonAlarmMessages::MAX_LENGTH + 1];
ESPHOME_strncpy_P(alarm_message,
reinterpret_cast<const char *>(
HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX)),
sizeof(alarm_message));
#else
const char *alarm_message = HonAlarmMessages::get_progmem_str(alarm_code, HonAlarmMessages::LAST_INDEX);
#endif
esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
alarm_code, alarm_message);
if (alarm_status) {
@@ -810,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_;
@@ -1044,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();
@@ -1229,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()) {
@@ -1256,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};
-56
View File
@@ -197,60 +197,4 @@ enum class SubcommandsControl : uint16_t {
// content: all values like in status packet)
};
const std::string HON_ALARM_MESSAGES[] = {
"Outdoor module failure",
"Outdoor defrost sensor failure",
"Outdoor compressor exhaust sensor failure",
"Outdoor EEPROM abnormality",
"Indoor coil sensor failure",
"Indoor-outdoor communication failure",
"Power supply overvoltage protection",
"Communication failure between panel and indoor unit",
"Outdoor compressor overheat protection",
"Outdoor environmental sensor abnormality",
"Full water protection",
"Indoor EEPROM failure",
"Outdoor out air sensor failure",
"CBD and module communication failure",
"Indoor DC fan failure",
"Outdoor DC fan failure",
"Door switch failure",
"Dust filter needs cleaning reminder",
"Water shortage protection",
"Humidity sensor failure",
"Indoor temperature sensor failure",
"Manipulator limit failure",
"Indoor PM2.5 sensor failure",
"Outdoor PM2.5 sensor failure",
"Indoor heating overload/high load alarm",
"Outdoor AC current protection",
"Outdoor compressor operation abnormality",
"Outdoor DC current protection",
"Outdoor no-load failure",
"CT current abnormality",
"Indoor cooling freeze protection",
"High and low pressure protection",
"Compressor out air temperature is too high",
"Outdoor evaporator sensor failure",
"Outdoor cooling overload",
"Water pump drainage failure",
"Three-phase power supply failure",
"Four-way valve failure",
"External alarm/scraper flow switch failure",
"Temperature cutoff protection alarm",
"Different mode operation failure",
"Electronic expansion valve failure",
"Dual heat source sensor Tw failure",
"Communication failure with the wired controller",
"Indoor unit address duplication failure",
"50Hz zero crossing failure",
"Outdoor unit failure",
"Formaldehyde sensor failure",
"VOC sensor failure",
"CO2 sensor failure",
"Firewall failure",
};
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
} // namespace esphome::haier::hon_protocol
@@ -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:
+1
View File
@@ -204,6 +204,7 @@ class IT8951Display : public Display,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
int get_width() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->height_ : this->width_; }
int get_height() override { return (this->effective_transform_ & TRANSFORM_SWAP_XY) ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & TRANSFORM_SWAP_XY; }
protected:
int get_height_internal() override { return this->height_; }
+5 -13
View File
@@ -130,19 +130,8 @@ class MipiSpi : public display::Display,
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
int get_width() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return HEIGHT;
return WIDTH;
}
int get_height() override {
if (this->rotation_ == display::DISPLAY_ROTATION_90_DEGREES ||
this->rotation_ == display::DISPLAY_ROTATION_270_DEGREES)
return WIDTH;
return HEIGHT;
}
int get_width() override { return display::rotation_swaps_axes(this->rotation_) ? HEIGHT : WIDTH; }
int get_height() override { return display::rotation_swaps_axes(this->rotation_) ? WIDTH : HEIGHT; }
void set_init_sequence(const uint8_t *sequence, size_t len) {
this->init_sequence_ = sequence;
@@ -621,6 +610,9 @@ class MipiSpiBuffer
#endif
}
// With hardware rotation the buffer is written in drawn coordinates.
bool pixel_axes_swapped() const override { return !HAS_HARDWARE_ROTATION && display::Display::pixel_axes_swapped(); }
// Draw a pixel at the given coordinates.
void draw_pixel_at(int x, int y, Color color) override {
if (this->is_point_clipped(x, y))
+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"
+7 -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,9 @@
#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_WIFI_SCAN_RESULTS_LOCK
#define USE_ESP32_BLE
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
@@ -478,6 +480,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 +492,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 +695,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
+20
View File
@@ -0,0 +1,20 @@
from pathlib import Path
import esphome.codegen as cg
from esphome.const import CONF_ESPHOME, CONF_INCLUDES
from esphome.core import CORE
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
TESTS_DIR = Path(__file__).parent
def override_manifest(manifest: ComponentManifestOverride) -> None:
# The column walk in Display::draw_pixels_at() is only built with PSRAM; define it
# when display's own tests are built so both walk orders run, and leave other
# components' test binaries on the plain build.
async def to_code_testing(config: ConfigType) -> None:
if any(Path(p) == TESTS_DIR for p in CORE.config[CONF_ESPHOME][CONF_INCLUDES]):
cg.add_define("USE_PSRAM")
manifest.to_code = to_code_testing
@@ -0,0 +1,109 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <initializer_list>
#include <tuple>
#include <vector>
#include "esphome/components/display/display.h"
namespace esphome::display::testing {
// Records every pixel write; pixel_axes_swapped() picks the walk order.
class RecordingDisplay : public Display {
public:
void set_axes_swapped(bool swapped) { this->axes_swapped_ = swapped; }
bool pixel_axes_swapped() const override { return this->axes_swapped_; }
void draw_pixel_at(int x, int y, Color color) override { this->pixels_.emplace_back(x, y, color.raw_32); }
void update() override {}
DisplayType get_display_type() override { return DisplayType::DISPLAY_TYPE_COLOR; }
int get_width_internal() override { return 64; }
int get_height_internal() override { return 64; }
std::vector<std::tuple<int, int, uint32_t>> sorted_pixels() const {
auto sorted = this->pixels_;
std::sort(sorted.begin(), sorted.end());
return sorted;
}
size_t count() const { return this->pixels_.size(); }
const std::vector<std::tuple<int, int, uint32_t>> &pixels() const { return this->pixels_; }
protected:
bool axes_swapped_{false};
std::vector<std::tuple<int, int, uint32_t>> pixels_;
};
// A 7x5 region with a 2 pixel left offset, a 3 line top offset and 4 pixels of
// right padding, so the source index arithmetic of both walks is exercised.
static constexpr int W = 7, H = 5, X_OFFSET = 2, Y_OFFSET = 3, X_PAD = 4;
static constexpr int LINE_STRIDE = X_OFFSET + W + X_PAD;
static constexpr int LINES = Y_OFFSET + H;
static std::vector<uint8_t> make_source(int bytes_per_pixel) {
std::vector<uint8_t> source(LINE_STRIDE * LINES * bytes_per_pixel);
uint8_t value = 1;
for (auto &byte : source)
byte = value++;
return source;
}
static void expect_walks_match(ColorBitness bitness, int bytes_per_pixel, bool big_endian) {
const auto source = make_source(bytes_per_pixel);
RecordingDisplay rows;
RecordingDisplay columns;
columns.set_axes_swapped(true);
rows.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
columns.draw_pixels_at(10, 20, W, H, source.data(), COLOR_ORDER_RGB, bitness, big_endian, X_OFFSET, Y_OFFSET, X_PAD);
EXPECT_EQ(rows.count(), static_cast<size_t>(W * H));
EXPECT_EQ(columns.count(), static_cast<size_t>(W * H));
EXPECT_EQ(rows.sorted_pixels(), columns.sorted_pixels());
}
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk332) { expect_walks_match(COLOR_BITNESS_332, 1, true); }
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565BigEndian) { expect_walks_match(COLOR_BITNESS_565, 2, true); }
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk565LittleEndian) { expect_walks_match(COLOR_BITNESS_565, 2, false); }
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888BigEndian) { expect_walks_match(COLOR_BITNESS_888, 3, true); }
TEST(DrawPixelsAt, ColumnWalkMatchesRowWalk888LittleEndian) { expect_walks_match(COLOR_BITNESS_888, 3, false); }
// One known pixel per bitness and byte order, so a decode that swapped bytes or channels
// could not pass the walk comparison above by being wrong the same way twice.
static void expect_golden(ColorBitness bitness, std::initializer_list<uint8_t> bytes, bool big_endian, Color expected) {
const std::vector<uint8_t> source(bytes);
RecordingDisplay display;
display.draw_pixels_at(3, 4, 1, 1, source.data(), COLOR_ORDER_RGB, bitness, big_endian, 0, 0, 0);
ASSERT_EQ(display.count(), 1u);
EXPECT_EQ(display.pixels()[0], std::make_tuple(3, 4, expected.raw_32));
}
TEST(DrawPixelsAt, Golden565BigEndian) {
expect_golden(COLOR_BITNESS_565, {0xE0, 0x07}, true, Color(230, 0, 57)); // 0xE007: r 28, g 0, b 7
}
TEST(DrawPixelsAt, Golden565LittleEndian) {
expect_golden(COLOR_BITNESS_565, {0xE0, 0x07}, false, Color(0, 255, 0)); // 0x07E0: pure green
}
TEST(DrawPixelsAt, Golden888BigEndian) {
expect_golden(COLOR_BITNESS_888, {0x10, 0x20, 0x30}, true, Color(0x10, 0x20, 0x30));
}
TEST(DrawPixelsAt, Golden888LittleEndian) {
expect_golden(COLOR_BITNESS_888, {0x10, 0x20, 0x30}, false, Color(0x30, 0x20, 0x10));
}
TEST(DrawPixelsAt, Golden332) {
expect_golden(COLOR_BITNESS_332, {0xE3}, true, Color(255, 0, 255)); // r 7 of 7, g 0, b 3 of 3
}
// The column walk really is a different order, otherwise the tests above prove nothing.
TEST(DrawPixelsAt, ColumnWalkChangesOrder) {
const auto source = make_source(1);
RecordingDisplay rows;
RecordingDisplay columns;
columns.set_axes_swapped(true);
rows.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET, X_PAD);
columns.draw_pixels_at(0, 0, W, H, source.data(), COLOR_ORDER_RGB, COLOR_BITNESS_332, true, X_OFFSET, Y_OFFSET,
X_PAD);
ASSERT_GT(rows.count(), 1u);
ASSERT_GT(columns.count(), 1u);
EXPECT_EQ(std::get<0>(rows.pixels()[1]), 1); // the row walk steps x first
EXPECT_EQ(std::get<1>(columns.pixels()[1]), 1); // the column walk steps y first
}
} // namespace esphome::display::testing
+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,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")