mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 11:26:39 +00:00
Merge remote-tracking branch 'upstream/dev' into 20260218-zigbee-proxy
# Conflicts: # esphome/components/api/api_connection.cpp # esphome/components/api/api_pb2.h # esphome/components/api/api_pb2_defines.h # esphome/components/api/api_pb2_service.cpp # esphome/components/api/api_pb2_service.h # esphome/components/serial_proxy/serial_proxy.cpp
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
# Deprecated `pin: TEMPERATURE`, superseded by the `internal_temperature` platform.
|
||||
# Remove before 2027.2.0
|
||||
sensor:
|
||||
- id: adc_temperature_sensor
|
||||
platform: adc
|
||||
pin: TEMPERATURE
|
||||
name: ADC Test temperature
|
||||
@@ -3,7 +3,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
airthings_ble:
|
||||
@@ -0,0 +1,6 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
airthings_ble:
|
||||
ble_hub_id: ble_tracker_hub
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ble: !include ../../test_build_components/common/ble/esp32-idf.yaml
|
||||
airthings_ble: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
airthings_ble: !include common-ln.yaml
|
||||
@@ -0,0 +1,8 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
airthings_ble:
|
||||
ble_hub_id: ble_tracker_hub
|
||||
@@ -0,0 +1,21 @@
|
||||
# `platform: animation` entry exercising the shared `defaults:`/`files:` expansion.
|
||||
display:
|
||||
- platform: sdl
|
||||
id: animation_display
|
||||
auto_clear_enabled: false
|
||||
dimensions:
|
||||
width: 480
|
||||
height: 480
|
||||
|
||||
image:
|
||||
- platform: animation
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
resize: 50x50
|
||||
files:
|
||||
- id: platform_defaults_animation
|
||||
file: $component_dir/anim.gif
|
||||
- id: platform_defaults_animation_rgb
|
||||
file: $component_dir/anim.apng
|
||||
type: rgb
|
||||
@@ -9,6 +9,14 @@ esphome:
|
||||
event: esphome.button_pressed
|
||||
data:
|
||||
message: Button was pressed
|
||||
- homeassistant.event:
|
||||
event: esphome.button_pressed_with_variables
|
||||
data_template:
|
||||
message: Button {{ button_name }} ({{ button_index }}) was pressed from {{ button_source }}
|
||||
variables:
|
||||
button_name: !lambda 'return std::string("test_button");'
|
||||
button_index: !lambda 'return 1;'
|
||||
button_source: static_value
|
||||
- homeassistant.action:
|
||||
action: notify.html5
|
||||
data:
|
||||
@@ -53,8 +61,12 @@ api:
|
||||
reboot_timeout: 0min
|
||||
actions:
|
||||
- action: hello_world
|
||||
description: Log a greeting
|
||||
variables:
|
||||
name: string
|
||||
name:
|
||||
type: string
|
||||
description: Name to greet
|
||||
example: World
|
||||
then:
|
||||
- logger.log:
|
||||
format: Hello World %s!
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<<: !include common-base.yaml
|
||||
packages:
|
||||
base: !include common-base.yaml
|
||||
|
||||
api:
|
||||
encryption:
|
||||
|
||||
@@ -54,7 +54,7 @@ static void verify_mac(uint64_t mac, size_t expected_bytes) {
|
||||
size_t ref_len = reference_encode(mac, ref_buf);
|
||||
|
||||
APIBuffer api_buf;
|
||||
api_buf.resize(16);
|
||||
ASSERT_TRUE(api_buf.resize(16));
|
||||
uint8_t *pos = api_buf.data();
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
uint8_t *proto_debug_end_ = api_buf.data() + api_buf.size();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
sensor:
|
||||
- platform: atc_mithermometer
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
temperature:
|
||||
name: ATC Temperature
|
||||
humidity:
|
||||
name: ATC Humidity
|
||||
@@ -1,7 +1,10 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: atc_mithermometer
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
temperature:
|
||||
name: ATC Temperature
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
atc_mithermometer: !include common-ln.yaml
|
||||
@@ -0,0 +1,10 @@
|
||||
# The esp32_ble_id -> ble_hub_id alias (removal 2027.2.0) still validates.
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
- platform: atc_mithermometer
|
||||
esp32_ble_id: ble_tracker_hub
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
temperature:
|
||||
name: ATC Legacy Key Temperature
|
||||
@@ -0,0 +1,17 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_hub
|
||||
|
||||
sensor:
|
||||
- platform: atc_mithermometer
|
||||
ble_hub_id: ble_hub
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
temperature:
|
||||
name: BK ATC Temperature
|
||||
# No ble_hub_id: exercises the generated binding real configs use.
|
||||
- platform: atc_mithermometer
|
||||
mac_address: A4:C1:38:4E:16:79
|
||||
temperature:
|
||||
name: BK ATC Implicit Temperature
|
||||
@@ -4,4 +4,5 @@ media_source:
|
||||
- platform: audio_http
|
||||
id: audio_http_source
|
||||
buffer_size: 100000
|
||||
persistent_ring_buffer: true
|
||||
task_stack_in_psram: true
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
sensor:
|
||||
- platform: b_parasite
|
||||
mac_address: F0:CA:F0:CA:01:01
|
||||
humidity:
|
||||
name: b-parasite Air Humidity
|
||||
temperature:
|
||||
name: b-parasite Air Temperature
|
||||
@@ -1,7 +1,10 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: b_parasite
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: F0:CA:F0:CA:01:01
|
||||
humidity:
|
||||
name: b-parasite Air Humidity
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
b_parasite: !include common-ln.yaml
|
||||
@@ -0,0 +1,26 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: b_parasite
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: F0:CA:F0:CA:01:01
|
||||
humidity:
|
||||
name: b-parasite Air Humidity
|
||||
temperature:
|
||||
name: b-parasite Air Temperature
|
||||
moisture:
|
||||
name: b-parasite Soil Moisture
|
||||
battery_voltage:
|
||||
name: b-parasite Battery Voltage
|
||||
illuminance:
|
||||
name: b-parasite Illuminance
|
||||
# No ble_hub_id: exercises the generated binding real configs use.
|
||||
- platform: b_parasite
|
||||
mac_address: F0:CA:F0:CA:01:02
|
||||
temperature:
|
||||
name: BK b-parasite Implicit Temperature
|
||||
@@ -1,6 +1,6 @@
|
||||
light:
|
||||
- platform: beken_spi_led_strip
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
pin: P16
|
||||
num_leds: 30
|
||||
chipset: ws2812
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0.
|
||||
# Config-only, and only one strip because P16 is the sole supported pin.
|
||||
light:
|
||||
- platform: beken_spi_led_strip
|
||||
name: Legacy RGBW
|
||||
pin: P16
|
||||
num_leds: 30
|
||||
chipset: sk6812
|
||||
rgb_order: GRB
|
||||
is_rgbw: true # -> GRBW
|
||||
@@ -136,3 +136,19 @@ binary_sensor:
|
||||
invalid_cooldown: 2s
|
||||
then:
|
||||
- logger.log: "Click with custom cooldown"
|
||||
|
||||
# Test on_click and on_double_click (compiles match_interval via
|
||||
# USE_BINARY_SENSOR_CLICK_TRIGGER)
|
||||
- platform: template
|
||||
id: click_triggers
|
||||
name: "Click Triggers"
|
||||
on_click:
|
||||
min_length: 50ms
|
||||
max_length: 350ms
|
||||
then:
|
||||
- logger.log: "Clicked"
|
||||
on_double_click:
|
||||
min_length: 50ms
|
||||
max_length: 350ms
|
||||
then:
|
||||
- logger.log: "Double clicked"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Passive scanning variant: the package merge keeps the shared parameters from
|
||||
# common.yaml and overrides only the mode.
|
||||
packages:
|
||||
bk72xx_ble_tracker: !include common.yaml
|
||||
|
||||
bk72xx_ble_tracker:
|
||||
scan_parameters:
|
||||
active: false
|
||||
@@ -6,7 +6,11 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# resolve_irk() is compiled only when a sensor configures irk:
|
||||
# (request_irk_support() emits USE_BLE_DEVICE_IRK). The unit-test build has
|
||||
# no sensors, so emit the define here to put the real IRK path under test.
|
||||
# Likewise the scan-response merger (emitted by the split-report trackers)
|
||||
# and the listener vector it dispatches into (codegen-sized by consumers).
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_BLE_DEVICE_IRK")
|
||||
cg.add_define("USE_BLE_SCAN_RESPONSE_MERGER")
|
||||
cg.add_define("ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT", 4)
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
|
||||
@@ -19,6 +19,33 @@ const uint8_t TAG[4] = {0x48, 0x4d, 0xaa, 0x56};
|
||||
const uint8_t PLAINTEXT[7] = {0x02, 0x01, 0x64, 0x03, 0x10, 0x8a, 0x01};
|
||||
} // namespace
|
||||
|
||||
// Xiaomi's parameters differ from BTHome's: a 12-byte nonce and a 1-byte AAD
|
||||
// (0x11). Both the AAD block and the l = 3 length encoding are only reachable
|
||||
// through this shape, so they need their own vector. Generated the same way.
|
||||
namespace {
|
||||
const uint8_t NONCE_XIAOMI[12] = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b};
|
||||
const uint8_t AAD_XIAOMI[1] = {0x11};
|
||||
const uint8_t CIPHERTEXT_XIAOMI[5] = {0xc3, 0x7e, 0x0a, 0x1d, 0x23};
|
||||
const uint8_t TAG_XIAOMI[4] = {0x98, 0x79, 0x87, 0xc6};
|
||||
const uint8_t PLAINTEXT_XIAOMI[5] = {0x04, 0x10, 0x02, 0xd4, 0x00};
|
||||
} // namespace
|
||||
|
||||
TEST(BleAesCcm, DecryptsXiaomiShapedVector) {
|
||||
uint8_t out[sizeof(PLAINTEXT_XIAOMI)] = {};
|
||||
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE_XIAOMI, sizeof(NONCE_XIAOMI), AAD_XIAOMI, sizeof(AAD_XIAOMI),
|
||||
CIPHERTEXT_XIAOMI, sizeof(CIPHERTEXT_XIAOMI), out, TAG_XIAOMI, sizeof(TAG_XIAOMI)));
|
||||
EXPECT_EQ(0, memcmp(out, PLAINTEXT_XIAOMI, sizeof(PLAINTEXT_XIAOMI)));
|
||||
}
|
||||
|
||||
TEST(BleAesCcm, RejectsWrongAssociatedData) {
|
||||
uint8_t bad_aad[sizeof(AAD_XIAOMI)];
|
||||
memcpy(bad_aad, AAD_XIAOMI, sizeof(AAD_XIAOMI));
|
||||
bad_aad[0] ^= 0x01;
|
||||
uint8_t out[sizeof(PLAINTEXT_XIAOMI)] = {};
|
||||
EXPECT_FALSE(aes_ccm_auth_decrypt(KEY, NONCE_XIAOMI, sizeof(NONCE_XIAOMI), bad_aad, sizeof(bad_aad),
|
||||
CIPHERTEXT_XIAOMI, sizeof(CIPHERTEXT_XIAOMI), out, TAG_XIAOMI, sizeof(TAG_XIAOMI)));
|
||||
}
|
||||
|
||||
TEST(BleAesCcm, DecryptsAndAuthenticatesKnownVector) {
|
||||
uint8_t out[sizeof(PLAINTEXT)] = {};
|
||||
EXPECT_TRUE(aes_ccm_auth_decrypt(KEY, NONCE, sizeof(NONCE), nullptr, 0, CIPHERTEXT, sizeof(CIPHERTEXT), out, TAG,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// The GATT client contract compiles in no real build until a hub backend is
|
||||
// configured; this TU pins it on the host so the header cannot rot unseen.
|
||||
// The contract is a concept (BLEGattConnection is a per-platform alias), so
|
||||
// the minimal backend here proves the concept stays satisfiable and routes
|
||||
// events through the GattClientListener interface the way a real backend does.
|
||||
#define USE_BLE_GATT_CLIENT
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_gatt_client.h"
|
||||
@@ -8,44 +11,55 @@
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
class RecordingListener : public GattClientEventListener {
|
||||
// Overrides only what it records; the interface's defaults cover the rest.
|
||||
class RecordingListener : public GattClientListener {
|
||||
public:
|
||||
void on_connection_state(bool connected, uint16_t mtu, int error) override { this->connected_ = connected; }
|
||||
void on_service_discovery_done(int error) override { this->discovery_error_ = error; }
|
||||
void on_read_result(uint16_t handle, const uint8_t *data, uint16_t len, int error) override {}
|
||||
void on_write_result(uint16_t handle, int error) override {}
|
||||
void on_notify_state(uint16_t handle, bool enabled, int error) override {}
|
||||
void on_notify_data(uint16_t handle, const uint8_t *data, uint16_t len) override {}
|
||||
void on_write_result(uint16_t handle, int error) override { this->write_handle_ = handle; }
|
||||
|
||||
bool connected_{false};
|
||||
int discovery_error_{0};
|
||||
uint16_t write_handle_{0};
|
||||
};
|
||||
|
||||
class MinimalConnection : public BLEGattConnection {
|
||||
class MinimalConnection {
|
||||
public:
|
||||
int connect(uint64_t address, uint8_t addr_type) override {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_connection_state(true, 517, 0);
|
||||
void set_listener(GattClientListener *listener) { this->listener_ = listener; }
|
||||
|
||||
int connect(uint64_t address, uint8_t addr_type) {
|
||||
this->listener_->on_connection_state(true, 517, 0);
|
||||
return 0;
|
||||
}
|
||||
int disconnect() override { return 0; }
|
||||
int discover_services() override {
|
||||
if (this->listener_ != nullptr)
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
bool cancel_gatt_disconnect() { return false; }
|
||||
int gatt_disconnect() { return 0; }
|
||||
int discover_services() {
|
||||
this->listener_->on_service_discovery_done(0);
|
||||
return 0;
|
||||
}
|
||||
int read_characteristic(uint16_t handle) override { return GATT_ERR_NOT_CONNECTED; }
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) override { return 0; }
|
||||
int read_descriptor(uint16_t handle) override { return 0; }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) override { return 0; }
|
||||
int notify_characteristic(uint16_t handle, bool enable) override { return 0; }
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
|
||||
uint16_t timeout) override {
|
||||
int read_characteristic(uint16_t handle) { return GATT_ERR_NOT_CONNECTED; }
|
||||
int write_characteristic(uint16_t handle, const uint8_t *data, uint16_t len, bool response) {
|
||||
this->listener_->on_write_result(handle, 0);
|
||||
return 0;
|
||||
}
|
||||
GattServiceTable get_service_table() override { return {}; }
|
||||
void release_services() override {}
|
||||
int read_descriptor(uint16_t handle) { return 0; }
|
||||
int write_descriptor(uint16_t handle, const uint8_t *data, uint16_t len) { return 0; }
|
||||
int notify_characteristic(uint16_t handle, bool enable) { return 0; }
|
||||
int pair() { return GATT_ERR_NOT_CONNECTED; }
|
||||
int update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) {
|
||||
return 0;
|
||||
}
|
||||
GattServiceTable get_service_table() { return {}; }
|
||||
void release_services() {}
|
||||
void set_connection_type(ConnectionType ct) {}
|
||||
|
||||
protected:
|
||||
GattClientListener *listener_{nullptr};
|
||||
};
|
||||
|
||||
static_assert(BLEGattConnectionContract<MinimalConnection>,
|
||||
"a minimal backend must satisfy the contract the alias asserts");
|
||||
|
||||
TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
MinimalConnection connection;
|
||||
RecordingListener listener;
|
||||
@@ -55,6 +69,8 @@ TEST(BleGattClientContract, MinimalImplementerCompilesAndRoutesEvents) {
|
||||
EXPECT_EQ(connection.discover_services(), 0);
|
||||
EXPECT_EQ(listener.discovery_error_, 0);
|
||||
EXPECT_EQ(connection.read_characteristic(1), GATT_ERR_NOT_CONNECTED);
|
||||
EXPECT_EQ(connection.write_characteristic(7, nullptr, 0, true), 0);
|
||||
EXPECT_EQ(listener.write_handle_, 7);
|
||||
|
||||
// A default table is empty and safe to walk.
|
||||
GattServiceTable table = connection.get_service_table();
|
||||
|
||||
@@ -13,17 +13,12 @@ namespace esphome::ble_device_base::testing {
|
||||
//
|
||||
// The in-tree emit site (BK72xxBLETracker::on_scan_report) compiles against
|
||||
// the Beken SDK and cannot run host-side, so the guard-and-fire semantics are
|
||||
// pinned here through a minimal host BLEHub implementation instead.
|
||||
// pinned here through a minimal host hub carrying only the slot under test.
|
||||
namespace {
|
||||
|
||||
class FakeHub : public BLEHub {
|
||||
class FakeHub {
|
||||
public:
|
||||
void register_listener(ESPBTDeviceListener *listener) override {}
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) override { this->callback_ = callback; }
|
||||
HubCapabilities get_capabilities() const override { return {false, false, false}; }
|
||||
void get_adapter_mac(uint8_t out[6]) override {}
|
||||
bool scan_running() override { return false; }
|
||||
bool scan_active() override { return false; }
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) { this->callback_ = callback; }
|
||||
|
||||
/// The emit path every tracker implements: fire only when a subscriber is set.
|
||||
void emit(const RawAdvertisement &adv) {
|
||||
@@ -46,13 +41,13 @@ struct CapturingSubscriber {
|
||||
}
|
||||
};
|
||||
|
||||
// Device AA:BB:CC:DD:EE:FF — controller order delivers FF first.
|
||||
const uint8_t MAC_LSB_FIRST[6] = {0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa};
|
||||
// Device AA:BB:CC:DD:EE:FF, packed the way the API speaks it.
|
||||
constexpr uint64_t TEST_ADDRESS = 0xAABBCCDDEEFFULL;
|
||||
const uint8_t ADV_DATA[4] = {0x02, 0x01, 0x06, 0x00};
|
||||
|
||||
RawAdvertisement make_test_adv() {
|
||||
return RawAdvertisement{
|
||||
.mac = MAC_LSB_FIRST, .data = ADV_DATA, .data_len = sizeof(ADV_DATA), .rssi = -63, .addr_type = 1};
|
||||
.address = TEST_ADDRESS, .data = ADV_DATA, .data_len = sizeof(ADV_DATA), .rssi = -63, .addr_type = 1};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -70,7 +65,7 @@ TEST(RawAdvertisementCallback, SubscriberSeesFieldsUnchanged) {
|
||||
hub.emit(make_test_adv());
|
||||
|
||||
ASSERT_EQ(subscriber.calls, 1);
|
||||
EXPECT_EQ(subscriber.last.mac, MAC_LSB_FIRST);
|
||||
EXPECT_EQ(subscriber.last.address, TEST_ADDRESS);
|
||||
EXPECT_EQ(subscriber.last.data, ADV_DATA);
|
||||
EXPECT_EQ(subscriber.last.data_len, sizeof(ADV_DATA));
|
||||
EXPECT_EQ(subscriber.last.rssi, -63);
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Pins the request_scan_mode() contract: the base default refuses (so hubs
|
||||
// without a mode switch — and out-of-tree trackers — keep building and
|
||||
// callers report the real state), while an overriding hub both honors the
|
||||
// request and applies it.
|
||||
namespace {
|
||||
|
||||
class DefaultHub : public BLEHub {
|
||||
public:
|
||||
void register_listener(ESPBTDeviceListener *listener) override {}
|
||||
void set_raw_advertisement_callback(RawAdvertisementCallback callback) override {}
|
||||
HubCapabilities get_capabilities() const override { return {false, false, false}; }
|
||||
void get_adapter_mac(uint8_t out[6]) override {}
|
||||
bool scan_running() override { return false; }
|
||||
// Backed by real state so "changes nothing" is observable: a base default
|
||||
// that silently mutated the hub would flip this and fail the assertion.
|
||||
bool scan_active() override { return this->active_; }
|
||||
|
||||
protected:
|
||||
bool active_{true};
|
||||
};
|
||||
|
||||
class SwitchingHub : public DefaultHub {
|
||||
public:
|
||||
HubCapabilities get_capabilities() const override { return {true, false, false, /* scan_mode_switch = */ true}; }
|
||||
bool request_scan_mode(bool active) override {
|
||||
this->active_ = active;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// The esp32 shape: the controller supports active scanning but the hub keeps
|
||||
// the refusing default (mode is driven through its own tracker API).
|
||||
class CapableRefusingHub : public DefaultHub {
|
||||
public:
|
||||
HubCapabilities get_capabilities() const override { return {true, false, false}; }
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(BLEHubScanModeRequest, DefaultRefusesAndChangesNothing) {
|
||||
DefaultHub hub;
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
EXPECT_FALSE(hub.request_scan_mode(false));
|
||||
// Refused, not applied-and-reported-false: the state is untouched.
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
EXPECT_FALSE(hub.request_scan_mode(true));
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
}
|
||||
|
||||
TEST(BLEHubScanModeRequest, OverrideHonorsAndApplies) {
|
||||
SwitchingHub hub;
|
||||
EXPECT_TRUE(hub.get_capabilities().scan_mode_switch);
|
||||
EXPECT_TRUE(hub.request_scan_mode(true));
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
EXPECT_TRUE(hub.request_scan_mode(false));
|
||||
EXPECT_FALSE(hub.scan_active());
|
||||
}
|
||||
|
||||
TEST(BLEHubScanModeRequest, CapabilityAndSwitchAreIndependent) {
|
||||
CapableRefusingHub hub;
|
||||
EXPECT_TRUE(hub.get_capabilities().active_scan);
|
||||
// The esp32 shape advertises no runtime switch, and the request refuses.
|
||||
EXPECT_FALSE(hub.get_capabilities().scan_mode_switch);
|
||||
EXPECT_FALSE(hub.request_scan_mode(false));
|
||||
EXPECT_TRUE(hub.scan_active());
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,193 @@
|
||||
// The host test build gets this from the manifest override; clang-tidy does not.
|
||||
#ifndef USE_BLE_SCAN_RESPONSE_MERGER
|
||||
#define USE_BLE_SCAN_RESPONSE_MERGER
|
||||
#endif
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/components/ble_device_base/scan_response_merger.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
namespace {
|
||||
|
||||
// Pins the merge policy three trackers share (ln882h, rp2, bk72xx): slot
|
||||
// bookkeeping, the same-device reuse path, the table-full fallback, the
|
||||
// 62-byte truncation, the advertisement-RSSI choice and the raw_only gate.
|
||||
// Delivery is observed through a real AdvDispatcher: the raw callback sees
|
||||
// every frame (including raw_only), a listener only the parsed ones.
|
||||
|
||||
struct DeliveredFrame {
|
||||
uint64_t address;
|
||||
std::vector<uint8_t> data;
|
||||
int8_t rssi;
|
||||
};
|
||||
|
||||
struct RawCapture {
|
||||
std::vector<DeliveredFrame> frames;
|
||||
|
||||
static void trampoline(void *self, const RawAdvertisement &adv) {
|
||||
auto *capture = static_cast<RawCapture *>(self);
|
||||
capture->frames.push_back({adv.address, std::vector<uint8_t>(adv.data, adv.data + adv.data_len), adv.rssi});
|
||||
}
|
||||
};
|
||||
|
||||
class CountingListener : public ESPBTDeviceListener {
|
||||
public:
|
||||
bool parse_device(const ESPBTDevice &device) override {
|
||||
this->parsed++;
|
||||
return true; // claimed: keeps the discovered log quiet
|
||||
}
|
||||
int parsed{0};
|
||||
};
|
||||
|
||||
class ScanResponseMergerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
this->dispatcher_.set_raw_advertisement_callback({&this->raw_, &RawCapture::trampoline});
|
||||
this->dispatcher_.register_listener(&this->listener_);
|
||||
this->merger_.bind(&this->dispatcher_, &this->scan_continuous_, "test");
|
||||
}
|
||||
|
||||
void stash_(const uint8_t (&mac)[6], int8_t rssi, uint8_t data_len, uint8_t fill, uint32_t now = 0) {
|
||||
std::vector<uint8_t> data(data_len, fill);
|
||||
this->merger_.stash_adv(mac, rssi, 0, data.data(), data_len, now);
|
||||
}
|
||||
|
||||
void scan_rsp_(const uint8_t (&mac)[6], int8_t rssi, uint8_t data_len, uint8_t fill) {
|
||||
std::vector<uint8_t> data(data_len, fill);
|
||||
this->merger_.submit_scan_rsp(mac, rssi, 0, data.data(), data_len);
|
||||
}
|
||||
|
||||
ScanResponseMerger merger_;
|
||||
AdvDispatcher dispatcher_;
|
||||
RawCapture raw_;
|
||||
CountingListener listener_;
|
||||
bool scan_continuous_{true};
|
||||
};
|
||||
|
||||
constexpr uint8_t MAC_A[6] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
|
||||
constexpr uint8_t MAC_B[6] = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16};
|
||||
|
||||
TEST_F(ScanResponseMergerTest, MatchedPairDeliversOneMergedFrameWithAdvRssi) {
|
||||
this->stash_(MAC_A, -40, 20, 0xAA);
|
||||
EXPECT_TRUE(this->raw_.frames.empty()); // held, not delivered
|
||||
|
||||
this->scan_rsp_(MAC_A, -70, 10, 0xBB);
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u);
|
||||
const auto &frame = this->raw_.frames[0];
|
||||
ASSERT_EQ(frame.data.size(), 30u); // adv + response as ONE frame
|
||||
EXPECT_EQ(frame.data[0], 0xAA);
|
||||
EXPECT_EQ(frame.data[19], 0xAA);
|
||||
EXPECT_EQ(frame.data[20], 0xBB);
|
||||
// The advertisement's RSSI, never the scan response's.
|
||||
EXPECT_EQ(frame.rssi, -40);
|
||||
EXPECT_EQ(this->listener_.parsed, 1);
|
||||
EXPECT_TRUE(this->merger_.empty());
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, ReAdvertisementDeliversHeldFrameAndReusesSlot) {
|
||||
this->stash_(MAC_A, -40, 20, 0xAA);
|
||||
this->stash_(MAC_A, -45, 22, 0xCC); // same device again: first frame is delivered
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u);
|
||||
EXPECT_EQ(this->raw_.frames[0].data.size(), 20u);
|
||||
EXPECT_EQ(this->raw_.frames[0].rssi, -40);
|
||||
EXPECT_FALSE(this->merger_.empty()); // the second advertisement now holds the slot
|
||||
|
||||
this->scan_rsp_(MAC_A, -70, 5, 0xBB);
|
||||
ASSERT_EQ(this->raw_.frames.size(), 2u);
|
||||
EXPECT_EQ(this->raw_.frames[1].data.size(), 27u); // 22 + 5, merged from the reused slot
|
||||
EXPECT_EQ(this->raw_.frames[1].rssi, -45);
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, FullTableDegradesToUnmergedDelivery) {
|
||||
uint8_t mac[6] = {0x20, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
mac[5] = i;
|
||||
this->stash_(mac, -50, 10, i);
|
||||
}
|
||||
EXPECT_TRUE(this->raw_.frames.empty()); // 8 slots, all held
|
||||
|
||||
mac[5] = 8;
|
||||
this->stash_(mac, -50, 10, 8); // 9th device: no slot left
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u); // delivered immediately, unmerged
|
||||
EXPECT_EQ(this->raw_.frames[0].data.size(), 10u);
|
||||
|
||||
this->merger_.flush(); // the 8 held frames are all still intact
|
||||
EXPECT_EQ(this->raw_.frames.size(), 9u);
|
||||
EXPECT_TRUE(this->merger_.empty());
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, MergeTruncatesAtBufferCapacity) {
|
||||
this->stash_(MAC_A, -40, 31, 0xAA);
|
||||
this->scan_rsp_(MAC_A, -70, 40, 0xBB); // only 31 bytes of room remain
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u);
|
||||
EXPECT_EQ(this->raw_.frames[0].data.size(), 62u);
|
||||
EXPECT_EQ(this->raw_.frames[0].data[31], 0xBB);
|
||||
EXPECT_EQ(this->raw_.frames[0].data[61], 0xBB);
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, UnmatchedScanResponseIsRawOnly) {
|
||||
this->scan_rsp_(MAC_B, -60, 12, 0xDD);
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u); // still forwarded on the raw path
|
||||
EXPECT_EQ(this->raw_.frames[0].rssi, -60);
|
||||
EXPECT_EQ(this->listener_.parsed, 0); // but never parsed for listeners
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, AddrTypeIsPartOfTheMatchKey) {
|
||||
std::vector<uint8_t> adv(20, 0xAA);
|
||||
this->merger_.stash_adv(MAC_A, -40, /*addr_type=*/0, adv.data(), adv.size(), 0);
|
||||
std::vector<uint8_t> rsp(10, 0xBB);
|
||||
this->merger_.submit_scan_rsp(MAC_A, -70, /*addr_type=*/1, rsp.data(), rsp.size());
|
||||
// Same MAC, different addr_type: no merge — the response goes out raw_only.
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u);
|
||||
EXPECT_EQ(this->raw_.frames[0].data.size(), 10u);
|
||||
EXPECT_EQ(this->listener_.parsed, 0);
|
||||
EXPECT_FALSE(this->merger_.empty()); // the advertisement is still held
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, SweepDeliversOnlyPastTheTimeout) {
|
||||
this->stash_(MAC_A, -40, 20, 0xAA, /*now=*/1000);
|
||||
this->merger_.sweep(1300); // exactly 300 ms: not yet past the timeout
|
||||
EXPECT_TRUE(this->raw_.frames.empty());
|
||||
this->merger_.sweep(1301);
|
||||
ASSERT_EQ(this->raw_.frames.size(), 1u);
|
||||
EXPECT_EQ(this->raw_.frames[0].rssi, -40);
|
||||
EXPECT_EQ(this->listener_.parsed, 1); // timeout delivery is a full parse, not raw_only
|
||||
EXPECT_TRUE(this->merger_.empty());
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, FlushDeliversEverythingImmediately) {
|
||||
this->stash_(MAC_A, -40, 20, 0xAA, /*now=*/1000);
|
||||
this->stash_(MAC_B, -50, 15, 0xBB, /*now=*/1000);
|
||||
this->merger_.flush();
|
||||
EXPECT_EQ(this->raw_.frames.size(), 2u);
|
||||
EXPECT_EQ(this->listener_.parsed, 2);
|
||||
EXPECT_TRUE(this->merger_.empty());
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, UnboundMergerDropsInsteadOfCrashing) {
|
||||
ScanResponseMerger unbound;
|
||||
std::vector<uint8_t> data(20, 0xAA);
|
||||
unbound.stash_adv(MAC_A, -40, 0, data.data(), data.size(), 0);
|
||||
unbound.submit_scan_rsp(MAC_A, -70, 0, data.data(), data.size());
|
||||
unbound.sweep(1000);
|
||||
unbound.flush(); // no null jump anywhere
|
||||
EXPECT_TRUE(unbound.empty());
|
||||
}
|
||||
|
||||
TEST_F(ScanResponseMergerTest, PartialBindIsTreatedAsUnbound) {
|
||||
ScanResponseMerger partial;
|
||||
partial.bind(&this->dispatcher_, nullptr, "test");
|
||||
std::vector<uint8_t> data(20, 0xAA);
|
||||
partial.submit_scan_rsp(MAC_A, -70, 0, data.data(), data.size());
|
||||
partial.stash_adv(MAC_A, -40, 0, data.data(), data.size(), 0);
|
||||
partial.flush(); // dropped, not dispatched through half a binding
|
||||
EXPECT_TRUE(this->raw_.frames.empty());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/ble_device_base/ble_hub.h"
|
||||
|
||||
namespace esphome::ble_device_base::testing {
|
||||
|
||||
// Pins the ScannerStateCallback slot semantics, mirroring test_raw_callback:
|
||||
// a default-constructed slot is "no subscriber", a set slot delivers the
|
||||
// state, and a new registration replaces the old.
|
||||
namespace {
|
||||
|
||||
struct CapturingSubscriber {
|
||||
ScannerState last{ScannerState::IDLE};
|
||||
int calls{0};
|
||||
|
||||
static void trampoline(void *self, ScannerState state) {
|
||||
auto *sub = static_cast<CapturingSubscriber *>(self);
|
||||
sub->last = state;
|
||||
sub->calls++;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(ScannerStateCallback, DefaultConstructedSlotIsNotSet) {
|
||||
const ScannerStateCallback callback{};
|
||||
EXPECT_FALSE(callback.is_set());
|
||||
}
|
||||
|
||||
TEST(ScannerStateCallback, SubscriberSeesState) {
|
||||
CapturingSubscriber subscriber;
|
||||
ScannerStateCallback callback{&subscriber, CapturingSubscriber::trampoline};
|
||||
ASSERT_TRUE(callback.is_set());
|
||||
callback.invoke(ScannerState::RUNNING);
|
||||
EXPECT_EQ(subscriber.calls, 1);
|
||||
EXPECT_EQ(subscriber.last, ScannerState::RUNNING);
|
||||
}
|
||||
|
||||
TEST(ScannerStateCallback, NewSubscriberReplacesOld) {
|
||||
CapturingSubscriber first;
|
||||
CapturingSubscriber second;
|
||||
ScannerStateCallback callback{&first, CapturingSubscriber::trampoline};
|
||||
callback = {&second, CapturingSubscriber::trampoline};
|
||||
callback.invoke(ScannerState::STOPPED);
|
||||
EXPECT_EQ(first.calls, 0);
|
||||
EXPECT_EQ(second.calls, 1);
|
||||
EXPECT_EQ(second.last, ScannerState::STOPPED);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_device_base::testing
|
||||
@@ -0,0 +1,4 @@
|
||||
binary_sensor:
|
||||
- platform: ble_presence
|
||||
mac_address: 11:22:33:44:55:66
|
||||
name: BLE Test Presence
|
||||
@@ -1,7 +1,10 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
binary_sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: ble_presence
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
name: ESP32 BLE Tracker Google Home Mini
|
||||
- platform: ble_presence
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
ble_presence: !include common-ln.yaml
|
||||
@@ -0,0 +1,14 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_hub
|
||||
|
||||
binary_sensor:
|
||||
- platform: ble_presence
|
||||
ble_hub_id: ble_hub
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
name: BK BLE Presence
|
||||
- platform: ble_presence
|
||||
irk: 1234567890abcdef1234567890abcdef
|
||||
name: BK BLE Presence IRK
|
||||
@@ -0,0 +1,5 @@
|
||||
sensor:
|
||||
- platform: ble_rssi
|
||||
# irk: is the only thing that emits USE_BLE_DEVICE_IRK off ESP32
|
||||
irk: 1234567890abcdef1234567890abcdef
|
||||
name: BLE Test RSSI
|
||||
@@ -1,7 +1,10 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: ble_rssi
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
name: BLE Google Home Mini RSSI value
|
||||
- platform: ble_rssi
|
||||
@@ -14,7 +17,9 @@ sensor:
|
||||
service_uuid: 11223344-5566-7788-99aa-bbccddeeff00
|
||||
name: BLE Test Service 128
|
||||
- platform: ble_rssi
|
||||
service_uuid: 11223344-5566-7788-99aa-bbccddeeff00
|
||||
ibeacon_uuid: 11223344-5566-7788-99aa-bbccddeeff00
|
||||
ibeacon_major: 100
|
||||
ibeacon_minor: 1
|
||||
name: BLE Test iBeacon UUID
|
||||
- platform: ble_rssi
|
||||
irk: 1234567890abcdef1234567890abcdef
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
ble_rssi: !include common-ln.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
# Config-only: pins the esp32_ble_id: -> ble_hub_id: deprecation alias — the
|
||||
# legacy key must keep validating (with a rename warning) until its removal
|
||||
# release (2027.2.0).
|
||||
esp32_ble_tracker:
|
||||
id: legacy_tracker
|
||||
|
||||
sensor:
|
||||
- platform: ble_rssi
|
||||
esp32_ble_id: legacy_tracker
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
name: Legacy Key RSSI
|
||||
@@ -0,0 +1,14 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_hub
|
||||
|
||||
sensor:
|
||||
- platform: ble_rssi
|
||||
ble_hub_id: ble_hub
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
name: BK BLE RSSI
|
||||
- platform: ble_rssi
|
||||
irk: 1234567890abcdef1234567890abcdef
|
||||
name: BK BLE RSSI IRK
|
||||
@@ -0,0 +1,3 @@
|
||||
text_sensor:
|
||||
- platform: ble_scanner
|
||||
name: BLE Test Scanner
|
||||
@@ -1,5 +1,8 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
text_sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: ble_scanner
|
||||
ble_hub_id: ble_tracker_hub
|
||||
name: Scanner
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
ble_scanner: !include common-ln.yaml
|
||||
@@ -0,0 +1,13 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_hub
|
||||
|
||||
text_sensor:
|
||||
- platform: ble_scanner
|
||||
ble_hub_id: ble_hub
|
||||
name: BK Scanner
|
||||
# No ble_hub_id: exercises the generated binding _require_hub guards.
|
||||
- platform: ble_scanner
|
||||
name: BK Scanner Implicit
|
||||
@@ -0,0 +1,22 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# close_service_batch compiles only under USE_BLUETOOTH_PROXY_CONNECTIONS;
|
||||
# emit the backend define so the host build exercises it.
|
||||
async def to_code_testing(config):
|
||||
# These defines are global to the merged host test binary; safe
|
||||
# because no co-compiled test observes them.
|
||||
cg.add_define("USE_BLE_GATT_CLIENT")
|
||||
cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND")
|
||||
cg.add_define("USE_BLUETOOTH_PROXY")
|
||||
# Gates the connection half of the API surface, which is what
|
||||
# close_service_batch and the GATT response types live behind.
|
||||
cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS")
|
||||
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1)
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
# The batcher sizes api protobuf messages.
|
||||
manifest.dependencies = manifest.dependencies + ["api"]
|
||||
@@ -0,0 +1,8 @@
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
ota:
|
||||
- platform: esphome
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
|
||||
namespace esphome::bluetooth_connection {
|
||||
|
||||
// The three cursor behaviors: a fitting service advances and continues, an
|
||||
// overflowing batch with >1 service pops and retries it, and a single
|
||||
// oversized service is force-advanced so the stream cannot wedge.
|
||||
|
||||
static void add_service(api::BluetoothGATTGetServicesResponse &resp, uint16_t characteristics) {
|
||||
resp.services.emplace_back();
|
||||
auto &svc = resp.services.back();
|
||||
svc.handle = resp.services.size();
|
||||
svc.uuid = {0x1234567890ABCDEFULL, 0xFEDCBA0987654321ULL};
|
||||
svc.characteristics.init(characteristics);
|
||||
for (uint16_t i = 0; i < characteristics; i++) {
|
||||
auto &chr = svc.characteristics.emplace_back();
|
||||
chr.handle = 100 + i;
|
||||
chr.properties = 0x12;
|
||||
chr.uuid = {0x1234567890ABCDEFULL, 0xFEDCBA0987654321ULL};
|
||||
}
|
||||
}
|
||||
|
||||
TEST(CloseServiceBatch, FittingServiceAdvancesAndContinues) {
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
add_service(resp, 1);
|
||||
size_t current_size = 0;
|
||||
int16_t cursor = 0;
|
||||
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::CONTINUE);
|
||||
EXPECT_EQ(cursor, 1);
|
||||
EXPECT_GT(current_size, 0u);
|
||||
}
|
||||
|
||||
TEST(CloseServiceBatch, OverflowPopsAndRetriesWithoutAdvancing) {
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
add_service(resp, 1);
|
||||
add_service(resp, 1);
|
||||
size_t current_size = MAX_PACKET_SIZE - 10; // any service is bigger than 10 bytes
|
||||
int16_t cursor = 5;
|
||||
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::SEND);
|
||||
EXPECT_EQ(resp.services.size(), 1u); // popped for the next batch
|
||||
EXPECT_EQ(cursor, 5); // not advanced: retried next batch
|
||||
}
|
||||
|
||||
TEST(CloseServiceBatch, SingleOversizedServiceForceAdvances) {
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
add_service(resp, 60); // ~30 bytes per characteristic, far past the budget
|
||||
ASSERT_GT(resp.services.back().calculate_size(), MAX_PACKET_SIZE);
|
||||
size_t current_size = 0;
|
||||
int16_t cursor = 7;
|
||||
EXPECT_EQ(close_service_batch(resp, current_size, cursor, 0, "AA:BB"), BatchClose::SEND);
|
||||
EXPECT_EQ(cursor, 8); // advanced despite not fitting, so the stream moves on
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection
|
||||
@@ -0,0 +1,49 @@
|
||||
// Pins the shared UUID wire packing and the size-estimate budget the service
|
||||
// streamers rely on, in both efficient and legacy client modes.
|
||||
#include "esphome/components/bluetooth_connection/bluetooth_connection.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
namespace esphome::bluetooth_connection::testing {
|
||||
|
||||
using ble_device_base::ESPBTUUID;
|
||||
|
||||
TEST(GattUuidPacking, ShortUuidUsedWhenClientSupportsIt) {
|
||||
std::array<uint64_t, 2> uuid128{};
|
||||
uint32_t short_uuid = 0;
|
||||
fill_gatt_uuid(uuid128, short_uuid, ESPBTUUID::from_uint16(0x180F), true);
|
||||
EXPECT_EQ(short_uuid, 0x180Fu);
|
||||
EXPECT_EQ(uuid128[0], 0u);
|
||||
EXPECT_EQ(uuid128[1], 0u);
|
||||
}
|
||||
|
||||
TEST(GattUuidPacking, LegacyClientGetsBaseUuidExpansion) {
|
||||
// 0000180F-0000-1000-8000-00805F9B34FB
|
||||
std::array<uint64_t, 2> uuid128{};
|
||||
uint32_t short_uuid = 0;
|
||||
fill_gatt_uuid(uuid128, short_uuid, ESPBTUUID::from_uint16(0x180F), false);
|
||||
EXPECT_EQ(short_uuid, 0u);
|
||||
EXPECT_EQ(uuid128[0], 0x0000180F00001000ULL);
|
||||
EXPECT_EQ(uuid128[1], 0x800000805F9B34FBULL);
|
||||
}
|
||||
|
||||
TEST(GattUuidPacking, FullUuidPassesThroughBigEndian) {
|
||||
// 12345678-90AB-CDEF-1122-334455667788, stored little-endian in ESPBTUUID.
|
||||
const uint8_t big_endian[16] = {0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF,
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
|
||||
std::array<uint64_t, 2> uuid128{};
|
||||
uint32_t short_uuid = 0;
|
||||
// Efficient mode must still use the 128-bit form for 128-bit UUIDs.
|
||||
fill_gatt_uuid(uuid128, short_uuid, ESPBTUUID::from_raw_reversed(big_endian), true);
|
||||
EXPECT_EQ(short_uuid, 0u);
|
||||
EXPECT_EQ(uuid128[0], 0x1234567890ABCDEFULL);
|
||||
EXPECT_EQ(uuid128[1], 0x1122334455667788ULL);
|
||||
}
|
||||
|
||||
TEST(GattUuidPacking, EstimateGrowsWithCharacteristicsAndMode) {
|
||||
// The estimate only gates batching; pin its shape, not exact bytes.
|
||||
EXPECT_LT(estimate_service_size(0, true), estimate_service_size(0, false));
|
||||
EXPECT_LT(estimate_service_size(1, false), estimate_service_size(2, false));
|
||||
}
|
||||
|
||||
} // namespace esphome::bluetooth_connection::testing
|
||||
@@ -0,0 +1,12 @@
|
||||
# Distinct shape from bluetooth_proxy's own rp2 fixtures: explicit slot count
|
||||
# on the platform whose backend lives in this component (validate-only, so it
|
||||
# never collides with grouped builds).
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
rp2_ble_tracker:
|
||||
|
||||
# Two slots: the one shape where the wrap pools are smaller than the cap.
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
connection_slots: 2
|
||||
@@ -0,0 +1,12 @@
|
||||
# Advertisement-only proxy on esp32 by explicit choice: no GATT backend is
|
||||
# compiled (USE_BLE_GATT_CLIENT unset), which pins the HAS_GATT gating and the
|
||||
# address-scoped maintenance path that a connections build never exercises.
|
||||
# Under batch grouping the active default build is what runs; the standalone
|
||||
# compile of this fixture is what exercises the passive gating.
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
esp32_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
active: false
|
||||
@@ -0,0 +1,11 @@
|
||||
# Advertisement-only proxy on rp2 by explicit choice. Variant tests compile
|
||||
# as their own builds when this component is tested individually; under CI
|
||||
# batch grouping the active default build is what runs, so this fixture's
|
||||
# guarantee is the individual run plus config validation.
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
rp2_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
active: false
|
||||
@@ -1,5 +1,7 @@
|
||||
# Advertisement-only proxy on the rp2 BLE hub — the one non-esp32 platform the
|
||||
# proxy admits today (active-scan-capable), and a target CI fully compiles.
|
||||
# Full proxy on the rp2 BLE hub: active defaults to true here (esp32 parity),
|
||||
# so this compiles the BTstack GATT client backend with the default three
|
||||
# connection slots, exercising the rp2040_ble/btstack_memory.cpp pool --wrap
|
||||
# link.
|
||||
# No explicit ble_hub_id: the generated binding resolves the single declared
|
||||
# hub, and an inline id here would collide with rp2_ble_tracker's own fixture
|
||||
# once CI merges both components into one grouped rp2040-ard build (grouped
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Pico 2 W build of the full proxy: links the rp2350 framework archive, so
|
||||
# the pool --wrap overrides and their per-architecture layout asserts are
|
||||
# exercised for this chip too (see test.rp2040-ard.yaml for the slot shape).
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
rp2_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
@@ -0,0 +1,11 @@
|
||||
# Advertisement-only proxy on the bk72xx BLE hub (active-scan-capable since the
|
||||
# tracker's packed-command start). Config-only: the CI base board generic-bk7252
|
||||
# is BLE 4.2 and cannot compile the BLE 5.x tracker. Same bare-hub arrangement
|
||||
# as test.ln882x-ard.yaml: no explicit ble_hub_id so a grouped build cannot
|
||||
# collide with bk72xx_ble_tracker's own fixture id.
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
|
||||
bk72xx_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
@@ -0,0 +1,9 @@
|
||||
sensor:
|
||||
- platform: bthome_mithermometer
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
# bindkey compiles ble_device_base::aes_ccm_auth_decrypt off Espressif
|
||||
bindkey: eef418daf699a0c188f3bfd17e4565d9
|
||||
temperature:
|
||||
name: BTHome Temperature
|
||||
humidity:
|
||||
name: BTHome Humidity
|
||||
@@ -1,7 +1,10 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
sensor:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
- platform: bthome_mithermometer
|
||||
ble_hub_id: ble_tracker_hub
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
bindkey: eef418daf699a0c188f3bfd17e4565d9
|
||||
temperature:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
bthome_mithermometer: !include common-ln.yaml
|
||||
@@ -0,0 +1,18 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_hub
|
||||
|
||||
sensor:
|
||||
- platform: bthome_mithermometer
|
||||
ble_hub_id: ble_hub
|
||||
mac_address: A4:C1:38:4E:16:78
|
||||
bindkey: eef418daf699a0c188f3bfd17e4565d9
|
||||
temperature:
|
||||
name: BK BTHome Temperature
|
||||
# No ble_hub_id: exercises the generated binding real configs use.
|
||||
- platform: bthome_mithermometer
|
||||
mac_address: A4:C1:38:4E:16:79
|
||||
temperature:
|
||||
name: BK BTHome Implicit Temperature
|
||||
@@ -0,0 +1,11 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# No host camera platform exists to emit USE_CAMERA; define it here so
|
||||
# the iterator CAMERA state compiles into the test binary.
|
||||
async def to_code_testing(config):
|
||||
cg.add_define("USE_CAMERA")
|
||||
|
||||
manifest.to_code = to_code_testing
|
||||
@@ -0,0 +1,79 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/component_iterator.h"
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
#include "esphome/components/camera/camera.h"
|
||||
|
||||
namespace esphome::testing {
|
||||
|
||||
class StubCamera : public camera::Camera {
|
||||
public:
|
||||
void add_listener(camera::CameraListener *listener) override {}
|
||||
camera::CameraImageReader *create_image_reader() override { return nullptr; }
|
||||
void request_image(camera::CameraRequester requester) override {}
|
||||
void start_stream(camera::CameraRequester requester) override {}
|
||||
void stop_stream(camera::CameraRequester requester) override {}
|
||||
};
|
||||
|
||||
// Iterator that accepts everything except the camera, which can refuse a
|
||||
// configurable number of times. The CAMERA state is a singleton path
|
||||
// distinct from process_platform_item_; this pins the same contract:
|
||||
// a refused camera is re-offered, never skipped.
|
||||
class CameraRefusingIterator : public ComponentIterator {
|
||||
public:
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
|
||||
bool on_##singular(type *obj) override { return true; }
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
|
||||
bool on_camera(camera::Camera *obj) override {
|
||||
this->camera_calls++;
|
||||
if (this->camera_refusals > 0) {
|
||||
this->camera_refusals--;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
int camera_calls{0};
|
||||
int camera_refusals{0};
|
||||
};
|
||||
|
||||
// Far above the fixed number of iterator states
|
||||
static constexpr size_t BIG_BUDGET = 1000;
|
||||
|
||||
class ComponentIteratorCameraTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// Constructing a Camera installs the process-wide singleton
|
||||
static StubCamera stub_camera;
|
||||
ASSERT_EQ(camera::Camera::instance(), &stub_camera);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ComponentIteratorCameraTest, RefusedCameraIsReofferedNotSkipped) {
|
||||
CameraRefusingIterator it;
|
||||
it.camera_refusals = 2;
|
||||
it.begin();
|
||||
// Runs until the camera refuses, which stops the pass
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.camera_calls, 1);
|
||||
EXPECT_FALSE(it.completed());
|
||||
// The camera is re-offered once per call, not skipped
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.camera_calls, 2);
|
||||
EXPECT_FALSE(it.completed());
|
||||
// Once accepted, the iteration completes
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
EXPECT_EQ(it.camera_calls, 3);
|
||||
}
|
||||
|
||||
} // namespace esphome::testing
|
||||
#endif // USE_CAMERA
|
||||
@@ -0,0 +1,73 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "esphome/components/climate/climate.h"
|
||||
|
||||
namespace esphome::climate::testing {
|
||||
|
||||
// Minimal concrete Climate that offers a fixed set of modes, so the restore path can be exercised
|
||||
// without any hardware or platform component.
|
||||
class TestClimate : public Climate {
|
||||
public:
|
||||
ClimateTraits traits() override {
|
||||
auto traits = ClimateTraits();
|
||||
traits.set_supported_modes({CLIMATE_MODE_OFF, CLIMATE_MODE_COOL});
|
||||
traits.set_supported_fan_modes({CLIMATE_FAN_LOW, CLIMATE_FAN_HIGH});
|
||||
return traits;
|
||||
}
|
||||
|
||||
protected:
|
||||
void control(const ClimateCall &call) override {}
|
||||
};
|
||||
|
||||
TEST(ClimateRestoreStateTest, RestoresASupportedMode) {
|
||||
TestClimate climate;
|
||||
// Value-initialized: several members (mode, swing_mode, the temperature union) have no default
|
||||
// member initializer, so leaving the {} off would read indeterminate values.
|
||||
ClimateDeviceRestoreState state{};
|
||||
state.mode = CLIMATE_MODE_COOL;
|
||||
|
||||
state.apply(&climate);
|
||||
|
||||
EXPECT_EQ(climate.mode, CLIMATE_MODE_COOL);
|
||||
}
|
||||
|
||||
TEST(ClimateRestoreStateTest, DoesNotRestoreAnUnsupportedMode) {
|
||||
TestClimate climate;
|
||||
ClimateDeviceRestoreState state{};
|
||||
state.mode = CLIMATE_MODE_HEAT;
|
||||
|
||||
state.apply(&climate);
|
||||
|
||||
// The device never advertised HEAT, so the mode stays where it was.
|
||||
EXPECT_EQ(climate.mode, CLIMATE_MODE_OFF);
|
||||
}
|
||||
|
||||
TEST(ClimateRestoreStateTest, LeavesTheCurrentModeAloneRatherThanForcingOff) {
|
||||
TestClimate climate;
|
||||
// apply() is public and nothing restricts it to setup(), so the entity is not necessarily off
|
||||
// when an unsupported mode is dropped. It keeps what it had rather than being forced to OFF.
|
||||
climate.mode = CLIMATE_MODE_COOL;
|
||||
ClimateDeviceRestoreState state{};
|
||||
state.mode = CLIMATE_MODE_HEAT;
|
||||
|
||||
state.apply(&climate);
|
||||
|
||||
EXPECT_EQ(climate.mode, CLIMATE_MODE_COOL);
|
||||
}
|
||||
|
||||
TEST(ClimateRestoreStateTest, KeepsRestoringTheOtherFieldsWhenTheModeIsDropped) {
|
||||
TestClimate climate;
|
||||
ClimateDeviceRestoreState state{};
|
||||
state.mode = CLIMATE_MODE_HEAT;
|
||||
state.target_temperature = 21.0f;
|
||||
state.uses_custom_fan_mode = false;
|
||||
state.fan_mode = CLIMATE_FAN_HIGH;
|
||||
|
||||
state.apply(&climate);
|
||||
|
||||
EXPECT_EQ(climate.mode, CLIMATE_MODE_OFF);
|
||||
EXPECT_FLOAT_EQ(climate.target_temperature, 21.0f);
|
||||
// Compared as an optional: this asserts both that the fan mode was restored and what it holds.
|
||||
EXPECT_EQ(climate.fan_mode, CLIMATE_FAN_HIGH);
|
||||
}
|
||||
|
||||
} // namespace esphome::climate::testing
|
||||
@@ -12,5 +12,8 @@ climate:
|
||||
- platform: climate_ir_lg
|
||||
name: LG Climate
|
||||
transmitter_id: xmitr
|
||||
header_high: 3300us
|
||||
header_low: 9840us
|
||||
advanced_commands_support: true
|
||||
sensor: climate_ir_lg_temp_sensor
|
||||
humidity_sensor: humidity_sensor
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Pulls in sensor so entity iteration paths compile (USE_SENSOR);
|
||||
# tests register their own instances. Plain yaml.safe_load, no ESPHome tags.
|
||||
# An alphabetically-earlier component's sensor: block shadows this one in
|
||||
# combined builds; the tests' sensor-count ASSERT catches a capacity drop.
|
||||
sensor:
|
||||
- platform: template
|
||||
id: bench_sensor_a
|
||||
name: "Bench A"
|
||||
- platform: template
|
||||
id: bench_sensor_b
|
||||
name: "Bench B"
|
||||
@@ -83,4 +83,50 @@ TEST(StaticVectorTest, ConvertingConstructorSameSize) {
|
||||
EXPECT_EQ(dst[2], 3);
|
||||
}
|
||||
|
||||
TEST(StringContainsIgnoreCaseTest, NullPointerAlwaysFalse) {
|
||||
const char *haystack = nullptr;
|
||||
const char *needle = nullptr;
|
||||
|
||||
EXPECT_FALSE(str_contains_ignore_case(haystack, needle));
|
||||
EXPECT_FALSE(str_contains_ignore_case("Hello World", needle));
|
||||
EXPECT_FALSE(str_contains_ignore_case(haystack, "anything"));
|
||||
}
|
||||
|
||||
TEST(StringContainsIgnoreCaseTest, EmptySearchMatches) {
|
||||
const char *haystack = "Hello World";
|
||||
|
||||
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, ""));
|
||||
}
|
||||
|
||||
TEST(StringContainsIgnoreCaseTest, MiscCaseMatches) {
|
||||
const char *haystack = "Hello World";
|
||||
|
||||
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hello"));
|
||||
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hello"));
|
||||
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "HELLO"));
|
||||
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "hELLO"));
|
||||
}
|
||||
|
||||
TEST(StringContainsIgnoreCaseTest, MiscNotMatching) {
|
||||
const char *haystack = "Hello World";
|
||||
|
||||
// Expected to match
|
||||
EXPECT_TRUE(str_contains_ignore_case_fallback(haystack, "Hell"));
|
||||
|
||||
// Expected not to match
|
||||
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Heaven"));
|
||||
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "Hello!"));
|
||||
EXPECT_FALSE(str_contains_ignore_case_fallback(haystack, "world!"));
|
||||
}
|
||||
|
||||
TEST(StringContainsIgnoreCaseTest, FallbackMatchesLibc) {
|
||||
const char *haystack = "Hello World";
|
||||
for (const char *needle : {"", "Hello", "hELLO", "HELLO", "Hell", "world", "World", "Heaven", "Hello!", "d"}) {
|
||||
EXPECT_EQ(str_contains_ignore_case_fallback(haystack, needle), str_contains_ignore_case(haystack, needle))
|
||||
<< "needle: " << needle;
|
||||
}
|
||||
EXPECT_EQ(str_contains_ignore_case_fallback("", ""), str_contains_ignore_case("", ""));
|
||||
EXPECT_EQ(str_contains_ignore_case_fallback("ab", "abc"), str_contains_ignore_case("ab", "abc"));
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/component_iterator.h"
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/application.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::testing {
|
||||
|
||||
// Iterator whose begin/end callbacks can refuse a configurable number of
|
||||
// times; all entity callbacks accept (any registered entities are accepted).
|
||||
class RefusingIterator : public ComponentIterator {
|
||||
public:
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
|
||||
bool on_##singular(type *obj) override { return true; }
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
#undef ENTITY_TYPE_
|
||||
#undef ENTITY_CONTROLLER_TYPE_
|
||||
// NOLINTEND(bugprone-macro-parentheses)
|
||||
|
||||
bool on_begin() override { return step(this->begin_calls, this->begin_refusals); }
|
||||
bool on_end() override { return step(this->end_calls, this->end_refusals); }
|
||||
|
||||
int begin_calls{0};
|
||||
int end_calls{0};
|
||||
int begin_refusals{0};
|
||||
int end_refusals{0};
|
||||
|
||||
protected:
|
||||
static bool step(int &calls, int &refusals) {
|
||||
calls++;
|
||||
if (refusals > 0) {
|
||||
refusals--;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// Far above the fixed number of iterator states
|
||||
static constexpr size_t BIG_BUDGET = 1000;
|
||||
|
||||
TEST(ComponentIterator, NotRunningMakesNoProgress) {
|
||||
RefusingIterator it;
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
EXPECT_EQ(it.begin_calls, 0);
|
||||
EXPECT_EQ(it.end_calls, 0);
|
||||
}
|
||||
|
||||
TEST(ComponentIterator, CompletesInOneCallWithoutRefusals) {
|
||||
RefusingIterator it;
|
||||
it.begin();
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
EXPECT_EQ(it.begin_calls, 1);
|
||||
EXPECT_EQ(it.end_calls, 1);
|
||||
}
|
||||
|
||||
TEST(ComponentIterator, StepBudgetIsHonored) {
|
||||
RefusingIterator it;
|
||||
it.begin();
|
||||
it.try_advance(1);
|
||||
EXPECT_EQ(it.begin_calls, 1);
|
||||
EXPECT_EQ(it.end_calls, 0);
|
||||
EXPECT_FALSE(it.completed());
|
||||
}
|
||||
|
||||
TEST(ComponentIterator, RefusedStepStopsBatchAndRetriesSameStep) {
|
||||
RefusingIterator it;
|
||||
it.end_refusals = 3;
|
||||
it.begin();
|
||||
// First call runs until the refused end step, which stops the pass
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.end_calls, 1);
|
||||
EXPECT_FALSE(it.completed());
|
||||
// The refused step is retried once per call, not skipped
|
||||
it.try_advance(BIG_BUDGET);
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.end_calls, 3);
|
||||
EXPECT_FALSE(it.completed());
|
||||
// Once accepted, the iteration completes
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
EXPECT_EQ(it.end_calls, 4);
|
||||
}
|
||||
|
||||
TEST(ComponentIterator, RefusedBeginStopsBatchAndRetries) {
|
||||
RefusingIterator it;
|
||||
it.begin_refusals = 2;
|
||||
it.begin();
|
||||
it.try_advance(BIG_BUDGET);
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.begin_calls, 2);
|
||||
EXPECT_FALSE(it.completed());
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
EXPECT_EQ(it.begin_calls, 3);
|
||||
}
|
||||
|
||||
// The deprecated advance() wrapper must keep the legacy once-per-loop
|
||||
// pattern working during the deprecation window.
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
TEST(ComponentIterator, DeprecatedAdvanceKeepsLegacyPatternWorking) {
|
||||
RefusingIterator it;
|
||||
it.end_refusals = 2;
|
||||
it.begin();
|
||||
size_t guard = 0;
|
||||
while (!it.completed() && guard++ < BIG_BUDGET) {
|
||||
it.advance();
|
||||
}
|
||||
EXPECT_TRUE(it.completed());
|
||||
// Two refused end steps were retried, then accepted
|
||||
EXPECT_EQ(it.end_calls, 3);
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
// Iterator whose sensor callback can refuse or yield; pins the per-item
|
||||
// contract: a refused item is re-offered with at_ unchanged, never skipped.
|
||||
class ItemRefusingIterator : public RefusingIterator {
|
||||
public:
|
||||
bool on_sensor(sensor::Sensor *obj) override {
|
||||
this->last_sensor = obj;
|
||||
if (!step(this->sensor_calls, this->sensor_refusals))
|
||||
return false;
|
||||
if (this->yield_on_sensor)
|
||||
this->yield_after_step_();
|
||||
return true;
|
||||
}
|
||||
sensor::Sensor *last_sensor{nullptr};
|
||||
int sensor_calls{0};
|
||||
int sensor_refusals{0};
|
||||
bool yield_on_sensor{false};
|
||||
};
|
||||
|
||||
class ComponentIteratorSensorTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
static sensor::Sensor sensor_a;
|
||||
static sensor::Sensor sensor_b;
|
||||
static bool registered = false;
|
||||
if (!registered) {
|
||||
App.register_sensor(&sensor_a);
|
||||
App.register_sensor(&sensor_b);
|
||||
registered = true;
|
||||
}
|
||||
// StaticVector drops silently when full; fail the fixture, not the contract
|
||||
ASSERT_EQ(App.get_sensors().size(), 2u) << "benchmark.yaml sensor count too small";
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ComponentIteratorSensorTest, RefusedItemIsReofferedNotSkipped) {
|
||||
ItemRefusingIterator it;
|
||||
it.sensor_refusals = 2;
|
||||
it.begin();
|
||||
// Runs until the first sensor refuses
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.sensor_calls, 1);
|
||||
EXPECT_FALSE(it.completed());
|
||||
// The refused item is re-offered, not skipped
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.sensor_calls, 2);
|
||||
sensor::Sensor *refused = it.last_sensor;
|
||||
// Once accepted, iteration continues through the second sensor to the end
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
EXPECT_NE(it.last_sensor, refused);
|
||||
EXPECT_EQ(it.sensor_calls, 4);
|
||||
}
|
||||
|
||||
TEST_F(ComponentIteratorSensorTest, YieldAfterStepEndsPassAndResumes) {
|
||||
ItemRefusingIterator it;
|
||||
it.yield_on_sensor = true;
|
||||
it.begin();
|
||||
// The pass ends right after the first sensor despite a big budget
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.sensor_calls, 1);
|
||||
EXPECT_FALSE(it.completed());
|
||||
// The next pass ends after the second sensor
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_EQ(it.sensor_calls, 2);
|
||||
// Remaining states then run to completion in one pass
|
||||
it.try_advance(BIG_BUDGET);
|
||||
EXPECT_TRUE(it.completed());
|
||||
}
|
||||
#endif // USE_SENSOR
|
||||
|
||||
} // namespace esphome::testing
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
@@ -213,4 +215,137 @@ TEST(BufAppendSepStr, Truncation) {
|
||||
EXPECT_EQ(end - buf, 7);
|
||||
}
|
||||
|
||||
// --- base64 encode/decode ---
|
||||
|
||||
static const char BASE64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// Pack 6-bit indices 0..63 into 48 bytes so encoding yields the full alphabet in order
|
||||
TEST(Base64, EncodeProducesCanonicalAlphabet) {
|
||||
uint8_t bytes[48];
|
||||
size_t n = 0;
|
||||
for (uint8_t i = 0; i < 64; i += 4) {
|
||||
bytes[n++] = (i << 2) | ((i + 1) >> 4);
|
||||
bytes[n++] = ((i + 1) & 0x0F) << 4 | ((i + 2) >> 2);
|
||||
bytes[n++] = ((i + 2) & 0x03) << 6 | (i + 3);
|
||||
}
|
||||
std::string encoded = base64_encode(bytes, sizeof(bytes)); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(encoded, BASE64_ALPHABET);
|
||||
}
|
||||
|
||||
// Decode the alphabet then re-encode: locks the encode and decode mappings together
|
||||
TEST(Base64, DecodeCanonicalAlphabetRoundTrip) {
|
||||
uint8_t buf[48];
|
||||
size_t len = base64_decode(std::string(BASE64_ALPHABET), buf, sizeof(buf));
|
||||
EXPECT_EQ(len, 48u);
|
||||
std::string reencoded = base64_encode(buf, len); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(reencoded, BASE64_ALPHABET);
|
||||
}
|
||||
|
||||
TEST(Base64, DecodeBase64UrlMatchesStandard) {
|
||||
std::string url = BASE64_ALPHABET;
|
||||
for (char &c : url) {
|
||||
if (c == '+')
|
||||
c = '-';
|
||||
if (c == '/')
|
||||
c = '_';
|
||||
}
|
||||
uint8_t standard[48], urlsafe[48];
|
||||
size_t len_standard = base64_decode(std::string(BASE64_ALPHABET), standard, sizeof(standard));
|
||||
size_t len_url = base64_decode(url, urlsafe, sizeof(urlsafe));
|
||||
EXPECT_EQ(len_standard, len_url);
|
||||
EXPECT_EQ(memcmp(standard, urlsafe, len_standard), 0);
|
||||
}
|
||||
|
||||
// RFC 4648 vectors cover both padding cases (len % 3 == 1 and len % 3 == 2)
|
||||
TEST(Base64, Rfc4648Vectors) {
|
||||
const struct {
|
||||
const char *plain;
|
||||
const char *encoded;
|
||||
} vectors[] = {
|
||||
{"", ""},
|
||||
{"f", "Zg=="},
|
||||
{"fo", "Zm8="},
|
||||
{"foo", "Zm9v"},
|
||||
{"foob", "Zm9vYg=="},
|
||||
{"fooba", "Zm9vYmE="},
|
||||
{"foobar", "Zm9vYmFy"},
|
||||
};
|
||||
for (const auto &v : vectors) {
|
||||
const auto *plain = reinterpret_cast<const uint8_t *>(v.plain);
|
||||
std::string encoded = base64_encode(plain, strlen(v.plain)); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(encoded, v.encoded);
|
||||
uint8_t buf[8];
|
||||
size_t len = base64_decode(reinterpret_cast<const uint8_t *>(v.encoded), strlen(v.encoded), buf, sizeof(buf));
|
||||
EXPECT_EQ(len, strlen(v.plain));
|
||||
EXPECT_EQ(memcmp(buf, v.plain, len), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// --- step_to_accuracy_decimals() ---
|
||||
|
||||
TEST(StepToAccuracyDecimals, TypicalSteps) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.001f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.005f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.01f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.025f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.05f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.1f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.25f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.5f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1.5f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(2.5f), 1);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, WholeSteps) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(2.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(5.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(10.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(100.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1000.0f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, FiveSignificantDigits) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1.23456f), 4);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(12.345f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(123.45f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1234.5f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(12345.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.33333f), 5);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.0001f), 4);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, TrailingZerosDropped) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.3f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.7f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.125f), 3);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.0625f), 4);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, RoundsUpToWholeNumber) {
|
||||
// Rounds to five significant digits first, so this becomes 10 with no decimals.
|
||||
EXPECT_EQ(step_to_accuracy_decimals(9.999999f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, OutsideFixedNotationRange) {
|
||||
// %.5g would print these in exponent form; the count is now the real one rather than a parse of "1e-05".
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.00001f), 5);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.000125f), 6);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(123456.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(1000000.0f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, SignIgnored) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-0.1f), 1);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-0.25f), 2);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-1.0f), 0);
|
||||
}
|
||||
|
||||
TEST(StepToAccuracyDecimals, NonFiniteAndZero) {
|
||||
EXPECT_EQ(step_to_accuracy_decimals(0.0f), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(NAN), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(INFINITY), 0);
|
||||
EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0);
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
// Pins the preferences contract concepts so the surface they enforce cannot
|
||||
// drift unnoticed: a minimal conforming type must satisfy each concept, and a
|
||||
// type missing a method or returning the wrong type must not.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/core/preference_backend.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
|
||||
struct MinimalBackend {
|
||||
bool save(const uint8_t *, size_t) { return true; }
|
||||
bool load(uint8_t *, size_t) { return true; }
|
||||
};
|
||||
static_assert(PreferenceBackendContract<MinimalBackend>);
|
||||
|
||||
struct BackendMissingLoad {
|
||||
bool save(const uint8_t *, size_t) { return true; }
|
||||
};
|
||||
static_assert(!PreferenceBackendContract<BackendMissingLoad>);
|
||||
|
||||
struct BackendWrongReturn {
|
||||
void save(const uint8_t *, size_t) {}
|
||||
bool load(uint8_t *, size_t) { return true; }
|
||||
};
|
||||
static_assert(!PreferenceBackendContract<BackendWrongReturn>);
|
||||
|
||||
struct MinimalPreferences : public PreferencesMixin<MinimalPreferences> {
|
||||
using PreferencesMixin<MinimalPreferences>::make_preference;
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||
bool sync() { return true; }
|
||||
bool reset() { return true; }
|
||||
};
|
||||
static_assert(PreferencesContract<MinimalPreferences>);
|
||||
|
||||
struct PreferencesMissingTwoArgForm : public PreferencesMixin<PreferencesMissingTwoArgForm> {
|
||||
using PreferencesMixin<PreferencesMissingTwoArgForm>::make_preference;
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||
bool sync() { return true; }
|
||||
bool reset() { return true; }
|
||||
};
|
||||
static_assert(!PreferencesContract<PreferencesMissingTwoArgForm>);
|
||||
|
||||
struct PreferencesMissingReset : public PreferencesMixin<PreferencesMissingReset> {
|
||||
using PreferencesMixin<PreferencesMissingReset>::make_preference;
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||
bool sync() { return true; }
|
||||
};
|
||||
static_assert(!PreferencesContract<PreferencesMissingReset>);
|
||||
|
||||
struct PreferencesWrongSyncReturn : public PreferencesMixin<PreferencesWrongSyncReturn> {
|
||||
using PreferencesMixin<PreferencesWrongSyncReturn>::make_preference;
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||
void sync() {}
|
||||
bool reset() { return true; }
|
||||
};
|
||||
static_assert(!PreferencesContract<PreferencesWrongSyncReturn>);
|
||||
|
||||
// Forgot `using PreferencesMixin<X>::make_preference;`, so the derived
|
||||
// overloads hide the template forms (see the PreferencesContract note in
|
||||
// preference_backend.h); the concept must reject the class.
|
||||
struct PreferencesForgotUsingDeclaration : public PreferencesMixin<PreferencesForgotUsingDeclaration> {
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t, bool) { return {}; }
|
||||
ESPPreferenceObject make_preference(size_t, uint32_t) { return {}; }
|
||||
bool sync() { return true; }
|
||||
bool reset() { return true; }
|
||||
};
|
||||
static_assert(!PreferencesContract<PreferencesForgotUsingDeclaration>);
|
||||
|
||||
struct MinimalKeyLookup {
|
||||
bool load_from_key(uint32_t, uint8_t *, size_t) { return true; }
|
||||
};
|
||||
static_assert(PreferencesKeyLookupContract<MinimalKeyLookup>);
|
||||
|
||||
struct KeyLookupMissingMethod {};
|
||||
static_assert(!PreferencesKeyLookupContract<KeyLookupMissingMethod>);
|
||||
|
||||
TEST(PreferenceContract, NullBackendRefusesBothOperations) {
|
||||
// ESPPreferenceObject forwards to whichever backend the platform binds; a
|
||||
// default-constructed object has no backend and must refuse both operations
|
||||
// instead of crashing.
|
||||
ESPPreferenceObject without_backend;
|
||||
uint32_t value = 42;
|
||||
EXPECT_FALSE(without_backend.save(&value));
|
||||
EXPECT_FALSE(without_backend.load(&value));
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
@@ -0,0 +1,3 @@
|
||||
sensor:
|
||||
- platform: d01
|
||||
name: D01 PM2.5 Concentration
|
||||
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
d01: !include common.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO2
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
d01: !include common.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
|
||||
d01: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
sensor:
|
||||
- platform: ds1603l
|
||||
name: ds1603l Distance
|
||||
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO1
|
||||
rx_pin: GPIO3
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
|
||||
ds1603l: !include common.yaml
|
||||
@@ -0,0 +1,7 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO0
|
||||
rx_pin: GPIO2
|
||||
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
|
||||
ds1603l: !include common.yaml
|
||||
@@ -5,7 +5,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
effects:
|
||||
|
||||
@@ -5,7 +5,7 @@ light:
|
||||
id: led_matrix_32x8
|
||||
default_transition_length: 500ms
|
||||
chipset: ws2812
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
num_leds: 256
|
||||
pin: ${pin}
|
||||
effects:
|
||||
|
||||
@@ -6,7 +6,7 @@ light:
|
||||
pin: 2
|
||||
pio: 0
|
||||
num_leds: 256
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: WS2812
|
||||
effects:
|
||||
- e131:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
packages:
|
||||
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
emontx: !include common.yaml
|
||||
|
||||
# Validate that each sensor type gets the correct default state_class,
|
||||
# unit_of_measurement, device_class, and accuracy_decimals when NO overrides
|
||||
# are provided. The values are intentionally omitted so apply_tag_defaults is
|
||||
# exercised, not the user-override path.
|
||||
|
||||
sensor:
|
||||
# Energy sensor (E prefix): expects state_class=total_increasing, unit=Wh,
|
||||
# device_class=energy, accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: E1
|
||||
name: Energy 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Power sensor (P prefix): expects state_class=measurement, unit=W,
|
||||
# device_class=power, accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: P1
|
||||
name: Power 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Voltage sensor (V prefix): expects state_class=measurement, unit=V,
|
||||
# device_class=voltage, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: V1
|
||||
name: Voltage 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Current sensor (I prefix): expects state_class=measurement, unit=A,
|
||||
# device_class=current, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: I1
|
||||
name: Current 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Temperature sensor (T prefix): expects state_class=measurement, unit=°C,
|
||||
# device_class=temperature, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: T1
|
||||
name: Temperature 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Pulse sensor (PULSE pattern): expects state_class=total_increasing,
|
||||
# unit=pulses, device_class=energy, accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: PULSE1
|
||||
name: Pulse 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Power factor sensor (PF pattern): expects state_class=measurement,
|
||||
# device_class=power_factor, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: PF1
|
||||
name: Power Factor 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Apparent power sensor (AP pattern): expects state_class=measurement,
|
||||
# unit=VA, device_class=apparent_power, accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: AP1
|
||||
name: Apparent Power 1
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Frequency sensor (F, matched exactly, not as a prefix): expects
|
||||
# state_class=measurement, unit=Hz, device_class=frequency,
|
||||
# accuracy_decimals=2
|
||||
- platform: emontx
|
||||
tag_name: F
|
||||
name: Frequency
|
||||
emontx_id: test_emontx
|
||||
|
||||
# Unknown tag: no prefix match, falls back to state_class=measurement,
|
||||
# accuracy_decimals=0
|
||||
- platform: emontx
|
||||
tag_name: CUSTOM1
|
||||
name: Custom sensor
|
||||
emontx_id: test_emontx
|
||||
|
||||
# User override: verify that explicit values are respected and not clobbered
|
||||
- platform: emontx
|
||||
tag_name: E2
|
||||
name: Energy 2 (user override)
|
||||
emontx_id: test_emontx
|
||||
state_class: measurement
|
||||
accuracy_decimals: 3
|
||||
@@ -255,3 +255,45 @@ display:
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 60, Color(255, 0, 0));
|
||||
|
||||
# Waveshare 7.5" V2 mono (800x480, UC8179 controller, EPD_7in5_V2)
|
||||
# full_update_every > 1 exercises the fast/partial refresh paths
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: waveshare-7.5in-v2
|
||||
full_update_every: 4
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
lambda: |-
|
||||
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
|
||||
it.circle(it.get_width() / 2, it.get_height() / 2, 100, Color::BLACK);
|
||||
|
||||
# Seeed reTerminal E1001 - 7.5" mono e-paper (800x480, UC8179)
|
||||
# Pins overridden to avoid conflicts with the E1002 defaults above
|
||||
- platform: epaper_spi
|
||||
spi_id: spi_bus
|
||||
model: seeed-reterminal-e1001
|
||||
cs_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO5
|
||||
dc_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO17
|
||||
reset_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO16
|
||||
busy_pin:
|
||||
allow_other_uses: true
|
||||
number: GPIO4
|
||||
inverted: true
|
||||
|
||||
@@ -7,7 +7,7 @@ esp32:
|
||||
enable_lwip_mdns_queries: true
|
||||
enable_lwip_bridge_interface: true
|
||||
disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization
|
||||
use_full_certificate_bundle: false # Test CMN bundle (default)
|
||||
use_full_certificate_bundle: false # Bundle stays off without a component that needs it
|
||||
include_builtin_idf_components:
|
||||
- freertos # Test escape hatch (freertos is always included anyway)
|
||||
enable_full_printf: false
|
||||
|
||||
@@ -6,3 +6,6 @@ update:
|
||||
type: embedded
|
||||
path: $component_dir/test_firmware.bin
|
||||
sha256: de2f256064a0af797747c2b97505dc0b9f3df0de4f489eac731c23ae9ca9cc31
|
||||
on_update_available:
|
||||
then:
|
||||
- logger.log: "Coprocessor update available"
|
||||
|
||||
@@ -8,3 +8,6 @@ update:
|
||||
type: http
|
||||
source: https://esphome.github.io/esp-hosted-firmware/manifest/esp32c6.json
|
||||
update_interval: 6h
|
||||
on_update_available:
|
||||
then:
|
||||
- logger.log: "Coprocessor update available"
|
||||
|
||||
@@ -3,13 +3,13 @@ light:
|
||||
id: led_strip1
|
||||
pin: ${pin1}
|
||||
num_leds: 60
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: ws2812
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: led_strip2
|
||||
pin: ${pin2}
|
||||
num_leds: 60
|
||||
rgbw_order: RWGB
|
||||
channel_colors: RWGB
|
||||
bit0_high: 100us
|
||||
bit0_low: 100us
|
||||
bit1_high: 100us
|
||||
|
||||
@@ -8,14 +8,14 @@ light:
|
||||
id: led_strip1
|
||||
pin: ${pin1}
|
||||
num_leds: 60
|
||||
rgb_order: GRB
|
||||
channel_colors: GRB
|
||||
chipset: ws2812
|
||||
use_dma: "true"
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: led_strip2
|
||||
pin: ${pin2}
|
||||
num_leds: 60
|
||||
rgb_order: RGB
|
||||
channel_colors: RGB
|
||||
bit0_high: 100us
|
||||
bit0_low: 100us
|
||||
bit1_high: 100us
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# The deprecated rgb_order / is_rgbw / is_wrgb keys, kept working until 2027.3.0.
|
||||
# Config-only: each strip below must migrate to the channel_colors shown in the comment.
|
||||
light:
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: legacy_rgb
|
||||
pin: GPIO13
|
||||
num_leds: 60
|
||||
chipset: ws2812
|
||||
rgb_order: GRB # -> GRB
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: legacy_rgbw
|
||||
pin: GPIO14
|
||||
num_leds: 60
|
||||
chipset: sk6812
|
||||
rgb_order: GRB
|
||||
is_rgbw: true # -> GRBW
|
||||
- platform: esp32_rmt_led_strip
|
||||
id: legacy_wrgb
|
||||
pin: GPIO15
|
||||
num_leds: 60
|
||||
chipset: sk6812
|
||||
rgb_order: GRB
|
||||
is_wrgb: true # -> WGRB
|
||||
@@ -0,0 +1,19 @@
|
||||
ethernet:
|
||||
type: CH390
|
||||
clk_pin: 19
|
||||
mosi_pin: 21
|
||||
miso_pin: 23
|
||||
cs_pin: 18
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
subnet: 255.255.255.0
|
||||
domain: .local
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
on_connect:
|
||||
- logger.log: "Ethernet connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Ethernet disconnected!"
|
||||
@@ -0,0 +1,17 @@
|
||||
ethernet:
|
||||
type: W5500
|
||||
spi_id: spi_bus
|
||||
cs_pin: 5
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
subnet: 255.255.255.0
|
||||
domain: .local
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
on_connect:
|
||||
- logger.log: "Ethernet connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Ethernet disconnected!"
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common-ch390.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
ethernet: !include common-w5500-spi-id.yaml
|
||||
@@ -0,0 +1,5 @@
|
||||
exposure_notifications:
|
||||
on_exposure_notification:
|
||||
then:
|
||||
- lambda: |
|
||||
ESP_LOGD("main", "RSSI: %d", x.rssi);
|
||||
@@ -1,7 +1,10 @@
|
||||
esp32_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
exposure_notifications:
|
||||
on_exposure_notification:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
ble_hub_id: ble_tracker_hub
|
||||
then:
|
||||
- lambda: |
|
||||
ESP_LOGD("main", "Got notification:");
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
ln882h_ble_tracker: !include ../ln882h_ble_tracker/common.yaml
|
||||
exposure_notifications: !include common-ln.yaml
|
||||
@@ -0,0 +1,15 @@
|
||||
# Config-only: the CI base board (generic-bk7252, BLE 4.2) cannot compile the
|
||||
# BLE 5.x tracker, so this fixture proves validation (schema + neutral binding)
|
||||
# on a non-esp32 platform; codegen and compilation are not exercised here.
|
||||
bk72xx_ble_tracker:
|
||||
id: ble_tracker_hub
|
||||
|
||||
exposure_notifications:
|
||||
on_exposure_notification:
|
||||
# Explicit ble_hub_id: pins the neutral binding as a declared key.
|
||||
ble_hub_id: ble_tracker_hub
|
||||
then:
|
||||
- lambda: |
|
||||
ESP_LOGD("main", "Got notification:");
|
||||
ESP_LOGD("main", " RPI: %s", format_hex(x.rolling_proximity_identifier).c_str());
|
||||
ESP_LOGD("main", " RSSI: %d", x.rssi);
|
||||
@@ -0,0 +1,52 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/hoermann_hcp/binary_sensor/hoermann_hcp_binary_sensor.h"
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::hoermann_hcp::testing {
|
||||
|
||||
// Nothing has been heard from the bus controller yet, so the sensor starts out seeded as disconnected.
|
||||
TEST(HoermannHcpBinarySensorTest, StartsDisconnected) {
|
||||
HoermannHcp door;
|
||||
HoermannHcpConnectedBinarySensor sensor(&door);
|
||||
sensor.setup();
|
||||
EXPECT_TRUE(sensor.has_state());
|
||||
EXPECT_FALSE(sensor.state);
|
||||
}
|
||||
|
||||
// The connection flag follows the bus controller in both directions.
|
||||
TEST(HoermannHcpBinarySensorTest, FollowsTheConnectionState) {
|
||||
TestableHoermannHcp door;
|
||||
HoermannHcpConnectedBinarySensor sensor(&door);
|
||||
sensor.setup();
|
||||
ASSERT_FALSE(sensor.state);
|
||||
|
||||
connect_controller(door);
|
||||
door.update();
|
||||
EXPECT_TRUE(sensor.state);
|
||||
|
||||
door.set_valid_(false);
|
||||
door.update();
|
||||
EXPECT_FALSE(sensor.state);
|
||||
}
|
||||
|
||||
// Any hub change re-runs the publish path, so an unchanged connection must not be reported twice.
|
||||
TEST(HoermannHcpBinarySensorTest, UnchangedConnectionIsPublishedOnce) {
|
||||
HoermannHcp door;
|
||||
HoermannHcpConnectedBinarySensor sensor(&door);
|
||||
sensor.setup();
|
||||
int publishes = 0;
|
||||
sensor.add_on_state_callback([&publishes](bool /*state*/) { publishes++; });
|
||||
|
||||
connect_controller(door);
|
||||
door.update();
|
||||
ASSERT_EQ(publishes, 1);
|
||||
|
||||
// A status broadcast changes the door state without touching the connection.
|
||||
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0064, 0x0100}));
|
||||
door.update();
|
||||
EXPECT_EQ(publishes, 1);
|
||||
}
|
||||
|
||||
} // namespace esphome::hoermann_hcp::testing
|
||||
@@ -0,0 +1,72 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "esphome/components/hoermann_hcp/button/hoermann_hcp_button.h"
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::hoermann_hcp::testing {
|
||||
|
||||
// The intermediate positions are named in the second register, which repeats that name on release.
|
||||
TEST(HoermannHcpButtonTest, VentButtonSendsTheVentCommand) {
|
||||
TestableHoermannHcp door;
|
||||
HoermannHcpVentButton vent(&door);
|
||||
connect_controller(door);
|
||||
|
||||
vent.press();
|
||||
|
||||
auto [pressed, pressed_2] = poll_command(door);
|
||||
EXPECT_EQ(pressed, 0x0200);
|
||||
EXPECT_EQ(pressed_2, 0x4000);
|
||||
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
||||
auto [released, released_2] = poll_command(door);
|
||||
EXPECT_EQ(released, 0x0100);
|
||||
EXPECT_EQ(released_2, 0x4000);
|
||||
}
|
||||
|
||||
TEST(HoermannHcpButtonTest, HalfOpenButtonSendsTheHalfOpenCommand) {
|
||||
TestableHoermannHcp door;
|
||||
HoermannHcpHalfOpenButton half_open(&door);
|
||||
connect_controller(door);
|
||||
|
||||
half_open.press();
|
||||
|
||||
auto [pressed, pressed_2] = poll_command(door);
|
||||
EXPECT_EQ(pressed, 0x0200);
|
||||
EXPECT_EQ(pressed_2, 0x0400);
|
||||
std::this_thread::sleep_for(KEY_PRESS_ELAPSED);
|
||||
auto [released, released_2] = poll_command(door);
|
||||
EXPECT_EQ(released, 0x0100);
|
||||
EXPECT_EQ(released_2, 0x0400);
|
||||
}
|
||||
|
||||
// The door drives to the vent position on its own, so a position the cover was still travelling to must not
|
||||
// stop it on the way there.
|
||||
TEST(HoermannHcpButtonTest, VentAbandonsAnArmedTarget) {
|
||||
TestableHoermannHcp door; // starts out fully closed
|
||||
HoermannHcpVentButton vent(&door);
|
||||
connect_controller(door);
|
||||
door.set_position(0.5f);
|
||||
consume_command(door);
|
||||
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0014, 0x0100}));
|
||||
ASSERT_EQ(door.get_door_state(), DoorState::OPENING);
|
||||
|
||||
vent.press();
|
||||
consume_command(door);
|
||||
|
||||
// Position 120/200 = 0.6 is past the abandoned target, which must no longer stop the door.
|
||||
door.on_write_registers(BROADCAST_REG, make_registers({0x0000, 0x0078, 0x0100}));
|
||||
EXPECT_EQ(poll_command(door).first, 0x0000);
|
||||
}
|
||||
|
||||
// A button carries no state, so a refused press is simply dropped rather than fired once the controller
|
||||
// turns up, which could be much later.
|
||||
TEST(HoermannHcpButtonTest, PressWithoutABusControllerSendsNothing) {
|
||||
HoermannHcp door; // never contacted by a bus controller
|
||||
HoermannHcpVentButton vent(&door);
|
||||
|
||||
vent.press();
|
||||
|
||||
EXPECT_EQ(poll_command(door).first, 0x0000);
|
||||
}
|
||||
|
||||
} // namespace esphome::hoermann_hcp::testing
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user