mirror of
https://github.com/esphome/esphome.git
synced 2026-10-03 01:35:59 +00:00
Merge remote-tracking branch 'upstream/dev' into HEAD
This commit is contained in:
@@ -3,8 +3,9 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
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.
|
||||
# api must run its to_code to define USE_API and USE_API_NOISE. The
|
||||
# AUTO_LOADed noise component runs its own to_code via the override in
|
||||
# tests/benchmarks/components/noise/__init__.py.
|
||||
manifest.enable_codegen()
|
||||
|
||||
original_to_code = manifest.to_code
|
||||
|
||||
@@ -49,7 +49,7 @@ static void Encode_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
auto msg = make_sensor_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -69,7 +69,7 @@ static void CalcAndEncode_ListEntitiesSensorResponse(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
@@ -117,7 +117,7 @@ static void Encode_ListEntitiesBinarySensorResponse(benchmark::State &state) {
|
||||
auto msg = make_binary_sensor_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -137,7 +137,7 @@ static void CalcAndEncode_ListEntitiesBinarySensorResponse(benchmark::State &sta
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
@@ -202,7 +202,7 @@ static void Encode_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
auto msg = make_light_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -222,7 +222,7 @@ static void CalcAndEncode_ListEntitiesLightResponse(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ static void Encode_LogResponse_Typical(benchmark::State &state) {
|
||||
msg.level = enums::LOG_LEVEL_DEBUG;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kTypicalLogLine), strlen(kTypicalLogLine));
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -42,7 +42,7 @@ static void Encode_LogResponse_Short(benchmark::State &state) {
|
||||
msg.level = enums::LOG_LEVEL_INFO;
|
||||
msg.set_message(reinterpret_cast<const uint8_t *>(kShortLogLine), strlen(kShortLogLine));
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -84,7 +84,7 @@ static void CalcAndEncode_LogResponse_Typical(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ static void CalcAndEncode_LogResponse_Typical_Fresh(benchmark::State &state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
|
||||
@@ -33,7 +33,7 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) {
|
||||
// Pre-init buffer to typical TCP MSS size to avoid benchmarking
|
||||
// heap allocation — in real use the buffer is reused across writes.
|
||||
APIBuffer buffer;
|
||||
buffer.reserve(1460);
|
||||
(void) buffer.reserve(1460);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -44,7 +44,7 @@ static void PlaintextFrame_WriteSensorState(benchmark::State &state) {
|
||||
msg.missing_state = false;
|
||||
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(padding + size);
|
||||
(void) buffer.resize(padding + size);
|
||||
ProtoWriteBuffer writer(&buffer, padding);
|
||||
msg.encode(writer);
|
||||
|
||||
@@ -70,7 +70,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) {
|
||||
// Pre-init buffer to typical TCP MSS size to avoid benchmarking
|
||||
// heap allocation — in real use the buffer is reused across writes.
|
||||
APIBuffer buffer;
|
||||
buffer.reserve(1460);
|
||||
(void) buffer.reserve(1460);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -85,7 +85,7 @@ static void PlaintextFrame_WriteBatch5(benchmark::State &state) {
|
||||
msg.missing_state = false;
|
||||
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(offset + padding + size + footer);
|
||||
(void) buffer.resize(offset + padding + size + footer);
|
||||
ProtoWriteBuffer writer(&buffer, offset + padding);
|
||||
msg.encode(writer);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ static constexpr int kInnerIterations = 2000;
|
||||
template<typename T> static APIBuffer encode_message(const T &msg) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
return buffer;
|
||||
|
||||
@@ -19,7 +19,7 @@ static void Encode_SensorStateResponse(benchmark::State &state) {
|
||||
msg.state = 23.5f;
|
||||
msg.missing_state = false;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -60,7 +60,7 @@ static void CalcAndEncode_SensorStateResponse(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ static void CalcAndEncode_SensorStateResponse_Fresh(benchmark::State &state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
@@ -103,7 +103,7 @@ static void Encode_BinarySensorStateResponse(benchmark::State &state) {
|
||||
msg.state = true;
|
||||
msg.missing_state = false;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -126,7 +126,7 @@ static void Encode_HelloResponse(benchmark::State &state) {
|
||||
msg.server_info = StringRef::from_lit("esphome v2026.3.0");
|
||||
msg.name = StringRef::from_lit("living-room-sensor");
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -158,7 +158,7 @@ static void Encode_LightStateResponse(benchmark::State &state) {
|
||||
msg.warm_white = 0.0f;
|
||||
msg.effect = StringRef::from_lit("rainbow");
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -243,7 +243,7 @@ static void Encode_DeviceInfoResponse(benchmark::State &state) {
|
||||
auto msg = make_device_info_response();
|
||||
APIBuffer buffer;
|
||||
uint32_t total_size = msg.calculate_size();
|
||||
buffer.resize(total_size);
|
||||
(void) buffer.resize(total_size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -264,7 +264,7 @@ static void CalcAndEncode_DeviceInfoResponse(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
@@ -285,7 +285,7 @@ static void CalcAndEncode_DeviceInfoResponse_Fresh(benchmark::State &state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
@@ -335,7 +335,7 @@ 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);
|
||||
(void) buffer.resize(total_size);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -355,7 +355,7 @@ static void CalcAndEncode_BLERawAdvs12(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
}
|
||||
@@ -372,7 +372,7 @@ static void CalcAndEncode_BLERawAdvs12_Fresh(benchmark::State &state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
APIBuffer buffer;
|
||||
uint32_t size = msg.calculate_size();
|
||||
buffer.resize(size);
|
||||
(void) buffer.resize(size);
|
||||
ProtoWriteBuffer writer(&buffer, 0);
|
||||
msg.encode(writer);
|
||||
benchmark::DoNotOptimize(buffer.data());
|
||||
|
||||
@@ -16,7 +16,7 @@ static constexpr int kInnerIterations = 2000;
|
||||
// Encodes `src` into `out`. Caller owns `out` and must keep it alive across
|
||||
// the decode loop (decoded messages may store pointers back into its bytes).
|
||||
template<typename T> static void encode_into(APIBuffer &out, const T &src) {
|
||||
out.resize(src.calculate_size());
|
||||
(void) out.resize(src.calculate_size());
|
||||
ProtoWriteBuffer writer(&out, 0);
|
||||
src.encode(writer);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ static void Encode_ZWaveProxyFrame(benchmark::State &state) {
|
||||
msg.data = kZWaveFrameData;
|
||||
msg.data_len = sizeof(kZWaveFrameData);
|
||||
APIBuffer buffer;
|
||||
buffer.resize(msg.calculate_size());
|
||||
(void) buffer.resize(msg.calculate_size());
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -111,7 +111,7 @@ static void Encode_SerialProxyDataReceived(benchmark::State &state) {
|
||||
msg.instance = 0;
|
||||
msg.set_data(kSerialPayload, kSerialPayloadSize);
|
||||
APIBuffer buffer;
|
||||
buffer.resize(msg.calculate_size());
|
||||
(void) buffer.resize(msg.calculate_size());
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -171,7 +171,7 @@ static void Encode_InfraredRFReceiveEvent(benchmark::State &state) {
|
||||
msg.key = 0xDEADBEEF;
|
||||
msg.timings = &get_ir_timings_100();
|
||||
APIBuffer buffer;
|
||||
buffer.resize(msg.calculate_size());
|
||||
(void) buffer.resize(msg.calculate_size());
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -254,7 +254,7 @@ static APIBuffer build_infrared_rf_transmit_wire() {
|
||||
put_varint(1);
|
||||
|
||||
APIBuffer buf;
|
||||
buf.resize(len);
|
||||
(void) buf.resize(len);
|
||||
std::memcpy(buf.data(), bytes, len);
|
||||
return buf;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ BENCHMARK(ProtoVarInt_Parse_FiveByte);
|
||||
|
||||
static void Encode_Varint_Small(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
buffer.resize(16);
|
||||
(void) buffer.resize(16);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -73,7 +73,7 @@ BENCHMARK(Encode_Varint_Small);
|
||||
|
||||
static void Encode_Varint_Large(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
buffer.resize(16);
|
||||
(void) buffer.resize(16);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
@@ -88,7 +88,7 @@ BENCHMARK(Encode_Varint_Large);
|
||||
|
||||
static void Encode_Varint_MaxUint32(benchmark::State &state) {
|
||||
APIBuffer buffer;
|
||||
buffer.resize(16);
|
||||
(void) buffer.resize(16);
|
||||
|
||||
for (auto _ : state) {
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
# to_code must run: it defines USE_NOISE and adds the noise-c library
|
||||
# the api benchmark sources need.
|
||||
manifest.enable_codegen()
|
||||
@@ -4,6 +4,6 @@ from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_TIME_TIMEZONE")
|
||||
cg.add_define("USE_TIME_TIMEZONE")
|
||||
|
||||
manifest.to_code = to_code
|
||||
|
||||
@@ -363,4 +363,47 @@ static void Snprintf_Uint32_Large(benchmark::State &state) {
|
||||
}
|
||||
BENCHMARK(Snprintf_Uint32_Large);
|
||||
|
||||
// --- step_to_accuracy_decimals() ---
|
||||
// Called from climate traits and web_server for every number/climate step.
|
||||
|
||||
static void StepToAccuracyDecimals_Tenth(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += step_to_accuracy_decimals(0.1f);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(StepToAccuracyDecimals_Tenth);
|
||||
|
||||
static void StepToAccuracyDecimals_Whole(benchmark::State &state) {
|
||||
for (auto _ : state) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += step_to_accuracy_decimals(1.0f);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(StepToAccuracyDecimals_Whole);
|
||||
|
||||
static void StepToAccuracyDecimals_Mixed(benchmark::State &state) {
|
||||
static constexpr float steps[] = {
|
||||
0.001f, 0.01f, 0.05f, 0.1f, 0.25f, 0.5f, 1.0f, 2.5f, 5.0f, 10.0f,
|
||||
};
|
||||
static constexpr int num_steps = sizeof(steps) / sizeof(steps[0]);
|
||||
for (auto _ : state) {
|
||||
int result = 0;
|
||||
for (int i = 0; i < kInnerIterations; i++) {
|
||||
result += step_to_accuracy_decimals(steps[i % num_steps]);
|
||||
}
|
||||
benchmark::DoNotOptimize(result);
|
||||
}
|
||||
state.SetItemsProcessed(state.iterations() * kInnerIterations);
|
||||
}
|
||||
BENCHMARK(StepToAccuracyDecimals_Mixed);
|
||||
|
||||
} // namespace esphome::benchmarks
|
||||
|
||||
@@ -13,17 +13,18 @@ namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
|
||||
namespace uart {
|
||||
enum class UARTFlushResult : uint8_t {
|
||||
UART_FLUSH_RESULT_SUCCESS,
|
||||
UART_FLUSH_RESULT_ASSUMED_SUCCESS,
|
||||
UART_FLUSH_RESULT_TIMEOUT,
|
||||
UART_FLUSH_RESULT_FAILED,
|
||||
};
|
||||
} // namespace uart
|
||||
|
||||
namespace serial_proxy {
|
||||
|
||||
enum class SerialProxyResult : uint8_t {
|
||||
SERIAL_PROXY_RESULT_OK,
|
||||
SERIAL_PROXY_RESULT_ASSUMED_SUCCESS,
|
||||
SERIAL_PROXY_RESULT_PORT_IN_USE,
|
||||
SERIAL_PROXY_RESULT_INVALID_ARGUMENT,
|
||||
SERIAL_PROXY_RESULT_ERROR,
|
||||
SERIAL_PROXY_RESULT_TIMEOUT,
|
||||
SERIAL_PROXY_RESULT_NOT_SUPPORTED,
|
||||
};
|
||||
|
||||
class SerialProxy {
|
||||
public:
|
||||
void set_instance_index(uint32_t index) { this->instance_index_ = index; }
|
||||
@@ -31,13 +32,23 @@ class SerialProxy {
|
||||
const char *get_name() const { return ""; }
|
||||
api::enums::SerialProxyPortType get_port_type() const { return {}; }
|
||||
api::APIConnection *get_api_connection() { return nullptr; }
|
||||
void serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {}
|
||||
void configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
|
||||
uint32_t stop_bits, uint32_t data_size) {}
|
||||
SerialProxyResult serial_proxy_request(api::APIConnection *conn, api::enums::SerialProxyRequestType type) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
SerialProxyResult configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control, uint8_t parity,
|
||||
uint8_t stop_bits, uint8_t data_size) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
void write_from_client(api::APIConnection *api_connection, const uint8_t *data, size_t len) {}
|
||||
void set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {}
|
||||
SerialProxyResult set_mode_from_client(api::APIConnection *api_connection, api::enums::SerialProxyMode mode) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
SerialProxyResult set_modem_pins(api::APIConnection *api_connection, uint32_t line_states) {
|
||||
return SerialProxyResult::SERIAL_PROXY_RESULT_OK;
|
||||
}
|
||||
uint32_t get_modem_pins() const { return 0; }
|
||||
uart::UARTFlushResult flush_port() { return uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS; }
|
||||
uint32_t get_configured_modem_pins() const { return 0; }
|
||||
SerialProxyResult flush_port(api::APIConnection *api_connection) { return SerialProxyResult::SERIAL_PROXY_RESULT_OK; }
|
||||
|
||||
protected:
|
||||
uint32_t instance_index_{0};
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace zwave_proxy {
|
||||
class ZWaveProxy {
|
||||
public:
|
||||
api::APIConnection *get_api_connection() { return nullptr; }
|
||||
void zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {}
|
||||
api::enums::ZWaveProxyStatus zwave_proxy_request(api::APIConnection *conn, api::enums::ZWaveProxyRequestType type) {
|
||||
return api::enums::ZWAVE_PROXY_STATUS_OK;
|
||||
}
|
||||
void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length) {}
|
||||
void api_connection_authenticated(api::APIConnection *conn) {}
|
||||
uint32_t get_feature_flags() const { return 0; }
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
api:
|
||||
port: 6054
|
||||
reboot_timeout: 0s
|
||||
batch_delay: 0ms
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
api:
|
||||
port: 6053
|
||||
reboot_timeout: 15min
|
||||
batch_delay: 100ms
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Tests for user-defined action field metadata (description / example)."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.api import (
|
||||
_action_strings,
|
||||
_action_strings_size,
|
||||
_has_action_metadata,
|
||||
_validate_esp8266_action_strings,
|
||||
validate_variable,
|
||||
)
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import PlatformFramework
|
||||
from esphome.core import CORE
|
||||
from esphome.cpp_generator import safe_exp
|
||||
from esphome.helpers import fnv1_hash
|
||||
from tests.component_tests.helpers import get_define_value
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
CONFIG = "tests/component_tests/api/test_action_metadata.yaml"
|
||||
CONFIG_ESP8266 = "tests/component_tests/api/test_action_metadata_esp8266.yaml"
|
||||
CONFIG_SHORTHAND = "tests/component_tests/api/test_action_metadata_shorthand.yaml"
|
||||
|
||||
|
||||
def test_metadata_is_emitted_as_progmem_table(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""Every action string is a PROGMEM array referenced from one PROGMEM table."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert (
|
||||
'static constexpr char api_action_str0[] PROGMEM = "play_buzzer";' in main_cpp
|
||||
)
|
||||
assert (
|
||||
'static constexpr char api_action_str1[] PROGMEM = "Play an RTTTL melody on the buzzer";'
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
'static constexpr char api_action_str4[] PROGMEM = "two_short:d=4,o=5,b=100:16e6,16e6";'
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
"static constexpr const char * api_action0_strings[] PROGMEM = {"
|
||||
"api_action_str0, api_action_str1, api_action_str2, api_action_str3, "
|
||||
"api_action_str4, api_action_str5, nullptr, nullptr};" in main_cpp
|
||||
)
|
||||
# An action without metadata still carries the metadata slots (as nullptr)
|
||||
assert (
|
||||
"static constexpr const char * api_action1_strings[] PROGMEM = {"
|
||||
"api_action_str6, nullptr, api_action_str7, nullptr, nullptr};" in main_cpp
|
||||
)
|
||||
assert f"(api_action0_strings, {safe_exp(fnv1_hash('play_buzzer'))});" in main_cpp
|
||||
assert "USE_API_USER_DEFINED_ACTION_METADATA" in {d.name for d in CORE.defines}
|
||||
assert get_define_value("API_USER_ACTION_STRINGS_SCRATCH_SIZE") is None
|
||||
|
||||
|
||||
def test_esp8266_sizes_scratch_buffer_for_largest_action(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""ESP8266 gets a scratch buffer define equal to the byte total of the largest action."""
|
||||
generate_main(CONFIG_ESP8266)
|
||||
|
||||
# play_buzzer: name, description, two variable names, one description, one example,
|
||||
# each with a terminator
|
||||
assert get_define_value("API_USER_ACTION_STRINGS_SCRATCH_SIZE") == "117"
|
||||
|
||||
|
||||
def test_shorthand_variables_emit_no_metadata(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""The name: type shorthand emits a name-only table and no define."""
|
||||
main_cpp = generate_main(CONFIG_SHORTHAND)
|
||||
|
||||
assert (
|
||||
"static constexpr const char * api_action0_strings[] PROGMEM = "
|
||||
"{api_action_str0, api_action_str1};" in main_cpp
|
||||
)
|
||||
assert "USE_API_USER_DEFINED_ACTION_METADATA" not in {d.name for d in CORE.defines}
|
||||
|
||||
|
||||
def test_variable_shorthand_normalizes_to_mapping() -> None:
|
||||
"""A bare type string validates to the mapping form."""
|
||||
assert validate_variable("string") == {"type": "string"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value",
|
||||
[
|
||||
{"description": "no type given"},
|
||||
{"type": "string", "selector": "text"},
|
||||
"stringy",
|
||||
{"type": "stringy"},
|
||||
],
|
||||
)
|
||||
def test_variable_rejects_invalid(value: object) -> None:
|
||||
"""Missing or unknown type and unknown keys raise in both forms."""
|
||||
with pytest.raises(Invalid):
|
||||
validate_variable(value)
|
||||
|
||||
|
||||
def _oversized_action_config() -> dict:
|
||||
return {
|
||||
"actions": [
|
||||
{
|
||||
"action": "big",
|
||||
"description": "x" * 300,
|
||||
"variables": {"a": {"type": "string", "example": "y" * 300}},
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_esp8266_rejects_actions_over_string_budget(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
||||
with pytest.raises(Invalid, match="ESP8266 allows at most 384 bytes"):
|
||||
_validate_esp8266_action_strings(_oversized_action_config())
|
||||
|
||||
|
||||
def test_other_platforms_have_no_string_budget(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
config = _oversized_action_config()
|
||||
assert _validate_esp8266_action_strings(config) is config
|
||||
|
||||
|
||||
def test_empty_metadata_is_unset_and_not_counted() -> None:
|
||||
"""An empty description or example emits nullptr and takes no scratch space."""
|
||||
conf = {
|
||||
"action": "a",
|
||||
"description": "",
|
||||
"variables": {"b": {"type": "int", "description": "", "example": "ex"}},
|
||||
}
|
||||
strings = _action_strings(conf, has_metadata=True)
|
||||
assert strings == ["a", None, "b", None, "ex"]
|
||||
# Every emitted string counts its terminator: "a" + "b" + "ex"
|
||||
assert _action_strings_size(strings) == 2 + 2 + 3
|
||||
|
||||
|
||||
def test_empty_metadata_does_not_enable_the_define() -> None:
|
||||
actions = [
|
||||
{
|
||||
"action": "a",
|
||||
"description": "",
|
||||
"variables": {"b": {"type": "int", "example": ""}},
|
||||
}
|
||||
]
|
||||
assert not _has_action_metadata(actions)
|
||||
actions[0]["variables"]["b"]["example"] = "1"
|
||||
assert _has_action_metadata(actions)
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
logger:
|
||||
|
||||
packages:
|
||||
api: !include test_action_metadata_common.yaml
|
||||
@@ -0,0 +1,18 @@
|
||||
api:
|
||||
actions:
|
||||
- action: play_buzzer
|
||||
description: Play an RTTTL melody on the buzzer
|
||||
variables:
|
||||
song_str:
|
||||
type: string
|
||||
description: RTTTL melody string
|
||||
example: "two_short:d=4,o=5,b=100:16e6,16e6"
|
||||
volume:
|
||||
type: int
|
||||
then:
|
||||
- logger.log: Action Called
|
||||
- action: plain_action
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log: Action Called
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
logger:
|
||||
|
||||
packages:
|
||||
api: !include test_action_metadata_common.yaml
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
logger:
|
||||
|
||||
api:
|
||||
actions:
|
||||
- action: plain_action
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log: Action Called
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Tests that the api component only emits setters for non default values."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config_file", ["bare.yaml", "defaults.yaml"])
|
||||
def test_default_values_are_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
) -> None:
|
||||
"""Port 6053, a 15 min reboot timeout and 100 ms batch delay are C++ initializers.
|
||||
|
||||
Both the schema defaults and the same values written explicitly take the skip path.
|
||||
"""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
assert "api_apiserver_id->set_port(" not in main_cpp
|
||||
assert "api_apiserver_id->set_reboot_timeout(" not in main_cpp
|
||||
assert "api_apiserver_id->set_batch_delay(" not in main_cpp
|
||||
|
||||
|
||||
def test_custom_values_are_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Non default values still reach the C++ setters."""
|
||||
main_cpp = generate_main(component_config_path("custom.yaml"))
|
||||
|
||||
assert "api_apiserver_id->set_port(6054);" in main_cpp
|
||||
assert "api_apiserver_id->set_reboot_timeout(0);" in main_cpp
|
||||
assert "api_apiserver_id->set_batch_delay(0);" in main_cpp
|
||||
@@ -9,7 +9,7 @@ def test_synchronous_chain_keeps_zero_copy_args(generate_main):
|
||||
|
||||
assert (
|
||||
"api::UserServiceTrigger<api::enums::SUPPORTS_RESPONSE_NONE, StringRef>"
|
||||
'("zero_copy_args", {"message"})' in main_cpp
|
||||
"(api_action0_strings," in main_cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ def test_response_callback_args_are_owning(generate_main):
|
||||
|
||||
assert (
|
||||
"api::UserServiceTrigger<api::enums::SUPPORTS_RESPONSE_NONE, std::string>"
|
||||
'("response_args", {"message"})' in main_cpp
|
||||
"(api_action1_strings," in main_cpp
|
||||
)
|
||||
assert "api::HomeAssistantServiceCallAction<std::string>" in main_cpp
|
||||
assert "api::HomeAssistantServiceCallAction<StringRef>" not in main_cpp
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Tests for variables handling in homeassistant.event and homeassistant.action."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
CONFIG = "tests/component_tests/api/test_homeassistant_variables.yaml"
|
||||
|
||||
|
||||
def test_plain_string_with_return_is_compiled_as_lambda_with_warning(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A plain string with a return statement compiles as a lambda and warns."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert main_cpp.count('add_variable(ESPHOME_F("lambda_var"), []() {') == 2
|
||||
assert "return millis();" in main_cpp
|
||||
# The source text must not be sent as a static string value.
|
||||
assert '"return millis();"' not in main_cpp
|
||||
assert "missing the !lambda tag" in caplog.text
|
||||
|
||||
|
||||
def test_static_string_is_kept_as_static_value(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A static string stays static, PROGMEM wrapped, with no warning."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert (
|
||||
main_cpp.count(
|
||||
'add_variable(ESPHOME_F("static_var"), ESPHOME_F("static value"));'
|
||||
)
|
||||
== 2
|
||||
)
|
||||
assert "static value" not in caplog.text
|
||||
|
||||
|
||||
def test_static_id_value_stays_literal_with_hint(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Lambda source without a return stays literal text but warns."""
|
||||
with caplog.at_level(logging.WARNING):
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert 'ESPHOME_F("id(test_sensor).state")' in main_cpp
|
||||
assert "sent as literal text" in caplog.text
|
||||
|
||||
|
||||
def test_explicit_lambda_tag_is_compiled_as_lambda(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""A !lambda value keeps working unchanged."""
|
||||
main_cpp = generate_main(CONFIG)
|
||||
|
||||
assert 'add_variable(ESPHOME_F("tagged_var"), []() {' in main_cpp
|
||||
assert "return App.get_name();" in main_cpp
|
||||
@@ -0,0 +1,32 @@
|
||||
esphome:
|
||||
name: test
|
||||
on_boot:
|
||||
then:
|
||||
# Plain strings with a return statement compile as lambdas
|
||||
- homeassistant.event:
|
||||
event: esphome.test_event
|
||||
data_template:
|
||||
message: "{{ lambda_var }} {{ static_var }} {{ tagged_var }}"
|
||||
variables:
|
||||
lambda_var: |-
|
||||
return millis();
|
||||
static_var: static value
|
||||
tagged_var: !lambda return App.get_name();
|
||||
hint_var: id(test_sensor).state
|
||||
- homeassistant.action:
|
||||
action: notify.notify
|
||||
data_template:
|
||||
message: "{{ lambda_var }} {{ static_var }}"
|
||||
variables:
|
||||
lambda_var: |-
|
||||
return millis();
|
||||
static_var: static value
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: SomeNetwork
|
||||
password: SomePassword
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: bk-family-gate-7238
|
||||
|
||||
bk72xx:
|
||||
board: generic-bk7238
|
||||
|
||||
bk72xx_ble:
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: bk-power-save
|
||||
|
||||
bk72xx:
|
||||
board: cb2s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
power_save_mode: high
|
||||
|
||||
bk72xx_ble:
|
||||
@@ -16,6 +16,7 @@ from esphome.core import EsphomeError
|
||||
("test_bk7231t.yaml", "BK7231T.*BLE 4.2"),
|
||||
("test_bk7252.yaml", "BK7251.*BLE 4.2"),
|
||||
("test_bk7231q.yaml", "BK7231Q.*no BLE"),
|
||||
("test_bk7238.yaml", "BK7238.*bootloader"),
|
||||
],
|
||||
)
|
||||
def test_unsupported_family_rejected(
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""bk72xx_ble keeps WiFi power save off: the Beken SDK's MCU sleep does not
|
||||
wake up once the station is stopped while the BLE controller runs."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_power_save_mode_is_not_applied_with_ble(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("test_power_save.yaml"))
|
||||
|
||||
assert "bk72xx_ble::BK72xxBLE" in main_cpp
|
||||
assert "set_power_save_mode(" not in main_cpp
|
||||
assert "power_save_mode HIGH is not applied" in caplog.text
|
||||
assert "issues/18592" in caplog.text
|
||||
@@ -0,0 +1,13 @@
|
||||
"""The codegen hook external filtering components use to turn on the filter slot."""
|
||||
|
||||
from esphome.components import bluetooth_proxy
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
def test_enable_advertisement_filter_emits_define() -> None:
|
||||
"""External components call this rather than emitting the define."""
|
||||
bluetooth_proxy.enable_advertisement_filter()
|
||||
|
||||
assert "USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER" in {
|
||||
define.name for define in CORE.defines
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Tests for the bridge cdc_acm_uart platform's final validation."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.cdc_acm_uart import bridge
|
||||
from esphome.components.cdc_acm_uart.bridge import CONF_USB_CDC_ACM_ID
|
||||
from esphome.config import Config
|
||||
from esphome.const import CONF_DEBUG, CONF_ID, CONF_UART_ID, PlatformFramework
|
||||
from esphome.core import ID
|
||||
from esphome.types import ConfigType
|
||||
from tests.component_tests.types import SetCoreConfigCallable
|
||||
|
||||
_final_validate = bridge._final_validate
|
||||
|
||||
|
||||
def _set_esp32_s3(set_core_config: SetCoreConfigCallable, **kwargs) -> None:
|
||||
from esphome.components.esp32 import KEY_VARIANT, VARIANT_ESP32S3
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={KEY_VARIANT: VARIANT_ESP32S3},
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def _full_config(uarts: list[ConfigType] | None = None, **domains) -> Config:
|
||||
"""A full config declaring uart_0 and uart_1 (plus any extra entries), as the ID
|
||||
pass leaves it, so the debug check can resolve a uart_id to its declaration."""
|
||||
uarts = uarts or [{CONF_ID: ID("uart_0")}, {CONF_ID: ID("uart_1")}]
|
||||
full = Config()
|
||||
full["uart"] = uarts
|
||||
for index, uart_conf in enumerate(uarts):
|
||||
full.declare_ids.append((uart_conf[CONF_ID], ["uart", index, CONF_ID]))
|
||||
full.update(domains)
|
||||
return full
|
||||
|
||||
|
||||
def _bridge_config(uart_id: str, cdc_id: str) -> dict:
|
||||
return {CONF_UART_ID: ID(uart_id), CONF_USB_CDC_ACM_ID: ID(cdc_id)}
|
||||
|
||||
|
||||
def test_accepts_distinct_uart_and_cdc_interfaces(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(set_core_config, full_config=_full_config())
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
_final_validate(_bridge_config("uart_1", "cdc_acm_2"))
|
||||
|
||||
|
||||
def test_rejects_two_bridges_sharing_a_uart(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(set_core_config, full_config=_full_config())
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
with pytest.raises(cv.Invalid, match="already bridged"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_2"))
|
||||
|
||||
|
||||
def test_rejects_two_bridges_sharing_a_cdc_interface(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(set_core_config, full_config=_full_config())
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
with pytest.raises(cv.Invalid, match="already bridged"):
|
||||
_final_validate(_bridge_config("uart_1", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_uart_shared_with_another_component(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
sensor=[{"platform": "pzemac", CONF_UART_ID: ID("uart_0")}],
|
||||
),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="exclusive"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_cdc_interface_shared_with_another_component(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# The CDC instance is itself a uart::UARTComponent, so other components can bind
|
||||
# it as a plain UART via uart_id -- that must be rejected just like UART sharing.
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
sensor=[{"platform": "pzemac", CONF_UART_ID: ID("cdc_acm_1")}],
|
||||
),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="exclusive"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_uart_referenced_from_nested_config(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# References can sit arbitrarily deep, e.g. inside an automation's action list.
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
binary_sensor=[
|
||||
{
|
||||
"platform": "gpio",
|
||||
"on_press": [{"then": [{CONF_UART_ID: ID("uart_0")}]}],
|
||||
}
|
||||
],
|
||||
),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="exclusive"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_ignores_other_components_on_other_uarts(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
sensor=[{"platform": "pzemac", CONF_UART_ID: ID("uart_1")}],
|
||||
# The bridge domain itself is skipped: this bridge's own entry (and any
|
||||
# bridge-vs-bridge sharing, which the seen-set already rejects) must not
|
||||
# trip the exclusivity scan.
|
||||
bridge=[_bridge_config("uart_0", "cdc_acm_1")],
|
||||
),
|
||||
)
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_rejects_debug_on_bridged_uart(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
# The bridge talks to the IDF driver directly, so the uart debugger would see
|
||||
# nothing and its dummy_receiver would steal RX bytes.
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(uarts=[{CONF_ID: ID("uart_0"), CONF_DEBUG: {}}]),
|
||||
)
|
||||
with pytest.raises(cv.Invalid, match="debug"):
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
|
||||
|
||||
def test_allows_debug_on_other_uart(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
_set_esp32_s3(
|
||||
set_core_config,
|
||||
full_config=_full_config(
|
||||
uarts=[{CONF_ID: ID("uart_0")}, {CONF_ID: ID("uart_1"), CONF_DEBUG: {}}]
|
||||
),
|
||||
)
|
||||
_final_validate(_bridge_config("uart_0", "cdc_acm_1"))
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Power-user fields are marked as advanced on the shared schemas.
|
||||
|
||||
``filters``, ``manual_ip`` and the GPIO switch interlock options are knobs
|
||||
whose defaults suit nearly every user, so a schema-aware editor should keep
|
||||
them behind its "advanced settings" disclosure rather than on the main form.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import binary_sensor, ethernet, sensor, text_sensor, wifi
|
||||
import esphome.config_validation as cv
|
||||
|
||||
|
||||
def _markers(schema: cv.Schema) -> dict[str, object]:
|
||||
s = schema
|
||||
if hasattr(s, "validators"):
|
||||
# cv.All -> the schema is the first validator.
|
||||
s = s.validators[0]
|
||||
return {str(k): k for k in s.schema}
|
||||
|
||||
|
||||
def _gpio_switch_schema() -> cv.Schema:
|
||||
return importlib.import_module("esphome.components.gpio.switch").CONFIG_SCHEMA
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("label", "schema_factory", "fields"),
|
||||
[
|
||||
("sensor", sensor.sensor_schema, ["filters"]),
|
||||
("binary_sensor", binary_sensor.binary_sensor_schema, ["filters"]),
|
||||
("text_sensor", text_sensor.text_sensor_schema, ["filters"]),
|
||||
("wifi_network", lambda: wifi.WIFI_NETWORK_BASE, ["manual_ip"]),
|
||||
("wifi", lambda: wifi.CONFIG_SCHEMA, ["manual_ip"]),
|
||||
("ethernet", lambda: ethernet.BASE_SCHEMA, ["manual_ip"]),
|
||||
("gpio_switch", _gpio_switch_schema, ["interlock", "interlock_wait_time"]),
|
||||
],
|
||||
)
|
||||
def test_power_user_fields_are_advanced(
|
||||
label: str, schema_factory, fields: list[str]
|
||||
) -> None:
|
||||
markers = _markers(schema_factory())
|
||||
for field in fields:
|
||||
assert markers[field].visibility is cv.Visibility.ADVANCED, f"{label}.{field}"
|
||||
|
||||
|
||||
def test_interlock_wait_time_keeps_its_default() -> None:
|
||||
"""Marking the field advanced must not drop its default."""
|
||||
markers = _markers(_gpio_switch_schema())
|
||||
assert markers["interlock_wait_time"].default() == "0ms"
|
||||
@@ -57,6 +57,14 @@ def reset_core() -> Generator[None]:
|
||||
CORE.reset()
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_full_config() -> Generator[None]:
|
||||
"""Give each test a clean final-validate config and restore it after."""
|
||||
token = final_validate.full_config.set(Config())
|
||||
yield
|
||||
final_validate.full_config.reset(token)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
"""Fixture to set up the core configuration for tests."""
|
||||
@@ -67,7 +75,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
*,
|
||||
core_data: ConfigType | None = None,
|
||||
platform_data: ConfigType | None = None,
|
||||
full_config: dict[str, ConfigType] | None = None,
|
||||
full_config: dict[str, ConfigType] | Config | None = None,
|
||||
) -> None:
|
||||
platform, framework = platform_framework.value
|
||||
|
||||
@@ -86,7 +94,12 @@ def set_core_config() -> Generator[SetCoreConfigCallable]:
|
||||
CORE.data[platform.value] = platform_data
|
||||
|
||||
config.path_context.set([])
|
||||
final_validate.full_config.set(full_config or Config())
|
||||
# Production always installs a Config (a FinalValidateConfig), never a plain dict.
|
||||
if not isinstance(full_config, Config):
|
||||
full = Config()
|
||||
full.update(full_config or {})
|
||||
full_config = full
|
||||
final_validate.full_config.set(full_config)
|
||||
|
||||
yield setter
|
||||
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
"""Tests for the deep sleep component."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components import deep_sleep
|
||||
from esphome.const import CONF_WAKEUP_PIN, PlatformFramework
|
||||
|
||||
from ..types import SetCoreConfigCallable
|
||||
|
||||
|
||||
def test_deep_sleep_setup(generate_main):
|
||||
"""
|
||||
@@ -83,3 +91,35 @@ def test_deep_sleep_run_duration_dictionary(generate_main):
|
||||
" .gpio_cause = 30000,\n"
|
||||
"});"
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_deep_sleep_bk72xx_wakeup_pin_mode_at_both_levels_rejected(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""On BK72xx, wakeup_pin_mode at the top level and under the pin entry is an error."""
|
||||
set_core_config(PlatformFramework.BK72XX_ARDUINO)
|
||||
config = {
|
||||
CONF_WAKEUP_PIN: [
|
||||
{"pin": "GPIO12", deep_sleep.CONF_WAKEUP_PIN_MODE: "KEEP_AWAKE"}
|
||||
],
|
||||
deep_sleep.CONF_WAKEUP_PIN_MODE: "INVERT_WAKEUP",
|
||||
}
|
||||
with pytest.raises(cv.Invalid, match="not both"):
|
||||
deep_sleep.validate_config(config)
|
||||
|
||||
|
||||
def test_deep_sleep_bk72xx_top_level_wakeup_pin_mode_moved_onto_single_pin(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""On BK72xx, a top-level wakeup_pin_mode is moved onto the only pin entry."""
|
||||
set_core_config(PlatformFramework.BK72XX_ARDUINO)
|
||||
config = {
|
||||
CONF_WAKEUP_PIN: [{"pin": "GPIO12"}],
|
||||
deep_sleep.CONF_WAKEUP_PIN_MODE: "INVERT_WAKEUP",
|
||||
}
|
||||
result = deep_sleep.validate_config(config)
|
||||
|
||||
assert deep_sleep.CONF_WAKEUP_PIN_MODE not in result
|
||||
assert (
|
||||
result[CONF_WAKEUP_PIN][0][deep_sleep.CONF_WAKEUP_PIN_MODE] == "INVERT_WAKEUP"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Tests for emontx sensor tag defaults."""
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components import sensor
|
||||
from esphome.components.emontx.sensor import CONFIG_SCHEMA, apply_tag_defaults
|
||||
from esphome.const import (
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_STATE_CLASS,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
STATE_CLASS_TOTAL_INCREASING,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_EMPTY,
|
||||
UNIT_HERTZ,
|
||||
UNIT_PULSES,
|
||||
UNIT_VOLT,
|
||||
UNIT_VOLT_AMPS,
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_via_config_schema(tag: str) -> dict:
|
||||
"""Run a minimal config through the real CONFIG_SCHEMA pipeline, the
|
||||
same path a user's YAML goes through."""
|
||||
return CONFIG_SCHEMA(
|
||||
{"tag_name": tag, "emontx_id": "my_emontx", "name": f"{tag} sensor"}
|
||||
)
|
||||
|
||||
|
||||
def test_config_schema_applies_tag_default_state_class():
|
||||
"""If sensor_schema(state_class=...) is reintroduced, the schema-level
|
||||
default wins over apply_tag_defaults' per-prefix value, and E1 would
|
||||
resolve to measurement instead of total_increasing. Driving the real
|
||||
CONFIG_SCHEMA (not just apply_tag_defaults) catches that, since
|
||||
sensor_schema() runs before apply_tag_defaults in the cv.All() chain.
|
||||
"""
|
||||
result = _resolve_via_config_schema("E1")
|
||||
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(
|
||||
STATE_CLASS_TOTAL_INCREASING
|
||||
)
|
||||
|
||||
|
||||
def test_config_schema_applies_tag_default_accuracy_decimals():
|
||||
"""Same root cause as the state_class regression: reintroducing
|
||||
sensor_schema(accuracy_decimals=...) would make V1 resolve to the
|
||||
schema-level default instead of the prefix-specific value of 2.
|
||||
"""
|
||||
result = _resolve_via_config_schema("V1")
|
||||
assert result[CONF_ACCURACY_DECIMALS] == 2
|
||||
|
||||
|
||||
def _make_config(tag: str) -> dict:
|
||||
"""Minimal config dict with only tag_name set — no overrides."""
|
||||
return {"tag_name": tag}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "expected_state_class", "expected_decimals"),
|
||||
[
|
||||
# Known numeric-index prefixes
|
||||
("E1", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
("E12", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
("P1", STATE_CLASS_MEASUREMENT, 0),
|
||||
("V1", STATE_CLASS_MEASUREMENT, 2),
|
||||
("I1", STATE_CLASS_MEASUREMENT, 2),
|
||||
("T1", STATE_CLASS_MEASUREMENT, 2),
|
||||
# Known patterns
|
||||
("PULSE1", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
("PULSE12", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
("PF1", STATE_CLASS_MEASUREMENT, 2),
|
||||
("AP1", STATE_CLASS_MEASUREMENT, 2),
|
||||
("AP12", STATE_CLASS_MEASUREMENT, 2),
|
||||
# Frequency: reported as a single, un-numbered tag
|
||||
("F", STATE_CLASS_MEASUREMENT, 2),
|
||||
# Unknown / free-form tags fall back to generic defaults
|
||||
("CUSTOM1", STATE_CLASS_MEASUREMENT, 0),
|
||||
("X", STATE_CLASS_MEASUREMENT, 0),
|
||||
# "F1" is not the exact "F" tag, so it falls back to generic defaults
|
||||
("F1", STATE_CLASS_MEASUREMENT, 0),
|
||||
# "PULSE" (no index) is how some real emonTx firmware reports a
|
||||
# single pulse counter, so it still resolves to the PULSE defaults
|
||||
("PULSE", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
# Real firmware sends this lowercase; tag_upper's case-folding must
|
||||
# still match it against the PULSE pattern
|
||||
("pulse", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
# PF/AP require a numeric index; the bare prefix alone (no index)
|
||||
# falls back to generic defaults
|
||||
("PF", STATE_CLASS_MEASUREMENT, 0),
|
||||
("AP", STATE_CLASS_MEASUREMENT, 0),
|
||||
],
|
||||
)
|
||||
def test_apply_tag_defaults(tag, expected_state_class, expected_decimals):
|
||||
"""apply_tag_defaults must inject the correct state_class and accuracy_decimals
|
||||
for each tag type when no user overrides are present."""
|
||||
config = _make_config(tag)
|
||||
result = apply_tag_defaults(config)
|
||||
|
||||
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(expected_state_class)
|
||||
assert result[CONF_ACCURACY_DECIMALS] == expected_decimals
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "expected_unit", "expected_device_class"),
|
||||
[
|
||||
# Known numeric-index prefixes
|
||||
("E1", UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY),
|
||||
("E12", UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY),
|
||||
("P1", UNIT_WATT, DEVICE_CLASS_POWER),
|
||||
("V1", UNIT_VOLT, DEVICE_CLASS_VOLTAGE),
|
||||
("I1", UNIT_AMPERE, DEVICE_CLASS_CURRENT),
|
||||
("T1", UNIT_CELSIUS, DEVICE_CLASS_TEMPERATURE),
|
||||
# Known patterns
|
||||
("PULSE1", UNIT_PULSES, DEVICE_CLASS_ENERGY),
|
||||
("PULSE12", UNIT_PULSES, DEVICE_CLASS_ENERGY),
|
||||
# Bare "PULSE" (no index), as reported by some real emonTx firmware
|
||||
("PULSE", UNIT_PULSES, DEVICE_CLASS_ENERGY),
|
||||
# Real firmware sends this lowercase; tag_upper's case-folding must
|
||||
# still match it against the PULSE pattern
|
||||
("pulse", UNIT_PULSES, DEVICE_CLASS_ENERGY),
|
||||
("PF1", UNIT_EMPTY, DEVICE_CLASS_POWER_FACTOR),
|
||||
("AP1", UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER),
|
||||
("AP12", UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER),
|
||||
# Frequency: reported as a single, un-numbered tag
|
||||
("F", UNIT_HERTZ, DEVICE_CLASS_FREQUENCY),
|
||||
],
|
||||
)
|
||||
def test_apply_tag_defaults_unit_and_device_class(
|
||||
tag, expected_unit, expected_device_class
|
||||
):
|
||||
"""apply_tag_defaults must inject the correct, validated unit_of_measurement
|
||||
and device_class for each tag type when no user overrides are present."""
|
||||
config = _make_config(tag)
|
||||
result = apply_tag_defaults(config)
|
||||
|
||||
assert result[CONF_UNIT_OF_MEASUREMENT] == sensor.validate_unit_of_measurement(
|
||||
expected_unit
|
||||
)
|
||||
assert result[CONF_DEVICE_CLASS] == sensor.validate_device_class(
|
||||
expected_device_class
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tag",
|
||||
[
|
||||
"CUSTOM1",
|
||||
"X",
|
||||
# Non-numeric suffixes must not collide with a PATTERN_CONFIGS prefix
|
||||
# (e.g. "APPLE" starting with "AP", "PFX" starting with "PF").
|
||||
"APPLE",
|
||||
"PFX",
|
||||
"PULSE_A",
|
||||
# "F1" is not the exact "F" tag
|
||||
"F1",
|
||||
# Bare "PF"/"AP" (no numeric index) don't match; unlike "PULSE",
|
||||
# real firmware never reports these without an index
|
||||
"PF",
|
||||
"AP",
|
||||
],
|
||||
)
|
||||
def test_apply_tag_defaults_unknown_tag_has_no_unit_or_device_class(tag):
|
||||
"""Unknown / free-form tags only get generic state_class and
|
||||
accuracy_decimals defaults; unit_of_measurement and device_class are left
|
||||
for the user to set explicitly."""
|
||||
config = _make_config(tag)
|
||||
result = apply_tag_defaults(config)
|
||||
|
||||
assert CONF_UNIT_OF_MEASUREMENT not in result
|
||||
assert CONF_DEVICE_CLASS not in result
|
||||
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(
|
||||
STATE_CLASS_MEASUREMENT
|
||||
)
|
||||
assert result[CONF_ACCURACY_DECIMALS] == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag", "user_state_class", "user_decimals"),
|
||||
[
|
||||
# User overrides must not be clobbered by defaults
|
||||
("E1", STATE_CLASS_MEASUREMENT, 3),
|
||||
("PULSE1", STATE_CLASS_MEASUREMENT, 1),
|
||||
("V1", STATE_CLASS_TOTAL_INCREASING, 0),
|
||||
("CUSTOM1", STATE_CLASS_TOTAL_INCREASING, 4),
|
||||
],
|
||||
)
|
||||
def test_apply_tag_defaults_respects_user_overrides(
|
||||
tag, user_state_class, user_decimals
|
||||
):
|
||||
"""apply_tag_defaults must not overwrite values already set by the user."""
|
||||
config = _make_config(tag)
|
||||
config[CONF_STATE_CLASS] = sensor.validate_state_class(user_state_class)
|
||||
config[CONF_ACCURACY_DECIMALS] = user_decimals
|
||||
|
||||
result = apply_tag_defaults(config)
|
||||
|
||||
assert result[CONF_STATE_CLASS] == sensor.validate_state_class(user_state_class)
|
||||
assert result[CONF_ACCURACY_DECIMALS] == user_decimals
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
variant: esp32s3
|
||||
|
||||
spi:
|
||||
clk_pin: GPIO7
|
||||
mosi_pin: GPIO9
|
||||
|
||||
display:
|
||||
- platform: epaper_spi
|
||||
id: epaper_display
|
||||
model: seeed-reterminal-e1001
|
||||
@@ -439,6 +439,23 @@ def test_enable_pin_multiple(
|
||||
assert all(pin["mode"]["output"] is True for pin in enable_pins)
|
||||
|
||||
|
||||
def test_uc8179_e1001_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that the reTerminal E1001 model generates the UC8179 driver and init sequence."""
|
||||
main_cpp = generate_main(component_config_path("uc8179_e1001_test.yaml"))
|
||||
|
||||
# The model must instantiate the UC8179 driver class with the panel dimensions
|
||||
assert "epaper_spi::EPaperUC8179" in main_cpp
|
||||
assert re.search(r'"SEEED-RETERMINAL-E1001",\s*800,\s*480', main_cpp)
|
||||
|
||||
# The generated init sequence must contain the UC8179 resolution setting
|
||||
# for 800x480: command 0x61, 4 data bytes 0x03 0x20 0x01 0xE0
|
||||
# (rendered as decimal in the generated array)
|
||||
assert "97, 4, 3, 32, 1, 224" in main_cpp
|
||||
|
||||
|
||||
def test_enable_pin_code_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
libraries:
|
||||
- NetworkClientSecure
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: arduino
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
use_full_certificate_bundle: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE: y
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
i2c:
|
||||
sda: 21
|
||||
scl: 22
|
||||
|
||||
output:
|
||||
- platform: ledc
|
||||
id: ledc_out
|
||||
pin: 25
|
||||
- platform: ac_dimmer
|
||||
id: dimmer_out
|
||||
gate_pin: 26
|
||||
zero_cross_pin: 27
|
||||
@@ -0,0 +1,15 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
esp32_camera_web_server:
|
||||
port: 8080
|
||||
mode: stream
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
espnow:
|
||||
channel: 1
|
||||
auto_add_peer: true
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: false
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
sensor:
|
||||
- platform: internal_temperature
|
||||
name: Internal Temperature
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
mqtt:
|
||||
broker: "10.0.0.1"
|
||||
@@ -0,0 +1,20 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
uart:
|
||||
tx_pin: 17
|
||||
rx_pin: 16
|
||||
baud_rate: 115200
|
||||
|
||||
display:
|
||||
- platform: nextion
|
||||
tft_url: "http://10.0.0.1/display.tft"
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_NVS_ENCRYPTION: y
|
||||
CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC: y
|
||||
CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID: "0"
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
web_server:
|
||||
version: 3
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
esp32_ble_tracker:
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32-s3-devkitc-1
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
sensor:
|
||||
- platform: internal_temperature
|
||||
name: Internal Temperature
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_NVS_ENCRYPTION: n
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s31
|
||||
board: esp32-s31-devkitc
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
execute_from_psram: true
|
||||
|
||||
psram:
|
||||
mode: octal
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
version: 5.0.6
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: gd
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: generic
|
||||
@@ -0,0 +1,10 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_mode: opi
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
flash_chip: mxic_opi
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32s3
|
||||
flash_mode: opi
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
network:
|
||||
enable_ipv6: true
|
||||
|
||||
openthread:
|
||||
channel: 13
|
||||
network_name: OpenThread-8f28
|
||||
network_key: 0xdfd34f0f05cad978ec4e32b0413038ff
|
||||
pan_id: 0x8f28
|
||||
ext_pan_id: 0xd63e8e3e495ebbc3
|
||||
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
|
||||
mesh_local_prefix: fd53:145f:ed22:ad81::/64
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
disable_mbedtls_tls_server: false
|
||||
disable_mbedtls_tls_extras: false
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
sdkconfig_options:
|
||||
CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT: y
|
||||
CONFIG_MBEDTLS_CCM_C: y
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
eap:
|
||||
identity: "user@example.org"
|
||||
username: "user"
|
||||
password: "secret"
|
||||
|
||||
http_request:
|
||||
verify_ssl: true
|
||||
@@ -6,6 +6,8 @@ esp32:
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
mdns:
|
||||
|
||||
ethernet:
|
||||
type: W5500
|
||||
clk_pin: 19
|
||||
|
||||
@@ -6,6 +6,8 @@ esp32:
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
mdns:
|
||||
|
||||
wifi:
|
||||
ssid: "test_ssid"
|
||||
password: "test_password"
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32c6
|
||||
framework:
|
||||
type: esp-idf
|
||||
advanced:
|
||||
enable_full_printf: true
|
||||
@@ -10,19 +10,25 @@ from typing import Any
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import (
|
||||
ESP32_FLASH_CHIPS,
|
||||
KEY_FATFS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_EXTRAS_REQUIRED,
|
||||
KEY_MBEDTLS_TLS_SERVER_REQUIRED,
|
||||
KEY_VFS_DIR_REQUIRED,
|
||||
KEY_VFS_SELECT_REQUIRED,
|
||||
KEY_VFS_TERMIOS_REQUIRED,
|
||||
MBEDTLS_TLS_EXTRA_OPTIONS,
|
||||
VARIANT_ESP32,
|
||||
VARIANTS,
|
||||
NetworkSdkconfigData,
|
||||
RawSdkconfigValue,
|
||||
_ota_downgrade_protection_errors,
|
||||
_reconcile_network_sdkconfig,
|
||||
_reconcile_vfs_fatfs_sdkconfig,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
KEY_ESP32,
|
||||
KEY_EXCLUDE_COMPONENTS,
|
||||
KEY_NETWORK_SDKCONFIG,
|
||||
KEY_SDKCONFIG_OPTIONS,
|
||||
KEY_VARIANT,
|
||||
@@ -131,6 +137,20 @@ def test_esp32_rejects_unsupported_toolchains(
|
||||
CONFIG_SCHEMA({"variant": VARIANT_ESP32, "toolchain": config_toolchain})
|
||||
|
||||
|
||||
def test_esp32_rejects_unsupported_cli_toolchain(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A --toolchain the platform cannot serve fails instead of silently
|
||||
building with PlatformIO (the CLI path bypasses the YAML validator)."""
|
||||
set_core_config(PlatformFramework.ESP32_IDF)
|
||||
|
||||
from esphome.components.esp32 import CONFIG_SCHEMA
|
||||
|
||||
CORE.toolchain = Toolchain.ARDUINO
|
||||
with pytest.raises(cv.Invalid, match="Unsupported toolchain 'arduino'"):
|
||||
CONFIG_SCHEMA({"variant": VARIANT_ESP32})
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error_match"),
|
||||
[
|
||||
@@ -187,6 +207,18 @@ def test_esp32_rejects_unsupported_toolchains(
|
||||
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_requires_psram_p4_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s31",
|
||||
"board": "esp32-s31-devkitc",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"execute_from_psram": True},
|
||||
},
|
||||
},
|
||||
r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]",
|
||||
id="execute_from_psram_requires_psram_s31_config",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
@@ -221,6 +253,51 @@ def test_esp32_rejects_unsupported_toolchains(
|
||||
r"value must be at most 5 .* @ data\['framework'\]\['advanced'\]\['nvs_encryption'\]\['key_id'\]",
|
||||
id="nvs_encryption_key_id_out_of_range",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32",
|
||||
"board": "esp32dev",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"flash_chip": "mxic_opi"},
|
||||
},
|
||||
},
|
||||
r"'flash_chip: mxic_opi' is only supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
|
||||
id="flash_chip_mxic_opi_only_on_s3",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"flash_mode": "opi",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"flash_chip": "gd"},
|
||||
},
|
||||
},
|
||||
r"'flash_chip: gd' does not match 'flash_mode: opi'; octal flash uses mxic_opi @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
|
||||
id="flash_chip_must_match_opi_mode",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32s3",
|
||||
"framework": {
|
||||
"type": "esp-idf",
|
||||
"advanced": {"flash_chip": "mxic_opi"},
|
||||
},
|
||||
},
|
||||
r"'flash_chip: mxic_opi' requires 'flash_mode: opi' @ data\['framework'\]\['advanced'\]\['flash_chip'\]",
|
||||
id="flash_chip_mxic_opi_requires_opi_mode",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"variant": "esp32",
|
||||
"board": "esp32dev",
|
||||
"flash_mode": "opi",
|
||||
"framework": {"type": "esp-idf"},
|
||||
},
|
||||
r"'flash_mode: opi' is only supported on ESP32S3 @ data\['flash_mode'\]",
|
||||
id="flash_mode_opi_only_on_s3",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_esp32_configuration_errors(
|
||||
@@ -236,16 +313,182 @@ def test_esp32_configuration_errors(
|
||||
FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "reincluded"),
|
||||
[
|
||||
pytest.param(
|
||||
"exclusion_reincludes.yaml",
|
||||
("esp_driver_i2c", "esp_driver_ledc", "esp_driver_gptimer"),
|
||||
id="i2c_ledc_ac_dimmer",
|
||||
),
|
||||
# esp-tls has three owners; a per-owner config makes a dropped
|
||||
# re-include from any single one fail the test.
|
||||
pytest.param(
|
||||
"exclusion_reincludes_http_request.yaml",
|
||||
("esp-tls", "esp_http_client"),
|
||||
id="http_request",
|
||||
),
|
||||
pytest.param(
|
||||
# "mqtt" itself is deliberately not asserted: on IDF >= 6.0 it
|
||||
# is a managed component and never leaves the exclusion set.
|
||||
"exclusion_reincludes_mqtt.yaml",
|
||||
("esp-tls",),
|
||||
id="mqtt",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_web_server.yaml",
|
||||
("esp-tls", "esp_http_server"),
|
||||
id="web_server_idf",
|
||||
),
|
||||
pytest.param(
|
||||
"nvs_encryption_s3.yaml",
|
||||
("nvs_sec_provider",),
|
||||
id="nvs_encryption",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_nvs_sdkconfig.yaml",
|
||||
("nvs_sec_provider",),
|
||||
id="nvs_encryption_raw_sdkconfig",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_camera_web_server.yaml",
|
||||
("esp_http_server",),
|
||||
id="esp32_camera_web_server",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_nextion.yaml",
|
||||
("esp-tls", "esp_http_client"),
|
||||
id="nextion",
|
||||
),
|
||||
pytest.param(
|
||||
# esp_wifi/wpa_supplicant from request_wifi(), bt from
|
||||
# request_bluetooth(), esp_coex from esp32_ble_tracker's software
|
||||
# coexistence (defaults on with wifi). esp_phy stays excluded;
|
||||
# IDF requirement expansion pulls it back via esp_wifi.
|
||||
"exclusion_reincludes_wifi_ble.yaml",
|
||||
("esp_wifi", "wpa_supplicant", "bt", "esp_coex"),
|
||||
id="wifi_ble",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_espnow.yaml",
|
||||
("esp_wifi",),
|
||||
id="espnow",
|
||||
),
|
||||
pytest.param(
|
||||
# temprature_sens_read() on the original ESP32 lives in the esp_phy blob.
|
||||
"exclusion_reincludes_internal_temperature.yaml",
|
||||
("esp_phy",),
|
||||
id="internal_temperature",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_default_exclusions_reincluded_by_owning_components(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
reincluded: tuple[str, ...],
|
||||
) -> None:
|
||||
"""Components whose IDF driver is excluded by default must re-include it
|
||||
during codegen; a dropped include_builtin_idf_component() call would only
|
||||
surface as a missing-header failure in a full compile job."""
|
||||
generate_main(component_config_path(config_file))
|
||||
excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
|
||||
for name in reincluded:
|
||||
assert name not in excluded, f"{name} should have been re-included"
|
||||
|
||||
# Components no part of this config touches stay excluded.
|
||||
assert "unity" in excluded
|
||||
assert "fatfs" in excluded
|
||||
# The HTTP server only comes back for configs that run one.
|
||||
assert ("esp_http_server" in excluded) == ("esp_http_server" not in reincluded)
|
||||
|
||||
|
||||
def test_esp_phy_stays_excluded_for_internal_temperature_on_newer_variants(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Only the original ESP32 reads the PHY blob; other variants use esp_driver_tsens."""
|
||||
generate_main(component_config_path("exclusion_stays_internal_temperature_s3.yaml"))
|
||||
assert "esp_phy" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
|
||||
|
||||
def test_nvs_sec_provider_stays_excluded_when_encryption_is_off(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""An explicit CONFIG_NVS_ENCRYPTION=n keeps nvs_sec_provider excluded."""
|
||||
generate_main(component_config_path("exclusion_stays_nvs_sdkconfig_off.yaml"))
|
||||
assert "nvs_sec_provider" in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
|
||||
|
||||
_BUNDLE_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE",
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN",
|
||||
"CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("exclusion_reincludes.yaml", (False, None, None), id="no_tls"),
|
||||
pytest.param(
|
||||
"certificate_bundle_http_request.yaml",
|
||||
(True, True, False),
|
||||
id="http_request",
|
||||
),
|
||||
pytest.param(
|
||||
"exclusion_reincludes_http_request.yaml",
|
||||
(False, None, None),
|
||||
id="http_request_no_verify",
|
||||
),
|
||||
pytest.param(
|
||||
"certificate_bundle_full.yaml", (True, None, True), id="full_option"
|
||||
),
|
||||
pytest.param(
|
||||
"certificate_bundle_arduino_tls.yaml",
|
||||
(True, True, False),
|
||||
id="arduino_network_client_secure",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_certificate_bundle_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: tuple[bool | None, ...],
|
||||
) -> None:
|
||||
"""The bundle and its CMN/FULL variant are written only when requested."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _BUNDLE_OPTIONS) == expected
|
||||
|
||||
|
||||
def test_user_sdkconfig_certificate_bundle_wins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A raw sdkconfig_options bundle setting is kept and still pins CMN."""
|
||||
generate_main(component_config_path("certificate_bundle_sdkconfig.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
value = sdkconfig["CONFIG_MBEDTLS_CERTIFICATE_BUNDLE"]
|
||||
assert isinstance(value, RawSdkconfigValue)
|
||||
assert value.value == "y"
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN") is True
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL") is False
|
||||
|
||||
|
||||
def test_execute_from_psram_s3_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig options."""
|
||||
"""Test that execute_from_psram on ESP32-S3 sets the correct sdkconfig option."""
|
||||
generate_main(component_config_path("execute_from_psram_s3.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_FETCH_INSTRUCTIONS") is True
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_RODATA") is True
|
||||
assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_XIP_FROM_PSRAM") is True
|
||||
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_execute_from_psram_p4_sdkconfig(
|
||||
@@ -260,6 +503,18 @@ def test_execute_from_psram_p4_sdkconfig(
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_execute_from_psram_s31_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test that execute_from_psram on ESP32-S31 sets the correct sdkconfig option."""
|
||||
generate_main(component_config_path("execute_from_psram_s31.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_SPIRAM_XIP_FROM_PSRAM") is True
|
||||
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
|
||||
|
||||
def test_nvs_encryption_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
@@ -449,6 +704,27 @@ def test_platformio_arduino_enables_reproducible_build(
|
||||
assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
("reproducible_build.yaml", True),
|
||||
("reproducible_build_arduino.yaml", True),
|
||||
("file_macro_idf_5_0.yaml", False),
|
||||
],
|
||||
)
|
||||
def test_file_macro_is_basename_only(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""__FILE__ becomes the basename on GCC 12 toolchains; IDF 5.0 (GCC 11) is skipped."""
|
||||
generate_main(component_config_path(config_file))
|
||||
|
||||
assert ("-D__FILE__=__FILE_NAME__" in CORE.build_flags) is expected
|
||||
assert ("-Wno-builtin-macro-redefined" in CORE.build_flags) is expected
|
||||
|
||||
|
||||
def test_native_idf_enables_reproducible_build(
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
@@ -474,10 +750,59 @@ def test_flash_mode_sets_sdkconfig_and_pio_option(
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHMODE_QIO") is True
|
||||
assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHFREQ_80M") is True
|
||||
assert sdkconfig.get("CONFIG_ESPTOOLPY_OCT_FLASH") is False
|
||||
assert CORE.platformio_options.get("board_build.flash_mode") == "qio"
|
||||
assert CORE.platformio_options.get("board_build.f_flash") == "80000000L"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "enabled"),
|
||||
[
|
||||
pytest.param("flash_chip_gd.yaml", "CONFIG_SPI_FLASH_SUPPORT_GD_CHIP", id="gd"),
|
||||
pytest.param("flash_chip_generic.yaml", None, id="generic"),
|
||||
pytest.param(
|
||||
"flash_chip_mxic_opi_s3.yaml",
|
||||
"CONFIG_SPI_FLASH_SUPPORT_MXIC_OPI_CHIP",
|
||||
id="mxic_opi_s3",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_flash_chip_keeps_one_vendor_driver(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
enabled: str | None,
|
||||
) -> None:
|
||||
"""flash_chip enables only the chosen vendor driver."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
vendors = {
|
||||
k: v for k, v in sdkconfig.items() if k.startswith("CONFIG_SPI_FLASH_SUPPORT_")
|
||||
}
|
||||
assert vendors == {flag: flag == enabled for flag in ESP32_FLASH_CHIPS.values()}
|
||||
|
||||
|
||||
def test_flash_chip_unset_keeps_idf_defaults(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without flash_chip every vendor driver stays at its ESP-IDF default."""
|
||||
generate_main(component_config_path("flash_mode_default.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert not any(key.startswith("CONFIG_SPI_FLASH_SUPPORT_") for key in sdkconfig)
|
||||
|
||||
|
||||
def test_flash_mode_opi_enables_octal_flash(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""flash_mode: opi needs the octal flash switch or ESP-IDF ignores the mode."""
|
||||
generate_main(component_config_path("flash_mode_opi_s3.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_ESPTOOLPY_FLASHMODE_OPI") is True
|
||||
assert sdkconfig.get("CONFIG_ESPTOOLPY_OCT_FLASH") is True
|
||||
|
||||
|
||||
def test_flash_mode_unset_leaves_defaults(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
@@ -783,6 +1108,14 @@ def test_network_wifi_only_reconciles_end_to_end(
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_ESP_WIFI_SOFTAP_SUPPORT") is False
|
||||
assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False
|
||||
# request_wifi() also puts the WiFi components back in the build set;
|
||||
# esp_phy stays excluded, IDF requirement expansion pulls it back.
|
||||
excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
assert "esp_wifi" not in excluded
|
||||
assert "wpa_supplicant" not in excluded
|
||||
assert "esp_phy" in excluded
|
||||
# With wifi present mdns keeps its predefined interfaces.
|
||||
assert "CONFIG_MDNS_PREDEF_NETIF_STA" not in sdkconfig
|
||||
# WiFi stack stays enabled (no ethernet) and no Bluetooth requested.
|
||||
assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig
|
||||
assert "CONFIG_BT_ENABLED" not in sdkconfig
|
||||
@@ -798,6 +1131,12 @@ def test_network_ethernet_only_reconciles_end_to_end(
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_ESP_WIFI_ENABLED") is False
|
||||
assert sdkconfig.get("CONFIG_SW_COEXIST_ENABLE") is False
|
||||
# The whole radio stack stays out of the build set as well.
|
||||
excluded = CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
|
||||
assert {"esp_wifi", "wpa_supplicant", "esp_phy", "esp_coex", "bt"} <= excluded
|
||||
# Without wifi, mdns drops its predefined STA/AP interfaces.
|
||||
assert sdkconfig.get("CONFIG_MDNS_PREDEF_NETIF_STA") is False
|
||||
assert sdkconfig.get("CONFIG_MDNS_PREDEF_NETIF_AP") is False
|
||||
|
||||
|
||||
def test_network_wifi_ble_coexistence_reconciles_end_to_end(
|
||||
@@ -1072,3 +1411,146 @@ def test_parse_pio_platform_version(value: str, expected: str) -> None:
|
||||
from esphome.components.esp32 import _parse_pio_platform_version
|
||||
|
||||
assert _parse_pio_platform_version(value) == expected
|
||||
|
||||
|
||||
def test_esp32_s31_gpio_validation(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""S31: GPIO26-28/30-32 are reserved for the SPI flash interface, GPIO29
|
||||
and GPIO41 do not exist, GPIO33 is a normal pin, and GPIO36 is a
|
||||
strapping pin."""
|
||||
from esphome.components.esp32.const import VARIANT_ESP32S31
|
||||
from esphome.components.esp32.gpio import validate_supports
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_OPEN_DRAIN, CONF_OUTPUT
|
||||
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32S31}
|
||||
)
|
||||
|
||||
input_mode = {CONF_INPUT: True, CONF_OUTPUT: False, CONF_OPEN_DRAIN: False}
|
||||
|
||||
# Not reserved; a normal GPIO
|
||||
pin = {CONF_NUMBER: 33, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
assert validate_gpio_pin(pin)[CONF_NUMBER] == 33
|
||||
|
||||
# Reserved for the SPI flash interface, but can be bypassed with
|
||||
# ignore_pin_validation_error
|
||||
for num in (26, 27, 28, 30, 31, 32):
|
||||
with pytest.raises(cv.Invalid, match=f"GPIO{num} is reserved"):
|
||||
validate_gpio_pin(
|
||||
{CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
)
|
||||
pin = {CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: True}
|
||||
assert validate_gpio_pin(pin)[CONF_NUMBER] == num
|
||||
|
||||
for num in (29, 41):
|
||||
with pytest.raises(cv.Invalid, match=f"GPIO{num} does not exist"):
|
||||
validate_gpio_pin(
|
||||
{CONF_NUMBER: num, CONF_IGNORE_PIN_VALIDATION_ERROR: False}
|
||||
)
|
||||
# Also rejected in validate_supports so ignore_pin_validation_error
|
||||
# cannot bypass it
|
||||
with pytest.raises(cv.Invalid, match=f"GPIO{num} does not exist"):
|
||||
validate_supports({CONF_NUMBER: num, CONF_MODE: input_mode})
|
||||
|
||||
pin = {CONF_NUMBER: 36, CONF_MODE: input_mode}
|
||||
with caplog.at_level("WARNING"):
|
||||
validate_supports(pin)
|
||||
assert "GPIO36 is a strapping PIN" in caplog.text
|
||||
|
||||
|
||||
_TLS_SERVER_OPTIONS = (
|
||||
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
|
||||
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "server", "extras"),
|
||||
[
|
||||
pytest.param("mbedtls_tls_default.yaml", (True, False), False, id="default"),
|
||||
pytest.param("mbedtls_tls_opt_out.yaml", (None, None), None, id="opt_out"),
|
||||
pytest.param("mbedtls_tls_wifi_eap.yaml", (True, False), None, id="wifi_eap"),
|
||||
],
|
||||
)
|
||||
def test_mbedtls_tls_trim_sdkconfig(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
server: tuple[bool | None, bool | None],
|
||||
extras: bool | None,
|
||||
) -> None:
|
||||
"""Client-only TLS and the unused-feature trims apply unless opted out or required."""
|
||||
generate_main(component_config_path(config_file))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == server
|
||||
assert {sdkconfig.get(name) for name in MBEDTLS_TLS_EXTRA_OPTIONS} == {extras}
|
||||
|
||||
|
||||
_OPENTHREAD_EXTRAS = {"CONFIG_MBEDTLS_CCM_C", "CONFIG_MBEDTLS_ECDSA_DETERMINISTIC"}
|
||||
|
||||
|
||||
def test_mbedtls_tls_openthread_keeps_only_what_it_uses(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The OpenThread config keeps the DTLS server, CCM and deterministic ECDSA; the rest is trimmed."""
|
||||
generate_main(component_config_path("mbedtls_tls_openthread.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert tuple(sdkconfig.get(name) for name in _TLS_SERVER_OPTIONS) == (None, None)
|
||||
for name in MBEDTLS_TLS_EXTRA_OPTIONS:
|
||||
assert sdkconfig.get(name) is (None if name in _OPENTHREAD_EXTRAS else False)
|
||||
|
||||
|
||||
def test_mbedtls_tls_user_sdkconfig_wins(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""A user-set TLS role member leaves the whole choice alone; other user values are kept."""
|
||||
generate_main(component_config_path("mbedtls_tls_user_sdkconfig.yaml"))
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_MBEDTLS_TLS_CLIENT_ONLY") is None
|
||||
role = sdkconfig["CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT"]
|
||||
assert isinstance(role, RawSdkconfigValue) and role.value == "y"
|
||||
ccm = sdkconfig["CONFIG_MBEDTLS_CCM_C"]
|
||||
assert isinstance(ccm, RawSdkconfigValue) and ccm.value == "y"
|
||||
assert {
|
||||
sdkconfig.get(name)
|
||||
for name in MBEDTLS_TLS_EXTRA_OPTIONS
|
||||
if name != "CONFIG_MBEDTLS_CCM_C"
|
||||
} == {False}
|
||||
|
||||
|
||||
def test_mbedtls_tls_openthread_requires_server_and_extras(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The OpenThread hooks mark the DTLS server and CCM/deterministic ECDSA as required."""
|
||||
generate_main(component_config_path("mbedtls_tls_openthread.yaml"))
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_SERVER_REQUIRED] is True
|
||||
assert CORE.data[KEY_ESP32][KEY_MBEDTLS_TLS_EXTRAS_REQUIRED] == _OPENTHREAD_EXTRAS
|
||||
|
||||
|
||||
_VASPRINTF_STUB_FLAGS = {"-Wl,--wrap=vasprintf", "-Wl,--undefined=__wrap_vasprintf"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "expected"),
|
||||
[
|
||||
pytest.param("vasprintf_stub_c6.yaml", True, id="c6"),
|
||||
pytest.param("vasprintf_stub_c6_full_printf.yaml", False, id="c6_full_printf"),
|
||||
pytest.param("exclusion_reincludes.yaml", False, id="esp32"),
|
||||
],
|
||||
)
|
||||
def test_vasprintf_stub_only_on_rom_vsnprintf_variants(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
expected: bool,
|
||||
) -> None:
|
||||
"""The vasprintf wrap is emitted only where the ROM lacks vasprintf but has vsnprintf."""
|
||||
generate_main(component_config_path(config_file))
|
||||
assert (CORE.build_flags >= _VASPRINTF_STUB_FLAGS) is expected
|
||||
defines = {define.name for define in CORE.defines}
|
||||
assert ("USE_ESP32_VASPRINTF_STUB" in defines) is expected
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
# esp32_ble_server is only auto-loaded here, so it has no services of its own.
|
||||
improv_ble:
|
||||
authorizer: none
|
||||
@@ -0,0 +1,9 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble_server
|
||||
manufacturer_data: [0x72, 0x04, 0x00, 0x23]
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
variant: esp32
|
||||
|
||||
esp32_ble_server:
|
||||
id: ble_server
|
||||
services:
|
||||
- uuid: 2a24b789-7aab-4535-af3e-ee76a35cc12d
|
||||
characteristics:
|
||||
- uuid: cad48e28-7fbe-41cf-bae9-d77a6c233423
|
||||
read: true
|
||||
value: [1, 2, 3, 4]
|
||||
@@ -1,5 +1,10 @@
|
||||
"""Tests for esp32_ble_server configuration helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32_ble_server import (
|
||||
@@ -45,3 +50,26 @@ def test_uuid_is_matches_descriptor_short_strings(uuid16) -> None:
|
||||
assert uuid_is(uuid16, uuid16)
|
||||
assert uuid_is(f"{uuid16:04X}", uuid16)
|
||||
assert uuid_is(f"{uuid16:08X}", uuid16)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "required"),
|
||||
[
|
||||
# Auto-loaded by improv_ble only: nothing to find until Improv asks for it
|
||||
("improv_only.yaml", False),
|
||||
# The configuration defines a service clients are meant to connect to
|
||||
("own_service.yaml", True),
|
||||
# Manufacturer data is only useful if it is actually broadcast
|
||||
("manufacturer_data_only.yaml", True),
|
||||
],
|
||||
)
|
||||
def test_advertising_required(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
config_file: str,
|
||||
required: bool,
|
||||
) -> None:
|
||||
"""The server only requests advertising when the configuration needs it."""
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
|
||||
assert f"set_advertising_required({str(required).lower()})" in main_cpp
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
esphome:
|
||||
name: scan-window-explicit
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
|
||||
esp32_ble_tracker:
|
||||
scan_parameters:
|
||||
window: 30ms
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: scan-window-raised
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
|
||||
esp32_ble_tracker:
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
|
||||
api:
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: scan-window-scan-only
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
|
||||
esp32_ble_tracker:
|
||||
@@ -0,0 +1,14 @@
|
||||
esphome:
|
||||
name: scan-window-user-scan-only
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
|
||||
esp32_ble_tracker:
|
||||
scan_parameters:
|
||||
connection_scan_window: 20ms
|
||||
@@ -12,11 +12,13 @@ arbiter a full-duty scan would starve wifi, so the 30 ms default is kept.
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.ble_device_base import to_ble_units
|
||||
from esphome.components.ble_device_base import CONF_CONNECTION_SCAN_WINDOW, to_ble_units
|
||||
from esphome.components.const import CONF_SCAN_PARAMETERS, CONF_WINDOW
|
||||
from esphome.components.esp32 import KEY_IDF_VERSION
|
||||
from esphome.components.esp32_ble_tracker import (
|
||||
@@ -120,3 +122,154 @@ def test_short_interval_without_window_still_rejected(
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
with pytest.raises(cv.Invalid, match="needs to be smaller than scan interval"):
|
||||
_scan_params({"scan_parameters": {"interval": "20ms"}})
|
||||
|
||||
|
||||
# The connection-time fallback window: while a GATT connection is active the
|
||||
# scanner drops from a raised full-duty window back to this value so the
|
||||
# connection gets guaranteed airtime.
|
||||
|
||||
|
||||
def test_raise_arms_connection_scan_window_default(
|
||||
stage_esp32: Callable[..., None],
|
||||
) -> None:
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
params = _scan_params({})
|
||||
assert params[CONF_WINDOW] == params[CONF_INTERVAL]
|
||||
assert to_ble_units(params[CONF_CONNECTION_SCAN_WINDOW]) == 48
|
||||
|
||||
|
||||
def test_user_connection_scan_window_survives_raise(
|
||||
stage_esp32: Callable[..., None],
|
||||
) -> None:
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
params = _scan_params({"scan_parameters": {"connection_scan_window": "60ms"}})
|
||||
assert params[CONF_WINDOW] == params[CONF_INTERVAL]
|
||||
assert to_ble_units(params[CONF_CONNECTION_SCAN_WINDOW]) == 96
|
||||
|
||||
|
||||
def test_unraised_window_gets_no_connection_scan_window_default(
|
||||
stage_esp32: Callable[..., None],
|
||||
) -> None:
|
||||
stage_esp32("5.5.4", wifi=True)
|
||||
assert CONF_CONNECTION_SCAN_WINDOW not in _scan_params({})
|
||||
|
||||
|
||||
def test_connection_scan_window_above_interval_rejected(
|
||||
stage_esp32: Callable[..., None],
|
||||
) -> None:
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="connection_scan_window .* needs to be smaller"
|
||||
):
|
||||
_scan_params({"scan_parameters": {"connection_scan_window": "400ms"}})
|
||||
|
||||
|
||||
def test_connection_scan_window_above_window_rejected(
|
||||
stage_esp32: Callable[..., None],
|
||||
) -> None:
|
||||
"""A connection window above the (post-raise) window would widen the scan
|
||||
during connections; the reject runs after the raise so a fallback below a
|
||||
raised window still validates (covered by the survives-raise test)."""
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
with pytest.raises(
|
||||
cv.Invalid, match="connection_scan_window .* needs to be smaller"
|
||||
):
|
||||
_scan_params(
|
||||
{"scan_parameters": {"window": "30ms", "connection_scan_window": "300ms"}}
|
||||
)
|
||||
|
||||
|
||||
def test_connection_scan_window_truncation_collapse_rejected(
|
||||
stage_esp32: Callable[..., None],
|
||||
) -> None:
|
||||
"""A connection window that truncates into the interval's 0.625 ms unit
|
||||
would silently program a full-duty scan during connections."""
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
with pytest.raises(cv.Invalid, match="connection_scan_window .* both truncate"):
|
||||
_scan_params(
|
||||
{
|
||||
"scan_parameters": {
|
||||
"interval": "320.5ms",
|
||||
"connection_scan_window": "320.2ms",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config_file", "window_call", "connection_call", "warns"),
|
||||
[
|
||||
# Raised window with GATT clients: the injected fallback is emitted.
|
||||
("scan_window_raised.yaml", "set_scan_window(512)", True, False),
|
||||
# Explicit window: nothing injected.
|
||||
("scan_window_explicit.yaml", "set_scan_window(48)", False, False),
|
||||
# Scan-only build compiles the path out: the injected default is
|
||||
# dropped silently, a user-set value warns.
|
||||
("scan_window_scan_only.yaml", "set_scan_window(512)", False, False),
|
||||
("scan_window_user_set_scan_only.yaml", "set_scan_window(512)", False, True),
|
||||
],
|
||||
)
|
||||
def test_connection_scan_window_codegen(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
config_file: str,
|
||||
window_call: str,
|
||||
connection_call: bool,
|
||||
warns: bool,
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path(config_file))
|
||||
assert window_call in main_cpp
|
||||
assert ("set_connection_scan_window(48)" in main_cpp) == connection_call
|
||||
assert ("'connection_scan_window' has no effect" in caplog.text) == warns
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("wifi", "params", "expect_warning"),
|
||||
[
|
||||
(True, {"interval": "1100ms", "window": "1100ms"}, True),
|
||||
(True, {"interval": "1100ms", "window": "601ms"}, True),
|
||||
(True, {"interval": "1100ms", "window": "600ms"}, False),
|
||||
(False, {"interval": "1100ms", "window": "1100ms"}, False),
|
||||
],
|
||||
)
|
||||
def test_long_window_with_wifi_warns(
|
||||
stage_esp32: Callable[..., None],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
wifi: bool,
|
||||
params: ConfigType,
|
||||
expect_warning: bool,
|
||||
) -> None:
|
||||
"""A scan window above 600 ms warns only when wifi shares the radio."""
|
||||
stage_esp32("5.5.5", wifi=wifi)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_scan_params({"scan_parameters": params})
|
||||
assert ("starves wifi" in caplog.text) is expect_warning
|
||||
|
||||
|
||||
def test_long_window_warns_with_coexistence_disabled(
|
||||
stage_esp32: Callable[..., None],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Disabling the arbiter is the worst case for a long window, so it still warns."""
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_scan_params(
|
||||
{
|
||||
CONF_SOFTWARE_COEXISTENCE: False,
|
||||
"scan_parameters": {"interval": "1100ms", "window": "1100ms"},
|
||||
}
|
||||
)
|
||||
assert "BLE scan window of 1100ms" in caplog.text
|
||||
|
||||
|
||||
def test_raised_window_warning_points_at_interval(
|
||||
stage_esp32: Callable[..., None],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""When the window was raised to a long interval, the warning names the interval."""
|
||||
stage_esp32("5.5.5", wifi=True)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_scan_params({"scan_parameters": {"interval": "1s"}})
|
||||
assert "BLE scan interval of 1s" in caplog.text
|
||||
assert "BLE scan window of" not in caplog.text
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
output:
|
||||
- platform: esp8266_pwm
|
||||
id: default_frequency
|
||||
pin: GPIO4
|
||||
frequency: 1kHz
|
||||
- platform: esp8266_pwm
|
||||
id: custom_frequency
|
||||
pin: GPIO5
|
||||
frequency: 2kHz
|
||||
- platform: esp8266_pwm
|
||||
id: schema_default_frequency
|
||||
pin: GPIO12
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Tests for the esp8266_pwm output codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_default_frequency_is_not_emitted(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""The 1 kHz default already lives in the C++ initializer."""
|
||||
main_cpp = generate_main(component_config_path("frequency.yaml"))
|
||||
|
||||
assert "default_frequency->set_frequency(" not in main_cpp
|
||||
assert "schema_default_frequency->set_frequency(" not in main_cpp
|
||||
assert "custom_frequency->set_frequency(2000.0f);" in main_cpp
|
||||
@@ -0,0 +1,21 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
interface: spi2
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
|
||||
ethernet:
|
||||
id: eth_component
|
||||
type: W5500
|
||||
spi_id: spi_bus
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 20MHz
|
||||
@@ -0,0 +1,16 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
ethernet:
|
||||
id: eth_component
|
||||
type: W5500
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
miso_pin: GPIO19
|
||||
cs_pin: GPIO5
|
||||
interrupt_pin: GPIO36
|
||||
reset_pin: GPIO22
|
||||
clock_speed: 20MHz
|
||||
@@ -27,14 +27,6 @@ _CH390_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_full_config():
|
||||
"""Reset fv.full_config so each test starts with a clean slate."""
|
||||
token = fv.full_config.set({})
|
||||
yield
|
||||
fv.full_config.reset(token)
|
||||
|
||||
|
||||
def test_rejects_wifi_and_ethernet_without_priority() -> None:
|
||||
"""Wi-Fi + ethernet without a network: priority: list must be rejected."""
|
||||
fv.full_config.set({"wifi": {}, "ethernet": {}})
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
"""Tests for the ethernet `spi_id:` option (attach to a shared spi bus)."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from voluptuous import Invalid
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.esp32 import (
|
||||
KEY_BOARD,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_VARIANT,
|
||||
VARIANT_ESP32S3,
|
||||
)
|
||||
from esphome.components.ethernet import CONF_INTERFACE, CONFIG_SCHEMA, _final_validate
|
||||
from esphome.components.rp2.const import KEY_BOARD as RP2_KEY_BOARD
|
||||
|
||||
# Registers the rp2 pin schema so RP2 configs can validate pins.
|
||||
import esphome.components.rp2.gpio # noqa: F401
|
||||
from esphome.components.spi import CONF_INTERFACE_INDEX
|
||||
from esphome.const import (
|
||||
CONF_CLK_PIN,
|
||||
CONF_ID,
|
||||
CONF_MISO_PIN,
|
||||
CONF_MOSI_PIN,
|
||||
CONF_SPI,
|
||||
CONF_SPI_ID,
|
||||
CONF_TYPE,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, ID
|
||||
import esphome.final_validate as fv
|
||||
|
||||
from ..types import SetCoreConfigCallable
|
||||
|
||||
_W5500_PIN_CONFIG = {
|
||||
"type": "W5500",
|
||||
"clk_pin": 47,
|
||||
"mosi_pin": 48,
|
||||
"miso_pin": 14,
|
||||
"cs_pin": 21,
|
||||
}
|
||||
|
||||
_W5500_SPI_ID_CONFIG = {
|
||||
"type": "W5500",
|
||||
"spi_id": "spi_bus",
|
||||
"cs_pin": 21,
|
||||
}
|
||||
|
||||
|
||||
def _set_esp32_s3(set_core_config: SetCoreConfigCallable) -> None:
|
||||
set_core_config(
|
||||
PlatformFramework.ESP32_IDF,
|
||||
platform_data={
|
||||
KEY_BOARD: "esp32-s3-devkitc-1",
|
||||
KEY_VARIANT: VARIANT_ESP32S3,
|
||||
KEY_IDF_VERSION: cv.Version(5, 3, 2),
|
||||
},
|
||||
)
|
||||
# _validate derives use_address from the node name, which has no default here.
|
||||
CORE.name = "spi-id-test"
|
||||
|
||||
|
||||
def test_spi_id_accepted_without_pins_or_interface(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""With spi_id set, the pin options are not required and no interface is defaulted."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
config = CONFIG_SCHEMA(dict(_W5500_SPI_ID_CONFIG))
|
||||
assert config[CONF_SPI_ID] == ID("spi_bus")
|
||||
# The interface comes from the referenced bus; no default may be injected.
|
||||
assert CONF_INTERFACE not in config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key", "value"),
|
||||
[
|
||||
(CONF_CLK_PIN, 47),
|
||||
(CONF_MOSI_PIN, 48),
|
||||
(CONF_MISO_PIN, 14),
|
||||
(CONF_INTERFACE, "spi2"),
|
||||
],
|
||||
)
|
||||
def test_spi_id_rejects_bus_options(
|
||||
set_core_config: SetCoreConfigCallable, key: str, value: int | str
|
||||
) -> None:
|
||||
"""Options provided by the referenced bus must be rejected alongside spi_id."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
with pytest.raises(Invalid, match=f"'{key}' cannot be used together with 'spi_id'"):
|
||||
CONFIG_SCHEMA({**_W5500_SPI_ID_CONFIG, key: value})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("key", [CONF_CLK_PIN, CONF_MOSI_PIN, CONF_MISO_PIN])
|
||||
def test_bus_pins_still_required_without_spi_id(
|
||||
set_core_config: SetCoreConfigCallable, key: str
|
||||
) -> None:
|
||||
"""Without spi_id, the bus pin options stay required."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
config = {k: v for k, v in _W5500_PIN_CONFIG.items() if k != key}
|
||||
with pytest.raises(
|
||||
Invalid, match=f"'{key}' is a required option when 'spi_id' is not set"
|
||||
):
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def test_spi_id_rejected_on_rp2(set_core_config: SetCoreConfigCallable) -> None:
|
||||
"""spi_id is ESP32-only; the RP2 path is unchanged."""
|
||||
set_core_config(
|
||||
PlatformFramework.RP2_ARDUINO, platform_data={RP2_KEY_BOARD: "rpipicow"}
|
||||
)
|
||||
CORE.name = "spi-id-test"
|
||||
config = {
|
||||
"type": "W5500",
|
||||
"spi_id": "spi_bus",
|
||||
"clk_pin": 18,
|
||||
"mosi_pin": 19,
|
||||
"miso_pin": 16,
|
||||
"cs_pin": 17,
|
||||
}
|
||||
with pytest.raises(Invalid, match="only available on"):
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def _eth_spi_id_final_config() -> dict:
|
||||
return {CONF_TYPE: "W5500", CONF_SPI_ID: ID("spi_bus")}
|
||||
|
||||
|
||||
class _FakeFinalConfig(dict):
|
||||
"""Dict-backed FinalValidateConfig with just enough ID resolution for
|
||||
fv.id_declaration_match_schema to find an spi bus fragment."""
|
||||
|
||||
def get_path_for_id(self, id: ID) -> list:
|
||||
for index, conf in enumerate(self[CONF_SPI]):
|
||||
if conf[CONF_ID] == id:
|
||||
return [CONF_SPI, index, CONF_ID]
|
||||
raise KeyError(id)
|
||||
|
||||
def get_config_for_path(self, path: list) -> dict:
|
||||
return self[path[0]][path[1]]
|
||||
|
||||
|
||||
def _set_spi_buses(*buses: dict) -> None:
|
||||
fv.full_config.set(_FakeFinalConfig({CONF_SPI: list(buses)}))
|
||||
|
||||
|
||||
_SHAREABLE_BUS = {
|
||||
CONF_ID: ID("spi_bus"),
|
||||
CONF_INTERFACE_INDEX: 0,
|
||||
CONF_MISO_PIN: {},
|
||||
CONF_MOSI_PIN: {},
|
||||
}
|
||||
|
||||
|
||||
def test_final_validate_accepts_hardware_bus_with_data_pins(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A hardware spi bus that declares miso_pin and mosi_pin may be shared."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
# An unrelated bus first: the ID lookup must skip past it.
|
||||
_set_spi_buses({CONF_ID: ID("other_bus"), CONF_INTERFACE_INDEX: 1}, _SHAREABLE_BUS)
|
||||
_final_validate(_eth_spi_id_final_config())
|
||||
|
||||
|
||||
def test_final_validate_rejects_software_bus(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""A software spi bus (no hardware interface index) cannot be shared."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
bus = {k: v for k, v in _SHAREABLE_BUS.items() if k != CONF_INTERFACE_INDEX}
|
||||
_set_spi_buses(bus)
|
||||
with pytest.raises(Invalid, match="requires this spi bus to use a hardware"):
|
||||
_final_validate(_eth_spi_id_final_config())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("pin_key", [CONF_MISO_PIN, CONF_MOSI_PIN])
|
||||
def test_final_validate_rejects_bus_without_data_pin(
|
||||
set_core_config: SetCoreConfigCallable, pin_key: str
|
||||
) -> None:
|
||||
"""The shared bus must declare both data pins to drive the ethernet chip."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
bus = {k: v for k, v in _SHAREABLE_BUS.items() if k != pin_key}
|
||||
_set_spi_buses(bus)
|
||||
with pytest.raises(Invalid, match=f"requires this spi bus to declare a {pin_key}"):
|
||||
_final_validate(_eth_spi_id_final_config())
|
||||
|
||||
|
||||
def test_final_validate_rejects_colliding_host_without_spi_id(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Without spi_id, claiming the same host as an spi bus stays an error."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
fv.full_config.set({CONF_SPI: [{CONF_ID: ID("spi_bus"), CONF_INTERFACE_INDEX: 0}]})
|
||||
config = {CONF_TYPE: "W5500", CONF_INTERFACE: "spi2"}
|
||||
with pytest.raises(Invalid, match="both using interface 'SPI2_HOST'"):
|
||||
_final_validate(config)
|
||||
|
||||
|
||||
def test_final_validate_accepts_distinct_host_without_spi_id(
|
||||
set_core_config: SetCoreConfigCallable,
|
||||
) -> None:
|
||||
"""Without spi_id, a different host than the spi bus is accepted."""
|
||||
_set_esp32_s3(set_core_config)
|
||||
fv.full_config.set({CONF_SPI: [{CONF_ID: ID("spi_bus"), CONF_INTERFACE_INDEX: 0}]})
|
||||
_final_validate({CONF_TYPE: "W5500", CONF_INTERFACE: "spi3"})
|
||||
|
||||
|
||||
def test_generated_code_uses_spi_parent(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""With spi_id, codegen wires the spi parent and skips the bus options."""
|
||||
main_cpp = generate_main(component_config_path("spi_id_shared_bus.yaml"))
|
||||
|
||||
assert "eth_component->set_spi_parent(spi_bus);" in main_cpp
|
||||
assert "eth_component->set_cs_pin(5);" in main_cpp
|
||||
assert "eth_component->set_clk_pin(" not in main_cpp
|
||||
assert "eth_component->set_miso_pin(" not in main_cpp
|
||||
assert "eth_component->set_mosi_pin(" not in main_cpp
|
||||
assert "eth_component->set_interface(" not in main_cpp
|
||||
|
||||
|
||||
def test_generated_code_without_spi_id_initializes_own_bus(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Without spi_id, codegen still emits the pin and interface setters."""
|
||||
main_cpp = generate_main(component_config_path("spi_own_bus.yaml"))
|
||||
|
||||
assert "eth_component->set_spi_parent(" not in main_cpp
|
||||
assert "eth_component->set_clk_pin(18);" in main_cpp
|
||||
assert "eth_component->set_miso_pin(19);" in main_cpp
|
||||
assert "eth_component->set_mosi_pin(23);" in main_cpp
|
||||
assert "eth_component->set_cs_pin(5);" in main_cpp
|
||||
assert "eth_component->set_interface(::SPI3_HOST);" in main_cpp
|
||||
@@ -1,16 +1,21 @@
|
||||
"""Tests for the external_components skip-update behavior driven by CORE.skip_external_update."""
|
||||
"""Tests for the external_components config pass."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.components.external_components import do_external_components_pass
|
||||
from esphome.const import (
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
CONF_PATH,
|
||||
CONF_REFRESH,
|
||||
CONF_SOURCE,
|
||||
CONF_URL,
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
)
|
||||
from esphome.core import CORE, TimePeriodSeconds
|
||||
|
||||
@@ -69,3 +74,112 @@ def test_external_components_normal_refresh(
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
|
||||
def test_external_components_logs_built_in_override(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A source that provides a component with the same name as a built-in one logs an info message."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
for name in ("gpio", "some_custom_component"):
|
||||
component_dir = tmp_path / "components" / name
|
||||
component_dir.mkdir()
|
||||
(component_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
do_external_components_pass(config)
|
||||
|
||||
assert (
|
||||
"External components are overriding built-in components:\n"
|
||||
" source: https://github.com/test/components\n"
|
||||
" components: gpio" in caplog.text
|
||||
)
|
||||
assert "some_custom_component" not in caplog.text
|
||||
|
||||
|
||||
def test_external_components_override_log_includes_ref(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A git source with a ref logs the ref appended to the url."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
config = _make_config(tmp_path)
|
||||
config[CONF_EXTERNAL_COMPONENTS][0][CONF_SOURCE] = "github://test/components@main"
|
||||
|
||||
component_dir = tmp_path / "components" / "gpio"
|
||||
component_dir.mkdir()
|
||||
(component_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
do_external_components_pass(config)
|
||||
|
||||
assert " source: https://github.com/test/components.git@main\n" in caplog.text
|
||||
|
||||
|
||||
def test_external_components_override_log_includes_git_path(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A git source with a subdirectory path logs the path after the url."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
config = _make_config(tmp_path)
|
||||
config[CONF_EXTERNAL_COMPONENTS][0][CONF_SOURCE][CONF_PATH] = "components"
|
||||
|
||||
component_dir = tmp_path / "components" / "gpio"
|
||||
component_dir.mkdir()
|
||||
(component_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
do_external_components_pass(config)
|
||||
|
||||
assert " source: https://github.com/test/components (components)\n" in caplog.text
|
||||
|
||||
|
||||
def test_external_components_override_log_local_source(
|
||||
tmp_path: Path,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A local source logs its resolved path."""
|
||||
components_dir = tmp_path / "my_components"
|
||||
gpio_dir = components_dir / "gpio"
|
||||
gpio_dir.mkdir(parents=True)
|
||||
(gpio_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
CORE.config_path = tmp_path / "dummy.yaml"
|
||||
config = {
|
||||
CONF_EXTERNAL_COMPONENTS: [
|
||||
{CONF_SOURCE: {"type": TYPE_LOCAL, CONF_PATH: "my_components"}}
|
||||
]
|
||||
}
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
do_external_components_pass(config)
|
||||
|
||||
assert f" source: {components_dir}\n" in caplog.text
|
||||
assert " components: gpio" in caplog.text
|
||||
|
||||
|
||||
def test_external_components_no_override_no_log(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""A source that only provides components not shipped with ESPHome logs nothing."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
with caplog.at_level(logging.INFO):
|
||||
do_external_components_pass(config)
|
||||
|
||||
assert "are overriding built-in components" not in caplog.text
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.core import CORE
|
||||
|
||||
INTERRUPT_DEFINE = "USE_GPIO_BINARY_SENSOR_INTERRUPT"
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_basic_setup(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
@@ -69,3 +74,62 @@ def test_gpio_binary_sensor_explicit_polling_mode(
|
||||
)
|
||||
|
||||
assert "bs_polling->set_use_interrupt(false);" in main_cpp
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_interrupt_emits_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
An interrupt-mode sensor must emit the define that compiles the ISR code,
|
||||
since the platform ISR pin implementation is only built when needed
|
||||
"""
|
||||
generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml")
|
||||
|
||||
assert INTERRUPT_DEFINE in {d.name for d in CORE.defines}
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_polling_omits_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
A polling-only config must not emit the interrupt define, so the ISR code
|
||||
(and its reference to ISRInternalGPIOPin) is compiled out
|
||||
"""
|
||||
generate_main("tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml")
|
||||
|
||||
assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines}
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_mixed_modes_emit_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
) -> None:
|
||||
"""
|
||||
With one interrupt and one polling sensor, the define is emitted and the
|
||||
polling instance still opts out via its setter
|
||||
"""
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_mixed.yaml"
|
||||
)
|
||||
|
||||
assert INTERRUPT_DEFINE in {d.name for d in CORE.defines}
|
||||
assert "bs_polling->set_use_interrupt(false);" in main_cpp
|
||||
assert "bs_interrupt->set_use_interrupt" not in main_cpp
|
||||
|
||||
|
||||
def test_gpio_binary_sensor_expander_pin_omits_define(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""
|
||||
An expander pin can't use interrupts: final validation falls back to
|
||||
polling and the interrupt define must not be emitted. This is the config
|
||||
that fails to link if the ISR code is compiled without an internal pin
|
||||
"""
|
||||
with caplog.at_level(logging.INFO):
|
||||
main_cpp = generate_main(
|
||||
"tests/component_tests/gpio/test_gpio_binary_sensor_expander.yaml"
|
||||
)
|
||||
|
||||
assert "bs_expander->set_use_interrupt(false);" in main_cpp
|
||||
assert INTERRUPT_DEFINE not in {d.name for d in CORE.defines}
|
||||
assert "falling back to polling mode" in caplog.text
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
i2c:
|
||||
scl: 16
|
||||
sda: 17
|
||||
|
||||
ch422g:
|
||||
- id: ch422g_hub
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
name: "Expander Sensor"
|
||||
id: bs_expander
|
||||
pin:
|
||||
ch422g: ch422g_hub
|
||||
number: 1
|
||||
mode: INPUT
|
||||
@@ -0,0 +1,17 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 5
|
||||
name: "Interrupt Sensor"
|
||||
id: bs_interrupt
|
||||
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
name: "Polling Sensor"
|
||||
id: bs_polling
|
||||
use_interrupt: false
|
||||
@@ -0,0 +1,12 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
timeout: 10s
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
timeout: 10s
|
||||
watchdog_timeout: 20s
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
watchdog_timeout: 60s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
timeout: 10s
|
||||
@@ -0,0 +1,11 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: nodemcu-32s
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp8266:
|
||||
board: d1_mini
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
timeout: 10s
|
||||
verify_ssl: false
|
||||
@@ -0,0 +1,13 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
rp2:
|
||||
board: rpipicow
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
timeout: 10s
|
||||
verify_ssl: false
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Tests for the http_request watchdog timeout default."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.config import read_config
|
||||
from esphome.const import CONF_WATCHDOG_TIMEOUT
|
||||
from esphome.core import CORE, TimePeriodMilliseconds
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("yaml_file", "expected_ms"),
|
||||
[
|
||||
# stock 4.5s timeout: 3 x 4.5s plus 1s margin
|
||||
("test_esp32_stock.yaml", 14500),
|
||||
# 3 x 10s plus 1s margin
|
||||
("test_esp32_default.yaml", 31000),
|
||||
# esp32.watchdog_timeout: 60s is wider than the derived value and wins
|
||||
("test_esp32_platform_wider.yaml", 60000),
|
||||
# explicit value is kept as is
|
||||
("test_esp32_explicit.yaml", 20000),
|
||||
],
|
||||
)
|
||||
def test_esp32_watchdog_timeout(
|
||||
component_config_path: Callable[[str], Path], yaml_file: str, expected_ms: int
|
||||
) -> None:
|
||||
CORE.config_path = component_config_path(yaml_file)
|
||||
config = read_config({})
|
||||
assert config["http_request"][CONF_WATCHDOG_TIMEOUT] == TimePeriodMilliseconds(
|
||||
milliseconds=expected_ms
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("yaml_file", ["test_esp8266.yaml", "test_rp2040.yaml"])
|
||||
def test_other_platforms_leave_watchdog_unset(
|
||||
component_config_path: Callable[[str], Path], yaml_file: str
|
||||
) -> None:
|
||||
CORE.config_path = component_config_path(yaml_file)
|
||||
config = read_config({})
|
||||
assert CONF_WATCHDOG_TIMEOUT not in config["http_request"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user