Merge remote-tracking branch 'origin/dev' into web-server-offline-hint

This commit is contained in:
J. Nick Koston
2026-08-26 19:48:45 -05:00
149 changed files with 6779 additions and 895 deletions
+8
View File
@@ -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:
+73
View File
@@ -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,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
@@ -12,6 +12,13 @@ binary_sensor:
is_connected:
name: Garage Connected
button:
- platform: hoermann_hcp
vent:
name: Garage Vent
half_open:
name: Garage Half Open
light:
- platform: hoermann_hcp
name: Garage Light
+2 -2
View File
@@ -12,7 +12,7 @@ esphome:
data_template:
message: The humidity is {{ my_variable }}%.
variables:
my_variable: "return id(ha_hello_world_temperature).state;"
my_variable: !lambda "return id(ha_hello_world_temperature).state;"
- homeassistant.action:
action: notify.html5
data:
@@ -24,7 +24,7 @@ esphome:
data_template:
message: The humidity is {{ my_variable }}%.
variables:
my_variable: "return id(ha_hello_world_temperature).state;"
my_variable: !lambda "return id(ha_hello_world_temperature).state;"
wifi:
ssid: MySSID
+32
View File
@@ -1181,6 +1181,38 @@ lvgl:
- logger.log:
format: "bar value %f"
args: [x]
- table:
id: table_id
align: top_mid
y: 60
columns:
- width: 40%
- width: 80
rows:
- ["Name", "Value"]
- cells:
- text: "Temp"
merge_right: true
- text: "22.5"
text_crop: true
selected_row: 0
on_value:
then:
- logger.log:
format: "table selected row %u col %u"
args: [row, column]
on_click:
then:
- lvgl.table.cell.update:
id: table_id
row: 1
column: 1
text: !lambda return str_sprintf("%.1f", (float) rand() / RAND_MAX * 100);
merge_right: false
- lvgl.table.update:
id: table_id
selected_row: !lambda return (int) ((float) rand() / RAND_MAX * 2);
selected_column: 0
- line:
id: lv_line_id
align: center
@@ -13,7 +13,7 @@ namespace esphome::modbus_controller::testing {
// malformed. Built at its true byte count, the oversize frame is refused by the hub's size check with
// a log instead.
TEST(ModbusCommandPayload, CoilWritePayloadIsExactSizedNotTruncated) {
ModbusController controller;
ModbusController controller(nullptr, 1);
std::vector<bool> coils(modbus::MAX_NUM_OF_COILS_TO_WRITE + 1, true);
auto cmd = ModbusCommandItem::create_write_multiple_coils(&controller, 0x10, coils);
EXPECT_EQ(cmd.payload.size(), modbus::packed_bit_bytes(coils.size()));
@@ -21,7 +21,7 @@ TEST(ModbusCommandPayload, CoilWritePayloadIsExactSizedNotTruncated) {
// LSB-first packing with zeroed pad bits, matching the wire layout the PDU builders produce.
TEST(ModbusCommandPayload, CoilWritePacksLsbFirstWithZeroPad) {
ModbusController controller;
ModbusController controller(nullptr, 1);
const std::vector<bool> coils{true, false, true, true};
auto cmd = ModbusCommandItem::create_write_multiple_coils(&controller, 0x10, coils);
ASSERT_EQ(cmd.payload.size(), 1u);
@@ -2,6 +2,7 @@ remote_transmitter:
id: xmitr
pin: GPIO26
carrier_duty_percent: 50%
# non_blocking is bk7231n/bk7238-only; the CI board is a BK7252
packages:
buttons: !include common-buttons.yaml
@@ -2,6 +2,7 @@ remote_transmitter:
id: xmitr
pin: GPIO12
carrier_duty_percent: 50%
# non_blocking is rtl8720c-only; the CI board is an RTL8710B
packages:
buttons: !include common-buttons.yaml