mirror of
https://github.com/esphome/esphome.git
synced 2026-10-04 02:05:59 +00:00
Merge remote-tracking branch 'upstream/dev' into neutral-ble-client
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -57,6 +57,21 @@ sensor:
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,3 @@
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
ethernet: !include common-w5500-spi-id.yaml
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -1214,6 +1246,64 @@ lvgl:
|
||||
id: checkbox_id
|
||||
text: Checkbox
|
||||
align: bottom_right
|
||||
- list:
|
||||
id: test_list_id
|
||||
align: top_right
|
||||
width: 150px
|
||||
height: 120px
|
||||
pad_row: 4
|
||||
on_add:
|
||||
- logger.log:
|
||||
format: "list entry added at %d"
|
||||
args: [list_index]
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "list entry removed at %d"
|
||||
args: [list_index]
|
||||
on_click:
|
||||
- lvgl.list.add_text:
|
||||
id: test_list_id
|
||||
text: !lambda return "Section";
|
||||
- lvgl.list.add_text:
|
||||
id: test_list_id
|
||||
text: "Pinned section"
|
||||
index: 0
|
||||
- lvgl.list.add:
|
||||
id: test_list_id
|
||||
button:
|
||||
text: "Entry"
|
||||
checkable: true
|
||||
- lvgl.list.add:
|
||||
id: test_list_id
|
||||
index: 1
|
||||
obj:
|
||||
widgets:
|
||||
- label:
|
||||
text: !lambda return "Dynamic row " + std::to_string(millis());
|
||||
- button:
|
||||
widgets:
|
||||
- label:
|
||||
text: "Tap"
|
||||
on_click:
|
||||
- lambda: |-
|
||||
ESP_LOGD("lvgl", "dynamic row button clicked, row %d",
|
||||
lvgl::lv_list_get_row_index(id(test_list_id), static_cast<lv_obj_t *>(lv_event_get_target(event))));
|
||||
- dropdown:
|
||||
options:
|
||||
- "One"
|
||||
- "Two"
|
||||
on_value:
|
||||
- lambda: |-
|
||||
ESP_LOGD("lvgl", "dynamic row dropdown changed, row %d",
|
||||
lvgl::lv_list_get_row_index(id(test_list_id), static_cast<lv_obj_t *>(lv_event_get_target(event))));
|
||||
- lvgl.list.remove:
|
||||
id: test_list_id
|
||||
index: 0
|
||||
- lvgl.list.clear:
|
||||
id: test_list_id
|
||||
- lvgl.list.update:
|
||||
id: test_list_id
|
||||
pad_row: 8
|
||||
- slider:
|
||||
id: slider_id
|
||||
align: top_mid
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
esphome:
|
||||
name: lvgl-list-validate
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
|
||||
display:
|
||||
- platform: sdl
|
||||
id: sdl0
|
||||
dimensions:
|
||||
width: 320
|
||||
height: 240
|
||||
|
||||
lvgl:
|
||||
displays: sdl0
|
||||
widgets:
|
||||
# Two independent lists, each with their own on_add/on_remove and, for list_a,
|
||||
# more than one automation under the same trigger key -- checks that the
|
||||
# per-list trigger bookkeeping is keyed correctly and doesn't require exactly
|
||||
# one automation.
|
||||
- list:
|
||||
id: validate_list_a
|
||||
align: center
|
||||
pad_row: 6
|
||||
on_add:
|
||||
- logger.log:
|
||||
format: "a: added %d"
|
||||
args: [list_index]
|
||||
- logger.log:
|
||||
format: "a: also added %d"
|
||||
args: [list_index]
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "a: removed %d"
|
||||
args: [list_index]
|
||||
on_boot:
|
||||
# lvgl.list.add_text and lvgl.list.add both take an optional, templatable index.
|
||||
- lvgl.list.add_text:
|
||||
id: validate_list_a
|
||||
text: "Header"
|
||||
index: !lambda return 0;
|
||||
# any registered widget type is valid as the single lvgl.list.add key.
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
checkbox:
|
||||
align: center
|
||||
text: "Option"
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
index: !lambda return 0;
|
||||
switch:
|
||||
align: center
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
spinner:
|
||||
align: center
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
obj:
|
||||
align: center
|
||||
- lvgl.list.remove:
|
||||
id: validate_list_a
|
||||
index: !lambda return 0;
|
||||
- lvgl.list.clear:
|
||||
id: validate_list_a
|
||||
- list:
|
||||
id: validate_list_b
|
||||
align: center
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "b: removed %d"
|
||||
args: [list_index]
|
||||
@@ -15,11 +15,10 @@ struct MitsubishiCN105ClimateTestContext {
|
||||
|
||||
TEST(MitsubishiCN105ClimateTests, CelsiusTemperatureMappingAndTraitsMatchExpectedValues) {
|
||||
MitsubishiCN105ClimateTestContext context;
|
||||
const auto mapping = TemperatureMapping();
|
||||
|
||||
for (int temperature = 16; temperature <= 31; ++temperature) {
|
||||
EXPECT_EQ(mapping.to_mitsubishi(temperature), temperature);
|
||||
EXPECT_EQ(mapping.from_mitsubishi(temperature), temperature);
|
||||
EXPECT_EQ(context.component.get_temperature_mapping().to_mitsubishi(temperature), temperature);
|
||||
EXPECT_EQ(context.component.get_temperature_mapping().from_mitsubishi(temperature), temperature);
|
||||
}
|
||||
|
||||
const auto traits = context.sut.traits();
|
||||
@@ -32,8 +31,6 @@ TEST(MitsubishiCN105ClimateTests, CelsiusTemperatureMappingAndTraitsMatchExpecte
|
||||
|
||||
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingAndTraitsMatchExpectedValues) {
|
||||
MitsubishiCN105ClimateTestContext context;
|
||||
auto mapping = TemperatureMapping();
|
||||
mapping.set_use_fahrenheit(true);
|
||||
context.component.set_use_fahrenheit(true);
|
||||
|
||||
const std::array cases{
|
||||
@@ -46,8 +43,8 @@ TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingAndTraitsMatchExpe
|
||||
};
|
||||
|
||||
for (const auto &[fahrenheit, mitsubishi_celsius] : cases) {
|
||||
EXPECT_FLOAT_EQ(mapping.to_mitsubishi(fahrenheit), mitsubishi_celsius);
|
||||
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(mitsubishi_celsius), fahrenheit);
|
||||
EXPECT_FLOAT_EQ(context.component.get_temperature_mapping().to_mitsubishi(fahrenheit), mitsubishi_celsius);
|
||||
EXPECT_FLOAT_EQ(context.component.get_temperature_mapping().from_mitsubishi(mitsubishi_celsius), fahrenheit);
|
||||
}
|
||||
const auto traits = context.sut.traits();
|
||||
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT);
|
||||
|
||||
@@ -42,11 +42,17 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
|
||||
// All bytes from UART should be consumed
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
// After successful connect we request status, first settings (0x02)
|
||||
// Defer the first settings request (0x02) until the next update.
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::DEFERRED_STATUS_REQUEST);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
|
||||
ctx.sut.set_current_time(201);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B));
|
||||
EXPECT_EQ(ctx.sut.operation_start_ms_, 200);
|
||||
EXPECT_EQ(ctx.sut.operation_start_ms_, 201);
|
||||
|
||||
// Clear TX bytes.
|
||||
ctx.uart.tx.clear();
|
||||
@@ -75,15 +81,24 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
EXPECT_EQ(ctx.sut.status().vane_mode, MitsubishiCN105::VaneMode::POSITION_4);
|
||||
EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::SWING);
|
||||
|
||||
// Now fetch telemetry (0x03)
|
||||
// Defer the telemetry request (0x03) until the next update.
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::DEFERRED_STATUS_REQUEST);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
|
||||
ctx.sut.set_current_time(301);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A));
|
||||
EXPECT_EQ(ctx.sut.operation_start_ms_, 300);
|
||||
EXPECT_EQ(ctx.sut.operation_start_ms_, 301);
|
||||
|
||||
// Clear TX bytes.
|
||||
ctx.uart.tx.clear();
|
||||
|
||||
// Queue a setting while waiting for telemetry.
|
||||
ctx.sut.set_power(true);
|
||||
|
||||
// Telemetry response
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x0B, 0x00, 0x00,
|
||||
0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5});
|
||||
@@ -103,6 +118,13 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
EXPECT_EQ(ctx.sut.operation_start_ms_, 400);
|
||||
|
||||
// Apply the pending setting on the next update, outside RX processing.
|
||||
ctx.sut.set_current_time(401);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS);
|
||||
EXPECT_FALSE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.operation_start_ms_, 401);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
|
||||
@@ -469,6 +491,36 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
|
||||
EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, PendingSettingsTakePriorityOverDueTelemetry) {
|
||||
MitsubishiCN105TestsContext ctx;
|
||||
|
||||
ctx.sut.status_.target_temperature = 24.0f;
|
||||
ctx.sut.status_.room_temperature = 21.0f;
|
||||
ASSERT_TRUE(ctx.sut.is_status_initialized());
|
||||
|
||||
ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED;
|
||||
ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
ctx.sut.set_current_time(1000);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
ctx.uart.tx.clear();
|
||||
|
||||
ctx.sut.set_power(true);
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07,
|
||||
0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C});
|
||||
|
||||
ctx.sut.set_current_time(1001);
|
||||
ASSERT_TRUE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
|
||||
ctx.sut.set_current_time(1002);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, SetAndClearRemoteRoomTemp) {
|
||||
MitsubishiCN105TestsContext ctx;
|
||||
|
||||
|
||||
@@ -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 @@ modbus_controller:
|
||||
- id: modbus_controller1
|
||||
address: 0x2
|
||||
modbus_id: modbus_bus
|
||||
continuous: true
|
||||
on_online:
|
||||
then:
|
||||
logger.log: "Module Online"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
then:
|
||||
- rp2_ble_tracker.start_scan
|
||||
- rp2_ble_tracker.start_scan:
|
||||
continuous: true
|
||||
# Lambda arm of the templatable value — different codegen instantiation.
|
||||
- rp2_ble_tracker.start_scan:
|
||||
continuous: !lambda return false;
|
||||
- rp2_ble_tracker.stop_scan
|
||||
- rp2_ble_tracker.stop_scan: ble_tracker
|
||||
|
||||
rp2_ble_tracker:
|
||||
on_ble_advertise:
|
||||
- mac_address: AC:37:43:77:5F:4C
|
||||
then:
|
||||
- lambda: |-
|
||||
char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGD("main", "The device address is %s", x.address_str_to(addr));
|
||||
- mac_address:
|
||||
- AC:37:43:77:5F:4C
|
||||
- AC:37:43:77:5F:4D
|
||||
then:
|
||||
- lambda: |-
|
||||
char addr[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGD("main", "The device address is %s", x.address_str_to(addr));
|
||||
on_ble_service_data_advertise:
|
||||
- service_uuid: ABCD
|
||||
# mac_address exercises the UUID triggers' set_address() codegen branch.
|
||||
mac_address: AC:37:43:77:5F:4C
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Length of service data is %zu", x.size());
|
||||
- service_uuid: ABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "32-bit service data is %zu", x.size());
|
||||
- service_uuid: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "128-bit service data is %zu", x.size());
|
||||
on_ble_manufacturer_data_advertise:
|
||||
- manufacturer_id: ABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Length of manufacturer data is %zu", x.size());
|
||||
- manufacturer_id: ABCDABCD-ABCD-ABCD-ABCD-ABCDABCDABCD
|
||||
then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "128-bit manufacturer data is %zu", x.size());
|
||||
on_scan_end:
|
||||
- then:
|
||||
- lambda: |-
|
||||
ESP_LOGD("main", "Scan ended");
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
rp2_ble_tracker: !include common.yaml
|
||||
automations: !include common-automations.yaml
|
||||
@@ -0,0 +1,61 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "esphome/components/runtime_image/runtime_image.h"
|
||||
|
||||
namespace esphome::runtime_image::testing {
|
||||
|
||||
TEST(RuntimeImageMime, FormatForKnownMimeTypes) {
|
||||
EXPECT_EQ(get_format_for_mime_type("image/bmp"), BMP);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-ms-bmp"), BMP);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-bmp"), BMP);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/png"), PNG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/x-png"), PNG);
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
EXPECT_EQ(get_format_for_mime_type("image/jpeg"), JPEG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/jpg"), JPEG);
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
}
|
||||
|
||||
TEST(RuntimeImageMime, FormatMatchingIsCaseInsensitive) {
|
||||
EXPECT_EQ(get_format_for_mime_type("Image/PNG"), PNG);
|
||||
EXPECT_EQ(get_format_for_mime_type("IMAGE/BMP"), BMP);
|
||||
}
|
||||
|
||||
TEST(RuntimeImageMime, FormatMatchesContentTypeWithParameters) {
|
||||
// Content-Type headers may carry parameters after the media type
|
||||
EXPECT_EQ(get_format_for_mime_type("image/png; charset=binary"), PNG);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/bmp;name=\"a.bmp\""), BMP);
|
||||
}
|
||||
|
||||
TEST(RuntimeImageMime, UnknownMimeTypeHasNoFormat) {
|
||||
EXPECT_EQ(get_format_for_mime_type("text/html"), std::nullopt);
|
||||
EXPECT_EQ(get_format_for_mime_type("application/octet-stream"), std::nullopt);
|
||||
EXPECT_EQ(get_format_for_mime_type("image/*"), std::nullopt);
|
||||
EXPECT_EQ(get_format_for_mime_type(""), std::nullopt);
|
||||
EXPECT_EQ(get_format_for_mime_type(nullptr), std::nullopt);
|
||||
}
|
||||
|
||||
TEST(RuntimeImageMime, MimeTypeForFormatRoundTrip) {
|
||||
EXPECT_STREQ(get_mime_type_for_format(BMP), "image/bmp");
|
||||
EXPECT_STREQ(get_mime_type_for_format(PNG), "image/png");
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
EXPECT_STREQ(get_mime_type_for_format(JPEG), "image/jpeg");
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
// AUTO has no single MIME type and falls back to the wildcard
|
||||
EXPECT_STREQ(get_mime_type_for_format(AUTO), "image/*");
|
||||
|
||||
// Every decodable format must resolve back to itself through its MIME type
|
||||
for (ImageFormat format : {
|
||||
BMP,
|
||||
PNG,
|
||||
#ifdef USE_RUNTIME_IMAGE_JPEG
|
||||
JPEG,
|
||||
#endif // USE_RUNTIME_IMAGE_JPEG
|
||||
}) {
|
||||
EXPECT_EQ(get_format_for_mime_type(get_mime_type_for_format(format)), format) << format;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::runtime_image::testing
|
||||
@@ -0,0 +1,12 @@
|
||||
sensor:
|
||||
- platform: sfa40
|
||||
i2c_id: i2c_bus
|
||||
wait_for_ready: false
|
||||
formaldehyde:
|
||||
name: SFA40 formaldehyde
|
||||
temperature:
|
||||
name: SFA40 temperature
|
||||
humidity:
|
||||
name: SFA40 humidity
|
||||
address: 0x5D
|
||||
update_interval: 30s
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
sfa40: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
sfa40: !include common.yaml
|
||||
@@ -0,0 +1,3 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
sfa40: !include common.yaml
|
||||
@@ -0,0 +1,9 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
sensor:
|
||||
- platform: sfa40
|
||||
i2c_id: i2c_bus
|
||||
wait_for_ready: true
|
||||
formaldehyde:
|
||||
name: SFA40 formaldehyde
|
||||
@@ -4,6 +4,6 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
|
||||
cg.add_define("USE_TIME_TIMEZONE")
|
||||
|
||||
manifest.to_code = to_code
|
||||
|
||||
@@ -41,3 +41,6 @@ number:
|
||||
min_value: 2
|
||||
max_value: 100
|
||||
step: 1
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
@@ -10,6 +10,3 @@ zigbee:
|
||||
on_start:
|
||||
then:
|
||||
- logger.log: "Started zigbee stack"
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
@@ -5,3 +5,6 @@ zigbee:
|
||||
on_join:
|
||||
then:
|
||||
- logger.log: "Joined network"
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
Reference in New Issue
Block a user