mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
Merge remote-tracking branch 'upstream/dev' into ota-wakeable-delay-yield
# Conflicts: # esphome/core/wake/wake_esp8266.h # esphome/core/wake/wake_rp2040.cpp
This commit is contained in:
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.11
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -597,7 +597,7 @@ async def component_resume_action_to_code(
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
|
||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
|
||||
cg.add(var.set_update_interval(template_))
|
||||
return var
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
@@ -181,7 +185,7 @@ async def setup_alarm_control_panel_core_(var, config):
|
||||
async def register_alarm_control_panel(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_alarm_control_panel(var))
|
||||
queue_entity_register("alarm_control_panel", config)
|
||||
CORE.register_platform_component("alarm_control_panel", var)
|
||||
await setup_alarm_control_panel_core_(var, config)
|
||||
|
||||
|
||||
@@ -62,7 +62,12 @@ void Animation::set_frame(int frame) {
|
||||
}
|
||||
|
||||
void Animation::update_data_start_() {
|
||||
const uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
uint32_t image_size = this->get_width_stride() * this->height_;
|
||||
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
|
||||
// plane within each frame, so the per-frame stride includes the alpha bytes.
|
||||
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
|
||||
image_size += static_cast<uint32_t>(this->width_) * this->height_;
|
||||
}
|
||||
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
|
||||
}
|
||||
|
||||
|
||||
@@ -1025,6 +1025,13 @@ message CameraImageRequest {
|
||||
bool stream = 2;
|
||||
}
|
||||
|
||||
// ==================== TEMPERATURE UNIT ====================
|
||||
enum TemperatureUnit {
|
||||
TEMPERATURE_UNIT_CELSIUS = 0;
|
||||
TEMPERATURE_UNIT_FAHRENHEIT = 1;
|
||||
TEMPERATURE_UNIT_KELVIN = 2;
|
||||
}
|
||||
|
||||
// ==================== CLIMATE ====================
|
||||
enum ClimateMode {
|
||||
CLIMATE_MODE_OFF = 0;
|
||||
@@ -1110,6 +1117,7 @@ message ListEntitiesClimateResponse {
|
||||
float visual_max_humidity = 25;
|
||||
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
|
||||
uint32 feature_flags = 27;
|
||||
TemperatureUnit temperature_unit = 28;
|
||||
}
|
||||
message ClimateStateResponse {
|
||||
option (id) = 47;
|
||||
@@ -1203,6 +1211,7 @@ message ListEntitiesWaterHeaterResponse {
|
||||
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
|
||||
// Bitmask of WaterHeaterFeature flags
|
||||
uint32 supported_features = 12;
|
||||
TemperatureUnit temperature_unit = 13;
|
||||
}
|
||||
|
||||
message WaterHeaterStateResponse {
|
||||
@@ -1410,6 +1419,8 @@ enum LockState {
|
||||
LOCK_STATE_JAMMED = 3;
|
||||
LOCK_STATE_LOCKING = 4;
|
||||
LOCK_STATE_UNLOCKING = 5;
|
||||
LOCK_STATE_OPENING = 6;
|
||||
LOCK_STATE_OPEN = 7;
|
||||
}
|
||||
enum LockCommand {
|
||||
LOCK_UNLOCK = 0;
|
||||
|
||||
@@ -1439,6 +1439,7 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id);
|
||||
#endif
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 28, static_cast<uint32_t>(this->temperature_unit));
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesClimateResponse::calculate_size() const {
|
||||
@@ -1488,6 +1489,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const {
|
||||
size += ProtoSize::calc_uint32(2, this->device_id);
|
||||
#endif
|
||||
size += ProtoSize::calc_uint32(2, this->feature_flags);
|
||||
size += this->temperature_unit ? 3 : 0;
|
||||
return size;
|
||||
}
|
||||
uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -1645,6 +1647,7 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true);
|
||||
}
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->temperature_unit));
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
|
||||
@@ -1667,6 +1670,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
|
||||
size += this->supported_modes->size() * 2;
|
||||
}
|
||||
size += ProtoSize::calc_uint32(1, this->supported_features);
|
||||
size += this->temperature_unit ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
|
||||
@@ -92,6 +92,11 @@ enum SupportsResponseType : uint32_t {
|
||||
SUPPORTS_RESPONSE_STATUS = 100,
|
||||
};
|
||||
#endif
|
||||
enum TemperatureUnit : uint32_t {
|
||||
TEMPERATURE_UNIT_CELSIUS = 0,
|
||||
TEMPERATURE_UNIT_FAHRENHEIT = 1,
|
||||
TEMPERATURE_UNIT_KELVIN = 2,
|
||||
};
|
||||
#ifdef USE_CLIMATE
|
||||
enum ClimateMode : uint32_t {
|
||||
CLIMATE_MODE_OFF = 0,
|
||||
@@ -176,6 +181,8 @@ enum LockState : uint32_t {
|
||||
LOCK_STATE_JAMMED = 3,
|
||||
LOCK_STATE_LOCKING = 4,
|
||||
LOCK_STATE_UNLOCKING = 5,
|
||||
LOCK_STATE_OPENING = 6,
|
||||
LOCK_STATE_OPEN = 7,
|
||||
};
|
||||
enum LockCommand : uint32_t {
|
||||
LOCK_UNLOCK = 0,
|
||||
@@ -1372,7 +1379,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
|
||||
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 46;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 150;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 153;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); }
|
||||
#endif
|
||||
@@ -1394,6 +1401,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
||||
float visual_min_humidity{0.0f};
|
||||
float visual_max_humidity{0.0f};
|
||||
uint32_t feature_flags{0};
|
||||
enums::TemperatureUnit temperature_unit{};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1471,7 +1479,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
|
||||
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 132;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 63;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 65;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); }
|
||||
#endif
|
||||
@@ -1480,6 +1488,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
|
||||
float target_temperature_step{0.0f};
|
||||
const water_heater::WaterHeaterModeMask *supported_modes{};
|
||||
uint32_t supported_features{0};
|
||||
enums::TemperatureUnit temperature_unit{};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
|
||||
@@ -297,6 +297,18 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::
|
||||
}
|
||||
}
|
||||
#endif
|
||||
template<> const char *proto_enum_to_string<enums::TemperatureUnit>(enums::TemperatureUnit value) {
|
||||
switch (value) {
|
||||
case enums::TEMPERATURE_UNIT_CELSIUS:
|
||||
return ESPHOME_PSTR("TEMPERATURE_UNIT_CELSIUS");
|
||||
case enums::TEMPERATURE_UNIT_FAHRENHEIT:
|
||||
return ESPHOME_PSTR("TEMPERATURE_UNIT_FAHRENHEIT");
|
||||
case enums::TEMPERATURE_UNIT_KELVIN:
|
||||
return ESPHOME_PSTR("TEMPERATURE_UNIT_KELVIN");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#ifdef USE_CLIMATE
|
||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||
switch (value) {
|
||||
@@ -475,6 +487,10 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
|
||||
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
|
||||
case enums::LOCK_STATE_UNLOCKING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
|
||||
case enums::LOCK_STATE_OPENING:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPENING");
|
||||
case enums::LOCK_STATE_OPEN:
|
||||
return ESPHOME_PSTR("LOCK_STATE_OPEN");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -1539,6 +1555,7 @@ const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
|
||||
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -1612,6 +1629,7 @@ const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4);
|
||||
}
|
||||
dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features);
|
||||
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -183,19 +183,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
cg.add(var.set_sensing_distance(template_))
|
||||
|
||||
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int32)
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int_)
|
||||
cg.add(var.set_poweron_selfcheck_time(template_))
|
||||
|
||||
if protect := config.get(CONF_PROTECT_TIME):
|
||||
template_ = await cg.templatable(protect, args, cg.int32)
|
||||
template_ = await cg.templatable(protect, args, cg.int_)
|
||||
cg.add(var.set_protect_time(template_))
|
||||
|
||||
if trig_base := config.get(CONF_TRIGGER_BASE):
|
||||
template_ = await cg.templatable(trig_base, args, cg.int32)
|
||||
template_ = await cg.templatable(trig_base, args, cg.int_)
|
||||
cg.add(var.set_trigger_base(template_))
|
||||
|
||||
if trig_keep := config.get(CONF_TRIGGER_KEEP):
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int32)
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int_)
|
||||
cg.add(var.set_trigger_keep(template_))
|
||||
|
||||
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
|
||||
|
||||
@@ -220,7 +220,7 @@ async def to_code(config):
|
||||
data = _get_data()
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.2.0")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
|
||||
@@ -154,7 +154,7 @@ void BH1750Sensor::loop() {
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
|
||||
ESP_LOGV(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
|
||||
this->status_clear_warning();
|
||||
this->publish_state(lx);
|
||||
this->state_ = IDLE;
|
||||
|
||||
@@ -62,6 +62,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -624,7 +625,7 @@ async def setup_binary_sensor_core_(var, config):
|
||||
async def register_binary_sensor(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_binary_sensor(var))
|
||||
queue_entity_register("binary_sensor", config)
|
||||
CORE.register_platform_component("binary_sensor", var)
|
||||
await setup_binary_sensor_core_(var, config)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_BK7251,
|
||||
)
|
||||
|
||||
@@ -24,16 +25,32 @@ BK72XX_BOARDS = {
|
||||
"name": "WB2L_M1 Wi-Fi Module",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"xh-wb3s": {
|
||||
"name": "NiceMCU XH-WB3S",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"cbu": {
|
||||
"name": "CBU Wi-Fi Module",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"t1-u": {
|
||||
"name": "T1-U Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"generic-bk7238-tuya": {
|
||||
"name": "Generic - BK7238 (Tuya T1)",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"t1-m": {
|
||||
"name": "T1-M Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"name": "Generic - BK7231T (Tuya QFN32)",
|
||||
"name": "Generic - BK7231T (Tuya)",
|
||||
"family": FAMILY_BK7231T,
|
||||
},
|
||||
"generic-bk7231n-qfn32-tuya": {
|
||||
"name": "Generic - BK7231N (Tuya QFN32)",
|
||||
"name": "Generic - BK7231N (Tuya)",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"cb1s": {
|
||||
@@ -64,6 +81,10 @@ BK72XX_BOARDS = {
|
||||
"name": "Generic - BK7252",
|
||||
"family": FAMILY_BK7251,
|
||||
},
|
||||
"t1-3s": {
|
||||
"name": "T1-3S Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"wb2l": {
|
||||
"name": "WB2L Wi-Fi Module",
|
||||
"family": FAMILY_BK7231T,
|
||||
@@ -80,6 +101,10 @@ BK72XX_BOARDS = {
|
||||
"name": "CB2S Wi-Fi Module",
|
||||
"family": FAMILY_BK7231N,
|
||||
},
|
||||
"generic-bk7238": {
|
||||
"name": "Generic - BK7238",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"wa2": {
|
||||
"name": "WA2 Wi-Fi Module",
|
||||
"family": FAMILY_BK7231Q,
|
||||
@@ -100,6 +125,10 @@ BK72XX_BOARDS = {
|
||||
"name": "WB3L Wi-Fi Module",
|
||||
"family": FAMILY_BK7231T,
|
||||
},
|
||||
"t1-2s": {
|
||||
"name": "T1-2S Wi-Fi Module",
|
||||
"family": FAMILY_BK7238,
|
||||
},
|
||||
"wb2s": {
|
||||
"name": "WB2S Wi-Fi Module",
|
||||
"family": FAMILY_BK7231T,
|
||||
@@ -158,6 +187,83 @@ BK72XX_BOARD_PINS = {
|
||||
"D12": 22,
|
||||
"A0": 23,
|
||||
},
|
||||
"xh-wb3s": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P7": 7,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM1": 7,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 7,
|
||||
"D1": 23,
|
||||
"D2": 14,
|
||||
"D3": 26,
|
||||
"D4": 24,
|
||||
"D5": 6,
|
||||
"D6": 9,
|
||||
"D7": 0,
|
||||
"D8": 1,
|
||||
"D9": 8,
|
||||
"D10": 10,
|
||||
"D11": 11,
|
||||
"D12": 16,
|
||||
"D13": 20,
|
||||
"D14": 21,
|
||||
"D15": 22,
|
||||
"D16": 15,
|
||||
"D17": 17,
|
||||
"A0": 28,
|
||||
"A1": 26,
|
||||
"A2": 24,
|
||||
"A3": 1,
|
||||
"A4": 10,
|
||||
"A5": 20,
|
||||
},
|
||||
"cbu": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
@@ -230,6 +336,204 @@ BK72XX_BOARD_PINS = {
|
||||
"D18": 21,
|
||||
"A0": 23,
|
||||
},
|
||||
"t1-u": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 14,
|
||||
"D1": 16,
|
||||
"D2": 23,
|
||||
"D3": 22,
|
||||
"D4": 20,
|
||||
"D5": 1,
|
||||
"D6": 0,
|
||||
"D7": 24,
|
||||
"D8": 9,
|
||||
"D9": 26,
|
||||
"D10": 6,
|
||||
"D11": 8,
|
||||
"D12": 11,
|
||||
"D13": 10,
|
||||
"D14": 28,
|
||||
"D15": 21,
|
||||
"D16": 17,
|
||||
"D17": 15,
|
||||
"A0": 20,
|
||||
"A1": 1,
|
||||
"A2": 24,
|
||||
"A3": 26,
|
||||
"A4": 10,
|
||||
"A5": 28,
|
||||
},
|
||||
"generic-bk7238-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P7": 7,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM1": 7,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 6,
|
||||
"D3": 7,
|
||||
"D4": 8,
|
||||
"D5": 9,
|
||||
"D6": 10,
|
||||
"D7": 11,
|
||||
"D8": 14,
|
||||
"D9": 15,
|
||||
"D10": 16,
|
||||
"D11": 17,
|
||||
"D12": 20,
|
||||
"D13": 21,
|
||||
"D14": 22,
|
||||
"D15": 23,
|
||||
"D16": 24,
|
||||
"D17": 26,
|
||||
"D18": 28,
|
||||
"A0": 1,
|
||||
"A1": 10,
|
||||
"A2": 20,
|
||||
"A3": 24,
|
||||
"A4": 26,
|
||||
"A5": 28,
|
||||
},
|
||||
"t1-m": {
|
||||
"WIRE2_SCL": 24,
|
||||
"WIRE2_SDA": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCL2": 24,
|
||||
"SDA2": 26,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 26,
|
||||
"D1": 6,
|
||||
"D2": 8,
|
||||
"D3": 1,
|
||||
"D4": 10,
|
||||
"D5": 11,
|
||||
"D6": 9,
|
||||
"D7": 24,
|
||||
"D11": 0,
|
||||
"A0": 26,
|
||||
"A1": 10,
|
||||
"A2": 1,
|
||||
"A3": 24,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
@@ -781,6 +1085,75 @@ BK72XX_BOARD_PINS = {
|
||||
"A6": 12,
|
||||
"A7": 13,
|
||||
},
|
||||
"t1-3s": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 20,
|
||||
"D1": 22,
|
||||
"D2": 6,
|
||||
"D3": 8,
|
||||
"D4": 9,
|
||||
"D5": 23,
|
||||
"D6": 0,
|
||||
"D7": 1,
|
||||
"D8": 24,
|
||||
"D9": 26,
|
||||
"D10": 10,
|
||||
"D11": 11,
|
||||
"D12": 17,
|
||||
"D13": 16,
|
||||
"D14": 15,
|
||||
"D15": 14,
|
||||
"A0": 20,
|
||||
"A1": 1,
|
||||
"A2": 24,
|
||||
"A3": 26,
|
||||
"A4": 10,
|
||||
},
|
||||
"wb2l": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
@@ -965,6 +1338,84 @@ BK72XX_BOARD_PINS = {
|
||||
"D10": 21,
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7238": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL_0": 15,
|
||||
"WIRE2_SCL_1": 24,
|
||||
"WIRE2_SDA_0": 17,
|
||||
"WIRE2_SDA_1": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC3": 20,
|
||||
"ADC4": 28,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"CS": 15,
|
||||
"MISO": 17,
|
||||
"MOSI": 16,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P7": 7,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P14": 14,
|
||||
"P15": 15,
|
||||
"P16": 16,
|
||||
"P17": 17,
|
||||
"P20": 20,
|
||||
"P21": 21,
|
||||
"P22": 22,
|
||||
"P23": 23,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"P28": 28,
|
||||
"PWM0": 6,
|
||||
"PWM1": 7,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCK": 14,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 0,
|
||||
"D1": 1,
|
||||
"D2": 6,
|
||||
"D3": 7,
|
||||
"D4": 8,
|
||||
"D5": 9,
|
||||
"D6": 10,
|
||||
"D7": 11,
|
||||
"D8": 14,
|
||||
"D9": 15,
|
||||
"D10": 16,
|
||||
"D11": 17,
|
||||
"D12": 20,
|
||||
"D13": 21,
|
||||
"D14": 22,
|
||||
"D15": 23,
|
||||
"D16": 24,
|
||||
"D17": 26,
|
||||
"D18": 28,
|
||||
"A0": 1,
|
||||
"A1": 10,
|
||||
"A2": 20,
|
||||
"A3": 24,
|
||||
"A4": 26,
|
||||
"A5": 28,
|
||||
},
|
||||
"wa2": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
@@ -1235,6 +1686,51 @@ BK72XX_BOARD_PINS = {
|
||||
"D15": 1,
|
||||
"A0": 23,
|
||||
},
|
||||
"t1-2s": {
|
||||
"WIRE2_SCL": 24,
|
||||
"WIRE2_SDA": 26,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_RX": 1,
|
||||
"SERIAL2_TX": 0,
|
||||
"ADC1": 26,
|
||||
"ADC2": 24,
|
||||
"ADC5": 1,
|
||||
"ADC6": 10,
|
||||
"P0": 0,
|
||||
"P1": 1,
|
||||
"P6": 6,
|
||||
"P8": 8,
|
||||
"P9": 9,
|
||||
"P10": 10,
|
||||
"P11": 11,
|
||||
"P24": 24,
|
||||
"P26": 26,
|
||||
"PWM0": 6,
|
||||
"PWM2": 8,
|
||||
"PWM3": 9,
|
||||
"PWM4": 24,
|
||||
"PWM5": 26,
|
||||
"RX1": 10,
|
||||
"RX2": 1,
|
||||
"SCL2": 24,
|
||||
"SDA2": 26,
|
||||
"TX1": 11,
|
||||
"TX2": 0,
|
||||
"D0": 26,
|
||||
"D1": 6,
|
||||
"D2": 8,
|
||||
"D3": 1,
|
||||
"D4": 10,
|
||||
"D5": 11,
|
||||
"D6": 9,
|
||||
"D7": 24,
|
||||
"D11": 0,
|
||||
"A0": 26,
|
||||
"A1": 10,
|
||||
"A2": 1,
|
||||
"A3": 24,
|
||||
},
|
||||
"wb2s": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
|
||||
@@ -30,19 +30,6 @@ void BluetoothProxy::setup() {
|
||||
this->configured_scan_active_ = this->parent_->get_scan_active();
|
||||
|
||||
this->parent_->add_scanner_state_listener(this);
|
||||
|
||||
this->set_interval(100, [this]() {
|
||||
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
|
||||
this->flush_pending_advertisements_();
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
|
||||
@@ -133,6 +120,25 @@ void BluetoothProxy::dump_config() {
|
||||
YESNO(this->active_), this->connection_count_);
|
||||
}
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
// Run advertisement flush / connection cleanup every 100ms
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_advertisement_flush_time_ < 100)
|
||||
return;
|
||||
this->last_advertisement_flush_time_ = now;
|
||||
|
||||
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
|
||||
this->flush_pending_advertisements_();
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
|
||||
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
|
||||
}
|
||||
@@ -201,7 +207,6 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
|
||||
this->log_connection_info_(connection, "v3 without cache");
|
||||
}
|
||||
uint64_to_bd_addr(msg.address, connection->remote_bda_);
|
||||
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
|
||||
connection->set_state(espbt::ClientState::DISCOVERED);
|
||||
this->send_connections_free();
|
||||
|
||||
@@ -65,6 +65,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
|
||||
void dump_config() override;
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
@@ -176,6 +177,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
// BLE advertisement batching
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
// Pre-allocated response message - always ready to send
|
||||
api::BluetoothConnectionsFreeResponse connections_free_response_;
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
|
||||
async def register_button(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_button(var))
|
||||
queue_entity_register("button", config)
|
||||
CORE.register_platform_component("button", var)
|
||||
await setup_button_core_(var, config)
|
||||
|
||||
|
||||
@@ -49,7 +49,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -442,7 +446,7 @@ async def setup_climate_core_(var, config):
|
||||
async def register_climate(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_climate(var))
|
||||
queue_entity_register("climate", config)
|
||||
CORE.register_platform_component("climate", var)
|
||||
await setup_climate_core_(var, config)
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -232,7 +233,7 @@ async def setup_cover_core_(var, config):
|
||||
async def register_cover(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_cover(var))
|
||||
queue_entity_register("cover", config)
|
||||
CORE.register_platform_component("cover", var)
|
||||
await setup_cover_core_(var, config)
|
||||
|
||||
|
||||
@@ -22,7 +22,11 @@ from esphome.const import (
|
||||
CONF_YEAR,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@rfdarter", "@jesserockz"]
|
||||
@@ -160,7 +164,7 @@ async def register_datetime(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
entity_type = config[CONF_TYPE].lower()
|
||||
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
|
||||
queue_entity_register(entity_type, config)
|
||||
CORE.register_platform_component(entity_type, var)
|
||||
await setup_datetime_core_(var, config)
|
||||
|
||||
|
||||
@@ -1724,6 +1724,11 @@ async def to_code(config):
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
# Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep
|
||||
# volatile build path/time data out of the binary so equivalent projects can
|
||||
# produce reproducible outputs and downstream tooling can reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
|
||||
@@ -18,6 +18,12 @@ struct NVSData {
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
// open() runs from app_main() before the logger is initialized, so any failure
|
||||
// must be deferred until after global_logger is set. This is emitted from the
|
||||
// first make_preference() call, which runs from the generated setup() after
|
||||
// log->pre_setup() has run at EARLY_INIT priority.
|
||||
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
@@ -70,12 +76,14 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
}
|
||||
|
||||
void ESP32Preferences::open() {
|
||||
// Runs from app_main() before the logger is initialized; any logging here
|
||||
// must be deferred. See s_open_err and make_preference() below.
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
s_open_err = err;
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
@@ -87,6 +95,14 @@ void ESP32Preferences::open() {
|
||||
}
|
||||
|
||||
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
|
||||
if (s_open_err != ESP_OK) {
|
||||
if (this->nvs_handle == 0) {
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
|
||||
} else {
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
|
||||
}
|
||||
s_open_err = ESP_OK;
|
||||
}
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
@@ -166,8 +166,9 @@ void ESP32BLETracker::loop() {
|
||||
ClientStateCounts counts = this->count_client_states_();
|
||||
if (counts != this->client_state_counts_) {
|
||||
this->client_state_counts_ = counts;
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d",
|
||||
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
|
||||
this->client_state_counts_.disconnecting, this->client_state_counts_.active);
|
||||
}
|
||||
|
||||
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
|
||||
@@ -190,10 +191,18 @@ void ESP32BLETracker::loop() {
|
||||
*/
|
||||
|
||||
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
|
||||
// all clients are idle (their state changes increment version when they finish)
|
||||
// no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states
|
||||
// (their state changes increment version when they finish). CONNECTED / ESTABLISHED
|
||||
// clients do NOT block this branch — the coex revert below has its own active-count gate.
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
this->update_coex_preference_(false);
|
||||
// Only revert to BALANCE when no connections are active. Established connections
|
||||
// continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic
|
||||
// (advertisement upload, log streaming) competes for the shared radio. Reverting too
|
||||
// early causes Bluedroid to time out at ~20s and synthesize status=133.
|
||||
if (!counts.active) {
|
||||
this->update_coex_preference_(false);
|
||||
}
|
||||
#endif
|
||||
if (this->scan_continuous_) {
|
||||
this->start_scan_(false); // first = false
|
||||
@@ -701,9 +710,10 @@ void ESP32BLETracker::dump_config() {
|
||||
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Scanner State: %s\n"
|
||||
" Connecting: %d, discovered: %d, disconnecting: %d",
|
||||
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
|
||||
this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting,
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
|
||||
this->client_state_counts_.active);
|
||||
if (this->scan_start_fail_count_) {
|
||||
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
|
||||
}
|
||||
|
||||
@@ -160,9 +160,13 @@ struct ClientStateCounts {
|
||||
uint8_t connecting = 0;
|
||||
uint8_t discovered = 0;
|
||||
uint8_t disconnecting = 0;
|
||||
// CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT
|
||||
// while active connections may still need to send/receive GATT traffic.
|
||||
uint8_t active = 0;
|
||||
|
||||
bool operator==(const ClientStateCounts &other) const {
|
||||
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
|
||||
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting &&
|
||||
active == other.active;
|
||||
}
|
||||
|
||||
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
|
||||
@@ -381,6 +385,10 @@ class ESP32BLETracker : public Component,
|
||||
case ClientState::CONNECTING:
|
||||
counts.connecting++;
|
||||
break;
|
||||
case ClientState::CONNECTED:
|
||||
case ClientState::ESTABLISHED:
|
||||
counts.active++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -216,6 +216,7 @@ void ESP32TouchComponent::setup() {
|
||||
// Do initial oneshot scans to populate baseline values
|
||||
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
|
||||
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
|
||||
App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err));
|
||||
}
|
||||
|
||||
@@ -292,6 +292,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
bool update_started = false;
|
||||
size_t total = 0;
|
||||
uint32_t last_progress = 0;
|
||||
uint32_t last_data_ms = 0;
|
||||
uint8_t buf[OTA_BUFFER_SIZE];
|
||||
char *sbuf = reinterpret_cast<char *>(buf);
|
||||
size_t ota_size;
|
||||
@@ -350,8 +351,18 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
// Acknowledge MD5 OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
|
||||
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
||||
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
||||
// can't wedge the device indefinitely. Without this, the loop only exits
|
||||
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
|
||||
// TCP keepalive isn't enabled here.
|
||||
last_data_ms = millis();
|
||||
while (total < ota_size) {
|
||||
// TODO: timeout check
|
||||
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
|
||||
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
size_t remaining = ota_size - total;
|
||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||
ssize_t read = this->client_->read(buf, requested);
|
||||
@@ -369,6 +380,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
last_data_ms = millis();
|
||||
error_code = this->backend_->write(buf, read);
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
ESP_LOGW(TAG, "Flash write err %d", error_code);
|
||||
|
||||
@@ -19,6 +19,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -108,7 +109,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||
async def register_event(var, config, *, event_types: list[str]):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_event(var))
|
||||
queue_entity_register("event", config)
|
||||
CORE.register_platform_component("event", var)
|
||||
await setup_event_core_(var, config, event_types=event_types)
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from esphome import git, loader
|
||||
import esphome.config_validation as cv
|
||||
@@ -17,7 +18,7 @@ from esphome.const import (
|
||||
TYPE_GIT,
|
||||
TYPE_LOCAL,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, TimePeriodSeconds
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -35,17 +36,15 @@ CONFIG_SCHEMA = cv.ensure_list(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: dict[str, Any]) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str:
|
||||
# When skip_update is True, use NEVER_REFRESH to prevent updates
|
||||
actual_refresh = git.NEVER_REFRESH if skip_update else refresh
|
||||
def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> Path:
|
||||
repo_dir, _ = git.clone_or_update(
|
||||
url=config[CONF_URL],
|
||||
ref=config.get(CONF_REF),
|
||||
refresh=actual_refresh,
|
||||
refresh=refresh,
|
||||
domain=DOMAIN,
|
||||
username=config.get(CONF_USERNAME),
|
||||
password=config.get(CONF_PASSWORD),
|
||||
@@ -72,12 +71,12 @@ def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str
|
||||
return components_dir
|
||||
|
||||
|
||||
def _process_single_config(config: dict, skip_update: bool = False):
|
||||
def _process_single_config(config: dict[str, Any]) -> None:
|
||||
conf = config[CONF_SOURCE]
|
||||
if conf[CONF_TYPE] == TYPE_GIT:
|
||||
with cv.prepend_path([CONF_SOURCE]):
|
||||
components_dir = _process_git_config(
|
||||
config[CONF_SOURCE], config[CONF_REFRESH], skip_update
|
||||
config[CONF_SOURCE], config[CONF_REFRESH]
|
||||
)
|
||||
elif conf[CONF_TYPE] == TYPE_LOCAL:
|
||||
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
|
||||
@@ -107,7 +106,7 @@ def _process_single_config(config: dict, skip_update: bool = False):
|
||||
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
|
||||
|
||||
|
||||
def do_external_components_pass(config: dict, skip_update: bool = False) -> None:
|
||||
def do_external_components_pass(config: dict[str, Any]) -> None:
|
||||
conf = config.get(DOMAIN)
|
||||
if conf is None:
|
||||
return
|
||||
@@ -115,4 +114,4 @@ def do_external_components_pass(config: dict, skip_update: bool = False) -> None
|
||||
conf = CONFIG_SCHEMA(conf)
|
||||
for i, c in enumerate(conf):
|
||||
with cv.prepend_path(i):
|
||||
_process_single_config(c, skip_update)
|
||||
_process_single_config(c)
|
||||
|
||||
@@ -32,7 +32,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -292,7 +296,7 @@ async def setup_fan_core_(var, config):
|
||||
async def register_fan(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_fan(var))
|
||||
queue_entity_register("fan", config)
|
||||
CORE.register_platform_component("fan", var)
|
||||
await setup_fan_core_(var, config)
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
namespace esphome::feedback {
|
||||
|
||||
static const char *const TAG = "feedback.cover";
|
||||
|
||||
static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
|
||||
|
||||
using namespace esphome::cover;
|
||||
|
||||
void FeedbackCover::setup() {
|
||||
@@ -37,7 +38,7 @@ void FeedbackCover::setup() {
|
||||
}
|
||||
#endif
|
||||
|
||||
this->last_recompute_time_ = this->start_dir_time_ = millis();
|
||||
this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
CoverTraits FeedbackCover::get_traits() {
|
||||
@@ -135,7 +136,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop
|
||||
#endif
|
||||
|
||||
void FeedbackCover::endstop_reached_(bool open_endstop) {
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
|
||||
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
|
||||
|
||||
@@ -174,7 +175,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool
|
||||
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
|
||||
#endif
|
||||
{
|
||||
auto now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
this->current_operation = operation;
|
||||
this->start_dir_time_ = this->last_recompute_time_ = now;
|
||||
this->publish_state();
|
||||
@@ -306,7 +307,7 @@ void FeedbackCover::control(const CoverCall &call) {
|
||||
|
||||
void FeedbackCover::stop_prev_trigger_() {
|
||||
if (this->direction_change_waittime_.has_value()) {
|
||||
this->cancel_timeout("direction_change");
|
||||
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
|
||||
}
|
||||
if (this->prev_command_trigger_ != nullptr) {
|
||||
this->prev_command_trigger_->stop_action();
|
||||
@@ -377,7 +378,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
|
||||
this->start_direction_(COVER_OPERATION_IDLE);
|
||||
|
||||
this->set_timeout("direction_change", *this->direction_change_waittime_,
|
||||
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_,
|
||||
[this, dir]() { this->start_direction_(dir); });
|
||||
|
||||
} else {
|
||||
@@ -395,7 +396,7 @@ void FeedbackCover::recompute_position_() {
|
||||
if (this->current_operation == COVER_OPERATION_IDLE)
|
||||
return;
|
||||
|
||||
const uint32_t now = millis();
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
float dir;
|
||||
float action_dur;
|
||||
float min_pos;
|
||||
@@ -451,5 +452,4 @@ void FeedbackCover::recompute_position_() {
|
||||
this->last_recompute_time_ = now;
|
||||
}
|
||||
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
} // namespace esphome::feedback
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#endif
|
||||
#include "esphome/components/cover/cover.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace feedback {
|
||||
namespace esphome::feedback {
|
||||
|
||||
class FeedbackCover : public cover::Cover, public Component {
|
||||
public:
|
||||
@@ -85,5 +84,4 @@ class FeedbackCover : public cover::Cover, public Component {
|
||||
uint32_t update_interval_{1000};
|
||||
};
|
||||
|
||||
} // namespace feedback
|
||||
} // namespace esphome
|
||||
} // namespace esphome::feedback
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <csignal>
|
||||
#include <sched.h>
|
||||
#include <time.h>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace {
|
||||
@@ -22,9 +21,7 @@ void HOT yield() { ::sched_yield(); }
|
||||
uint32_t IRAM_ATTR HOT millis() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
time_t seconds = spec.tv_sec;
|
||||
uint32_t ms = round(spec.tv_nsec / 1e6);
|
||||
return ((uint32_t) seconds) * 1000U + ms;
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
|
||||
}
|
||||
uint64_t millis_64() {
|
||||
struct timespec spec;
|
||||
@@ -43,9 +40,7 @@ void HOT delay(uint32_t ms) {
|
||||
uint32_t IRAM_ATTR HOT micros() {
|
||||
struct timespec spec;
|
||||
clock_gettime(CLOCK_MONOTONIC, &spec);
|
||||
time_t seconds = spec.tv_sec;
|
||||
uint32_t us = round(spec.tv_nsec / 1e3);
|
||||
return ((uint32_t) seconds) * 1000000U + us;
|
||||
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
|
||||
}
|
||||
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
|
||||
struct timespec ts;
|
||||
|
||||
@@ -744,21 +744,28 @@ async def write_image(config, all_frames=False):
|
||||
if frame_count <= 1:
|
||||
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||
|
||||
total_rows = height * frame_count
|
||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
# Encode each frame with its own encoder and concatenate. This keeps every
|
||||
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
|
||||
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
|
||||
# correct without needing to know the total frame count.
|
||||
byte_order = config.get(CONF_BYTE_ORDER)
|
||||
combined_data: list[int] = []
|
||||
encoder: ImageEncoder | None = None
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
encoder.end_image()
|
||||
encoder.end_image()
|
||||
combined_data.extend(encoder.data)
|
||||
|
||||
rhs = [HexInt(x) for x in encoder.data]
|
||||
rhs = [HexInt(x) for x in combined_data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
image_type = get_image_type_enum(type)
|
||||
trans_value = get_transparency_enum(encoder.transparency)
|
||||
|
||||
@@ -12,7 +12,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
from esphome.core.entity_helpers import queue_entity_register, setup_entity
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -54,8 +54,8 @@ async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
"""Register an infrared device with the core."""
|
||||
cg.add_define("USE_IR_RF")
|
||||
await cg.register_component(var, config)
|
||||
queue_entity_register("infrared", config)
|
||||
await setup_infrared_core_(var, config)
|
||||
cg.add(cg.App.register_infrared(var))
|
||||
CORE.register_platform_component("infrared", var)
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,73 @@
|
||||
#include "ir_rf_proxy.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ir_rf_proxy {
|
||||
|
||||
static const char *const TAG = "ir_rf_proxy";
|
||||
|
||||
// ========== Shared transmit helper ==========
|
||||
// Static template: all instantiations occur in this translation unit.
|
||||
|
||||
template<typename CallT>
|
||||
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
|
||||
const CallT &call) {
|
||||
if (transmitter == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!call.has_raw_timings()) {
|
||||
ESP_LOGE(TAG, "No raw timings provided");
|
||||
return;
|
||||
}
|
||||
|
||||
auto transmit_call = transmitter->transmit();
|
||||
auto *transmit_data = transmit_call.get_data();
|
||||
transmit_data->set_carrier_frequency(carrier_frequency);
|
||||
|
||||
if (call.is_packed()) {
|
||||
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
|
||||
call.get_packed_count());
|
||||
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
|
||||
call.get_repeat_count());
|
||||
} else if (call.is_base64url()) {
|
||||
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
|
||||
ESP_LOGE(TAG, "Invalid base64url data");
|
||||
return;
|
||||
}
|
||||
constexpr int32_t max_timing_us = 500000;
|
||||
for (int32_t timing : transmit_data->get_data()) {
|
||||
int32_t abs_timing = timing < 0 ? -timing : timing;
|
||||
if (abs_timing > max_timing_us) {
|
||||
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
|
||||
call.get_repeat_count());
|
||||
} else {
|
||||
transmit_data->set_data(call.get_raw_timings());
|
||||
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
|
||||
call.get_repeat_count());
|
||||
}
|
||||
|
||||
if (call.get_repeat_count() > 0) {
|
||||
transmit_call.set_send_times(call.get_repeat_count());
|
||||
}
|
||||
|
||||
transmit_call.perform();
|
||||
}
|
||||
|
||||
// ========== IrRfProxy (Infrared platform) ==========
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
|
||||
void IrRfProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"IR/RF Proxy '%s'\n"
|
||||
"IR Proxy '%s'\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
@@ -20,4 +80,54 @@ void IrRfProxy::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
void IrRfProxy::control(const infrared::InfraredCall &call) {
|
||||
uint32_t carrier = call.get_carrier_frequency().value_or(0);
|
||||
transmit_raw_timings(this->transmitter_, carrier, call);
|
||||
}
|
||||
|
||||
#endif // USE_IR_RF
|
||||
|
||||
// ========== RfProxy (Radio Frequency platform) ==========
|
||||
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
|
||||
void RfProxy::setup() {
|
||||
this->traits_.set_supports_transmitter(this->transmitter_ != nullptr);
|
||||
this->traits_.set_supports_receiver(this->receiver_ != nullptr);
|
||||
|
||||
// remote_transmitter/receiver always uses OOK (on-off keying)
|
||||
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
|
||||
|
||||
if (this->receiver_ != nullptr) {
|
||||
this->receiver_->register_listener(this);
|
||||
}
|
||||
}
|
||||
|
||||
void RfProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"RF Proxy '%s'\n"
|
||||
" Backend: remote_transmitter/receiver\n"
|
||||
" Supports Transmitter: %s\n"
|
||||
" Supports Receiver: %s",
|
||||
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
|
||||
const auto &traits = this->traits_;
|
||||
if (traits.get_frequency_min_hz() > 0) {
|
||||
if (traits.get_frequency_min_hz() == traits.get_frequency_max_hz()) {
|
||||
ESP_LOGCONFIG(TAG, " Frequency: %.3f MHz (fixed)", traits.get_frequency_min_hz() / 1e6f);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, " Frequency Range: %.3f - %.3f MHz", traits.get_frequency_min_hz() / 1e6f,
|
||||
traits.get_frequency_max_hz() / 1e6f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
|
||||
// RF: no IR carrier modulation
|
||||
transmit_raw_timings(this->transmitter_, 0, call);
|
||||
}
|
||||
|
||||
#endif // USE_RADIO_FREQUENCY
|
||||
|
||||
} // namespace esphome::ir_rf_proxy
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
// without following the normal breaking changes policy. Use at your own risk.
|
||||
// Once the API is considered stable, this warning will be removed.
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
#include "esphome/components/radio_frequency/radio_frequency.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::ir_rf_proxy {
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
|
||||
class IrRfProxy : public infrared::Infrared {
|
||||
public:
|
||||
@@ -26,8 +35,36 @@ class IrRfProxy : public infrared::Infrared {
|
||||
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
|
||||
|
||||
protected:
|
||||
void control(const infrared::InfraredCall &call) override;
|
||||
|
||||
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
|
||||
uint32_t frequency_khz_{0};
|
||||
};
|
||||
#endif // USE_IR_RF
|
||||
|
||||
#ifdef USE_RADIO_FREQUENCY
|
||||
/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend
|
||||
class RfProxy : public radio_frequency::RadioFrequency {
|
||||
public:
|
||||
RfProxy() = default;
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
/// Set the remote transmitter component
|
||||
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
|
||||
/// Set the remote receiver component
|
||||
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
|
||||
|
||||
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
|
||||
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
|
||||
|
||||
protected:
|
||||
void control(const radio_frequency::RadioFrequencyCall &call) override;
|
||||
|
||||
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
|
||||
remote_base::RemoteReceiverBase *receiver_{nullptr};
|
||||
};
|
||||
#endif // USE_RADIO_FREQUENCY
|
||||
|
||||
} // namespace esphome::ir_rf_proxy
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import radio_frequency, remote_receiver, remote_transmitter
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["radio_frequency"]
|
||||
|
||||
RfProxy = ir_rf_proxy_ns.class_("RfProxy", radio_frequency.RadioFrequency)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
radio_frequency.radio_frequency_schema(RfProxy).extend(
|
||||
{
|
||||
cv.Optional(CONF_FREQUENCY): cv.frequency,
|
||||
cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id(
|
||||
remote_receiver.RemoteReceiverComponent
|
||||
),
|
||||
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
|
||||
remote_transmitter.RemoteTransmitterComponent
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
"""Validate that RF transmitters have carrier duty set to 100%."""
|
||||
if CONF_REMOTE_TRANSMITTER_ID not in config:
|
||||
return
|
||||
|
||||
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
|
||||
full_config = fv.full_config.get()
|
||||
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
|
||||
transmitter_config = full_config.get_config_for_path(transmitter_path)
|
||||
|
||||
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
|
||||
if duty_percent is not None and duty_percent != 100:
|
||||
raise cv.Invalid(
|
||||
f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' "
|
||||
"set to 100% for RF transmission. Dedicated RF hardware handles modulation; "
|
||||
"applying a carrier duty cycle would corrupt the signal"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
"""Code generation for remote_base radio frequency platform."""
|
||||
var = await radio_frequency.new_radio_frequency(config)
|
||||
|
||||
if CONF_FREQUENCY in config:
|
||||
cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY])))
|
||||
|
||||
if CONF_REMOTE_TRANSMITTER_ID in config:
|
||||
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
|
||||
cg.add(var.set_transmitter(transmitter))
|
||||
|
||||
if CONF_REMOTE_RECEIVER_ID in config:
|
||||
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
|
||||
cg.add(var.set_receiver(receiver))
|
||||
@@ -58,6 +58,7 @@ COMPONENT_RTL87XX = "rtl87xx"
|
||||
FAMILY_BK7231N = "BK7231N"
|
||||
FAMILY_BK7231Q = "BK7231Q"
|
||||
FAMILY_BK7231T = "BK7231T"
|
||||
FAMILY_BK7238 = "BK7238"
|
||||
FAMILY_BK7251 = "BK7251"
|
||||
FAMILY_LN882H = "LN882H"
|
||||
FAMILY_RTL8710B = "RTL8710B"
|
||||
@@ -66,6 +67,7 @@ FAMILIES = [
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
FAMILY_BK7231T,
|
||||
FAMILY_BK7238,
|
||||
FAMILY_BK7251,
|
||||
FAMILY_LN882H,
|
||||
FAMILY_RTL8710B,
|
||||
@@ -75,6 +77,7 @@ FAMILY_FRIENDLY = {
|
||||
FAMILY_BK7231N: "BK7231N",
|
||||
FAMILY_BK7231Q: "BK7231Q",
|
||||
FAMILY_BK7231T: "BK7231T",
|
||||
FAMILY_BK7238: "BK7238",
|
||||
FAMILY_BK7251: "BK7251",
|
||||
FAMILY_LN882H: "LN882H",
|
||||
FAMILY_RTL8710B: "RTL8710B",
|
||||
@@ -84,6 +87,7 @@ FAMILY_COMPONENT = {
|
||||
FAMILY_BK7231N: COMPONENT_BK72XX,
|
||||
FAMILY_BK7231Q: COMPONENT_BK72XX,
|
||||
FAMILY_BK7231T: COMPONENT_BK72XX,
|
||||
FAMILY_BK7238: COMPONENT_BK72XX,
|
||||
FAMILY_BK7251: COMPONENT_BK72XX,
|
||||
FAMILY_LN882H: COMPONENT_LN882X,
|
||||
FAMILY_RTL8710B: COMPONENT_RTL87XX,
|
||||
|
||||
@@ -40,7 +40,11 @@ from esphome.const import (
|
||||
CONF_WHITE,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
@@ -405,7 +409,7 @@ async def setup_light_core_(light_var, config, output_var):
|
||||
|
||||
async def register_light(output_var, config):
|
||||
light_var = cg.new_Pvariable(config[CONF_ID], output_var)
|
||||
cg.add(cg.App.register_light(light_var))
|
||||
queue_entity_register("light", config)
|
||||
CORE.register_platform_component("light", light_var)
|
||||
await cg.register_component(light_var, config)
|
||||
await setup_light_core_(light_var, config, output_var)
|
||||
|
||||
@@ -13,7 +13,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -112,7 +116,7 @@ async def _setup_lock_core(var, config):
|
||||
async def register_lock(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_lock(var))
|
||||
queue_entity_register("lock", config)
|
||||
CORE.register_platform_component("lock", var)
|
||||
await _setup_lock_core(var, config)
|
||||
|
||||
|
||||
@@ -4,15 +4,25 @@ from esphome.components.binary_sensor import (
|
||||
new_binary_sensor,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_STATE
|
||||
|
||||
from ..defines import CONF_WIDGET
|
||||
from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, lv_pseudo_button_t
|
||||
from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant
|
||||
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t
|
||||
from ..widgets import Widget, get_widgets, wait_for_widgets
|
||||
|
||||
STATE_PRESSED = "PRESSED"
|
||||
STATE_CHECKED = "CHECKED"
|
||||
|
||||
BS_STATE = LvConstant(
|
||||
"LV_STATE_",
|
||||
STATE_PRESSED,
|
||||
STATE_CHECKED,
|
||||
)
|
||||
CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend(
|
||||
{
|
||||
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
|
||||
cv.Optional(CONF_STATE, default=STATE_PRESSED): BS_STATE.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -22,16 +32,23 @@ async def to_code(config):
|
||||
widget = await get_widgets(config, CONF_WIDGET)
|
||||
widget = widget[0]
|
||||
assert isinstance(widget, Widget)
|
||||
state = await BS_STATE.process(config[CONF_STATE])
|
||||
await wait_for_widgets()
|
||||
async with LambdaContext(EVENT_ARG) as pressed_ctx:
|
||||
pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
|
||||
is_pressed = str(state) == str(LV_STATE.PRESSED)
|
||||
test_expr = widget.is_pressed() if is_pressed else widget.is_checked()
|
||||
async with LambdaContext(EVENT_ARG) as test_ctx:
|
||||
test_ctx.add(sensor.publish_state(test_expr))
|
||||
async with LvContext() as ctx:
|
||||
ctx.add(sensor.publish_initial_state(widget.is_pressed()))
|
||||
ctx.add(sensor.publish_initial_state(test_expr))
|
||||
if is_pressed:
|
||||
events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED]
|
||||
widget.add_flag(LV_OBJ_FLAG.CLICKABLE)
|
||||
else:
|
||||
events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT]
|
||||
ctx.add(
|
||||
lvgl_static.add_event_cb(
|
||||
widget.obj,
|
||||
await pressed_ctx.get_lambda(),
|
||||
LV_EVENT.PRESSED,
|
||||
LV_EVENT.RELEASED,
|
||||
await test_ctx.get_lambda(),
|
||||
*events,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.core import CORE
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
inherit_property_from,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
@@ -262,7 +263,7 @@ async def setup_media_player_core_(var, config):
|
||||
async def register_media_player(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_media_player(var))
|
||||
queue_entity_register("media_player", config)
|
||||
CORE.register_platform_component("media_player", var)
|
||||
await setup_media_player_core_(var, config)
|
||||
|
||||
|
||||
@@ -352,7 +352,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) {
|
||||
ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
|
||||
return;
|
||||
}
|
||||
this->status_.target_temperature = std::round(target_temperature);
|
||||
this->status_.target_temperature = target_temperature;
|
||||
this->pending_updates_.set(UpdateFlag::TEMPERATURE);
|
||||
}
|
||||
|
||||
@@ -387,9 +387,9 @@ void MitsubishiCN105::apply_settings_() {
|
||||
if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) {
|
||||
payload[1] |= 0x04;
|
||||
if (this->use_temperature_encoding_b_) {
|
||||
payload[14] = static_cast<uint8_t>(this->status_.target_temperature * 2.0f + 128.0f);
|
||||
payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128);
|
||||
} else {
|
||||
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature);
|
||||
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,13 @@ namespace esphome::nextion {
|
||||
static const char *const TAG = "nextion.upload.arduino";
|
||||
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
|
||||
|
||||
// Timeout for display acknowledgment during TFT upload (ms).
|
||||
// A single value is used for all chunks; the happy path returns as soon as
|
||||
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
|
||||
// reports showed the previous 500ms steady-state value was too tight for
|
||||
// some firmware variants.
|
||||
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
|
||||
|
||||
// Followed guide
|
||||
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
|
||||
|
||||
@@ -80,14 +87,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
|
||||
recv_string.clear();
|
||||
this->write_array(buffer, buffer_size);
|
||||
App.feed_wdt();
|
||||
this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true);
|
||||
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
|
||||
this->content_length_ -= read_len;
|
||||
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
|
||||
ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
|
||||
EspClass::getFreeHeap());
|
||||
this->upload_first_chunk_sent_ = true;
|
||||
if (recv_string.empty()) {
|
||||
ESP_LOGW(TAG, "No response from display during upload");
|
||||
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
|
||||
allocator.deallocate(buffer, 4096);
|
||||
buffer = nullptr;
|
||||
return -1;
|
||||
|
||||
@@ -19,6 +19,13 @@ namespace esphome::nextion {
|
||||
static const char *const TAG = "nextion.upload.esp32";
|
||||
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
|
||||
|
||||
// Timeout for display acknowledgment during TFT upload (ms).
|
||||
// A single value is used for all chunks; the happy path returns as soon as
|
||||
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
|
||||
// reports showed the previous 500ms steady-state value was too tight for
|
||||
// some firmware variants.
|
||||
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
|
||||
|
||||
// Followed guide
|
||||
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
|
||||
|
||||
@@ -96,7 +103,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
|
||||
recv_string.clear();
|
||||
this->write_array(buffer, buffer_size);
|
||||
App.feed_wdt();
|
||||
this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true);
|
||||
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
|
||||
this->content_length_ -= read_len;
|
||||
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
|
||||
#ifdef USE_PSRAM
|
||||
@@ -109,7 +116,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
|
||||
#endif
|
||||
upload_first_chunk_sent_ = true;
|
||||
if (recv_string.empty()) {
|
||||
ESP_LOGW(TAG, "No response from display during upload");
|
||||
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
|
||||
allocator.deallocate(buffer, 4096);
|
||||
buffer = nullptr;
|
||||
return -1;
|
||||
|
||||
@@ -82,6 +82,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
setup_unit_of_measurement,
|
||||
@@ -301,7 +302,7 @@ async def register_number(
|
||||
):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_number(var))
|
||||
queue_entity_register("number", config)
|
||||
CORE.register_platform_component("number", var)
|
||||
await setup_number_core_(
|
||||
var, config, min_value=min_value, max_value=max_value, step=step
|
||||
|
||||
@@ -205,7 +205,7 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either:
|
||||
)
|
||||
|
||||
|
||||
def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
|
||||
def _process_remote_package(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Clone/update a git repo and load the YAML files listed in the package definition.
|
||||
|
||||
Returns ``{"packages": {<filename>: <loaded_yaml>, ...}}`` so the caller
|
||||
@@ -215,11 +215,10 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
|
||||
If loading fails after cloning, attempts a revert and retry in case
|
||||
a prior cached checkout is stale.
|
||||
"""
|
||||
actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH]
|
||||
repo_dir, revert = git.clone_or_update(
|
||||
url=config[CONF_URL],
|
||||
ref=config.get(CONF_REF),
|
||||
refresh=actual_refresh,
|
||||
refresh=config[CONF_REFRESH],
|
||||
domain=DOMAIN,
|
||||
username=config.get(CONF_USERNAME),
|
||||
password=config.get(CONF_PASSWORD),
|
||||
@@ -378,9 +377,8 @@ def _substitute_package_definition(
|
||||
Local package contents are left untouched — they will be substituted
|
||||
later during the main substitution pass.
|
||||
"""
|
||||
if isinstance(package_config, str) or (
|
||||
isinstance(package_config, dict) and is_remote_package(package_config)
|
||||
):
|
||||
|
||||
def do_substitute(package_config: dict | str) -> dict | str:
|
||||
# Collect undefined-variable errors (rather than raising strict) so the
|
||||
# path walked through a remote-package dict is preserved and the user
|
||||
# sees which field (url / path / ref / ...) referenced the undefined
|
||||
@@ -394,6 +392,22 @@ def _substitute_package_definition(
|
||||
errors=errors,
|
||||
)
|
||||
raise_first_undefined(errors, "package definition")
|
||||
return package_config
|
||||
|
||||
if isinstance(package_config, str):
|
||||
return do_substitute(package_config)
|
||||
|
||||
if isinstance(package_config, dict) and is_remote_package(package_config):
|
||||
# Mark vars as literal to avoid substituting variables in the vars block itself, since they are meant to be
|
||||
# passed as-is to the package YAML and may contain their own substitution expressions that should not
|
||||
# be prematurely evaluated here.
|
||||
if CONF_FILES in package_config:
|
||||
for file_def in package_config[CONF_FILES]:
|
||||
if isinstance(file_def, dict) and CONF_VARS in file_def:
|
||||
file_def[CONF_VARS] = yaml_util.make_literal(file_def[CONF_VARS])
|
||||
|
||||
package_config = do_substitute(package_config)
|
||||
|
||||
return package_config
|
||||
|
||||
|
||||
@@ -441,11 +455,9 @@ class _PackageProcessor:
|
||||
self,
|
||||
substitutions: UserDict,
|
||||
command_line_substitutions: dict[str, Any] | None,
|
||||
skip_update: bool,
|
||||
) -> None:
|
||||
self.substitutions = substitutions
|
||||
self.parent_context = UserDict(command_line_substitutions or {})
|
||||
self.skip_update = skip_update
|
||||
|
||||
def resolve_package(
|
||||
self,
|
||||
@@ -493,7 +505,7 @@ class _PackageProcessor:
|
||||
)
|
||||
|
||||
if is_remote_package(package_config):
|
||||
package_config = _process_remote_package(package_config, self.skip_update)
|
||||
package_config = _process_remote_package(package_config)
|
||||
return package_config
|
||||
|
||||
def collect_substitutions(self, package_config: dict) -> None:
|
||||
@@ -537,11 +549,10 @@ class _PackageProcessor:
|
||||
|
||||
|
||||
def do_packages_pass(
|
||||
config: dict,
|
||||
config: dict[str, Any],
|
||||
*,
|
||||
command_line_substitutions: dict[str, Any] | None = None,
|
||||
skip_update: bool = False,
|
||||
) -> dict:
|
||||
) -> dict[str, Any]:
|
||||
"""Load, validate, and flatten all packages in the config.
|
||||
|
||||
Returns the config with all packages loaded in-place (but not yet merged)
|
||||
@@ -556,9 +567,7 @@ def do_packages_pass(
|
||||
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
|
||||
)
|
||||
)
|
||||
processor = _PackageProcessor(
|
||||
substitutions, command_line_substitutions, skip_update
|
||||
)
|
||||
processor = _PackageProcessor(substitutions, command_line_substitutions)
|
||||
_update_substitutions_context(processor.parent_context, substitutions)
|
||||
|
||||
context_vars = push_context(
|
||||
|
||||
@@ -12,7 +12,7 @@ import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import setup_entity
|
||||
from esphome.core.entity_helpers import queue_entity_register, setup_entity
|
||||
from esphome.coroutine import CoroPriority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -55,8 +55,8 @@ async def register_radio_frequency(var: cg.Pvariable, config: ConfigType) -> Non
|
||||
"""Register a radio frequency device with the core."""
|
||||
cg.add_define("USE_RADIO_FREQUENCY")
|
||||
await cg.register_component(var, config)
|
||||
queue_entity_register("radio_frequency", config)
|
||||
await setup_radio_frequency_core_(var, config)
|
||||
cg.add(cg.App.register_radio_frequency(var))
|
||||
CORE.register_platform_component("radio_frequency", var)
|
||||
|
||||
|
||||
|
||||
@@ -129,6 +129,6 @@ async def to_code(config):
|
||||
async def sensor_template_publish_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
template_ = await cg.templatable(config[CONF_VALUE], args, cg.int32)
|
||||
template_ = await cg.templatable(config[CONF_VALUE], args, cg.int_)
|
||||
cg.add(var.set_value(template_))
|
||||
return var
|
||||
|
||||
@@ -93,7 +93,9 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_gain(config[CONF_GAIN]))
|
||||
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
if config.get(CONF_ON_FINISHED_PLAYBACK):
|
||||
cg.add_define("USE_RTTTL_FINISHED_PLAYBACK_CALLBACK")
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -424,7 +424,9 @@ void Rtttl::set_state_(State state) {
|
||||
// Clear loop_done when transitioning from `State::STOPPED` to any other state
|
||||
if (state == State::STOPPED) {
|
||||
this->disable_loop();
|
||||
#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
|
||||
this->on_finished_playback_callback_.call();
|
||||
#endif
|
||||
ESP_LOGD(TAG, "Playback finished");
|
||||
} else if (old_state == State::STOPPED) {
|
||||
this->enable_loop();
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_OUTPUT
|
||||
#include "esphome/components/output/float_output.h"
|
||||
@@ -45,9 +47,11 @@ class Rtttl : public Component {
|
||||
|
||||
bool is_playing() { return this->state_ != State::STOPPED; }
|
||||
|
||||
#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
|
||||
template<typename F> void add_on_finished_playback_callback(F &&callback) {
|
||||
this->on_finished_playback_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
inline uint16_t get_integer_() {
|
||||
@@ -106,8 +110,10 @@ class Rtttl : public Component {
|
||||
uint32_t samples_gap_{0};
|
||||
#endif // USE_SPEAKER
|
||||
|
||||
#ifdef USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
|
||||
/// The callback to call when playback is finished.
|
||||
CallbackManager<void()> on_finished_playback_callback_;
|
||||
#endif
|
||||
};
|
||||
|
||||
template<typename... Ts> class PlayAction : public Action<Ts...> {
|
||||
|
||||
@@ -76,8 +76,9 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if config.get(CONF_ON_SAFE_MODE):
|
||||
if on_safe_mode := config.get(CONF_ON_SAFE_MODE):
|
||||
cg.add_define("USE_SAFE_MODE_CALLBACK")
|
||||
cg.add_define("ESPHOME_SAFE_MODE_CALLBACK_COUNT", len(on_safe_mode))
|
||||
await automation.build_callback_automations(
|
||||
var, config, _CALLBACK_AUTOMATIONS
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ class SafeModeComponent final : public Component {
|
||||
// Larger objects at the end
|
||||
ESPPreferenceObject rtc_;
|
||||
#ifdef USE_SAFE_MODE_CALLBACK
|
||||
CallbackManager<void()> safe_mode_callback_{};
|
||||
StaticCallbackManager<ESPHOME_SAFE_MODE_CALLBACK_COUNT, void()> safe_mode_callback_{};
|
||||
#endif
|
||||
|
||||
static const uint32_t ENTER_SAFE_MODE_MAGIC =
|
||||
|
||||
@@ -19,7 +19,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass, TemplateArguments
|
||||
from esphome.cpp_types import global_ns
|
||||
|
||||
@@ -113,7 +117,7 @@ async def setup_select_core_(var, config, *, options: list[str]):
|
||||
async def register_select(var, config, *, options: list[str]):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_select(var))
|
||||
queue_entity_register("select", config)
|
||||
CORE.register_platform_component("select", var)
|
||||
await setup_select_core_(var, config, options=options)
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
setup_unit_of_measurement,
|
||||
@@ -982,7 +983,7 @@ async def setup_sensor_core_(var, config):
|
||||
async def register_sensor(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_sensor(var))
|
||||
queue_entity_register("sensor", config)
|
||||
CORE.register_platform_component("sensor", var)
|
||||
await setup_sensor_core_(var, config)
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -166,7 +167,7 @@ async def setup_switch_core_(var, config):
|
||||
async def register_switch(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_switch(var))
|
||||
queue_entity_register("switch", config)
|
||||
CORE.register_platform_component("switch", var)
|
||||
await setup_switch_core_(var, config)
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
CODEOWNERS = ["@mauritskorse"]
|
||||
@@ -122,7 +126,7 @@ async def register_text(
|
||||
):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_text(var))
|
||||
queue_entity_register("text", config)
|
||||
CORE.register_platform_component("text", var)
|
||||
await setup_text_core_(
|
||||
var, config, min_length=min_length, max_length=max_length, pattern=pattern
|
||||
|
||||
@@ -22,6 +22,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -221,7 +222,7 @@ async def setup_text_sensor_core_(var, config):
|
||||
async def register_text_sensor(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_text_sensor(var))
|
||||
queue_entity_register("text_sensor", config)
|
||||
CORE.register_platform_component("text_sensor", var)
|
||||
await setup_text_sensor_core_(var, config)
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -113,7 +114,7 @@ async def setup_update_core_(var, config):
|
||||
async def register_update(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_update(var))
|
||||
queue_entity_register("update", config)
|
||||
CORE.register_platform_component("update", var)
|
||||
await setup_update_core_(var, config)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
@@ -162,7 +163,7 @@ async def _setup_valve_core(var, config):
|
||||
async def register_valve(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_valve(var))
|
||||
queue_entity_register("valve", config)
|
||||
CORE.register_platform_component("valve", var)
|
||||
await _setup_valve_core(var, config)
|
||||
|
||||
|
||||
@@ -9,7 +9,11 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -90,7 +94,7 @@ async def register_water_heater(var: cg.Pvariable, config: ConfigType) -> cg.Pva
|
||||
|
||||
cg.add_define("USE_WATER_HEATER")
|
||||
|
||||
cg.add(cg.App.register_water_heater(var))
|
||||
queue_entity_register("water_heater", config)
|
||||
|
||||
CORE.register_platform_component("water_heater", var)
|
||||
await setup_water_heater_core_(var, config)
|
||||
|
||||
@@ -179,7 +179,10 @@ void WiFiComponent::wifi_pre_setup_() {
|
||||
#endif // USE_WIFI_AP
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
// cfg.nvs_enable = false;
|
||||
if (global_preferences->nvs_handle == 0) {
|
||||
ESP_LOGW(TAG, "starting wifi without nvs");
|
||||
cfg.nvs_enable = false;
|
||||
}
|
||||
err = esp_wifi_init(&cfg);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err));
|
||||
|
||||
@@ -75,6 +75,13 @@ SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor)
|
||||
SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch)
|
||||
NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number)
|
||||
|
||||
|
||||
def _validate_router_sleepy(config: ConfigType) -> ConfigType:
|
||||
if config.get(CONF_ROUTER) and config.get(CONF_SLEEPY):
|
||||
raise cv.Invalid("router and sleepy are mutually exclusive")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -82,10 +89,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MODEL, default=CORE.name): cv.All(
|
||||
cv.string, cv.Length(max=31)
|
||||
),
|
||||
cv.OnlyWith(CONF_ROUTER, "esp32", default=False): cv.All(
|
||||
cv.requires_component("esp32"),
|
||||
cv.boolean,
|
||||
),
|
||||
cv.Optional(CONF_ROUTER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_JOIN): cv.All(
|
||||
cv.requires_component("nrf52"),
|
||||
automation.validate_automation(single=True),
|
||||
@@ -113,6 +117,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_router_sleepy,
|
||||
zigbee_require_vfs_select,
|
||||
zigbee_set_core_data,
|
||||
cv.Any(
|
||||
|
||||
@@ -190,7 +190,9 @@ void ZigbeeComponent::setup() {
|
||||
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
|
||||
return;
|
||||
}
|
||||
#ifdef CONFIG_ZIGBEE_ROLE_END_DEVICE
|
||||
zigbee_configure_sleepy_behavior(this->sleepy_);
|
||||
#endif
|
||||
zigbee_enable();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,7 @@ from esphome.types import ConfigType
|
||||
from .const import (
|
||||
CONF_ON_JOIN,
|
||||
CONF_POWER_SOURCE,
|
||||
CONF_ROUTER,
|
||||
CONF_WIPE_ON_BOOT,
|
||||
KEY_ZIGBEE,
|
||||
POWER_SOURCE,
|
||||
@@ -160,7 +161,10 @@ zephyr_number = cv.Schema(
|
||||
async def zephyr_to_code(config: ConfigType) -> None:
|
||||
zephyr_add_prj_conf("ZIGBEE", True)
|
||||
zephyr_add_prj_conf("ZIGBEE_APP_UTILS", True)
|
||||
zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True)
|
||||
if config[CONF_ROUTER]:
|
||||
zephyr_add_prj_conf("ZIGBEE_ROLE_ROUTER", True)
|
||||
else:
|
||||
zephyr_add_prj_conf("ZIGBEE_ROLE_END_DEVICE", True)
|
||||
|
||||
zephyr_add_prj_conf("ZIGBEE_CHANNEL_SELECTION_MODE_MULTI", True)
|
||||
|
||||
|
||||
+6
-3
@@ -997,6 +997,8 @@ def validate_config(
|
||||
) -> Config:
|
||||
result = Config()
|
||||
|
||||
CORE.skip_external_update = skip_external_update
|
||||
|
||||
loader.clear_component_meta_finders()
|
||||
loader.install_custom_components_meta_finder()
|
||||
|
||||
@@ -1009,7 +1011,6 @@ def validate_config(
|
||||
config = do_packages_pass(
|
||||
config,
|
||||
command_line_substitutions=command_line_substitutions,
|
||||
skip_update=skip_external_update,
|
||||
)
|
||||
except vol.Invalid as err:
|
||||
result.update(config)
|
||||
@@ -1050,7 +1051,7 @@ def validate_config(
|
||||
|
||||
result.add_output_path([CONF_EXTERNAL_COMPONENTS], CONF_EXTERNAL_COMPONENTS)
|
||||
try:
|
||||
do_external_components_pass(config, skip_update=skip_external_update)
|
||||
do_external_components_pass(config)
|
||||
except vol.Invalid as err:
|
||||
result.update(config)
|
||||
result.add_error(err)
|
||||
@@ -1341,7 +1342,9 @@ def strip_default_ids(config):
|
||||
return config
|
||||
|
||||
|
||||
def read_config(command_line_substitutions, skip_external_update=False):
|
||||
def read_config(
|
||||
command_line_substitutions: dict[str, Any], skip_external_update: bool = False
|
||||
) -> Config | None:
|
||||
_LOGGER.info("Reading configuration %s...", CORE.config_path)
|
||||
try:
|
||||
res = load_config(command_line_substitutions, skip_external_update)
|
||||
|
||||
@@ -615,6 +615,9 @@ class EsphomeCore:
|
||||
self.address_cache: AddressCache | None = None
|
||||
# Cached config hash (computed lazily)
|
||||
self._config_hash: int | None = None
|
||||
# When True, skip network freshness checks for cached external files
|
||||
# (e.g. for `esphome logs`, where remote downloads aren't needed)
|
||||
self.skip_external_update: bool = False
|
||||
|
||||
def reset(self):
|
||||
from esphome.pins import PIN_SCHEMA_REGISTRY
|
||||
@@ -644,6 +647,7 @@ class EsphomeCore:
|
||||
self.current_component = None
|
||||
self.address_cache = None
|
||||
self._config_hash = None
|
||||
self.skip_external_update = False
|
||||
PIN_SCHEMA_REGISTRY.reset()
|
||||
|
||||
@contextmanager
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <sdkconfig.h> // for CONFIG_ESP_TASK_WDT_TIMEOUT_S (drives WDT_FEED_INTERVAL_MS)
|
||||
#endif
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
#include "esphome/core/device.h"
|
||||
#endif
|
||||
@@ -99,10 +103,19 @@ class Application {
|
||||
void set_current_component(Component *component) { this->current_component_ = component; }
|
||||
Component *get_current_component() { return this->current_component_; }
|
||||
|
||||
// Entity register methods (generated from entity_types.h)
|
||||
// Entity register methods (generated from entity_types.h).
|
||||
// Each entity type gets two overloads:
|
||||
// - register_<entity>(obj) — bare push_back
|
||||
// - register_<entity>(obj, name, hash, fields) — configure_entity_ + push_back
|
||||
// The 4-arg form lets codegen collapse `App.register_<entity>(obj); obj->configure_entity_(...);`
|
||||
// into a single call site, saving flash and a `main.cpp` line per entity.
|
||||
// NOLINTBEGIN(bugprone-macro-parentheses)
|
||||
#define ENTITY_TYPE_(type, singular, plural, count, upper) \
|
||||
void register_##singular(type *obj) { this->plural##_.push_back(obj); }
|
||||
void register_##singular(type *obj) { this->plural##_.push_back(obj); } \
|
||||
void register_##singular(type *obj, const char *name, uint32_t object_id_hash, uint32_t entity_fields) { \
|
||||
obj->configure_entity_(name, object_id_hash, entity_fields); \
|
||||
this->plural##_.push_back(obj); \
|
||||
}
|
||||
#define ENTITY_CONTROLLER_TYPE_(type, singular, plural, count, upper, callback) \
|
||||
ENTITY_TYPE_(type, singular, plural, count, upper)
|
||||
#include "esphome/core/entity_types.h"
|
||||
@@ -216,16 +229,30 @@ class Application {
|
||||
/// loops and scheduler items still feed after every op, so any op exceeding
|
||||
/// this threshold triggers a real feed naturally.
|
||||
/// Safety margins vs. platform watchdog timeouts:
|
||||
/// - ESP32 task WDT default (5 s): ~16x
|
||||
/// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change
|
||||
/// must keep comfortable margin here
|
||||
/// - ESP8266 HW WDT (~6 s): ~20x
|
||||
/// - BK72xx HW WDT (10 s): ~5x <-- platform override below
|
||||
/// - ESP32 task WDT (user-configurable): ~5x <-- auto-scaled below
|
||||
/// - ESP8266 soft WDT (~1.6 s): ~5x <-- floor case; any future change
|
||||
/// must keep comfortable margin here
|
||||
/// - ESP8266 HW WDT (~6 s): ~20x
|
||||
/// - BK72xx HW WDT (10 s): ~5x <-- platform override below
|
||||
#ifdef USE_BK72XX
|
||||
// BDK busy-waits 200us per WDT reload (sctrl_dpll_delay200us). LibreTiny
|
||||
// sets HW WDT to 10s; 2000ms keeps ~5x margin. See wdt_ctrl WCMD_RELOAD_PERIOD:
|
||||
// https://github.com/libretiny-eu/framework-beken-bdk/blob/44800e7451ea30fbcbd3bb6e905315de59349fee/beken378/driver/wdt/wdt.c#L75-L87
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 2000;
|
||||
#elif defined(USE_ESP32)
|
||||
// Auto-scale to 1/5 of the configured ESP32 task WDT timeout so the safety
|
||||
// margin stays constant when the user raises esp32.watchdog_timeout (default
|
||||
// 5 s → 1000 ms feed; 10 s → 2000 ms; 60 s → 12000 ms). The esp32 component
|
||||
// writes CONFIG_ESP_TASK_WDT_TIMEOUT_S into sdkconfig (range is validated
|
||||
// to ≥ 5 s in esp32/__init__.py), giving us the value at compile time.
|
||||
// esp_task_wdt_reset() takes a spinlock and walks the WDT task list, so
|
||||
// each call costs tens of microseconds; longer intervals materially reduce
|
||||
// the main-loop's wdt bucket. Component loops and scheduler items still
|
||||
// feed after every op, so any op exceeding this threshold triggers a real
|
||||
// feed naturally regardless of the rate-limit.
|
||||
static_assert(CONFIG_ESP_TASK_WDT_TIMEOUT_S >= 5,
|
||||
"CONFIG_ESP_TASK_WDT_TIMEOUT_S must be at least 5s for a safe WDT feed interval");
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = (CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000U) / 5U;
|
||||
#else
|
||||
static constexpr uint32_t WDT_FEED_INTERVAL_MS = 300;
|
||||
#endif
|
||||
@@ -350,12 +377,16 @@ class Application {
|
||||
|
||||
protected:
|
||||
friend Component;
|
||||
friend class Scheduler;
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
friend class runtime_stats::RuntimeStatsCollector;
|
||||
#endif
|
||||
friend void ::setup();
|
||||
friend void ::original_setup();
|
||||
|
||||
/// Freshen the cached loop component start time. Called by Scheduler before each dispatch.
|
||||
void set_loop_component_start_time_(uint32_t now) { this->loop_component_start_time_ = now; }
|
||||
|
||||
/// Walk all registered components looking for any whose component_state_
|
||||
/// has the given flag set. Used by Component::status_clear_*_slow_path_()
|
||||
/// (which is a friend) to decide whether to clear the corresponding bit on
|
||||
@@ -364,7 +395,11 @@ class Application {
|
||||
|
||||
/// Register a component, detecting loop() override at compile time.
|
||||
/// Uses HasLoopOverride<T> which handles ambiguous &T::loop from multiple inheritance.
|
||||
template<typename T> void register_component_(T *comp) {
|
||||
/// Optionally sets the component source index in the same call to avoid emitting
|
||||
/// a separate set_component_source_() line in generated code.
|
||||
template<typename T> void register_component_(T *comp, uint8_t source_index = 0) {
|
||||
if (source_index != 0)
|
||||
comp->set_component_source_(source_index);
|
||||
this->register_component_impl_(comp, HasLoopOverride<T>::value);
|
||||
}
|
||||
|
||||
|
||||
@@ -242,6 +242,10 @@ PROJECT_MAX_LENGTH = 127
|
||||
# Max board/model string length (must fit in single-byte varint for proto encoding)
|
||||
BOARD_MAX_LENGTH = 127
|
||||
|
||||
# Keep in sync with ESPHOME_COMMENT_SIZE_MAX in esphome/core/application.h
|
||||
# (C++ side includes the null terminator).
|
||||
COMMENT_MAX_LEN = 255
|
||||
|
||||
AREA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(Area),
|
||||
@@ -275,7 +279,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
|
||||
),
|
||||
cv.Optional(CONF_AREA): validate_area_config,
|
||||
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
|
||||
cv.Optional(CONF_COMMENT): cv.All(
|
||||
cv.string, cv.ByteLength(max=COMMENT_MAX_LEN)
|
||||
),
|
||||
cv.Required(CONF_BUILD_PATH): cv.string,
|
||||
cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema(
|
||||
{
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
#define USE_PREFERENCES_SYNC_EVERY_LOOP
|
||||
#define USE_QR_CODE
|
||||
#define USE_SAFE_MODE_CALLBACK
|
||||
#define ESPHOME_SAFE_MODE_CALLBACK_COUNT 1
|
||||
#define USE_SELECT
|
||||
#define USE_SENSOR
|
||||
#define USE_SENSOR_FILTER
|
||||
@@ -198,6 +199,7 @@
|
||||
#define USE_MQTT
|
||||
#define USE_MQTT_COVER_JSON
|
||||
#define USE_NETWORK
|
||||
#define USE_RTTTL_FINISHED_PLAYBACK_CALLBACK
|
||||
#define USE_RUNTIME_IMAGE_BMP
|
||||
#define USE_RUNTIME_IMAGE_PNG
|
||||
#define USE_RUNTIME_IMAGE_JPEG
|
||||
|
||||
@@ -238,6 +238,9 @@ class EntityBase {
|
||||
protected:
|
||||
friend void ::setup();
|
||||
friend void ::original_setup();
|
||||
// Application's register_<entity>(obj, name, hash, fields) overloads call configure_entity_
|
||||
// before push_back, so codegen can emit a single combined call per entity.
|
||||
friend class Application;
|
||||
|
||||
/// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags.
|
||||
/// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above.
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.core.config import (
|
||||
UNIT_OF_MEASUREMENT_MAX_LENGTH,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
|
||||
from esphome.cpp_types import App
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case
|
||||
from esphome.types import ConfigType, EntityMetadata
|
||||
@@ -52,6 +53,12 @@ _KEY_INTERNAL = "_entity_internal"
|
||||
_KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default"
|
||||
_KEY_ENTITY_CATEGORY = "_entity_category"
|
||||
|
||||
# Private config key for the App.register_<method> entry point.
|
||||
# When set, finalize_entity_strings() emits a single combined call
|
||||
# `App.register_<method>(var, name, hash, packed)` instead of separate
|
||||
# `App.register_<method>(var)` and `var->configure_entity_(...)` calls.
|
||||
_KEY_REGISTER_METHOD = "_entity_register_method"
|
||||
|
||||
# Maximum unique strings per category (8-bit index, 0 = not set)
|
||||
_MAX_DEVICE_CLASSES = 0xFF # 255
|
||||
_MAX_UNITS = 0xFF # 255
|
||||
@@ -271,11 +278,26 @@ def _describe_packed_flags(config: ConfigType, entity_category: int) -> str:
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def queue_entity_register(method_name: str, config: ConfigType) -> None:
|
||||
"""Defer ``App.register_<method_name>(var)`` emission to ``finalize_entity_strings``.
|
||||
|
||||
When the deferred call is emitted, it is folded with ``configure_entity_`` into
|
||||
a single ``App.register_<method_name>(var, name, hash, packed)`` call site,
|
||||
which removes one statement and one method dispatch per entity from the
|
||||
generated ``main.cpp``.
|
||||
"""
|
||||
config[_KEY_REGISTER_METHOD] = method_name
|
||||
|
||||
|
||||
def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
|
||||
"""Emit a single configure_entity_() call with name, hash, packed string indices, and flags.
|
||||
"""Emit the entity-registration / configure_entity_ tail.
|
||||
|
||||
Call this at the end of each component's setup function, after
|
||||
setup_entity() and any register_device_class/register_unit_of_measurement calls.
|
||||
|
||||
If queue_entity_register() was called for this entity, emits one combined call
|
||||
``App.register_<method>(var, name, hash, packed)``. Otherwise falls back to a
|
||||
standalone ``var->configure_entity_(name, hash, packed)``.
|
||||
"""
|
||||
entity_name = config[_KEY_ENTITY_NAME]
|
||||
object_id_hash = config[_KEY_OBJECT_ID_HASH]
|
||||
@@ -295,7 +317,13 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
|
||||
)
|
||||
# Build inline comment describing the packed flags for readability
|
||||
comment = _describe_packed_flags(config, entity_category)
|
||||
expr = var.configure_entity_(entity_name, object_id_hash, packed)
|
||||
register_method = config.get(_KEY_REGISTER_METHOD)
|
||||
if register_method is not None:
|
||||
expr = getattr(App, f"register_{register_method}")(
|
||||
var, entity_name, object_id_hash, packed
|
||||
)
|
||||
else:
|
||||
expr = var.configure_entity_(entity_name, object_id_hash, packed)
|
||||
if comment:
|
||||
add(RawStatement(f"{expr}; // {comment}"))
|
||||
else:
|
||||
|
||||
@@ -772,6 +772,8 @@ Scheduler::SchedulerItem *HOT Scheduler::pop_raw_locked_() {
|
||||
// Helper to execute a scheduler item
|
||||
uint32_t HOT Scheduler::execute_item_(SchedulerItem *item, uint32_t now) {
|
||||
App.set_current_component(item->component);
|
||||
// Freshen so callbacks reading App.get_loop_component_start_time() see this item's dispatch time.
|
||||
App.set_loop_component_start_time_(now);
|
||||
WarnIfComponentBlockingGuard guard{item->component, now};
|
||||
item->callback();
|
||||
uint32_t end = guard.finish();
|
||||
|
||||
@@ -197,9 +197,9 @@ async def register_component(var, config):
|
||||
)
|
||||
if name is not None:
|
||||
idx = register_component_source(name)
|
||||
add(var.set_component_source_(idx))
|
||||
|
||||
add(App.register_component_(var))
|
||||
add(App.register_component_(var, idx))
|
||||
else:
|
||||
add(App.register_component_(var))
|
||||
|
||||
# Collect C++ type for compile-time looping component count
|
||||
comp_entries = CORE.data.setdefault("looping_component_entries", [])
|
||||
|
||||
@@ -437,7 +437,11 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
|
||||
class EsphomeLogsHandler(EsphomePortCommandWebSocket):
|
||||
async def build_command(self, json_message: dict[str, Any]) -> list[str]:
|
||||
"""Build the command to run."""
|
||||
return await self.build_device_command(["logs"], json_message)
|
||||
cmd = await self.build_device_command(["logs"], json_message)
|
||||
if json_message.get("no_states"):
|
||||
cmd.append("--no-states")
|
||||
_LOGGER.debug("Built command: %s", cmd)
|
||||
return cmd
|
||||
|
||||
|
||||
class EsphomeRenameHandler(EsphomeCommandWebSocket):
|
||||
|
||||
@@ -81,7 +81,10 @@ def compute_local_file_dir(domain: str) -> Path:
|
||||
return base_directory
|
||||
|
||||
|
||||
def download_content(url: str, path: Path, timeout=NETWORK_TIMEOUT) -> bytes:
|
||||
def download_content(url: str, path: Path, timeout: int = NETWORK_TIMEOUT) -> bytes:
|
||||
if CORE.skip_external_update and path.exists():
|
||||
_LOGGER.debug("Skipping update for %s (refresh disabled)", url)
|
||||
return path.read_bytes()
|
||||
if not has_remote_file_changed(url, path):
|
||||
_LOGGER.debug("Remote file has not changed %s", url)
|
||||
return path.read_bytes()
|
||||
|
||||
+1
-3
@@ -150,9 +150,7 @@ def clone_or_update(
|
||||
raise
|
||||
|
||||
else:
|
||||
# Check refresh needed
|
||||
# Skip refresh if NEVER_REFRESH is specified
|
||||
if refresh == NEVER_REFRESH:
|
||||
if refresh == NEVER_REFRESH or CORE.skip_external_update:
|
||||
_LOGGER.debug("Skipping update for %s (refresh disabled)", key)
|
||||
return repo_dir, None
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ dependencies:
|
||||
esphome/esp-micro-speech-features:
|
||||
version: 1.2.3
|
||||
esphome/micro-decoder:
|
||||
version: 0.1.1
|
||||
version: 0.2.0
|
||||
esphome/micro-flac:
|
||||
version: 0.1.1
|
||||
esphome/micro-opus:
|
||||
|
||||
+76
-25
@@ -22,7 +22,6 @@ from esphome.helpers import (
|
||||
read_file,
|
||||
rmtree,
|
||||
walk_files,
|
||||
write_file,
|
||||
write_file_if_changed,
|
||||
)
|
||||
from esphome.storage_json import StorageJSON, storage_path
|
||||
@@ -171,6 +170,7 @@ VERSION_H_FORMAT = """\
|
||||
DEFINES_H_TARGET = "esphome/core/defines.h"
|
||||
VERSION_H_TARGET = "esphome/core/version.h"
|
||||
BUILD_INFO_DATA_H_TARGET = "esphome/core/build_info_data.h"
|
||||
BUILD_INFO_DATA_CPP_TARGET = "esphome/core/build_info_data.cpp"
|
||||
ENTITY_TYPES_H_TARGET = "esphome/core/entity_types.h"
|
||||
ESPHOME_README_TXT = """
|
||||
THIS DIRECTORY IS AUTO-GENERATED, DO NOT MODIFY
|
||||
@@ -209,13 +209,22 @@ def copy_src_tree():
|
||||
|
||||
source_files_copy = source_files_map.copy()
|
||||
ignore_targets = [
|
||||
Path(x) for x in (DEFINES_H_TARGET, VERSION_H_TARGET, BUILD_INFO_DATA_H_TARGET)
|
||||
Path(x)
|
||||
for x in (
|
||||
DEFINES_H_TARGET,
|
||||
VERSION_H_TARGET,
|
||||
BUILD_INFO_DATA_H_TARGET,
|
||||
BUILD_INFO_DATA_CPP_TARGET,
|
||||
)
|
||||
]
|
||||
for t in ignore_targets:
|
||||
source_files_copy.pop(t, None)
|
||||
|
||||
# Files to exclude from sources_changed tracking (generated files)
|
||||
generated_files = {Path("esphome/core/build_info_data.h")}
|
||||
generated_files = {
|
||||
Path("esphome/core/build_info_data.h"),
|
||||
Path("esphome/core/build_info_data.cpp"),
|
||||
}
|
||||
|
||||
sources_changed = False
|
||||
for fname in walk_files(CORE.relative_src_path("esphome")):
|
||||
@@ -268,12 +277,15 @@ def copy_src_tree():
|
||||
build_info_data_h_path = CORE.relative_src_path(
|
||||
"esphome", "core", "build_info_data.h"
|
||||
)
|
||||
build_info_data_cpp_path = CORE.relative_src_path(
|
||||
"esphome", "core", "build_info_data.cpp"
|
||||
)
|
||||
build_info_json_path = CORE.relative_build_path("build_info.json")
|
||||
config_hash, build_time, build_time_str, comment = get_build_info()
|
||||
|
||||
# Defensively force a rebuild if the build_info files don't exist, or if
|
||||
# there was a config change which didn't actually cause a source change
|
||||
if not build_info_data_h_path.exists():
|
||||
if not build_info_data_h_path.exists() or not build_info_data_cpp_path.exists():
|
||||
sources_changed = True
|
||||
else:
|
||||
try:
|
||||
@@ -288,13 +300,19 @@ def copy_src_tree():
|
||||
|
||||
# Write build_info header and JSON metadata
|
||||
if sources_changed:
|
||||
write_file(
|
||||
# write_file_if_changed avoids bumping mtime on identical content,
|
||||
# which is what makes the stable header actually isolate metadata churn.
|
||||
write_file_if_changed(
|
||||
build_info_data_h_path,
|
||||
generate_build_info_data_h(
|
||||
generate_build_info_data_h(),
|
||||
)
|
||||
write_file_if_changed(
|
||||
build_info_data_cpp_path,
|
||||
generate_build_info_data_cpp(
|
||||
config_hash, build_time, build_time_str, comment
|
||||
),
|
||||
)
|
||||
write_file(
|
||||
write_file_if_changed(
|
||||
build_info_json_path,
|
||||
json.dumps(
|
||||
{
|
||||
@@ -345,27 +363,60 @@ def get_build_info() -> tuple[int, int, str, str]:
|
||||
return config_hash, build_time, build_time_str, comment
|
||||
|
||||
|
||||
def generate_build_info_data_h(
|
||||
config_hash: int, build_time: int, build_time_str: str, comment: str
|
||||
) -> str:
|
||||
"""Generate build_info_data.h header with config hash, build time, and comment."""
|
||||
# cpp_string_escape returns '"escaped"', slice off the quotes since template has them
|
||||
escaped_comment = cpp_string_escape(comment)[1:-1]
|
||||
# +1 for null terminator
|
||||
comment_size = len(comment) + 1
|
||||
return f"""#pragma once
|
||||
// Auto-generated build_info data
|
||||
#define ESPHOME_CONFIG_HASH 0x{config_hash:08x}U // NOLINT
|
||||
#define ESPHOME_BUILD_TIME {build_time} // NOLINT
|
||||
#define ESPHOME_COMMENT_SIZE {comment_size} // NOLINT
|
||||
def generate_build_info_data_h() -> str:
|
||||
"""Generate stable declarations for build info provided by generated C++."""
|
||||
return """#pragma once
|
||||
// Auto-generated build_info declarations
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
static const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
|
||||
static const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}";
|
||||
#else
|
||||
static const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
|
||||
static const char ESPHOME_COMMENT_STR[] = "{escaped_comment}";
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
extern const uint32_t ESPHOME_CONFIG_HASH;
|
||||
extern const time_t ESPHOME_BUILD_TIME;
|
||||
extern const size_t ESPHOME_COMMENT_SIZE;
|
||||
#ifdef USE_ESP8266
|
||||
extern const char ESPHOME_BUILD_TIME_STR[] PROGMEM;
|
||||
extern const char ESPHOME_COMMENT_STR[] PROGMEM;
|
||||
#else
|
||||
extern const char ESPHOME_BUILD_TIME_STR[];
|
||||
extern const char ESPHOME_COMMENT_STR[];
|
||||
#endif
|
||||
} // namespace esphome
|
||||
"""
|
||||
|
||||
|
||||
def generate_build_info_data_cpp(
|
||||
config_hash: int, build_time: int, build_time_str: str, comment: str
|
||||
) -> str:
|
||||
"""Generate build_info_data.cpp with config hash, build time, and comment."""
|
||||
from esphome.core.config import COMMENT_MAX_LEN
|
||||
|
||||
# Defense-in-depth clamp; errors="ignore" drops a partial trailing UTF-8
|
||||
# sequence so the literal never decodes to a truncated codepoint.
|
||||
encoded = comment.encode("utf-8")[:COMMENT_MAX_LEN]
|
||||
comment = encoded.decode("utf-8", errors="ignore")
|
||||
# cpp_string_escape wraps in quotes; strip them since the template has them.
|
||||
escaped_comment = cpp_string_escape(comment)[1:-1]
|
||||
comment_size = len(comment.encode("utf-8")) + 1 # +1 for NUL
|
||||
return f"""// Auto-generated build_info data
|
||||
#include "esphome/core/build_info_data.h"
|
||||
|
||||
namespace esphome {{
|
||||
const uint32_t ESPHOME_CONFIG_HASH = 0x{config_hash:08x}U; // NOLINT
|
||||
const time_t ESPHOME_BUILD_TIME = {build_time}; // NOLINT
|
||||
const size_t ESPHOME_COMMENT_SIZE = {comment_size}; // NOLINT
|
||||
#ifdef USE_ESP8266
|
||||
const char ESPHOME_BUILD_TIME_STR[] PROGMEM = "{build_time_str}";
|
||||
const char ESPHOME_COMMENT_STR[] PROGMEM = "{escaped_comment}";
|
||||
#else
|
||||
const char ESPHOME_BUILD_TIME_STR[] = "{build_time_str}";
|
||||
const char ESPHOME_COMMENT_STR[] = "{escaped_comment}";
|
||||
#endif
|
||||
}} // namespace esphome
|
||||
"""
|
||||
|
||||
|
||||
|
||||
+10
-1
@@ -113,6 +113,15 @@ def make_data_base(
|
||||
return value
|
||||
|
||||
|
||||
def make_literal(value: Any) -> ESPLiteralValue | Any:
|
||||
"""Wrap a value in an ESPLiteralValue object."""
|
||||
try:
|
||||
return add_class_to_obj(value, ESPLiteralValue)
|
||||
except TypeError:
|
||||
# Adding class failed, ignore error
|
||||
return value
|
||||
|
||||
|
||||
def add_context(value: Any, context_vars: dict[str, Any] | None) -> Any:
|
||||
"""Tags a list/string/dict value with context vars that must be applied to it and its children
|
||||
during the substitution pass. If no vars are given, no tagging is done.
|
||||
@@ -525,7 +534,7 @@ class ESPHomeLoaderMixin:
|
||||
obj = self.construct_sequence(node)
|
||||
elif isinstance(node, yaml.MappingNode):
|
||||
obj = self.construct_mapping(node)
|
||||
return add_class_to_obj(obj, ESPLiteralValue)
|
||||
return make_literal(obj)
|
||||
|
||||
@_add_data_ref
|
||||
def construct_extend(self, node: yaml.Node) -> Extend:
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
cryptography==46.0.7
|
||||
cryptography==47.0.0
|
||||
voluptuous==0.16.0
|
||||
PyYAML==6.0.3
|
||||
paho-mqtt==1.6.1
|
||||
@@ -6,13 +6,13 @@ colorama==0.4.6
|
||||
icmplib==3.0.4
|
||||
tornado==6.5.5
|
||||
tzlocal==5.3.1 # from time
|
||||
tzdata>=2026.1 # from time
|
||||
tzdata>=2026.2 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.2.0
|
||||
click==8.3.3
|
||||
esphome-dashboard==20260408.1
|
||||
aioesphomeapi==44.21.0
|
||||
esphome-dashboard==20260425.0
|
||||
aioesphomeapi==44.22.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.19.1 # dashboard_import
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pylint==4.0.5
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.11 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.15.12 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
|
||||
+44
-2
@@ -511,6 +511,40 @@ def lint_no_std_string_view(fname, match):
|
||||
)
|
||||
|
||||
|
||||
@lint_re_check(
|
||||
r"(?:"
|
||||
# `from esphome.components.const import ...`
|
||||
r"from\s+esphome\.components\.const\s+import"
|
||||
r"|"
|
||||
# `import esphome.components.const` (with optional `as` alias)
|
||||
r"import\s+esphome\.components\.const\b"
|
||||
r"|"
|
||||
# `from esphome.components import [(] ... const ... [)]`
|
||||
# Handles parenthesized + multiline import lists by allowing newlines inside
|
||||
# the parens via [^)]*. Single-line form falls back to the [^#\n]* branch.
|
||||
r"from\s+esphome\.components\s+import\s*"
|
||||
r"(?:\([^)]*\bconst\b[^)]*\)|(?:[^#\n]*[\s,])?\bconst\b)"
|
||||
r")",
|
||||
include=["*.py"],
|
||||
exclude=[
|
||||
"esphome/components/*",
|
||||
"tests/*",
|
||||
"script/ci-custom.py",
|
||||
],
|
||||
)
|
||||
def lint_no_components_const_outside_components(fname, match):
|
||||
return (
|
||||
f"Constants in {highlight('esphome/components/const/__init__.py')} are intended "
|
||||
f"to be shared only between components in {highlight('esphome/components/')}. "
|
||||
f"Code outside this folder must not import from "
|
||||
f"{highlight('esphome.components.const')}.\n"
|
||||
f"For core code (used outside {highlight('esphome/components/')}), define the "
|
||||
f"constant in {highlight('esphome/const.py')} instead. When adding a new "
|
||||
f"{highlight('CONF_')} constant there, bump {highlight('CONST_PY_MAX_CONF')} "
|
||||
f"in this file accordingly (see {highlight('lint_const_py_frozen')})."
|
||||
)
|
||||
|
||||
|
||||
@lint_post_check
|
||||
def lint_constants_usage():
|
||||
errs = []
|
||||
@@ -837,7 +871,16 @@ def lint_no_std_to_string(fname, match):
|
||||
f"{highlight('std::to_string()')} (including unqualified {highlight('to_string()')}) "
|
||||
f"allocates heap memory. On long-running embedded devices, repeated heap allocations "
|
||||
f"fragment memory over time.\n"
|
||||
f"Please use {highlight('snprintf()')} with a stack buffer instead.\n"
|
||||
f"\n"
|
||||
f"For plain integer formatting, prefer the dedicated helpers in helpers.h over "
|
||||
f"{highlight('snprintf()')} — they avoid pulling in printf formatting code and are "
|
||||
f"smaller and faster:\n"
|
||||
f" int8_t: {highlight('int8_to_str(buf, val)')} (buf >= 5 bytes)\n"
|
||||
f" uint8_t/uint16_t/uint32_t: {highlight('uint32_to_str(buf, val)')} (buf = UINT32_MAX_STR_SIZE; smaller types auto-widen)\n"
|
||||
f"Example: {highlight('char buf[UINT32_MAX_STR_SIZE]; uint32_to_str(buf, value);')}\n"
|
||||
f"For sensor values, use {highlight('value_accuracy_to_buf()')} from helpers.h.\n"
|
||||
f"\n"
|
||||
f"Otherwise use {highlight('snprintf()')} with a stack buffer.\n"
|
||||
f"\n"
|
||||
f"Buffer sizes and format specifiers (sizes include sign and null terminator):\n"
|
||||
f" uint8_t: 4 chars - %u (or PRIu8)\n"
|
||||
@@ -851,7 +894,6 @@ def lint_no_std_to_string(fname, match):
|
||||
f" float/double: 24 chars - %.8g (15 digits + sign + decimal + e+XXX)\n"
|
||||
f" 317 chars - %f (for DBL_MAX: 309 int digits + decimal + 6 frac + sign)\n"
|
||||
f"\n"
|
||||
f"For sensor values, use value_accuracy_to_buf() from helpers.h.\n"
|
||||
f'Example: char buf[11]; snprintf(buf, sizeof(buf), "%" PRIu32, value);\n'
|
||||
f"(If strictly necessary, add `{highlight('// NOLINT')}` to the end of the line)"
|
||||
)
|
||||
|
||||
@@ -402,8 +402,11 @@ def should_run_benchmarks(branch: str | None = None) -> bool:
|
||||
Benchmarks run when any of the following conditions are met:
|
||||
|
||||
1. Core C++ files changed (esphome/core/*)
|
||||
2. A directly changed component has benchmark files (no dependency expansion)
|
||||
3. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py,
|
||||
2. The host platform changed (esphome/components/host/*) — benchmarks
|
||||
are built and run on the host platform, so its implementations of
|
||||
``millis()``/``micros()``/etc. affect every benchmark
|
||||
3. A directly changed component has benchmark files (no dependency expansion)
|
||||
4. Benchmark infrastructure changed (tests/benchmarks/*, script/cpp_benchmark.py,
|
||||
script/build_helpers.py, script/setup_codspeed_lib.py)
|
||||
|
||||
Unlike unit tests, benchmarks do NOT expand to dependent components.
|
||||
@@ -420,6 +423,10 @@ def should_run_benchmarks(branch: str | None = None) -> bool:
|
||||
if core_changed(files):
|
||||
return True
|
||||
|
||||
# Host platform supplies the runtime that benchmarks execute on
|
||||
if any(f.startswith("esphome/components/host/") for f in files):
|
||||
return True
|
||||
|
||||
# Check if benchmark infrastructure changed
|
||||
if any(
|
||||
f.startswith("tests/benchmarks/") or f in BENCHMARK_INFRASTRUCTURE_FILES
|
||||
|
||||
@@ -31,7 +31,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main):
|
||||
)
|
||||
|
||||
# Then
|
||||
assert 'bs_1->configure_entity_("test bs1",' in main_cpp
|
||||
assert 'App.register_binary_sensor(bs_1, "test bs1",' in main_cpp
|
||||
assert "bs_1->set_pin(" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_button_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||
|
||||
# Then
|
||||
assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp
|
||||
assert 'App.register_button(wol_1, "wol_test_1",' in main_cpp
|
||||
assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ def test_deep_sleep_setup(generate_main):
|
||||
in main_cpp
|
||||
)
|
||||
assert "new(deepsleep) deep_sleep::DeepSleepComponent();" in main_cpp
|
||||
assert "App.register_component_(deepsleep);" in main_cpp
|
||||
assert "App.register_component_(deepsleep, " in main_cpp
|
||||
|
||||
|
||||
def test_deep_sleep_sleep_duration(generate_main):
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
variant: esp32
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
variant: esp32
|
||||
framework:
|
||||
type: arduino
|
||||
@@ -16,6 +16,7 @@ from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_IGNORE_PIN_VALIDATION_ERROR,
|
||||
CONF_NUMBER,
|
||||
KEY_NATIVE_IDF,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
@@ -232,3 +233,41 @@ def test_execute_from_psram_disabled_sdkconfig(
|
||||
assert "CONFIG_SPIRAM_FETCH_INSTRUCTIONS" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_RODATA" not in sdkconfig
|
||||
assert "CONFIG_SPIRAM_XIP_FROM_PSRAM" not in sdkconfig
|
||||
|
||||
|
||||
def test_platformio_idf_enables_reproducible_build(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test PlatformIO ESP-IDF builds enable reproducible app metadata."""
|
||||
generate_main(component_config_path("reproducible_build.yaml"))
|
||||
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True
|
||||
|
||||
|
||||
def test_platformio_arduino_enables_reproducible_build(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test PlatformIO Arduino builds enable reproducible app metadata."""
|
||||
generate_main(component_config_path("reproducible_build_arduino.yaml"))
|
||||
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True
|
||||
|
||||
|
||||
def test_native_idf_enables_reproducible_build(
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test native ESP-IDF builds enable reproducible app metadata."""
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
|
||||
CORE.config_path = component_config_path("reproducible_build.yaml")
|
||||
CORE.config = read_config({})
|
||||
CORE.data[KEY_NATIVE_IDF] = True
|
||||
generate_cpp_contents(CORE.config)
|
||||
|
||||
sdkconfig = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
|
||||
assert sdkconfig.get("CONFIG_APP_REPRODUCIBLE_BUILD") is True
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for the external_components skip_update functionality."""
|
||||
"""Tests for the external_components skip-update behavior driven by CORE.skip_external_update."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -12,25 +12,17 @@ from esphome.const import (
|
||||
CONF_URL,
|
||||
TYPE_GIT,
|
||||
)
|
||||
from esphome.core import CORE, TimePeriodSeconds
|
||||
|
||||
|
||||
def test_external_components_skip_update_true(
|
||||
tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock
|
||||
) -> None:
|
||||
"""Test that external components don't update when skip_update=True."""
|
||||
# Create a components directory structure
|
||||
def _make_config(tmp_path: Path) -> dict[str, Any]:
|
||||
components_dir = tmp_path / "components"
|
||||
components_dir.mkdir()
|
||||
|
||||
# Create a test component
|
||||
test_component_dir = components_dir / "test_component"
|
||||
test_component_dir.mkdir()
|
||||
(test_component_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
# Set up mock to return our tmp_path
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
|
||||
config: dict[str, Any] = {
|
||||
return {
|
||||
CONF_EXTERNAL_COMPONENTS: [
|
||||
{
|
||||
CONF_SOURCE: {
|
||||
@@ -43,92 +35,37 @@ def test_external_components_skip_update_true(
|
||||
]
|
||||
}
|
||||
|
||||
# Call with skip_update=True
|
||||
do_external_components_pass(config, skip_update=True)
|
||||
|
||||
# Verify clone_or_update was called with NEVER_REFRESH
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
from esphome import git
|
||||
|
||||
assert call_args.kwargs["refresh"] == git.NEVER_REFRESH
|
||||
|
||||
|
||||
def test_external_components_skip_update_false(
|
||||
tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock
|
||||
def test_external_components_skip_update_via_core_flag(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
) -> None:
|
||||
"""Test that external components update when skip_update=False."""
|
||||
# Create a components directory structure
|
||||
components_dir = tmp_path / "components"
|
||||
components_dir.mkdir()
|
||||
|
||||
# Create a test component
|
||||
test_component_dir = components_dir / "test_component"
|
||||
test_component_dir.mkdir()
|
||||
(test_component_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
# Set up mock to return our tmp_path
|
||||
"""When CORE.skip_external_update is True, refresh is still passed through;
|
||||
git.clone_or_update itself short-circuits the actual fetch."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
CORE.skip_external_update = True
|
||||
do_external_components_pass(config)
|
||||
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
# Refresh is passed through verbatim — the global flag is enforced inside git.clone_or_update.
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
|
||||
def test_external_components_normal_refresh(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_install_meta_finder: MagicMock,
|
||||
) -> None:
|
||||
"""When CORE.skip_external_update is False, the configured refresh value is used."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
config = _make_config(tmp_path)
|
||||
|
||||
config: dict[str, Any] = {
|
||||
CONF_EXTERNAL_COMPONENTS: [
|
||||
{
|
||||
CONF_SOURCE: {
|
||||
"type": TYPE_GIT,
|
||||
CONF_URL: "https://github.com/test/components",
|
||||
},
|
||||
CONF_REFRESH: "1d",
|
||||
"components": "all",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Call with skip_update=False
|
||||
do_external_components_pass(config, skip_update=False)
|
||||
|
||||
# Verify clone_or_update was called with actual refresh value
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
from esphome.core import TimePeriodSeconds
|
||||
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
|
||||
def test_external_components_default_no_skip(
|
||||
tmp_path: Path, mock_clone_or_update: MagicMock, mock_install_meta_finder: MagicMock
|
||||
) -> None:
|
||||
"""Test that external components update by default when skip_update not specified."""
|
||||
# Create a components directory structure
|
||||
components_dir = tmp_path / "components"
|
||||
components_dir.mkdir()
|
||||
|
||||
# Create a test component
|
||||
test_component_dir = components_dir / "test_component"
|
||||
test_component_dir.mkdir()
|
||||
(test_component_dir / "__init__.py").write_text("# Test component")
|
||||
|
||||
# Set up mock to return our tmp_path
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
|
||||
config: dict[str, Any] = {
|
||||
CONF_EXTERNAL_COMPONENTS: [
|
||||
{
|
||||
CONF_SOURCE: {
|
||||
"type": TYPE_GIT,
|
||||
CONF_URL: "https://github.com/test/components",
|
||||
},
|
||||
CONF_REFRESH: "1d",
|
||||
"components": "all",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
# Call without skip_update parameter
|
||||
do_external_components_pass(config)
|
||||
|
||||
# Verify clone_or_update was called with actual refresh value
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
from esphome.core import TimePeriodSeconds
|
||||
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
@@ -8,12 +8,22 @@ INTERNAL_BIT = 1 << 24
|
||||
|
||||
|
||||
def extract_packed_value(main_cpp: str, var_name: str) -> int:
|
||||
"""Extract the third (packed) argument from a configure_entity_ call."""
|
||||
pattern = (
|
||||
rf"{re.escape(var_name)}->configure_entity_\("
|
||||
"""Extract the packed-fields argument from the entity's configure call.
|
||||
|
||||
Matches both legacy form ``var->configure_entity_(name, hash, packed)`` and the
|
||||
combined form ``App.register_<entity>(var, name, hash, packed)``.
|
||||
"""
|
||||
escaped_var = re.escape(var_name)
|
||||
legacy_pattern = (
|
||||
rf"{escaped_var}->configure_entity_\("
|
||||
r'"(?:\\.|[^"\\])*"'
|
||||
r",\s*\w+,\s*(\d+)\)"
|
||||
)
|
||||
match = re.search(pattern, main_cpp)
|
||||
assert match, f"configure_entity_ call not found for {var_name}"
|
||||
combined_pattern = (
|
||||
rf"App\.register_\w+\(\s*{escaped_var}\s*,\s*"
|
||||
r'"(?:\\.|[^"\\])*"'
|
||||
r",\s*\w+,\s*(\d+)\)"
|
||||
)
|
||||
match = re.search(combined_pattern, main_cpp) or re.search(legacy_pattern, main_cpp)
|
||||
assert match, f"configure call not found for {var_name}"
|
||||
return int(match.group(1))
|
||||
|
||||
@@ -7,10 +7,12 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from PIL import Image as PILImage
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.image import (
|
||||
CONF_ALPHA_CHANNEL,
|
||||
CONF_INVERT_ALPHA,
|
||||
CONF_OPAQUE,
|
||||
CONF_TRANSPARENCY,
|
||||
@@ -411,3 +413,70 @@ async def test_svg_with_mm_dimensions_succeeds(
|
||||
assert 30 < height < 50, (
|
||||
f"Height should be around 39 pixels for 10mm at 100dpi, got {height}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rgb565_alpha_animation_layout_per_frame(
|
||||
tmp_path: Path,
|
||||
mock_progmem_array: MagicMock,
|
||||
) -> None:
|
||||
"""RGB565+alpha animations must store each frame as a self-contained
|
||||
[RGB plane | alpha plane] block. Animation::update_data_start_ steps frames
|
||||
with a single per-frame stride, so any cross-frame layout (all RGB then all
|
||||
alpha) makes the C++ alpha read land in the next frame's RGB bytes — that
|
||||
was the regression behind issue #15999.
|
||||
"""
|
||||
# Build a 2-frame APNG where each frame is a solid color with a known
|
||||
# alpha. APNG preserves full RGBA per pixel (GIF only has 1-bit alpha so
|
||||
# round-tripping mid-range alpha values does not work). Frame 0 is fully
|
||||
# opaque red, frame 1 is fully transparent blue.
|
||||
width = 4
|
||||
height = 3
|
||||
frame0 = PILImage.new("RGBA", (width, height), (255, 0, 0, 0xFF))
|
||||
frame1 = PILImage.new("RGBA", (width, height), (0, 0, 255, 0x00))
|
||||
apng_path = tmp_path / "anim.png"
|
||||
frame0.save(
|
||||
apng_path,
|
||||
format="PNG",
|
||||
save_all=True,
|
||||
append_images=[frame1],
|
||||
duration=100,
|
||||
loop=0,
|
||||
)
|
||||
|
||||
config = {
|
||||
CONF_FILE: str(apng_path),
|
||||
CONF_TYPE: "RGB565",
|
||||
CONF_TRANSPARENCY: CONF_ALPHA_CHANNEL,
|
||||
CONF_DITHER: "NONE",
|
||||
CONF_INVERT_ALPHA: False,
|
||||
CONF_RAW_DATA_ID: "test_raw_data_id",
|
||||
}
|
||||
|
||||
_, _, _, _, _, frame_count = await write_image(config, all_frames=True)
|
||||
assert frame_count == 2
|
||||
|
||||
# Recover the bytes handed to progmem_array. Signature is (id_, rhs).
|
||||
_, raw_data = mock_progmem_array.call_args.args
|
||||
data = [int(x) for x in raw_data]
|
||||
|
||||
rgb_size = width * height * 2
|
||||
alpha_size = width * height
|
||||
frame_size = rgb_size + alpha_size
|
||||
assert len(data) == frame_size * frame_count, (
|
||||
"RGB565+alpha animation buffer must be (RGB + alpha) per frame, not "
|
||||
"all RGB followed by all alpha"
|
||||
)
|
||||
|
||||
# Frame 0: RGB plane is red, alpha plane is 0xFF. Frame 1: alpha plane is
|
||||
# 0x00. If the layout regresses to [all RGB | all alpha], the alpha bytes
|
||||
# would all land at the tail of the buffer and the per-frame slices below
|
||||
# would point at RGB565 noise instead.
|
||||
frame0_alpha = data[rgb_size : rgb_size + alpha_size]
|
||||
frame1_alpha = data[frame_size + rgb_size : frame_size + rgb_size + alpha_size]
|
||||
assert all(a == 0xFF for a in frame0_alpha), (
|
||||
f"Frame 0 alpha plane should be opaque, got {frame0_alpha}"
|
||||
)
|
||||
assert all(a == 0x00 for a in frame1_alpha), (
|
||||
f"Frame 1 alpha plane should be transparent, got {frame1_alpha}"
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
|
||||
assert "global_web_server_base" in main_cpp
|
||||
|
||||
# Check component is registered
|
||||
assert "App.register_component_(web_server_webserverotacomponent_id)" in main_cpp
|
||||
assert "App.register_component_(web_server_webserverotacomponent_id" in main_cpp
|
||||
|
||||
|
||||
def test_web_server_ota_with_callbacks(generate_main: Callable[[str], str]) -> None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests for the packages component skip_update functionality."""
|
||||
"""Tests for the packages skip-update behavior driven by CORE.skip_external_update."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -6,24 +6,12 @@ from unittest.mock import MagicMock
|
||||
|
||||
from esphome.components.packages import do_packages_pass
|
||||
from esphome.const import CONF_FILES, CONF_PACKAGES, CONF_REFRESH, CONF_URL
|
||||
from esphome.core import CORE, TimePeriodSeconds
|
||||
from esphome.util import OrderedDict
|
||||
|
||||
|
||||
def test_packages_skip_update_true(
|
||||
tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock
|
||||
) -> None:
|
||||
"""Test that packages don't update when skip_update=True."""
|
||||
# Set up mock to return our tmp_path
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
|
||||
# Create the test yaml file
|
||||
test_file = tmp_path / "test.yaml"
|
||||
test_file.write_text("sensor: []")
|
||||
|
||||
# Set mock_load_yaml to return some valid config
|
||||
mock_load_yaml.return_value = OrderedDict({"sensor": []})
|
||||
|
||||
config: dict[str, Any] = {
|
||||
def _make_config() -> dict[str, Any]:
|
||||
return {
|
||||
CONF_PACKAGES: {
|
||||
"test_package": {
|
||||
CONF_URL: "https://github.com/test/repo",
|
||||
@@ -33,82 +21,47 @@ def test_packages_skip_update_true(
|
||||
}
|
||||
}
|
||||
|
||||
# Call with skip_update=True
|
||||
do_packages_pass(config, skip_update=True)
|
||||
|
||||
# Verify clone_or_update was called with NEVER_REFRESH
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
from esphome import git
|
||||
|
||||
assert call_args.kwargs["refresh"] == git.NEVER_REFRESH
|
||||
|
||||
|
||||
def test_packages_skip_update_false(
|
||||
tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock
|
||||
def test_packages_skip_update_via_core_flag(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_load_yaml: MagicMock,
|
||||
) -> None:
|
||||
"""Test that packages update when skip_update=False."""
|
||||
# Set up mock to return our tmp_path
|
||||
"""When CORE.skip_external_update is True, refresh is still passed through;
|
||||
git.clone_or_update itself short-circuits the actual fetch."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
|
||||
# Create the test yaml file
|
||||
test_file = tmp_path / "test.yaml"
|
||||
test_file.write_text("sensor: []")
|
||||
|
||||
# Set mock_load_yaml to return some valid config
|
||||
mock_load_yaml.return_value = OrderedDict({"sensor": []})
|
||||
|
||||
config: dict[str, Any] = {
|
||||
CONF_PACKAGES: {
|
||||
"test_package": {
|
||||
CONF_URL: "https://github.com/test/repo",
|
||||
CONF_FILES: ["test.yaml"],
|
||||
CONF_REFRESH: "1d",
|
||||
}
|
||||
}
|
||||
}
|
||||
config = _make_config()
|
||||
|
||||
CORE.skip_external_update = True
|
||||
do_packages_pass(config, command_line_substitutions={})
|
||||
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
# Refresh is passed through verbatim — the global flag is enforced inside git.clone_or_update.
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
|
||||
def test_packages_normal_refresh(
|
||||
tmp_path: Path,
|
||||
mock_clone_or_update: MagicMock,
|
||||
mock_load_yaml: MagicMock,
|
||||
) -> None:
|
||||
"""When CORE.skip_external_update is False, the configured refresh value is used."""
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
|
||||
test_file = tmp_path / "test.yaml"
|
||||
test_file.write_text("sensor: []")
|
||||
mock_load_yaml.return_value = OrderedDict({"sensor": []})
|
||||
|
||||
config = _make_config()
|
||||
|
||||
# Call with skip_update=False (default)
|
||||
do_packages_pass(config, command_line_substitutions={}, skip_update=False)
|
||||
|
||||
# Verify clone_or_update was called with actual refresh value
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
from esphome.core import TimePeriodSeconds
|
||||
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
|
||||
def test_packages_default_no_skip(
|
||||
tmp_path: Path, mock_clone_or_update: MagicMock, mock_load_yaml: MagicMock
|
||||
) -> None:
|
||||
"""Test that packages update by default when skip_update not specified."""
|
||||
# Set up mock to return our tmp_path
|
||||
mock_clone_or_update.return_value = (tmp_path, None)
|
||||
|
||||
# Create the test yaml file
|
||||
test_file = tmp_path / "test.yaml"
|
||||
test_file.write_text("sensor: []")
|
||||
|
||||
# Set mock_load_yaml to return some valid config
|
||||
mock_load_yaml.return_value = OrderedDict({"sensor": []})
|
||||
|
||||
config: dict[str, Any] = {
|
||||
CONF_PACKAGES: {
|
||||
"test_package": {
|
||||
CONF_URL: "https://github.com/test/repo",
|
||||
CONF_FILES: ["test.yaml"],
|
||||
CONF_REFRESH: "1d",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Call without skip_update parameter
|
||||
do_packages_pass(config, command_line_substitutions={})
|
||||
|
||||
# Verify clone_or_update was called with actual refresh value
|
||||
mock_clone_or_update.assert_called_once()
|
||||
call_args = mock_clone_or_update.call_args
|
||||
from esphome.core import TimePeriodSeconds
|
||||
|
||||
assert call_args.kwargs["refresh"] == TimePeriodSeconds(days=1)
|
||||
|
||||
@@ -1491,3 +1491,133 @@ def test_substitute_package_definition_includes_source_location(tmp_path: Path)
|
||||
line, col = int(match.group(1)), int(match.group(2))
|
||||
assert line == 2, f"expected 1-based line 2, got {line} (err={err!r})"
|
||||
assert col >= 1, f"expected 1-based column ≥ 1, got {col} (err={err!r})"
|
||||
|
||||
|
||||
def test_substitute_package_definition_vars_preserved_literally() -> None:
|
||||
"""``vars:`` blocks in remote-package files are not substituted prematurely.
|
||||
|
||||
Variable references inside ``vars:`` may resolve to substitutions
|
||||
contributed by sibling packages that have not yet been loaded, so they
|
||||
must be passed through untouched and resolved later by the package YAML.
|
||||
"""
|
||||
pkg = {
|
||||
CONF_URL: "https://github.com/esphome/non-existant-repo",
|
||||
CONF_REF: "main",
|
||||
CONF_FILES: [
|
||||
{
|
||||
CONF_PATH: "common/somefile.yaml",
|
||||
CONF_VARS: {"pin": "${PIN}"},
|
||||
},
|
||||
],
|
||||
}
|
||||
# Note: PIN is intentionally NOT in the context — it is meant to
|
||||
# be resolved later, when the package YAML is processed.
|
||||
result = _substitute_package_definition(pkg, ContextVars())
|
||||
|
||||
assert result[CONF_FILES][0][CONF_VARS] == {"pin": "${PIN}"}
|
||||
|
||||
|
||||
def test_substitute_package_definition_other_fields_still_substituted() -> None:
|
||||
"""Marking ``vars:`` literal does not stop substitution of url/ref/path."""
|
||||
ctx = ContextVars({"branch": "release", "org": "esphome"})
|
||||
pkg = {
|
||||
CONF_URL: "https://github.com/${org}/firmware",
|
||||
CONF_REF: "${branch}",
|
||||
CONF_FILES: [
|
||||
{
|
||||
CONF_PATH: "common/sensor.yaml",
|
||||
CONF_VARS: {"pin": "${PIN}"},
|
||||
},
|
||||
],
|
||||
}
|
||||
result = _substitute_package_definition(pkg, ctx)
|
||||
|
||||
assert result[CONF_URL] == "https://github.com/esphome/firmware"
|
||||
assert result[CONF_REF] == "release"
|
||||
# vars passed through unchanged
|
||||
assert result[CONF_FILES][0][CONF_VARS] == {"pin": "${PIN}"}
|
||||
|
||||
|
||||
def test_substitute_package_definition_without_vars_unaffected() -> None:
|
||||
"""Files entries without a ``vars:`` block continue to work."""
|
||||
ctx = ContextVars({"branch": "main"})
|
||||
pkg = {
|
||||
CONF_URL: "https://github.com/esphome/firmware",
|
||||
CONF_REF: "${branch}",
|
||||
CONF_FILES: [
|
||||
{CONF_PATH: "file1.yaml"},
|
||||
"file2.yaml",
|
||||
],
|
||||
}
|
||||
result = _substitute_package_definition(pkg, ctx)
|
||||
|
||||
assert result[CONF_REF] == "main"
|
||||
assert result[CONF_FILES][0] == {CONF_PATH: "file1.yaml"}
|
||||
assert result[CONF_FILES][1] == "file2.yaml"
|
||||
|
||||
|
||||
@patch("esphome.yaml_util.load_yaml")
|
||||
@patch("pathlib.Path.is_file")
|
||||
@patch("esphome.git.clone_or_update")
|
||||
def test_remote_package_vars_resolved_against_sibling_package_substitutions(
|
||||
mock_clone_or_update, mock_is_file, mock_load_yaml
|
||||
) -> None:
|
||||
"""A ``vars:`` reference in one remote package can resolve to a
|
||||
substitution defined in a sibling remote package.
|
||||
|
||||
A higher-priority package declares ``substitutions:`` (e.g. ``SENSOR_PIN: 5``) and a
|
||||
lower-priority package's ``files: -> vars:`` references that substitution.
|
||||
Because packages are processed highest-priority first and ``vars:`` is now
|
||||
preserved literally during package-definition processing, the substitution
|
||||
is resolved correctly when the package YAML itself is loaded.
|
||||
"""
|
||||
mock_clone_or_update.return_value = (Path("/tmp/noexists"), MagicMock())
|
||||
mock_is_file.return_value = True
|
||||
|
||||
# Two YAML files mocked from the "remote" repo:
|
||||
# - platform.yaml exports a substitution ``SENSOR_PIN``
|
||||
# - sensor.yaml uses ``${pin}`` (which is bound from ``vars:`` to
|
||||
# ``${SENSOR_PIN}`` and resolved against the merged substitutions).
|
||||
mock_load_yaml.side_effect = [
|
||||
# Order matches reverse-priority traversal (highest priority first).
|
||||
OrderedDict(
|
||||
{
|
||||
CONF_SUBSTITUTIONS: {"SENSOR_PIN": "GPIO5"},
|
||||
}
|
||||
),
|
||||
OrderedDict(
|
||||
{
|
||||
CONF_SENSOR: [
|
||||
{
|
||||
CONF_PLATFORM: TEST_SENSOR_PLATFORM_1,
|
||||
CONF_NAME: TEST_SENSOR_NAME_1,
|
||||
"pin": "${pin}",
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
]
|
||||
|
||||
config = {
|
||||
CONF_PACKAGES: {
|
||||
"special_sensor": {
|
||||
CONF_URL: "https://github.com/esphome/non-existant-repo",
|
||||
CONF_FILES: [
|
||||
{
|
||||
CONF_PATH: "sensor.yaml",
|
||||
CONF_VARS: {"pin": "${SENSOR_PIN}"},
|
||||
},
|
||||
],
|
||||
CONF_REFRESH: "1d",
|
||||
},
|
||||
"platform": {
|
||||
CONF_URL: "https://github.com/esphome/non-existant-repo",
|
||||
CONF_FILES: ["platform.yaml"],
|
||||
CONF_REFRESH: "1d",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
actual = packages_pass(config)
|
||||
|
||||
assert actual[CONF_SENSOR][0]["pin"] == "GPIO5"
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_text_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert 'it_1->configure_entity_("test 1 text",' in main_cpp
|
||||
assert 'App.register_text(it_1, "test 1 text",' in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_internal_set(generate_main):
|
||||
|
||||
@@ -28,9 +28,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp
|
||||
assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp
|
||||
assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp
|
||||
assert 'App.register_text_sensor(ts_1, "Template Text Sensor 1",' in main_cpp
|
||||
assert 'App.register_text_sensor(ts_2, "Template Text Sensor 2",' in main_cpp
|
||||
assert 'App.register_text_sensor(ts_3, "Template Text Sensor 3",' in main_cpp
|
||||
|
||||
|
||||
def test_text_sensor_config_value_internal_set(generate_main):
|
||||
|
||||
@@ -16,10 +16,19 @@ binary_sensor:
|
||||
platform: template
|
||||
- id: left_sensor
|
||||
platform: template
|
||||
- platform: lvgl
|
||||
name: Button A pressed
|
||||
widget: button_a
|
||||
state: pressed
|
||||
- platform: lvgl
|
||||
name: Button A checked
|
||||
widget: button_a
|
||||
state: checked
|
||||
- platform: lvgl
|
||||
id: button_checker
|
||||
name: LVGL button
|
||||
widget: button_button
|
||||
state: checked
|
||||
on_state:
|
||||
then:
|
||||
- lvgl.checkbox.update:
|
||||
@@ -29,6 +38,12 @@ binary_sensor:
|
||||
auto y = x; // block inlining of one line return
|
||||
return y;
|
||||
|
||||
- platform: lvgl
|
||||
id: button_presser
|
||||
name: Button pressed
|
||||
widget: button_button
|
||||
state: pressed
|
||||
|
||||
lvgl:
|
||||
id: lvgl_id
|
||||
rotation: 90
|
||||
|
||||
@@ -341,6 +341,17 @@ TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB4, 0x00, 0xC5));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ApplySettingsHalfDegreeTemperatureEncodedB) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
ctx.sut.use_temperature_encoding_b_ = true;
|
||||
ctx.sut.set_target_temperature(26.5f);
|
||||
ctx.sut.apply_settings();
|
||||
|
||||
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB5, 0x00, 0xC4));
|
||||
}
|
||||
|
||||
TEST(MitsubishiCN105Tests, ApplyModeCool) {
|
||||
auto ctx = TestContext{};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user