mirror of
https://github.com/esphome/esphome.git
synced 2026-01-13 05:27:53 -07:00
Compare commits
46 Commits
2025.12.0b
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6f3a5d8b7 | ||
|
|
3322b04e00 | ||
|
|
47d0d3cfeb | ||
|
|
8255c02d5d | ||
|
|
8b4ba8dfe6 | ||
|
|
178a61b6fd | ||
|
|
b5df4cdf1d | ||
|
|
d8c23d4fc9 | ||
|
|
e9e0712959 | ||
|
|
062840dd7b | ||
|
|
f0f01c081a | ||
|
|
dd855985be | ||
|
|
5b5cede5f9 | ||
|
|
c737033cc4 | ||
|
|
0194bfd9ea | ||
|
|
339399eb70 | ||
|
|
99f7e9aeb7 | ||
|
|
ebb6babb3d | ||
|
|
0922f240e0 | ||
|
|
c8fb694dcb | ||
|
|
6054685dae | ||
|
|
61ec3508ed | ||
|
|
086ec770ea | ||
|
|
b055f5b4bf | ||
|
|
726db746c8 | ||
|
|
1922455fa7 | ||
|
|
dc943d7e7a | ||
|
|
93e38f2608 | ||
|
|
3a888326d8 | ||
|
|
f0d0ea60a7 | ||
|
|
7ca11764ab | ||
|
|
3e38a5e630 | ||
|
|
636be92c97 | ||
|
|
195b1c6323 | ||
|
|
7e08092012 | ||
|
|
0ea5f2fd81 | ||
|
|
fa3d998c3d | ||
|
|
864aaeec01 | ||
|
|
9c88e44300 | ||
|
|
4d6a93f92d | ||
|
|
7216120bfd | ||
|
|
8cf0ee38a3 | ||
|
|
4c926cca60 | ||
|
|
57634b612a | ||
|
|
8dff7ee746 | ||
|
|
803bb742c9 |
@@ -1 +1 @@
|
||||
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07
|
||||
5969e705693278d984c5292e998df0cbaf34f7e1f04dfc7f7b7ad7168527bfa7
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.12.0b3
|
||||
PROJECT_NUMBER = 2025.12.5
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*"
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
RUN if command -v apk > /dev/null; then \
|
||||
apk add --no-cache build-base; \
|
||||
else \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
|
||||
|
||||
@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ADE7880),
|
||||
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
|
||||
cv.frequency, cv.Range(min=45.0, max=66.0)
|
||||
cv.frequency, cv.float_range(min=45.0, max=66.0)
|
||||
),
|
||||
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,
|
||||
|
||||
@@ -11,6 +11,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
|
||||
|
||||
AUTO_LOAD = ["bme68x_bsec2"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
|
||||
bme68x_bsec2_i2c_ns = cg.esphome_ns.namespace("bme68x_bsec2_i2c")
|
||||
BME68xBSEC2I2CComponent = bme68x_bsec2_i2c_ns.class_(
|
||||
|
||||
@@ -160,41 +160,63 @@ HYST_LEVEL = {
|
||||
"High": HystLevel.HYST_LEVEL_HIGH,
|
||||
}
|
||||
|
||||
# Config key -> Validator mapping
|
||||
# Optional settings to generate setter calls for
|
||||
CONFIG_MAP = {
|
||||
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
|
||||
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
|
||||
CONF_DC_BLOCKING_FILTER: cv.boolean,
|
||||
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)),
|
||||
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
|
||||
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
|
||||
CONF_CHANNEL: cv.uint8_t,
|
||||
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
|
||||
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
|
||||
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
|
||||
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
|
||||
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
|
||||
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
|
||||
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
|
||||
CONF_MANCHESTER: cv.boolean,
|
||||
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
|
||||
CONF_SYNC1: cv.hex_uint8_t,
|
||||
CONF_SYNC0: cv.hex_uint8_t,
|
||||
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
|
||||
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
|
||||
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
|
||||
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
|
||||
CONF_LNA_PRIORITY: cv.boolean,
|
||||
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
|
||||
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
|
||||
CONF_FREEZE: cv.enum(FREEZE, upper=False),
|
||||
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
|
||||
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
|
||||
CONF_PACKET_MODE: cv.boolean,
|
||||
CONF_PACKET_LENGTH: cv.uint8_t,
|
||||
CONF_CRC_ENABLE: cv.boolean,
|
||||
CONF_WHITENING: cv.boolean,
|
||||
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
|
||||
cv.Optional(CONF_RX_ATTENUATION, default="0dB"): cv.enum(
|
||||
RX_ATTENUATION, upper=False
|
||||
),
|
||||
cv.Optional(CONF_DC_BLOCKING_FILTER, default=True): cv.boolean,
|
||||
cv.Optional(CONF_FREQUENCY, default="433.92MHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=300.0e6, max=928.0e6)
|
||||
),
|
||||
cv.Optional(CONF_IF_FREQUENCY, default="153kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=25000, max=788000)
|
||||
),
|
||||
cv.Optional(CONF_FILTER_BANDWIDTH, default="203kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=58000, max=812000)
|
||||
),
|
||||
cv.Optional(CONF_CHANNEL, default=0): cv.uint8_t,
|
||||
cv.Optional(CONF_CHANNEL_SPACING, default="200kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=25000, max=405000)
|
||||
),
|
||||
cv.Optional(CONF_FSK_DEVIATION): cv.All(
|
||||
cv.frequency, cv.float_range(min=1500, max=381000)
|
||||
),
|
||||
cv.Optional(CONF_MSK_DEVIATION): cv.int_range(min=1, max=8),
|
||||
cv.Optional(CONF_SYMBOL_RATE, default=5000): cv.float_range(min=600, max=500000),
|
||||
cv.Optional(CONF_SYNC_MODE, default="16/16"): cv.enum(SYNC_MODE, upper=False),
|
||||
cv.Optional(CONF_CARRIER_SENSE_ABOVE_THRESHOLD, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MODULATION_TYPE, default="ASK/OOK"): cv.enum(
|
||||
MODULATION, upper=False
|
||||
),
|
||||
cv.Optional(CONF_MANCHESTER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_NUM_PREAMBLE, default=2): cv.int_range(min=0, max=7),
|
||||
cv.Optional(CONF_SYNC1, default=0xD3): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_SYNC0, default=0x91): cv.hex_uint8_t,
|
||||
cv.Optional(CONF_MAGN_TARGET, default="42dB"): cv.enum(MAGN_TARGET, upper=False),
|
||||
cv.Optional(CONF_MAX_LNA_GAIN, default="Default"): cv.enum(
|
||||
MAX_LNA_GAIN, upper=False
|
||||
),
|
||||
cv.Optional(CONF_MAX_DVGA_GAIN, default="-3"): cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
cv.Optional(CONF_CARRIER_SENSE_ABS_THR): cv.int_range(min=-8, max=7),
|
||||
cv.Optional(CONF_CARRIER_SENSE_REL_THR): cv.enum(
|
||||
CARRIER_SENSE_REL_THR, upper=False
|
||||
),
|
||||
cv.Optional(CONF_LNA_PRIORITY, default=False): cv.boolean,
|
||||
cv.Optional(CONF_FILTER_LENGTH_FSK_MSK): cv.enum(
|
||||
FILTER_LENGTH_FSK_MSK, upper=False
|
||||
),
|
||||
cv.Optional(CONF_FILTER_LENGTH_ASK_OOK): cv.enum(
|
||||
FILTER_LENGTH_ASK_OOK, upper=False
|
||||
),
|
||||
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
|
||||
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
|
||||
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
|
||||
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_WHITENING, default=False): cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +239,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
|
||||
.extend(CONFIG_MAP)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True)),
|
||||
_validate_packet_mode,
|
||||
@@ -229,7 +251,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
for key in CONFIG_MAP:
|
||||
for opt in CONFIG_MAP:
|
||||
key = opt.schema
|
||||
if key in config:
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
|
||||
@@ -98,25 +98,8 @@ CC1101Component::CC1101Component() {
|
||||
this->state_.LENGTH_CONFIG = 2;
|
||||
this->state_.FS_AUTOCAL = 1;
|
||||
|
||||
// Default Settings
|
||||
this->set_frequency(433920);
|
||||
this->set_if_frequency(153);
|
||||
this->set_filter_bandwidth(203);
|
||||
this->set_channel(0);
|
||||
this->set_channel_spacing(200);
|
||||
this->set_symbol_rate(5000);
|
||||
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
|
||||
this->set_carrier_sense_above_threshold(true);
|
||||
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
|
||||
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
|
||||
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
|
||||
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
|
||||
this->set_lna_priority(false);
|
||||
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
|
||||
|
||||
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
this->set_output_power(10.0f);
|
||||
}
|
||||
|
||||
void CC1101Component::setup() {
|
||||
@@ -157,7 +140,10 @@ void CC1101Component::setup() {
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->strobe_(Command::RX);
|
||||
if (!this->enter_rx_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
@@ -180,36 +166,35 @@ void CC1101Component::loop() {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, flushing");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read packet
|
||||
uint8_t payload_length;
|
||||
uint8_t payload_length, expected_rx;
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->read_(Register::FIFO, &payload_length, 1);
|
||||
expected_rx = payload_length + 1;
|
||||
} else {
|
||||
payload_length = this->state_.PKTLEN;
|
||||
expected_rx = payload_length;
|
||||
}
|
||||
if (payload_length == 0 || payload_length > 64) {
|
||||
ESP_LOGW(TAG, "Invalid payload length: %u", payload_length);
|
||||
if (payload_length == 0 || payload_length > 64 || rx_bytes != expected_rx) {
|
||||
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return;
|
||||
}
|
||||
this->packet_.resize(payload_length);
|
||||
this->read_(Register::FIFO, this->packet_.data(), payload_length);
|
||||
|
||||
// Read status and trigger
|
||||
uint8_t status[2];
|
||||
this->read_(Register::FIFO, status, 2);
|
||||
int8_t rssi_raw = static_cast<int8_t>(status[0]);
|
||||
float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = status[1] & STATUS_LQI_MASK;
|
||||
// Read status from registers (more reliable than FIFO status bytes due to timing issues)
|
||||
this->read_(Register::RSSI);
|
||||
this->read_(Register::LQI);
|
||||
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
|
||||
}
|
||||
@@ -217,8 +202,7 @@ void CC1101Component::loop() {
|
||||
// Return to rx
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
|
||||
void CC1101Component::dump_config() {
|
||||
@@ -249,9 +233,8 @@ void CC1101Component::begin_tx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::TX, 50)) {
|
||||
ESP_LOGW(TAG, "Timed out waiting for TX state!");
|
||||
if (!this->enter_tx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter TX state!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +243,9 @@ void CC1101Component::begin_rx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
this->strobe_(Command::RX);
|
||||
if (!this->enter_rx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter RX state!");
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::reset() {
|
||||
@@ -286,11 +271,33 @@ bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_calibrated_(State target_state, Command cmd) {
|
||||
// The PLL must be recalibrated until PLL lock is achieved
|
||||
for (uint8_t retries = PLL_LOCK_RETRIES; retries > 0; retries--) {
|
||||
this->strobe_(cmd);
|
||||
if (!this->wait_for_state_(target_state)) {
|
||||
return false;
|
||||
}
|
||||
this->read_(Register::FSCAL1);
|
||||
if (this->state_.FSCAL1 != FSCAL1_PLL_NOT_LOCKED) {
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "PLL lock failed, retrying calibration");
|
||||
this->enter_idle_();
|
||||
}
|
||||
ESP_LOGE(TAG, "PLL lock failed after retries");
|
||||
return false;
|
||||
}
|
||||
|
||||
void CC1101Component::enter_idle_() {
|
||||
this->strobe_(Command::IDLE);
|
||||
this->wait_for_state_(State::IDLE);
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); }
|
||||
|
||||
bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); }
|
||||
|
||||
uint8_t CC1101Component::strobe_(Command cmd) {
|
||||
uint8_t index = static_cast<uint8_t>(cmd);
|
||||
if (cmd < Command::RES || cmd > Command::NOP) {
|
||||
@@ -352,18 +359,26 @@ CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet)
|
||||
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
|
||||
}
|
||||
this->write_(Register::FIFO, packet.data(), packet.size());
|
||||
|
||||
// Calibrate PLL
|
||||
if (!this->enter_calibrated_(State::FSTXON, Command::FSTXON)) {
|
||||
ESP_LOGW(TAG, "PLL lock failed during TX");
|
||||
this->enter_idle_();
|
||||
this->enter_rx_();
|
||||
return CC1101Error::PLL_LOCK;
|
||||
}
|
||||
|
||||
// Transmit packet
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::IDLE, 1000)) {
|
||||
ESP_LOGW(TAG, "TX timeout");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return CC1101Error::TIMEOUT;
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return CC1101Error::NONE;
|
||||
}
|
||||
|
||||
@@ -420,7 +435,7 @@ void CC1101Component::set_frequency(float value) {
|
||||
this->write_(Register::FREQ2);
|
||||
this->write_(Register::FREQ1);
|
||||
this->write_(Register::FREQ0);
|
||||
this->strobe_(Command::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +462,7 @@ void CC1101Component::set_channel(uint8_t value) {
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->write_(Register::CHANNR);
|
||||
this->strobe_(Command::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +531,7 @@ void CC1101Component::set_modulation_type(Modulation value) {
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->write_(Register::MDMCFG2);
|
||||
this->write_(Register::FREND0);
|
||||
this->strobe_(Command::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,12 +648,15 @@ void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
|
||||
this->state_.FIFO_THR = 15;
|
||||
// Don't append status bytes to FIFO - we read from registers instead
|
||||
this->state_.APPEND_STATUS = 0;
|
||||
} else {
|
||||
// Configure GDO0 for serial data (async serial mode)
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTCTRL1);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW };
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
||||
|
||||
class CC1101Component : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
@@ -102,7 +102,10 @@ class CC1101Component : public Component,
|
||||
|
||||
// State Management
|
||||
bool wait_for_state_(State target_state, uint32_t timeout_ms = 100);
|
||||
bool enter_calibrated_(State target_state, Command cmd);
|
||||
void enter_idle_();
|
||||
bool enter_rx_();
|
||||
bool enter_tx_();
|
||||
};
|
||||
|
||||
// Action Wrappers
|
||||
|
||||
@@ -9,6 +9,9 @@ static constexpr float XTAL_FREQUENCY = 26000000;
|
||||
static constexpr float RSSI_OFFSET = 74.0f;
|
||||
static constexpr float RSSI_STEP = 0.5f;
|
||||
|
||||
static constexpr uint8_t FSCAL1_PLL_NOT_LOCKED = 0x3F;
|
||||
static constexpr uint8_t PLL_LOCK_RETRIES = 3;
|
||||
|
||||
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
|
||||
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ bool MenuItemSelect::select_next() {
|
||||
|
||||
if (this->select_var_ != nullptr) {
|
||||
this->select_var_->make_call().select_next(true).perform();
|
||||
this->on_value_();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
@@ -65,6 +66,7 @@ bool MenuItemSelect::select_prev() {
|
||||
|
||||
if (this->select_var_ != nullptr) {
|
||||
this->select_var_->make_call().select_previous(true).perform();
|
||||
this->on_value_();
|
||||
changed = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import itertools
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from esphome import yaml_util
|
||||
import esphome.codegen as cg
|
||||
@@ -616,10 +617,13 @@ def require_vfs_dir() -> None:
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
if "^" not in value:
|
||||
raise cv.Invalid(f"Invalid IDF component shorthand '{value}'")
|
||||
name, ref = value.split("^", 1)
|
||||
return {CONF_NAME: name, CONF_REF: ref}
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
if match := re.search(r"(~=|>=|<=|==|!=|>|<|\^|~)(\d|\*)", value):
|
||||
return {CONF_NAME: value[: match.start()], CONF_REF: value[match.start() :]}
|
||||
raise cv.Invalid(
|
||||
f"Invalid IDF component shorthand '{value}'. "
|
||||
f"Expected format: 'owner/component<op>version' where <op> is one of: ^, ~, ~=, ==, !=, >=, >, <=, <"
|
||||
)
|
||||
|
||||
|
||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -22,7 +22,6 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||
DOMAIN = "esp32_ble"
|
||||
|
||||
|
||||
@@ -308,13 +308,21 @@ bool ESP32BLE::ble_setup_() {
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
return false;
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
|
||||
if (err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Already disabled");
|
||||
}
|
||||
err = esp_bluedroid_deinit();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
|
||||
return false;
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already deinitialized, which is fine
|
||||
if (err != ESP_ERR_INVALID_STATE) {
|
||||
ESP_LOGE(TAG, "esp_bluedroid_deinit failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Already deinitialized");
|
||||
}
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
|
||||
@@ -212,17 +212,23 @@ extern ESP32BLE *global_ble;
|
||||
|
||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_ble->is_active(); }
|
||||
bool check(const Ts &...x) override { return global_ble != nullptr && global_ble->is_active(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_ble->enable(); }
|
||||
void play(const Ts &...x) override {
|
||||
if (global_ble != nullptr)
|
||||
global_ble->enable();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_ble->disable(); }
|
||||
void play(const Ts &...x) override {
|
||||
if (global_ble != nullptr)
|
||||
global_ble->disable();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
@@ -185,7 +185,10 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
|
||||
|
||||
void ESP32BLETracker::stop_scan_() {
|
||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
// If scanner is already idle, there's nothing to stop - this is not an error
|
||||
if (this->scanner_state_ != ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Reset timeout state machine when stopping scan
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
|
||||
from esphome.components.psram import DOMAIN as psram_domain
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -186,7 +186,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number,
|
||||
cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All(
|
||||
cv.frequency, cv.Range(min=8e6, max=20e6)
|
||||
cv.frequency, cv.float_range(min=8e6, max=20e6)
|
||||
),
|
||||
}
|
||||
),
|
||||
@@ -352,6 +352,8 @@ async def to_code(config):
|
||||
cg.add_define("USE_CAMERA")
|
||||
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
|
||||
|
||||
for conf in config.get(CONF_ON_STREAM_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace esphome {
|
||||
namespace esp32_camera {
|
||||
|
||||
static const char *const TAG = "esp32_camera";
|
||||
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
|
||||
static constexpr uint32_t FRAME_LOG_INTERVAL_MS = 60000;
|
||||
#endif
|
||||
|
||||
/* ---------------- public API (derivated) ---------------- */
|
||||
void ESP32Camera::setup() {
|
||||
@@ -204,7 +207,20 @@ void ESP32Camera::loop() {
|
||||
}
|
||||
this->current_image_ = std::make_shared<ESP32CameraImage>(fb, this->single_requesters_ | this->stream_requesters_);
|
||||
|
||||
ESP_LOGD(TAG, "Got Image: len=%u", fb->len);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, "Got Image: len=%u", fb->len);
|
||||
#else
|
||||
// Initialize log time on first frame to ensure accurate interval measurement
|
||||
if (this->frame_count_ == 0) {
|
||||
this->last_log_time_ = now;
|
||||
}
|
||||
this->frame_count_++;
|
||||
if (now - this->last_log_time_ >= FRAME_LOG_INTERVAL_MS) {
|
||||
ESP_LOGD(TAG, "Received %u images in last %us", this->frame_count_, FRAME_LOG_INTERVAL_MS / 1000);
|
||||
this->last_log_time_ = now;
|
||||
this->frame_count_ = 0;
|
||||
}
|
||||
#endif
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->on_camera_image(this->current_image_);
|
||||
}
|
||||
|
||||
@@ -213,6 +213,10 @@ class ESP32Camera : public camera::Camera {
|
||||
|
||||
uint32_t last_idle_request_{0};
|
||||
uint32_t last_update_{0};
|
||||
#if ESPHOME_LOG_LEVEL < ESPHOME_LOG_LEVEL_VERBOSE
|
||||
uint32_t last_log_time_{0};
|
||||
uint16_t frame_count_{0};
|
||||
#endif
|
||||
#ifdef USE_I2C
|
||||
i2c::InternalI2CBus *i2c_bus_{nullptr};
|
||||
#endif // USE_I2C
|
||||
|
||||
@@ -16,7 +16,7 @@ def valid_pwm_pin(value):
|
||||
esp8266_pwm_ns = cg.esphome_ns.namespace("esp8266_pwm")
|
||||
ESP8266PWM = esp8266_pwm_ns.class_("ESP8266PWM", output.FloatOutput, cg.Component)
|
||||
SetFrequencyAction = esp8266_pwm_ns.class_("SetFrequencyAction", automation.Action)
|
||||
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6))
|
||||
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
|
||||
@@ -255,6 +255,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
||||
size_t read_index = 0;
|
||||
while (container->get_bytes_read() < max_length) {
|
||||
int read = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
|
||||
if (read <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
read_index += read;
|
||||
|
||||
@@ -132,11 +132,18 @@ uint8_t OtaHttpRequestComponent::do_ota_() {
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
} else if (bufsize > 0 && bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// Exit loop if no data available (stream closed or end of data)
|
||||
if (bufsize <= 0) {
|
||||
if (bufsize < 0) {
|
||||
ESP_LOGE(TAG, "Stream closed with error");
|
||||
this->cleanup_(std::move(backend), container);
|
||||
return OTA_CONNECTION_ERROR;
|
||||
}
|
||||
// bufsize == 0: no more data available, exit loop
|
||||
break;
|
||||
}
|
||||
|
||||
if (bufsize <= OtaHttpRequestComponent::HTTP_RECV_BUFFER) {
|
||||
// add read bytes to MD5
|
||||
md5_receive.add(buf, bufsize);
|
||||
|
||||
@@ -247,6 +254,9 @@ bool OtaHttpRequestComponent::http_get_md5_() {
|
||||
int read_len = 0;
|
||||
while (container->get_bytes_read() < MD5_SIZE) {
|
||||
read_len = container->read((uint8_t *) this->md5_expected_.data(), MD5_SIZE);
|
||||
if (read_len <= 0) {
|
||||
break;
|
||||
}
|
||||
App.feed_wdt();
|
||||
yield();
|
||||
}
|
||||
|
||||
@@ -76,6 +76,11 @@ void HttpRequestUpdate::update_task(void *params) {
|
||||
|
||||
yield();
|
||||
|
||||
if (read_bytes <= 0) {
|
||||
// Network error or connection closed - break to avoid infinite loop
|
||||
break;
|
||||
}
|
||||
|
||||
read_index += read_bytes;
|
||||
}
|
||||
|
||||
|
||||
@@ -93,35 +93,35 @@ CONF_DOUBLE_BUFFER = "double_buffer"
|
||||
CONF_MIN_REFRESH_RATE = "min_refresh_rate"
|
||||
|
||||
# Map to hub75 library enums (in global namespace)
|
||||
ShiftDriver = cg.global_ns.enum("ShiftDriver", is_class=True)
|
||||
Hub75ShiftDriver = cg.global_ns.enum("Hub75ShiftDriver", is_class=True)
|
||||
SHIFT_DRIVERS = {
|
||||
"GENERIC": ShiftDriver.GENERIC,
|
||||
"FM6126A": ShiftDriver.FM6126A,
|
||||
"ICN2038S": ShiftDriver.ICN2038S,
|
||||
"FM6124": ShiftDriver.FM6124,
|
||||
"MBI5124": ShiftDriver.MBI5124,
|
||||
"DP3246": ShiftDriver.DP3246,
|
||||
"GENERIC": Hub75ShiftDriver.GENERIC,
|
||||
"FM6126A": Hub75ShiftDriver.FM6126A,
|
||||
"ICN2038S": Hub75ShiftDriver.ICN2038S,
|
||||
"FM6124": Hub75ShiftDriver.FM6124,
|
||||
"MBI5124": Hub75ShiftDriver.MBI5124,
|
||||
"DP3246": Hub75ShiftDriver.DP3246,
|
||||
}
|
||||
|
||||
PanelLayout = cg.global_ns.enum("PanelLayout", is_class=True)
|
||||
Hub75PanelLayout = cg.global_ns.enum("Hub75PanelLayout", is_class=True)
|
||||
PANEL_LAYOUTS = {
|
||||
"HORIZONTAL": PanelLayout.HORIZONTAL,
|
||||
"TOP_LEFT_DOWN": PanelLayout.TOP_LEFT_DOWN,
|
||||
"TOP_RIGHT_DOWN": PanelLayout.TOP_RIGHT_DOWN,
|
||||
"BOTTOM_LEFT_UP": PanelLayout.BOTTOM_LEFT_UP,
|
||||
"BOTTOM_RIGHT_UP": PanelLayout.BOTTOM_RIGHT_UP,
|
||||
"TOP_LEFT_DOWN_ZIGZAG": PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
|
||||
"TOP_RIGHT_DOWN_ZIGZAG": PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
|
||||
"BOTTOM_LEFT_UP_ZIGZAG": PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
|
||||
"BOTTOM_RIGHT_UP_ZIGZAG": PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
|
||||
"HORIZONTAL": Hub75PanelLayout.HORIZONTAL,
|
||||
"TOP_LEFT_DOWN": Hub75PanelLayout.TOP_LEFT_DOWN,
|
||||
"TOP_RIGHT_DOWN": Hub75PanelLayout.TOP_RIGHT_DOWN,
|
||||
"BOTTOM_LEFT_UP": Hub75PanelLayout.BOTTOM_LEFT_UP,
|
||||
"BOTTOM_RIGHT_UP": Hub75PanelLayout.BOTTOM_RIGHT_UP,
|
||||
"TOP_LEFT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_LEFT_DOWN_ZIGZAG,
|
||||
"TOP_RIGHT_DOWN_ZIGZAG": Hub75PanelLayout.TOP_RIGHT_DOWN_ZIGZAG,
|
||||
"BOTTOM_LEFT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_LEFT_UP_ZIGZAG,
|
||||
"BOTTOM_RIGHT_UP_ZIGZAG": Hub75PanelLayout.BOTTOM_RIGHT_UP_ZIGZAG,
|
||||
}
|
||||
|
||||
ScanPattern = cg.global_ns.enum("ScanPattern", is_class=True)
|
||||
Hub75ScanWiring = cg.global_ns.enum("Hub75ScanWiring", is_class=True)
|
||||
SCAN_PATTERNS = {
|
||||
"STANDARD_TWO_SCAN": ScanPattern.STANDARD_TWO_SCAN,
|
||||
"FOUR_SCAN_16PX_HIGH": ScanPattern.FOUR_SCAN_16PX_HIGH,
|
||||
"FOUR_SCAN_32PX_HIGH": ScanPattern.FOUR_SCAN_32PX_HIGH,
|
||||
"FOUR_SCAN_64PX_HIGH": ScanPattern.FOUR_SCAN_64PX_HIGH,
|
||||
"STANDARD_TWO_SCAN": Hub75ScanWiring.STANDARD_TWO_SCAN,
|
||||
"FOUR_SCAN_16PX_HIGH": Hub75ScanWiring.FOUR_SCAN_16PX_HIGH,
|
||||
"FOUR_SCAN_32PX_HIGH": Hub75ScanWiring.FOUR_SCAN_32PX_HIGH,
|
||||
"FOUR_SCAN_64PX_HIGH": Hub75ScanWiring.FOUR_SCAN_64PX_HIGH,
|
||||
}
|
||||
|
||||
Hub75ClockSpeed = cg.global_ns.enum("Hub75ClockSpeed", is_class=True)
|
||||
@@ -528,7 +528,7 @@ def _build_config_struct(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
add_idf_component(
|
||||
name="esphome/esp-hub75",
|
||||
ref="0.1.6",
|
||||
ref="0.1.7",
|
||||
)
|
||||
|
||||
# Set compile-time configuration via defines
|
||||
|
||||
@@ -111,6 +111,9 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
driver_->set_pixel(x, y, color.r, color.g, color.b);
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
nrf52="100kHz",
|
||||
): cv.All(
|
||||
cv.frequency,
|
||||
cv.Range(min=0, min_included=False),
|
||||
cv.float_range(min=0, min_included=False),
|
||||
),
|
||||
cv.Optional(CONF_TIMEOUT): cv.All(
|
||||
cv.only_with_framework(["arduino", "esp-idf"]),
|
||||
|
||||
@@ -45,7 +45,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(LEDCOutput),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency,
|
||||
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=0, min_included=False)
|
||||
),
|
||||
cv.Optional(CONF_CHANNEL): cv.int_range(min=0, max=15),
|
||||
cv.Optional(CONF_PHASE_ANGLE): cv.All(
|
||||
cv.only_with_esp_idf, cv.angle, cv.float_range(min=0.0, max=360.0)
|
||||
|
||||
@@ -14,7 +14,9 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(LibreTinyPWM),
|
||||
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.frequency,
|
||||
cv.Optional(CONF_FREQUENCY, default="1kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=0, min_included=False)
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from esphome import codegen as cg, config_validation as cv
|
||||
from esphome.const import CONF_ITEMS
|
||||
@@ -96,13 +96,9 @@ class LValidator:
|
||||
return None
|
||||
if isinstance(value, Lambda):
|
||||
# Local import to avoid circular import
|
||||
from .lvcode import CodeContext, LambdaContext
|
||||
from .lvcode import get_lambda_context_args
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# CodeContext does not have get_automation_parameters
|
||||
# so we need to assert the type here
|
||||
assert isinstance(CodeContext.code_context, LambdaContext)
|
||||
args = args or CodeContext.code_context.get_automation_parameters()
|
||||
args = args or get_lambda_context_args()
|
||||
return cg.RawExpression(
|
||||
call_lambda(
|
||||
await cg.process_lambda(value, args, return_type=self.rtype)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import image
|
||||
@@ -404,14 +404,9 @@ class TextValidator(LValidator):
|
||||
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
|
||||
) -> Expression:
|
||||
# Local import to avoid circular import at module level
|
||||
from .lvcode import get_lambda_context_args
|
||||
|
||||
from .lvcode import CodeContext, LambdaContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# CodeContext does not have get_automation_parameters
|
||||
# so we need to assert the type here
|
||||
assert isinstance(CodeContext.code_context, LambdaContext)
|
||||
args = args or CodeContext.code_context.get_automation_parameters()
|
||||
args = args or get_lambda_context_args()
|
||||
|
||||
if isinstance(value, dict):
|
||||
if format_str := value.get(CONF_FORMAT):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import abc
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome import codegen as cg
|
||||
from esphome.config import Config
|
||||
@@ -200,6 +201,21 @@ class LvContext(LambdaContext):
|
||||
return self.add(*args)
|
||||
|
||||
|
||||
def get_lambda_context_args() -> list[tuple[SafeExpType, str]]:
|
||||
"""Get automation parameters from the current lambda context if available.
|
||||
|
||||
When called from outside LVGL's context (e.g., from interval),
|
||||
CodeContext.code_context will be None, so return empty args.
|
||||
"""
|
||||
if CodeContext.code_context is None:
|
||||
return []
|
||||
if TYPE_CHECKING:
|
||||
# CodeContext base class doesn't define get_automation_parameters(),
|
||||
# but LambdaContext and LvContext (the concrete implementations) do.
|
||||
assert isinstance(CodeContext.code_context, LambdaContext)
|
||||
return CodeContext.code_context.get_automation_parameters()
|
||||
|
||||
|
||||
class LocalVariable(MockObj):
|
||||
"""
|
||||
Create a local variable and enclose the code using it within a block.
|
||||
|
||||
@@ -85,11 +85,11 @@ class ArcType(NumberType):
|
||||
lv.arc_set_range(w.obj, min_value, max_value)
|
||||
|
||||
await w.set_property(
|
||||
CONF_START_ANGLE,
|
||||
"bg_start_angle",
|
||||
await lv_angle_degrees.process(config.get(CONF_START_ANGLE)),
|
||||
)
|
||||
await w.set_property(
|
||||
CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
|
||||
"bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
|
||||
)
|
||||
await w.set_property(
|
||||
CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION))
|
||||
|
||||
@@ -38,7 +38,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(PCA9685Output),
|
||||
cv.Optional(CONF_FREQUENCY): cv.All(
|
||||
cv.frequency, cv.Range(min=23.84, max=1525.88)
|
||||
cv.frequency, cv.float_range(min=23.84, max=1525.88)
|
||||
),
|
||||
cv.Optional(CONF_EXTERNAL_CLOCK_INPUT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PHASE_BALANCER, default="linear"): cv.enum(
|
||||
|
||||
@@ -7,10 +7,10 @@ from esphome.const import (
|
||||
CONF_UPDATE_INTERVAL,
|
||||
DEVICE_CLASS_PM25,
|
||||
ICON_BLUR,
|
||||
SCHEDULER_DONT_RUN,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from esphome.core import TimePeriodMilliseconds
|
||||
|
||||
CODEOWNERS = ["@habbie"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -41,16 +41,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
def validate_interval_uart(config):
|
||||
require_tx = False
|
||||
|
||||
interval = config.get(CONF_UPDATE_INTERVAL)
|
||||
|
||||
if isinstance(interval, TimePeriodMilliseconds):
|
||||
# 'never' is encoded as a very large int, not as a TimePeriodMilliseconds objects
|
||||
require_tx = True
|
||||
|
||||
uart.final_validate_device_schema(
|
||||
"pm1006", baud_rate=9600, require_rx=True, require_tx=require_tx
|
||||
"pm1006",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=interval.total_milliseconds != SCHEDULER_DONT_RUN,
|
||||
)(config)
|
||||
|
||||
|
||||
|
||||
@@ -232,10 +232,10 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
|
||||
data.set_message_id(this->message_id_.value(x...));
|
||||
data.auto_message_id = this->auto_message_id_.value(x...);
|
||||
std::vector<uint8_t> data_vec;
|
||||
if (this->len_ >= 0) {
|
||||
if (this->len_ > 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
data_vec.assign(this->data_.data, this->data_.data + this->len_);
|
||||
} else {
|
||||
} else if (this->len_ < 0) {
|
||||
// Template mode: call function
|
||||
data_vec = this->data_.func(x...);
|
||||
}
|
||||
@@ -245,7 +245,7 @@ template<typename... Ts> class ABBWelcomeAction : public RemoteTransmitterAction
|
||||
}
|
||||
|
||||
protected:
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
ssize_t len_{0}; // <0 = template mode, >=0 = static mode with length
|
||||
union Data {
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
|
||||
@@ -11,7 +11,7 @@ DEPENDENCIES = ["rp2040"]
|
||||
rp2040_pwm_ns = cg.esphome_ns.namespace("rp2040_pwm")
|
||||
RP2040PWM = rp2040_pwm_ns.class_("RP2040PWM", output.FloatOutput, cg.Component)
|
||||
SetFrequencyAction = rp2040_pwm_ns.class_("SetFrequencyAction", automation.Action)
|
||||
validate_frequency = cv.All(cv.frequency, cv.Range(min=1.0e-6))
|
||||
validate_frequency = cv.All(cv.frequency, cv.float_range(min=1.0e-6))
|
||||
|
||||
CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ void SN74HC595GPIOComponent::write_gpio() {
|
||||
void SN74HC595SPIComponent::write_gpio() {
|
||||
for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) {
|
||||
this->enable();
|
||||
this->transfer_byte(output_byte);
|
||||
this->write_byte(output_byte);
|
||||
this->disable();
|
||||
}
|
||||
SN74HC595Component::write_gpio();
|
||||
|
||||
@@ -47,6 +47,8 @@ def require_wake_loop_threadsafe() -> None:
|
||||
This enables the shared UDP loopback socket mechanism (~208 bytes RAM).
|
||||
The socket is shared across all components that use this feature.
|
||||
|
||||
This call is a no-op if networking is not enabled in the configuration.
|
||||
|
||||
IMPORTANT: This is for background thread context only, NOT ISR context.
|
||||
Socket operations are not safe to call from ISR handlers.
|
||||
|
||||
@@ -56,8 +58,11 @@ def require_wake_loop_threadsafe() -> None:
|
||||
async def to_code(config):
|
||||
socket.require_wake_loop_threadsafe()
|
||||
"""
|
||||
|
||||
# Only set up once (idempotent - multiple components can call this)
|
||||
if not CORE.data.get(KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False):
|
||||
if CORE.has_networking and not CORE.data.get(
|
||||
KEY_WAKE_LOOP_THREADSAFE_REQUIRED, False
|
||||
):
|
||||
CORE.data[KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
|
||||
cg.add_define("USE_WAKE_LOOP_THREADSAFE")
|
||||
# Consume 1 socket for the shared wake notification socket
|
||||
|
||||
@@ -188,7 +188,7 @@ class LWIPRawImpl : public Socket {
|
||||
errno = EINVAL;
|
||||
return -1;
|
||||
}
|
||||
return this->ip2sockaddr_(&pcb_->local_ip, pcb_->local_port, name, addrlen);
|
||||
return this->ip2sockaddr_(&pcb_->remote_ip, pcb_->remote_port, name, addrlen);
|
||||
}
|
||||
std::string getpeername() override {
|
||||
if (pcb_ == nullptr) {
|
||||
|
||||
@@ -199,9 +199,13 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_CRC_INITIAL, default=0x1D0F): cv.All(
|
||||
cv.hex_int, cv.Range(min=0, max=0xFFFF)
|
||||
),
|
||||
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
|
||||
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=0, max=100000)
|
||||
),
|
||||
cv.Required(CONF_DIO1_PIN): pins.gpio_input_pin_schema,
|
||||
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
|
||||
cv.Required(CONF_FREQUENCY): cv.All(
|
||||
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
||||
),
|
||||
cv.Required(CONF_HW_VERSION): cv.one_of(
|
||||
"sx1261", "sx1262", "sx1268", "llcc68", lower=True
|
||||
),
|
||||
|
||||
@@ -196,9 +196,13 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_BITSYNC): cv.boolean,
|
||||
cv.Optional(CONF_CODING_RATE, default="CR_4_5"): cv.enum(CODING_RATE),
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DEVIATION, default=5000): cv.int_range(min=0, max=100000),
|
||||
cv.Optional(CONF_DEVIATION, default="5kHz"): cv.All(
|
||||
cv.frequency, cv.float_range(min=0, max=100000)
|
||||
),
|
||||
cv.Optional(CONF_DIO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Required(CONF_FREQUENCY): cv.int_range(min=137000000, max=1020000000),
|
||||
cv.Required(CONF_FREQUENCY): cv.All(
|
||||
cv.frequency, cv.float_range(min=137.0e6, max=1020.0e6)
|
||||
),
|
||||
cv.Required(CONF_MODULATION): cv.enum(MOD),
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
cv.Optional(CONF_PA_PIN, default="BOOST"): cv.enum(PA_PIN),
|
||||
|
||||
@@ -34,7 +34,15 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t
|
||||
severity = LOG_LEVEL_TO_SYSLOG_SEVERITY[level];
|
||||
}
|
||||
int pri = this->facility_ * 8 + severity;
|
||||
auto timestamp = this->time_->now().strftime("%b %e %H:%M:%S");
|
||||
auto now = this->time_->now();
|
||||
std::string timestamp;
|
||||
if (now.is_valid()) {
|
||||
timestamp = now.strftime("%b %e %H:%M:%S");
|
||||
} else {
|
||||
// RFC 5424: A syslog application MUST use the NILVALUE as TIMESTAMP if the syslog application is incapable of
|
||||
// obtaining system time.
|
||||
timestamp = "-";
|
||||
}
|
||||
size_t len = message_len;
|
||||
// remove color formatting
|
||||
if (this->strip_ && message[0] == 0x1B && len > 11) {
|
||||
|
||||
@@ -39,6 +39,7 @@ enum TemplateAlarmControlPanelRestoreMode {
|
||||
ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED,
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct SensorDataStore {
|
||||
bool last_chime_state;
|
||||
};
|
||||
@@ -49,7 +50,6 @@ struct SensorInfo {
|
||||
uint8_t store_index;
|
||||
};
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
struct AlarmSensor {
|
||||
binary_sensor::BinarySensor *sensor;
|
||||
SensorInfo info;
|
||||
@@ -139,6 +139,9 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
FixedVector<AlarmSensor> sensors_;
|
||||
// a list of automatically bypassed sensors
|
||||
std::vector<uint8_t> bypassed_sensor_indicies_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
uint8_t next_store_index_ = 0;
|
||||
#endif
|
||||
TemplateAlarmControlPanelRestoreMode restore_mode_{};
|
||||
|
||||
@@ -154,14 +157,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
|
||||
uint32_t trigger_time_;
|
||||
// a list of codes
|
||||
std::vector<std::string> codes_;
|
||||
// Per sensor data store
|
||||
std::vector<SensorDataStore> sensor_data_;
|
||||
// requires a code to arm
|
||||
bool requires_code_to_arm_ = false;
|
||||
bool supports_arm_home_ = false;
|
||||
bool supports_arm_night_ = false;
|
||||
bool sensors_ready_ = false;
|
||||
uint8_t next_store_index_ = 0;
|
||||
// check if the code is valid
|
||||
bool is_code_valid_(optional<std::string> code);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "driver/gpio.h"
|
||||
#include "soc/gpio_num.h"
|
||||
#include "soc/uart_pins.h"
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
@@ -139,6 +140,22 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
return;
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
// Workaround for ESP-IDF issue: https://github.com/espressif/esp-idf/issues/17459
|
||||
// Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks
|
||||
// UART on default UART0 pins that may have residual state from boot console.
|
||||
// Reset these pins before configuring UART to ensure they're in a clean state.
|
||||
if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(tx));
|
||||
}
|
||||
if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) {
|
||||
gpio_reset_pin(static_cast<gpio_num_t>(rx));
|
||||
}
|
||||
|
||||
// Setup pins after reset to preserve open drain/pullup/pulldown flags
|
||||
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
|
||||
if (!pin) {
|
||||
return;
|
||||
@@ -154,10 +171,6 @@ void IDFUARTComponent::load_settings(bool dump_config) {
|
||||
setup_pin_if_needed(this->tx_pin_);
|
||||
}
|
||||
|
||||
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
|
||||
int8_t rx = this->rx_pin_ != nullptr ? this->rx_pin_->get_pin() : -1;
|
||||
int8_t flow_control = this->flow_control_pin_ != nullptr ? this->flow_control_pin_->get_pin() : -1;
|
||||
|
||||
uint32_t invert = 0;
|
||||
if (this->tx_pin_ != nullptr && this->tx_pin_->is_inverted()) {
|
||||
invert |= UART_SIGNAL_TXD_INV;
|
||||
|
||||
@@ -528,6 +528,16 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) {
|
||||
for (auto *listener : global_wifi_component->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(global_wifi_component->wifi_ssid(), global_wifi_component->wifi_bssid());
|
||||
}
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = global_wifi_component->get_selected_sta_();
|
||||
config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : global_wifi_component->ip_state_listeners_) {
|
||||
listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(),
|
||||
global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -483,6 +483,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
s_sta_connected = false;
|
||||
s_sta_connect_error = false;
|
||||
s_sta_connect_not_found = false;
|
||||
// Reset IP address flags - ensures we don't report connected before DHCP completes
|
||||
// (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect)
|
||||
this->got_ipv4_address_ = false;
|
||||
#if USE_NETWORK_IPV6
|
||||
this->num_ipv6_addresses_ = 0;
|
||||
#endif
|
||||
|
||||
err = esp_wifi_connect();
|
||||
if (err != ESP_OK) {
|
||||
@@ -739,6 +745,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
|
||||
}
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
} else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
|
||||
@@ -305,6 +305,14 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
|
||||
}
|
||||
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -259,6 +259,15 @@ void WiFiComponent::wifi_loop_() {
|
||||
for (auto *listener : this->connect_state_listeners_) {
|
||||
listener->on_wifi_connect_state(this->wifi_ssid(), this->wifi_bssid());
|
||||
}
|
||||
// For static IP configurations, notify IP listeners immediately as the IP is already configured
|
||||
#ifdef USE_WIFI_MANUAL_IP
|
||||
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
|
||||
s_sta_had_ip = true;
|
||||
for (auto *listener : this->ip_state_listeners_) {
|
||||
listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
} else if (!is_connected && s_sta_was_connected) {
|
||||
// Just disconnected
|
||||
|
||||
@@ -71,17 +71,20 @@ void WTS01Sensor::process_packet_() {
|
||||
}
|
||||
|
||||
// Extract temperature value
|
||||
int8_t temp = this->buffer_[6];
|
||||
int32_t sign = 1;
|
||||
const uint8_t raw = this->buffer_[6];
|
||||
|
||||
// Handle negative temperatures
|
||||
if (temp < 0) {
|
||||
sign = -1;
|
||||
// WTS01 encodes sign in bit 7, magnitude in bits 0-6
|
||||
const bool negative = (raw & 0x80) != 0;
|
||||
const uint8_t magnitude = raw & 0x7F;
|
||||
|
||||
const float decimal = static_cast<float>(this->buffer_[7]) / 100.0f;
|
||||
|
||||
float temperature = static_cast<float>(magnitude) + decimal;
|
||||
|
||||
if (negative) {
|
||||
temperature = -temperature;
|
||||
}
|
||||
|
||||
// Calculate temperature (temp + decimal/100)
|
||||
float temperature = static_cast<float>(temp) + (sign * static_cast<float>(this->buffer_[7]) / 100.0f);
|
||||
|
||||
ESP_LOGV(TAG, "Received new temperature: %.2f°C", temperature);
|
||||
|
||||
this->publish_state(temperature);
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.12.0b3"
|
||||
__version__ = "2025.12.5"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -703,6 +703,25 @@ class EsphomeCore:
|
||||
def config_filename(self) -> str:
|
||||
return self.config_path.name
|
||||
|
||||
def has_at_least_one_component(self, *components: str) -> bool:
|
||||
"""
|
||||
Are any of the given components configured?
|
||||
:param components: component names
|
||||
:return: true if so
|
||||
"""
|
||||
if self.config is None:
|
||||
raise ValueError("Config has not been loaded yet")
|
||||
|
||||
return any(component in self.config for component in components)
|
||||
|
||||
@property
|
||||
def has_networking(self) -> bool:
|
||||
"""
|
||||
Is a network component configured?
|
||||
:return: true if so
|
||||
"""
|
||||
return self.has_at_least_one_component("wifi", "ethernet", "openthread")
|
||||
|
||||
def relative_config_path(self, *path: str | Path) -> Path:
|
||||
path_ = Path(*path).expanduser()
|
||||
return self.config_dir / path_
|
||||
|
||||
@@ -27,3 +27,7 @@ dependencies:
|
||||
version: "1.7.6~1"
|
||||
rules:
|
||||
- if: "target in [esp32s2, esp32s3, esp32p4]"
|
||||
esphome/esp-hub75:
|
||||
version: 0.1.7
|
||||
rules:
|
||||
- if: "target in [esp32, esp32s2, esp32s3, esp32p4]"
|
||||
|
||||
@@ -99,14 +99,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
|
||||
|
||||
|
||||
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
|
||||
if (
|
||||
# ESP32 uses CMake for both Arduino and ESP-IDF frameworks
|
||||
return (
|
||||
old.loaded_integrations != new.loaded_integrations
|
||||
or old.loaded_platforms != new.loaded_platforms
|
||||
) and new.core_platform == PLATFORM_ESP32:
|
||||
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
|
||||
|
||||
return new.framework == FRAMEWORK_ESP_IDF
|
||||
return False
|
||||
) and new.core_platform == PLATFORM_ESP32
|
||||
|
||||
|
||||
def update_storage_json() -> None:
|
||||
|
||||
@@ -156,7 +156,6 @@ lib_deps =
|
||||
esphome/ESP32-audioI2S@2.3.0 ; i2s_audio
|
||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||
esphome/esp-audio-libs@2.0.1 ; audio
|
||||
esphome/esp-hub75@0.1.6 ; hub75
|
||||
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
@@ -180,7 +179,6 @@ lib_deps =
|
||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
|
||||
esphome/esp-audio-libs@2.0.1 ; audio
|
||||
esphome/esp-hub75@0.1.6 ; hub75
|
||||
build_flags =
|
||||
${common:idf.build_flags}
|
||||
-Wno-nonnull-compare
|
||||
|
||||
@@ -4,6 +4,7 @@ from esphome.core import CORE
|
||||
|
||||
def test_require_wake_loop_threadsafe__first_call() -> None:
|
||||
"""Test that first call sets up define and consumes socket."""
|
||||
CORE.config = {"wifi": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify CORE.data was updated
|
||||
@@ -17,6 +18,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
|
||||
"""Test that subsequent calls are idempotent."""
|
||||
# Set up initial state as if already called
|
||||
CORE.data[socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED] = True
|
||||
CORE.config = {"ethernet": True}
|
||||
|
||||
# Call again - should not raise or fail
|
||||
socket.require_wake_loop_threadsafe()
|
||||
@@ -31,6 +33,7 @@ def test_require_wake_loop_threadsafe__idempotent() -> None:
|
||||
def test_require_wake_loop_threadsafe__multiple_calls() -> None:
|
||||
"""Test that multiple calls only set up once."""
|
||||
# Call three times
|
||||
CORE.config = {"openthread": True}
|
||||
socket.require_wake_loop_threadsafe()
|
||||
socket.require_wake_loop_threadsafe()
|
||||
socket.require_wake_loop_threadsafe()
|
||||
@@ -40,3 +43,35 @@ def test_require_wake_loop_threadsafe__multiple_calls() -> None:
|
||||
|
||||
# Verify the define was added (only once, but we can just check it exists)
|
||||
assert any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__no_networking() -> None:
|
||||
"""Test that wake loop is NOT configured when no networking is configured."""
|
||||
# Set up config without any networking components
|
||||
CORE.config = {"esphome": {"name": "test"}, "logger": {}}
|
||||
|
||||
# Call require_wake_loop_threadsafe
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify CORE.data flag was NOT set (since has_networking returns False)
|
||||
assert socket.KEY_WAKE_LOOP_THREADSAFE_REQUIRED not in CORE.data
|
||||
|
||||
# Verify the define was NOT added
|
||||
assert not any(d.name == "USE_WAKE_LOOP_THREADSAFE" for d in CORE.defines)
|
||||
|
||||
|
||||
def test_require_wake_loop_threadsafe__no_networking_does_not_consume_socket() -> None:
|
||||
"""Test that no socket is consumed when no networking is configured."""
|
||||
# Set up config without any networking components
|
||||
CORE.config = {"logger": {}}
|
||||
|
||||
# Track initial socket consumer state
|
||||
initial_consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {})
|
||||
|
||||
# Call require_wake_loop_threadsafe
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Verify no socket was consumed
|
||||
consumers = CORE.data.get(socket.KEY_SOCKET_CONSUMERS, {})
|
||||
assert "socket.wake_loop_threadsafe" not in consumers
|
||||
assert consumers == initial_consumers
|
||||
|
||||
@@ -718,3 +718,65 @@ class TestEsphomeCore:
|
||||
# Even though "web_server" is in loaded_integrations due to the platform,
|
||||
# web_port must return None because the full web_server component is not configured
|
||||
assert target.web_port is None
|
||||
|
||||
def test_has_at_least_one_component__none_configured(self, target):
|
||||
"""Test has_at_least_one_component returns False when none of the components are configured."""
|
||||
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
|
||||
|
||||
assert target.has_at_least_one_component("wifi", "ethernet") is False
|
||||
|
||||
def test_has_at_least_one_component__one_configured(self, target):
|
||||
"""Test has_at_least_one_component returns True when one component is configured."""
|
||||
target.config = {const.CONF_WIFI: {}, "logger": {}}
|
||||
|
||||
assert target.has_at_least_one_component("wifi", "ethernet") is True
|
||||
|
||||
def test_has_at_least_one_component__multiple_configured(self, target):
|
||||
"""Test has_at_least_one_component returns True when multiple components are configured."""
|
||||
target.config = {
|
||||
const.CONF_WIFI: {},
|
||||
const.CONF_ETHERNET: {},
|
||||
"logger": {},
|
||||
}
|
||||
|
||||
assert (
|
||||
target.has_at_least_one_component("wifi", "ethernet", "bluetooth") is True
|
||||
)
|
||||
|
||||
def test_has_at_least_one_component__single_component(self, target):
|
||||
"""Test has_at_least_one_component works with a single component."""
|
||||
target.config = {const.CONF_MQTT: {}}
|
||||
|
||||
assert target.has_at_least_one_component("mqtt") is True
|
||||
assert target.has_at_least_one_component("wifi") is False
|
||||
|
||||
def test_has_at_least_one_component__config_not_loaded(self, target):
|
||||
"""Test has_at_least_one_component raises ValueError when config is not loaded."""
|
||||
target.config = None
|
||||
|
||||
with pytest.raises(ValueError, match="Config has not been loaded yet"):
|
||||
target.has_at_least_one_component("wifi")
|
||||
|
||||
def test_has_networking__with_wifi(self, target):
|
||||
"""Test has_networking returns True when wifi is configured."""
|
||||
target.config = {const.CONF_WIFI: {}}
|
||||
|
||||
assert target.has_networking is True
|
||||
|
||||
def test_has_networking__with_ethernet(self, target):
|
||||
"""Test has_networking returns True when ethernet is configured."""
|
||||
target.config = {const.CONF_ETHERNET: {}}
|
||||
|
||||
assert target.has_networking is True
|
||||
|
||||
def test_has_networking__with_openthread(self, target):
|
||||
"""Test has_networking returns True when openthread is configured."""
|
||||
target.config = {const.CONF_OPENTHREAD: {}}
|
||||
|
||||
assert target.has_networking is True
|
||||
|
||||
def test_has_networking__without_networking(self, target):
|
||||
"""Test has_networking returns False when no networking component is configured."""
|
||||
target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}}
|
||||
|
||||
assert target.has_networking is False
|
||||
|
||||
@@ -9,6 +9,13 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.writer import (
|
||||
@@ -21,6 +28,7 @@ from esphome.writer import (
|
||||
clean_build,
|
||||
clean_cmake_cache,
|
||||
storage_should_clean,
|
||||
storage_should_update_cmake_cache,
|
||||
update_storage_json,
|
||||
write_cpp,
|
||||
write_gitignore,
|
||||
@@ -164,6 +172,86 @@ def test_storage_edge_case_from_empty_integrations(
|
||||
assert storage_should_clean(old, new) is False
|
||||
|
||||
|
||||
# Tests for storage_should_update_cmake_cache
|
||||
|
||||
|
||||
@pytest.mark.parametrize("framework", ["arduino", "esp-idf"])
|
||||
def test_storage_should_update_cmake_cache_when_integration_added_esp32(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
framework: str,
|
||||
) -> None:
|
||||
"""Test cmake cache update triggered when integration added on ESP32."""
|
||||
old = create_storage(
|
||||
loaded_integrations=["api", "wifi"],
|
||||
core_platform=PLATFORM_ESP32,
|
||||
framework=framework,
|
||||
)
|
||||
new = create_storage(
|
||||
loaded_integrations=["api", "wifi", "restart"],
|
||||
core_platform=PLATFORM_ESP32,
|
||||
framework=framework,
|
||||
)
|
||||
assert storage_should_update_cmake_cache(old, new) is True
|
||||
|
||||
|
||||
def test_storage_should_update_cmake_cache_when_platform_changed_esp32(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
) -> None:
|
||||
"""Test cmake cache update triggered when platforms change on ESP32."""
|
||||
old = create_storage(
|
||||
loaded_integrations=["api", "wifi"],
|
||||
loaded_platforms={"sensor"},
|
||||
core_platform=PLATFORM_ESP32,
|
||||
framework="arduino",
|
||||
)
|
||||
new = create_storage(
|
||||
loaded_integrations=["api", "wifi"],
|
||||
loaded_platforms={"sensor", "binary_sensor"},
|
||||
core_platform=PLATFORM_ESP32,
|
||||
framework="arduino",
|
||||
)
|
||||
assert storage_should_update_cmake_cache(old, new) is True
|
||||
|
||||
|
||||
def test_storage_should_not_update_cmake_cache_when_nothing_changes(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
) -> None:
|
||||
"""Test cmake cache not updated when nothing changes."""
|
||||
old = create_storage(
|
||||
loaded_integrations=["api", "wifi"],
|
||||
core_platform=PLATFORM_ESP32,
|
||||
framework="arduino",
|
||||
)
|
||||
new = create_storage(
|
||||
loaded_integrations=["api", "wifi"],
|
||||
core_platform=PLATFORM_ESP32,
|
||||
framework="arduino",
|
||||
)
|
||||
assert storage_should_update_cmake_cache(old, new) is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"core_platform",
|
||||
[PLATFORM_ESP8266, PLATFORM_RP2040, PLATFORM_BK72XX, PLATFORM_RTL87XX],
|
||||
)
|
||||
def test_storage_should_not_update_cmake_cache_for_non_esp32(
|
||||
create_storage: Callable[..., StorageJSON],
|
||||
core_platform: str,
|
||||
) -> None:
|
||||
"""Test cmake cache not updated for non-ESP32 platforms."""
|
||||
old = create_storage(
|
||||
loaded_integrations=["api", "wifi"],
|
||||
core_platform=core_platform,
|
||||
framework="arduino",
|
||||
)
|
||||
new = create_storage(
|
||||
loaded_integrations=["api", "wifi", "restart"],
|
||||
core_platform=core_platform,
|
||||
framework="arduino",
|
||||
)
|
||||
assert storage_should_update_cmake_cache(old, new) is False
|
||||
|
||||
|
||||
@patch("esphome.writer.clean_build")
|
||||
@patch("esphome.writer.StorageJSON")
|
||||
@patch("esphome.writer.storage_path")
|
||||
|
||||
Reference in New Issue
Block a user