Compare commits

...
11 Commits
Author SHA1 Message Date
91e0edc11d [pn532] Fix NDEF parsing greater than 255 bytes (#18188)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:15:48 -10:00
Jonathan Swoboda 9fe75e262f [ci] Add a host clang-tidy environment and job (#20747) 2026-10-14 08:09:53 -10:00
Kevin Ahrendt 0132f722a3 [audio] Bump esp-audio-libs to v4.1.0 (#20754) 2026-10-14 13:24:36 -04:00
Jonathan Swoboda 7d7a78308d [ci] Test tinyusb_keyboard in isolation (#20749) 2026-10-14 10:05:02 -04:00
J. Nick Koston 9c568525b0 [core] Add CodSpeed benchmarks for importing entity components (#20648) 2026-10-14 09:08:41 -04:00
J. Nick Koston 10f42e3e7c [shelly_dimmer] Build the STM32 firmware update path in CI (#20677) 2026-10-14 09:03:34 -04:00
J. Nick Koston 25d4b88605 [nfc] Add unit tests for the URI record prefix codes (#20724) 2026-10-14 09:01:59 -04:00
J. Nick Koston d6abf35138 [sx126x][sx127x] Stop the watchdog reset loop when a packet is sent before setup (#20727) 2026-10-14 09:01:10 -04:00
J. Nick Koston 66ea99ce65 [seeed_mr60fda2] Compile without binary sensors or selects (#20726) 2026-10-14 09:00:08 -04:00
J. Nick Koston d9298cb7b9 [seeed_mr60bha2] Compile without sensors or binary sensors (#20729) 2026-10-14 08:59:27 -04:00
J. Nick Koston 34e1cf96d5 [bmi270] Build the component on ESP8266 in CI (#20731) 2026-10-14 08:58:56 -04:00
48 changed files with 768 additions and 105 deletions
+20 -6
View File
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
determine-jobs:
name: Determine which jobs to run
@@ -415,8 +415,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -695,6 +695,10 @@ jobs:
name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2
pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy
name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
@@ -707,6 +711,16 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python
id: restore-python
@@ -1102,8 +1116,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+1 -1
View File
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
data = _get_data()
+2 -2
View File
@@ -59,8 +59,8 @@ namespace esphome::host {
char **get_argv() { return s_argv; }
const std::string &get_exe_path() {
static const std::string empty;
return s_exe_path != nullptr ? *s_exe_path : empty;
static const std::string EMPTY;
return s_exe_path != nullptr ? *s_exe_path : EMPTY;
}
void arm_reexec(const std::string &path) {
+1 -1
View File
@@ -5,7 +5,7 @@
#include "esphome/core/log.h"
#include "core.h"
#include <time.h>
#include <ctime>
#include <unistd.h>
#include <cerrno>
#include <cstdlib>
+2 -2
View File
@@ -39,8 +39,8 @@ bool Mutex::try_lock() { return static_cast<std::mutex *>(handle_)->try_lock();
void Mutex::unlock() { static_cast<std::mutex *>(handle_)->unlock(); }
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
static const uint8_t esphome_host_mac_address[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, esphome_host_mac_address, sizeof(esphome_host_mac_address));
static constexpr uint8_t HOST_MAC_ADDRESS[MAC_ADDRESS_SIZE] = USE_ESPHOME_HOST_MAC_ADDRESS;
memcpy(mac, HOST_MAC_ADDRESS, sizeof(HOST_MAC_ADDRESS));
}
} // namespace esphome
+4 -4
View File
@@ -2,6 +2,7 @@
#include <filesystem>
#include <fstream>
#include <utility>
#include "preferences.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -45,11 +46,10 @@ void HostPreferences::setup_() {
break;
if (fread(&len, sizeof(len), 1, fp) != 1)
break;
uint8_t data[len];
if (fread(data, sizeof(uint8_t), len, fp) != len)
std::vector<uint8_t> data(len);
if (fread(data.data(), sizeof(uint8_t), len, fp) != len)
break;
std::vector vec(data, data + len);
this->data_[key] = vec;
this->data_[key] = std::move(data);
}
fclose(fp);
}
@@ -53,7 +53,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
for (const auto &[name, value] : request_headers) {
h_headers.emplace(name, value);
}
httplib::Client client(scheme_host.c_str());
httplib::Client client(scheme_host);
if (!client.is_valid()) {
ESP_LOGE(TAG, "HTTP Request failed; Invalid URL: %s", url);
return nullptr;
@@ -114,7 +114,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const char *url, const c
}
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
for (const auto &header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first); // NOLINT
if (should_collect_header(lower_case_collect_headers, lower_name)) {
+17 -17
View File
@@ -32,7 +32,7 @@ ESPHOME_LOG_TAG(TAG, "ota");
constexpr size_t MAX_OTA_SIZE = 256u * 1024u * 1024u; // 256 MiB
constexpr size_t HEADER_PEEK_SIZE = 64;
ssize_t read_header_(const char *path, uint8_t *buf, size_t len) {
ssize_t read_header(const char *path, uint8_t *buf, size_t len) {
int fd = ::open(path, O_RDONLY);
if (fd < 0)
return -1;
@@ -50,7 +50,7 @@ struct ElfIdent {
uint16_t e_type;
};
ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
ElfIdent parse_elf(const uint8_t *buf, size_t len) {
ElfIdent out{};
if (len < EI_NIDENT + 4)
return out;
@@ -76,17 +76,17 @@ ElfIdent parse_elf_(const uint8_t *buf, size_t len) {
return out;
}
bool validate_elf_(const char *staging_path, const std::string &exe_path) {
bool validate_elf(const char *staging_path, const std::string &exe_path) {
uint8_t new_buf[HEADER_PEEK_SIZE];
uint8_t cur_buf[HEADER_PEEK_SIZE];
ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
ssize_t new_n = read_header(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header(exe_path.c_str(), cur_buf, sizeof(cur_buf));
if (new_n < static_cast<ssize_t>(EI_NIDENT + 4) || cur_n < static_cast<ssize_t>(EI_NIDENT + 4)) {
ESP_LOGE(TAG, "ELF header read failed");
return false;
}
ElfIdent new_id = parse_elf_(new_buf, new_n);
ElfIdent cur_id = parse_elf_(cur_buf, cur_n);
ElfIdent new_id = parse_elf(new_buf, new_n);
ElfIdent cur_id = parse_elf(cur_buf, cur_n);
if (!new_id.valid) {
ESP_LOGE(TAG, "Uploaded payload is not a valid ELF");
return false;
@@ -122,7 +122,7 @@ struct MachOIdent {
uint32_t cpusubtype;
};
MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
MachOIdent parse_macho(const uint8_t *buf, size_t len) {
MachOIdent out{};
// mach_header is the common prefix of mach_header and mach_header_64;
// cputype/cpusubtype/filetype have identical offsets in both.
@@ -153,18 +153,18 @@ MachOIdent parse_macho_(const uint8_t *buf, size_t len) {
return out;
}
bool validate_macho_(const char *staging_path, const std::string &exe_path) {
bool validate_macho(const char *staging_path, const std::string &exe_path) {
uint8_t new_buf[HEADER_PEEK_SIZE];
uint8_t cur_buf[HEADER_PEEK_SIZE];
ssize_t new_n = read_header_(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header_(exe_path.c_str(), cur_buf, sizeof(cur_buf));
ssize_t new_n = read_header(staging_path, new_buf, sizeof(new_buf));
ssize_t cur_n = read_header(exe_path.c_str(), cur_buf, sizeof(cur_buf));
if (new_n < static_cast<ssize_t>(sizeof(struct mach_header)) ||
cur_n < static_cast<ssize_t>(sizeof(struct mach_header))) {
ESP_LOGE(TAG, "Mach-O header read failed");
return false;
}
MachOIdent new_id = parse_macho_(new_buf, new_n);
MachOIdent cur_id = parse_macho_(cur_buf, cur_n);
MachOIdent new_id = parse_macho(new_buf, new_n);
MachOIdent cur_id = parse_macho(cur_buf, cur_n);
if (!new_id.valid) {
ESP_LOGE(TAG, "Uploaded payload is not a valid thin Mach-O executable");
return false;
@@ -182,11 +182,11 @@ bool validate_macho_(const char *staging_path, const std::string &exe_path) {
}
#endif // __APPLE__
bool validate_executable_(const char *staging_path, const std::string &exe_path) {
bool validate_executable(const char *staging_path, const std::string &exe_path) {
#ifdef __linux__
return validate_elf_(staging_path, exe_path);
return validate_elf(staging_path, exe_path);
#elif defined(__APPLE__)
return validate_macho_(staging_path, exe_path);
return validate_macho(staging_path, exe_path);
#else
(void) staging_path;
(void) exe_path;
@@ -298,7 +298,7 @@ OTAResponseTypes HostOTABackend::end() {
::close(this->fd_);
this->fd_ = -1;
if (!validate_executable_(this->staging_path_.c_str(), this->final_path_)) {
if (!validate_executable(this->staging_path_.c_str(), this->final_path_)) {
::unlink(this->staging_path_.c_str());
this->staging_path_.clear();
return OTA_RESPONSE_ERROR_UPDATE_END;
+7 -4
View File
@@ -50,13 +50,15 @@ static constexpr size_t PN532_FRAME_MAX_DATA_SIZE = 254;
static constexpr size_t PN532_FRAME_MAX_SIZE = 1 + 6 + PN532_FRAME_MAX_DATA_SIZE + 2;
/// Holds one frame in either direction so bus traffic never allocates
using PN532Frame = StaticVector<uint8_t, PN532_FRAME_MAX_SIZE>;
/// Holds pages 3 to 6 (16 bytes) plus an NDEF message of up to 255 bytes and its TLV header, rounded up to whole reads
using UltralightReadBuffer = StaticVector<uint8_t, 272>;
/// Longest NDEF message accepted from a MIFARE Ultralight/NTAG tag (the user memory of an NTAG216)
static constexpr uint32_t MIFARE_ULTRALIGHT_MAX_NDEF_SIZE = 888;
/// Longest NDEF message accepted from a MIFARE Classic tag (the capacity of a 4K tag)
static constexpr uint32_t MIFARE_CLASSIC_MAX_NDEF_SIZE = 3440;
/// A MIFARE READ answers with 16 bytes: one Classic block or four Ultralight pages
static constexpr size_t MIFARE_READ_SIZE = 16;
using MifareReadData = std::array<uint8_t, MIFARE_READ_SIZE>;
/// Pages 3 to 6, read in one go: the capability container and the start of the NDEF TLV
using UltralightHeader = std::array<uint8_t, MIFARE_READ_SIZE>;
class PN532BinarySensor;
@@ -134,10 +136,11 @@ class PN532 : public PollingComponent {
bool write_mifare_classic_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
std::unique_ptr<nfc::NfcTag> read_mifare_ultralight_tag_(nfc::NfcTagUid &uid);
bool read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, UltralightReadBuffer &data);
/// Fills `data` with the bytes from `start_page` onward
bool read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data);
bool is_mifare_ultralight_formatted_(std::span<const uint8_t> page_3_to_6);
uint16_t read_mifare_ultralight_capacity_();
bool find_mifare_ultralight_ndef_(std::span<const uint8_t> page_3_to_6, uint8_t &message_length,
bool find_mifare_ultralight_ndef_(std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
uint8_t &message_start_index);
bool write_mifare_ultralight_page_(uint8_t page_num, std::span<const uint8_t> write_data);
bool write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message);
@@ -1,5 +1,6 @@
#include <algorithm>
#include <array>
#include <cinttypes>
#include <memory>
#include "pn532.h"
@@ -10,10 +11,9 @@ namespace esphome::pn532 {
ESPHOME_LOG_TAG(TAG, "pn532.mifare_ultralight");
std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &uid) {
UltralightReadBuffer data;
UltralightHeader data;
// pages 3 to 6 contain various info we are interested in -- do one read to grab it all
if (!this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE * nfc::MIFARE_ULTRALIGHT_READ_SIZE,
data)) {
if (!this->read_mifare_ultralight_bytes_(3, data)) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
@@ -22,49 +22,68 @@ std::unique_ptr<nfc::NfcTag> PN532::read_mifare_ultralight_tag_(nfc::NfcTagUid &
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
uint8_t message_length;
uint32_t message_length;
uint8_t message_start_index;
if (!this->find_mifare_ultralight_ndef_(data, message_length, message_start_index)) {
ESP_LOGW(TAG, "Couldn't find NDEF message");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
ESP_LOGVV(TAG, "NDEF message length: %u, start: %u", message_length, message_start_index);
ESP_LOGVV(TAG, "NDEF message length: %" PRIu32 ", start: %u", message_length, message_start_index);
if (message_length == 0) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
// we already read pages 3-6 earlier -- pick up where we left off so we're not re-reading pages
const uint8_t read_length = message_length + message_start_index > 12 ? message_length + message_start_index - 12 : 0;
// data[2] is the CC byte read earlier as part of pages 3-6, same value used by read_mifare_ultralight_capacity_()
const uint32_t capacity = data[2] * 8U;
const uint32_t total_length = message_length + message_start_index;
if (total_length > capacity) {
ESP_LOGW(TAG, "NDEF message of %" PRIu32 " bytes at offset %u exceeds tag capacity %" PRIu32, message_length,
message_start_index, capacity);
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
if (message_length > MIFARE_ULTRALIGHT_MAX_NDEF_SIZE) {
ESP_LOGE(TAG, "NDEF message too long: %" PRIu32 " bytes", message_length);
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
// we already read pages 3-6 earlier, pick up where we left off so we're not re-reading pages
constexpr size_t header_message_bytes = MIFARE_READ_SIZE - nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // pages 4 to 6
const uint16_t read_length =
total_length > header_message_bytes ? static_cast<uint16_t>(total_length - header_message_bytes) : 0;
// A message up to the old 255 byte limit stays on the stack, a longer one borrows the heap for the read
SmallBufferWithHeapFallback<MIFARE_READ_SIZE + 256> buffer(data.size() + read_length);
std::span<uint8_t> message(buffer.get(), data.size() + read_length);
std::copy(data.begin(), data.end(), message.begin());
if (read_length) {
if (!read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3, read_length, data)) {
if (!this->read_mifare_ultralight_bytes_(nfc::MIFARE_ULTRALIGHT_DATA_START_PAGE + 3,
message.subspan(data.size()))) {
ESP_LOGE(TAG, "Error reading tag data");
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
}
// skip page 3 as well as any bytes ahead of message_start_index
const size_t skip = message_start_index + nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
if (skip >= data.size()) {
if (skip >= message.size()) {
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2);
}
return make_unique<nfc::NfcTag>(uid, nfc::NFC_FORUM_TYPE_2,
make_unique<nfc::NdefMessage>(std::span<const uint8_t>(data).subspan(skip)));
make_unique<nfc::NdefMessage>(std::span<const uint8_t>(message).subspan(skip)));
}
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes, UltralightReadBuffer &data) {
bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, std::span<uint8_t> data) {
MifareReadData chunk;
for (uint8_t i = 0; i * MIFARE_READ_SIZE < num_bytes; i++) {
for (uint8_t i = 0; i * MIFARE_READ_SIZE < data.size(); i++) {
// a READ returns 4 pages (16 bytes)
if (!this->mifare_read_(uint8_t(i * nfc::MIFARE_ULTRALIGHT_READ_SIZE + start_page), chunk)) {
return false;
}
// keep only the bytes still wanted from this read
const uint16_t remaining = num_bytes - i * MIFARE_READ_SIZE;
const size_t count = std::min<size_t>(MIFARE_READ_SIZE, remaining);
for (const uint8_t byte : std::span<const uint8_t>(chunk).subspan(0, count)) {
data.push_back(byte);
}
const size_t offset = i * MIFARE_READ_SIZE;
const size_t count = std::min(MIFARE_READ_SIZE, data.size() - offset);
std::copy_n(chunk.begin(), count, data.begin() + offset);
}
char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
@@ -82,15 +101,15 @@ bool PN532::is_mifare_ultralight_formatted_(const std::span<const uint8_t> page_
}
uint16_t PN532::read_mifare_ultralight_capacity_() {
UltralightReadBuffer data;
if (this->read_mifare_ultralight_bytes_(3, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE, data) && data.size() > 2) {
std::array<uint8_t, nfc::MIFARE_ULTRALIGHT_PAGE_SIZE> data;
if (this->read_mifare_ultralight_bytes_(3, data)) {
ESP_LOGV(TAG, "Tag capacity is %u bytes", data[2] * 8U);
return data[2] * 8U;
}
return 0;
}
bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint8_t &message_length,
bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_to_6, uint32_t &message_length,
uint8_t &message_start_index) {
const uint8_t p4_offset = nfc::MIFARE_ULTRALIGHT_PAGE_SIZE; // page 4 will begin 4 bytes into the vector
@@ -98,16 +117,31 @@ bool PN532::find_mifare_ultralight_ndef_(const std::span<const uint8_t> page_3_t
return false;
}
uint8_t tlv_offset;
if (page_3_to_6[p4_offset + 0] == 0x03) {
message_length = page_3_to_6[p4_offset + 1];
message_start_index = 2;
return true;
tlv_offset = 0;
} else if (page_3_to_6[p4_offset + 5] == 0x03) {
message_length = page_3_to_6[p4_offset + 6];
message_start_index = 7;
return true;
tlv_offset = 5;
} else {
return false;
}
return false;
// Long-form TLV length: 0x03 0xFF LEN_HI LEN_LO, used whenever the message is >= 255 bytes.
// Short-form is just 0x03 LEN. See NFC Forum Type 2 Tag Operation spec section 2.3.
if (page_3_to_6[p4_offset + tlv_offset + 1] == 0xFF) {
if (page_3_to_6.size() <= p4_offset + tlv_offset + 3) {
// long-form marker present but the length bytes weren't read -- fail explicitly instead of
// misreporting a 255-byte short-form message
return false;
}
message_length =
(static_cast<uint32_t>(page_3_to_6[p4_offset + tlv_offset + 2]) << 8) | page_3_to_6[p4_offset + tlv_offset + 3];
message_start_index = tlv_offset + 4;
} else {
message_length = page_3_to_6[p4_offset + tlv_offset + 1];
message_start_index = tlv_offset + 2;
}
return true;
}
bool PN532::write_mifare_ultralight_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) {
@@ -1,5 +1,5 @@
#include "prometheus_handler.h"
#ifdef USE_NETWORK
#if defined(USE_NETWORK) && !defined(USE_HOST)
#include "esphome/core/application.h"
namespace esphome::prometheus {
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
#include <map>
#include <utility>
+3 -3
View File
@@ -157,7 +157,7 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
} else {
auto stride = x_offset + w + x_pad;
auto data = ptr + (stride * y_offset + x_offset) * 2;
const auto *data = ptr + (stride * y_offset + x_offset) * 2;
SDL_UpdateTexture(this->texture_, &rect, data, stride * 2);
}
this->redraw_(rect);
@@ -199,7 +199,7 @@ void Sdl::process_key(uint32_t keycode, bool down) {
callback->second(down);
}
Sdl *Sdl::instance_for_window_(uint32_t window_id) {
Sdl *Sdl::instance_for_window(uint32_t window_id) {
SDL_Window *window = SDL_GetWindowFromID(window_id);
if (window == nullptr)
return nullptr;
@@ -293,7 +293,7 @@ void Sdl::loop() {
continue;
}
Sdl *target = instance_for_window_(window_id);
Sdl *target = instance_for_window(window_id);
if (target == nullptr) {
// Nothing to route this to: the window has gone, or it is not one of ours. Say so, otherwise
// input that stops working leaves no trace at all.
+2 -2
View File
@@ -42,7 +42,7 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void dump_config() override { LOG_DISPLAY("", "SDL", this); }
template<typename F> void add_key_listener(int32_t keycode, F &&callback) {
if (!this->key_callbacks_.count(keycode)) {
if (!this->key_callbacks_.contains(keycode)) {
this->key_callbacks_[keycode] = CallbackManager<void(bool)>();
}
this->key_callbacks_[keycode].add(std::forward<F>(callback));
@@ -66,7 +66,7 @@ class Sdl final : public display::Display, public snapshot::Snapshot {
bool capture_bgr(uint8_t *dest, size_t row_stride) override;
void handle_event_(const SDL_Event &event);
/// The display owning the given window, or nullptr if it is not one of ours.
static Sdl *instance_for_window_(uint32_t window_id);
static Sdl *instance_for_window(uint32_t window_id);
SDL_Renderer *renderer_{};
SDL_Window *window_{};
SDL_Texture *texture_{};
@@ -162,6 +162,7 @@ bool MR60BHA2Component::validate_message_() {
void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, const uint8_t *data, size_t length) {
switch (frame_type) {
case BREATH_RATE_TYPE_BUFFER:
#ifdef USE_SENSOR
if (this->breath_rate_sensor_ != nullptr && length >= 4) {
uint32_t current_breath_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
if (current_breath_rate_int != 0) {
@@ -170,11 +171,14 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
this->breath_rate_sensor_->publish_state(breath_rate_float);
}
}
#endif
break;
case PEOPLE_EXIST_TYPE_BUFFER:
#ifdef USE_BINARY_SENSOR
if (this->has_target_binary_sensor_ != nullptr && length >= 2) {
uint16_t has_target_int = encode_uint16(data[1], data[0]);
this->has_target_binary_sensor_->publish_state(has_target_int);
#ifdef USE_SENSOR
if (has_target_int == 0) {
if (this->breath_rate_sensor_ != nullptr)
this->breath_rate_sensor_->publish_state(0.0);
@@ -185,9 +189,12 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
if (this->num_targets_sensor_ != nullptr)
this->num_targets_sensor_->publish_state(0);
}
#endif
}
#endif
break;
case HEART_RATE_TYPE_BUFFER:
#ifdef USE_SENSOR
if (this->heart_rate_sensor_ != nullptr && length >= 4) {
uint32_t current_heart_rate_int = encode_uint32(data[3], data[2], data[1], data[0]);
if (current_heart_rate_int != 0) {
@@ -196,8 +203,10 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
this->heart_rate_sensor_->publish_state(heart_rate_float);
}
}
#endif
break;
case DISTANCE_TYPE_BUFFER:
#ifdef USE_SENSOR
if (length >= 1 && data[0] != 0) {
if (this->distance_sensor_ != nullptr && length >= 8) {
uint32_t current_distance_int = encode_uint32(data[7], data[6], data[5], data[4]);
@@ -206,12 +215,15 @@ void MR60BHA2Component::process_frame_(uint16_t frame_id, uint16_t frame_type, c
this->distance_sensor_->publish_state(distance_float);
}
}
#endif
break;
case PRINT_CLOUD_BUFFER:
#ifdef USE_SENSOR
if (this->num_targets_sensor_ != nullptr && length >= 4) {
uint32_t current_num_targets_int = encode_uint32(data[3], data[2], data[1], data[0]);
this->num_targets_sensor_->publish_state(current_num_targets_int);
}
#endif
break;
default:
break;
@@ -96,6 +96,7 @@ static bool validate_checksum(const uint8_t *data, size_t len, uint8_t expected_
return calculate_checksum(data, len) == expected_checksum;
}
#ifdef USE_SELECT
static uint8_t find_nearest_index(float value, const float *arr, int size) {
int nearest_index = 0;
float min_diff = std::abs(value - arr[0]);
@@ -108,6 +109,7 @@ static uint8_t find_nearest_index(float value, const float *arr, int size) {
}
return nearest_index;
}
#endif
/**
* @brief Convert a float value to a byte array.
@@ -250,15 +252,19 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) {
void MR60FDA2Component::process_frame_() {
switch (this->current_frame_type_) {
case IS_FALL_TYPE_BUFFER:
#ifdef USE_BINARY_SENSOR
if (this->fall_detected_binary_sensor_ != nullptr) {
this->fall_detected_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
}
#endif
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
break;
case PEOPLE_EXIST_TYPE_BUFFER:
#ifdef USE_BINARY_SENSOR
if (this->people_exist_binary_sensor_ != nullptr)
this->people_exist_binary_sensor_->publish_state(this->current_frame_buf_[LEN_TO_HEAD_CKSUM]);
#endif
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
break;
@@ -293,6 +299,7 @@ void MR60FDA2Component::process_frame_() {
float install_height_float = 0;
float height_threshold_float = 0;
uint32_t current_sensitivity = 0;
#ifdef USE_SELECT
if (this->install_height_select_ != nullptr) {
uint32_t current_install_height_int =
encode_uint32(current_data_buf_[3], current_data_buf_[2], current_data_buf_[1], current_data_buf_[0]);
@@ -318,6 +325,7 @@ void MR60FDA2Component::process_frame_() {
uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3);
this->sensitivity_select_->publish_state(select_index);
}
#endif
ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float,
height_threshold_float, current_sensitivity);
+3 -3
View File
@@ -50,9 +50,9 @@ struct BitWriter {
uint32_t buffer{0};
unsigned count{0};
void put(uint32_t code, unsigned size) {
this->buffer |= code << this->count;
this->count += size;
void put(uint32_t value, unsigned bits) {
this->buffer |= value << this->count;
this->count += bits;
while (this->count >= 8) {
this->out.push_back(static_cast<uint8_t>(this->buffer & 0xFF));
this->buffer >>= 8;
+1 -1
View File
@@ -55,7 +55,7 @@ std::string sanitise_filename(const char *const name, const char *extension, boo
break;
}
char c = *p;
if (!(std::isalnum(static_cast<unsigned char>(c)) || c == '.' || c == '_' || c == '-')) {
if (!std::isalnum(static_cast<unsigned char>(c)) && c != '.' && c != '_' && c != '-') {
c = '_';
changed = true;
}
+7
View File
@@ -1,4 +1,5 @@
#include "sx126x.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -313,6 +314,11 @@ void SX126x::set_packet_params_(uint8_t payload_length) {
}
SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
// Callers such as a template number's on_value can run before setup() or after a failed setup
if (!this->is_ready()) {
ESP_LOGW(TAG, "Radio not ready, packet dropped");
return SX126xError::SX126X_ERROR_NOT_READY;
}
if (this->payload_length_ > 0 && this->payload_length_ != len) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX126xError::INVALID_PARAMS;
@@ -333,6 +339,7 @@ SX126xError SX126x::transmit_packet(const uint8_t *data, size_t len) {
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio1_pin_->digital_read()) {
App.feed_wdt();
if (millis() - start > TRANSMIT_TIMEOUT_MS) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX126xError::TIMEOUT;
+1 -1
View File
@@ -48,7 +48,7 @@ enum SX126xBw : uint8_t {
SX126X_BW_500000,
};
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
enum class SX126xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX126X_ERROR_NOT_READY };
class SX126xListener {
public:
+7
View File
@@ -1,4 +1,5 @@
#include "sx127x.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -256,6 +257,11 @@ size_t SX127x::get_max_packet_size() {
}
SX127xError SX127x::transmit_packet(const uint8_t *data, size_t len) {
// Callers such as a template number's on_value can run before setup() or after a failed setup
if (!this->is_ready()) {
ESP_LOGW(TAG, "Radio not ready, packet dropped");
return SX127xError::SX127X_ERROR_NOT_READY;
}
if (this->payload_length_ > 0 && this->payload_length_ != len) {
ESP_LOGE(TAG, "Packet size does not match config");
return SX127xError::INVALID_PARAMS;
@@ -292,6 +298,7 @@ SX127xError SX127x::transmit_packet(const uint8_t *data, size_t len) {
// wait until transmit completes, typically the delay will be less than 100 ms
uint32_t start = millis();
while (!this->dio0_pin_->digital_read()) {
App.feed_wdt();
if (millis() - start > 4000) {
ESP_LOGE(TAG, "Transmit packet failure");
ret = SX127xError::TIMEOUT;
+1 -1
View File
@@ -36,7 +36,7 @@ enum SX127xBw : uint8_t {
SX127X_BW_500_0,
};
enum class SX127xError { NONE = 0, TIMEOUT, INVALID_PARAMS };
enum class SX127xError { NONE = 0, TIMEOUT, INVALID_PARAMS, SX127X_ERROR_NOT_READY };
class SX127xListener {
public:
@@ -9,11 +9,11 @@
#error This HostUartComponent implementation is not supported on this host OS
#endif
#include <stdio.h>
#include <string.h>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <termios.h>
#include <sys/ioctl.h>
@@ -210,7 +210,6 @@ void HostUartComponent::write_array(const uint8_t *data, size_t len) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
}
#endif
return;
}
bool HostUartComponent::peek_byte(uint8_t *data) {
@@ -2,6 +2,8 @@
#ifdef USE_HOST
#include <utility>
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "uart_component.h"
@@ -10,7 +12,7 @@ namespace esphome::uart {
class HostUartComponent final : public UARTComponent, public Component {
public:
virtual ~HostUartComponent();
~HostUartComponent();
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::BUS; }
@@ -19,7 +21,7 @@ class HostUartComponent final : public UARTComponent, public Component {
bool read_array(uint8_t *data, size_t len) override;
size_t available() override;
UARTFlushResult flush() override;
void set_name(std::string port_name) { port_name_ = port_name; };
void set_name(std::string port_name) { this->port_name_ = std::move(port_name); }
protected:
void update_error_(const std::string &error);
@@ -1,5 +1,5 @@
#include "web_server_base.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
namespace esphome::web_server_base {
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR)
#if defined(USE_NETWORK) && !defined(USE_ZEPHYR) && !defined(USE_HOST)
#include <vector>
#include "esphome/core/progmem.h"
+7 -1
View File
@@ -331,7 +331,7 @@
#define USE_NOISE
#define USE_NOISE_SPARE_EPHEMERAL
#define USE_SHA256
#ifndef USE_RP2 // no MQTT backend or esp_wireguard library on RP2
#if !defined(USE_RP2) && !defined(USE_HOST) // no MQTT backend or esp_wireguard library on RP2 and host
#define USE_MQTT
#define USE_MQTT_COVER_JSON
#define USE_WIREGUARD
@@ -420,7 +420,9 @@
#define USE_ESPNOW
#define USE_ESPNOW_MAX_PAYLOAD_SIZE 1470
#ifndef USE_HOST // web_server, captive_portal: no ESPAsyncWebServer on host
#define USE_CAPTIVE_PORTAL
#endif
#define USE_WIFI_SCAN_RESULTS_LOCK
#define USE_ESP32_BLE
#define USE_ESP32_BLE_MAX_CONNECTIONS 3
@@ -478,6 +480,7 @@
#define USE_SPI
#define USE_SPI_PSRAM_DMA
#define USE_VOICE_ASSISTANT
#ifndef USE_HOST
#define USE_WEBSERVER
#define USE_WEBSERVER_AUTH
#define USE_WEBSERVER_AUTH_DIGEST
@@ -489,6 +492,7 @@
#define USE_WEBSERVER_ALLOWED_ORIGINS
#define WEB_SERVER_DEFAULT_HEADERS_COUNT 1
#define USE_CAPTIVE_PORTAL_GZIP
#endif
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_FAST_CONNECT_IN_FLASH
@@ -691,6 +695,8 @@
#endif
#ifdef USE_HOST
#define USE_ESPHOME_HOST_MAC_ADDRESS \
{ 0x98, 0x35, 0x69, 0xab, 0xf6, 0x79 }
#define USE_HTTP_REQUEST_RESPONSE
// Host only: the uart arm would shadow the native logger UART arms in other envs
#define USE_IMPROV_SERIAL_UART
+1 -1
View File
@@ -4,7 +4,7 @@ dependencies:
esphome/dlms_parser:
version: 2.2.0
esphome/esp-audio-libs:
version: 4.0.1
version: 4.1.0
esphome/esp-micro-speech-features:
version: 1.2.3
esphome/micro-decoder:
+17
View File
@@ -654,6 +654,23 @@ build_flags =
build_unflags =
${common.build_unflags}
[env:host-tidy]
extends = env:host
; every library a header under esphome/ may include: a missing one is a
; fatal error that breaks the all-include analysis for every later header
lib_deps =
${common.lib_deps}
${common:idf-component-libs.lib_deps}
${env:host.lib_deps}
build_flags =
${env:host.build_flags}
${flags:clangtidy.build_flags}
; analyze with the same language model as the embedded targets
-fno-exceptions
-fno-rtti
-I/usr/include/SDL2 ; sdl
-D__LINUX__ ; runtime_image: JPEGDEC's host detection, see its __init__.py
;;;;;;;; nRF52 ;;;;;;;;
[env:nrf52]
+1
View File
@@ -98,6 +98,7 @@ ISOLATED_COMPONENTS = {
"neopixelbus": "RMT type conflict with ESP32 Arduino/ESP-IDF headers (enum vs struct rmt_channel_t)",
"packages": "cannot merge packages",
"tinyusb": "Conflicts with usb_host component - cannot be used together",
"tinyusb_keyboard": "Depends on tinyusb which conflicts with usb_host",
"uart_mux": "Depends on tinyusb which conflicts with usb_host",
"usb_cdc_acm": "Depends on tinyusb which conflicts with usb_host",
}
+33 -9
View File
@@ -68,6 +68,9 @@ def clang_options(idedata, environment):
# extract target architecture from triplet in g++ filename
triplet = Path(idedata["cxx_path"]).name[:-4]
# env:host compiles with the system g++, so there is no triplet and the
# host's own target and headers are the right ones.
native = environment.startswith("host")
if triplet.startswith("xtensa-"):
# clang has an Xtensa frontend, but only a generic core -- the esp32 IDF
# toolchain headers (xtruntime, xtensa/config) need the GCC core config
@@ -83,6 +86,8 @@ def clang_options(idedata, environment):
cmd.append("-D__XTENSA__")
cmd.append("-D__XTENSA_EL__")
cmd.append("-D_LIBC")
elif native:
cmd.append("-Qunused-arguments")
else:
# RISC-V (and other non-Xtensa targets) have a real clang backend, so
# compile for the actual triplet. Espressif's RISC-V GCC -march adds
@@ -118,7 +123,7 @@ def clang_options(idedata, environment):
"-mfp16-format=ieee",
"--param=min-pagesize=0",
)
else:
elif not native:
cmd.extend(
[
# disable built-in include directories from the host
@@ -127,10 +132,11 @@ def clang_options(idedata, environment):
)
# set flags
if not native:
# disable built-in include directories from the host
cmd.append("-nostdinc")
cmd.extend(
[
# disable built-in include directories from the host
"-nostdinc",
# allow to condition code on the presence of clang-tidy
"-DCLANG_TIDY",
# (esp-idf) Fix __once_callable in some libstdc++ headers
@@ -314,11 +320,17 @@ def run_tidy(
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx")):
# MMIO peripheral access on these bare-metal platforms is all
# fixed-address.
disabled_checks.append("-clang-analyzer-core.FixedAddressDereference")
if args.environment.startswith("host"):
# glibc's assert() expands to a call the check mistakes for a
# constant condition.
disabled_checks += ["-misc-static-assert", "-cert-dcl03-c"]
if args.environment.startswith(("rp2", "bk72xx", "ln882h", "rtl87xx", "host")):
# bugprone-pointer-arithmetic-on-polymorphic-object (and its
# cert-ctr56-cpp alias) crashes clang-tidy 22 with infinite matcher
# recursion on lvgl_esphome.h under the RP2 defines.
# recursion on lvgl_esphome.h under the RP2 defines and on the
# all-include file under the host defines.
disabled_checks += [
"-clang-analyzer-core.FixedAddressDereference",
"-bugprone-pointer-arithmetic-on-polymorphic-object",
"-cert-ctr56-cpp",
]
@@ -327,6 +339,8 @@ def run_tidy(
if disabled_checks:
invocation.append("--checks=" + ",".join(disabled_checks))
invocation.append(f"--header-filter={Path(basepath).resolve()}/.*")
# Vendored third-party headers are kept as upstream wrote them.
invocation.append("--exclude-header-filter=.*/http_request/httplib\\.h")
invocation.append(str(Path(path).resolve()))
invocation.append("--")
invocation.extend(options)
@@ -448,6 +462,11 @@ def main():
if args.grep:
files = filter_grep(files, args.grep)
# Not every tidy environment carries googletest (LibreTiny does not),
# so a platform grep that matched a unit test would fail on
# <gtest/gtest.h>. The tests are analyzed by the full ESP32 IDF run,
# which has no --grep.
files = [f for f in files if not f.startswith("tests/")]
if args.exclude_grep:
excluded = set(filter_grep(files, args.exclude_grep))
@@ -464,11 +483,16 @@ def main():
# Add header file for checking (before early exit check)
if args.all_headers and args.split_at in (None, 1):
all_headers = [
os.path.relpath(p, cwd) for p in git_ls_files(["esphome/**/*.h"])
]
# The path filter applies to headers too: a component left out because
# it does not build in this environment must not come back via the
# all-include file.
if args.files:
all_headers = [p for p in all_headers if file_name_re.search(p)]
# When --changed is used, only include changed headers instead of all headers
if args.changed:
all_headers = [
os.path.relpath(p, cwd) for p in git_ls_files(["esphome/**/*.h"])
]
changed_headers = filter_changed(all_headers)
if changed_headers:
build_all_include(changed_headers)
@@ -476,7 +500,7 @@ def main():
else:
print("No changed headers to check")
else:
build_all_include()
build_all_include(all_headers)
files.insert(0, temp_header_file)
print(f"Added all-include header file, new total: {len(files)}")
@@ -0,0 +1,105 @@
"""CodSpeed benchmarks for importing entity components.
Every ``esphome config`` or ``esphome compile`` imports the Python module of
each component the YAML uses. A component that imports its optional
integrations (mqtt, web_server, zigbee) at module scope drags their whole
import chains in for every user, so the cost of a fresh import is what these
benchmarks pin down.
The core of esphome is imported once up front and stays loaded; each run
drops only the component modules from ``sys.modules`` and imports the
component again, so the import machinery and the module bodies of the
component and everything it pulls in run every time, as they do in a new
process, while a change to the core moves no number here. The modules and
registry entries the session had before a case are put back afterwards, so
later tests keep the objects they already hold.
Skipped when ``pytest-codspeed`` isn't installed so the regular unit-test
suite keeps working unchanged.
"""
from __future__ import annotations
from collections.abc import Generator, Iterable
import importlib
import sys
import pytest
pytest.importorskip("pytest_codspeed")
# Load the core before the first case so no case pays for it.
from esphome import automation, pins # noqa: E402
import esphome.codegen # noqa: E402, F401
import esphome.config_validation # noqa: E402, F401
COMPONENTS = ("binary_sensor", "sensor", "logger")
COMPONENT_PACKAGE = "esphome.components"
COMPONENT_PREFIX = COMPONENT_PACKAGE + "."
# Core registries that component module bodies write to when they are imported
REGISTRIES = (
automation.ACTION_REGISTRY,
automation.CONDITION_REGISTRY,
pins.PIN_SCHEMA_REGISTRY,
)
def _component_modules() -> dict[str, object]:
# The package itself goes too: ``from esphome.components import x`` is
# answered from its attributes, so a stale package would hide the import.
return {
n: m
for n, m in sys.modules.items()
if n == COMPONENT_PACKAGE or n.startswith(COMPONENT_PREFIX)
}
def _forget_component_modules() -> None:
for name in _component_modules():
del sys.modules[name]
def _import_component(name: str) -> object:
_forget_component_modules()
return importlib.import_module(COMPONENT_PREFIX + name)
_MISSING = object()
def _parent_bindings(names: Iterable[str]) -> list[tuple[object, str, object]]:
# An import binds the module to its parent package; a package may hold something else
# under that name (substitutions keeps a Jinja() instance as ``jinja``), so the value
# the parent has now is what goes back, not the module.
bindings = []
for name in names:
parent, _, child = name.rpartition(".")
bindings.append(
(sys.modules[parent], child, getattr(sys.modules[parent], child, _MISSING))
)
return bindings
@pytest.fixture
def restore_component_modules() -> Generator[None]:
saved = _component_modules()
saved_bindings = _parent_bindings(saved)
saved_registries = [dict(registry) for registry in REGISTRIES]
yield
_forget_component_modules()
sys.modules.update(saved)
for parent, child, value in saved_bindings:
if value is _MISSING:
delattr(parent, child)
else:
setattr(parent, child, value)
# The re-imported bodies registered their actions, conditions and pin schemas again
for registry, entries in zip(REGISTRIES, saved_registries, strict=True):
registry.clear()
registry.update(entries)
@pytest.mark.usefixtures("restore_component_modules")
@pytest.mark.parametrize("component", COMPONENTS)
def test_import_component(benchmark, component: str) -> None:
benchmark(_import_component, component)
@@ -0,0 +1,3 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
bmi270: !include common.yaml
@@ -0,0 +1,96 @@
#include <gtest/gtest.h>
#include <iterator>
#include <string>
#include <vector>
#include "esphome/components/nfc/ndef_record_uri.h"
namespace esphome::nfc::testing {
static std::string decode(const std::vector<uint8_t> &payload) { return NdefRecordUri(payload).get_payload(); }
static uint8_t encoded_prefix(const std::string &uri) { return NdefRecordUri(uri).get_encoded_payload()[0]; }
TEST(NdefRecordUri, DecodeExpandsPrefixCode) {
EXPECT_EQ(decode({0x01, 'a', '.', 'b'}), "http://www.a.b");
EXPECT_EQ(decode({0x04, 'a', '.', 'b'}), "https://a.b");
EXPECT_EQ(decode({0x07, 'a'}), "ftp://anonymous:anonymous@a");
}
// URI identifier codes from the NFC Forum URI Record Type Definition, kept separate from the
// component's table so a reordered entry fails here
static constexpr const char *SPEC_PREFIXES[] = {"",
"http://www.",
"https://www.",
"http://",
"https://",
"tel:",
"mailto:",
"ftp://anonymous:anonymous@",
"ftp://ftp.",
"ftps://",
"sftp://",
"smb://",
"nfs://",
"ftp://",
"dav://",
"news:",
"telnet://",
"imap:",
"rtsp://",
"urn:",
"pop:",
"sip:",
"sips:",
"tftp:",
"btspp://",
"btl2cap://",
"btgoep://",
"tcpobex://",
"irdaobex://",
"file://",
"urn:epc:id:",
"urn:epc:tag:",
"urn:epc:pat:",
"urn:epc:raw:",
"urn:epc:",
"urn:nfc:"};
TEST(NdefRecordUri, DecodeEveryPrefixCode) {
for (size_t code = 1; code < std::size(SPEC_PREFIXES); code++)
EXPECT_EQ(decode({static_cast<uint8_t>(code), 'x'}), std::string(SPEC_PREFIXES[code]) + "x") << "code " << code;
}
TEST(NdefRecordUri, DecodeAcceptsLastPrefixCode) { EXPECT_EQ(decode({0x23, 'x'}), "urn:nfc:x"); }
TEST(NdefRecordUri, DecodeIgnoresUnknownPrefixCode) {
EXPECT_EQ(decode({0x00, 'x'}), "x");
EXPECT_EQ(decode({0x24, 'x'}), "x");
}
// Encode takes the first prefix in table order that matches, so codes 0x1E to 0x23 are never written;
// these tests pin that current behaviour, not the spec's longest match
TEST(NdefRecordUri, EncodePicksFirstMatchingPrefix) {
EXPECT_EQ(encoded_prefix("https://www.a.b"), 0x02);
EXPECT_EQ(encoded_prefix("http://a.b"), 0x03);
EXPECT_EQ(encoded_prefix("urn:epc:id:x"), 0x13);
}
TEST(NdefRecordUri, EncodeStopsBeforeLastPrefixCode) {
// urn:nfc: (0x23) is never chosen; the shorter urn: (0x13) matches first
EXPECT_EQ(encoded_prefix("urn:nfc:x"), 0x13);
}
TEST(NdefRecordUri, EncodeStripsPrefix) {
const std::vector<uint8_t> expected = {0x02, 'a', '.', 'b'};
EXPECT_EQ(NdefRecordUri("https://www.a.b").get_encoded_payload(), expected);
}
TEST(NdefRecordUri, EncodeWithoutPrefix) {
EXPECT_EQ(encoded_prefix("ht"), 0x00);
const std::vector<uint8_t> expected = {0x00, 'x', 'y'};
EXPECT_EQ(NdefRecordUri("xy").get_encoded_payload(), expected);
}
} // namespace esphome::nfc::testing
+5 -7
View File
@@ -163,9 +163,8 @@ TEST(PN532Mifare, UltralightReadTrimsLastChunk) {
pn532.responses.push_back(first);
pn532.responses.push_back(second);
UltralightReadBuffer data;
ASSERT_TRUE(pn532.read_mifare_ultralight_bytes_(4, 20, data));
ASSERT_EQ(data.size(), 20u);
std::array<uint8_t, 20> data{};
ASSERT_TRUE(pn532.read_mifare_ultralight_bytes_(4, data));
EXPECT_EQ(data[15], 15);
EXPECT_EQ(data[16], 0x10);
EXPECT_EQ(data[19], 0x13);
@@ -177,15 +176,14 @@ TEST(PN532Mifare, UltralightReadTrimsLastChunk) {
TEST(PN532Mifare, UltralightReadRejectsBadResponses) {
FakePN532 pn532;
UltralightReadBuffer data;
std::array<uint8_t, 16> data{};
pn532.responses.push_back({0x00, 0x01, 0x02}); // short response
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, 16, data));
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, data));
std::vector<uint8_t> failed(17, 0x00);
failed[0] = 0x01; // timeout status
pn532.responses.push_back(failed);
data.clear();
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, 16, data));
EXPECT_FALSE(pn532.read_mifare_ultralight_bytes_(4, data));
}
} // namespace esphome::pn532
@@ -0,0 +1,273 @@
#include <gtest/gtest.h>
#include <memory>
#include <span>
#include <vector>
#include "esphome/components/nfc/ndef_message.h"
#include "esphome/components/nfc/ndef_record.h"
#include "esphome/components/nfc/nfc.h"
#include "esphome/components/pn532/pn532.h"
namespace esphome::pn532::testing {
/// Exposes the protected Ultralight helpers and stands in for the bus. Each MIFARE READ is answered with the
/// 16 bytes of `tag_memory` starting at the requested page, so the full read path can run against synthetic tags.
class TestablePN532 : public PN532 {
public:
using PN532::find_mifare_ultralight_ndef_;
using PN532::read_mifare_ultralight_tag_;
/// Tag memory starting at page 0 (4 bytes per page)
std::vector<uint8_t> tag_memory;
protected:
bool is_read_ready() override { return true; }
bool write_data(std::span<const uint8_t> data) override {
this->last_written_.assign(data.begin(), data.end());
return true;
}
// only used for ACK frames; index 0 is the I2C status byte
bool read_data(PN532Frame &data, size_t len) override {
data = {0x01, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00};
return true;
}
bool read_response(uint8_t command, PN532Frame &data) override {
// frame is preamble, start code (2), LEN, LCS, TFI, InDataExchange, Tg, READ, page, DCS, postamble
if (this->last_written_.size() < 3)
return false;
const size_t start = this->last_written_[this->last_written_.size() - 3] * nfc::MIFARE_ULTRALIGHT_PAGE_SIZE;
data.clear();
data.push_back(0x00); // status: success
for (size_t i = start; i < start + MIFARE_READ_SIZE; i++) {
data.push_back(i < this->tag_memory.size() ? this->tag_memory[i] : 0x00);
}
return true;
}
std::vector<uint8_t> last_written_;
};
/// Builds Ultralight/NTAG tag memory holding `tlv` at page 4, with a capability container that reports
/// `capacity_bytes` of user memory.
std::vector<uint8_t> make_tag_memory(const std::vector<uint8_t> &tlv, uint16_t capacity_bytes) {
// pages 0-2: UID, lock bytes; page 3: CC (magic, version, size / 8, access)
std::vector<uint8_t> memory(12, 0x00);
memory.insert(memory.end(), {0xE1, 0x10, static_cast<uint8_t>(capacity_bytes / 8), 0x00});
memory.insert(memory.end(), tlv.begin(), tlv.end());
memory.resize(16 + capacity_bytes, 0x00);
return memory;
}
/// Wraps an encoded NDEF message in a Type 2 Tag TLV, using the long form when it is 255 bytes or more.
std::vector<uint8_t> wrap_tlv(const std::vector<uint8_t> &encoded) {
std::vector<uint8_t> tlv = {0x03};
if (encoded.size() < 255) {
tlv.push_back(static_cast<uint8_t>(encoded.size()));
} else {
tlv.insert(tlv.end(), {0xFF, static_cast<uint8_t>(encoded.size() >> 8), static_cast<uint8_t>(encoded.size())});
}
tlv.insert(tlv.end(), encoded.begin(), encoded.end());
tlv.push_back(0xFE);
return tlv;
}
// Builds a 16-byte page_3_to_6 buffer: 4 filler bytes for page 3 (unused by this function),
// followed by the given bytes starting at page 4, zero-padded out to the real 4-page read size.
std::vector<uint8_t> make_page_3_to_6(std::initializer_list<uint8_t> page4_onward) {
std::vector<uint8_t> data = {0x00, 0x00, 0x00, 0x00};
data.insert(data.end(), page4_onward);
data.resize(16, 0x00);
return data;
}
TEST(FindMifareUltralightNdef, ShortFormAtPage4Start) {
TestablePN532 pn532;
// 0x03 LEN -- message starts right after.
auto data = make_page_3_to_6({0x03, 0x20});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 0x20u);
EXPECT_EQ(message_start_index, 2);
}
TEST(FindMifareUltralightNdef, ShortFormAfterLeadingPadding) {
TestablePN532 pn532;
// A non-0x03 byte at page 4 (e.g. a lock/CC TLV) pushes the message TLV to offset 5.
auto data = make_page_3_to_6({0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0x10});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 0x10u);
EXPECT_EQ(message_start_index, 7);
}
// Regression test: prior to this fix, a >=255 byte NDEF message's TLV length was always read as
// a single byte, so the 0xFF long-form marker was mistaken for a length of 255 and the two real
// length bytes were mistaken for message content. That misalignment is what produced the
// "Corrupt record encountered; NdefMessage constructor aborting" error for large tags
TEST(FindMifareUltralightNdef, LongFormAtPage4Start) {
TestablePN532 pn532;
// 0x03 FF 01 2C -> long-form length = 0x012C = 300
auto data = make_page_3_to_6({0x03, 0xFF, 0x01, 0x2C, 0xD1, 0x01, 0x0C, 0x55});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 300u);
EXPECT_EQ(message_start_index, 4);
}
TEST(FindMifareUltralightNdef, LongFormAfterLeadingPadding) {
TestablePN532 pn532;
auto data = make_page_3_to_6({0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF, 0x02, 0x00});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
EXPECT_EQ(message_length, 0x0200u); // 512
EXPECT_EQ(message_start_index, 9);
}
TEST(FindMifareUltralightNdef, TooShortBufferIsRejected) {
TestablePN532 pn532;
std::vector<uint8_t> data = {0x00, 0x00, 0x00, 0x00, 0x03, 0x05}; // well under the size floor
uint32_t message_length = 0;
uint8_t message_start_index = 0;
EXPECT_FALSE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
}
TEST(FindMifareUltralightNdef, NoTlvTagIsRejected) {
TestablePN532 pn532;
auto data = make_page_3_to_6({0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00});
uint32_t message_length = 0;
uint8_t message_start_index = 0;
EXPECT_FALSE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
}
// Regression test: the entry size guard (page_3_to_6.size() > p4_offset + 6) is loose enough to admit
// buffers that are too short to hold the two long-form length bytes once the TLV is offset by leading
// padding (tlv_offset == 5). Before this fix, that case silently fell through to the short-form branch
// and reported a bogus 255-byte message instead of failing.
TEST(FindMifareUltralightNdef, LongFormMarkerWithMissingLengthBytesIsRejected) {
TestablePN532 pn532;
// Padding pushes the TLV to offset 5; 0x03 FF present, but the two length bytes are missing (size 11).
std::vector<uint8_t> data = {0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFF};
ASSERT_EQ(data.size(), 11u);
uint32_t message_length = 0;
uint8_t message_start_index = 0;
EXPECT_FALSE(pn532.find_mifare_ultralight_ndef_(data, message_length, message_start_index));
}
/// End-to-end regression guard: builds a real 3-record NDEF message (URL, text, and a MIME
/// record) padded past 255 bytes and wraps it in a Type 2 Tag TLV the way a real tag stores it,
/// then runs the wrapper through find_mifare_ultralight_ndef_ to locate it, and feeds the located slice back
/// into NdefMessage to confirm all three records survive the round trip.
TEST(FindMifareUltralightNdef, LongFormMessageRoundTripsThroughNdefMessage) {
nfc::NdefMessage message;
message.add_uri_record("https://esphome.io/");
message.add_text_record("Hello, ESPHome!");
std::string json_payload = R"({"key":"value","pad":")" + std::string(255, 'x') + R"("})";
auto json_record = make_unique<nfc::NdefRecord>();
json_record->set_tnf(nfc::TNF_MIME_MEDIA);
json_record->set_type("application/json");
json_record->set_payload(json_payload);
message.add_record(std::move(json_record));
auto encoded = message.encode();
ASSERT_GE(encoded.size(), 255u) << "test fixture must exercise the long-form TLV path";
// Wrap exactly like write_mifare_ultralight_tag_ does: 0x03 FF LEN_HI LEN_LO ... 0xFE
std::vector<uint8_t> wrapped = {0x03, 0xFF, static_cast<uint8_t>((encoded.size() >> 8) & 0xFF),
static_cast<uint8_t>(encoded.size() & 0xFF)};
wrapped.insert(wrapped.end(), encoded.begin(), encoded.end());
wrapped.push_back(0xFE);
// Real tag memory is [page 3 (4 bytes, unused here)] [wrapped TLV ...].
std::vector<uint8_t> tag_memory = {0x00, 0x00, 0x00, 0x00};
tag_memory.insert(tag_memory.end(), wrapped.begin(), wrapped.end());
// find_mifare_ultralight_ndef_ only ever sees the first 16 bytes (pages 3-6) when locating the
// TLV header -- exactly what read_mifare_ultralight_tag_'s first, fixed-size read provides.
std::vector<uint8_t> page_3_to_6(tag_memory.begin(), tag_memory.begin() + 16);
TestablePN532 pn532;
uint32_t message_length = 0;
uint8_t message_start_index = 0;
ASSERT_TRUE(pn532.find_mifare_ultralight_ndef_(page_3_to_6, message_length, message_start_index));
EXPECT_EQ(message_length, encoded.size());
EXPECT_EQ(message_start_index, 4);
// Reconstruct what read_mifare_ultralight_tag_ hands to NdefMessage: page 3 trimmed off, then
// sliced to [message_start_index, message_start_index + message_length).
std::vector<uint8_t> full_tag_data(tag_memory.begin() + 4, tag_memory.end());
std::vector<uint8_t> message_bytes(full_tag_data.begin() + message_start_index,
full_tag_data.begin() + message_start_index + message_length);
nfc::NdefMessage parsed(message_bytes);
const auto &records = parsed.get_records();
ASSERT_EQ(records.size(), 3u);
EXPECT_EQ(records[0]->get_type(), "U");
EXPECT_EQ(records[0]->get_payload(), "https://esphome.io/");
EXPECT_EQ(records[1]->get_type(), "T");
EXPECT_EQ(records[1]->get_payload(), "Hello, ESPHome!");
EXPECT_EQ(records[2]->get_type(), "application/json");
EXPECT_EQ(records[2]->get_payload(), json_payload);
}
/// End-to-end: a tag holding a message well past 255 bytes is read in full. The read buffer must grow to fit the
/// whole message; a fixed 255-byte buffer would cut it short and the records would fail to parse.
TEST(ReadMifareUltralightTag, LongFormMessageIsReadInFull) {
nfc::NdefMessage message;
message.add_uri_record("https://esphome.io/");
const std::string text(600, 'x');
message.add_text_record(text);
auto encoded = message.encode();
ASSERT_GT(encoded.size(), 600u);
TestablePN532 pn532;
pn532.tag_memory = make_tag_memory(wrap_tlv(encoded), 888); // NTAG216
nfc::NfcTagUid uid = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
auto tag = pn532.read_mifare_ultralight_tag_(uid);
ASSERT_TRUE(tag->has_ndef_message());
const auto &records = tag->get_ndef_message()->get_records();
ASSERT_EQ(records.size(), 2u);
EXPECT_EQ(records[0]->get_payload(), "https://esphome.io/");
EXPECT_EQ(records[1]->get_payload(), text);
}
TEST(ReadMifareUltralightTag, ShortFormMessageIsRead) {
nfc::NdefMessage message;
message.add_uri_record("https://esphome.io/");
auto encoded = message.encode();
TestablePN532 pn532;
pn532.tag_memory = make_tag_memory(wrap_tlv(encoded), 144); // NTAG213
nfc::NfcTagUid uid = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
auto tag = pn532.read_mifare_ultralight_tag_(uid);
ASSERT_TRUE(tag->has_ndef_message());
const auto &records = tag->get_ndef_message()->get_records();
ASSERT_EQ(records.size(), 1u);
EXPECT_EQ(records[0]->get_payload(), "https://esphome.io/");
}
/// A length larger than the tag's capacity is rejected before any further pages are read.
TEST(ReadMifareUltralightTag, LengthBeyondCapacityIsRejected) {
TestablePN532 pn532;
pn532.tag_memory = make_tag_memory({0x03, 0xFF, 0x01, 0x2C}, 144); // claims 300 bytes on a 144-byte tag
nfc::NfcTagUid uid = {0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
auto tag = pn532.read_mifare_ultralight_tag_(uid);
EXPECT_FALSE(tag->has_ndef_message());
}
} // namespace esphome::pn532::testing
@@ -0,0 +1,9 @@
# The hub with only the presence binary sensor, which nests a USE_SENSOR guard in process_frame_()
seeed_mr60bha2:
id: my_seeed_mr60bha2_binary_sensor
binary_sensor:
- platform: seeed_mr60bha2
mr60bha2_id: my_seeed_mr60bha2_binary_sensor
has_target:
name: "Person Information Only"
@@ -0,0 +1,3 @@
# The hub alone, without the sensors and binary sensor that process_frame_() guards
seeed_mr60bha2:
id: my_seeed_mr60bha2_minimal
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60bha2: !include common-binary-sensor.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60bha2: !include common-minimal.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60bha2: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
seeed_mr60bha2: !include common.yaml
@@ -0,0 +1,3 @@
# The hub alone, without the binary sensors and selects that process_frame_() guards
seeed_mr60fda2:
id: my_seeed_mr60fda2_minimal
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60fda2: !include common-minimal.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
seeed_mr60fda2: !include common.yaml
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
seeed_mr60fda2: !include common.yaml
@@ -0,0 +1,15 @@
light:
- platform: shelly_dimmer
name: Shelly Dimmer Light
power:
name: Shelly Dimmer Power
voltage:
name: Shelly Dimmer Voltage
current:
name: Shelly Dimmer Current
max_brightness: 500
firmware:
version: "51.6"
update: true
nrst_pin: 13
boot0_pin: 14
@@ -0,0 +1,3 @@
packages:
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
shelly_dimmer: !include common-update.yaml