Merge remote-tracking branch 'origin/dev' into neutral-ble-client

# Conflicts:
#	esphome/components/ble_client/automation.h
This commit is contained in:
J. Nick Koston
2026-09-14 10:58:23 -05:00
552 changed files with 16456 additions and 3705 deletions
+1
View File
@@ -4,3 +4,4 @@ output:
gate_pin: ${gate_pin}
zero_cross_pin: ${zero_cross_pin}
zero_cross_interrupt_type: ANY
min_power: 0%
+17
View File
@@ -0,0 +1,17 @@
import esphome.codegen as cg
from esphome.core import CORE
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# USE_API compiles every api source, so emit what they need. No socket
# override: an __init__.py there makes pytest import its conftest as socket.conftest.
async def to_code_testing(config):
cg.add_define("USE_API")
cg.add_define("USE_API_PLAINTEXT")
cg.add_define("API_MAX_SEND_QUEUE", 8)
cg.add_define("MAX_API_CONNECTIONS", 1)
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
CORE.register_controller() # api_server registers with the controller registry
manifest.to_code = to_code_testing
+65
View File
@@ -0,0 +1,65 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstring>
#include "esphome/components/api/api_buffer.h"
namespace esphome::api::testing {
// Pointer plus two 16 bit sizes
static_assert(sizeof(APIBuffer) <= 2 * sizeof(void *));
TEST(APIBuffer, RefusesSizesAbove16Bits) {
APIBuffer buf;
ASSERT_TRUE(buf.resize(16));
EXPECT_FALSE(buf.reserve(UINT16_MAX + 1));
EXPECT_EQ(buf.size(), 16u);
EXPECT_EQ(buf.capacity(), 16u);
EXPECT_TRUE(buf.reserve(UINT16_MAX));
EXPECT_EQ(buf.capacity(), UINT16_MAX);
}
static const uint8_t BYTES[] = {1, 2, 3, 4, 5, 6};
TEST(APIBuffer, AppendReturnsTheNewBytes) {
APIBuffer buf;
ASSERT_TRUE(buf.reserve(8));
uint8_t *first = buf.append(3);
ASSERT_NE(first, nullptr);
std::memcpy(first, BYTES, 3);
EXPECT_EQ(buf.size(), 3u);
EXPECT_EQ(buf.capacity(), 8u);
// Grows through realloc and keeps what was there
uint8_t *second = buf.append(6);
ASSERT_EQ(second, buf.data() + 3);
std::memcpy(second, BYTES + 3, 3);
EXPECT_EQ(buf.size(), 9u);
EXPECT_EQ(buf.capacity(), 9u);
EXPECT_EQ(std::memcmp(buf.data(), BYTES, 6), 0);
}
TEST(APIBuffer, DropFrontSlidesTheRestDown) {
APIBuffer buf;
uint8_t *bytes = buf.append(6);
ASSERT_NE(bytes, nullptr);
std::memcpy(bytes, BYTES, 6);
buf.drop_front(2);
EXPECT_EQ(buf.size(), 4u);
EXPECT_EQ(buf.capacity(), 6u);
EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0);
// Growing afterwards keeps the slid bytes
ASSERT_TRUE(buf.reserve(64));
EXPECT_EQ(buf.size(), 4u);
EXPECT_EQ(std::memcmp(buf.data(), BYTES + 2, 4), 0);
// Dropping everything leaves an empty buffer with its capacity
buf.drop_front(4);
EXPECT_EQ(buf.size(), 0u);
EXPECT_EQ(buf.capacity(), 64u);
}
} // namespace esphome::api::testing
@@ -0,0 +1,510 @@
#include <gtest/gtest.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <cstdint>
#include <cstring>
#include <initializer_list>
#include <memory>
#include <vector>
#include "esphome/components/api/api_overflow_buffer.h"
#ifdef USE_HOST
namespace esphome::api::testing {
// Idle cost is the buffer plus one word of bookkeeping
static_assert(sizeof(APIOverflowBuffer) <= sizeof(APIBuffer) + sizeof(void *));
// Exposes storage so tests can check it is reused, not reallocated
class TestOverflowBuffer : public APIOverflowBuffer {
public:
using APIOverflowBuffer::LEN_PREFIX;
using APIOverflowBuffer::MAX_BYTES;
using APIOverflowBuffer::MAX_LONE_BYTES;
struct Storage {
size_t capacity;
const uint8_t *data;
bool operator==(const Storage &) const = default;
};
size_t capacity() const { return this->buf_.capacity(); }
Storage storage() const { return {this->buf_.capacity(), this->buf_.data()}; }
uint8_t count() const { return this->count_; }
size_t live() const { return this->buf_.size() - this->head_; }
/// Simulates a socket write inside try_drain() re-entering the send path
void set_draining(bool draining) { this->draining_ = draining; }
};
static std::vector<uint8_t> make_message(size_t len, uint8_t seed) {
std::vector<uint8_t> msg(len);
for (size_t i = 0; i < len; i++)
msg[i] = static_cast<uint8_t>(seed + i);
return msg;
}
static bool enqueue(TestOverflowBuffer &buf, const std::vector<uint8_t> &msg, uint16_t skip = 0) {
struct iovec iov = {const_cast<uint8_t *>(msg.data()), msg.size()};
return buf.enqueue_iov(&iov, 1, static_cast<uint16_t>(msg.size()), skip);
}
static void append(std::vector<uint8_t> &dst, const std::vector<uint8_t> &src, size_t skip = 0) {
dst.insert(dst.end(), src.begin() + skip, src.end());
}
static std::vector<uint8_t> concat(std::initializer_list<std::vector<uint8_t>> parts) {
std::vector<uint8_t> out;
for (const auto &part : parts)
append(out, part);
return out;
}
/// The pipe delivers the filler first, then the drained messages.
static void expect_after_filler(const std::vector<uint8_t> &received, size_t filler,
const std::vector<uint8_t> &expected) {
ASSERT_EQ(received.size(), filler + expected.size());
EXPECT_TRUE(std::equal(expected.begin(), expected.end(), received.begin() + filler));
}
// Non-blocking socket pair with small buffers, so the writer fills like a stalled TCP connection
class OverflowBufferTest : public ::testing::Test {
protected:
void SetUp() override {
int fds[2];
ASSERT_EQ(::socketpair(AF_UNIX, SOCK_STREAM, 0, fds), 0);
int size = 4096;
ASSERT_EQ(::setsockopt(fds[0], SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)), 0);
ASSERT_EQ(::setsockopt(fds[1], SOL_SOCKET, SO_RCVBUF, &size, sizeof(size)), 0);
ASSERT_EQ(::fcntl(fds[1], F_SETFL, O_NONBLOCK), 0);
this->reader_ = fds[1];
this->sock_ = std::make_unique<socket::Socket>(fds[0]);
ASSERT_EQ(this->sock_->setblocking(false), 0);
}
void TearDown() override { ::close(this->reader_); }
/// Write filler until the socket refuses; returns the bytes accepted
size_t fill_pipe_() {
uint8_t junk[512];
std::memset(junk, 0xEE, sizeof(junk));
size_t total = 0;
for (;;) {
ssize_t written = this->sock_->write(junk, sizeof(junk));
if (written <= 0)
break;
total += static_cast<size_t>(written);
}
return total;
}
/// Append whatever the pipe currently holds.
void read_into_(std::vector<uint8_t> &out) {
uint8_t tmp[1024];
for (;;) {
ssize_t n = ::read(this->reader_, tmp, sizeof(tmp));
if (n <= 0)
break;
out.insert(out.end(), tmp, tmp + n);
}
}
/// Drain once; a refusal must be a would-block, never a hard error.
ssize_t drain_(TestOverflowBuffer &buf) {
ssize_t sent = buf.try_drain(this->sock_.get());
if (sent == -1) {
EXPECT_TRUE(errno == EWOULDBLOCK || errno == EAGAIN);
}
return sent;
}
/// Read and drain until the backlog is empty; returns all bytes received
std::vector<uint8_t> drain_all_(TestOverflowBuffer &buf) {
std::vector<uint8_t> received;
for (int i = 0; i < 10000 && !buf.empty(); i++) {
this->read_into_(received);
// A hard socket error would never clear the backlog; stop instead of spinning
if (this->drain_(buf) == -1 && errno != EWOULDBLOCK && errno != EAGAIN)
break;
}
EXPECT_TRUE(buf.empty());
this->read_into_(received);
return received;
}
struct Stall {
size_t filler;
std::vector<uint8_t> first, second, received;
TestOverflowBuffer::Storage before;
};
/// Park two messages, then drain the first fully and the second part way
void stall_mid_message_(TestOverflowBuffer &buf, Stall &s) {
s.filler = this->fill_pipe_();
s.first = make_message(1500, 20);
ASSERT_GT(s.filler, s.first.size()); // the first message must drain in one go
// Larger than the whole pipe, so a drain always stops inside it
s.second = make_message(std::max<size_t>(s.filler + 1, std::min<size_t>(s.filler * 3, 12000)), 60);
ASSERT_GT(s.second.size(), s.filler);
ASSERT_TRUE(enqueue(buf, s.first));
ASSERT_TRUE(enqueue(buf, s.second));
s.before = buf.storage();
this->read_into_(s.received);
ASSERT_GT(this->drain_(buf), 0);
ASSERT_EQ(buf.count(), 1);
}
int reader_{-1};
std::unique_ptr<socket::Socket> sock_;
};
TEST_F(OverflowBufferTest, IdleBufferOwnsNoStorage) {
TestOverflowBuffer buf;
EXPECT_TRUE(buf.empty());
EXPECT_EQ(buf.capacity(), 0u);
EXPECT_EQ(buf.storage().data, nullptr);
}
TEST_F(OverflowBufferTest, StorageIsReusedAcrossStalls) {
TestOverflowBuffer buf;
auto msg = make_message(1000, 1);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
const auto storage = buf.storage();
EXPECT_GE(storage.capacity, msg.size() + TestOverflowBuffer::LEN_PREFIX);
for (int stall = 0; stall < 5; stall++) {
expect_after_filler(this->drain_all_(buf), filler, msg);
EXPECT_TRUE(buf.empty());
// Same allocation every time: no free, no new allocation
EXPECT_EQ(buf.storage(), storage);
filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
EXPECT_EQ(buf.storage(), storage);
}
}
TEST_F(OverflowBufferTest, ReleaseWhileQueuedFreesOnceDrained) {
TestOverflowBuffer buf;
auto msg = make_message(1000, 7);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
const size_t capacity = buf.capacity();
// Requested while the backlog still holds data: storage must stay until sent
buf.release();
EXPECT_FALSE(buf.empty());
EXPECT_EQ(buf.capacity(), capacity);
expect_after_filler(this->drain_all_(buf), filler, msg);
EXPECT_TRUE(buf.empty());
EXPECT_EQ(buf.capacity(), 0u);
EXPECT_EQ(buf.storage().data, nullptr);
// A later stall allocates again and keeps it, since nobody asked for a release
filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
EXPECT_GT(buf.capacity(), 0u);
this->drain_all_(buf);
EXPECT_GT(buf.capacity(), 0u);
}
TEST_F(OverflowBufferTest, ReleaseWhenEmptyFreesImmediately) {
TestOverflowBuffer buf;
auto msg = make_message(100, 3);
this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
this->drain_all_(buf);
EXPECT_GT(buf.capacity(), 0u);
buf.release();
EXPECT_EQ(buf.capacity(), 0u);
EXPECT_EQ(buf.storage().data, nullptr);
}
TEST_F(OverflowBufferTest, PreservesOrderAndSkipsSentPrefix) {
TestOverflowBuffer buf;
auto first = make_message(700, 10);
auto second_a = make_message(300, 50);
auto second_b = make_message(400, 90);
auto third = make_message(200, 130);
size_t filler = this->fill_pipe_();
// 100 bytes of the first message were already accepted by the socket
ASSERT_TRUE(enqueue(buf, first, 100));
// Two iovecs with the skip covering all of the first one plus part of the second
struct iovec iov[2] = {{second_a.data(), second_a.size()}, {second_b.data(), second_b.size()}};
const uint16_t second_skip = static_cast<uint16_t>(second_a.size() + 5);
ASSERT_TRUE(buf.enqueue_iov(iov, 2, static_cast<uint16_t>(second_a.size() + second_b.size()), second_skip));
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.count(), 3);
// Nothing can go out while the pipe is full
EXPECT_EQ(this->drain_(buf), -1);
EXPECT_EQ(buf.count(), 3);
std::vector<uint8_t> expected;
append(expected, first, 100);
append(expected, second_b, 5);
append(expected, third);
expect_after_filler(this->drain_all_(buf), filler, expected);
}
TEST_F(OverflowBufferTest, RefusesWhenQueueIsFull) {
TestOverflowBuffer buf;
auto msg = make_message(16, 1);
size_t filler = this->fill_pipe_();
for (int i = 0; i < API_MAX_SEND_QUEUE; i++) {
ASSERT_TRUE(enqueue(buf, msg)) << "message " << i;
}
EXPECT_FALSE(enqueue(buf, msg));
EXPECT_EQ(buf.count(), API_MAX_SEND_QUEUE);
// Draining frees the slots again
std::vector<uint8_t> expected;
for (int i = 0; i < API_MAX_SEND_QUEUE; i++)
append(expected, msg);
expect_after_filler(this->drain_all_(buf), filler, expected);
this->fill_pipe_();
EXPECT_TRUE(enqueue(buf, msg));
EXPECT_EQ(buf.count(), 1);
}
TEST_F(OverflowBufferTest, SkipAtIovecBoundary) {
TestOverflowBuffer buf;
auto sent = make_message(300, 50);
auto unsent = make_message(400, 90);
size_t filler = this->fill_pipe_();
// The skip covers the first iovec exactly, so only the second is copied
struct iovec iov[2] = {{sent.data(), sent.size()}, {unsent.data(), unsent.size()}};
ASSERT_TRUE(
buf.enqueue_iov(iov, 2, static_cast<uint16_t>(sent.size() + unsent.size()), static_cast<uint16_t>(sent.size())));
EXPECT_EQ(buf.live(), unsent.size() + TestOverflowBuffer::LEN_PREFIX);
expect_after_filler(this->drain_all_(buf), filler, unsent);
}
TEST_F(OverflowBufferTest, AppendsBehindSentPrefixWhenItFits) {
TestOverflowBuffer buf;
size_t filler = this->fill_pipe_();
auto first = make_message(200, 20);
// Size the second message so the two land half way into a 256 byte step,
// leaving exactly 128 bytes of slack whatever the pipe accepted
const size_t base = std::max<size_t>(filler + 1, std::min<size_t>(filler * 3, 12000));
const size_t second_len = (base / 256 + 1) * 256 + 128 - first.size() - 2 * TestOverflowBuffer::LEN_PREFIX;
auto second = make_message(second_len, 60);
ASSERT_GT(second.size(), filler);
ASSERT_TRUE(enqueue(buf, first));
ASSERT_TRUE(enqueue(buf, second));
const auto storage = buf.storage();
const size_t slack = storage.capacity - first.size() - second.size() - 2 * TestOverflowBuffer::LEN_PREFIX;
ASSERT_EQ(slack, 128u);
auto third = make_message(slack - TestOverflowBuffer::LEN_PREFIX, 200);
std::vector<uint8_t> received;
this->read_into_(received);
ASSERT_GT(this->drain_(buf), 0);
ASSERT_EQ(buf.count(), 1);
const size_t live = buf.live();
// Fits in the tail, so the sent prefix is left alone
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.storage(), storage);
EXPECT_EQ(buf.live(), live + third.size() + TestOverflowBuffer::LEN_PREFIX);
append(received, this->drain_all_(buf));
expect_after_filler(received, filler, concat({first, second, third}));
}
TEST_F(OverflowBufferTest, ReleaseSurvivesFurtherEnqueues) {
TestOverflowBuffer buf;
auto first = make_message(300, 7);
auto second = make_message(300, 70);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, first));
buf.release();
ASSERT_TRUE(enqueue(buf, second));
EXPECT_GT(buf.capacity(), 0u);
expect_after_filler(this->drain_all_(buf), filler, concat({first, second}));
EXPECT_EQ(buf.capacity(), 0u);
}
TEST_F(OverflowBufferTest, RefusesWhenByteLimitIsExceeded) {
TestOverflowBuffer buf;
// Two of these fill the byte budget exactly, well before the slot count is reached
static_assert(API_MAX_SEND_QUEUE >= 3);
auto msg = make_message(TestOverflowBuffer::MAX_BYTES / 2 - TestOverflowBuffer::LEN_PREFIX, 1);
this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
ASSERT_TRUE(enqueue(buf, msg));
EXPECT_FALSE(enqueue(buf, msg));
EXPECT_EQ(buf.count(), 2);
}
TEST_F(OverflowBufferTest, LoneMessageMayExceedByteLimit) {
TestOverflowBuffer buf;
// The oversized message must still fit under the lone message ceiling
static_assert(TestOverflowBuffer::MAX_BYTES + 100 + TestOverflowBuffer::LEN_PREFIX <=
TestOverflowBuffer::MAX_LONE_BYTES);
auto big = make_message(TestOverflowBuffer::MAX_BYTES + 100, 5);
auto small = make_message(16, 9);
// Refusing the only message would drop the connection for nothing
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, big));
EXPECT_EQ(buf.count(), 1);
// With a backlog present the byte limit applies again
EXPECT_FALSE(enqueue(buf, small));
EXPECT_EQ(buf.count(), 1);
expect_after_filler(this->drain_all_(buf), filler, big);
}
TEST_F(OverflowBufferTest, LoneMessageAboveOffsetLimitIsRefused) {
TestOverflowBuffer buf;
// Payload plus prefix is past the lone message ceiling
auto msg = make_message(TestOverflowBuffer::MAX_LONE_BYTES, 3);
this->fill_pipe_();
EXPECT_FALSE(enqueue(buf, msg));
EXPECT_TRUE(buf.empty());
EXPECT_EQ(buf.capacity(), 0u);
}
TEST_F(OverflowBufferTest, HardSocketErrorLeavesBacklogIntact) {
TestOverflowBuffer buf;
auto msg = make_message(300, 40);
this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
// A closed socket fails every write outright, unlike a full one
ASSERT_EQ(this->sock_->close(), 0);
errno = 0;
EXPECT_EQ(buf.try_drain(this->sock_.get()), -1);
EXPECT_NE(errno, EWOULDBLOCK);
EXPECT_NE(errno, EAGAIN);
EXPECT_EQ(buf.count(), 1);
EXPECT_EQ(buf.live(), msg.size() + TestOverflowBuffer::LEN_PREFIX);
}
TEST_F(OverflowBufferTest, GrowsWhileReclaimingSentPrefix) {
TestOverflowBuffer buf;
Stall s;
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
// One byte too many to fit even after the sent prefix is reclaimed: grows in one copy
auto third = make_message(s.before.capacity - buf.live() + 1, 200);
ASSERT_TRUE(enqueue(buf, third));
EXPECT_GT(buf.capacity(), s.before.capacity);
EXPECT_EQ(buf.count(), 2);
append(s.received, this->drain_all_(buf));
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
}
TEST_F(OverflowBufferTest, NestedDrainMakesNoProgress) {
TestOverflowBuffer buf;
auto msg = make_message(300, 40);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, msg));
std::vector<uint8_t> received;
this->read_into_(received);
// Room is available, but a nested drain must leave the outer one's message alone
buf.set_draining(true);
EXPECT_EQ(this->drain_(buf), 0);
EXPECT_EQ(buf.count(), 1);
std::vector<uint8_t> nothing;
this->read_into_(nothing);
EXPECT_TRUE(nothing.empty());
buf.set_draining(false);
append(received, this->drain_all_(buf));
expect_after_filler(received, filler, msg);
}
TEST_F(OverflowBufferTest, NestedEnqueueAppendsWithinCapacity) {
TestOverflowBuffer buf;
auto first = make_message(500, 10);
auto second = make_message(4, 90);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, first));
const auto storage = buf.storage();
ASSERT_GE(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX);
buf.set_draining(true);
EXPECT_TRUE(enqueue(buf, second));
EXPECT_EQ(buf.count(), 2);
EXPECT_EQ(buf.storage(), storage);
buf.set_draining(false);
expect_after_filler(this->drain_all_(buf), filler, concat({first, second}));
}
TEST_F(OverflowBufferTest, NestedEnqueueRefusesToGrow) {
TestOverflowBuffer buf;
auto first = make_message(500, 10);
auto second = make_message(100, 90);
size_t filler = this->fill_pipe_();
ASSERT_TRUE(enqueue(buf, first));
const auto storage = buf.storage();
ASSERT_LT(storage.capacity, first.size() + second.size() + 2 * TestOverflowBuffer::LEN_PREFIX);
// Growing would free the bytes the outer write() is sending from
buf.set_draining(true);
EXPECT_FALSE(enqueue(buf, second));
EXPECT_EQ(buf.count(), 1);
EXPECT_EQ(buf.storage(), storage);
buf.set_draining(false);
expect_after_filler(this->drain_all_(buf), filler, first);
}
TEST_F(OverflowBufferTest, NestedEnqueueRefusesToCompact) {
TestOverflowBuffer buf;
Stall s;
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
auto third = make_message(1000, 200);
// Sliding the remainder down would move the bytes the outer write() points at
buf.set_draining(true);
EXPECT_FALSE(enqueue(buf, third));
EXPECT_EQ(buf.count(), 1);
EXPECT_EQ(buf.storage(), s.before);
buf.set_draining(false);
// Once the drain is over the same enqueue compacts and succeeds
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.storage(), s.before);
append(s.received, this->drain_all_(buf));
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
}
TEST_F(OverflowBufferTest, CompactsInsteadOfGrowingAfterPartialDrain) {
TestOverflowBuffer buf;
Stall s;
ASSERT_NO_FATAL_FAILURE(this->stall_mid_message_(buf, s));
auto third = make_message(1000, 200);
// The sent first message is reclaimed by sliding the remainder down, not by reallocating
ASSERT_TRUE(enqueue(buf, third));
EXPECT_EQ(buf.storage(), s.before);
append(s.received, this->drain_all_(buf));
expect_after_filler(s.received, s.filler, concat({s.first, s.second, third}));
}
} // namespace esphome::api::testing
#endif // USE_HOST
+5
View File
@@ -0,0 +1,5 @@
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
manifest.dependencies = manifest.dependencies + ["sensor", "spi"]
@@ -0,0 +1,62 @@
#include <gtest/gtest.h>
#include "esphome/components/atm90e32/atm90e32.h"
namespace esphome::atm90e32::testing {
TEST(ATM90E32OffsetRegisterVerification, AcceptsExactSignedReadback) {
EXPECT_TRUE(offset_register_value_matches(0x007B, 123));
EXPECT_TRUE(offset_register_value_matches(0xFF85, -123));
}
TEST(ATM90E32OffsetRegisterVerification, RejectsMismatchedReadback) {
EXPECT_FALSE(offset_register_value_matches(0x007C, 123));
EXPECT_FALSE(offset_register_value_matches(0xFF84, -123));
}
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedStoredValuesAsRestored) {
const auto state = resolve_offset_restore_state(true, true, false);
EXPECT_TRUE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsVerifiedConfigFallbackAsNotRestored) {
const auto state = resolve_offset_restore_state(true, false, true);
EXPECT_FALSE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsFailedConfigFallbackAsUnverified) {
const auto state = resolve_offset_restore_state(true, false, false);
EXPECT_FALSE(state.restored);
EXPECT_FALSE(state.values_verified);
}
TEST(ATM90E32OffsetRestoreState, ReportsConfigWithoutStoredValuesAsNotRestored) {
const auto state = resolve_offset_restore_state(false, true, false);
EXPECT_FALSE(state.restored);
EXPECT_TRUE(state.values_verified);
}
TEST(ATM90E32OffsetPersistence, RollsBackStoredValuesOrZeroSentinel) {
const OffsetCalibration previous[3]{{1, -1}, {2, -2}, {3, -3}};
OffsetCalibration rollback[3]{};
prepare_offset_rollback(previous, true, rollback);
for (uint8_t phase = 0; phase < 3; phase++) {
EXPECT_EQ(rollback[phase].first_offset, previous[phase].first_offset);
EXPECT_EQ(rollback[phase].second_offset, previous[phase].second_offset);
}
prepare_offset_rollback(previous, false, rollback);
for (const auto &phase : rollback) {
EXPECT_EQ(phase.first_offset, 0);
EXPECT_EQ(phase.second_offset, 0);
}
}
} // namespace esphome::atm90e32::testing
@@ -6,15 +6,14 @@ def override_manifest(manifest: ComponentManifestOverride) -> None:
# close_service_batch compiles only under USE_BLUETOOTH_PROXY_CONNECTIONS;
# emit the backend define so the host build exercises it.
async def to_code_testing(config):
# These defines are global to the merged host test binary; safe
# because no co-compiled test observes them.
# These defines are global to the merged host test binary. The api sources are
# compiled in it too (the api tests define USE_API), and USE_BLUETOOTH_PROXY would make
# them include and call bluetooth_proxy, which has no host build without a BLE hub.
cg.add_define("USE_BLE_GATT_CLIENT")
cg.add_define("USE_BLE_GATT_CLIENT_STUB_BACKEND")
cg.add_define("USE_BLUETOOTH_PROXY")
# Gates the connection half of the API surface, which is what
# close_service_batch and the GATT response types live behind.
cg.add_define("USE_BLUETOOTH_PROXY_CONNECTIONS")
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", 1)
manifest.to_code = to_code_testing
@@ -0,0 +1,12 @@
# Compile the gated filter path; no external component is in-tree to call
# enable_advertisement_filter(), so the define is forced here.
<<: !include common.yaml
esphome:
build_flags:
- "-DUSE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER"
esp32_ble_tracker:
bluetooth_proxy:
active: true
+18
View File
@@ -0,0 +1,18 @@
tinyusb:
id: tinyusb_test
usb_lang_id: 0x0123
usb_manufacturer_str: ESPHomeTestManufacturer
usb_product_id: 0x1234
usb_product_str: ESPHomeTestProduct
usb_serial_str: ESPHomeTestSerialNumber
usb_vendor_id: 0x2345
uart:
- id: uart_0
tx_pin: 14
rx_pin: 13
baud_rate: 115200
usb_cdc_acm:
interfaces:
- id: cdc_acm_1
@@ -0,0 +1,12 @@
# Second UART/CDC pair for a two-bridge setup. Kept out of common.yaml because the
# ESP32-S2 has only two UART controllers and the logger occupies one, so a second
# uart there would fail at runtime.
uart:
- id: uart_1
tx_pin: 15
rx_pin: 16
baud_rate: 115200
usb_cdc_acm:
interfaces:
- id: cdc_acm_2
@@ -0,0 +1,15 @@
packages:
cdc_acm_uart: !include common.yaml
cdc_acm_uart_dual: !include common_dual.yaml
bridge:
- platform: cdc_acm_uart
uart_id: uart_0
usb_cdc_acm_id: cdc_acm_1
dtr_pin: 40
rts_pin: 41
- platform: cdc_acm_uart
uart_id: uart_1
usb_cdc_acm_id: cdc_acm_2
dtr_pin: 20
rts_pin: 21
@@ -0,0 +1,14 @@
# ESP32-S2 has no USB_SERIAL_JTAG, so the logger defaults to USB_CDC, which shares
# the USB OTG peripheral with tinyusb. Use a hardware UART for logging instead.
logger:
hardware_uart: UART0
packages:
cdc_acm_uart: !include common.yaml
bridge:
- platform: cdc_acm_uart
uart_id: uart_0
usb_cdc_acm_id: cdc_acm_1
dtr_pin: 40
rts_pin: 41
@@ -0,0 +1,17 @@
packages:
cdc_acm_uart: !include common.yaml
cdc_acm_uart_dual: !include common_dual.yaml
bridge:
- platform: cdc_acm_uart
uart_id: uart_0
usb_cdc_acm_id: cdc_acm_1
dtr_pin: 40
rts_pin: 41
- platform: cdc_acm_uart
uart_id: uart_1
usb_cdc_acm_id: cdc_acm_2
# GPIO19/20 are USB D-/D+ on the S3 (which the CDC side itself uses); use
# unrelated free pins here.
dtr_pin: 17
rts_pin: 18
+1 -2
View File
@@ -30,8 +30,7 @@ climate:
- switch.turn_on: climate_heater_switch
- switch.turn_off: climate_cooler_switch
# Thermostat-based climate so climate.control: action variants get build
# coverage (bang_bang doesn't support fan modes, presets, etc.). Climate
# has no template platform, so thermostat is the right vehicle.
# coverage (bang_bang doesn't support fan modes, presets, etc.).
- platform: thermostat
id: climate_test_thermostat
name: Test Thermostat
+71
View File
@@ -348,4 +348,75 @@ TEST(StepToAccuracyDecimals, NonFiniteAndZero) {
EXPECT_EQ(step_to_accuracy_decimals(-INFINITY), 0);
}
// --- FixedVector::try_init() ---
// Keeps the block observable, else the compiler may drop the malloc and free pair and fold the check
static void escape(const void *p) { asm volatile("" : : "g"(p) : "memory"); }
TEST(FixedVectorTryInit, ReportsExhaustionAndStaysEmpty) {
FixedVector<uint32_t> v;
const bool ok = v.try_init(SIZE_MAX / sizeof(uint32_t));
escape(&v);
EXPECT_FALSE(ok);
EXPECT_EQ(v.capacity(), 0u);
EXPECT_FALSE(v.try_init(SIZE_MAX / sizeof(uint32_t) + 1)); // byte count would wrap
EXPECT_EQ(v.capacity(), 0u);
EXPECT_TRUE(v.try_init(0));
EXPECT_TRUE(v.try_init(4));
v.push_back(7);
EXPECT_EQ(v.size(), 1u);
}
// --- RAMAllocator::make_unique() ---
namespace {
struct Probe {
static inline int live = 0;
int a;
int b;
Probe(int a, int b) : a(a), b(b) { live++; }
~Probe() { live--; }
};
} // namespace
static_assert(sizeof(RAMUniquePtr<Probe>) == sizeof(Probe *), "the deleter must not add storage");
TEST(RAMAllocatorMakeUnique, ForwardsArgsAndDestroysOnce) {
auto p = RAMAllocator<Probe>().make_unique(3, 4);
ASSERT_NE(p, nullptr);
EXPECT_EQ(p->a, 3);
EXPECT_EQ(p->b, 4);
EXPECT_EQ(Probe::live, 1);
p.reset();
EXPECT_EQ(Probe::live, 0);
}
TEST(RAMAllocatorMakeUnique, ValueInitializesLikeMakeUnique) {
struct Plain {
uint32_t words[8];
};
// Dirty a block of the same size first so a recycled allocation is not zero by chance
auto dirty = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(sizeof(Plain));
std::memset(dirty.get(), 0xFF, sizeof(Plain));
dirty.reset();
auto p = RAMAllocator<Plain>().make_unique();
ASSERT_NE(p, nullptr);
// Under ASan fresh blocks are filled with 0xbe, so this holds even when the dirtied block is not reused
EXPECT_TRUE(std::all_of(std::begin(p->words), std::end(p->words), [](uint32_t w) { return w == 0; }));
}
TEST(RAMAllocatorMakeUnique, ArrayFormRejectsOverflowAndZero) {
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(SIZE_MAX / sizeof(uint32_t) + 1), nullptr);
EXPECT_EQ(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(0), nullptr);
EXPECT_NE(RAMAllocator<uint32_t>().make_unique_array_for_overwrite(1), nullptr);
}
TEST(RAMAllocatorMakeUnique, ArrayFormAllocatesElements) {
RAMUniquePtr<uint8_t[]> buf = RAMAllocator<uint8_t>().make_unique_array_for_overwrite(256);
ASSERT_NE(buf, nullptr);
std::memset(buf.get(), 0xA5, 256);
EXPECT_EQ(buf[0], 0xA5);
EXPECT_EQ(buf[255], 0xA5);
}
} // namespace esphome::core::testing
+1 -1
View File
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO14
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp32-idf.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO3
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/esp8266-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart_38400: !include ../../test_build_components/common/uart_38400/rp2040-ard.yaml
uart_38400_even: !include ../../test_build_components/common/uart_38400_even/rp2040-ard.yaml
<<: !include common.yaml
@@ -17,6 +17,8 @@ esp32:
disable_dev_null_vfs: true
disable_mbedtls_peer_cert: true
disable_mbedtls_pkcs7: true
disable_mbedtls_tls_server: true
disable_mbedtls_tls_extras: true
disable_regi2c_in_iram: true
disable_fatfs: true
sram1_as_iram: true
@@ -0,0 +1,5 @@
# Exercises the ESP-NOW-over-hosted shim: on the ESP32-P4 host, esp32_hosted
# supplies the esp_now_* symbols that the espnow component links against.
packages:
esp32_hosted: !include common.yaml
espnow: !include ../espnow/common.yaml
@@ -0,0 +1,4 @@
substitutions:
verify_ssl: "true"
<<: !include common.yaml
@@ -1,4 +1,4 @@
packages:
uart_1200: !include ../../test_build_components/common/uart_1200/esp32-idf.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
uart_rx_pin: GPIO3
packages:
uart_1200: !include ../../test_build_components/common/uart_1200/esp8266-ard.yaml
uart_1200_none_2stopbits: !include ../../test_build_components/common/uart_1200_none_2stopbits/esp8266-ard.yaml
<<: !include common.yaml
+15 -1
View File
@@ -30,6 +30,18 @@ binary_sensor:
widget: button_button
state: pressed
globals:
- id: counter
type: int
script:
- id: add_row
then:
- lvgl.list.add:
id: test_list_id
label:
text: row
lvgl:
id: lvgl_id
rotation: 90
@@ -1291,7 +1303,7 @@ lvgl:
then:
- logger.log:
format: "table selected row %u col %u"
args: [row, column]
args: [(unsigned)row, (unsigned)column]
on_click:
then:
- lvgl.table.cell.update:
@@ -1347,10 +1359,12 @@ lvgl:
- logger.log:
format: "list entry added at %d"
args: [list_index]
- lambda: "id(counter)++;"
on_remove:
- logger.log:
format: "list entry removed at %d"
args: [list_index]
- lambda: "id(counter)--;"
on_click:
- lvgl.list.add_text:
id: test_list_id
@@ -68,6 +68,14 @@ class Initiator {
static const uint8_t PROLOGUE[] = {'t', 'e', 's', 't', 'p', 'r', 'o', 'l', 'o', 'g', 'u', 'e'};
// The context only points at the key and init() copies it before returning,
// so a temporary context over a temporary key is safe within one call
static NoiseContext ctx_for(const psk_t &psk) {
NoiseContext ctx;
ctx.set_psk(psk.data());
return ctx;
}
static psk_t make_psk(uint8_t seed) {
psk_t psk;
for (size_t i = 0; i < psk.size(); i++) {
@@ -102,7 +110,7 @@ TEST(NoiseResponderHandshakeTest, MessageMethodsErrorBeforeInit) {
TEST(NoiseResponderHandshakeTest, FullHandshakeAndTransportRoundTrip) {
const psk_t psk = make_psk(7);
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0);
EXPECT_EQ(responder.action(), Action::ACTION_READ);
Initiator initiator(psk, PROLOGUE, sizeof(PROLOGUE));
@@ -155,8 +163,8 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {
// proves the restart took effect; the old state surviving would fail the
// MAC here.
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(make_psk(9), PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(ctx_for(make_psk(9)), PROLOGUE, sizeof(PROLOGUE)), 0);
EXPECT_EQ(responder.action(), Action::ACTION_READ);
Initiator initiator(make_psk(9), PROLOGUE, sizeof(PROLOGUE));
@@ -168,7 +176,7 @@ TEST(NoiseResponderHandshakeTest, ReInitRestartsHandshake) {
TEST(NoiseResponderHandshakeTest, WrongPskFailsWithMacFailure) {
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(make_psk(7), PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(ctx_for(make_psk(7)), PROLOGUE, sizeof(PROLOGUE)), 0);
Initiator initiator(make_psk(200), PROLOGUE, sizeof(PROLOGUE));
uint8_t msg[MAX_HANDSHAKE_SIZE];
@@ -185,7 +193,7 @@ TEST(NoiseResponderHandshakeTest, MismatchedPrologueFailsWithMacFailure) {
// tampered preamble must fail even with the right key.
const psk_t psk = make_psk(7);
NoiseResponderHandshake responder;
ASSERT_EQ(responder.init(psk, PROLOGUE, sizeof(PROLOGUE)), 0);
ASSERT_EQ(responder.init(ctx_for(psk), PROLOGUE, sizeof(PROLOGUE)), 0);
static const uint8_t TAMPERED[] = {'x'};
Initiator initiator(psk, TAMPERED, sizeof(TAMPERED));
@@ -17,12 +17,17 @@ TEST(NoiseContextTest, AllZerosPskIsReserved) {
EXPECT_FALSE(NoiseContext::is_all_zeros(psk));
NoiseContext ctx;
psk_t loaded;
EXPECT_FALSE(ctx.has_psk());
ctx.set_psk(zeros);
EXPECT_FALSE(ctx.has_psk());
ctx.set_psk(psk);
ctx.load_psk(loaded);
EXPECT_EQ(loaded, zeros);
ctx.set_psk(psk.data());
EXPECT_TRUE(ctx.has_psk());
EXPECT_EQ(ctx.get_psk(), psk);
ctx.load_psk(loaded);
EXPECT_EQ(loaded, psk);
// Callers map the reserved key to nullptr; the context just stores what it is given
ctx.set_psk(nullptr);
EXPECT_FALSE(ctx.has_psk());
}
TEST(WireFormatTest, FrameHeaderIsIndicatorPlusBigEndianLength) {
+12
View File
@@ -0,0 +1,12 @@
wifi:
ssid: MySSID
password: password1
api:
encryption:
key: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
ota:
- platform: esphome
port: 3290
password: "superlongpasswordthatnoonewillknow"
+10
View File
@@ -0,0 +1,10 @@
wifi:
ssid: MySSID
password: password1
api:
encryption:
ota:
- platform: esphome
port: 3291
@@ -0,0 +1,2 @@
packages:
ota: !include api_key_offer.yaml
@@ -0,0 +1,2 @@
packages:
ota: !include api_key_offer.yaml
@@ -0,0 +1,2 @@
packages:
ota: !include api_runtime_key.yaml
@@ -0,0 +1,2 @@
packages:
ota: !include api_runtime_key.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_115200: !include ../../test_build_components/common/uart_115200/rp2040-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,6 @@
# A receiver with no dumpers and no listeners compiles both lists out.
# Only built while remote_receiver is tested in isolation: the counts are global defines,
# so this variant cannot be merged with configs that register any.
remote_receiver:
- id: rcvr_bare
pin: ${pin}
@@ -0,0 +1,5 @@
substitutions:
pin: GPIO2
packages:
bare: !include bare-common.yaml
@@ -2,7 +2,7 @@ remote_transmitter:
id: xmitr
pin: GPIO26
carrier_duty_percent: 50%
# non_blocking is bk7231n/bk7238-only; the CI board is a BK7252
# non_blocking is bk7238-only; the CI board is a BK7252, so this builds the bit-bang path
packages:
buttons: !include common-buttons.yaml
+27
View File
@@ -14,6 +14,15 @@ display:
position:
x: 100
y: 100
snapshot_key: SDLK_F12
- platform: sdl
id: headless_display
headless: true
show_test_card: true
dimensions:
width: 320
height: 240
- platform: sdl
id: second_display
@@ -46,3 +55,21 @@ binary_sensor:
sdl_id: sdl_sdl_display
id: key_enter
key: SDLK_RETURN
esphome:
# A name of your own is only good for one snapshot - a second one under the same name fails
# rather than writing over the first - so these run once rather than on a repeating interval.
on_boot:
- delay: 2s
- snapshot.take:
id: headless_display
filename: test_card.bmp
- snapshot.take:
id: headless_display
filename: !lambda 'return "shot.bmp";'
interval:
# A generated name has the time in it, so this one can repeat.
- interval: 10s
then:
- snapshot.take: sdl_sdl_display
+29
View File
@@ -0,0 +1,29 @@
# Config-only test for the headless and screenshot options. The combinations that must be
# rejected are covered by tests/component_tests/sdl/test_sdl.py; this file checks that the
# accepted forms validate together.
host:
mac_address: "62:23:45:AF:B3:DD"
display:
- platform: sdl
id: headless_display
headless: true
dimensions: 320x240
- platform: sdl
id: windowed_display
dimensions: 320x240
snapshot_key: SDLK_F12
binary_sensor:
- platform: sdl
sdl_id: windowed_display
id: key_up
key: SDLK_UP
interval:
- interval: 10s
then:
- snapshot.take:
id: headless_display
filename: periodic.bmp
@@ -4,3 +4,6 @@ psram:
sendspin:
id: sendspin_hub_id
task_stack_in_psram: true
manufacturer: Test Manufacturer
model: Test Model
firmware_version: 1.2.3
@@ -0,0 +1,14 @@
substitutions:
tx_pin: GPIO4
rx_pin: GPIO5
# Compile the tap code paths; no tap is attached, so this exercises the
# null-tap branches that a normal build never defines.
esphome:
platformio_options:
build_flags:
- "-DUSE_SERIAL_PROXY_TAP"
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
serial_proxy: !include common.yaml
+34
View File
@@ -0,0 +1,34 @@
display:
- platform: snapshot
id: snapshot_display
update_interval: 1s
show_test_card: true
# An odd width exercises the row padding in the BMP writer
dimensions:
width: 101
height: 64
- platform: snapshot
id: snapshot_rotated
rotation: 90
dimensions: 320x240
lambda: |-
it.filled_rectangle(0, 0, 40, 20, Color(0xFF, 0x80, 0x00));
esphome:
# A name of your own is only good for one snapshot - a second one under the same name fails
# rather than writing over the first - so these run once rather than on a repeating interval.
on_boot:
- delay: 2s
- snapshot.take:
id: snapshot_display
filename: test_card.bmp
- snapshot.take:
id: snapshot_rotated
filename: !lambda 'return "rotated.bmp";'
interval:
# A generated name has the time in it, so this one can repeat.
- interval: 10s
then:
- snapshot.take: snapshot_display
+5
View File
@@ -0,0 +1,5 @@
host:
mac_address: "62:23:45:AF:B3:DE"
packages:
snapshot: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/esp32-idf.yaml
uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/esp32-idf.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO2
packages:
uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml
uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/esp8266-ard.yaml
<<: !include common.yaml
@@ -3,6 +3,6 @@ substitutions:
rx_pin: GPIO5
packages:
uart: !include ../../test_build_components/common/uart/rp2040-ard.yaml
uart_1200_even_7bits: !include ../../test_build_components/common/uart_1200_even_7bits/rp2040-ard.yaml
<<: !include common.yaml
@@ -0,0 +1,14 @@
packages:
uart_9600_even_7bits: !include ../../test_build_components/common/uart_9600_even_7bits/esp32-idf.yaml
teleinfo:
id: test_teleinfo_standard
historical_mode: false
update_interval: 60s
sensor:
- platform: teleinfo
name: sinsts
tag_name: SINSTS
teleinfo_id: test_teleinfo_standard
unit_of_measurement: VA
+113
View File
@@ -25,6 +25,27 @@ esphome:
away: !lambda "return true;"
is_on: !lambda "return false;"
- climate.template.publish:
id: template_climate
current_temperature: 21.0
mode: HEAT
fan_mode: AUTO
swing_mode: "OFF"
preset: NONE
target_temperature: 22.0
# Templated
- climate.template.publish:
id: template_climate
current_temperature: !lambda "return 21.5f;"
mode: !lambda "return climate::CLIMATE_MODE_COOL;"
target_temperature: !lambda "return 23.0f;"
- climate.template.publish:
id: template_climate_custom_modes
custom_fan_mode: "turbo"
custom_preset: "eco_plus"
# Test C++ API: set_template() with stateless lambda (no captures)
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
- lambda: |-
@@ -513,6 +534,98 @@ alarm_control_panel:
codes:
- "1234"
climate:
- platform: template
id: template_climate
name: "Template Climate"
optimistic: true
sensor: template_template_sens
supports_action: true
supports_current_humidity: true
restore_mode: NO_RESTORE
initial_state:
mode: HEAT
target_temperature: 21.0
fan_mode: LOW
supported_modes:
- "OFF"
- HEAT
- COOL
supported_fan_modes:
- AUTO
- LOW
- HIGH
supported_swing_modes:
- "OFF"
- VERTICAL
supported_presets:
- NONE
- ECO
visual:
min_temperature: 16.0
max_temperature: 30.0
temperature_step: 0.5
set_mode_action:
- logger.log:
format: "set_mode_action %d"
args: ["(int) x"]
set_target_temperature_action:
- logger.log:
format: "set_target_temperature_action %.1f"
args: ["x"]
set_target_humidity_action:
- logger.log:
format: "set_target_humidity_action %.1f"
args: ["x"]
set_fan_mode_action:
- logger.log:
format: "set_fan_mode_action %d"
args: ["(int) x"]
set_swing_mode_action:
- logger.log:
format: "set_swing_mode_action %d"
args: ["(int) x"]
set_preset_action:
- logger.log:
format: "set_preset_action %d"
args: ["(int) x"]
on_control:
- logger.log: "on_control fired"
on_state:
- logger.log: "on_state fired"
- platform: template
id: template_climate_custom_modes
name: "Template Climate Custom Modes"
optimistic: true
sensor: template_template_sens
supported_modes:
- "OFF"
- HEAT
custom_fan_modes:
- turbo
- silent
- eco
custom_presets:
- eco_plus
- power_save
- max
set_custom_fan_mode_action:
- logger.log:
format: "set_custom_fan_mode_action %s"
args: ["x.c_str()"]
set_custom_preset_action:
- logger.log:
format: "set_custom_preset_action %s"
args: ["x.c_str()"]
initial_state:
custom_fan_mode: eco
custom_preset: max
visual:
min_temperature: 16.0
max_temperature: 30.0
temperature_step: 0.5
water_heater:
- platform: template
id: template_water_heater
@@ -0,0 +1,29 @@
# Tuya without any network component (no wifi/ethernet/api), as used on
# serial-only or BLE-only Tuya MCU boards. Regression test for
# https://github.com/esphome/esphome/issues/18942
substitutions:
status_pin: P6
packages:
uart: !include ../../test_build_components/common/uart/bk72xx-ard.yaml
tuya:
status_pin: ${status_pin}
binary_sensor:
- platform: tuya
id: tuya_presence
sensor_datapoint: 101
sensor:
- platform: tuya
id: tuya_light_intensity
sensor_datapoint: 103
number:
- platform: tuya
id: tuya_far_detection
number_datapoint: 109
min_value: 0
max_value: 600
step: 1
+1
View File
@@ -14,6 +14,7 @@ esphome:
condition: wifi.ap_active
then:
- logger.log: "WiFi AP is active!"
- wifi.roam
wifi:
networks: