Merge branch 'dev' into store-yaml-firmware

This commit is contained in:
J. Nick Koston
2026-07-03 19:04:12 -05:00
committed by GitHub
1326 changed files with 27006 additions and 16857 deletions
+28
View File
@@ -109,6 +109,34 @@ api:
- name.c_str()
- int_arr.size()
- string_arr.size()
# Test string + array args used by homeassistant.action's deferred
# on_success/on_error response callback. homeassistant.action registers
# synchronous=False, so the api codegen must fall back to owning
# std::string / std::vector args here: the non-owning defaults would
# dangle once rx_buf_ is reused before the response arrives, and the
# non-copyable FixedVector would fail to compile when captured into
# the response callback.
- action: action_response_args
variables:
name: string
int_arr: int[]
then:
- homeassistant.action:
action: notify.notify
data:
message: !lambda 'return name;'
on_success:
- logger.log:
format: "Notified %s (%u ints)"
args:
- name.c_str()
- int_arr.size()
on_error:
- logger.log:
format: "Notify failed (%s): %s"
args:
- error.c_str()
- name.c_str()
# Test ContinuationAction (IfAction with then/else branches)
- action: test_if_action
variables:
@@ -0,0 +1,7 @@
<<: !include common.yaml
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
+1 -1
View File
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO14
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_4800: !include ../../test_build_components/common/uart_4800/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO3
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_4800: !include ../../test_build_components/common/uart_4800/esp8266-ard.yaml
<<: !include common.yaml
+1 -1
View File
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_4800: !include ../../test_build_components/common/uart_4800/rp2040-ard.yaml
<<: !include common.yaml
+23 -11
View File
@@ -3,52 +3,64 @@ sensor:
name: "BMI270 Temperature"
- platform: motion
motion_id: bmi270_motion
type: acceleration_x
name: "Accel X"
name: "BMI270 Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
motion_id: bmi270_motion
type: acceleration_y
name: "Accel Y"
name: "BMI270 Accel Y"
accuracy_decimals: 4
- platform: motion
motion_id: bmi270_motion
type: acceleration_z
name: "Accel Z"
name: "BMI270 Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
motion_id: bmi270_motion
type: gyroscope_x
name: "Gyro X"
name: "BMI270 Gyro X"
- platform: motion
motion_id: bmi270_motion
type: gyroscope_y
name: "Gyro Y"
name: "BMI270 Gyro Y"
- platform: motion
motion_id: bmi270_motion
type: gyroscope_z
name: "Gyro Z"
name: "BMI270 Gyro Z"
- platform: motion
motion_id: bmi270_motion
type: angular_rate_x
name: "Angular Rate X"
name: "BMI270 Angular Rate X"
- platform: motion
motion_id: bmi270_motion
type: angular_rate_y
name: "Angular Rate Y"
name: "BMI270 Angular Rate Y"
- platform: motion
motion_id: bmi270_motion
type: angular_rate_z
name: "Angular Rate Z"
name: "BMI270 Angular Rate Z"
- platform: motion
motion_id: bmi270_motion
type: pitch
name: "Pitch"
name: "BMI270 Pitch"
- platform: motion
motion_id: bmi270_motion
type: roll
name: "Roll"
name: "BMI270 Roll"
motion:
- platform: bmi270
id: bmi270_motion
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
+16
View File
@@ -0,0 +1,16 @@
display:
- id: cst9220_display
platform: ili9xxx
model: ili9342
cs_pin: ${cs_pin}
dc_pin: ${dc_pin}
reset_pin: ${disp_reset_pin}
invert_colors: false
touchscreen:
- id: ts_cst9220
i2c_id: i2c_bus
platform: cst9220
display: cst9220_display
interrupt_pin: ${interrupt_pin}
reset_pin: ${reset_pin}
@@ -0,0 +1,12 @@
substitutions:
cs_pin: GPIO4
dc_pin: GPIO5
disp_reset_pin: GPIO12
interrupt_pin: GPIO15
reset_pin: GPIO25
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,5 @@
substitutions:
wakeup_pin: GPIO4
<<: !include common.yaml
<<: !include common-esp32-ext1.yaml
@@ -0,0 +1,5 @@
substitutions:
wakeup_pin: GPIO4
<<: !include common.yaml
<<: !include common-esp32-ext1.yaml
@@ -161,3 +161,66 @@ display:
busy_pin:
allow_other_uses: true
number: GPIO4
# Waveshare 2.13" V4 B series 3-color e-paper (122x250, BWR, SSD1680)
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-2.13in-bv4
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
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# Soldered Inkplate 2 3-color e-paper (104x212, BWR)
- platform: epaper_spi
spi_id: spi_bus
model: inkplate2
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
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 20, Color::BLACK);
it.circle(it.get_width() / 2, it.get_height() / 2, 15, Color(255, 0, 0));
# Waveshare 7.5" V2 BWR (800x480, UC8179 controller, EDP_7in5b_V2)
- platform: epaper_spi
spi_id: spi_bus
model: waveshare-7.5in-bv2-bwr
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
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);
it.circle(it.get_width() / 2, it.get_height() / 2, 60, Color(255, 0, 0));
@@ -77,3 +77,34 @@ esp32_ble_server:
id: test_change_descriptor
value:
data: [0x01, 0x02, 0x03]
# Regression test for #17142: the set_value action used from a trigger that passes
# its argument by reference (climate on_control supplies ClimateCall&) previously
# failed to compile.
sensor:
- platform: template
id: ble_test_temp
lambda: "return 20.0;"
output:
- platform: template
id: ble_test_output
type: float
write_action:
- logger.log: "out"
climate:
- platform: pid
name: "BLE Test Climate"
id: ble_test_climate
sensor: ble_test_temp
default_target_temperature: 20
heat_output: ble_test_output
control_parameters:
kp: 0.1
ki: 0.001
kd: 0.1
on_control:
- ble_server.characteristic.set_value:
id: test_notify_characteristic
value: !lambda "return std::vector<uint8_t>{0, 1, 2};"
@@ -1,7 +1,31 @@
# P0.2, P0.4 and P0.5 all live on the same Zephyr port device (gpio0) and each
# attaches its own interrupt. This locks in shared-port behavior: every pin owns
# a separate gpio_callback initialized with its own BIT(pin) mask, so Zephyr
# dispatches to each pin independently even though the port device is shared.
binary_sensor:
- platform: gpio
pin: 2
id: gpio_binary_sensor
use_interrupt: true
interrupt_type: ANY
# Inverted pin with an edge-specific interrupt: exercises the inversion-aware
# interrupt-arming path (logical RISING must arm on the physical falling edge).
- platform: gpio
pin:
number: P0.4
inverted: true
id: gpio_binary_sensor_inverted
use_interrupt: true
interrupt_type: RISING
# Second non-inverted interrupt on the same port (gpio0) as P0.2 above: verifies
# multiple pins sharing one port device each get their own callback/pin_mask.
- platform: gpio
pin: P0.5
id: gpio_binary_sensor_shared_port
use_interrupt: true
interrupt_type: FALLING
output:
- platform: gpio
+174
View File
@@ -0,0 +1,174 @@
#ifdef USE_HOST
#include <gtest/gtest.h>
#include <cstdlib>
#include <filesystem>
#include "esphome/components/host/preferences.h"
#include "esphome/core/application.h"
namespace esphome::host::testing {
namespace fs = std::filesystem;
/// RAII helper to save and restore an environment variable.
class ScopedEnvVar {
public:
explicit ScopedEnvVar(const char *name) : name_(name) {
const char *val = getenv(name);
if (val != nullptr) {
saved_value_ = val;
was_set_ = true;
}
}
~ScopedEnvVar() {
if (this->was_set_) {
setenv(this->name_.c_str(), this->saved_value_.c_str(), 1);
} else {
unsetenv(this->name_.c_str());
}
}
ScopedEnvVar(const ScopedEnvVar &) = delete;
ScopedEnvVar &operator=(const ScopedEnvVar &) = delete;
private:
std::string name_;
std::string saved_value_;
bool was_set_{false};
};
class HostPreferencesTest : public ::testing::Test {
protected:
void SetUp() override {
// Create a unique temp directory for this test
this->temp_dir_ = fs::temp_directory_path() / "esphome_prefs_test";
fs::create_directories(this->temp_dir_);
// Set up App name — string literal has static storage so StringRef is safe
App.pre_setup("test_prefs", 10, "", 0);
}
void TearDown() override {
std::error_code ec;
fs::remove_all(this->temp_dir_, ec);
}
fs::path temp_dir_;
};
TEST_F(HostPreferencesTest, BothVarsUnset_SyncReturnsFalse) {
ScopedEnvVar home_guard("HOME");
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
unsetenv("HOME");
unsetenv("ESPHOME_PREFDIR");
HostPreferences prefs;
EXPECT_FALSE(prefs.sync());
}
TEST_F(HostPreferencesTest, BothVarsUnset_SaveSucceedsInMemory) {
ScopedEnvVar home_guard("HOME");
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
unsetenv("HOME");
unsetenv("ESPHOME_PREFDIR");
HostPreferences prefs;
uint32_t value = 42;
// save() stores in memory even without a valid file path
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
// But sync to disk should fail
EXPECT_FALSE(prefs.sync());
}
TEST_F(HostPreferencesTest, PrefDirSet_SaveAndSync) {
ScopedEnvVar home_guard("HOME");
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
auto prefdir = this->temp_dir_ / "prefdir";
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
unsetenv("HOME");
HostPreferences prefs;
uint32_t value = 42;
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
EXPECT_TRUE(prefs.sync());
// Verify file was created in ESPHOME_PREFDIR
auto expected_file = prefdir / "test_prefs.prefs";
EXPECT_TRUE(fs::exists(expected_file));
}
TEST_F(HostPreferencesTest, HomeSet_SaveAndSync) {
ScopedEnvVar home_guard("HOME");
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
auto home = this->temp_dir_ / "home";
setenv("HOME", home.c_str(), 1);
unsetenv("ESPHOME_PREFDIR");
HostPreferences prefs;
uint32_t value = 42;
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
EXPECT_TRUE(prefs.sync());
// Verify file was created in HOME/.esphome/prefs
auto expected_file = home / ".esphome" / "prefs" / "test_prefs.prefs";
EXPECT_TRUE(fs::exists(expected_file));
}
TEST_F(HostPreferencesTest, PrefDirTakesPrecedenceOverHome) {
ScopedEnvVar home_guard("HOME");
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
auto prefdir = this->temp_dir_ / "prefdir";
auto home = this->temp_dir_ / "home";
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
setenv("HOME", home.c_str(), 1);
HostPreferences prefs;
uint32_t value = 42;
EXPECT_TRUE(prefs.save(0x1234, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
EXPECT_TRUE(prefs.sync());
// File should be in ESPHOME_PREFDIR, not HOME
auto prefdir_file = prefdir / "test_prefs.prefs";
auto home_file = home / ".esphome" / "prefs" / "test_prefs.prefs";
EXPECT_TRUE(fs::exists(prefdir_file));
EXPECT_FALSE(fs::exists(home_file));
}
TEST_F(HostPreferencesTest, SaveAndLoadRoundTrip) {
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
auto prefdir = this->temp_dir_ / "roundtrip";
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
// Save data with one instance
{
HostPreferences prefs;
uint32_t value = 0xDEADBEEF;
EXPECT_TRUE(prefs.save(0xABCD, reinterpret_cast<const uint8_t *>(&value), sizeof(value)));
EXPECT_TRUE(prefs.sync());
}
// Load with a fresh instance (reads from file)
{
HostPreferences prefs;
uint32_t loaded = 0;
EXPECT_TRUE(prefs.load(0xABCD, reinterpret_cast<uint8_t *>(&loaded), sizeof(loaded)));
EXPECT_EQ(loaded, 0xDEADBEEFu);
}
}
TEST_F(HostPreferencesTest, LoadNonExistentKeyReturnsFalse) {
ScopedEnvVar prefdir_guard("ESPHOME_PREFDIR");
auto prefdir = this->temp_dir_ / "nokey";
setenv("ESPHOME_PREFDIR", prefdir.c_str(), 1);
HostPreferences prefs;
uint32_t loaded = 0;
EXPECT_FALSE(prefs.load(0x9999, reinterpret_cast<uint8_t *>(&loaded), sizeof(loaded)));
}
} // namespace esphome::host::testing
#endif
@@ -0,0 +1,109 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
display:
# Generic IT8951 with explicit dimensions
- platform: it8951
spi_id: spi_bus
model: it8951
dimensions:
width: 1872
height: 1404
cs_pin:
allow_other_uses: true
number: GPIO5
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
enable_pin:
- GPIO17
- GPIO18
vcom: 1500
update_interval: 60s
# Exercise an alias for the update_mode config option.
update_mode: fast
lambda: |-
it.circle(64, 64, 50, Color::BLACK);
# m5stack-m5paper (960x540) — model supplies pin defaults
- platform: it8951
id: m5epd_display
spi_id: spi_bus
model: m5stack-m5paper
cs_pin:
allow_other_uses: true
number: GPIO5
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
full_update_every: 30
invert_colors: false
sleep_when_done: true
grayscale: true
update_mode: GC16
rotation: 270
transform:
mirror_x: false
mirror_y: false
lambda: |-
it.filled_rectangle(0, 0, it.get_width(), it.get_height(), Color::WHITE);
it.circle(it.get_width() / 2, it.get_height() / 2, 30, Color::BLACK);
# seeed-reterminal-e1003 (1872x1404)
- platform: it8951
spi_id: spi_bus
model: seeed-reterminal-e1003
cs_pin:
allow_other_uses: true
number: GPIO5
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
vcom: 1400
sleep_when_done: false
lambda: |-
it.filled_rectangle(0, 0, 128, 128, Color::BLACK);
# seeed-ee03 (1872x1404), monochrome fast path
- platform: it8951
spi_id: spi_bus
model: seeed-ee03
cs_pin:
allow_other_uses: true
number: GPIO5
reset_pin:
allow_other_uses: true
number: GPIO16
busy_pin:
allow_other_uses: true
number: GPIO4
grayscale: false
dithering: false
update_mode: DU
lambda: |-
it.circle(128, 128, 64, Color::BLACK);
# Exercise the it8951.update automation: alias modes, a direct enum-name mode,
# and the bare (default-mode) form.
interval:
- interval: 30s
then:
- it8951.update:
id: m5epd_display
mode: fast
- it8951.update:
id: m5epd_display
mode: full
- it8951.update:
id: m5epd_display
mode: A2
- it8951.update: m5epd_display
+3
View File
@@ -18,6 +18,9 @@ class MockUARTComponent : public uart::UARTComponent {
MOCK_METHOD(size_t, available, (), (override));
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override));
#if defined(USE_ESP8266) || defined(USE_ESP32)
void load_settings(bool dump_config) override {}
#endif // USE_ESP8266 || USE_ESP32
};
// Expose protected members for testing.
+24 -12
View File
@@ -1,54 +1,66 @@
sensor:
- platform: lsm6ds
name: "lsm6ds Temperature"
name: "LSM6DS Temperature"
- platform: motion
motion_id: lsm6ds_motion
type: acceleration_x
name: "Accel X"
name: "LSM6DS Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
motion_id: lsm6ds_motion
type: acceleration_y
name: "Accel Y"
name: "LSM6DS Accel Y"
accuracy_decimals: 4
- platform: motion
motion_id: lsm6ds_motion
type: acceleration_z
name: "Accel Z"
name: "LSM6DS Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
motion_id: lsm6ds_motion
type: gyroscope_x
name: "Gyro X"
name: "LSM6DS Gyro X"
- platform: motion
motion_id: lsm6ds_motion
type: gyroscope_y
name: "Gyro Y"
name: "LSM6DS Gyro Y"
- platform: motion
motion_id: lsm6ds_motion
type: gyroscope_z
name: "Gyro Z"
name: "LSM6DS Gyro Z"
- platform: motion
motion_id: lsm6ds_motion
type: angular_rate_x
name: "Angular Rate X"
name: "LSM6DS Angular Rate X"
- platform: motion
motion_id: lsm6ds_motion
type: angular_rate_y
name: "Angular Rate Y"
name: "LSM6DS Angular Rate Y"
- platform: motion
motion_id: lsm6ds_motion
type: angular_rate_z
name: "Angular Rate Z"
name: "LSM6DS Angular Rate Z"
- platform: motion
motion_id: lsm6ds_motion
type: pitch
name: "Pitch"
name: "LSM6DS Pitch"
- platform: motion
motion_id: lsm6ds_motion
type: roll
name: "Roll"
name: "LSM6DS Roll"
motion:
- platform: lsm6ds
id: lsm6ds_motion
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
@@ -0,0 +1,7 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
mdns:
@@ -1,7 +1,11 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
- !include ../../test_build_components/common/i2c/esp32-s3-idf.yaml
psram:
mode: octal
<<: !include common.yaml
ch422g:
display:
- platform: mipi_rgb
model: WAVESHARE-5-1024X600
@@ -4,6 +4,181 @@
namespace esphome::modbus::helpers {
using FC = ModbusFunctionCode;
// --- server_frame_length ---------------------------------------------------
// Frame layout: address(1) + function(1) + ... + CRC(2). Fixtures borrowed from
// tests/integration/fixtures/uart_mock_modbus.yaml.
TEST(ModbusServerFrameLength, TooShortReturnsMinimum) {
const uint8_t frame[] = {0x01};
EXPECT_EQ(server_frame_length(frame, 1), MIN_FRAME_SIZE);
}
TEST(ModbusServerFrameLength, ReadHoldingUsesByteCount) {
// inject_rx for basic_register: 2 data bytes -> 5 + 2 = 7
const uint8_t frame[] = {0x01, 0x03, 0x02, 0x01, 0x03, 0xF9, 0xD5};
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 7);
}
TEST(ModbusServerFrameLength, ReadByteCountCappedAtMax) {
const uint8_t frame[] = {0x01, 0x03, 0xFF}; // claim 255 bytes
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 5 + MAX_NUM_OF_REGISTERS_TO_READ * 2);
}
TEST(ModbusServerFrameLength, ReadMissingByteCountReturnsHeaderOnly) {
const uint8_t frame[] = {0x01, 0x03};
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 5);
}
TEST(ModbusServerFrameLength, ExceptionResponse) {
// exception_response fixture: function code 0x83 has the exception bit set
const uint8_t frame[] = {0x01, 0x83, 0x02, 0xC0, 0xF1};
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 5);
}
TEST(ModbusServerFrameLength, WriteResponsesAreFixed) {
for (FC fc :
{FC::WRITE_SINGLE_COIL, FC::WRITE_SINGLE_REGISTER, FC::WRITE_MULTIPLE_COILS, FC::WRITE_MULTIPLE_REGISTERS}) {
const uint8_t frame[] = {0x01, static_cast<uint8_t>(fc)};
EXPECT_EQ(server_frame_length(frame, sizeof(frame)), 8) << "fc=" << static_cast<int>(fc);
}
}
TEST(ModbusServerFrameLength, MiscFixedAndUnknown) {
const uint8_t mask[] = {0x01, static_cast<uint8_t>(FC::MASK_WRITE_REGISTER)};
const uint8_t fifo[] = {0x01, static_cast<uint8_t>(FC::READ_FIFO_QUEUE)};
const uint8_t unknown[] = {0x01, 0x42};
EXPECT_EQ(server_frame_length(mask, sizeof(mask)), 10);
EXPECT_EQ(server_frame_length(fifo, sizeof(fifo)), 6);
EXPECT_EQ(server_frame_length(unknown, sizeof(unknown)), MIN_FRAME_SIZE);
}
// --- client_frame_length ---------------------------------------------------
TEST(ModbusClientFrameLength, TooShortReturnsMinimum) {
const uint8_t frame[] = {0x01};
EXPECT_EQ(client_frame_length(frame, 1), MIN_FRAME_SIZE);
}
TEST(ModbusClientFrameLength, ReadAndWriteSingleAreFixed) {
// basic_register request fixture is a read-holding request -> 8 bytes
const uint8_t read[] = {0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A};
EXPECT_EQ(client_frame_length(read, sizeof(read)), 8);
for (FC fc : {FC::READ_COILS, FC::READ_DISCRETE_INPUTS, FC::READ_INPUT_REGISTERS, FC::WRITE_SINGLE_COIL,
FC::WRITE_SINGLE_REGISTER}) {
const uint8_t frame[] = {0x01, static_cast<uint8_t>(fc)};
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 8) << "fc=" << static_cast<int>(fc);
}
}
TEST(ModbusClientFrameLength, WriteMultipleUsesByteCount) {
// write 2 registers (4 data bytes): addr(2)+qty(2)+count(1) then data; count is frame[6]
const uint8_t frame[] = {0x01, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16};
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9 + 4);
}
TEST(ModbusClientFrameLength, WriteMultipleByteCountCapped) {
const uint8_t frame[] = {0x01, 0x0F, 0x00, 0x00, 0x00, 0x02, 0xFF};
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9 + MAX_NUM_OF_REGISTERS_TO_WRITE * 2);
}
TEST(ModbusClientFrameLength, WriteMultipleMissingByteCount) {
const uint8_t frame[] = {0x01, 0x10, 0x00, 0x00, 0x00, 0x02};
EXPECT_EQ(client_frame_length(frame, sizeof(frame)), 9);
}
TEST(ModbusClientFrameLength, MiscFixedAndUnknown) {
const uint8_t mask[] = {0x01, static_cast<uint8_t>(FC::MASK_WRITE_REGISTER)};
const uint8_t fifo[] = {0x01, static_cast<uint8_t>(FC::READ_FIFO_QUEUE)};
const uint8_t unknown[] = {0x01, 0x42};
EXPECT_EQ(client_frame_length(mask, sizeof(mask)), 10);
EXPECT_EQ(client_frame_length(fifo, sizeof(fifo)), 6);
EXPECT_EQ(client_frame_length(unknown, sizeof(unknown)), MIN_FRAME_SIZE);
}
// --- create_client_pdu -----------------------------------------------------
// PDU = function code + data (no address, no CRC).
TEST(ModbusCreateClientPdu, ReadHolding) {
auto pdu = create_client_pdu(FC::READ_HOLDING_REGISTERS, 0x0003, 1);
const std::vector<uint8_t> expected{0x03, 0x00, 0x03, 0x00, 0x01};
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
}
TEST(ModbusCreateClientPdu, WriteSingleOmitsQuantity) {
const uint8_t values[] = {0x00, 0x0B};
auto pdu = create_client_pdu(FC::WRITE_SINGLE_REGISTER, 0x0003, 1, values, sizeof(values));
const std::vector<uint8_t> expected{0x06, 0x00, 0x03, 0x00, 0x0B};
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
}
TEST(ModbusCreateClientPdu, WriteSingleTooFewValuesReturnsEmpty) {
const uint8_t values[] = {0x00};
auto pdu = create_client_pdu(FC::WRITE_SINGLE_COIL, 0x0003, 1, values, sizeof(values));
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, WriteMultipleIncludesByteCount) {
const uint8_t values[] = {0x00, 0x0B, 0x00, 0x16};
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 2, values, sizeof(values));
const std::vector<uint8_t> expected{0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00, 0x0B, 0x00, 0x16};
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
}
TEST(ModbusCreateClientPdu, WriteMultipleOverCapacityReturnsEmpty) {
std::vector<uint8_t> values(MAX_PDU_SIZE - 6 + 1, 0xAA);
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 1, values.data(), values.size());
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, UnsupportedFunctionCodeReturnsEmpty) {
auto pdu = create_client_pdu(FC::READ_FIFO_QUEUE, 0x0000, 1);
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, ZeroEntitiesReturnsEmpty) {
auto pdu = create_client_pdu(FC::READ_HOLDING_REGISTERS, 0x0000, 0);
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, WriteWithoutValuesReturnsEmpty) {
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, 1, nullptr, 0);
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, ReadHoldingOverMaxReturnsEmpty) {
auto pdu = create_client_pdu(FC::READ_HOLDING_REGISTERS, 0x0000, MAX_NUM_OF_REGISTERS_TO_READ + 1);
EXPECT_TRUE(pdu.empty());
}
// Regression: coils allow up to 2000 entities, well above the 125 register limit.
// A switch fall-through previously subjected coil/discrete reads to the register limit.
TEST(ModbusCreateClientPdu, ReadCoilsAboveRegisterLimitIsValid) {
const uint16_t quantity = MAX_NUM_OF_REGISTERS_TO_READ + 1; // 126: valid for coils, too many for registers
auto pdu = create_client_pdu(FC::READ_COILS, 0x0000, quantity);
const std::vector<uint8_t> expected{0x01, 0x00, 0x00, static_cast<uint8_t>(quantity >> 8),
static_cast<uint8_t>(quantity & 0xFF)};
EXPECT_EQ(std::vector<uint8_t>(pdu.begin(), pdu.end()), expected);
}
TEST(ModbusCreateClientPdu, ReadCoilsOverMaxReturnsEmpty) {
auto pdu = create_client_pdu(FC::READ_COILS, 0x0000, MAX_NUM_OF_COILS_TO_READ + 1);
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, ReadDiscreteInputsOverMaxReturnsEmpty) {
auto pdu = create_client_pdu(FC::READ_DISCRETE_INPUTS, 0x0000, MAX_NUM_OF_DISCRETE_INPUTS_TO_READ + 1);
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusCreateClientPdu, WriteMultipleOverEntityLimitReturnsEmpty) {
const uint8_t values[] = {0x00, 0x0B};
auto pdu = create_client_pdu(FC::WRITE_MULTIPLE_REGISTERS, 0x0000, MAX_NUM_OF_REGISTERS_TO_WRITE + 1, values,
sizeof(values));
EXPECT_TRUE(pdu.empty());
}
TEST(ModbusHelpersTest, PayloadToNumberRejectsOffsetAtEndOfBuffer) {
const std::vector<uint8_t> data{0x12, 0x34};
EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 2, 0xFFFFFFFF), 0);
@@ -19,4 +194,40 @@ TEST(ModbusHelpersTest, PayloadToNumberDecodesValidWord) {
EXPECT_EQ(payload_to_number(data, SensorValueType::U_WORD, 0, 0xFFFFFFFF), 0x1234);
}
// --- registers_to_number ---------------------------------------------------
// Register words are host byte order; results must match the byte-based payload_to_number.
TEST(ModbusHelpersTest, RegistersToNumberDecodesWord) {
const uint16_t registers[] = {0x1234};
EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_WORD), 0x1234);
}
TEST(ModbusHelpersTest, RegistersToNumberDecodesDwordHighWordFirst) {
const uint16_t registers[] = {0x1234, 0x5678};
EXPECT_EQ(registers_to_number(registers, 2, SensorValueType::U_DWORD), 0x12345678);
}
TEST(ModbusHelpersTest, RegistersToNumberDecodesAtSpanStart) {
// The function decodes the value at the start of the span; the caller advances the pointer.
const uint16_t registers[] = {0xAAAA, 0x1234};
EXPECT_EQ(registers_to_number(registers + 1, 1, SensorValueType::U_WORD), 0x1234);
}
TEST(ModbusHelpersTest, RegistersToNumberMatchesPayloadToNumber) {
// Same value via both decoders: registers (host order) vs big-endian bytes.
const uint16_t registers[] = {0x8001, 0x0002};
const std::vector<uint8_t> bytes{0x80, 0x01, 0x00, 0x02};
for (auto value_type : {SensorValueType::S_DWORD, SensorValueType::U_DWORD, SensorValueType::S_DWORD_R}) {
EXPECT_EQ(registers_to_number(registers, 2, value_type), payload_to_number(bytes, value_type, 0, 0xFFFFFFFF))
<< "value_type=" << static_cast<int>(value_type);
}
}
TEST(ModbusHelpersTest, RegistersToNumberRejectsTruncatedMultiRegisterValue) {
const uint16_t registers[] = {0x1234};
bool error = false;
EXPECT_EQ(registers_to_number(registers, 1, SensorValueType::U_DWORD, &error), 0);
EXPECT_TRUE(error);
}
} // namespace esphome::modbus::helpers
-59
View File
@@ -1,59 +0,0 @@
#include <gtest/gtest.h>
#include "esphome/components/modbus/modbus.h"
#include "esphome/core/helpers.h"
namespace esphome::modbus {
// Exposes protected methods for testing.
class TestModbus : public Modbus {
public:
bool test_parse_modbus_byte(uint8_t byte) { return this->parse_modbus_byte_(byte); }
void test_clear_rx_buffer() { this->rx_buffer_.clear(); }
void set_waiting(uint8_t addr) { this->waiting_for_response_ = addr; }
};
class MockDevice : public ModbusDevice {
public:
void on_modbus_data(const std::vector<uint8_t> &data) override { this->data_received = true; }
bool data_received{false};
};
TEST(ModbusTest, TwoByteRegressionTest) {
TestModbus modbus;
modbus.set_role(ModbusRole::CLIENT);
// First byte (at=0)
EXPECT_TRUE(modbus.test_parse_modbus_byte(0x01));
// Second byte (at=1)
// This used to reach raw[2] because it skipped the if(at==2) check, causing a
// buffer overflow.
EXPECT_TRUE(modbus.test_parse_modbus_byte(0x03));
}
TEST(ModbusTest, TestValidFrame) {
TestModbus modbus;
modbus.set_role(ModbusRole::CLIENT);
MockDevice device;
device.set_parent(&modbus);
device.set_address(0x01);
modbus.register_device(&device);
modbus.set_waiting(0x01);
// Address 1, Function 3, Length 2, Data 0x1234
uint8_t frame_data[] = {0x01, 0x03, 0x02, 0x12, 0x34};
uint16_t crc = esphome::crc16(frame_data, sizeof(frame_data));
std::vector<uint8_t> frame;
for (uint8_t b : frame_data)
frame.push_back(b);
frame.push_back(crc & 0xFF);
frame.push_back((crc >> 8) & 0xFF);
for (size_t i = 0; i < frame.size(); i++) {
bool result = modbus.test_parse_modbus_byte(frame[i]);
EXPECT_TRUE(result) << "Failed at byte " << i << " (0x" << std::hex << (int) frame[i] << ")";
}
EXPECT_TRUE(device.data_received);
}
} // namespace esphome::modbus
@@ -18,6 +18,7 @@ modbus_server:
registers:
- address: 0x9
value_type: S_DWORD
allow_partial_read: true
read_lambda: |-
return 31;
write_lambda: |-
@@ -0,0 +1,285 @@
#include <gtest/gtest.h>
#include "esphome/components/modbus_server/modbus_server.h"
namespace esphome::modbus_server {
using modbus::ModbusExceptionCode;
using modbus::RegisterValues;
namespace {
RegisterValues make_registers(std::initializer_list<uint16_t> values) {
RegisterValues registers;
for (uint16_t value : values)
registers.push_back(value);
return registers;
}
} // namespace
// A single writable WORD register is applied and the handler reports success (nullopt).
TEST(ModbusServerWrite, SingleWordSucceeds) {
ModbusServer server;
int64_t written = -1;
ServerRegister reg(0x0000, SensorValueType::U_WORD, 1);
reg.write_lambda = [&written](int64_t value) {
written = value;
return true;
};
server.add_server_register(&reg);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234}));
EXPECT_FALSE(status.has_value()); // nullopt == success
EXPECT_EQ(written, 0x1234);
}
// A multi-register value is decoded high word first and applied as a single number.
TEST(ModbusServerWrite, DwordSucceeds) {
ModbusServer server;
int64_t written = -1;
ServerRegister reg(0x0000, SensorValueType::U_DWORD, 2);
reg.write_lambda = [&written](int64_t value) {
written = value;
return true;
};
server.add_server_register(&reg);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234, 0x5678}));
EXPECT_FALSE(status.has_value());
EXPECT_EQ(written, 0x12345678);
}
// Regression: a request that under-supplies a multi-register value is rejected before any
// write_lambda runs, so no register is partially written.
TEST(ModbusServerWrite, UnderSuppliedValueAppliesNothing) {
ModbusServer server;
bool word_written = false;
ServerRegister word_reg(0x0000, SensorValueType::U_WORD, 1);
word_reg.write_lambda = [&word_written](int64_t) {
word_written = true;
return true;
};
bool dword_written = false;
ServerRegister dword_reg(0x0001, SensorValueType::U_DWORD, 2); // needs two registers
dword_reg.write_lambda = [&dword_written](int64_t) {
dword_written = true;
return true;
};
server.add_server_register(&word_reg);
server.add_server_register(&dword_reg);
// Two words supplied: one for the WORD at 0x0000, but only one of the two the DWORD at 0x0001 needs.
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1111, 0x2222}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_VALUE);
EXPECT_FALSE(word_written); // the writable WORD must NOT have been applied
EXPECT_FALSE(dword_written);
}
// A read-only register (no write_lambda) yields ILLEGAL_DATA_ADDRESS and applies nothing.
TEST(ModbusServerWrite, UnwritableRegisterRejected) {
ModbusServer server;
ServerRegister read_only(0x0000, SensorValueType::U_WORD, 1); // no write_lambda set
server.add_server_register(&read_only);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0x1234}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
}
// An address with no registered register yields ILLEGAL_DATA_ADDRESS.
TEST(ModbusServerWrite, UnmatchedAddressRejected) {
ModbusServer server;
auto status = server.on_modbus_write_registers(0x0005, make_registers({0x1234}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
}
// A write_lambda failing at runtime is the one non-atomic case: the earlier register is already
// applied, and the handler reports SERVICE_DEVICE_FAILURE.
TEST(ModbusServerWrite, CallbackFailureIsServiceDeviceFailure) {
ModbusServer server;
bool first_written = false;
ServerRegister first(0x0000, SensorValueType::U_WORD, 1);
first.write_lambda = [&first_written](int64_t) {
first_written = true;
return true;
};
ServerRegister second(0x0001, SensorValueType::U_WORD, 1);
second.write_lambda = [](int64_t) { return false; }; // rejects at runtime
server.add_server_register(&first);
server.add_server_register(&second);
auto status = server.on_modbus_write_registers(0x0000, make_registers({0xAAAA, 0xBBBB}));
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
EXPECT_TRUE(first_written); // pre-validation passed, so the first write applied before the failure
}
// --- on_modbus_read_registers --------------------------------------------------
TEST(ModbusServerRead, SingleWordSucceeds) {
ModbusServer server;
ServerRegister reg(0x0000, SensorValueType::U_WORD, 1);
reg.read_lambda = []() -> int64_t { return 0x1234; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x1234);
}
TEST(ModbusServerRead, DwordReturnsTwoWordsHighFirst) {
ModbusServer server;
ServerRegister reg(0x0000, SensorValueType::U_DWORD, 2);
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 2, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0], 0x1234);
EXPECT_EQ(out[1], 0x5678);
}
// Starting inside a multi-register value is rejected with ILLEGAL_DATA_ADDRESS -- not masked by the courtesy
// default -- and the read_lambda is never invoked.
TEST(ModbusServerRead, StartInsideValueRejected) {
ModbusServer server;
bool read_called = false;
ServerRegister reg(0x0010, SensorValueType::U_DWORD, 2); // occupies 0x0010 and 0x0011
reg.read_lambda = [&read_called]() -> int64_t {
read_called = true;
return 0;
};
server.set_server_courtesy_response(
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0011, 1, out); // the second cell of the DWORD
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
EXPECT_FALSE(read_called);
}
// A read that stops short of a value's end clips it -> ILLEGAL_DATA_ADDRESS, and the read_lambda is not invoked.
TEST(ModbusServerRead, ClippedTailRejected) {
ModbusServer server;
bool read_called = false;
ServerRegister reg(0x0000, SensorValueType::U_DWORD, 2);
reg.read_lambda = [&read_called]() -> int64_t {
read_called = true;
return 0;
};
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out); // only 1 of the DWORD's 2 registers
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
EXPECT_FALSE(read_called);
}
// A write-only register (no read_lambda) is not readable -> ILLEGAL_DATA_ADDRESS, not a courtesy default.
TEST(ModbusServerRead, WriteOnlyRegisterRejected) {
ModbusServer server;
ServerRegister reg(0x0000, SensorValueType::U_WORD, 1); // no read_lambda set
server.set_server_courtesy_response(
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0000, 1, out);
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
}
// An unregistered address with courtesy enabled returns the default value for each cell.
TEST(ModbusServerRead, CourtesyDefaultForUnregistered) {
ModbusServer server;
server.set_server_courtesy_response(
ServerCourtesyResponse{.enabled = true, .register_last_address = 0xFFFF, .register_value = 0xABCD});
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0005, 2, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 2u);
EXPECT_EQ(out[0], 0xABCD);
EXPECT_EQ(out[1], 0xABCD);
}
// An unregistered address with courtesy disabled is rejected.
TEST(ModbusServerRead, UnregisteredRejectedWithoutCourtesy) {
ModbusServer server;
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0005, 1, out);
ASSERT_TRUE(status.has_value());
if (status.has_value())
EXPECT_EQ(status.value(), ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
}
// --- partial reads (opt-in) ----------------------------------------------------
// With allow_partial_read, reading only the first register of a DWORD returns its high word.
TEST(ModbusServerRead, PartialReadHighWord) {
ModbusServer server;
ServerRegister reg(0x0010, SensorValueType::U_DWORD, 2);
reg.allow_partial_read = true;
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0010, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x1234);
}
// With allow_partial_read, starting at the interior cell returns the low word.
TEST(ModbusServerRead, PartialReadLowWordFromInterior) {
ModbusServer server;
ServerRegister reg(0x0010, SensorValueType::U_DWORD, 2);
reg.allow_partial_read = true;
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues out;
auto status = server.on_modbus_read_registers(0x0011, 1, out);
EXPECT_FALSE(status.has_value());
ASSERT_EQ(out.size(), 1u);
EXPECT_EQ(out[0], 0x5678);
}
// Slicing is in wire order, so a reversed value type partials correctly: U_DWORD_R emits the low word
// first, so 0x0010 holds 0x5678 and 0x0011 holds 0x1234.
TEST(ModbusServerRead, PartialReadReversedType) {
ModbusServer server;
ServerRegister reg(0x0010, SensorValueType::U_DWORD_R, 2);
reg.allow_partial_read = true;
reg.read_lambda = []() -> int64_t { return 0x12345678; };
server.add_server_register(&reg);
RegisterValues first;
ASSERT_FALSE(server.on_modbus_read_registers(0x0010, 1, first).has_value());
ASSERT_EQ(first.size(), 1u);
EXPECT_EQ(first[0], 0x5678);
RegisterValues second;
ASSERT_FALSE(server.on_modbus_read_registers(0x0011, 1, second).has_value());
ASSERT_EQ(second.size(), 1u);
EXPECT_EQ(second[0], 0x1234);
}
} // namespace esphome::modbus_server
@@ -0,0 +1,2 @@
packages:
common: !include common.yaml
@@ -1 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
@@ -1 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
@@ -1 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
@@ -19,5 +19,3 @@ nrf52:
reg0:
voltage: 2.1V
uicr_erase: true
framework:
version: "2.6.1-b"
+2
View File
@@ -0,0 +1,2 @@
network:
enable_ipv6: true
@@ -0,0 +1,20 @@
<<: !include common.yaml
openthread:
device_type: MTD
force_dataset: false
use_address: open-thread-test.local
tlv: 0e080000000000010000000300001035060004001fffe00208e227ac6a7f24052f0708fdb753eb517cb4d3051062b2442a928d9ea3b947a1618fc4085a030f4f70656e5468726561642d393837330102987304105330d857354330133c05e1fd7ae81a910c0402a0f7f8
poll_period: 5s
switch:
- platform: template
name: "Radio Always On"
optimistic: true
restore_mode: ALWAYS_OFF
turn_on_action:
then:
- openthread.set_poll_period: 0s
turn_off_action:
then:
- openthread.set_poll_period: 5s
@@ -1,14 +1,6 @@
esp32:
board: esp32-c6-devkitc-1
framework:
type: esp-idf
log_level: DEBUG
network:
enable_ipv6: true
<<: !include common.yaml
openthread:
device_type: MTD
channel: 13
network_name: OpenThread-8f28
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
@@ -16,7 +8,4 @@ openthread:
ext_pan_id: 0xd63e8e3e495ebbc3
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
mesh_local_prefix: fd53:145f:ed22:ad81::/64
force_dataset: true
use_address: open-thread-test.local
poll_period: 20sec
output_power: 1dBm
@@ -0,0 +1,5 @@
network:
enable_ipv6: true
openthread:
tlv: 0E080000000000010000
+26
View File
@@ -0,0 +1,26 @@
display:
- platform: pixoo
id: pixoo_display
model: 64x64
cs_pin: GPIO5
data_rate: 10MHz
update_interval: 1s
lambda: |-
it.fill(Color(0, 0, 0));
it.filled_rectangle(0, 0, 16, 16, Color(255, 0, 0));
it.line(0, 0, 63, 63, Color(0, 255, 0));
- platform: pixoo
id: pixoo_display_pages
model: 64x64
cs_pin: GPIO21
rotation: 90
pages:
- id: pixoo_page
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height(), Color(0, 0, 255));
light:
- platform: pixoo
pixoo_id: pixoo_display
name: Pixoo Brightness
@@ -0,0 +1,4 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,47 @@
#include <gtest/gtest.h>
#include "esphome/components/power_supply/power_supply.h"
#include "esphome/core/gpio.h"
#include "esphome/core/component.h"
namespace esphome::power_supply::testing {
// Minimal dummy internal GPIO pin implementation for testing
class DummyInternalPin : public InternalGPIOPin {
public:
DummyInternalPin() = default;
void setup() override {}
void pin_mode(esphome::gpio::Flags) override {}
esphome::gpio::Flags get_flags() const override { return esphome::gpio::FLAG_NONE; }
bool digital_read() override { return false; }
void digital_write(bool) override {}
void detach_interrupt() const override {}
ISRInternalGPIOPin to_isr() const override { return ISRInternalGPIOPin(); }
uint8_t get_pin() const override { return 0; }
bool is_inverted() const override { return false; }
protected:
// Implement protected attach_interrupt required by InternalGPIOPin
void attach_interrupt(void (*func)(void *), void *arg, esphome::gpio::InterruptType type) const override {}
};
TEST(PowerSupply, HasHigherPriorityThanBusWhenInternalAndEnableOnBoot) {
power_supply::PowerSupply ps;
DummyInternalPin pin;
ps.set_pin(&pin);
ps.set_enable_on_boot(true);
// POWER priority should be greater than BUS priority
EXPECT_GT(ps.get_setup_priority(), setup_priority::BUS);
}
TEST(PowerSupply, FallsBackToIOWhenNotEnableOnBoot) {
power_supply::PowerSupply ps;
DummyInternalPin pin;
ps.set_pin(&pin);
ps.set_enable_on_boot(false);
EXPECT_EQ(ps.get_setup_priority(), setup_priority::IO);
}
} // namespace esphome::power_supply::testing
@@ -0,0 +1,5 @@
# Config-only: the ESP32-S3 supports both quad and octal. The compile test uses
# octal; this exercises the other branch of the per-variant mode enum (quad) and
# lets speed fall back to its 40MHz default.
psram:
mode: quad
@@ -0,0 +1,4 @@
# Config-only: with no options the single-mode ESP32 resolves mode -> quad and
# speed -> 40MHz from the per-variant defaults. Compiling adds no signal here,
# so this only runs through `esphome config`.
psram:
@@ -0,0 +1,4 @@
# Config-only: the ESP32-P4 has a distinct value set (hex mode, 20/100/200MHz).
# With no options it resolves mode -> hex and speed -> 20MHz, exercising the
# P4-specific default branch of the per-variant enums.
psram:
+70
View File
@@ -0,0 +1,70 @@
sensor:
- platform: qmi8658
name: "QMI8658 Temperature"
- platform: motion
type: acceleration_x
name: "Accel X"
accuracy_decimals: 4
filters:
- sliding_window_moving_average:
window_size: 4
send_every: 1
- platform: motion
type: acceleration_y
name: "Accel Y"
accuracy_decimals: 4
- platform: motion
type: acceleration_z
name: "Accel Z"
accuracy_decimals: 4
# Gyroscope axes (unit: °/s)
- platform: motion
type: gyroscope_x
name: "Gyro X"
- platform: motion
type: gyroscope_y
name: "Gyro Y"
- platform: motion
type: gyroscope_z
name: "Gyro Z"
- platform: motion
type: angular_rate_x
name: "Angular Rate X"
- platform: motion
type: angular_rate_y
name: "Angular Rate Y"
- platform: motion
type: angular_rate_z
name: "Angular Rate Z"
- platform: motion
type: pitch
name: "Pitch"
- platform: motion
type: roll
name: "Roll"
motion:
- platform: qmi8658
i2c_id: i2c_bus
# Accelerometer full-scale range: 2G | 4G | 8G | 16G
accelerometer_range: 4G
# Accelerometer output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
accelerometer_odr: 1000HZ
# Gyroscope full-scale range: 16DPS | 32DPS | 64DPS | 128DPS |
# 256DPS | 512DPS | 1024DPS | 2048DPS
gyroscope_range: 2048DPS
# Gyroscope output data rate: 31_25HZ | 62_5HZ | 125HZ | 250HZ |
# 500HZ | 1000HZ | 2000HZ | 4000HZ | 8000HZ
gyroscope_odr: 1000HZ
axis_map:
x: y
y: x
z: -z
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
<<: !include common.yaml
+5
View File
@@ -7,3 +7,8 @@ speaker:
- platform: resampler
id: resampler_speaker_id
output_speaker: resampler_i2s_speaker_id
bits_per_sample: 16
- platform: resampler
id: resampler_speaker_2_id
output_speaker: resampler_speaker_id
bits_per_sample: passthrough
@@ -1,4 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_19200: !include ../../test_build_components/common/uart_19200/esp32-idf.yaml
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_19200: !include ../../test_build_components/common/uart_19200/esp8266-ard.yaml
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_19200: !include ../../test_build_components/common/uart_19200/rp2040-ard.yaml
<<: !include common.yaml
@@ -0,0 +1 @@
socket:
@@ -0,0 +1 @@
socket:
@@ -0,0 +1 @@
socket:
+18
View File
@@ -0,0 +1,18 @@
display:
- platform: ssd1306_i2c
i2c_id: i2c_bus
id: st7123_ssd1306_i2c_display
model: SSD1306_128X64
reset_pin: ${display_reset_pin}
pages:
- id: st7123_page1
lambda: |-
it.rectangle(0, 0, it.get_width(), it.get_height());
touchscreen:
- platform: st7123
i2c_id: i2c_bus
id: st7123_touchscreen
display: st7123_ssd1306_i2c_display
interrupt_pin: ${interrupt_pin}
reset_pin: ${reset_pin}
@@ -0,0 +1,9 @@
substitutions:
display_reset_pin: "10"
interrupt_pin: "20"
reset_pin: "21"
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
+2
View File
@@ -21,6 +21,8 @@ sx126x:
coding_rate: CR_4_6
tcxo_voltage: 1_8V
tcxo_delay: 5ms
whitening_enable: false
whitening_initial: 0x1FF
on_packet:
then:
- lambda: |-
+30
View File
@@ -0,0 +1,30 @@
ufm01:
id: ufm01_component
uart_id: uart_bus
sensor:
- platform: ufm01
accumulated_flow:
id: accumulated_flow
name: "Accumulated flow"
flow:
id: flow
name: "Flow"
temperature:
id: temperature
name: "Temperature"
binary_sensor:
- platform: ufm01
ufc_chip_error:
id: ufc_chip_error
name: "UFC chip error"
flow_direction_wrong:
id: flow_direction_wrong
name: "Flow direction wrong"
empty_tube:
id: empty_tube
name: "Empty tube"
flow_rate_out_of_range:
id: flow_rate_out_of_range
name: "Flow rate out of range"
@@ -0,0 +1,4 @@
packages:
uart_2400_even: !include ../../test_build_components/common/uart_2400_even/esp32-idf.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
uart_2400_even: !include ../../test_build_components/common/uart_2400_even/esp8266-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,4 @@
packages:
uart_2400_even: !include ../../test_build_components/common/uart_2400_even/rp2040-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,34 @@
waveshare_io_ch32v003:
- id: wave_io
i2c_id: i2c_bus
address: 0x24
binary_sensor:
- platform: gpio
id: wave_io_binary_sensor
pin:
waveshare_io_ch32v003: wave_io
number: 3
mode: INPUT
inverted: false
output:
- platform: gpio
id: wave_io_output
pin:
waveshare_io_ch32v003: wave_io
number: 0
mode: OUTPUT
inverted: false
- platform: waveshare_io_ch32v003
id: wave_io_pwm_output
inverted: true
zero_means_zero: true
safe_pwm_levels:
min_value: 0
max_value: 247
sensor:
- platform: waveshare_io_ch32v003
id: wave_io_adc
@@ -0,0 +1,4 @@
packages:
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
+5 -1
View File
@@ -1,15 +1,19 @@
psram:
# Tests the high performance request and release; requires the USE_WIFI_RUNTIME_POWER_SAVE define
# Tests the high performance and roaming suppression request/release APIs;
# requires the USE_WIFI_RUNTIME_POWER_SAVE and USE_WIFI_RUNTIME_ROAMING_SUPPRESSION defines
esphome:
platformio_options:
build_flags:
- "-DUSE_WIFI_RUNTIME_POWER_SAVE"
- "-DUSE_WIFI_RUNTIME_ROAMING_SUPPRESSION"
on_boot:
- then:
- lambda: |-
esphome::wifi::global_wifi_component->request_high_performance();
esphome::wifi::global_wifi_component->release_high_performance();
esphome::wifi::global_wifi_component->request_roaming_suppression();
esphome::wifi::global_wifi_component->release_roaming_suppression();
wifi:
use_psram: true
+1 -1
View File
@@ -4,7 +4,7 @@ packages:
binary_sensor:
- platform: template
name: "Garage Door Open 10"
report: "enable"
report: "default"
- platform: template
name: "Garage Door Open 12"
report: "force"