Merge branch 'dev' into sendspin-artwork

This commit is contained in:
Kevin Ahrendt
2026-05-04 06:37:51 -04:00
committed by GitHub
330 changed files with 11733 additions and 2571 deletions
+18
View File
@@ -91,6 +91,24 @@ api:
- float_arr.size()
- string_arr[0].c_str()
- string_arr.size()
# Test array + string args used after a non-synchronous action (delay).
# The default non-owning types (StringRef, const FixedVector&) would
# dangle once rx_buf_ is reused, and FixedVector is non-copyable so
# DelayAction's lambda capture would fail to compile. The api codegen
# must fall back to owning std::string / std::vector here.
- action: array_with_delay
variables:
name: string
int_arr: int[]
string_arr: string[]
then:
- delay: 20ms
- logger.log:
format: "Delayed: %s (%u ints, %u strings)"
args:
- name.c_str()
- int_arr.size()
- string_arr.size()
# Test ContinuationAction (IfAction with then/else branches)
- action: test_if_action
variables:
@@ -0,0 +1,123 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <ios>
#include <random>
#include "esphome/components/api/api_buffer.h"
#include "esphome/components/api/proto.h"
namespace esphome::api::testing {
// Generic varint decoder, used to verify the encoded bytes round-trip back to
// the original 48-bit MAC value, independent of the specialized encoder under
// test.
static uint64_t decode_varint(const uint8_t *buf, size_t len, size_t *consumed) {
uint64_t value = 0;
int shift = 0;
for (size_t i = 0; i < len; i++) {
value |= static_cast<uint64_t>(buf[i] & 0x7F) << shift;
if ((buf[i] & 0x80) == 0) {
*consumed = i + 1;
return value;
}
shift += 7;
}
*consumed = 0;
return 0;
}
// Reference encoder mirroring ProtoEncode::encode_varint_raw_64.
static size_t reference_encode(uint64_t value, uint8_t *out) {
uint8_t *p = out;
if (value < 128) {
*p++ = static_cast<uint8_t>(value);
return p - out;
}
do {
*p++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
} while (value > 0x7F);
*p++ = static_cast<uint8_t>(value);
return p - out;
}
// Encode `mac` via the 48-bit fast path and verify:
// - byte-identical output to the reference loop
// - encoded byte length matches `expected_bytes`
// - calc_uint64_48bit_force agrees on the size
// - the bytes round-trip through a generic varint decoder
static void verify_mac(uint64_t mac, size_t expected_bytes) {
ASSERT_LT(mac, 1ULL << 48) << "test fixture mac exceeds 48 bits";
uint8_t ref_buf[16] = {0};
size_t ref_len = reference_encode(mac, ref_buf);
APIBuffer api_buf;
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();
#endif
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, mac);
size_t new_len = pos - api_buf.data();
EXPECT_EQ(new_len, expected_bytes) << "mac=0x" << std::hex << mac << std::dec;
EXPECT_EQ(ref_len, expected_bytes) << "reference disagrees on length for mac=0x" << std::hex << mac << std::dec;
for (size_t i = 0; i < new_len; i++) {
EXPECT_EQ(api_buf.data()[i], ref_buf[i])
<< "byte " << i << " differs for mac=0x" << std::hex << mac << " (got 0x" << static_cast<int>(api_buf.data()[i])
<< ", expected 0x" << static_cast<int>(ref_buf[i]) << ")" << std::dec;
}
size_t consumed = 0;
uint64_t decoded = decode_varint(api_buf.data(), new_len, &consumed);
EXPECT_EQ(consumed, new_len) << "decoder did not consume all bytes for mac=0x" << std::hex << mac << std::dec;
EXPECT_EQ(decoded, mac) << "round-trip mismatch for mac=0x" << std::hex << mac << std::dec;
// Verify the size helper agrees. field_id_size = 1 (typical 1-byte tag).
uint32_t calc_size = ProtoSize::calc_uint64_48bit_force(1, mac);
EXPECT_EQ(calc_size, 1 + expected_bytes)
<< "calc_uint64_48bit_force size mismatch for mac=0x" << std::hex << mac << std::dec;
}
// Compute the canonical varint byte length for a value < 1<<48.
static size_t expected_varint_len(uint64_t v) {
if (v < (1ULL << 7))
return 1;
if (v < (1ULL << 14))
return 2;
if (v < (1ULL << 21))
return 3;
if (v < (1ULL << 28))
return 4;
if (v < (1ULL << 35))
return 5;
if (v < (1ULL << 42))
return 6;
return 7;
}
// --- Specific MACs requested for verification ---
TEST(ProtoMacVarint, AllZeros) { verify_mac(0x000000000000ULL, 1); } // 00:00:00:00:00:00
TEST(ProtoMacVarint, FirstByteOnly) { verify_mac(0x110000000000ULL, 7); } // 11:00:00:00:00:00
TEST(ProtoMacVarint, SecondByteOnly) { verify_mac(0x00AA00000000ULL, 6); } // 00:AA:00:00:00:00
TEST(ProtoMacVarint, ThirdByteOnly) { verify_mac(0x0000BB000000ULL, 5); } // 00:00:BB:00:00:00
TEST(ProtoMacVarint, FourthByteOnly) { verify_mac(0x000000CC0000ULL, 4); } // 00:00:00:CC:00:00
TEST(ProtoMacVarint, FifthByteOnly) { verify_mac(0x00000000DD00ULL, 3); } // 00:00:00:00:DD:00
TEST(ProtoMacVarint, SixthByteOnly) { verify_mac(0x0000000000EEULL, 2); } // 00:00:00:00:00:EE
TEST(ProtoMacVarint, AllOnes) { verify_mac(0xFFFFFFFFFFFFULL, 7); } // FF:FF:FF:FF:FF:FF
// 100 deterministic-random 48-bit MACs to catch regressions across the space.
TEST(ProtoMacVarint, RandomSample) {
// NOLINTNEXTLINE(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) -- fixed seed for reproducibility
std::mt19937_64 rng(0xC0FFEE);
for (int i = 0; i < 100; i++) {
uint64_t mac = rng() & 0xFFFFFFFFFFFFULL;
verify_mac(mac, expected_varint_len(mac));
}
}
} // namespace esphome::api::testing
+14
View File
@@ -0,0 +1,14 @@
audio:
codecs:
flac:
buffer_memory: internal
mp3:
buffer_memory: psram
opus:
floating_point: false
state_memory: psram
pseudostack:
threadsafe: false
buffer_memory: internal
size: 80000
wav:
@@ -0,0 +1 @@
<<: !include common.yaml
+71
View File
@@ -29,3 +29,74 @@ climate:
heat_action:
- switch.turn_on: climate_heater_switch
- switch.turn_off: climate_cooler_switch
# Thermostat-based climate so climate.control: action variants get build
# coverage (bang_bang doesn't support fan modes, presets, etc.). Climate
# has no template platform, so thermostat is the right vehicle.
- platform: thermostat
id: climate_test_thermostat
name: Test Thermostat
sensor: climate_temperature_sensor
min_idle_time: 30s
min_heating_off_time: 300s
min_heating_run_time: 300s
min_cooling_off_time: 300s
min_cooling_run_time: 300s
heat_action:
- logger.log: heating
idle_action:
- logger.log: idle
cool_action:
- logger.log: cooling
auto_mode:
- logger.log: auto
heat_cool_mode:
- logger.log: heat_cool
preset:
- name: Default
default_target_temperature_low: 18°C
default_target_temperature_high: 22°C
button:
# Exercise the climate.control: action so ControlAction templates get
# build coverage. Various field combinations are tested.
- platform: template
name: "Climate Control Mode"
on_press:
- climate.control:
id: climate_test_thermostat
mode: HEAT
- platform: template
name: "Climate Control Mode And Temps"
on_press:
- climate.control:
id: climate_test_thermostat
mode: HEAT_COOL
target_temperature_low: 19.0°C
target_temperature_high: 23.0°C
- platform: template
name: "Climate Control Lambda Temp"
on_press:
- climate.control:
id: climate_test_thermostat
target_temperature_high: !lambda "return 21.5;"
- platform: template
name: "Climate Control Off"
on_press:
- climate.control:
id: climate_test_thermostat
mode: "OFF"
# Exercise climate.control inside a trigger with non-empty Ts (number on_value
# passes float).
number:
- platform: template
id: climate_target_temp_number
optimistic: true
min_value: 16
max_value: 28
step: 0.5
on_value:
then:
- climate.control:
id: climate_test_thermostat
target_temperature_high: !lambda "return x;"
+57
View File
@@ -9,6 +9,14 @@ fan:
has_oscillating: true
has_direction: true
speed_count: 3
# Exercise fan.turn_on inside a trigger whose Ts pack is non-empty
# (StringRef from on_preset_set) so the apply-lambda + inner-lambda
# codegen runs through the cvref-normalized path.
on_preset_set:
then:
- fan.turn_on:
id: test_fan
speed: !lambda "return x.empty() ? 1 : 3;"
# Test lambdas using get_preset_mode() which returns StringRef
# These examples match the migration guide in the PR description
@@ -57,3 +65,52 @@ binary_sensor:
return true;
}
return false;
# Exercise fan.turn_on with various field combinations so the
# TurnOnAction codegen paths get build coverage.
button:
- platform: template
name: "Fan Speed Only"
on_press:
- fan.turn_on:
id: test_fan
speed: 2
- platform: template
name: "Fan Oscillating + Direction"
on_press:
- fan.turn_on:
id: test_fan
oscillating: true
direction: REVERSE
- platform: template
name: "Fan All Fields"
on_press:
- fan.turn_on:
id: test_fan
oscillating: false
speed: 3
direction: FORWARD
- platform: template
name: "Fan Lambda Speed"
on_press:
- fan.turn_on:
id: test_fan
speed: !lambda 'return 1;'
# Exercise fan.turn_on inside triggers with non-empty Ts:
# - number.on_value: Ts = float (Python value type; previously raised
# AttributeError on .operator("const"))
# - fan.on_preset_set: Ts = StringRef (already a value-type wrapper around
# a const char * + size; tests the cvref-normalized inner-lambda path)
number:
- platform: template
id: fan_speed_number
optimistic: true
min_value: 1
max_value: 3
step: 1
on_value:
then:
- fan.turn_on:
id: test_fan
speed: !lambda "return (int) x;"
+9
View File
@@ -0,0 +1,9 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# json's to_code calls cg.add_library("bblanchon/ArduinoJson", ...). C++
# unit test builds that pull json in transitively (e.g. api) need that
# library registration to happen, otherwise json_util.cpp fails to find
# ArduinoJson.h.
manifest.enable_codegen()
+19
View File
@@ -108,6 +108,10 @@ esphome:
relative_brightness: 5%
brightness_limits:
max_brightness: 90%
- light.dim_relative:
id: test_monochromatic_light
relative_brightness: -5%
transition_length: 250ms
- light.turn_on:
id: test_addressable_transition
brightness: 50%
@@ -123,6 +127,21 @@ esphome:
blue: 0%
transition_length: 1s
# Exercise light actions inside a trigger with non-empty Ts (number on_value
# passes float).
number:
- platform: template
id: test_number_brightness
optimistic: true
min_value: 0
max_value: 100
step: 1
on_value:
then:
- light.turn_on:
id: test_monochromatic_light
brightness: !lambda "return x / 100.0;"
light:
- platform: binary
id: test_binary_light
+98
View File
@@ -16,10 +16,19 @@ binary_sensor:
platform: template
- id: left_sensor
platform: template
- platform: lvgl
name: Button A pressed
widget: button_a
state: pressed
- platform: lvgl
name: Button A checked
widget: button_a
state: checked
- platform: lvgl
id: button_checker
name: LVGL button
widget: button_button
state: checked
on_state:
then:
- lvgl.checkbox.update:
@@ -29,6 +38,12 @@ binary_sensor:
auto y = x; // block inlining of one line return
return y;
- platform: lvgl
id: button_presser
name: Button pressed
widget: button_button
state: pressed
lvgl:
id: lvgl_id
rotation: 90
@@ -1098,6 +1113,8 @@ lvgl:
pad_row: 6px
pad_column: 0
multiple_widgets_per_cell: true
grid_cell_x_align: center
grid_cell_y_align: center
widgets:
- image:
grid_cell_row_pos: 0
@@ -1290,6 +1307,87 @@ lvgl:
hidden: true
mode: text_lower
# Grid shorthand "<rows>x": 3 rows specified, columns derived
# from widget count (4 widgets / 3 rows -> 2 columns)
- obj:
id: grid_rows_only_shorthand
layout: 3x
widgets:
- label:
text: "r1"
- label:
text: "r2"
- label:
text: "r3"
- label:
text: "r4"
# Grid shorthand "x<cols>": 4 columns specified, rows derived
# from widget count (5 widgets / 4 cols -> 2 rows)
- obj:
id: grid_cols_only_shorthand
layout: x4
widgets:
- label:
text: "a"
- label:
text: "b"
- label:
text: "c"
- label:
text: "d"
- label:
text: "e"
# Grid dict form with grid_rows as a plain integer; columns derived
- obj:
id: grid_rows_int
layout:
type: grid
grid_rows: 2
widgets:
- label:
text: "1"
- label:
text: "2"
- label:
text: "3"
# Grid dict form with grid_columns as a plain integer; rows derived
- obj:
id: grid_cols_int
layout:
type: grid
grid_columns: 3
widgets:
- label:
text: "x"
- label:
text: "y"
- label:
text: "z"
- label:
text: "w"
- label:
text: "v"
# Grid dict form with both grid_rows and grid_columns as plain integers
- obj:
id: grid_both_int
layout:
type: grid
grid_rows: 2
grid_columns: 2
widgets:
- label:
text: "1,1"
- label:
text: "1,2"
- label:
text: "2,1"
- label:
text: "2,2"
font:
- file: "gfonts://Roboto"
id: space16
+2
View File
@@ -21,6 +21,7 @@ mapping:
entries:
clear-night: image_1
sunny: image_2
default_value: image_1
- id: weather_map_2
from: string
to: image
@@ -35,6 +36,7 @@ mapping:
2: "two"
3: "three"
77: "seventy-seven"
default_value: unknown
- id: string_map
from: string
to: int
+1 -1
View File
@@ -4,7 +4,7 @@ packages:
display:
spi_id: spi_bus
platform: ili9xxx
platform: mipi_spi
id: main_lcd
model: ili9342
cs_pin: 12
@@ -4,7 +4,7 @@ packages:
display:
spi_id: spi_bus
platform: ili9xxx
platform: mipi_spi
id: main_lcd
model: ili9342
cs_pin: 5
@@ -4,7 +4,7 @@ packages:
display:
spi_id: spi_bus
platform: ili9xxx
platform: mipi_spi
id: main_lcd
model: ili9342
data_rate: 31.25MHz
@@ -0,0 +1,23 @@
ethernet:
type: W5500
clk_pin: 18
mosi_pin: 19
miso_pin: 16
cs_pin: 17
interrupt_pin: 21
reset_pin: 20
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"
mdns:
disabled: false
services:
- service: _test_service
protocol: _tcp
port: 8888
txt:
static_string: Anything
@@ -0,0 +1 @@
<<: !include common-enabled-ethernet.yaml
+1 -43
View File
@@ -1,53 +1,11 @@
modbus:
- id: mod_bus2
uart_id: uart_bus
role: server
modbus_controller:
- id: modbus_controller1
address: 0x2
modbus_id: modbus_bus
allow_duplicate_commands: false
on_online:
then:
logger.log: "Module Online"
- id: modbus_controller2
address: 0x2
modbus_id: mod_bus2
server_registers:
- address: 0x0000
value_type: S_DWORD_R
read_lambda: |-
return 42.3;
max_cmd_retries: 0
- id: modbus_controller3
address: 0x3
modbus_id: mod_bus2
server_registers:
- address: 0x0009
value_type: S_DWORD
read_lambda: |-
return 31;
write_lambda: |-
printf("address=%d, value=%d", x);
return true;
max_cmd_retries: 0
- id: modbus_controller4
modbus_id: mod_bus2
address: 0x4
server_courtesy_response:
enabled: true
register_last_address: 100
register_value: 0
server_registers:
- address: 0x0001
value_type: U_WORD
read_lambda: |-
return 0x8;
- address: 0x0005
value_type: U_WORD
read_lambda: |-
return (random_uint32() % 100);
binary_sensor:
- platform: modbus_controller
modbus_controller_id: modbus_controller1
@@ -0,0 +1,41 @@
modbus:
- id: mod_bus2
uart_id: uart_bus
role: server
modbus_server:
- id: modbus_server2
address: 0x2
modbus_id: mod_bus2
registers:
- address: 0x0
value_type: S_DWORD_R
read_lambda: |-
return 42.3;
- id: modbus_server3
address: 0x3
modbus_id: mod_bus2
registers:
- address: 0x9
value_type: S_DWORD
read_lambda: |-
return 31;
write_lambda: |-
printf("address=%d, value=%d", x);
return true;
- id: modbus_server4
modbus_id: mod_bus2
address: 0x4
courtesy_response:
enabled: true
register_last_address: 100
register_value: 0
registers:
- address: 0x1
value_type: U_WORD
read_lambda: |-
return 0x8;
- address: 0x5
value_type: U_WORD
read_lambda: |-
return (random_uint32() % 100);
@@ -0,0 +1,4 @@
packages:
modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
modbus: !include ../../test_build_components/common/modbus/esp8266-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
modbus: !include ../../test_build_components/common/modbus/rp2040-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,9 @@
nrf52:
dfu:
reset_pin:
number: 14
inverted: true
mode:
output: true
reg0:
voltage: 1.8V
@@ -1,9 +1,4 @@
nrf52:
dfu:
reset_pin:
number: 14
inverted: true
mode:
output: true
dfu: true
reg0:
voltage: 1.8V
@@ -0,0 +1,5 @@
ota:
- platform: esphome
allow_partition_access: true
<<: !include common.yaml
@@ -7,3 +7,4 @@ media_source:
initial_static_delay: 5ms
static_delay_adjustable: true
fixed_delay: 480us
decode_memory: internal
@@ -293,6 +293,60 @@ cover:
cover.is_closed: template_cover_with_triggers
then:
logger.log: Cover is closed
# Exercise cover.control / cover.template.publish action variants so they
# get build coverage in CI (and so memory-impact analysis on PRs that
# touch ControlAction / CoverPublishAction sees real instances).
- platform: template
name: "Template Cover Actions"
id: template_cover_actions
has_position: true
optimistic: true
open_action:
# CONF_STATE alias for the position bit
- cover.template.publish:
id: template_cover_actions
state: OPEN
- cover.template.publish:
id: template_cover_actions
position: 1.0
- cover.template.publish:
id: template_cover_actions
current_operation: IDLE
close_action:
- cover.template.publish:
id: template_cover_actions
position: 0.0
tilt: 0.0
stop_action:
- cover.template.publish:
id: template_cover_actions
current_operation: IDLE
tilt_action:
- lambda: |-
id(template_cover_actions).tilt = tilt;
id(template_cover_actions).publish_state();
on_idle:
# position only
- cover.control:
id: template_cover_actions
position: 50%
# tilt only
- cover.control:
id: template_cover_actions
tilt: 75%
# position + tilt
- cover.control:
id: template_cover_actions
position: 25%
tilt: 30%
# stop
- cover.control:
id: template_cover_actions
stop: true
# CONF_STATE alias for position
- cover.control:
id: template_cover_actions
state: OPEN
number:
- platform: template
@@ -302,6 +356,19 @@ number:
min_value: 0
max_value: 100
step: 1
# Exercise valve.control inside a trigger with non-empty Ts (number on_value
# passes float).
- platform: template
id: template_valve_position_number
optimistic: true
min_value: 0
max_value: 100
step: 1
on_value:
then:
- valve.control:
id: template_valve
position: !lambda "return x / 100.0f;"
select:
- platform: template
@@ -388,6 +455,20 @@ valve:
state: CLOSED
stop_action:
- logger.log: stop_action
# Exercise valve.control with various field combinations so the
# ControlAction codegen paths get build coverage.
- valve.control:
id: template_valve
stop: true
- valve.control:
id: template_valve
position: 50%
- valve.control:
id: template_valve
state: OPEN
- valve.control:
id: template_valve
position: !lambda 'return 0.25f;'
optimistic: true
text:
@@ -1,6 +1,7 @@
wifi:
min_auth_mode: WPA2
post_connect_roaming: true
phy_mode: 11G
packages:
- !include common.yaml
+8
View File
@@ -0,0 +1,8 @@
esphome:
on_boot:
- lambda: |-
ESP_LOGD("test", "millis=%u micros=%u cycles=%u",
(unsigned) millis(), (unsigned) micros(),
(unsigned) arch_get_cpu_cycle_count());
delay(1);
delayMicroseconds(1);
@@ -0,0 +1 @@
<<: !include common.yaml