mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91e0edc11d | ||
|
|
9fe75e262f | ||
|
|
0132f722a3 | ||
|
|
7d7a78308d | ||
|
|
9c568525b0 | ||
|
|
10f42e3e7c | ||
|
|
25d4b88605 | ||
|
|
d6abf35138 | ||
|
|
66ea99ce65 | ||
|
|
d9298cb7b9 | ||
|
|
34e1cf96d5 | ||
|
|
56cc03c4d4 | ||
|
|
35b416c6c0 | ||
|
|
7f2c3ba3f5 | ||
|
|
a97604d574 | ||
|
|
1146b77836 | ||
|
|
1338902ac5 | ||
|
|
60a1d70805 | ||
|
|
887d4c3a83 | ||
|
|
c1fa73470e | ||
|
|
e41d16ba7e |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -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]))
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "core.h"
|
||||
|
||||
#include <time.h>
|
||||
#include <ctime>
|
||||
#include <unistd.h>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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];
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user