mirror of
https://github.com/esphome/esphome.git
synced 2026-06-26 19:15:29 +00:00
Compare commits
26 Commits
app-loop-o
...
improv-ser
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab233e6d83 | ||
|
|
e87e78c544 | ||
|
|
0f25d91e68 | ||
|
|
8dbdcfc128 | ||
|
|
8950afc3c4 | ||
|
|
04d067196d | ||
|
|
502c010465 | ||
|
|
180105bb4b | ||
|
|
4c0dfb0e0d | ||
|
|
df987a7ffb | ||
|
|
c8d4420408 | ||
|
|
b084fa4490 | ||
|
|
68625a1b76 | ||
|
|
dc57969afd | ||
|
|
f092e619d8 | ||
|
|
58f6ad2d0c | ||
|
|
bc33260c61 | ||
|
|
4cab262ef8 | ||
|
|
9ad820c921 | ||
|
|
4f8feb86f0 | ||
|
|
b5ccd55f4e | ||
|
|
a437b3086b | ||
|
|
c27f9e512b | ||
|
|
f62972c2c6 | ||
|
|
f36efbc762 | ||
|
|
9caf9ee023 |
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
@@ -1372,7 +1377,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 +1399,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 +1477,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 +1486,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) {
|
||||
@@ -1539,6 +1551,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 +1625,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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
|
||||
|
||||
@@ -1729,6 +1729,10 @@ async def to_code(config):
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
# Strip volatile build path/time metadata from PlatformIO-managed
|
||||
# ESP-IDF builds so equivalent projects can produce reproducible
|
||||
# outputs and downstream tooling can safely reuse artifacts.
|
||||
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
|
||||
|
||||
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
|
||||
# exception class overhead. See throw_stubs.cpp for implementation.
|
||||
|
||||
@@ -84,8 +84,7 @@ static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-
|
||||
static StackType_t
|
||||
loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void __attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
loop_task(void *pv_params) {
|
||||
void loop_task(void *pv_params) {
|
||||
setup();
|
||||
while (true) {
|
||||
App.loop();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
@@ -77,8 +72,7 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
|
||||
|
||||
void setup();
|
||||
void loop();
|
||||
int __attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
main() {
|
||||
int main() {
|
||||
// Install signal handlers for graceful shutdown (flushes preferences to disk)
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
|
||||
@@ -17,6 +17,7 @@ void ImprovSerialComponent::setup() {
|
||||
global_improv_serial_component = this;
|
||||
#ifdef USE_ESP32
|
||||
this->uart_num_ = logger::global_logger->get_uart_num();
|
||||
this->uart_selection_ = logger::global_logger->get_uart();
|
||||
#elif defined(USE_ARDUINO)
|
||||
this->hw_serial_ = logger::global_logger->get_hw_serial();
|
||||
#endif
|
||||
@@ -29,7 +30,8 @@ void ImprovSerialComponent::setup() {
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::loop() {
|
||||
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (this->last_read_byte_ && (now - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
|
||||
this->last_read_byte_ = 0;
|
||||
this->rx_buffer_.clear();
|
||||
ESP_LOGV(TAG, "Timeout");
|
||||
@@ -38,7 +40,7 @@ void ImprovSerialComponent::loop() {
|
||||
auto byte = this->read_byte_();
|
||||
while (byte.has_value()) {
|
||||
if (this->parse_improv_serial_byte_(byte.value())) {
|
||||
this->last_read_byte_ = millis();
|
||||
this->last_read_byte_ = now;
|
||||
} else {
|
||||
this->last_read_byte_ = 0;
|
||||
this->rx_buffer_.clear();
|
||||
@@ -62,55 +64,6 @@ void ImprovSerialComponent::loop() {
|
||||
|
||||
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
|
||||
|
||||
optional<uint8_t> ImprovSerialComponent::read_byte_() {
|
||||
optional<uint8_t> byte;
|
||||
uint8_t data = 0;
|
||||
#ifdef USE_ESP32
|
||||
switch (logger::global_logger->get_uart()) {
|
||||
case logger::UART_SELECTION_UART0:
|
||||
case logger::UART_SELECTION_UART1:
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case logger::UART_SELECTION_UART2:
|
||||
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32C61 &&
|
||||
// !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
|
||||
if (this->uart_num_ >= 0) {
|
||||
size_t available;
|
||||
uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
if (available) {
|
||||
uart_read_bytes(this->uart_num_, &data, 1, 0);
|
||||
byte = data;
|
||||
}
|
||||
}
|
||||
break;
|
||||
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
|
||||
case logger::UART_SELECTION_USB_CDC:
|
||||
if (esp_usb_console_available_for_read()) {
|
||||
esp_usb_console_read_buf((char *) &data, 1);
|
||||
byte = data;
|
||||
}
|
||||
break;
|
||||
#endif // USE_LOGGER_USB_CDC
|
||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
|
||||
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
|
||||
byte = data;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif // USE_LOGGER_USB_SERIAL_JTAG
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#elif defined(USE_ARDUINO)
|
||||
if (this->hw_serial_->available()) {
|
||||
this->hw_serial_->readBytes(&data, 1);
|
||||
byte = data;
|
||||
}
|
||||
#endif
|
||||
return byte;
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
|
||||
// First, set length field
|
||||
this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
|
||||
@@ -134,7 +87,7 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
|
||||
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
|
||||
|
||||
#ifdef USE_ESP32
|
||||
switch (logger::global_logger->get_uart()) {
|
||||
switch (this->uart_selection_) {
|
||||
case logger::UART_SELECTION_UART0:
|
||||
case logger::UART_SELECTION_UART1:
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/improv_base/improv_base.h"
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
@@ -66,7 +67,53 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
|
||||
std::vector<uint8_t> build_rpc_settings_response_(improv::Command command);
|
||||
std::vector<uint8_t> build_version_info_();
|
||||
|
||||
optional<uint8_t> read_byte_();
|
||||
ESPHOME_ALWAYS_INLINE optional<uint8_t> read_byte_() {
|
||||
optional<uint8_t> byte;
|
||||
uint8_t data = 0;
|
||||
#ifdef USE_ESP32
|
||||
switch (this->uart_selection_) {
|
||||
case logger::UART_SELECTION_UART0:
|
||||
case logger::UART_SELECTION_UART1:
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
case logger::UART_SELECTION_UART2:
|
||||
#endif
|
||||
if (this->uart_num_ >= 0) {
|
||||
size_t available;
|
||||
uart_get_buffered_data_len(this->uart_num_, &available);
|
||||
if (available) {
|
||||
uart_read_bytes(this->uart_num_, &data, 1, 0);
|
||||
byte = data;
|
||||
}
|
||||
}
|
||||
break;
|
||||
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
|
||||
case logger::UART_SELECTION_USB_CDC:
|
||||
if (esp_usb_console_available_for_read()) {
|
||||
esp_usb_console_read_buf((char *) &data, 1);
|
||||
byte = data;
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
|
||||
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
|
||||
byte = data;
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
#elif defined(USE_ARDUINO)
|
||||
if (this->hw_serial_->available()) {
|
||||
this->hw_serial_->readBytes(&data, 1);
|
||||
byte = data;
|
||||
}
|
||||
#endif
|
||||
return byte;
|
||||
}
|
||||
void write_data_(const uint8_t *data = nullptr, size_t size = 0);
|
||||
|
||||
uint8_t tx_header_[TX_BUFFER_SIZE] = {
|
||||
@@ -86,6 +133,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
uart_port_t uart_num_;
|
||||
logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0};
|
||||
#elif defined(USE_ARDUINO)
|
||||
Stream *hw_serial_{nullptr};
|
||||
#endif
|
||||
|
||||
@@ -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
|
||||
|
||||
68
esphome/components/ir_rf_proxy/radio_frequency.py
Normal file
68
esphome/components/ir_rf_proxy/radio_frequency.py
Normal file
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -193,7 +193,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
# sendspin-cpp library
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.0")
|
||||
esp32.add_idf_component(name="sendspin/sendspin-cpp", ref="0.3.1")
|
||||
|
||||
cg.add_define("USE_SENDSPIN", True) # for MDNS
|
||||
|
||||
|
||||
@@ -95,8 +95,7 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
int __attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
main() {
|
||||
int main() {
|
||||
setup();
|
||||
while (true) {
|
||||
loop();
|
||||
|
||||
@@ -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
|
||||
@@ -216,16 +220,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
|
||||
@@ -541,8 +559,7 @@ inline ESPHOME_ALWAYS_INLINE Application::ComponentPhaseGuard::ComponentPhaseGua
|
||||
this->app_.in_loop_ = true;
|
||||
}
|
||||
|
||||
inline void ESPHOME_ALWAYS_INLINE __attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
|
||||
Application::loop() {
|
||||
inline void ESPHOME_ALWAYS_INLINE Application::loop() {
|
||||
#ifdef USE_RUNTIME_STATS
|
||||
// Capture the start of the active (non-sleeping) portion of this iteration.
|
||||
// Used to derive main-loop overhead = active time − Σ(component time) −
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -136,6 +136,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
|
||||
@@ -185,6 +186,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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -92,6 +92,6 @@ dependencies:
|
||||
esp32async/asynctcp:
|
||||
version: 3.4.91
|
||||
sendspin/sendspin-cpp:
|
||||
version: 0.3.0
|
||||
version: 0.3.1
|
||||
lvgl/lvgl:
|
||||
version: 9.5.0
|
||||
|
||||
@@ -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
|
||||
@@ -45,7 +44,7 @@ void setup() {
|
||||
App.setup();
|
||||
}
|
||||
|
||||
void __attribute__((optimize("O2"))) loop() {
|
||||
void loop() {
|
||||
App.loop();
|
||||
}
|
||||
""",
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -837,7 +837,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 +860,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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
variant: esp32
|
||||
framework:
|
||||
type: esp-idf
|
||||
@@ -232,3 +232,14 @@ 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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{};
|
||||
|
||||
|
||||
18
tests/components/radio_frequency/common-rx.yaml
Normal file
18
tests/components/radio_frequency/common-rx.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
remote_receiver:
|
||||
id: rf_receiver
|
||||
pin: ${rx_pin}
|
||||
|
||||
# Test radio_frequency platform with receiver
|
||||
radio_frequency:
|
||||
# RF 900MHz receiver
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_900_rx
|
||||
name: "RF 900 Receiver"
|
||||
frequency: 900 MHz
|
||||
remote_receiver_id: rf_receiver
|
||||
|
||||
# RF receiver (no frequency specified)
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_rx
|
||||
name: "RF Receiver"
|
||||
remote_receiver_id: rf_receiver
|
||||
19
tests/components/radio_frequency/common-tx.yaml
Normal file
19
tests/components/radio_frequency/common-tx.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
remote_transmitter:
|
||||
id: rf_transmitter
|
||||
pin: ${tx_pin}
|
||||
carrier_duty_percent: 100%
|
||||
|
||||
# Test radio_frequency platform with transmitter
|
||||
radio_frequency:
|
||||
# RF 433MHz transmitter
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_433_tx
|
||||
name: "RF 433 Transmitter"
|
||||
frequency: 433 MHz
|
||||
remote_transmitter_id: rf_transmitter
|
||||
|
||||
# RF transmitter (no frequency specified)
|
||||
- platform: ir_rf_proxy
|
||||
id: rf_tx
|
||||
name: "RF Transmitter"
|
||||
remote_transmitter_id: rf_transmitter
|
||||
7
tests/components/radio_frequency/common.yaml
Normal file
7
tests/components/radio_frequency/common.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
network:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
api:
|
||||
8
tests/components/radio_frequency/test.bk72xx-ard.yaml
Normal file
8
tests/components/radio_frequency/test.bk72xx-ard.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
8
tests/components/radio_frequency/test.esp32-idf.yaml
Normal file
8
tests/components/radio_frequency/test.esp32-idf.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
8
tests/components/radio_frequency/test.esp8266-ard.yaml
Normal file
8
tests/components/radio_frequency/test.esp8266-ard.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
8
tests/components/radio_frequency/test.rp2040-ard.yaml
Normal file
8
tests/components/radio_frequency/test.rp2040-ard.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
substitutions:
|
||||
tx_pin: GPIO4
|
||||
rx_pin: GPIO5
|
||||
|
||||
packages:
|
||||
common: !include common.yaml
|
||||
rx: !include common-rx.yaml
|
||||
tx: !include common-tx.yaml
|
||||
@@ -29,3 +29,8 @@ output:
|
||||
|
||||
rtttl:
|
||||
output: rtttl_output
|
||||
on_finished_playback:
|
||||
- then:
|
||||
- logger.log: "Playback finished 1"
|
||||
- then:
|
||||
- logger.log: "Playback finished 2"
|
||||
|
||||
@@ -1744,6 +1744,64 @@ def test_proc_on_exit_skips_when_already_closed() -> None:
|
||||
handler.close.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_esphome_logs_handler_appends_no_states_when_set() -> None:
|
||||
"""Test --no-states is appended when no_states is truthy in the message."""
|
||||
handler = Mock(spec=web_server.EsphomeLogsHandler)
|
||||
handler.build_device_command = AsyncMock(
|
||||
return_value=["esphome", "logs", "device.yaml", "--device", "OTA"]
|
||||
)
|
||||
|
||||
json_message = {
|
||||
"configuration": "device.yaml",
|
||||
"port": "OTA",
|
||||
"no_states": True,
|
||||
}
|
||||
cmd = await web_server.EsphomeLogsHandler.build_command(handler, json_message)
|
||||
|
||||
assert cmd == [
|
||||
"esphome",
|
||||
"logs",
|
||||
"device.yaml",
|
||||
"--device",
|
||||
"OTA",
|
||||
"--no-states",
|
||||
]
|
||||
handler.build_device_command.assert_awaited_once_with(["logs"], json_message)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_esphome_logs_handler_omits_no_states_when_missing() -> None:
|
||||
"""Test --no-states is not added when no_states is absent from the message."""
|
||||
handler = Mock(spec=web_server.EsphomeLogsHandler)
|
||||
handler.build_device_command = AsyncMock(
|
||||
return_value=["esphome", "logs", "device.yaml", "--device", "OTA"]
|
||||
)
|
||||
|
||||
cmd = await web_server.EsphomeLogsHandler.build_command(
|
||||
handler, {"configuration": "device.yaml", "port": "OTA"}
|
||||
)
|
||||
|
||||
assert "--no-states" not in cmd
|
||||
assert cmd == ["esphome", "logs", "device.yaml", "--device", "OTA"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_esphome_logs_handler_omits_no_states_when_false() -> None:
|
||||
"""Test --no-states is not added when no_states is explicitly False."""
|
||||
handler = Mock(spec=web_server.EsphomeLogsHandler)
|
||||
handler.build_device_command = AsyncMock(
|
||||
return_value=["esphome", "logs", "device.yaml", "--device", "OTA"]
|
||||
)
|
||||
|
||||
cmd = await web_server.EsphomeLogsHandler.build_command(
|
||||
handler,
|
||||
{"configuration": "device.yaml", "port": "OTA", "no_states": False},
|
||||
)
|
||||
|
||||
assert "--no-states" not in cmd
|
||||
|
||||
|
||||
def _make_auth_handler(auth_header: str | None = None) -> Mock:
|
||||
"""Create a mock handler with the given Authorization header."""
|
||||
handler = Mock()
|
||||
|
||||
@@ -1842,6 +1842,22 @@ def test_should_run_benchmarks_core_header_change() -> None:
|
||||
assert determine_jobs.should_run_benchmarks() is True
|
||||
|
||||
|
||||
def test_should_run_benchmarks_host_platform_change() -> None:
|
||||
"""Test benchmarks trigger on host platform changes.
|
||||
|
||||
Benchmarks build and run on the host platform, so changes to its
|
||||
millis()/micros()/etc. implementations affect every benchmark.
|
||||
"""
|
||||
for host_file in [
|
||||
"esphome/components/host/core.cpp",
|
||||
"esphome/components/host/__init__.py",
|
||||
]:
|
||||
with patch.object(determine_jobs, "changed_files", return_value=[host_file]):
|
||||
assert determine_jobs.should_run_benchmarks() is True, (
|
||||
f"Expected benchmarks to run for {host_file}"
|
||||
)
|
||||
|
||||
|
||||
def test_should_run_benchmarks_benchmark_infra_change() -> None:
|
||||
"""Test benchmarks trigger on benchmark infrastructure changes."""
|
||||
for infra_file in [
|
||||
|
||||
@@ -236,3 +236,49 @@ def test_download_content_with_network_error_no_cache_fails(
|
||||
|
||||
with pytest.raises(Invalid, match="Could not download from.*Network error"):
|
||||
external_files.download_content(url, test_file)
|
||||
|
||||
|
||||
@patch("esphome.external_files.requests.get")
|
||||
@patch("esphome.external_files.has_remote_file_changed")
|
||||
def test_download_content_skip_external_update_uses_cache(
|
||||
mock_has_changed: MagicMock,
|
||||
mock_get: MagicMock,
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""Test download_content skips network checks when CORE.skip_external_update is set."""
|
||||
test_file = setup_core / "cached.txt"
|
||||
cached_content = b"cached content"
|
||||
test_file.write_bytes(cached_content)
|
||||
|
||||
CORE.skip_external_update = True
|
||||
url = "https://example.com/file.txt"
|
||||
result = external_files.download_content(url, test_file)
|
||||
|
||||
assert result == cached_content
|
||||
mock_has_changed.assert_not_called()
|
||||
mock_get.assert_not_called()
|
||||
|
||||
|
||||
@patch("esphome.external_files.requests.get")
|
||||
@patch("esphome.external_files.has_remote_file_changed")
|
||||
def test_download_content_skip_external_update_downloads_when_missing(
|
||||
mock_has_changed: MagicMock,
|
||||
mock_get: MagicMock,
|
||||
setup_core: Path,
|
||||
) -> None:
|
||||
"""Test download_content still downloads when file is missing, even with skip_external_update."""
|
||||
test_file = setup_core / "missing.txt"
|
||||
new_content = b"fresh content"
|
||||
|
||||
mock_has_changed.return_value = True
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = new_content
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
CORE.skip_external_update = True
|
||||
url = "https://example.com/file.txt"
|
||||
result = external_files.download_content(url, test_file)
|
||||
|
||||
assert result == new_content
|
||||
assert test_file.read_bytes() == new_content
|
||||
|
||||
@@ -236,6 +236,35 @@ def test_clone_or_update_with_never_refresh(
|
||||
assert revert is None
|
||||
|
||||
|
||||
def test_clone_or_update_skips_when_core_skip_external_update(
|
||||
tmp_path: Path, mock_run_git_command: Mock
|
||||
) -> None:
|
||||
"""CORE.skip_external_update short-circuits the refresh for existing repos."""
|
||||
CORE.config_path = tmp_path / "test.yaml"
|
||||
|
||||
url = "https://github.com/test/repo"
|
||||
ref = None
|
||||
domain = "test"
|
||||
repo_dir = _compute_repo_dir(url, ref, domain)
|
||||
|
||||
repo_dir.mkdir(parents=True)
|
||||
git_dir = repo_dir / ".git"
|
||||
git_dir.mkdir()
|
||||
(git_dir / "FETCH_HEAD").write_text("test")
|
||||
|
||||
CORE.skip_external_update = True
|
||||
result_dir, revert = git.clone_or_update(
|
||||
url=url,
|
||||
ref=ref,
|
||||
refresh=TimePeriodSeconds(days=1),
|
||||
domain=domain,
|
||||
)
|
||||
|
||||
mock_run_git_command.assert_not_called()
|
||||
assert result_dir == repo_dir
|
||||
assert revert is None
|
||||
|
||||
|
||||
def test_clone_or_update_with_refresh_updates_old_repo(
|
||||
tmp_path: Path, mock_run_git_command: Mock
|
||||
) -> None:
|
||||
|
||||
@@ -654,7 +654,7 @@ def test_resolve_package_max_depth_exceeded(tmp_path: Path) -> None:
|
||||
package_config = yaml_util.IncludeFile(
|
||||
parent, "test.yaml", None, always_returns_include
|
||||
)
|
||||
processor = _PackageProcessor({}, None, False)
|
||||
processor = _PackageProcessor({}, None)
|
||||
with pytest.raises(
|
||||
cv.Invalid,
|
||||
match=f"Maximum include nesting depth \\({MAX_INCLUDE_DEPTH}\\) exceeded",
|
||||
@@ -776,7 +776,7 @@ def test_resolve_package_undefined_var_in_include_filename(tmp_path: Path) -> No
|
||||
package_config = yaml_util.IncludeFile(
|
||||
parent, "${undefined_var}.yaml", None, loader
|
||||
)
|
||||
processor = _PackageProcessor({}, None, False)
|
||||
processor = _PackageProcessor({}, None)
|
||||
with pytest.raises(cv.Invalid, match="unresolved substitutions"):
|
||||
processor.resolve_package(package_config, substitutions.ContextVars(), [])
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, patch
|
||||
@@ -32,6 +33,7 @@ from esphome.writer import (
|
||||
clean_build,
|
||||
clean_cmake_cache,
|
||||
copy_src_tree,
|
||||
generate_build_info_data_cpp,
|
||||
generate_build_info_data_h,
|
||||
get_build_info,
|
||||
storage_should_clean,
|
||||
@@ -782,8 +784,7 @@ def test_write_cpp_creates_new_file(
|
||||
assert CPP_AUTO_GENERATE_END in written_content
|
||||
assert test_code in written_content
|
||||
assert "void setup()" in written_content
|
||||
assert 'optimize("O2")' in written_content
|
||||
assert "loop()" in written_content
|
||||
assert "void loop()" in written_content
|
||||
assert "App.setup();" in written_content
|
||||
assert "App.loop();" in written_content
|
||||
|
||||
@@ -1616,49 +1617,62 @@ def test_get_build_info_build_time_str_format(
|
||||
|
||||
def test_generate_build_info_data_h_format() -> None:
|
||||
"""Test generate_build_info_data_h produces correct header content."""
|
||||
config_hash = 0x12345678
|
||||
build_time = 1700000000
|
||||
build_time_str = "2023-11-14 22:13:20 +0000"
|
||||
comment = "Test comment"
|
||||
|
||||
result = generate_build_info_data_h(
|
||||
config_hash, build_time, build_time_str, comment
|
||||
)
|
||||
result = generate_build_info_data_h()
|
||||
|
||||
assert "#pragma once" in result
|
||||
assert "#define ESPHOME_CONFIG_HASH 0x12345678U" in result
|
||||
assert "#define ESPHOME_BUILD_TIME 1700000000" in result
|
||||
assert "#define ESPHOME_COMMENT_SIZE 13" in result # len("Test comment") + 1
|
||||
assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result
|
||||
assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result
|
||||
assert "extern const uint32_t ESPHOME_CONFIG_HASH;" in result
|
||||
assert "extern const time_t ESPHOME_BUILD_TIME;" in result
|
||||
assert "extern const size_t ESPHOME_COMMENT_SIZE;" in result
|
||||
assert "extern const char ESPHOME_BUILD_TIME_STR[]" in result
|
||||
assert "extern const char ESPHOME_COMMENT_STR[]" in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_esp8266_progmem() -> None:
|
||||
"""Test generate_build_info_data_h includes PROGMEM for ESP8266."""
|
||||
result = generate_build_info_data_h(0xABCDEF01, 1700000000, "test", "comment")
|
||||
result = generate_build_info_data_h()
|
||||
|
||||
# Should have ESP8266 PROGMEM conditional
|
||||
assert "#ifdef USE_ESP8266" in result
|
||||
assert "#include <pgmspace.h>" in result
|
||||
assert "PROGMEM" in result
|
||||
# Both build time and comment should have PROGMEM versions
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_format() -> None:
|
||||
"""Test generate_build_info_data_cpp produces correct data definitions."""
|
||||
result = generate_build_info_data_cpp(
|
||||
0x12345678, 1700000000, "2023-11-14 22:13:20 +0000", "Test comment"
|
||||
)
|
||||
|
||||
assert '#include "esphome/core/build_info_data.h"' in result
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0x12345678U;" in result
|
||||
assert "const time_t ESPHOME_BUILD_TIME = 1700000000;" in result
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE = 13;" in result
|
||||
assert 'ESPHOME_BUILD_TIME_STR[] = "2023-11-14 22:13:20 +0000"' in result
|
||||
assert 'ESPHOME_COMMENT_STR[] = "Test comment"' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_esp8266_progmem() -> None:
|
||||
"""Test generate_build_info_data_cpp includes PROGMEM definitions."""
|
||||
result = generate_build_info_data_cpp(0xABCDEF01, 1700000000, "test", "comment")
|
||||
|
||||
assert "#ifdef USE_ESP8266" in result
|
||||
assert 'ESPHOME_BUILD_TIME_STR[] PROGMEM = "test"' in result
|
||||
assert 'ESPHOME_COMMENT_STR[] PROGMEM = "comment"' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_hash_formatting() -> None:
|
||||
"""Test generate_build_info_data_h formats hash with leading zeros."""
|
||||
def test_generate_build_info_data_cpp_hash_formatting() -> None:
|
||||
"""Test generate_build_info_data_cpp formats hash with leading zeros."""
|
||||
# Test with small hash value that needs leading zeros
|
||||
result = generate_build_info_data_h(0x00000001, 0, "test", "")
|
||||
assert "#define ESPHOME_CONFIG_HASH 0x00000001U" in result
|
||||
result = generate_build_info_data_cpp(0x00000001, 0, "test", "")
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0x00000001U;" in result
|
||||
|
||||
# Test with larger hash value
|
||||
result = generate_build_info_data_h(0xFFFFFFFF, 0, "test", "")
|
||||
assert "#define ESPHOME_CONFIG_HASH 0xffffffffU" in result
|
||||
result = generate_build_info_data_cpp(0xFFFFFFFF, 0, "test", "")
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0xffffffffU;" in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_comment_escaping() -> None:
|
||||
r"""Test generate_build_info_data_h properly escapes special characters in comment.
|
||||
def test_generate_build_info_data_cpp_comment_escaping() -> None:
|
||||
r"""Test generate_build_info_data_cpp properly escapes special characters in comment.
|
||||
|
||||
Uses cpp_string_escape which outputs octal escapes for special characters:
|
||||
- backslash (ASCII 92) -> \134
|
||||
@@ -1666,26 +1680,52 @@ def test_generate_build_info_data_h_comment_escaping() -> None:
|
||||
- newline (ASCII 10) -> \012
|
||||
"""
|
||||
# Test backslash escaping (ASCII 92 = octal 134)
|
||||
result = generate_build_info_data_h(0, 0, "test", "backslash\\here")
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "backslash\\here")
|
||||
assert 'ESPHOME_COMMENT_STR[] = "backslash\\134here"' in result
|
||||
|
||||
# Test quote escaping (ASCII 34 = octal 042)
|
||||
result = generate_build_info_data_h(0, 0, "test", 'has "quotes"')
|
||||
result = generate_build_info_data_cpp(0, 0, "test", 'has "quotes"')
|
||||
assert 'ESPHOME_COMMENT_STR[] = "has \\042quotes\\042"' in result
|
||||
|
||||
# Test newline escaping (ASCII 10 = octal 012)
|
||||
result = generate_build_info_data_h(0, 0, "test", "line1\nline2")
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "line1\nline2")
|
||||
assert 'ESPHOME_COMMENT_STR[] = "line1\\012line2"' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_h_empty_comment() -> None:
|
||||
"""Test generate_build_info_data_h handles empty comment."""
|
||||
result = generate_build_info_data_h(0, 0, "test", "")
|
||||
def test_generate_build_info_data_cpp_empty_comment() -> None:
|
||||
"""Test generate_build_info_data_cpp handles empty comment."""
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "")
|
||||
|
||||
assert "#define ESPHOME_COMMENT_SIZE 1" in result # Just null terminator
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE = 1;" in result # Just null terminator
|
||||
assert 'ESPHOME_COMMENT_STR[] = ""' in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_comment_size_counts_utf8_bytes() -> None:
|
||||
"""Comment size is in encoded UTF-8 bytes, not characters."""
|
||||
# "héllo" = 6 UTF-8 bytes + NUL.
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "héllo")
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE = 7;" in result
|
||||
|
||||
|
||||
def test_generate_build_info_data_cpp_comment_clamped_to_buffer() -> None:
|
||||
"""Generator clamps at byte level and never truncates mid-codepoint."""
|
||||
# 100 thermometer-with-VS-16 sequences = 700 bytes, past the 256 buffer.
|
||||
result = generate_build_info_data_cpp(0, 0, "test", "🌡️" * 100)
|
||||
|
||||
match = re.search(r"ESPHOME_COMMENT_SIZE = (\d+);", result)
|
||||
assert match is not None
|
||||
size = int(match.group(1))
|
||||
assert 1 < size <= 256
|
||||
|
||||
lit_match = re.search(r'ESPHOME_COMMENT_STR\[\] = "([^"]*)"', result)
|
||||
assert lit_match is not None
|
||||
raw = re.sub(
|
||||
r"\\([0-7]{3})", lambda m: chr(int(m.group(1), 8)), lit_match.group(1)
|
||||
).encode("latin-1")
|
||||
raw.decode("utf-8") # raises if truncation left a partial UTF-8 sequence
|
||||
assert len(raw) == size - 1
|
||||
|
||||
|
||||
@patch("esphome.writer.CORE")
|
||||
@patch("esphome.writer.iter_components")
|
||||
@patch("esphome.writer.walk_files")
|
||||
@@ -1759,15 +1799,21 @@ def test_copy_src_tree_writes_build_info_files(
|
||||
):
|
||||
copy_src_tree()
|
||||
|
||||
# Verify build_info_data.h was written
|
||||
# Verify build_info_data.h declarations and build_info_data.cpp values were written
|
||||
build_info_h_path = esphome_core_path / "build_info_data.h"
|
||||
assert build_info_h_path.exists()
|
||||
build_info_h_content = build_info_h_path.read_text()
|
||||
assert "#define ESPHOME_CONFIG_HASH 0xdeadbeefU" in build_info_h_content
|
||||
assert "#define ESPHOME_BUILD_TIME" in build_info_h_content
|
||||
assert "extern const uint32_t ESPHOME_CONFIG_HASH;" in build_info_h_content
|
||||
assert "ESPHOME_BUILD_TIME_STR" in build_info_h_content
|
||||
assert "#define ESPHOME_COMMENT_SIZE" in build_info_h_content
|
||||
assert "extern const size_t ESPHOME_COMMENT_SIZE;" in build_info_h_content
|
||||
assert "ESPHOME_COMMENT_STR" in build_info_h_content
|
||||
build_info_cpp_path = esphome_core_path / "build_info_data.cpp"
|
||||
assert build_info_cpp_path.exists()
|
||||
build_info_cpp_content = build_info_cpp_path.read_text()
|
||||
assert "const uint32_t ESPHOME_CONFIG_HASH = 0xdeadbeefU;" in build_info_cpp_content
|
||||
assert "const time_t ESPHOME_BUILD_TIME" in build_info_cpp_content
|
||||
assert "const size_t ESPHOME_COMMENT_SIZE" in build_info_cpp_content
|
||||
assert "ESPHOME_COMMENT_STR" in build_info_cpp_content
|
||||
|
||||
# Verify build_info.json was written
|
||||
build_info_json_path = build_path / "build_info.json"
|
||||
@@ -1834,7 +1880,9 @@ def test_copy_src_tree_detects_config_hash_change(
|
||||
|
||||
# Verify build_info files were updated due to config_hash change
|
||||
assert build_info_h_path.exists()
|
||||
new_content = build_info_h_path.read_text()
|
||||
build_info_cpp_path = esphome_core_path / "build_info_data.cpp"
|
||||
assert build_info_cpp_path.exists()
|
||||
new_content = build_info_cpp_path.read_text()
|
||||
assert "0xdeadbeef" in new_content.lower()
|
||||
|
||||
new_json = json.loads(build_info_json_path.read_text())
|
||||
|
||||
@@ -11,7 +11,13 @@ from esphome.config_helpers import Extend, Remove
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import DocumentLocation, DocumentRange, EsphomeError
|
||||
from esphome.util import OrderedDict
|
||||
from esphome.yaml_util import ESPHomeDataBase, format_path, make_data_base
|
||||
from esphome.yaml_util import (
|
||||
ESPHomeDataBase,
|
||||
ESPLiteralValue,
|
||||
format_path,
|
||||
make_data_base,
|
||||
make_literal,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -891,3 +897,57 @@ def test_format_path_empty_path_with_located_current_obj():
|
||||
obj = _located("${var}", "main.yaml", 0, 0)
|
||||
result = format_path([], obj)
|
||||
assert result == "In: in main.yaml 1:1"
|
||||
|
||||
|
||||
def test_make_literal_wraps_dict() -> None:
|
||||
"""A dict is wrapped so it becomes an ESPLiteralValue instance."""
|
||||
value = {"key": "${var}"}
|
||||
result = make_literal(value)
|
||||
assert isinstance(result, ESPLiteralValue)
|
||||
assert isinstance(result, dict)
|
||||
assert result == {"key": "${var}"}
|
||||
|
||||
|
||||
def test_make_literal_wraps_list() -> None:
|
||||
"""A list is wrapped so it becomes an ESPLiteralValue instance."""
|
||||
value = ["${var}", "plain"]
|
||||
result = make_literal(value)
|
||||
assert isinstance(result, ESPLiteralValue)
|
||||
assert isinstance(result, list)
|
||||
assert result == ["${var}", "plain"]
|
||||
|
||||
|
||||
def test_make_literal_wraps_string() -> None:
|
||||
"""A string is wrapped so it becomes an ESPLiteralValue instance."""
|
||||
result = make_literal("${var}")
|
||||
assert isinstance(result, ESPLiteralValue)
|
||||
assert result == "${var}"
|
||||
|
||||
|
||||
def test_make_literal_returns_already_wrapped_value_unchanged() -> None:
|
||||
"""Wrapping a value that is already an ESPLiteralValue returns it as-is."""
|
||||
value = make_literal({"key": "value"})
|
||||
assert isinstance(value, ESPLiteralValue)
|
||||
result = make_literal(value)
|
||||
assert result is value
|
||||
|
||||
|
||||
def test_make_literal_returns_none_unchanged() -> None:
|
||||
"""Values whose class cannot be augmented (e.g. ``None``) are returned as-is."""
|
||||
result = make_literal(None)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_make_literal_blocks_substitution() -> None:
|
||||
"""A value wrapped with make_literal is skipped by the substitution pass."""
|
||||
value = make_literal({"pin": "${PIN}"})
|
||||
result = substitutions.substitute(
|
||||
value,
|
||||
path=[],
|
||||
parent_context=substitutions.ContextVars(),
|
||||
strict_undefined=False,
|
||||
)
|
||||
# The literal block must remain untouched, even though the variable is
|
||||
# undefined in the context.
|
||||
assert result == {"pin": "${PIN}"}
|
||||
assert isinstance(result, ESPLiteralValue)
|
||||
|
||||
Reference in New Issue
Block a user