mirror of
https://github.com/esphome/esphome.git
synced 2026-10-07 03:16:37 +00:00
Merge branch 'dev' into fan-store-preset-modes-on-entity
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import esphome.codegen as cg
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
@@ -5,3 +6,16 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# api must run its to_code to define USE_API, USE_API_PLAINTEXT,
|
||||
# and add the noise-c library dependency.
|
||||
manifest.enable_codegen()
|
||||
|
||||
original_to_code = manifest.to_code
|
||||
|
||||
async def to_code(config):
|
||||
await original_to_code(config)
|
||||
# Enable BLE proto message types for benchmarks. The real
|
||||
# bluetooth_proxy component is ESP32-only; a lightweight stub
|
||||
# header in tests/benchmarks/stubs/ satisfies the include.
|
||||
cg.add_define("USE_BLUETOOTH_PROXY")
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 3)
|
||||
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
|
||||
|
||||
manifest.to_code = to_code
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#include "esphome/components/socket/socket.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
// Helper to drain accumulated data from the read side of a socket
|
||||
// to prevent the write side from blocking.
|
||||
inline void drain_socket(int fd) {
|
||||
char buf[65536];
|
||||
while (::read(fd, buf, sizeof(buf)) > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
// Create a TCP loopback socket pair. Returns the write-side Socket
|
||||
// (wrapped for ESPHome) and the raw read-side fd for draining.
|
||||
// Both ends are non-blocking with 16MB buffers.
|
||||
inline std::pair<std::unique_ptr<socket::Socket>, int> create_tcp_loopback() {
|
||||
// Create a TCP listener on loopback
|
||||
int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
int opt = 1;
|
||||
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
struct sockaddr_in addr {};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0; // OS-assigned port
|
||||
::bind(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(listen_fd, 1);
|
||||
|
||||
// Get the assigned port
|
||||
socklen_t addr_len = sizeof(addr);
|
||||
::getsockname(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len);
|
||||
|
||||
// Connect from client side
|
||||
int write_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
::connect(write_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
|
||||
|
||||
// Accept on server side (this is our read fd)
|
||||
int read_fd = ::accept(listen_fd, nullptr, nullptr);
|
||||
::close(listen_fd);
|
||||
|
||||
// Make both ends non-blocking
|
||||
int flags = ::fcntl(write_fd, F_GETFL, 0);
|
||||
::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
flags = ::fcntl(read_fd, F_GETFL, 0);
|
||||
::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Use large socket buffers so benchmarks never hit WOULD_BLOCK
|
||||
// during a single outer iteration (2000 × ~15B messages = ~30KB).
|
||||
int bufsize = 16 * 1024 * 1024;
|
||||
::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
|
||||
::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
|
||||
|
||||
return {std::make_unique<socket::Socket>(write_fd), read_fd};
|
||||
}
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -0,0 +1,235 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
#include "esphome/components/light/color_mode.h"
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- ListEntitiesSensorResponse ---
|
||||
|
||||
static ListEntitiesSensorResponse make_sensor_response() {
|
||||
ListEntitiesSensorResponse msg;
|
||||
msg.object_id = StringRef::from_lit("living_room_temperature");
|
||||
msg.key = 0x12345678;
|
||||
msg.name = StringRef::from_lit("Living Room Temperature");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = StringRef::from_lit("mdi:thermometer");
|
||||
#endif
|
||||
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
|
||||
msg.disabled_by_default = false;
|
||||
msg.unit_of_measurement = StringRef::from_lit("°C");
|
||||
msg.accuracy_decimals = 1;
|
||||
msg.force_update = false;
|
||||
msg.device_class = StringRef::from_lit("temperature");
|
||||
msg.state_class = enums::STATE_CLASS_MEASUREMENT;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = 1;
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_ListEntitiesSensorResponse);
|
||||
|
||||
static void Encode_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_ListEntitiesSensorResponse);
|
||||
|
||||
static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_ListEntitiesSensorResponse);
|
||||
|
||||
// --- ListEntitiesBinarySensorResponse ---
|
||||
|
||||
static ListEntitiesBinarySensorResponse make_binary_sensor_response() {
|
||||
ListEntitiesBinarySensorResponse msg;
|
||||
msg.object_id = StringRef::from_lit("front_door_contact");
|
||||
msg.key = 0xAABBCCDD;
|
||||
msg.name = StringRef::from_lit("Front Door Contact");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = StringRef::from_lit("mdi:door");
|
||||
#endif
|
||||
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
|
||||
msg.disabled_by_default = false;
|
||||
msg.device_class = StringRef::from_lit("door");
|
||||
msg.is_status_binary_sensor = false;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = 2;
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_ListEntitiesBinarySensorResponse);
|
||||
|
||||
static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_ListEntitiesBinarySensorResponse);
|
||||
|
||||
static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_ListEntitiesBinarySensorResponse);
|
||||
|
||||
// --- ListEntitiesLightResponse ---
|
||||
|
||||
static light::ColorModeMask light_color_modes;
|
||||
static FixedVector<const char *> light_effects;
|
||||
|
||||
static ListEntitiesLightResponse make_light_response() {
|
||||
// Initialize static data on first call
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
light_color_modes.insert(light::ColorMode::RGB_WHITE);
|
||||
light_color_modes.insert(light::ColorMode::COLOR_TEMPERATURE);
|
||||
light_effects.init(3);
|
||||
light_effects.push_back("None");
|
||||
light_effects.push_back("Rainbow");
|
||||
light_effects.push_back("Strobe");
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
ListEntitiesLightResponse msg;
|
||||
msg.object_id = StringRef::from_lit("kitchen_ceiling_light");
|
||||
msg.key = 0x55667788;
|
||||
msg.name = StringRef::from_lit("Kitchen Ceiling Light");
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = StringRef::from_lit("mdi:ceiling-light");
|
||||
#endif
|
||||
msg.entity_category = enums::ENTITY_CATEGORY_NONE;
|
||||
msg.disabled_by_default = false;
|
||||
msg.supported_color_modes = &light_color_modes;
|
||||
msg.min_mireds = 153.0f;
|
||||
msg.max_mireds = 500.0f;
|
||||
msg.effects = &light_effects;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = 3;
|
||||
#endif
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_ListEntitiesLightResponse);
|
||||
|
||||
static void Encode_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_ListEntitiesLightResponse);
|
||||
|
||||
static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_ListEntitiesLightResponse);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
@@ -2,12 +2,9 @@
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <fcntl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "bench_helpers.h"
|
||||
#include "esphome/components/api/api_frame_helper_plaintext.h"
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
@@ -16,57 +13,12 @@ namespace esphome::api::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Helper to drain accumulated data from the read side of a socket
|
||||
// to prevent the write side from blocking.
|
||||
static void drain_socket(int fd) {
|
||||
char buf[65536];
|
||||
while (::read(fd, buf, sizeof(buf)) > 0) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to create a TCP loopback connection with an APIPlaintextFrameHelper
|
||||
// on the write end. Returns the helper and the read-side fd.
|
||||
// Uses real TCP sockets so TCP_NODELAY succeeds during init().
|
||||
static std::pair<std::unique_ptr<APIPlaintextFrameHelper>, int> create_plaintext_helper() {
|
||||
// Create a TCP listener on loopback
|
||||
int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
int opt = 1;
|
||||
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
struct sockaddr_in addr {};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
addr.sin_port = 0; // OS-assigned port
|
||||
::bind(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
|
||||
::listen(listen_fd, 1);
|
||||
|
||||
// Get the assigned port
|
||||
socklen_t addr_len = sizeof(addr);
|
||||
::getsockname(listen_fd, reinterpret_cast<struct sockaddr *>(&addr), &addr_len);
|
||||
|
||||
// Connect from client side
|
||||
int write_fd = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||
::connect(write_fd, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr));
|
||||
|
||||
// Accept on server side (this is our read fd)
|
||||
int read_fd = ::accept(listen_fd, nullptr, nullptr);
|
||||
::close(listen_fd);
|
||||
|
||||
// Make both ends non-blocking
|
||||
int flags = ::fcntl(write_fd, F_GETFL, 0);
|
||||
::fcntl(write_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
flags = ::fcntl(read_fd, F_GETFL, 0);
|
||||
::fcntl(read_fd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Increase socket buffer sizes to reduce drain frequency
|
||||
int bufsize = 1024 * 1024;
|
||||
::setsockopt(write_fd, SOL_SOCKET, SO_SNDBUF, &bufsize, sizeof(bufsize));
|
||||
::setsockopt(read_fd, SOL_SOCKET, SO_RCVBUF, &bufsize, sizeof(bufsize));
|
||||
|
||||
auto sock = std::make_unique<socket::Socket>(write_fd);
|
||||
auto [sock, read_fd] = create_tcp_loopback();
|
||||
auto helper = std::make_unique<APIPlaintextFrameHelper>(std::move(sock));
|
||||
helper->init();
|
||||
|
||||
return {std::move(helper), read_fd};
|
||||
}
|
||||
|
||||
@@ -97,9 +49,6 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) {
|
||||
msg.encode(writer);
|
||||
|
||||
helper->write_protobuf_packet(SensorStateResponse::MESSAGE_TYPE, writer);
|
||||
|
||||
if ((i & 0xFF) == 0)
|
||||
drain_socket(read_fd);
|
||||
}
|
||||
drain_socket(read_fd);
|
||||
benchmark::DoNotOptimize(helper.get());
|
||||
@@ -144,9 +93,6 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) {
|
||||
}
|
||||
|
||||
helper->write_protobuf_messages(ProtoWriteBuffer(&buffer, 0), std::span<const MessageInfo>(messages, 5));
|
||||
|
||||
if ((i & 0xFF) == 0)
|
||||
drain_socket(read_fd);
|
||||
}
|
||||
drain_socket(read_fd);
|
||||
benchmark::DoNotOptimize(helper.get());
|
||||
|
||||
@@ -10,10 +10,9 @@ namespace esphome::api::benchmarks {
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Helper: encode a message into a buffer and return it.
|
||||
// Benchmarks encode once in setup, then decode the resulting bytes in a loop.
|
||||
// This keeps decode benchmarks in sync with the actual protobuf schema —
|
||||
// hand-encoded byte arrays would silently break when fields change.
|
||||
// Helper: encode a message into an APIBuffer for reuse in decode benchmarks.
|
||||
// Optimization barriers are applied to the decode target objects via
|
||||
// DoNotOptimize/ClobberMemory, not to this buffer.
|
||||
template<typename T> static APIBuffer encode_message(const T &msg) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
@@ -23,6 +22,12 @@ template<typename T> static APIBuffer encode_message(const T &msg) {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// Force a pointer through an asm barrier so the compiler cannot
|
||||
/// prove its contents are unchanged across iterations.
|
||||
/// benchmark::DoNotOptimize/ClobberMemory are insufficient under
|
||||
/// CodSpeed's valgrind-based instrumentation.
|
||||
static void escape(void *p) { asm volatile("" : : "g"(p) : "memory"); }
|
||||
|
||||
// --- HelloRequest decode (string + varint fields) ---
|
||||
|
||||
static void Decode_HelloRequest(benchmark::State &state) {
|
||||
@@ -31,13 +36,18 @@ static void Decode_HelloRequest(benchmark::State &state) {
|
||||
source.api_version_major = 1;
|
||||
source.api_version_minor = 10;
|
||||
auto encoded = encode_message(source);
|
||||
auto *data = encoded.data();
|
||||
auto size = encoded.size();
|
||||
benchmark::DoNotOptimize(data);
|
||||
benchmark::DoNotOptimize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
HelloRequest msg;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
msg.decode(encoded.data(), encoded.size());
|
||||
HelloRequest msg;
|
||||
escape(&msg);
|
||||
msg.decode(data, size);
|
||||
escape(&msg);
|
||||
}
|
||||
benchmark::DoNotOptimize(msg.api_version_major);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
@@ -50,13 +60,18 @@ static void Decode_SwitchCommandRequest(benchmark::State &state) {
|
||||
source.key = 0x12345678;
|
||||
source.state = true;
|
||||
auto encoded = encode_message(source);
|
||||
auto *data = encoded.data();
|
||||
auto size = encoded.size();
|
||||
benchmark::DoNotOptimize(data);
|
||||
benchmark::DoNotOptimize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
SwitchCommandRequest msg;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
msg.decode(encoded.data(), encoded.size());
|
||||
SwitchCommandRequest msg;
|
||||
escape(&msg);
|
||||
msg.decode(data, size);
|
||||
escape(&msg);
|
||||
}
|
||||
benchmark::DoNotOptimize(msg.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
@@ -78,13 +93,18 @@ static void Decode_LightCommandRequest(benchmark::State &state) {
|
||||
source.has_effect = true;
|
||||
source.effect = StringRef::from_lit("rainbow");
|
||||
auto encoded = encode_message(source);
|
||||
auto *data = encoded.data();
|
||||
auto size = encoded.size();
|
||||
benchmark::DoNotOptimize(data);
|
||||
benchmark::DoNotOptimize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
LightCommandRequest msg;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
msg.decode(encoded.data(), encoded.size());
|
||||
LightCommandRequest msg;
|
||||
escape(&msg);
|
||||
msg.decode(data, size);
|
||||
escape(&msg);
|
||||
}
|
||||
benchmark::DoNotOptimize(msg.brightness);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
|
||||
@@ -295,4 +295,93 @@ static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_DeviceInfoResponse_Fresh);
|
||||
|
||||
// --- BluetoothLERawAdvertisementsResponse (12 adverts, highest-volume BLE message) ---
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
|
||||
static BluetoothLERawAdvertisementsResponse make_ble_raw_advs_12() {
|
||||
static const uint8_t fake_adv_data[] = {
|
||||
0x02, 0x01, 0x06, 0x03, 0x03, 0x9F, 0xFE, 0x17, 0x16, 0x9F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
BluetoothLERawAdvertisementsResponse msg;
|
||||
msg.advertisements_len = 12;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
auto &adv = msg.advertisements[i];
|
||||
adv.address = 0xAABBCCDD0000ULL + i;
|
||||
adv.rssi = -60 - i;
|
||||
adv.address_type = 1;
|
||||
memcpy(adv.data, fake_adv_data, sizeof(fake_adv_data));
|
||||
adv.data_len = sizeof(fake_adv_data);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
static void CalculateSize_BLERawAdvs12(benchmark::State &state) {
|
||||
auto msg = make_ble_raw_advs_12();
|
||||
|
||||
for (auto _ : state) {
|
||||
uint32_t result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += msg.calculate_size();
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalculateSize_BLERawAdvs12);
|
||||
|
||||
static void Encode_BLERawAdvs12(benchmark::State &state) {
|
||||
auto msg = make_ble_raw_advs_12();
|
||||
APIBuffer buffer;
|
||||
uint32_t total_size = msg.calculate_size();
|
||||
buffer.resize(total_size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Encode_BLERawAdvs12);
|
||||
|
||||
static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) {
|
||||
auto msg = make_ble_raw_advs_12();
|
||||
APIBuffer buffer;
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_BLERawAdvs12);
|
||||
|
||||
static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) {
|
||||
auto msg = make_ble_raw_advs_12();
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
}
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(CalcAndEncode_BLERawAdvs12_Fresh);
|
||||
|
||||
#endif // USE_BLUETOOTH_PROXY
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_SENSOR)
|
||||
|
||||
#include <benchmark/benchmark.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "bench_helpers.h"
|
||||
#include "esphome/components/api/api_connection.h"
|
||||
#include "esphome/components/api/api_server.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Friend functions declared in APIConnection for benchmark access.
|
||||
void bench_enable_immediate_send(APIConnection *conn) { conn->flags_.should_try_send_immediately = true; }
|
||||
void bench_clear_batch(APIConnection *conn) { conn->clear_batch_(); }
|
||||
void bench_process_batch(APIConnection *conn) { conn->process_batch_(); }
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
namespace esphome::api::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Helper to create a TCP loopback connection with an APIConnection.
|
||||
// Returns the connection and the read-side fd for draining.
|
||||
static std::pair<std::unique_ptr<APIConnection>, int> create_api_connection() {
|
||||
auto [sock, read_fd] = create_tcp_loopback();
|
||||
auto conn = std::make_unique<APIConnection>(std::move(sock), global_api_server);
|
||||
conn->start();
|
||||
return {std::move(conn), read_fd};
|
||||
}
|
||||
|
||||
// Test subclass to access protected configure_entity_() for benchmark setup.
|
||||
class TestSensor : public sensor::Sensor {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
};
|
||||
|
||||
// --- send_sensor_state: immediate send path ---
|
||||
// Measures: send_message_smart_ → prepare buffer → dispatch_message_ →
|
||||
// try_send_sensor_state → fill key/device_id + proto encode → frame write →
|
||||
// TCP send. This is the per-client cost when batch_delay=0 and initial states
|
||||
// have been sent.
|
||||
|
||||
static void SendSensorState_Immediate(benchmark::State &state) {
|
||||
auto [conn, read_fd] = create_api_connection();
|
||||
bench_enable_immediate_send(conn.get());
|
||||
// batch_delay must be 0 for should_send_immediately_ to return true
|
||||
uint16_t saved_delay = global_api_server->get_batch_delay();
|
||||
global_api_server->set_batch_delay(0);
|
||||
|
||||
TestSensor sensor;
|
||||
sensor.configure("test_sensor");
|
||||
sensor.publish_state(23.5f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->send_sensor_state(&sensor);
|
||||
}
|
||||
drain_socket(read_fd);
|
||||
benchmark::DoNotOptimize(conn.get());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
|
||||
global_api_server->set_batch_delay(saved_delay);
|
||||
::close(read_fd);
|
||||
}
|
||||
BENCHMARK(SendSensorState_Immediate);
|
||||
|
||||
// --- send_sensor_state: batch path (cold — first call allocates) ---
|
||||
// Measures: send_message_smart_ → schedule_message_ → deferred batch add.
|
||||
// Includes one-time vector allocation cost.
|
||||
|
||||
static void SendSensorState_Batch_Cold(benchmark::State &state) {
|
||||
auto [conn, read_fd] = create_api_connection();
|
||||
|
||||
TestSensor sensor;
|
||||
sensor.configure("test_sensor");
|
||||
sensor.publish_state(23.5f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->send_sensor_state(&sensor);
|
||||
}
|
||||
benchmark::DoNotOptimize(conn.get());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
|
||||
::close(read_fd);
|
||||
}
|
||||
BENCHMARK(SendSensorState_Batch_Cold);
|
||||
|
||||
// --- send_sensor_state: batch path (warm — buffer already allocated) ---
|
||||
// Measures steady-state batch cost after the vector has been allocated
|
||||
// and cleared at least once. This is the typical path during normal
|
||||
// operation after the first batch has been processed.
|
||||
|
||||
static void SendSensorState_Batch_Warm(benchmark::State &state) {
|
||||
auto [conn, read_fd] = create_api_connection();
|
||||
|
||||
TestSensor sensor;
|
||||
sensor.configure("test_sensor");
|
||||
sensor.publish_state(23.5f);
|
||||
|
||||
// Warm up: send once to allocate, then clear to keep capacity
|
||||
conn->send_sensor_state(&sensor);
|
||||
bench_clear_batch(conn.get());
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->send_sensor_state(&sensor);
|
||||
}
|
||||
benchmark::DoNotOptimize(conn.get());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
|
||||
::close(read_fd);
|
||||
}
|
||||
BENCHMARK(SendSensorState_Batch_Warm);
|
||||
|
||||
// --- process_batch_: single sensor state (encode + frame + write) ---
|
||||
// Measures the deferred batch processing path: dispatch_message_ →
|
||||
// try_send_sensor_state → fill + proto encode → send_buffer → frame write.
|
||||
// This is the cost paid on the next loop() after batching.
|
||||
|
||||
static void ProcessBatch_SingleSensor(benchmark::State &state) {
|
||||
auto [conn, read_fd] = create_api_connection();
|
||||
|
||||
TestSensor sensor;
|
||||
sensor.configure("test_sensor");
|
||||
sensor.publish_state(23.5f);
|
||||
|
||||
// Warm up batch vector
|
||||
conn->send_sensor_state(&sensor);
|
||||
bench_process_batch(conn.get());
|
||||
drain_socket(read_fd);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
conn->send_sensor_state(&sensor);
|
||||
bench_process_batch(conn.get());
|
||||
}
|
||||
drain_socket(read_fd);
|
||||
benchmark::DoNotOptimize(conn.get());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
|
||||
::close(read_fd);
|
||||
}
|
||||
BENCHMARK(ProcessBatch_SingleSensor);
|
||||
|
||||
// --- process_batch_: 5 different sensors ---
|
||||
// Measures batch processing with multiple items queued.
|
||||
// This exercises the multi-message path in process_batch_.
|
||||
|
||||
static void ProcessBatch_5Sensors(benchmark::State &state) {
|
||||
auto [conn, read_fd] = create_api_connection();
|
||||
|
||||
TestSensor sensors[5];
|
||||
for (int i = 0; i < 5; i++) {
|
||||
char name[20];
|
||||
snprintf(name, sizeof(name), "sensor_%d", i);
|
||||
sensors[i].configure(name);
|
||||
sensors[i].publish_state(23.5f + static_cast<float>(i));
|
||||
}
|
||||
|
||||
// Warm up batch vector
|
||||
for (auto &s : sensors)
|
||||
conn->send_sensor_state(&s);
|
||||
bench_process_batch(conn.get());
|
||||
drain_socket(read_fd);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
for (auto &s : sensors)
|
||||
conn->send_sensor_state(&s);
|
||||
bench_process_batch(conn.get());
|
||||
}
|
||||
drain_socket(read_fd);
|
||||
benchmark::DoNotOptimize(conn.get());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
|
||||
::close(read_fd);
|
||||
}
|
||||
BENCHMARK(ProcessBatch_5Sensors);
|
||||
|
||||
} // namespace esphome::api::benchmarks
|
||||
|
||||
#endif // USE_API_PLAINTEXT && USE_SENSOR
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,55 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/button/button.h"
|
||||
|
||||
namespace esphome::button::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Button for benchmarking — press_action() is a no-op.
|
||||
class BenchButton : public Button {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void press_action() override {}
|
||||
};
|
||||
|
||||
// --- Button::press() ---
|
||||
// Measures: ESP_LOGD + press_action() + callback dispatch.
|
||||
|
||||
static void ButtonPress(benchmark::State &state) {
|
||||
BenchButton button;
|
||||
button.configure("test_button");
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
button.press();
|
||||
}
|
||||
benchmark::DoNotOptimize(&button);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ButtonPress);
|
||||
|
||||
// --- Button::press() with callback ---
|
||||
// Measures callback dispatch overhead.
|
||||
|
||||
static void ButtonPress_WithCallback(benchmark::State &state) {
|
||||
BenchButton button;
|
||||
button.configure("test_button");
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
button.add_on_press_callback([&callback_count]() { callback_count++; });
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
button.press();
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(ButtonPress_WithCallback);
|
||||
|
||||
} // namespace esphome::button::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
button:
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,121 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/number/number.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Number for benchmarking — control() publishes the value back.
|
||||
class BenchNumber : public number::Number {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void control(float value) override { this->publish_state(value); }
|
||||
};
|
||||
|
||||
// Helper to create a typical number entity for benchmarks.
|
||||
static void setup_number(BenchNumber &number) {
|
||||
number.configure("test_number");
|
||||
number.traits.set_min_value(0.0f);
|
||||
number.traits.set_max_value(100.0f);
|
||||
number.traits.set_step(1.0f);
|
||||
number.traits.set_mode(number::NUMBER_MODE_SLIDER);
|
||||
}
|
||||
|
||||
// --- Number::publish_state() ---
|
||||
// Measures the publish path: set_has_state, store value, callback dispatch.
|
||||
|
||||
static void NumberPublish_State(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.publish_state(static_cast<float>(i % 100));
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberPublish_State);
|
||||
|
||||
// --- Number::publish_state() with callback ---
|
||||
// Measures callback dispatch overhead.
|
||||
|
||||
static void NumberPublish_WithCallback(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
number.add_on_state_callback([&callback_count](float) { callback_count++; });
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.publish_state(static_cast<float>(i % 100));
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberPublish_WithCallback);
|
||||
|
||||
// --- NumberCall::perform() set value ---
|
||||
// The most common number call — setting an absolute value.
|
||||
// Exercises: validation against min/max, control() dispatch.
|
||||
|
||||
static void NumberCall_SetValue(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
number.publish_state(50.0f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
float val = static_cast<float>(i % 100);
|
||||
number.make_call().set_value(val).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberCall_SetValue);
|
||||
|
||||
// --- NumberCall::perform() increment ---
|
||||
// Exercises: state read, step arithmetic, max clamping.
|
||||
|
||||
static void NumberCall_Increment(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
number.publish_state(0.0f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.make_call().number_increment(true).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberCall_Increment);
|
||||
|
||||
// --- NumberCall::perform() decrement ---
|
||||
// Exercises: state read, step arithmetic, min clamping.
|
||||
|
||||
static void NumberCall_Decrement(benchmark::State &state) {
|
||||
BenchNumber number;
|
||||
setup_number(number);
|
||||
number.publish_state(100.0f);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
number.make_call().number_decrement(true).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(number.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(NumberCall_Decrement);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
number:
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,157 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/select/select.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Select for benchmarking — control() publishes directly by index.
|
||||
class BenchSelect : public select::Select {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void control(size_t index) override { this->publish_state(index); }
|
||||
};
|
||||
|
||||
// Helper to create a select with the given options.
|
||||
static void setup_select(BenchSelect &select, const char *name, std::initializer_list<const char *> options) {
|
||||
select.configure(name);
|
||||
select.traits.set_options(options);
|
||||
select.publish_state(size_t(0));
|
||||
}
|
||||
|
||||
// --- Select::publish_state(size_t) ---
|
||||
// The fast path: publish by index, no string lookup.
|
||||
|
||||
static void SelectPublish_ByIndex(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.publish_state(static_cast<size_t>(i % 4));
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectPublish_ByIndex);
|
||||
|
||||
// --- Select::publish_state(const char *) ---
|
||||
// The string path: requires index_of() lookup via strncmp.
|
||||
|
||||
static void SelectPublish_ByString(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
const char *options[] = {"off", "still", "move", "still+move"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.publish_state(options[i % 4]);
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectPublish_ByString);
|
||||
|
||||
// --- Select::publish_state() with callback ---
|
||||
// Measures callback dispatch overhead on the index path.
|
||||
|
||||
static void SelectPublish_WithCallback(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
select.add_on_state_callback([&callback_count](size_t) { callback_count++; });
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.publish_state(static_cast<size_t>(i % 4));
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectPublish_WithCallback);
|
||||
|
||||
// --- SelectCall::perform() set by index ---
|
||||
// The fast call path — no string matching needed.
|
||||
|
||||
static void SelectCall_SetByIndex(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().set_index(i % 4).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_SetByIndex);
|
||||
|
||||
// --- SelectCall::perform() set by option string ---
|
||||
// Exercises the string lookup path through index_of().
|
||||
|
||||
static void SelectCall_SetByOption(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
const char *options[] = {"off", "still", "move", "still+move"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().set_option(options[i % 4]).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_SetByOption);
|
||||
|
||||
// --- SelectCall::perform() next with cycling ---
|
||||
// Exercises the navigation path through active_index_.
|
||||
|
||||
static void SelectCall_NextCycle(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(select, "test_select", {"off", "still", "move", "still+move"});
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().select_next(true).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_NextCycle);
|
||||
|
||||
// --- SelectCall with 10 options (string lookup) ---
|
||||
// Worst-case string matching with more options.
|
||||
|
||||
static void SelectCall_SetByOption_10Options(benchmark::State &state) {
|
||||
BenchSelect select;
|
||||
setup_select(
|
||||
select, "test_select",
|
||||
{"off", "still", "move", "still+move", "custom1", "custom2", "custom3", "custom4", "custom5", "custom6"});
|
||||
|
||||
// Pick options spread across the list to exercise different search depths
|
||||
const char *picks[] = {"off", "custom3", "custom6", "move"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
select.make_call().set_option(picks[i % 4]).perform();
|
||||
}
|
||||
benchmark::DoNotOptimize(select.active_index());
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SelectCall_SetByOption_10Options);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
select:
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,137 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/switch/switch.h"
|
||||
|
||||
namespace esphome::benchmarks {
|
||||
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// Minimal Switch for benchmarking — write_state() publishes directly.
|
||||
class BenchSwitch : public switch_::Switch {
|
||||
public:
|
||||
void configure(const char *name) { this->configure_entity_(name, 0x12345678, 0); }
|
||||
|
||||
protected:
|
||||
void write_state(bool state) override { this->publish_state(state); }
|
||||
};
|
||||
|
||||
// --- Switch::publish_state() alternating ---
|
||||
// Forces state change every call, exercising the full publish path.
|
||||
|
||||
static void SwitchPublish_Alternating(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(i % 2 == 0);
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_Alternating);
|
||||
|
||||
// --- Switch::publish_state() no change ---
|
||||
// Tests the deduplication fast path in publish_dedup_.
|
||||
|
||||
static void SwitchPublish_NoChange(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(true);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(true);
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_NoChange);
|
||||
|
||||
// --- Switch::publish_state() with callback ---
|
||||
// Measures callback dispatch overhead on state changes.
|
||||
|
||||
static void SwitchPublish_WithCallback(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
sw.add_on_state_callback([&callback_count](bool) { callback_count++; });
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(i % 2 == 0);
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_WithCallback);
|
||||
|
||||
// --- Switch::turn_on() / turn_off() ---
|
||||
// The front-end call path: turn_on → write_state → publish_state.
|
||||
|
||||
static void SwitchTurnOn(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.turn_on();
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchTurnOn);
|
||||
|
||||
// --- Switch::toggle() alternating ---
|
||||
// Exercises the toggle path which reads current state to determine target.
|
||||
|
||||
static void SwitchToggle(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.toggle();
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchToggle);
|
||||
|
||||
// --- Switch::publish_state() inverted ---
|
||||
// Verifies the inversion path doesn't add significant overhead.
|
||||
|
||||
static void SwitchPublish_Inverted(benchmark::State &state) {
|
||||
BenchSwitch sw;
|
||||
sw.configure("test_switch");
|
||||
sw.set_restore_mode(switch_::SWITCH_ALWAYS_OFF);
|
||||
sw.set_inverted(true);
|
||||
sw.publish_state(false);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sw.publish_state(i % 2 == 0);
|
||||
}
|
||||
benchmark::DoNotOptimize(sw.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(SwitchPublish_Inverted);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
switch:
|
||||
@@ -0,0 +1,5 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.enable_codegen()
|
||||
@@ -0,0 +1,108 @@
|
||||
#include <benchmark/benchmark.h>
|
||||
|
||||
#include "esphome/components/text_sensor/text_sensor.h"
|
||||
|
||||
namespace esphome::text_sensor::benchmarks {
|
||||
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
|
||||
// --- publish_state(const char *) with short string, value changes each time ---
|
||||
// Exercises: memcmp check (mismatch), string assign, callback dispatch.
|
||||
|
||||
static void TextSensorPublish_Short_Changing(benchmark::State &state) {
|
||||
TextSensor sensor;
|
||||
|
||||
// Pre-populate with different short strings
|
||||
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sensor.publish_state(values[i % 4]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(TextSensorPublish_Short_Changing);
|
||||
|
||||
// --- publish_state(const char *) with short string, same value (dedup path) ---
|
||||
// Exercises: memcmp check (match), skips string assign.
|
||||
|
||||
static void TextSensorPublish_Short_NoChange(benchmark::State &state) {
|
||||
TextSensor sensor;
|
||||
sensor.publish_state("192.168.1.100");
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sensor.publish_state("192.168.1.100");
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(TextSensorPublish_Short_NoChange);
|
||||
|
||||
// --- publish_state with longer string (firmware version, MAC address) ---
|
||||
// Exercises: memcmp on longer strings, string assign with potential realloc.
|
||||
|
||||
static void TextSensorPublish_Long_Changing(benchmark::State &state) {
|
||||
TextSensor sensor;
|
||||
|
||||
const char *values[] = {
|
||||
"2025.12.0-dev (Jan 15 2025, 10:30:00)",
|
||||
"2025.12.1-dev (Feb 20 2025, 14:45:00)",
|
||||
"2025.12.2-dev (Mar 10 2025, 08:15:00)",
|
||||
"2025.12.3-dev (Apr 5 2025, 16:00:00)",
|
||||
};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sensor.publish_state(values[i % 4]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(TextSensorPublish_Long_Changing);
|
||||
|
||||
// --- publish_state with callback ---
|
||||
// Measures callback dispatch overhead for text sensors.
|
||||
|
||||
static void TextSensorPublish_WithCallback(benchmark::State &state) {
|
||||
TextSensor sensor;
|
||||
|
||||
uint64_t callback_count = 0;
|
||||
sensor.add_on_state_callback([&callback_count](const std::string &) { callback_count++; });
|
||||
|
||||
const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sensor.publish_state(values[i % 4]);
|
||||
}
|
||||
benchmark::DoNotOptimize(callback_count);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(TextSensorPublish_WithCallback);
|
||||
|
||||
// --- publish_state(const char *, size_t) direct ---
|
||||
// The lowest-level overload, avoids strlen.
|
||||
|
||||
static void TextSensorPublish_WithLen(benchmark::State &state) {
|
||||
TextSensor sensor;
|
||||
|
||||
static constexpr const char *values[] = {"192.168.1.1", "192.168.1.2", "192.168.1.3", "192.168.1.4"};
|
||||
static constexpr size_t lens[] = {11, 11, 11, 11};
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
sensor.publish_state(values[i % 4], lens[i % 4]);
|
||||
}
|
||||
benchmark::DoNotOptimize(sensor.state);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(TextSensorPublish_WithLen);
|
||||
|
||||
} // namespace esphome::text_sensor::benchmarks
|
||||
@@ -0,0 +1 @@
|
||||
text_sensor:
|
||||
@@ -8,7 +8,24 @@ namespace esphome::benchmarks {
|
||||
// Inner iteration count to amortize CodSpeed instrumentation overhead.
|
||||
// Without this, the ~60ns per-iteration valgrind start/stop cost dominates
|
||||
// sub-microsecond benchmarks.
|
||||
static constexpr int kInnerIterations = 2000;
|
||||
// Must be divisible by all batch sizes used below (3, 10) to avoid
|
||||
// pool imbalance at iteration boundaries that causes spurious malloc.
|
||||
static constexpr int kInnerIterations = 2100;
|
||||
|
||||
// Warm the scheduler pool by registering and replacing items twice.
|
||||
// The first batch allocates fresh items; the second batch cancels them and
|
||||
// populates the recycling pool with the cancelled items from the first batch.
|
||||
static void warm_pool(Scheduler &scheduler, Component *component, int batch_size, uint32_t delay) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < batch_size; i++) {
|
||||
scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {});
|
||||
}
|
||||
scheduler.call(++now);
|
||||
for (int i = 0; i < batch_size; i++) {
|
||||
scheduler.set_timeout(component, static_cast<uint32_t>(i), delay, []() {});
|
||||
}
|
||||
scheduler.call(++now);
|
||||
}
|
||||
|
||||
// --- Scheduler fast path: no work to do ---
|
||||
|
||||
@@ -83,11 +100,21 @@ static void Scheduler_SetTimeout(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
|
||||
// Register 3 timeouts then call() — realistic worst case where multiple
|
||||
// components schedule in the same loop iteration. Keeps item count within
|
||||
// the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free.
|
||||
static constexpr int kBatchSize = 3;
|
||||
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
|
||||
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
|
||||
for (auto _ : state) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 1000, []() {});
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
|
||||
if ((i + 1) % kBatchSize == 0) {
|
||||
scheduler.call(++now);
|
||||
}
|
||||
}
|
||||
scheduler.process_to_add();
|
||||
scheduler.call(++now);
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
@@ -99,22 +126,22 @@ BENCHMARK(Scheduler_SetTimeout);
|
||||
static void Scheduler_SetInterval(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
// Number of distinct interval keys; controls how many unique timers exist
|
||||
// simultaneously and the drain cadence for process_to_add().
|
||||
static constexpr int kKeyCount = 5;
|
||||
|
||||
// Register 3 intervals then call() — realistic worst case where multiple
|
||||
// components schedule in the same loop iteration. Keeps item count within
|
||||
// the recycling pool (MAX_POOL_SIZE=5) to avoid spurious malloc/free.
|
||||
static constexpr int kBatchSize = 3;
|
||||
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
|
||||
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
|
||||
for (auto _ : state) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kKeyCount), 1000, []() {});
|
||||
// Drain to_add_ periodically to reflect production behavior where
|
||||
// process_to_add() runs each main loop iteration. Without this,
|
||||
// cancelled items accumulate in to_add_ causing O(n²) scan cost.
|
||||
if ((i + 1) % kKeyCount == 0) {
|
||||
scheduler.process_to_add();
|
||||
scheduler.set_interval(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
|
||||
if ((i + 1) % kBatchSize == 0) {
|
||||
scheduler.call(++now);
|
||||
}
|
||||
}
|
||||
// Final drain in case kInnerIterations is not a multiple of 5
|
||||
scheduler.process_to_add();
|
||||
scheduler.call(++now);
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
@@ -128,16 +155,79 @@ static void Scheduler_Defer(benchmark::State &state) {
|
||||
Component dummy_component;
|
||||
|
||||
// defer() is Component::defer which calls set_timeout(delay=0).
|
||||
// Call set_timeout directly since defer() is protected.
|
||||
// Component::defer(func) passes nullptr as the name, which skips
|
||||
// cancel_item_locked_ entirely — matching production behavior where
|
||||
// defers are anonymous fire-and-forget callbacks.
|
||||
static constexpr int kBatchSize = 3;
|
||||
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
|
||||
warm_pool(scheduler, &dummy_component, kBatchSize, 0);
|
||||
for (auto _ : state) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % 5), 0, []() {});
|
||||
scheduler.set_timeout(&dummy_component, static_cast<const char *>(nullptr), 0, []() {});
|
||||
if ((i + 1) % kBatchSize == 0) {
|
||||
scheduler.call(++now);
|
||||
}
|
||||
}
|
||||
scheduler.process_to_add();
|
||||
scheduler.call(++now);
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_Defer);
|
||||
|
||||
// --- Scheduler: defer with same ID (cancel-and-replace pattern) ---
|
||||
|
||||
static void Scheduler_Defer_SameID(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
|
||||
// Measures defer with a fixed numeric ID — each call cancels the previous
|
||||
// pending defer before adding the new one. This is the pattern used by
|
||||
// components that defer work but want to coalesce rapid updates.
|
||||
static constexpr int kBatchSize = 3;
|
||||
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
|
||||
warm_pool(scheduler, &dummy_component, kBatchSize, 0);
|
||||
for (auto _ : state) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(0), 0, []() {});
|
||||
if ((i + 1) % kBatchSize == 0) {
|
||||
scheduler.call(++now);
|
||||
}
|
||||
}
|
||||
scheduler.call(++now);
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_Defer_SameID);
|
||||
|
||||
// --- Scheduler: set_timeout with batch size exceeding pool (cliff test) ---
|
||||
|
||||
static void Scheduler_SetTimeout_ExceedPool(benchmark::State &state) {
|
||||
Scheduler scheduler;
|
||||
Component dummy_component;
|
||||
|
||||
// Register 10 timeouts then call() — exceeds MAX_POOL_SIZE=5 to measure
|
||||
// the performance cliff when the recycling pool is exhausted and items
|
||||
// must be malloc'd/freed.
|
||||
static constexpr int kBatchSize = 10;
|
||||
static_assert(kInnerIterations % kBatchSize == 0, "kInnerIterations must be divisible by kBatchSize");
|
||||
warm_pool(scheduler, &dummy_component, kBatchSize, 1000);
|
||||
for (auto _ : state) {
|
||||
uint32_t now = millis();
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
scheduler.set_timeout(&dummy_component, static_cast<uint32_t>(i % kBatchSize), 1000, []() {});
|
||||
if ((i + 1) % kBatchSize == 0) {
|
||||
scheduler.call(++now);
|
||||
}
|
||||
}
|
||||
scheduler.call(++now);
|
||||
benchmark::DoNotOptimize(scheduler);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(Scheduler_SetTimeout_ExceedPool);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Stub for benchmark builds — provides the minimal interface that
|
||||
// api_connection.cpp needs when USE_BLUETOOTH_PROXY is defined,
|
||||
// without pulling in ESP32 BLE dependencies.
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/api/api_pb2.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
|
||||
namespace bluetooth_proxy {
|
||||
|
||||
class BluetoothProxy {
|
||||
public:
|
||||
api::APIConnection *get_api_connection() const { return nullptr; }
|
||||
void subscribe_api_connection(api::APIConnection *conn, uint32_t flags) {}
|
||||
void unsubscribe_api_connection(api::APIConnection *conn) {}
|
||||
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {}
|
||||
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {}
|
||||
void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {}
|
||||
void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {}
|
||||
void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {}
|
||||
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {}
|
||||
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {}
|
||||
void send_connections_free(api::APIConnection *conn) {}
|
||||
void bluetooth_scanner_set_mode(bool active) {}
|
||||
void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) {}
|
||||
uint32_t get_feature_flags() const { return 0; }
|
||||
void get_bluetooth_mac_address_pretty(char *buf) const { buf[0] = '\0'; }
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern BluetoothProxy *global_bluetooth_proxy;
|
||||
|
||||
} // namespace bluetooth_proxy
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Tests for display component metadata functions."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from esphome.components.display import (
|
||||
DisplayMetaData,
|
||||
add_metadata,
|
||||
get_all_display_metadata,
|
||||
get_display_metadata,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
|
||||
def test_add_metadata_with_string_id():
|
||||
"""Test adding metadata with a plain string ID."""
|
||||
with patch("esphome.components.display.CORE.data", {}):
|
||||
add_metadata("my_display", 320, 240, True)
|
||||
meta = get_display_metadata("my_display")
|
||||
assert meta == DisplayMetaData(
|
||||
width=320, height=240, has_writer=True, has_hardware_rotation=False
|
||||
)
|
||||
|
||||
|
||||
def test_add_metadata_with_mockobj_id():
|
||||
"""Test adding metadata with a MockObj ID (converted via str())."""
|
||||
with patch("esphome.components.display.CORE.data", {}):
|
||||
mock_id = MockObj("my_display_obj")
|
||||
add_metadata(mock_id, 480, 320, False, has_hardware_rotation=True)
|
||||
meta = get_display_metadata("my_display_obj")
|
||||
assert meta == DisplayMetaData(
|
||||
width=480, height=320, has_writer=False, has_hardware_rotation=True
|
||||
)
|
||||
|
||||
|
||||
def test_add_metadata_hardware_rotation_default():
|
||||
"""Test that has_hardware_rotation defaults to False."""
|
||||
with patch("esphome.components.display.CORE.data", {}):
|
||||
add_metadata("disp", 128, 64, False)
|
||||
meta = get_display_metadata("disp")
|
||||
assert meta.has_hardware_rotation is False
|
||||
|
||||
|
||||
def test_get_display_metadata_missing_returns_none():
|
||||
"""Test that querying a non-existent ID returns None."""
|
||||
with patch("esphome.components.display.CORE.data", {}):
|
||||
data = get_display_metadata("no_such_display")
|
||||
assert data.width == 0
|
||||
assert data.height == 0
|
||||
assert data.has_writer is False
|
||||
assert data.has_hardware_rotation is False
|
||||
|
||||
|
||||
def test_add_multiple_displays():
|
||||
"""Test adding metadata for multiple displays."""
|
||||
with patch("esphome.components.display.CORE.data", {}):
|
||||
add_metadata("disp_a", 320, 240, True)
|
||||
add_metadata("disp_b", 128, 64, False, has_hardware_rotation=True)
|
||||
|
||||
all_meta = get_all_display_metadata()
|
||||
assert len(all_meta) == 2
|
||||
assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, False)
|
||||
assert all_meta["disp_b"] == DisplayMetaData(128, 64, False, True)
|
||||
|
||||
|
||||
def test_add_metadata_overwrites_existing():
|
||||
"""Test that adding metadata for the same ID overwrites the previous entry."""
|
||||
with patch("esphome.components.display.CORE.data", {}):
|
||||
add_metadata("disp", 320, 240, True)
|
||||
add_metadata("disp", 640, 480, False, has_hardware_rotation=True)
|
||||
meta = get_display_metadata("disp")
|
||||
assert meta == DisplayMetaData(640, 480, False, True)
|
||||
|
||||
|
||||
def test_metadata_is_frozen():
|
||||
"""Test that DisplayMetaData instances are immutable (frozen dataclass)."""
|
||||
meta = DisplayMetaData(320, 240, True, False)
|
||||
try:
|
||||
meta.width = 640
|
||||
assert False, "Expected FrozenInstanceError"
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -133,6 +133,6 @@ def test_code_generation(
|
||||
assert "set_init_sequence({224, 1, 0, 225, 1, 147, 226, 1," in main_cpp
|
||||
assert "p4_nano->set_lane_bit_rate(1500.0f);" in main_cpp
|
||||
assert "p4_nano->set_rotation(display::DISPLAY_ROTATION_90_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" in main_cpp
|
||||
assert "p4_86->set_rotation(display::DISPLAY_ROTATION_0_DEGREES);" not in main_cpp
|
||||
assert "custom_id->set_rotation(display::DISPLAY_ROTATION_180_DEGREES);" in main_cpp
|
||||
# assert "backlight_id = new light::LightState(mipi_dsi_dsibacklight_id);" in main_cpp
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for mpip_spi configuration validation."""
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -12,6 +13,16 @@ from esphome.core import CORE
|
||||
from esphome.pins import gpio_pin_schema
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_spi_final_validate():
|
||||
"""Mock spi.final_validate_device_schema since unit tests have no real SPI bus config."""
|
||||
with mock.patch(
|
||||
"esphome.components.spi.final_validate_device_schema",
|
||||
return_value=lambda config: None,
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def choose_variant_with_pins() -> Generator[Callable[[list], None]]:
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Tests for display metadata created by mipi_spi component."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.components.display import (
|
||||
DisplayMetaData,
|
||||
get_all_display_metadata,
|
||||
get_display_metadata,
|
||||
)
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.mipi_spi.display import (
|
||||
CONFIG_SCHEMA,
|
||||
FINAL_VALIDATE_SCHEMA,
|
||||
get_instance,
|
||||
)
|
||||
from esphome.const import PlatformFramework
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def validated_config(config):
|
||||
"""Run schema + final validation and return the validated config."""
|
||||
config = CONFIG_SCHEMA(config)
|
||||
FINAL_VALIDATE_SCHEMA(config)
|
||||
return config
|
||||
|
||||
|
||||
def test_metadata_native_quad_default_test_card(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A quad-mode display with no explicit drawing gets a test card from final validation."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
config = validated_config({"model": "JC3636W518"})
|
||||
get_instance(config)
|
||||
meta = get_display_metadata(str(config["id"]))
|
||||
assert meta is not None
|
||||
assert meta.width == 360
|
||||
assert meta.height == 360
|
||||
# final validation auto-enables show_test_card when no drawing methods are configured
|
||||
assert meta.has_writer is True
|
||||
assert meta.has_hardware_rotation is True
|
||||
|
||||
|
||||
def test_metadata_single_mode_with_dc_pin(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A single-mode display with no explicit drawing gets a test card from final validation."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
config = validated_config(
|
||||
{
|
||||
"model": "ST7735",
|
||||
"dc_pin": 18,
|
||||
}
|
||||
)
|
||||
get_instance(config)
|
||||
meta = get_display_metadata(str(config["id"]))
|
||||
assert meta is not None
|
||||
assert meta.width == 128
|
||||
assert meta.height == 160
|
||||
assert meta.has_writer is True
|
||||
assert meta.has_hardware_rotation is True
|
||||
|
||||
|
||||
def test_metadata_custom_dimensions(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A custom model picks up explicit dimensions."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
config = validated_config(
|
||||
{
|
||||
"model": "custom",
|
||||
"dc_pin": 18,
|
||||
"dimensions": {"width": 480, "height": 320},
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
}
|
||||
)
|
||||
get_instance(config)
|
||||
meta = get_display_metadata(str(config["id"]))
|
||||
assert meta is not None
|
||||
assert meta.width == 480
|
||||
assert meta.height == 320
|
||||
# final validation auto-enables show_test_card
|
||||
assert meta.has_writer is True
|
||||
assert meta.has_hardware_rotation is True
|
||||
|
||||
|
||||
def test_metadata_with_test_card_has_writer(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""When show_test_card is enabled, has_writer should be True."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
config = validated_config(
|
||||
{
|
||||
"model": "custom",
|
||||
"dc_pin": 18,
|
||||
"dimensions": {"width": 240, "height": 240},
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
"show_test_card": True,
|
||||
}
|
||||
)
|
||||
get_instance(config)
|
||||
meta = get_display_metadata(str(config["id"]))
|
||||
assert meta is not None
|
||||
assert meta.has_writer is True
|
||||
|
||||
|
||||
def test_metadata_no_swap_xy_not_full_hardware_rotation(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A model that disables swap_xy should report has_hardware_rotation=False."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
|
||||
)
|
||||
# JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only
|
||||
config = validated_config({"model": "JC3248W535"})
|
||||
get_instance(config)
|
||||
meta = get_display_metadata(str(config["id"]))
|
||||
assert meta is not None
|
||||
assert meta.has_hardware_rotation is False
|
||||
|
||||
|
||||
def test_metadata_multiple_displays_independent(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Multiple displays each get their own metadata entry."""
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
|
||||
)
|
||||
config_a = validated_config(
|
||||
{
|
||||
"id": "disp_a",
|
||||
"model": "custom",
|
||||
"dc_pin": 18,
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
}
|
||||
)
|
||||
config_b = validated_config(
|
||||
{
|
||||
"id": "disp_b",
|
||||
"model": "custom",
|
||||
"dc_pin": 19,
|
||||
"dimensions": {"width": 128, "height": 64},
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
}
|
||||
)
|
||||
get_instance(config_a)
|
||||
get_instance(config_b)
|
||||
|
||||
all_meta = get_all_display_metadata()
|
||||
# final validation auto-enables show_test_card for both
|
||||
assert all_meta["disp_a"] == DisplayMetaData(320, 240, True, True)
|
||||
assert all_meta["disp_b"] == DisplayMetaData(128, 64, True, True)
|
||||
|
||||
|
||||
def test_metadata_via_code_generation_native(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Full code generation for native.yaml should produce correct metadata."""
|
||||
generate_main(component_fixture_path("native.yaml"))
|
||||
all_meta = get_all_display_metadata()
|
||||
# native.yaml: model JC3636W518 -> 360x360, no writer, full hardware rotation
|
||||
assert len(all_meta) == 1
|
||||
meta = next(iter(all_meta.values()))
|
||||
assert meta == DisplayMetaData(
|
||||
width=360, height=360, has_writer=True, has_hardware_rotation=True
|
||||
)
|
||||
|
||||
|
||||
def test_metadata_via_code_generation_lvgl(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_fixture_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Full code generation for lvgl.yaml should produce correct metadata."""
|
||||
generate_main(component_fixture_path("lvgl.yaml"))
|
||||
all_meta = get_all_display_metadata()
|
||||
# lvgl.yaml: model ST7735 -> 128x160, no writer (lvgl draws directly), full hw rotation
|
||||
assert len(all_meta) == 1
|
||||
meta = next(iter(all_meta.values()))
|
||||
assert meta == DisplayMetaData(
|
||||
width=128, height=160, has_writer=False, has_hardware_rotation=True
|
||||
)
|
||||
@@ -204,11 +204,6 @@ def test_transform_and_init_sequence_errors(
|
||||
r"extra keys not allowed @ data\['brightness'\]",
|
||||
id="brightness_not_supported",
|
||||
),
|
||||
pytest.param(
|
||||
{"model": "T-DISPLAY-S3-PRO"},
|
||||
"PSRAM is required for this display",
|
||||
id="psram_required",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_esp32s3_specific_errors(
|
||||
@@ -319,7 +314,7 @@ def test_native_generation(
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
||||
assert (
|
||||
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1, 1>()"
|
||||
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, 0, true, 1, 1>()"
|
||||
in main_cpp
|
||||
)
|
||||
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
|
||||
@@ -335,7 +330,7 @@ def test_lvgl_generation(
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("lvgl.yaml"))
|
||||
assert (
|
||||
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0>();"
|
||||
"mipi_spi::MipiSpi<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_SINGLE, 128, 160, 0, 0, 0, true>();"
|
||||
in main_cpp
|
||||
)
|
||||
assert "set_init_sequence({1, 0, 10, 255, 177" in main_cpp
|
||||
|
||||
@@ -4,3 +4,14 @@ sensor:
|
||||
tvoc:
|
||||
name: AGS10 TVOC
|
||||
update_interval: 60s
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test AGS10 Actions"
|
||||
on_press:
|
||||
- ags10.set_zero_point:
|
||||
id: ags10_1
|
||||
mode: CURRENT_VALUE
|
||||
- ags10.new_i2c_address:
|
||||
id: ags10_1
|
||||
address: 0x1A
|
||||
|
||||
@@ -12,6 +12,11 @@ esphome:
|
||||
trigger_keep: 10s
|
||||
stage_gain: 3
|
||||
power_consumption: 70uA
|
||||
- at581x.settings:
|
||||
id: waveradar
|
||||
frequency: !lambda "return 5800;"
|
||||
poweron_selfcheck_time: !lambda "return 2000;"
|
||||
power_consumption: !lambda "return 70;"
|
||||
- at581x.reset:
|
||||
id: waveradar
|
||||
|
||||
|
||||
@@ -13,3 +13,4 @@ dsmr:
|
||||
request_pin: ${request_pin}
|
||||
request_interval: 20s
|
||||
receive_timeout: 100ms
|
||||
thermal_mbus_id: 3
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
*** DO NOT USE THIS KEY...EVER ***
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIG5AIBAAKCAYEA0J665DlxzUzzouzH96fxqXybEfFU7H1oSf2fUHwoNMgUG7Vc
|
||||
SHxuFJkpsUnxg9br09/v5THOXfUj5t/Arog6FGiL7i0HXCYDMnSn2EzQWR+DY2Qj
|
||||
C3YzTLvcOQ40gFjDWfzAheAMCQmc5xQeB3YmaXQf+fUWH/PfFs9Pm+L92YTv2XC1
|
||||
B2q5s8K8hUWghO472A+UMrreuDcltNJ+TbuSRHK0NQzKpKo0Vkl4HycczGDgpa8D
|
||||
h68JL/BKVeJAjKxWd/xcj/FCk661ODXi0esB/mGQP3hAthWpwi+gdkWczWs1Ocr6
|
||||
VxKje1zFm9SEq+SmCViPY/Pu8Xs7steqz3b3JtRGtKQE0r3B+hBKI7aRudOZyz0s
|
||||
kqoL1zYrAWmoTWBqa2tj1ACPqtr2LyHGt2aVrHRQGJf21mPYIy9GIOv+3v3GzIAK
|
||||
az2B8Z93Bw1biwNZDr1SLNYfQVaJT1hQavmdlvwW8vqLUGDcQlk42yOF6nAmvAPu
|
||||
Wzxf+QFEtJT6Am65AgMBAAECggGAB0d+mG+LscDtYGI4MQNGaqZLJ+NelfjjPm+v
|
||||
0yhd48eWcggQPgQ/eA8HFiVRHMtPQ7+U2I+2Fm+zDr+AcuaUdjlWppsiHlxCMMzC
|
||||
vYiinXV8yWdJVMFNVXBZpRECknbmbBmmYxV3/gm8lJCOYq7D9NqFMhzT5o4FGv/l
|
||||
VHhlaKVblB/7ZRSbgbL6DoFpMjI42tdiUanVEyLzeR1+JDq3BhXlhVNar8ezl04t
|
||||
d5LPDa+UrxtN+XpJTQeqpFgGbhImSxjzCjo0kbGiEx/DwWuFJxguIcDU25sM4g2+
|
||||
ivtn7N11U0oaqNwsz7p4cKAm8toJYxxXWZvKdj1kZvCZ+BtyH0/MtOa2Q6v91HOh
|
||||
zY4KEl5wxQYnxJrgqevSm8rrC51tLOCidZ16cHba8sjrK69xysEazk43roHLFXDp
|
||||
JpH7Zd8LETjFWGVfUz6vppzkt6mrJk0DNuMLk/UwpPHzW2pu1qDiHPCb9+ra1S9U
|
||||
t55hT2TBFDcG/NmZZnyHQoh8METhAoHBAPIG0G8Cd4fmkvbgCRLzCIWsH6zGHS9o
|
||||
80Rj9Gu93B+m/F9GtgyYuX+DKSdMdw3IJamUsBwofT2wynmkuJFhLtD1FmYtlsXf
|
||||
TWp8g8CfFGrIXDvin5E3heyhvtFiOjXlw0Q8yMmQXr5LF0i3WyFCPQM20ugClB7N
|
||||
CQBOVAfpVoRU1fA6UjjHibFRwi1b4bLV69QiERPCJfcny/DPkZpu7I1fiINmwzEb
|
||||
O5mIFo5F4TQADEreWplXEmhEXIzDMFIwEQKBwQDcqimCcO3RSysZMQhhUfk8G19I
|
||||
yRNwvi2fK5LiGCZMYjeYKqg1rBN4yCf9PTwaqBNRqXTg13Fc7zrOkSI+0oDa4FWI
|
||||
/kMEztaUK+Kwd2NKc96aXHMBGF+1Sx7Ygnr9e2dyqDqRij2/qlQYY1EDz7cYldaX
|
||||
YNrXcQQeNJbqydjRDYi+9bI+wDkrK/5PxE1sGmqS1RMKxoJCZmxNiQT3PmXM/oNR
|
||||
Ev6N9CDklFtClWNcD0Uum+mxNJ53ldZDx4UI/CkCgcEA6R6BI3vX0FHaGureMp9f
|
||||
BQoulEdbEzBeqPAyHJkKbn50Nf0xGt78RYL7X7v6LI8tH7N1Eho5z/L6g8KSeI2H
|
||||
/4MiqRaeVEdrFPeMHDvd+aC1noUBt2komS2OU7XuZb3CoHZ/3A4wA9DmQ4dAwr8/
|
||||
b1oeOZVKQISzd9T6gYhSajIgwzwZuFESInaitvf6ZDxC49hQZJyr3u05NeFo2Lyh
|
||||
Iuby4cZYmnMlrBN1zmImseSd8ntL/sjslPvLvVXAtFlRAoHBAIDuG5rPiOTE2sW5
|
||||
VIAoeUuZYq8QbX9uXxGlUAkyuw3eRUVvhyD1DduAd30Ljla05bTNIjFNMDtwvBd9
|
||||
zViPfiJk+RU2GspwYAfrLGSXHTifQu1GHxwAtcsjvT4b3ujEdckUakQnVbTrPH+T
|
||||
Z/6mGwEOa3e/a559tj4/0/4TOc/L7J5GyILJpZ2H8uuAcww60xI/1QRywCEz3wve
|
||||
hzw/BRQlkWyJgJpIjf+Af2IEDy327iExj/WuHPkaXzrzFNQPIQKBwAM6qeNOxrO3
|
||||
V91wg4+44FAsOda62fZ0GlCM7ETnEjLbFamtCKEcDNfijwTa54LcZ6yObyutD1RN
|
||||
dhj4Z6QKuYnsE02agv9CtXdFEVEXaqj4pshdgVOwGK34OidT4yIJQGLrRAQ/JiGH
|
||||
x6CoGUCNIAq5J08VdosLTD9qdn1zv8USCAP0ReKnRMndTzENLYz9G3nQyHgt5GzI
|
||||
YoSRtrWnXrQp2Yn3epk74gFAJtKozWNV4Du35FJBjmSeMuRivonNMQ==
|
||||
-----END RSA PRIVATE KEY-----
|
||||
*** DO NOT USE THIS KEY...EVER ***
|
||||
@@ -0,0 +1,10 @@
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
signed_ota_verification:
|
||||
signing_key: ../../components/esp32/dummy_signing_key.pem
|
||||
signing_scheme: rsa3072
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -69,3 +69,11 @@ esp32_ble_server:
|
||||
- ble_server.descriptor.set_value:
|
||||
id: test_change_descriptor
|
||||
value: !lambda return bytebuffer::ByteBuffer::wrap({0x03, 0x04, 0x05}).get_data();
|
||||
- ble_server.characteristic.set_value:
|
||||
id: test_change_characteristic
|
||||
value:
|
||||
data: [0xfc, 0xef, 0xfe, 0x86]
|
||||
- ble_server.descriptor.set_value:
|
||||
id: test_change_descriptor
|
||||
value:
|
||||
data: [0x01, 0x02, 0x03]
|
||||
|
||||
@@ -14,3 +14,6 @@ esphome:
|
||||
assert(x == 95);
|
||||
x = clamp_at_most(x, 40);
|
||||
assert(x == 40);
|
||||
- lambda: |-
|
||||
float value = 0.0f;
|
||||
sscanf("3.14", "%f", &value);
|
||||
|
||||
@@ -29,6 +29,8 @@ espnow:
|
||||
data: !lambda 'return {0x01, 0x02, 0x03, 0x04, 0x05};'
|
||||
- espnow.broadcast:
|
||||
data: "Hello, World!"
|
||||
- espnow.broadcast:
|
||||
data: "it's a test"
|
||||
- espnow.broadcast:
|
||||
data: [0x01, 0x02, 0x03, 0x04, 0x05]
|
||||
- espnow.broadcast:
|
||||
|
||||
@@ -1 +1,20 @@
|
||||
<<: !include common-w5500.yaml
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 19
|
||||
mosi_pin: 21
|
||||
miso_pin: 23
|
||||
cs_pin: 18
|
||||
interrupt_pin: 36
|
||||
reset_pin: 22
|
||||
clock_speed: 10Mhz
|
||||
manual_ip:
|
||||
static_ip: 192.168.178.56
|
||||
gateway: 192.168.178.1
|
||||
subnet: 255.255.255.0
|
||||
domain: .local
|
||||
mac_address: "02:AA:BB:CC:DD:01"
|
||||
interface: spi2
|
||||
on_connect:
|
||||
- logger.log: "Ethernet connected!"
|
||||
on_disconnect:
|
||||
- logger.log: "Ethernet disconnected!"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
sensor:
|
||||
- platform: hdc2080
|
||||
temperature:
|
||||
name: Temperature
|
||||
humidity:
|
||||
name: Humidity
|
||||
update_interval: 15s
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,5 +1,6 @@
|
||||
sensor:
|
||||
- platform: htu21d
|
||||
id: htu21d_sensor
|
||||
i2c_id: i2c_bus
|
||||
model: htu21d
|
||||
temperature:
|
||||
@@ -9,3 +10,14 @@ sensor:
|
||||
heater:
|
||||
name: Heater
|
||||
update_interval: 15s
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test HTU21D Actions"
|
||||
on_press:
|
||||
- htu21d.set_heater:
|
||||
id: htu21d_sensor
|
||||
status: true
|
||||
- htu21d.set_heater_level:
|
||||
id: htu21d_sensor
|
||||
level: 5
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common.yaml
|
||||
@@ -30,18 +30,63 @@ binary_sensor:
|
||||
return y;
|
||||
|
||||
lvgl:
|
||||
id: lvgl_id
|
||||
rotation: 90
|
||||
log_level: debug
|
||||
resume_on_input: true
|
||||
on_pause:
|
||||
logger.log: LVGL is Paused
|
||||
- logger.log: LVGL is Paused
|
||||
- lvgl.display.set_rotation: 90
|
||||
on_resume:
|
||||
logger.log: LVGL has resumed
|
||||
- logger.log: LVGL has resumed
|
||||
- lvgl.display.set_rotation:
|
||||
rotation: 0
|
||||
lvgl_id: lvgl_id
|
||||
|
||||
on_boot:
|
||||
- logger.log: LVGL has started
|
||||
- lvgl.indicator.update:
|
||||
id: meter_arc_indicator
|
||||
start_value: 0
|
||||
end_value: 180
|
||||
on_invalidate_area:
|
||||
logger.log: Invalidate area
|
||||
on_resolution_change:
|
||||
logger.log: Resolution changed
|
||||
on_color_format_change:
|
||||
logger.log: Color format changed
|
||||
on_refr_request:
|
||||
logger.log: Refresh request
|
||||
on_refr_start:
|
||||
logger.log: Refresh start
|
||||
on_refr_ready:
|
||||
logger.log: Refresh ready
|
||||
on_render_start:
|
||||
logger.log: Render start
|
||||
on_render_ready:
|
||||
logger.log: Render ready
|
||||
on_flush_start:
|
||||
logger.log: Flush start
|
||||
on_flush_finish:
|
||||
logger.log: Flush finish
|
||||
on_flush_wait_start:
|
||||
logger.log: Flush wait start
|
||||
on_flush_wait_finish:
|
||||
logger.log: Flush wait finish
|
||||
on_update_layout_complete:
|
||||
logger.log: Update layout complete
|
||||
on_vsync:
|
||||
logger.log: Vsync
|
||||
on_vsync_request:
|
||||
logger.log: Vsync request
|
||||
on_screen_load_start:
|
||||
logger.log: Screen load start
|
||||
on_screen_load:
|
||||
logger.log: Screen loaded
|
||||
on_screen_unload:
|
||||
logger.log: Screen unloaded
|
||||
on_screen_unload_start:
|
||||
logger.log: Screen unload start
|
||||
bg_color: light_blue
|
||||
bottom_layer:
|
||||
widgets:
|
||||
@@ -49,6 +94,7 @@ lvgl:
|
||||
bg_color: 0x000000
|
||||
bg_opa: cover
|
||||
theme:
|
||||
dark_mode: true
|
||||
obj:
|
||||
border_width: 1
|
||||
|
||||
@@ -232,7 +278,7 @@ lvgl:
|
||||
- roller:
|
||||
id: lv_roller
|
||||
visible_row_count: 2
|
||||
anim_time: 500ms
|
||||
anim_duration: 500ms
|
||||
options:
|
||||
- Nov
|
||||
- Dec
|
||||
@@ -288,7 +334,9 @@ lvgl:
|
||||
- label:
|
||||
text: "Hello shiny day"
|
||||
text_color: 0xFFFFFF
|
||||
align: bottom_mid
|
||||
align_to:
|
||||
id: hello_label
|
||||
align: OUT_LEFT_TOP
|
||||
- label:
|
||||
id: setup_lambda_label
|
||||
# Test lambda in widget property during setup (LvContext)
|
||||
@@ -315,20 +363,27 @@ lvgl:
|
||||
align: top_left
|
||||
- container:
|
||||
align: center
|
||||
anim_duration: 1s
|
||||
arc_opa: COVER
|
||||
arc_color: 0xFF0000
|
||||
arc_rounded: false
|
||||
arc_width: 3
|
||||
anim_time: 1s
|
||||
base_dir: auto
|
||||
bg_color: light_blue
|
||||
bg_grad_color: light_blue
|
||||
bg_grad_dir: hor
|
||||
bg_grad_opa: cover
|
||||
bg_grad_stop: 128
|
||||
bg_image_opa: transp
|
||||
bg_image_recolor: light_blue
|
||||
bg_image_recolor_opa: 50%
|
||||
bg_main_opa: cover
|
||||
bg_main_stop: 0
|
||||
bg_opa: 20%
|
||||
blend_mode: normal
|
||||
blur_backdrop: false
|
||||
blur_quality: auto
|
||||
blur_radius: 0
|
||||
border_color: 0x00FF00
|
||||
border_opa: cover
|
||||
border_post: true
|
||||
@@ -336,7 +391,15 @@ lvgl:
|
||||
border_width: 4
|
||||
clip_corner: false
|
||||
color_filter_opa: transp
|
||||
drop_shadow_color: 0x000000
|
||||
drop_shadow_offset_x: 5
|
||||
drop_shadow_offset_y: 5
|
||||
drop_shadow_opa: cover
|
||||
drop_shadow_quality: precision
|
||||
drop_shadow_radius: 10
|
||||
ext_click_area: 100px
|
||||
height: 50%
|
||||
image_opa: cover
|
||||
image_recolor: light_blue
|
||||
image_recolor_opa: cover
|
||||
line_width: 10
|
||||
@@ -344,6 +407,10 @@ lvgl:
|
||||
line_dash_gap: 10
|
||||
line_rounded: false
|
||||
line_color: light_blue
|
||||
margin_bottom: 4
|
||||
margin_left: 4
|
||||
margin_right: 4
|
||||
margin_top: 4
|
||||
opa: cover
|
||||
opa_layered: cover
|
||||
outline_color: light_blue
|
||||
@@ -353,8 +420,12 @@ lvgl:
|
||||
pad_all: 10px
|
||||
pad_bottom: 10px
|
||||
pad_left: 10px
|
||||
pad_radial: 0
|
||||
pad_right: 10px
|
||||
pad_top: 10px
|
||||
recolor: 0xFF0000
|
||||
recolor_opa: transp
|
||||
rotary_sensitivity: 256
|
||||
shadow_color: light_blue
|
||||
shadow_opa: cover
|
||||
shadow_spread: 5
|
||||
@@ -366,6 +437,9 @@ lvgl:
|
||||
text_letter_space: 4
|
||||
text_line_space: 4
|
||||
text_opa: cover
|
||||
text_outline_stroke_color: 0x000000
|
||||
text_outline_stroke_opa: cover
|
||||
text_outline_stroke_width: 2
|
||||
transform_rotation: 90
|
||||
transform_height: 100
|
||||
transform_pivot_x: 50%
|
||||
@@ -375,8 +449,10 @@ lvgl:
|
||||
transform_scale_y: 0.8
|
||||
transform_skew_x: 10
|
||||
transform_skew_y: 20
|
||||
transform_width: 100
|
||||
shadow_offset_x: 3
|
||||
shadow_offset_y: 3
|
||||
translate_radial: 0
|
||||
translate_x: 10
|
||||
translate_y: 10
|
||||
max_height: 100
|
||||
@@ -481,6 +557,7 @@ lvgl:
|
||||
image:
|
||||
src: cat_image
|
||||
align: top_left
|
||||
bitmap_mask_src: alert
|
||||
on_click:
|
||||
- lvgl.widget.focus: spin_up
|
||||
- lvgl.widget.focus: next
|
||||
@@ -559,6 +636,76 @@ lvgl:
|
||||
logger.log: Button clicked
|
||||
on_long_press_repeat:
|
||||
logger.log: Button clicked
|
||||
on_pressing:
|
||||
logger.log: Button pressing
|
||||
on_press_lost:
|
||||
logger.log: Button press lost
|
||||
on_single_click:
|
||||
logger.log: Button single clicked
|
||||
on_double_click:
|
||||
logger.log: Button double clicked
|
||||
on_triple_click:
|
||||
logger.log: Button triple clicked
|
||||
on_scroll_throw_begin:
|
||||
logger.log: Scroll throw begin
|
||||
on_gesture:
|
||||
logger.log: Gesture detected
|
||||
on_key:
|
||||
logger.log: Key event
|
||||
on_rotary:
|
||||
logger.log: Rotary event
|
||||
on_leave:
|
||||
logger.log: Leave event
|
||||
on_hit_test:
|
||||
logger.log: Hit test
|
||||
on_indev_reset:
|
||||
logger.log: Indev reset
|
||||
on_hover_over:
|
||||
logger.log: Hover over
|
||||
on_hover_leave:
|
||||
logger.log: Hover leave
|
||||
on_cover_check:
|
||||
logger.log: Cover check
|
||||
on_refr_ext_draw_size:
|
||||
logger.log: Refr ext draw size
|
||||
on_draw_main_begin:
|
||||
logger.log: Draw main begin
|
||||
on_draw_main:
|
||||
logger.log: Draw main
|
||||
on_draw_main_end:
|
||||
logger.log: Draw main end
|
||||
on_draw_post_begin:
|
||||
logger.log: Draw post begin
|
||||
on_draw_post:
|
||||
logger.log: Draw post
|
||||
on_draw_post_end:
|
||||
logger.log: Draw post end
|
||||
on_draw_task_add:
|
||||
logger.log: Draw task add
|
||||
on_insert:
|
||||
logger.log: Insert event
|
||||
on_refresh:
|
||||
logger.log: Refresh event
|
||||
on_state_change:
|
||||
logger.log: State changed
|
||||
on_create:
|
||||
logger.log: Create event
|
||||
on_delete:
|
||||
logger.log: Delete event
|
||||
on_child_change:
|
||||
logger.log: Child changed
|
||||
on_child_create:
|
||||
logger.log: Child created
|
||||
on_child_delete:
|
||||
logger.log: Child deleted
|
||||
on_size_change:
|
||||
logger.log: Size changed
|
||||
on_style_change:
|
||||
logger.log: Style changed
|
||||
on_layout_change:
|
||||
logger.log: Layout changed
|
||||
on_get_self_size:
|
||||
logger.log: Get self size
|
||||
- led:
|
||||
id: lv_led
|
||||
color: 0x00FF00
|
||||
@@ -1051,7 +1198,7 @@ lvgl:
|
||||
- ticks:
|
||||
width: 1
|
||||
count: 61
|
||||
length: 20%
|
||||
length: 20
|
||||
radial_offset: 5
|
||||
color: 0xFFFFFF
|
||||
major:
|
||||
@@ -1159,6 +1306,13 @@ image:
|
||||
type: BINARY
|
||||
transparency: chroma_key
|
||||
|
||||
- id: alert
|
||||
file: $component_dir/logo-text.svg
|
||||
type: grayscale
|
||||
resize: 100x100
|
||||
invert_alpha: true
|
||||
transparency: alpha_channel
|
||||
|
||||
color:
|
||||
- id: light_blue
|
||||
hex: "3340FF"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
mcp23008:
|
||||
i2c_id: i2c_bus
|
||||
id: mcp23008_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23008_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23008_hub_int
|
||||
address: 0x21
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -9,6 +13,12 @@ binary_sensor:
|
||||
mcp23xxx: mcp23008_hub
|
||||
number: 0
|
||||
mode: INPUT
|
||||
- platform: gpio
|
||||
id: mcp23008_binary_sensor_int
|
||||
pin:
|
||||
mcp23xxx: mcp23008_hub_int
|
||||
number: 0
|
||||
mode: INPUT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
mcp23017:
|
||||
i2c_id: i2c_bus
|
||||
id: mcp23017_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23017_hub
|
||||
- i2c_id: i2c_bus
|
||||
id: mcp23017_hub_int
|
||||
address: 0x21
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
@@ -9,6 +13,12 @@ binary_sensor:
|
||||
mcp23xxx: mcp23017_hub
|
||||
number: 0
|
||||
mode: INPUT
|
||||
- platform: gpio
|
||||
id: mcp23017_binary_sensor_int
|
||||
pin:
|
||||
mcp23xxx: mcp23017_hub_int
|
||||
number: 0
|
||||
mode: INPUT
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -2,3 +2,4 @@ mcp23s08:
|
||||
- id: mcp23s08_hub
|
||||
cs_pin: ${cs_pin}
|
||||
deviceaddress: 0
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO15
|
||||
interrupt_pin: GPIO0
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
|
||||
@@ -2,3 +2,4 @@ mcp23s17:
|
||||
- id: mcp23s17_hub
|
||||
cs_pin: ${cs_pin}
|
||||
deviceaddress: 0
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp32-idf.yaml
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO15
|
||||
interrupt_pin: GPIO0
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
substitutions:
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
spi: !include ../../test_build_components/common/spi/rp2040-ard.yaml
|
||||
|
||||
@@ -61,3 +61,8 @@ media_player:
|
||||
- media_player.volume_up:
|
||||
- media_player.volume_down:
|
||||
- media_player.volume_set: 50%
|
||||
- media_player.enqueue: http://localhost/media.mp3
|
||||
- media_player.enqueue: !lambda 'return "http://localhost/media.mp3";'
|
||||
- media_player.enqueue:
|
||||
media_url: http://localhost/media.mp3
|
||||
announcement: true
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
#include "../common.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105::testing {
|
||||
|
||||
struct TestContext {
|
||||
MockUARTComponent uart;
|
||||
uart::UARTDevice device{&uart};
|
||||
TestableMitsubishiCN105 sut{device};
|
||||
|
||||
TestContext() { this->sut.set_current_time(0); }
|
||||
};
|
||||
|
||||
TEST(MitsubishiCN105Tests, InitSendsConnectPacket) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_current_time(123);
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::NOT_CONNECTED);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
|
||||
ctx.sut.initialize();
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{123});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Connect response
|
||||
ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55});
|
||||
|
||||
ctx.sut.set_current_time(200);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
|
||||
// All bytes from UART should be consumed
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
// After successful connect we request status, first settings (0x02)
|
||||
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.write_timeout_start_ms_, std::optional<uint32_t>{200});
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Clear TX bytes.
|
||||
ctx.uart.tx.clear();
|
||||
|
||||
// Settings response
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07,
|
||||
0x00, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x99});
|
||||
|
||||
// Settings should still have initial values
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan());
|
||||
EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::UNKNOWN);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::UNKNOWN);
|
||||
|
||||
ctx.sut.set_current_time(300);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
|
||||
// Check settings that we just read from received package
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f);
|
||||
EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::AUTO);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::AUTO);
|
||||
|
||||
// Now fetch room temperature (0x03)
|
||||
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.write_timeout_start_ms_, std::optional<uint32_t>{300});
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Clear TX bytes.
|
||||
ctx.uart.tx.clear();
|
||||
|
||||
// Room temperature 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});
|
||||
|
||||
// Room temperature should still have initial value
|
||||
EXPECT_THAT(ctx.sut.status().room_temperature, ::testing::IsNan());
|
||||
|
||||
ctx.sut.set_current_time(400);
|
||||
EXPECT_FALSE(ctx.sut.is_status_initialized());
|
||||
ASSERT_TRUE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
EXPECT_TRUE(ctx.sut.is_status_initialized());
|
||||
|
||||
// Check room temperature we just read from received package
|
||||
EXPECT_EQ(ctx.sut.status().room_temperature, 21.0f);
|
||||
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value());
|
||||
EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{400});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// No response (no RX data), no retry yet
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
|
||||
// Still no response after 1999ms, no retry yet
|
||||
ctx.sut.set_current_time(1999);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{0});
|
||||
|
||||
// Stop waiting after 2s and retry connect
|
||||
ctx.sut.set_current_time(2000);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8));
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{2000});
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// RX noise/unexpected traffic
|
||||
ctx.uart.push_rx({0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
||||
0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
|
||||
0x1D, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A,
|
||||
0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
|
||||
0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46});
|
||||
|
||||
// Make sure we have enough bytes in buffer.
|
||||
ASSERT_GT(ctx.uart.rx.size(), 64);
|
||||
|
||||
// No valid response, no state change expected
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
|
||||
// Watchdog interrupts reading (max. 64 bytes at once) so we do not spend the whole loop draining UART
|
||||
EXPECT_FALSE(ctx.uart.rx.empty());
|
||||
|
||||
// Next update will read remaining bytes, no state change expected
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.initialize();
|
||||
ctx.uart.tx.clear(); // Remove first connect packet bytes
|
||||
|
||||
// Mixed RX stream with partial, malformed, and oversized frames to test parser robustness
|
||||
ctx.uart.push_rx({// ─────────────────────────────
|
||||
// Noise (no 0xFC) -> should be ignored via preamble reset
|
||||
// ────────────────────────────
|
||||
0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Partial frame (declares payload len=5, but we cut it short)
|
||||
// Later bytes will eventually force checksum mismatch and reset
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x05, 0xAA, 0xBB,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Invalid header (header byte 3 should be 0x01, header byte 4 should be 0x30)
|
||||
// Should reset quickly on header mismatch
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0xFF, 0xFF, 0x02, 0x01, 0x02, 0x00,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Oversized length field (rejected by payload-too-large check at HEADER_LEN)
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0xFE, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A,
|
||||
0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Valid unknown-type frame (type=0x62), should be parsed successfully then ignored
|
||||
// Frame: FC 62 01 30 02 AA BB 30
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x02, 0xAA, 0xBB, 0x30,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Invalid checksum (should be rejected at checksum check)
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x02, 0x10, 0x20, 0xFF,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Back-to-back VALID frames (unknown type=0x62) to stress boundary handling.
|
||||
// Frame A: FC 62 01 30 01 02 6C
|
||||
// Frame B: FC 62 01 30 01 03 6B
|
||||
// ─────────────────────────────
|
||||
0xFC, 0x62, 0x01, 0x30, 0x01, 0x02, 0x6C, 0xFC, 0x62, 0x01, 0x30, 0x01, 0x03, 0x6B,
|
||||
|
||||
// ─────────────────────────────
|
||||
// Trailing noise
|
||||
// ─────────────────────────────
|
||||
0x55, 0x66, 0x77, 0x88});
|
||||
|
||||
// Drain RX - no valid response, no state change expected
|
||||
int iterations = 0;
|
||||
while (!ctx.uart.rx.empty() && iterations++ < 10) {
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING);
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
}
|
||||
|
||||
EXPECT_TRUE(ctx.uart.rx.empty());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_update_interval(2000);
|
||||
ctx.sut.set_current_time(80000);
|
||||
|
||||
// No scheduled status update
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
|
||||
// Status update completed, schedule next status update
|
||||
ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED;
|
||||
ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{80000});
|
||||
|
||||
// Wait for update_interval (ms) before doing another status update
|
||||
ASSERT_FALSE(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(81999);
|
||||
ASSERT_FALSE(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(82000);
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_FALSE(ctx.uart.tx.empty());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx(
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_TRUE(ctx.sut.status().power_on);
|
||||
EXPECT_FALSE(ctx.sut.use_temperature_encoding_b_);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f);
|
||||
EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::COOL);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::QUIET);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx(
|
||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xAD});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||
EXPECT_TRUE(ctx.sut.use_temperature_encoding_b_);
|
||||
EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f);
|
||||
EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::FAN_ONLY);
|
||||
EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::SPEED_4);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5D});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_EQ(ctx.sut.status().room_temperature, 16.0f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xA7});
|
||||
|
||||
ctx.sut.update();
|
||||
|
||||
EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f);
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_power(true);
|
||||
ctx.sut.apply_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, ApplySettingsTemperatureEncodedA) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_target_temperature(23.0f);
|
||||
ctx.sut.apply_settings();
|
||||
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x08,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.use_temperature_encoding_b_ = true;
|
||||
ctx.sut.set_target_temperature(26.0f);
|
||||
ctx.sut.apply_settings();
|
||||
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x00, 0xC5));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ApplyModeCool) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_mode(MitsubishiCN105::Mode::COOL);
|
||||
ctx.sut.apply_settings();
|
||||
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x02, 0x00, 0x00, 0x03, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::SPEED_1);
|
||||
ctx.sut.apply_settings();
|
||||
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00,
|
||||
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
// Waiting for next scheduled status update
|
||||
ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED;
|
||||
ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
|
||||
// Nothing to do in update (rx empty, no timeout)
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_TRUE(ctx.uart.tx.empty());
|
||||
|
||||
// Write new values
|
||||
ctx.sut.use_temperature_encoding_b_ = true;
|
||||
ctx.sut.set_power(false);
|
||||
ctx.sut.set_target_temperature(25.0f);
|
||||
ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT);
|
||||
ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO);
|
||||
|
||||
// Waiting for next status update must be interrupted and new values send to AC
|
||||
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, 0x0F, 0x00, 0x00, 0x01, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB));
|
||||
|
||||
// Write ACK response
|
||||
ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E});
|
||||
ASSERT_FALSE(ctx.sut.update());
|
||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||
}
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105::testing
|
||||
@@ -0,0 +1,7 @@
|
||||
#include "common.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; }
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105
|
||||
@@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.h"
|
||||
|
||||
namespace esphome::mitsubishi_cn105::testing {
|
||||
|
||||
class MockUARTComponent : public uart::UARTComponent {
|
||||
public:
|
||||
std::vector<uint8_t> tx;
|
||||
std::vector<uint8_t> rx;
|
||||
|
||||
void push_rx(std::initializer_list<uint8_t> data) { this->rx.insert(this->rx.end(), data.begin(), data.end()); }
|
||||
|
||||
// UARTComponent
|
||||
void write_array(const uint8_t *data, size_t len) override { this->tx.insert(this->tx.end(), data, data + len); }
|
||||
|
||||
bool read_array(uint8_t *data, size_t len) override {
|
||||
if (this->rx.size() < len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::copy(this->rx.begin(), this->rx.begin() + len, data);
|
||||
this->rx.erase(this->rx.begin(), this->rx.begin() + len);
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t available() override { return this->rx.size(); }
|
||||
|
||||
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
|
||||
MOCK_METHOD(uart::UARTFlushResult, flush, (), (override));
|
||||
MOCK_METHOD(void, check_logger_conflict, (), (override));
|
||||
};
|
||||
|
||||
class TestableMitsubishiCN105 : public MitsubishiCN105 {
|
||||
public:
|
||||
using MitsubishiCN105::MitsubishiCN105;
|
||||
using MitsubishiCN105::State;
|
||||
using MitsubishiCN105::state_;
|
||||
using MitsubishiCN105::write_timeout_start_ms_;
|
||||
using MitsubishiCN105::status_update_start_ms_;
|
||||
using MitsubishiCN105::use_temperature_encoding_b_;
|
||||
|
||||
void set_state(State s) { this->set_state_(s); }
|
||||
void apply_settings() { this->apply_settings_(); }
|
||||
|
||||
static inline uint32_t test_loop_time_ms = 0;
|
||||
|
||||
void set_current_time(uint32_t ms) { test_loop_time_ms = ms; }
|
||||
};
|
||||
|
||||
} // namespace esphome::mitsubishi_cn105::testing
|
||||
@@ -0,0 +1,4 @@
|
||||
climate:
|
||||
- platform: mitsubishi_cn105
|
||||
name: "AC Test"
|
||||
uart_id: uart_bus
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart_9600_even: !include ../../test_build_components/common/uart_9600_even/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -0,0 +1,4 @@
|
||||
packages:
|
||||
uart_9600_even: !include ../../test_build_components/common/uart_9600_even/rp2040-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -3,6 +3,11 @@ pca9554:
|
||||
i2c_id: i2c_bus
|
||||
pin_count: 8
|
||||
address: 0x3F
|
||||
- id: pca9554_hub_int
|
||||
i2c_id: i2c_bus
|
||||
pin_count: 8
|
||||
address: 0x3E
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -3,6 +3,11 @@ pcf8574:
|
||||
i2c_id: i2c_bus
|
||||
address: 0x21
|
||||
pcf8575: false
|
||||
- id: pcf8574_hub_int
|
||||
i2c_id: i2c_bus
|
||||
address: 0x22
|
||||
pcf8575: false
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
substitutions:
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
pi4ioe5v6408:
|
||||
i2c_id: i2c_bus
|
||||
id: pi4ioe1
|
||||
address: 0x44
|
||||
- i2c_id: i2c_bus
|
||||
id: pi4ioe1
|
||||
address: 0x44
|
||||
- i2c_id: i2c_bus
|
||||
id: pi4ioe1_int
|
||||
address: 0x45
|
||||
interrupt_pin: ${interrupt_pin}
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
@@ -16,3 +20,8 @@ binary_sensor:
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1
|
||||
number: 1
|
||||
- platform: gpio
|
||||
id: sensor1_int
|
||||
pin:
|
||||
pi4ioe5v6408: pi4ioe1_int
|
||||
number: 1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
i2c_sda: GPIO21
|
||||
i2c_scl: GPIO22
|
||||
interrupt_pin: GPIO15
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
substitutions:
|
||||
i2c_sda: GPIO4
|
||||
i2c_scl: GPIO5
|
||||
interrupt_pin: GPIO2
|
||||
|
||||
packages:
|
||||
i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import pytest
|
||||
|
||||
from esphome.components import socket
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def _setup_platform(platform=PLATFORM_ESP8266) -> None:
|
||||
"""Set up CORE.data with a platform for testing."""
|
||||
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__first_call() -> None:
|
||||
"""Test that first call sets up define and consumes socket."""
|
||||
_setup_platform()
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify CORE.data was updated
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
|
||||
# Verify the define was added
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__idempotent() -> None:
|
||||
"""Test that subsequent calls are idempotent."""
|
||||
# Set up initial state as if already called
|
||||
CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
|
||||
CORE.config = {"ethernet": True}
|
||||
|
||||
# Call again - should not raise or fail
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify state is still True
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
|
||||
# Define should not be added since flag was already True
|
||||
assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__multiple_calls() -> None:
|
||||
"""Test that multiple calls only set up once."""
|
||||
_setup_platform()
|
||||
# Call three times
|
||||
CORE.config = {"openthread": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
socket.require_wake_loop_threadsafe()
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify CORE.data was set
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
|
||||
# Verify the define was added (only once, but we can just check it exists)
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__no_networking() -> None:
|
||||
"""Test that wake loop is NOT configured when no networking is configured."""
|
||||
# Set up config without any networking components
|
||||
CORE.config = {"esphome": {"name": "test"}, "logger": {}}
|
||||
|
||||
# Call require_wake_loop_threadsafe
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify CORE.data flag was NOT set (since has_networking returns False)
|
||||
assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data
|
||||
|
||||
# Verify the define was NOT added
|
||||
assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None:
|
||||
"""Test that no socket is consumed when no networking is configured."""
|
||||
# Set up config without any networking components
|
||||
CORE.config = {"logger": {}}
|
||||
|
||||
# Track initial socket consumer state
|
||||
initial_udp = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
|
||||
# Call require_wake_loop_threadsafe
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify no socket was consumed
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
assert udp_consumers == initial_udp
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"platform",
|
||||
[PLATFORM_ESP32, PLATFORM_BK72XX, PLATFORM_RTL87XX, PLATFORM_LN882X],
|
||||
)
|
||||
def test_require_wake_loop_threadsafe__fast_select_no_udp_socket(
|
||||
platform: str,
|
||||
) -> None:
|
||||
"""Test that fast select platforms use task notifications instead of UDP socket."""
|
||||
_setup_platform(platform)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify the define was added
|
||||
assert CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] is True
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
# Verify no UDP socket was consumed (fast select platforms use FreeRTOS task notifications)
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert "socket.wake_loop_threadsafe" not in udp_consumers
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__non_fast_select_consumes_udp_socket() -> None:
|
||||
"""Test that platforms without fast select consume a UDP socket for wake notifications."""
|
||||
_setup_platform(PLATFORM_ESP8266)
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify UDP socket was consumed
|
||||
udp_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS_UDP, {})
|
||||
assert udp_consumers.get("socket.wake_loop_threadsafe") == 1
|
||||
@@ -6,5 +6,9 @@ api:
|
||||
|
||||
time:
|
||||
- platform: homeassistant
|
||||
on_time:
|
||||
- seconds: "0,10,20,30,40,50"
|
||||
then:
|
||||
- logger.log: "CronTrigger fired (every 10 seconds)"
|
||||
- platform: sntp
|
||||
id: sntp_time
|
||||
|
||||
@@ -758,6 +758,115 @@ TEST(PosixTzParser, EpochToLocalDstTransition) {
|
||||
EXPECT_EQ(local.tm_isdst, 1);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Leap year edge cases for closed-form year arithmetic
|
||||
// ============================================================================
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalLeapYear2000) {
|
||||
// 2000 is a leap year (divisible by 400)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
|
||||
|
||||
// Feb 29, 2000 12:00:00 UTC
|
||||
time_t epoch = make_utc(2000, 2, 29, 12);
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 100); // 2000
|
||||
EXPECT_EQ(local.tm_mon, 1); // February
|
||||
EXPECT_EQ(local.tm_mday, 29);
|
||||
EXPECT_EQ(local.tm_hour, 12);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalNonLeapYear2100) {
|
||||
// 2100 is NOT a leap year (divisible by 100 but not 400)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
|
||||
|
||||
// Mar 1, 2100 00:00:00 UTC — the day after what would be Feb 29
|
||||
time_t epoch = make_utc(2100, 3, 1);
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 200); // 2100
|
||||
EXPECT_EQ(local.tm_mon, 2); // March
|
||||
EXPECT_EQ(local.tm_mday, 1);
|
||||
|
||||
// Feb 28, 2100 23:59:59 UTC — last second of February (no Feb 29)
|
||||
epoch = make_utc(2100, 2, 28, 23, 59, 59);
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 200);
|
||||
EXPECT_EQ(local.tm_mon, 1); // February
|
||||
EXPECT_EQ(local.tm_mday, 28);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalLeapYear2400) {
|
||||
// 2400 is a leap year (divisible by 400)
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
|
||||
|
||||
time_t epoch = make_utc(2400, 2, 29, 6);
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 500); // 2400
|
||||
EXPECT_EQ(local.tm_mon, 1); // February
|
||||
EXPECT_EQ(local.tm_mday, 29);
|
||||
EXPECT_EQ(local.tm_hour, 6);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalNewYearBoundaries) {
|
||||
// Test year boundary — last second of 2099 and first second of 2100
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
|
||||
struct tm local;
|
||||
|
||||
// Dec 31, 2099 23:59:59 UTC
|
||||
time_t epoch = make_utc(2099, 12, 31, 23, 59, 59);
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 199); // 2099
|
||||
EXPECT_EQ(local.tm_mon, 11); // December
|
||||
EXPECT_EQ(local.tm_mday, 31);
|
||||
|
||||
// Jan 1, 2100 00:00:00 UTC
|
||||
epoch = make_utc(2100, 1, 1);
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 200); // 2100
|
||||
EXPECT_EQ(local.tm_mon, 0); // January
|
||||
EXPECT_EQ(local.tm_mday, 1);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalDstAcrossCenturyBoundary) {
|
||||
// DST transition in year 2100 (non-leap) with US Eastern rules
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz));
|
||||
|
||||
// July 4, 2100 16:00 UTC = 12:00 EDT
|
||||
time_t epoch = make_utc(2100, 7, 4, 16);
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_hour, 12);
|
||||
EXPECT_EQ(local.tm_isdst, 1);
|
||||
|
||||
// Jan 15, 2100 10:00 UTC = 05:00 EST
|
||||
epoch = make_utc(2100, 1, 15, 10);
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_hour, 5);
|
||||
EXPECT_EQ(local.tm_isdst, 0);
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, EpochToLocalFarFutureYear5000) {
|
||||
// Year 5000 — days/365 estimate overshoots by ~2 years due to leap days,
|
||||
// requiring multiple correction steps in days_to_year.
|
||||
ParsedTimezone tz;
|
||||
ASSERT_TRUE(parse_posix_tz("UTC0", tz));
|
||||
|
||||
time_t epoch = make_utc(5000, 6, 15, 12);
|
||||
struct tm local;
|
||||
ASSERT_TRUE(epoch_to_local_tm(epoch, tz, &local));
|
||||
EXPECT_EQ(local.tm_year, 3100); // 5000
|
||||
EXPECT_EQ(local.tm_mon, 5); // June
|
||||
EXPECT_EQ(local.tm_mday, 15);
|
||||
EXPECT_EQ(local.tm_hour, 12);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Verification against libc
|
||||
// ============================================================================
|
||||
|
||||
@@ -5,3 +5,5 @@ display:
|
||||
intensity: 3
|
||||
lambda: |-
|
||||
it.print("1234");
|
||||
static const uint8_t buf[] = {0x3f, 0x06, 0x5b, 0x4f | 0x80};
|
||||
it.set_buffer(buf, sizeof(buf));
|
||||
|
||||
@@ -198,6 +198,13 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
|
||||
' - "-g" # Add debug symbols',
|
||||
)
|
||||
|
||||
# Replace external component path placeholder if present
|
||||
if "EXTERNAL_COMPONENT_PATH" in content:
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
content = content.replace("EXTERNAL_COMPONENT_PATH", external_components_path)
|
||||
|
||||
return content
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
esphome:
|
||||
name: test-multi-click
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms
|
||||
services:
|
||||
- service: run_all_tests
|
||||
then:
|
||||
# Prime the binary sensor with an initial OFF state.
|
||||
# trigger_on_initial_state defaults to false, so the first
|
||||
# state change from unknown won't fire callbacks.
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: false
|
||||
- delay: 50ms
|
||||
|
||||
# Test 1: Single click (ON < 50ms, OFF >= 30ms)
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: true
|
||||
- delay: 20ms
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: false
|
||||
# Wait for single click trigger (30ms) + cooldown (100ms) + margin
|
||||
- delay: 200ms
|
||||
|
||||
# Test 2: Double click (ON < 50ms, OFF < 25ms, ON < 50ms, OFF >= 25ms)
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: true
|
||||
- delay: 20ms
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: false
|
||||
- delay: 15ms
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: true
|
||||
- delay: 20ms
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: false
|
||||
# Wait for double click trigger (25ms) + cooldown (100ms) + margin
|
||||
- delay: 200ms
|
||||
|
||||
# Test 3: Long press (ON >= 80ms)
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: true
|
||||
- delay: 100ms
|
||||
- binary_sensor.template.publish:
|
||||
id: test_button
|
||||
state: false
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
globals:
|
||||
- id: single_click_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
- id: double_click_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
- id: long_press_count
|
||||
type: int
|
||||
initial_value: "0"
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: "Test Button"
|
||||
id: test_button
|
||||
on_multi_click:
|
||||
# Single press
|
||||
- timing:
|
||||
- ON for at most 50ms
|
||||
- OFF for at least 30ms
|
||||
invalid_cooldown: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
id(single_click_count) += 1;
|
||||
ESP_LOGI("multi_click_test", "SINGLE_CLICK count=%d", id(single_click_count));
|
||||
|
||||
# Double press
|
||||
- timing:
|
||||
- ON for at most 50ms
|
||||
- OFF for at most 25ms
|
||||
- ON for at most 50ms
|
||||
- OFF for at least 25ms
|
||||
invalid_cooldown: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
id(double_click_count) += 1;
|
||||
ESP_LOGI("multi_click_test", "DOUBLE_CLICK count=%d", id(double_click_count));
|
||||
|
||||
# Long press
|
||||
- timing:
|
||||
- ON for at least 80ms
|
||||
invalid_cooldown: 100ms
|
||||
then:
|
||||
- lambda: |-
|
||||
id(long_press_count) += 1;
|
||||
ESP_LOGI("multi_click_test", "LONG_PRESS count=%d", id(long_press_count));
|
||||
@@ -22,6 +22,8 @@ uart_mock:
|
||||
baud_rate: 9600
|
||||
rx_full_threshold: 120
|
||||
rx_timeout: 2
|
||||
# auto_start must be false to avoid races: the test presses the
|
||||
# "Start Scenario" button only after subscribing to states.
|
||||
auto_start: false
|
||||
debug:
|
||||
responses:
|
||||
@@ -46,7 +48,7 @@ modbus:
|
||||
modbus_controller:
|
||||
- address: 1
|
||||
id: modbus_controller_ok
|
||||
max_cmd_retries: 0
|
||||
max_cmd_retries: 2
|
||||
update_interval: 1s
|
||||
- address: 2
|
||||
id: modbus_controller_slow
|
||||
@@ -89,4 +91,4 @@ button:
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
||||
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||
|
||||
@@ -22,6 +22,8 @@ uart:
|
||||
uart_mock:
|
||||
- id: virtual_uart_dev
|
||||
baud_rate: 9600
|
||||
# auto_start must be false to avoid races: the test presses the
|
||||
# "Start Scenario" button only after subscribing to states.
|
||||
auto_start: false
|
||||
debug:
|
||||
on_tx:
|
||||
@@ -40,7 +42,8 @@ uart_mock:
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
|
||||
- uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency)
|
||||
delay: 40ms
|
||||
data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
data:
|
||||
!lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC,
|
||||
0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B};
|
||||
|
||||
@@ -61,4 +64,4 @@ button:
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
||||
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-server-test
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
- id: virtual_uart_dev
|
||||
baud_rate: 9600
|
||||
rx_full_threshold: 120
|
||||
rx_timeout: 2
|
||||
auto_start: false
|
||||
debug:
|
||||
injections:
|
||||
- delay: 100ms
|
||||
inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read)
|
||||
- delay: 100ms
|
||||
# Read holding register 7 on device 2
|
||||
# Reply from device 2
|
||||
# Read holding register 5 on device 1 (read_after_peer_response)
|
||||
inject_rx:
|
||||
[
|
||||
0x02,
|
||||
0x03,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x01,
|
||||
0x35,
|
||||
0xF8,
|
||||
0x02,
|
||||
0x03,
|
||||
0x02,
|
||||
0x00,
|
||||
0xF0,
|
||||
0xFC,
|
||||
0x00,
|
||||
0x01,
|
||||
0x03,
|
||||
0x00,
|
||||
0x05,
|
||||
0x00,
|
||||
0x01,
|
||||
0x94,
|
||||
0x0B,
|
||||
]
|
||||
- delay: 100ms
|
||||
inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2, with no response
|
||||
- delay: 100ms
|
||||
# Read holding register 7 on device 2, with no response
|
||||
# Read holding register A on device 1 (read_after_peer_timeout)
|
||||
inject_rx:
|
||||
[
|
||||
0x02,
|
||||
0x03,
|
||||
0x00,
|
||||
0x07,
|
||||
0x00,
|
||||
0x01,
|
||||
0x35,
|
||||
0xF8,
|
||||
0x01,
|
||||
0x03,
|
||||
0x00,
|
||||
0x0A,
|
||||
0x00,
|
||||
0x01,
|
||||
0xA4,
|
||||
0x08,
|
||||
]
|
||||
|
||||
modbus:
|
||||
uart_id: virtual_uart_dev
|
||||
role: server
|
||||
|
||||
modbus_controller:
|
||||
- address: 1
|
||||
server_registers:
|
||||
- address: 0x03
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(basic_read).publish_state(1);
|
||||
return 1;
|
||||
- address: 0x05
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(read_after_peer_response).publish_state(1);
|
||||
return 1;
|
||||
- address: 0x0A
|
||||
value_type: U_WORD
|
||||
read_lambda: |-
|
||||
id(read_after_peer_timeout).publish_state(1);
|
||||
return 1;
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "basic_read"
|
||||
id: basic_read
|
||||
- platform: template
|
||||
name: "read_after_peer_response"
|
||||
id: read_after_peer_response
|
||||
- platform: template
|
||||
name: "read_after_peer_timeout"
|
||||
id: read_after_peer_timeout
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||
@@ -0,0 +1,180 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-server-contro
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
- id: virtual_uart_server
|
||||
baud_rate: 9600
|
||||
# auto_start must be true for loopback fixtures: the modbus controller
|
||||
# polls on its update_interval immediately at boot, so the uart_mock
|
||||
# forwarding must already be active or early requests are lost and
|
||||
# generate modbus warnings.
|
||||
auto_start: true
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_controller
|
||||
data: !lambda return data;
|
||||
- id: virtual_uart_controller
|
||||
baud_rate: 9600
|
||||
auto_start: true # See comment on virtual_uart_server above
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_server
|
||||
data: !lambda return data;
|
||||
|
||||
modbus:
|
||||
- uart_id: virtual_uart_server
|
||||
id: virtual_modbus_server
|
||||
role: server
|
||||
- uart_id: virtual_uart_controller
|
||||
id: virtual_modbus_controller
|
||||
role: client
|
||||
turnaround_time: 10ms
|
||||
|
||||
modbus_controller:
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_controller
|
||||
update_interval: 1s
|
||||
id: modbus_controller_1
|
||||
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_server
|
||||
id: modbus_server_1
|
||||
server_registers:
|
||||
- address: 0x01
|
||||
value_type: U_WORD
|
||||
read_lambda: return 99;
|
||||
- address: 0x03
|
||||
value_type: S_WORD
|
||||
read_lambda: return -99;
|
||||
- address: 0x05
|
||||
value_type: U_DWORD
|
||||
read_lambda: return 16909060;
|
||||
- address: 0x08
|
||||
value_type: S_DWORD
|
||||
read_lambda: return -16909060;
|
||||
- address: 0x0B
|
||||
value_type: U_DWORD_R
|
||||
read_lambda: return 67305985;
|
||||
- address: 0x0E
|
||||
value_type: S_DWORD_R
|
||||
read_lambda: return -67305985;
|
||||
- address: 0x11
|
||||
value_type: U_QWORD
|
||||
read_lambda: return 72623859790382856;
|
||||
- address: 0x16
|
||||
value_type: S_QWORD
|
||||
read_lambda: return -72623859790382856;
|
||||
- address: 0x1B
|
||||
value_type: U_QWORD_R
|
||||
read_lambda: return 578437695752307201;
|
||||
- address: 0x20
|
||||
value_type: S_QWORD_R
|
||||
read_lambda: return -578437695752307201;
|
||||
- address: 0x25
|
||||
value_type: FP32
|
||||
read_lambda: return 3.14;
|
||||
- address: 0x28
|
||||
value_type: FP32_R
|
||||
read_lambda: return 3.14;
|
||||
|
||||
sensor:
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_word"
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_word"
|
||||
address: 0x03
|
||||
register_type: holding
|
||||
value_type: S_WORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_dword"
|
||||
address: 0x05
|
||||
register_type: holding
|
||||
value_type: U_DWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_dword"
|
||||
address: 0x08
|
||||
register_type: holding
|
||||
value_type: S_DWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_dword_r"
|
||||
address: 0x0B
|
||||
register_type: holding
|
||||
value_type: U_DWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_dword_r"
|
||||
address: 0x0E
|
||||
register_type: holding
|
||||
value_type: S_DWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_qword"
|
||||
address: 0x11
|
||||
register_type: holding
|
||||
value_type: U_QWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_qword"
|
||||
address: 0x16
|
||||
register_type: holding
|
||||
value_type: S_QWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_qword_r"
|
||||
address: 0x1B
|
||||
register_type: holding
|
||||
value_type: U_QWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_qword_r"
|
||||
address: 0x20
|
||||
register_type: holding
|
||||
value_type: S_QWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_fp32"
|
||||
address: 0x25
|
||||
register_type: holding
|
||||
value_type: FP32
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_fp32_r"
|
||||
address: 0x28
|
||||
register_type: holding
|
||||
value_type: FP32_R
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: "id(virtual_uart_server).start_scenario();"
|
||||
- lambda: "id(virtual_uart_controller).start_scenario();"
|
||||
@@ -0,0 +1,118 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-server-mult
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
- id: virtual_uart_server
|
||||
baud_rate: 9600
|
||||
# auto_start must be true for loopback fixtures: the modbus controller
|
||||
# polls on its update_interval immediately at boot, so the uart_mock
|
||||
# forwarding must already be active or early requests are lost and
|
||||
# generate modbus warnings.
|
||||
auto_start: true
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_controller
|
||||
data: !lambda return data;
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_server_2
|
||||
data: !lambda return data;
|
||||
- id: virtual_uart_server_2
|
||||
baud_rate: 9600
|
||||
auto_start: true # See comment on virtual_uart_server above
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_server
|
||||
data: !lambda return data;
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_controller
|
||||
data: !lambda return data;
|
||||
- id: virtual_uart_controller
|
||||
baud_rate: 9600
|
||||
auto_start: true # See comment on virtual_uart_server above
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_server
|
||||
data: !lambda return data;
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_server_2
|
||||
data: !lambda return data;
|
||||
|
||||
modbus:
|
||||
- uart_id: virtual_uart_server
|
||||
id: virtual_modbus_server
|
||||
role: server
|
||||
- uart_id: virtual_uart_server_2
|
||||
id: virtual_modbus_server_2
|
||||
role: server
|
||||
- uart_id: virtual_uart_controller
|
||||
id: virtual_modbus_client
|
||||
role: client
|
||||
turnaround_time: 10ms
|
||||
|
||||
modbus_controller:
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_client
|
||||
update_interval: 1s
|
||||
id: modbus_controller_1
|
||||
- address: 2
|
||||
modbus_id: virtual_modbus_client
|
||||
update_interval: 1s
|
||||
id: modbus_controller_2
|
||||
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_server
|
||||
server_registers:
|
||||
- address: 0x01
|
||||
value_type: U_WORD
|
||||
read_lambda: return 919;
|
||||
- address: 2
|
||||
modbus_id: virtual_modbus_server_2
|
||||
server_registers:
|
||||
- address: 0x01
|
||||
value_type: U_WORD
|
||||
read_lambda: return 929;
|
||||
|
||||
sensor:
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_word"
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_2
|
||||
name: "reg_u_word_2"
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: "id(virtual_uart_server).start_scenario();"
|
||||
- lambda: "id(virtual_uart_server_2).start_scenario();"
|
||||
- lambda: "id(virtual_uart_controller).start_scenario();"
|
||||
@@ -0,0 +1,330 @@
|
||||
esphome:
|
||||
name: uart-mock-modbus-srv-write
|
||||
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
|
||||
# Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"]
|
||||
# The actual UART bus used is the uart_mock component below
|
||||
uart:
|
||||
baud_rate: 115200
|
||||
port: /dev/null
|
||||
|
||||
uart_mock:
|
||||
- id: virtual_uart_server
|
||||
baud_rate: 9600
|
||||
# auto_start must be true for loopback fixtures: the modbus controller
|
||||
# polls on its update_interval immediately at boot, so the uart_mock
|
||||
# forwarding must already be active or early requests are lost and
|
||||
# generate modbus warnings.
|
||||
auto_start: true
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_controller
|
||||
data: !lambda return data;
|
||||
- id: virtual_uart_controller
|
||||
baud_rate: 9600
|
||||
auto_start: true # See comment on virtual_uart_server above
|
||||
debug:
|
||||
on_tx:
|
||||
- then:
|
||||
- uart_mock.inject_rx:
|
||||
id: virtual_uart_server
|
||||
data: !lambda return data;
|
||||
|
||||
globals:
|
||||
- id: stored_u_word
|
||||
type: uint16_t
|
||||
initial_value: "11"
|
||||
- id: stored_s_word
|
||||
type: int16_t
|
||||
initial_value: "-11"
|
||||
- id: stored_u_dword
|
||||
type: uint32_t
|
||||
initial_value: "1001"
|
||||
- id: stored_s_dword
|
||||
type: int32_t
|
||||
initial_value: "-1001"
|
||||
- id: stored_u_dword_r
|
||||
type: uint32_t
|
||||
initial_value: "3003"
|
||||
- id: stored_s_dword_r
|
||||
type: int32_t
|
||||
initial_value: "-3003"
|
||||
- id: stored_u_qword
|
||||
type: uint64_t
|
||||
initial_value: "5005"
|
||||
- id: stored_s_qword
|
||||
type: int64_t
|
||||
initial_value: "-5005"
|
||||
- id: stored_u_qword_r
|
||||
type: uint64_t
|
||||
initial_value: "7007"
|
||||
- id: stored_s_qword_r
|
||||
type: int64_t
|
||||
initial_value: "-7007"
|
||||
- id: stored_fp32
|
||||
type: float
|
||||
initial_value: "1.5"
|
||||
- id: stored_fp32_r
|
||||
type: float
|
||||
initial_value: "2.5"
|
||||
|
||||
modbus:
|
||||
- uart_id: virtual_uart_server
|
||||
id: virtual_modbus_server
|
||||
role: server
|
||||
- uart_id: virtual_uart_controller
|
||||
id: virtual_modbus_controller
|
||||
role: client
|
||||
turnaround_time: 10ms
|
||||
|
||||
modbus_controller:
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_controller
|
||||
update_interval: 2s
|
||||
id: modbus_controller_1
|
||||
|
||||
- address: 1
|
||||
modbus_id: virtual_modbus_server
|
||||
id: modbus_server_1
|
||||
server_registers:
|
||||
- address: 0x01
|
||||
value_type: U_WORD
|
||||
read_lambda: return id(stored_u_word);
|
||||
write_lambda: id(stored_u_word) = x; return true;
|
||||
- address: 0x03
|
||||
value_type: S_WORD
|
||||
read_lambda: return id(stored_s_word);
|
||||
write_lambda: id(stored_s_word) = x; return true;
|
||||
- address: 0x05
|
||||
value_type: U_DWORD
|
||||
read_lambda: return id(stored_u_dword);
|
||||
write_lambda: id(stored_u_dword) = x; return true;
|
||||
- address: 0x08
|
||||
value_type: S_DWORD
|
||||
read_lambda: return id(stored_s_dword);
|
||||
write_lambda: id(stored_s_dword) = x; return true;
|
||||
- address: 0x0B
|
||||
value_type: U_DWORD_R
|
||||
read_lambda: return id(stored_u_dword_r);
|
||||
write_lambda: id(stored_u_dword_r) = x; return true;
|
||||
- address: 0x0E
|
||||
value_type: S_DWORD_R
|
||||
read_lambda: return id(stored_s_dword_r);
|
||||
write_lambda: id(stored_s_dword_r) = x; return true;
|
||||
- address: 0x11
|
||||
value_type: U_QWORD
|
||||
read_lambda: return id(stored_u_qword);
|
||||
write_lambda: id(stored_u_qword) = x; return true;
|
||||
- address: 0x16
|
||||
value_type: S_QWORD
|
||||
read_lambda: return id(stored_s_qword);
|
||||
write_lambda: id(stored_s_qword) = x; return true;
|
||||
- address: 0x1B
|
||||
value_type: U_QWORD_R
|
||||
read_lambda: return id(stored_u_qword_r);
|
||||
write_lambda: id(stored_u_qword_r) = x; return true;
|
||||
- address: 0x20
|
||||
value_type: S_QWORD_R
|
||||
read_lambda: return id(stored_s_qword_r);
|
||||
write_lambda: id(stored_s_qword_r) = x; return true;
|
||||
- address: 0x25
|
||||
value_type: FP32
|
||||
read_lambda: return id(stored_fp32);
|
||||
write_lambda: id(stored_fp32) = x; return true;
|
||||
- address: 0x28
|
||||
value_type: FP32_R
|
||||
read_lambda: return id(stored_fp32_r);
|
||||
write_lambda: id(stored_fp32_r) = x; return true;
|
||||
|
||||
sensor:
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_word"
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_word"
|
||||
address: 0x03
|
||||
register_type: holding
|
||||
value_type: S_WORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_dword"
|
||||
address: 0x05
|
||||
register_type: holding
|
||||
value_type: U_DWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_dword"
|
||||
address: 0x08
|
||||
register_type: holding
|
||||
value_type: S_DWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_dword_r"
|
||||
address: 0x0B
|
||||
register_type: holding
|
||||
value_type: U_DWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_dword_r"
|
||||
address: 0x0E
|
||||
register_type: holding
|
||||
value_type: S_DWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_qword"
|
||||
address: 0x11
|
||||
register_type: holding
|
||||
value_type: U_QWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_qword"
|
||||
address: 0x16
|
||||
register_type: holding
|
||||
value_type: S_QWORD
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_u_qword_r"
|
||||
address: 0x1B
|
||||
register_type: holding
|
||||
value_type: U_QWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_s_qword_r"
|
||||
address: 0x20
|
||||
register_type: holding
|
||||
value_type: S_QWORD_R
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_fp32"
|
||||
address: 0x25
|
||||
register_type: holding
|
||||
value_type: FP32
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "reg_fp32_r"
|
||||
address: 0x28
|
||||
register_type: holding
|
||||
value_type: FP32_R
|
||||
|
||||
number:
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_u_word"
|
||||
address: 0x01
|
||||
register_type: holding
|
||||
value_type: U_WORD
|
||||
min_value: 0
|
||||
max_value: 65535
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_s_word"
|
||||
address: 0x03
|
||||
register_type: holding
|
||||
value_type: S_WORD
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_u_dword"
|
||||
address: 0x05
|
||||
register_type: holding
|
||||
value_type: U_DWORD
|
||||
min_value: 0
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_s_dword"
|
||||
address: 0x08
|
||||
register_type: holding
|
||||
value_type: S_DWORD
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_u_dword_r"
|
||||
address: 0x0B
|
||||
register_type: holding
|
||||
value_type: U_DWORD_R
|
||||
min_value: 0
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_s_dword_r"
|
||||
address: 0x0E
|
||||
register_type: holding
|
||||
value_type: S_DWORD_R
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_u_qword"
|
||||
address: 0x11
|
||||
register_type: holding
|
||||
value_type: U_QWORD
|
||||
min_value: 0
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_s_qword"
|
||||
address: 0x16
|
||||
register_type: holding
|
||||
value_type: S_QWORD
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_u_qword_r"
|
||||
address: 0x1B
|
||||
register_type: holding
|
||||
value_type: U_QWORD_R
|
||||
min_value: 0
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_s_qword_r"
|
||||
address: 0x20
|
||||
register_type: holding
|
||||
value_type: S_QWORD_R
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_fp32"
|
||||
address: 0x25
|
||||
register_type: holding
|
||||
value_type: FP32
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
step: 0.01
|
||||
- platform: modbus_controller
|
||||
modbus_controller_id: modbus_controller_1
|
||||
name: "write_fp32_r"
|
||||
address: 0x28
|
||||
register_type: holding
|
||||
value_type: FP32_R
|
||||
min_value: -16777215
|
||||
max_value: 16777215
|
||||
step: 0.01
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: "id(virtual_uart_server).start_scenario();"
|
||||
- lambda: "id(virtual_uart_controller).start_scenario();"
|
||||
@@ -22,6 +22,8 @@ uart_mock:
|
||||
baud_rate: 9600
|
||||
rx_full_threshold: 120
|
||||
rx_timeout: 2
|
||||
# auto_start must be false to avoid races: the test presses the
|
||||
# "Start Scenario" button only after subscribing to states.
|
||||
auto_start: false
|
||||
debug:
|
||||
on_tx:
|
||||
@@ -61,4 +63,4 @@ button:
|
||||
name: "Start Scenario"
|
||||
id: start_scenario_btn
|
||||
on_press:
|
||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
||||
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||
|
||||
@@ -346,3 +346,109 @@ class SensorStateCollector:
|
||||
else:
|
||||
self._waiters.append((condition, future))
|
||||
return future
|
||||
|
||||
|
||||
class SensorTracker:
|
||||
"""Data-driven sensor state tracker with expected-value futures.
|
||||
|
||||
Tracks sensor state updates and resolves futures when sensors report
|
||||
specific expected values. Eliminates per-sensor future boilerplate.
|
||||
|
||||
Usage::
|
||||
|
||||
tracker = SensorTracker(["reg_u_word", "reg_s_word"])
|
||||
futures = tracker.expect_all({"reg_u_word": 99, "reg_s_word": -99})
|
||||
# ... subscribe_states with tracker.on_state, start scenario ...
|
||||
await tracker.await_all(futures)
|
||||
"""
|
||||
|
||||
def __init__(self, sensor_names: list[str]) -> None:
|
||||
self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names}
|
||||
self.key_to_sensor: dict[int, str] = {}
|
||||
self._expectations: dict[str, list[tuple[object, asyncio.Future]]] = {}
|
||||
|
||||
_ANY = object() # Sentinel: match any value
|
||||
|
||||
def expect(self, name: str, value: object) -> asyncio.Future:
|
||||
"""Register an expected value for *name* and return a future for it."""
|
||||
future: asyncio.Future = asyncio.get_running_loop().create_future()
|
||||
self._expectations.setdefault(name, []).append((value, future))
|
||||
return future
|
||||
|
||||
def expect_any(self, name: str) -> asyncio.Future:
|
||||
"""Register a future that resolves on *any* state update for *name*."""
|
||||
return self.expect(name, self._ANY)
|
||||
|
||||
def expect_all(self, expected: dict[str, object]) -> dict[str, asyncio.Future]:
|
||||
"""Call ``expect`` for every entry and return a dict of futures."""
|
||||
return {name: self.expect(name, value) for name, value in expected.items()}
|
||||
|
||||
def on_state(self, state: EntityState) -> None:
|
||||
"""State callback suitable for ``subscribe_states``."""
|
||||
if not isinstance(state, SensorState) or state.missing_state:
|
||||
return
|
||||
sensor_name = self.key_to_sensor.get(state.key)
|
||||
if not sensor_name or sensor_name not in self.sensor_states:
|
||||
return
|
||||
self.sensor_states[sensor_name].append(state.state)
|
||||
for expected_value, future in self._expectations.get(sensor_name, []):
|
||||
if not future.done() and (
|
||||
expected_value is self._ANY or state.state == expected_value
|
||||
):
|
||||
future.set_result(True)
|
||||
break
|
||||
|
||||
async def await_change(
|
||||
self, future: asyncio.Future, name: str, timeout: float = 2.0
|
||||
) -> None:
|
||||
"""Wait for a sensor future to resolve; fail the test on timeout."""
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout=timeout)
|
||||
except TimeoutError:
|
||||
import pytest
|
||||
|
||||
pytest.fail(
|
||||
f"Timeout waiting for {name} change. Received sensor states:\n"
|
||||
f" {name}: {self.sensor_states[name]}\n"
|
||||
)
|
||||
|
||||
async def await_must_not_change(
|
||||
self, future: asyncio.Future, name: str, timeout: float = 2.0
|
||||
) -> None:
|
||||
"""Assert a sensor future does NOT resolve within the timeout."""
|
||||
try:
|
||||
await asyncio.wait_for(future, timeout=timeout)
|
||||
except TimeoutError:
|
||||
return # Expected
|
||||
import pytest
|
||||
|
||||
pytest.fail(
|
||||
f"{name} change should not have been triggered, but was. "
|
||||
f"Received sensor states:\n {name}: {self.sensor_states[name]}\n"
|
||||
)
|
||||
|
||||
async def await_all(
|
||||
self, futures: dict[str, asyncio.Future], timeout: float = 2.0
|
||||
) -> None:
|
||||
"""Await every future in *futures*, failing with per-sensor diagnostics."""
|
||||
for name, future in futures.items():
|
||||
await self.await_change(future, name, timeout=timeout)
|
||||
|
||||
async def setup_and_start_scenario(self, client) -> list:
|
||||
"""Wire up subscriptions, wait for initial states, press Start Scenario."""
|
||||
entities, _ = await client.list_entities_services()
|
||||
self.key_to_sensor.update(
|
||||
build_key_to_entity_mapping(entities, list(self.sensor_states.keys()))
|
||||
)
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(self.on_state))
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
import pytest
|
||||
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
return entities
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Integration test for on_multi_click binary sensor automation.
|
||||
|
||||
Tests that on_multi_click correctly triggers for single click, double click,
|
||||
and long press patterns using a template binary sensor with timing
|
||||
orchestrated entirely in YAML.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_click_trigger(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that on_multi_click triggers for single, double, and long press patterns."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
single_click_pattern = re.compile(r"SINGLE_CLICK count=(\d+)")
|
||||
double_click_pattern = re.compile(r"DOUBLE_CLICK count=(\d+)")
|
||||
long_press_pattern = re.compile(r"LONG_PRESS count=(\d+)")
|
||||
|
||||
single_click_future: asyncio.Future[int] = loop.create_future()
|
||||
double_click_future: asyncio.Future[int] = loop.create_future()
|
||||
long_press_future: asyncio.Future[int] = loop.create_future()
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for multi-click trigger messages."""
|
||||
if m := single_click_pattern.search(line):
|
||||
if not single_click_future.done():
|
||||
single_click_future.set_result(int(m.group(1)))
|
||||
elif m := double_click_pattern.search(line):
|
||||
if not double_click_future.done():
|
||||
double_click_future.set_result(int(m.group(1)))
|
||||
elif (m := long_press_pattern.search(line)) and not long_press_future.done():
|
||||
long_press_future.set_result(int(m.group(1)))
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=check_output),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
_entities, services = await client.list_entities_services()
|
||||
|
||||
test_service = next((s for s in services if s.name == "run_all_tests"), None)
|
||||
assert test_service is not None, "run_all_tests service not found"
|
||||
|
||||
# Kick off the entire test sequence (runs in YAML with delays)
|
||||
await client.execute_service(test_service, {})
|
||||
|
||||
# Wait for all three triggers
|
||||
try:
|
||||
count = await asyncio.wait_for(single_click_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Timeout waiting for SINGLE_CLICK - on_multi_click did not trigger."
|
||||
)
|
||||
assert count == 1, f"Expected single click count=1, got {count}"
|
||||
|
||||
try:
|
||||
count = await asyncio.wait_for(double_click_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Timeout waiting for DOUBLE_CLICK - on_multi_click did not trigger."
|
||||
)
|
||||
assert count == 1, f"Expected double click count=1, got {count}"
|
||||
|
||||
try:
|
||||
count = await asyncio.wait_for(long_press_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
"Timeout waiting for LONG_PRESS - on_multi_click did not trigger."
|
||||
)
|
||||
assert count == 1, f"Expected long press count=1, got {count}"
|
||||
@@ -73,9 +73,16 @@ async def test_uart_mock_ld2410(
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see recovery frame values
|
||||
# Signal when we see ALL recovery frame values to avoid race where some
|
||||
# arrive after the waiter fires but before we index into the lists
|
||||
recovery_received = collector.add_waiter(
|
||||
lambda: pytest.approx(50.0) in collector.sensor_states["moving_distance"]
|
||||
lambda: (
|
||||
pytest.approx(50.0) in collector.sensor_states["moving_distance"]
|
||||
and pytest.approx(75.0) in collector.sensor_states["still_distance"]
|
||||
and pytest.approx(100.0) in collector.sensor_states["moving_energy"]
|
||||
and pytest.approx(80.0) in collector.sensor_states["still_energy"]
|
||||
and pytest.approx(127.0) in collector.sensor_states["detection_distance"]
|
||||
)
|
||||
)
|
||||
|
||||
async with (
|
||||
|
||||
@@ -83,11 +83,18 @@ async def test_uart_mock_ld2450(
|
||||
],
|
||||
)
|
||||
|
||||
# Signal when we see recovery frame values (target 1 distance ≈ 500mm)
|
||||
# Signal when we see all recovery frame values
|
||||
# Must wait for ALL values to avoid race where some arrive after the waiter fires
|
||||
recovery_received = collector.add_waiter(
|
||||
lambda: (
|
||||
pytest.approx(500.0, abs=1.0)
|
||||
in collector.sensor_states["target_1_distance"]
|
||||
and pytest.approx(300.0) in collector.sensor_states["target_1_x"]
|
||||
and pytest.approx(400.0) in collector.sensor_states["target_1_y"]
|
||||
and pytest.approx(30.0) in collector.sensor_states["target_1_speed"]
|
||||
and pytest.approx(1.0) in collector.sensor_states["target_count"]
|
||||
and pytest.approx(1.0) in collector.sensor_states["moving_target_count"]
|
||||
and pytest.approx(0.0) in collector.sensor_states["still_target_count"]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -14,15 +14,67 @@ test_uart_mock_modbus_no_threshold :
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, SensorState
|
||||
from aioesphomeapi import NumberInfo
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping, find_entity
|
||||
from .state_utils import SensorTracker, find_entity
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegisterTestCase:
|
||||
"""Test parameters for a single modbus register write/read round-trip."""
|
||||
|
||||
initial_value: object
|
||||
write_number_name: str
|
||||
write_value: float
|
||||
post_write_value: object
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _make_modbus_line_callback() -> tuple[Callable[[str], None], list[str], list[str]]:
|
||||
"""Return a (callback, error_lines, warning_lines) tuple for tracking modbus log output.
|
||||
|
||||
Only captures bus-level modbus messages ([modbus:]), not modbus_controller
|
||||
scheduling noise (e.g. "Duplicate modbus command found").
|
||||
"""
|
||||
error_log_lines: list[str] = []
|
||||
warning_log_lines: list[str] = []
|
||||
|
||||
def line_callback(line: str) -> None:
|
||||
if "[E][modbus:" in line:
|
||||
error_log_lines.append(line)
|
||||
if "[W][modbus:" in line:
|
||||
warning_log_lines.append(line)
|
||||
|
||||
return line_callback, error_log_lines, warning_log_lines
|
||||
|
||||
|
||||
def _assert_no_modbus_errors(
|
||||
error_log_lines: list[str], warning_log_lines: list[str]
|
||||
) -> None:
|
||||
assert len(error_log_lines) == 0, (
|
||||
"Expect no errors logged by the modbus mock, but got:\n"
|
||||
+ "\n".join(error_log_lines)
|
||||
)
|
||||
assert len(warning_log_lines) == 0, (
|
||||
"Expect no warnings logged by the modbus mock, but got:\n"
|
||||
+ "\n".join(warning_log_lines)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus(
|
||||
yaml_config: str,
|
||||
@@ -30,127 +82,41 @@ async def test_uart_mock_modbus(
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test basic modbus data parsing."""
|
||||
# Replace external component path placeholder
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
|
||||
tracker = SensorTracker(
|
||||
[
|
||||
"basic_register",
|
||||
"delayed_response",
|
||||
"late_response",
|
||||
"no_response",
|
||||
"exception_response",
|
||||
]
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track sensor state updates (after initial state is swallowed)
|
||||
sensor_states: dict[str, list[float]] = {
|
||||
"basic_register": [],
|
||||
"delayed_response": [],
|
||||
"late_response": [],
|
||||
"no_response": [],
|
||||
"exception_response": [],
|
||||
}
|
||||
|
||||
basic_register_changed = loop.create_future()
|
||||
delayed_response_changed = loop.create_future()
|
||||
late_response_changed = loop.create_future()
|
||||
no_response_changed = loop.create_future()
|
||||
exception_response_changed = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, SensorState) and not state.missing_state:
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
if sensor_name and sensor_name in sensor_states:
|
||||
sensor_states[sensor_name].append(state.state)
|
||||
if (
|
||||
sensor_name == "basic_register"
|
||||
and state.state == 259.0
|
||||
and not basic_register_changed.done()
|
||||
):
|
||||
basic_register_changed.set_result(True)
|
||||
elif (
|
||||
sensor_name == "delayed_response"
|
||||
and state.state == 255.0
|
||||
and not delayed_response_changed.done()
|
||||
):
|
||||
delayed_response_changed.set_result(True)
|
||||
elif (
|
||||
sensor_name == "late_response" and not late_response_changed.done()
|
||||
):
|
||||
late_response_changed.set_result(True)
|
||||
elif sensor_name == "no_response" and not no_response_changed.done():
|
||||
no_response_changed.set_result(True)
|
||||
elif (
|
||||
sensor_name == "exception_response"
|
||||
and not exception_response_changed.done()
|
||||
):
|
||||
exception_response_changed.set_result(True)
|
||||
basic_register_changed = tracker.expect("basic_register", 259.0)
|
||||
delayed_response_changed = tracker.expect("delayed_response", 255.0)
|
||||
# late_response / no_response / exception_response: expect *any* value
|
||||
# (these should never fire, so we use a permissive match via expect_any)
|
||||
late_response_changed = tracker.expect_any("late_response")
|
||||
no_response_changed = tracker.expect_any("no_response")
|
||||
exception_response_changed = tracker.expect_any("exception_response")
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
|
||||
# Build key mappings for all sensor types
|
||||
all_names = list(sensor_states.keys())
|
||||
key_to_sensor = build_key_to_entity_mapping(entities, all_names)
|
||||
|
||||
# Set up initial state helper
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Start the UART mock scenario now that we're subscribed
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(delayed_response_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for delayed_response change. Received sensor states:\n"
|
||||
f" delayed_response: {sensor_states['delayed_response']}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(late_response_changed, timeout=2.0)
|
||||
pytest.fail(
|
||||
f"late_response change should not have been triggered, but was. Received sensor states:\n"
|
||||
f" late_response: {sensor_states['late_response']}\n"
|
||||
)
|
||||
except TimeoutError:
|
||||
pass # Expected timeout since we never inject a response for late_response
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(no_response_changed, timeout=2.0)
|
||||
pytest.fail(
|
||||
f"no_response change should not have been triggered, but was. Received sensor states:\n"
|
||||
f" no_response: {sensor_states['no_response']}\n"
|
||||
)
|
||||
except TimeoutError:
|
||||
pass # Expected timeout since we never inject a response for no_response
|
||||
|
||||
# Wait for basic register to be updated with successful parse
|
||||
try:
|
||||
await asyncio.wait_for(basic_register_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for Basic Register change. Received sensor states:\n"
|
||||
f" basic_register: {sensor_states['basic_register']}\n"
|
||||
)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(exception_response_changed, timeout=2.0)
|
||||
pytest.fail(
|
||||
f"exception_response change should not have been triggered, but was. Received sensor states:\n"
|
||||
f" exception_response: {sensor_states['exception_response']}\n"
|
||||
)
|
||||
except TimeoutError:
|
||||
pass
|
||||
await tracker.await_change(delayed_response_changed, "delayed_response")
|
||||
await tracker.await_change(basic_register_changed, "basic_register")
|
||||
# Run all "must not change" checks concurrently — each waits the full
|
||||
# timeout, so sequential execution would multiply the wall time.
|
||||
await asyncio.gather(
|
||||
tracker.await_must_not_change(late_response_changed, "late_response"),
|
||||
tracker.await_must_not_change(no_response_changed, "no_response"),
|
||||
tracker.await_must_not_change(
|
||||
exception_response_changed, "exception_response"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -159,69 +125,17 @@ async def test_uart_mock_modbus_timing(
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test basic modbus data parsing."""
|
||||
# Replace external component path placeholder
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
"""Test modbus timing with multi-register SDM meter response."""
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track sensor state updates (after initial state is swallowed)
|
||||
sensor_states: dict[str, list[float]] = {
|
||||
"sdm_voltage": [],
|
||||
}
|
||||
|
||||
voltage_changed = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, SensorState) and not state.missing_state:
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
if sensor_name and sensor_name in sensor_states:
|
||||
sensor_states[sensor_name].append(state.state)
|
||||
# Check if this is a good voltage reading (243V)
|
||||
if (
|
||||
sensor_name == "sdm_voltage"
|
||||
and state.state > 200.0
|
||||
and not voltage_changed.done()
|
||||
):
|
||||
voltage_changed.set_result(True)
|
||||
tracker = SensorTracker(["sdm_voltage"])
|
||||
voltage_changed = tracker.expect_any("sdm_voltage")
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
|
||||
# Build key mappings for all sensor types
|
||||
all_names = list(sensor_states.keys())
|
||||
key_to_sensor = build_key_to_entity_mapping(entities, all_names)
|
||||
|
||||
# Set up initial state helper
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
# Start the UART mock scenario now that we're subscribed
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
|
||||
# Wait for voltage to be updated with successful parse
|
||||
try:
|
||||
await asyncio.wait_for(voltage_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for SDM voltage change. Received sensor states:\n"
|
||||
f" sdm_voltage: {sensor_states['sdm_voltage']}\n"
|
||||
)
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
await tracker.await_change(voltage_changed, "sdm_voltage")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -234,66 +148,187 @@ async def test_uart_mock_modbus_no_threshold(
|
||||
|
||||
Without the 50ms fallback timeout, the chunked response with a 40ms gap
|
||||
between USB packets would cause a false timeout and CRC failure cascade.
|
||||
Bus-level warnings (CRC failures, buffer clears) are expected during
|
||||
chunked reassembly — the test only verifies the final value arrives.
|
||||
"""
|
||||
# Replace external component path placeholder
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track sensor state updates (after initial state is swallowed)
|
||||
sensor_states: dict[str, list[float]] = {
|
||||
"sdm_voltage": [],
|
||||
}
|
||||
|
||||
voltage_changed = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if isinstance(state, SensorState) and not state.missing_state:
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
if sensor_name and sensor_name in sensor_states:
|
||||
sensor_states[sensor_name].append(state.state)
|
||||
# Check if this is a good voltage reading (243V)
|
||||
if (
|
||||
sensor_name == "sdm_voltage"
|
||||
and state.state > 200.0
|
||||
and not voltage_changed.done()
|
||||
):
|
||||
voltage_changed.set_result(True)
|
||||
tracker = SensorTracker(["sdm_voltage"])
|
||||
voltage_changed = tracker.expect_any("sdm_voltage")
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities, _ = await client.list_entities_services()
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
await tracker.await_change(voltage_changed, "sdm_voltage")
|
||||
|
||||
# Build key mappings for all sensor types
|
||||
all_names = list(sensor_states.keys())
|
||||
key_to_sensor = build_key_to_entity_mapping(entities, all_names)
|
||||
|
||||
# Set up initial state helper
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(
|
||||
reason="Modbus parser cannot handle server responses from other devices on the bus. Fix tracked in PR #11969.",
|
||||
strict=True,
|
||||
)
|
||||
async def test_uart_mock_modbus_server(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test modbus server parsing with peer traffic on a shared bus."""
|
||||
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
# Start the UART mock scenario now that we're subscribed
|
||||
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||
assert start_btn is not None, "Start Scenario button not found"
|
||||
client.button_command(start_btn.key)
|
||||
tracker = SensorTracker(
|
||||
["basic_read", "read_after_peer_response", "read_after_peer_timeout"]
|
||||
)
|
||||
futures = tracker.expect_all(
|
||||
{
|
||||
"basic_read": 1,
|
||||
"read_after_peer_response": 1,
|
||||
"read_after_peer_timeout": 1,
|
||||
}
|
||||
)
|
||||
|
||||
# Wait for voltage to be updated with successful parse
|
||||
try:
|
||||
await asyncio.wait_for(voltage_changed, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Timeout waiting for SDM voltage change. Received sensor states:\n"
|
||||
f" sdm_voltage: {sensor_states['sdm_voltage']}\n"
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
await tracker.await_all(futures)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus_server_controller(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test server/controller functionality for all read register types."""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
expected_values = {
|
||||
"reg_u_word": 99,
|
||||
"reg_s_word": -99,
|
||||
"reg_u_dword": 16909060,
|
||||
"reg_s_dword": -16909060,
|
||||
"reg_u_dword_r": pytest.approx(67305985),
|
||||
"reg_s_dword_r": pytest.approx(-67305985),
|
||||
"reg_u_qword": pytest.approx(72623859790382856),
|
||||
"reg_s_qword": pytest.approx(-72623859790382856),
|
||||
"reg_u_qword_r": pytest.approx(578437695752307201),
|
||||
"reg_s_qword_r": pytest.approx(-578437695752307201),
|
||||
"reg_fp32": pytest.approx(3.14),
|
||||
"reg_fp32_r": pytest.approx(3.14),
|
||||
}
|
||||
tracker = SensorTracker(list(expected_values.keys()))
|
||||
futures = tracker.expect_all(expected_values)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
await tracker.await_all(futures)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_uart_mock_modbus_server_controller_write(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test server/controller write functionality for all register value types.
|
||||
|
||||
Verifies that writing to modbus server registers via the controller updates
|
||||
the server's stored values, which are then read back correctly on the next poll.
|
||||
All 12 value types are tested: U/S_WORD, U/S_DWORD(_R), U/S_QWORD(_R), FP32(_R).
|
||||
"""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
register_test_cases: dict[str, RegisterTestCase] = {
|
||||
"reg_u_word": RegisterTestCase(11, "write_u_word", 42, 42),
|
||||
"reg_s_word": RegisterTestCase(-11, "write_s_word", -42, -42),
|
||||
"reg_u_dword": RegisterTestCase(1001, "write_u_dword", 2002, 2002),
|
||||
"reg_s_dword": RegisterTestCase(-1001, "write_s_dword", -2002, -2002),
|
||||
"reg_u_dword_r": RegisterTestCase(3003, "write_u_dword_r", 4004, 4004),
|
||||
"reg_s_dword_r": RegisterTestCase(-3003, "write_s_dword_r", -4004, -4004),
|
||||
"reg_u_qword": RegisterTestCase(5005, "write_u_qword", 6006, 6006),
|
||||
"reg_s_qword": RegisterTestCase(-5005, "write_s_qword", -6006, -6006),
|
||||
"reg_u_qword_r": RegisterTestCase(7007, "write_u_qword_r", 8008, 8008),
|
||||
"reg_s_qword_r": RegisterTestCase(-7007, "write_s_qword_r", -8008, -8008),
|
||||
"reg_fp32": RegisterTestCase(
|
||||
pytest.approx(1.5, abs=0.01),
|
||||
"write_fp32",
|
||||
3.14,
|
||||
pytest.approx(3.14, abs=0.01),
|
||||
),
|
||||
"reg_fp32_r": RegisterTestCase(
|
||||
pytest.approx(2.5, abs=0.01),
|
||||
"write_fp32_r",
|
||||
6.28,
|
||||
pytest.approx(6.28, abs=0.01),
|
||||
),
|
||||
}
|
||||
|
||||
tracker = SensorTracker(list(register_test_cases.keys()))
|
||||
|
||||
# Phase 1: expect initial baseline values
|
||||
initial_futures = tracker.expect_all(
|
||||
{name: case.initial_value for name, case in register_test_cases.items()}
|
||||
)
|
||||
# Phase 2: expect post-write values (registered now so on_state can match them)
|
||||
written_futures = tracker.expect_all(
|
||||
{name: case.post_write_value for name, case in register_test_cases.items()}
|
||||
)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
entities = await tracker.setup_and_start_scenario(client)
|
||||
|
||||
# Wait for initial baseline values to confirm the controller <-> server
|
||||
# connection is working before issuing writes
|
||||
await tracker.await_all(initial_futures, timeout=4.0)
|
||||
|
||||
# Issue write commands for all register types
|
||||
for case in register_test_cases.values():
|
||||
entity = find_entity(entities, case.write_number_name, NumberInfo)
|
||||
assert entity is not None, (
|
||||
f"{case.write_number_name} number entity not found"
|
||||
)
|
||||
client.number_command(entity.key, case.write_value)
|
||||
|
||||
# Wait for sensors to reflect the written values (round-trip write+read)
|
||||
await tracker.await_all(written_futures, timeout=4.0)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(
|
||||
reason="Modbus parser cannot handle server responses from other devices on the bus. Fix tracked in PR #11969.",
|
||||
strict=True,
|
||||
)
|
||||
async def test_uart_mock_modbus_server_controller_multiple(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test server/controller functionality with multiple servers."""
|
||||
|
||||
line_callback, error_log_lines, warning_log_lines = _make_modbus_line_callback()
|
||||
|
||||
expected_values = {"reg_u_word": 919, "reg_u_word_2": 929}
|
||||
tracker = SensorTracker(list(expected_values.keys()))
|
||||
futures = tracker.expect_all(expected_values)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=line_callback),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
await tracker.setup_and_start_scenario(client)
|
||||
await tracker.await_all(futures)
|
||||
_assert_no_modbus_errors(error_log_lines, warning_log_lines)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user