From bfeade1e2bcb1372fed563f813a24032cbb7d04b Mon Sep 17 00:00:00 2001 From: Leonardo Rivera Date: Fri, 17 Oct 2025 22:13:33 -0300 Subject: [PATCH 01/22] [remote_base] Add Symphony IR protocol (encode/decode) with command_repeats support (#10777) --- esphome/components/remote_base/__init__.py | 46 +++++++ .../remote_base/symphony_protocol.cpp | 120 ++++++++++++++++++ .../remote_base/symphony_protocol.h | 44 +++++++ .../remote_receiver/common-actions.yaml | 5 + .../remote_transmitter/common-buttons.yaml | 6 + 5 files changed, 221 insertions(+) create mode 100644 esphome/components/remote_base/symphony_protocol.cpp create mode 100644 esphome/components/remote_base/symphony_protocol.h diff --git a/esphome/components/remote_base/__init__.py b/esphome/components/remote_base/__init__.py index 42ebae77f7..ccf16a8beb 100644 --- a/esphome/components/remote_base/__init__.py +++ b/esphome/components/remote_base/__init__.py @@ -1056,6 +1056,52 @@ async def sony_action(var, config, args): cg.add(var.set_nbits(template_)) +# Symphony +SymphonyData, SymphonyBinarySensor, SymphonyTrigger, SymphonyAction, SymphonyDumper = ( + declare_protocol("Symphony") +) +SYMPHONY_SCHEMA = cv.Schema( + { + cv.Required(CONF_DATA): cv.hex_uint32_t, + cv.Required(CONF_NBITS): cv.int_range(min=1, max=32), + cv.Optional(CONF_COMMAND_REPEATS, default=2): cv.uint8_t, + } +) + + +@register_binary_sensor("symphony", SymphonyBinarySensor, SYMPHONY_SCHEMA) +def symphony_binary_sensor(var, config): + cg.add( + var.set_data( + cg.StructInitializer( + SymphonyData, + ("data", config[CONF_DATA]), + ("nbits", config[CONF_NBITS]), + ) + ) + ) + + +@register_trigger("symphony", SymphonyTrigger, SymphonyData) +def symphony_trigger(var, config): + pass + + +@register_dumper("symphony", SymphonyDumper) +def symphony_dumper(var, config): + pass + + +@register_action("symphony", SymphonyAction, SYMPHONY_SCHEMA) +async def symphony_action(var, config, args): + template_ = await cg.templatable(config[CONF_DATA], args, cg.uint32) + cg.add(var.set_data(template_)) + template_ = await cg.templatable(config[CONF_NBITS], args, cg.uint32) + cg.add(var.set_nbits(template_)) + template_ = await cg.templatable(config[CONF_COMMAND_REPEATS], args, cg.uint8) + cg.add(var.set_repeats(template_)) + + # Raw def validate_raw_alternating(value): assert isinstance(value, list) diff --git a/esphome/components/remote_base/symphony_protocol.cpp b/esphome/components/remote_base/symphony_protocol.cpp new file mode 100644 index 0000000000..34b5dba07f --- /dev/null +++ b/esphome/components/remote_base/symphony_protocol.cpp @@ -0,0 +1,120 @@ +#include "symphony_protocol.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace remote_base { + +static const char *const TAG = "remote.symphony"; + +// Reference implementation and timing details: +// IRremoteESP8266 ir_Symphony.cpp +// https://github.com/crankyoldgit/IRremoteESP8266/blob/master/src/ir_Symphony.cpp +// The implementation below mirrors the constant bit-time mapping and +// footer-gap handling used there. + +// Symphony protocol timing specifications (tuned to handset captures) +static const uint32_t BIT_ZERO_HIGH_US = 460; // short +static const uint32_t BIT_ZERO_LOW_US = 1260; // long +static const uint32_t BIT_ONE_HIGH_US = 1260; // long +static const uint32_t BIT_ONE_LOW_US = 460; // short +static const uint32_t CARRIER_FREQUENCY = 38000; + +// IRremoteESP8266 reference: kSymphonyFooterGap = 4 * (mark + space) +static const uint32_t FOOTER_GAP_US = 4 * (BIT_ZERO_HIGH_US + BIT_ZERO_LOW_US); +// Typical inter-frame gap (~34.8 ms observed) +static const uint32_t INTER_FRAME_GAP_US = 34760; + +void SymphonyProtocol::encode(RemoteTransmitData *dst, const SymphonyData &data) { + dst->set_carrier_frequency(CARRIER_FREQUENCY); + ESP_LOGD(TAG, "Sending Symphony: data=0x%0*X nbits=%u repeats=%u", (data.nbits + 3) / 4, (uint32_t) data.data, + data.nbits, data.repeats); + // Each bit produces a mark+space (2 entries). We fold the inter-frame/footer gap + // into the last bit's space of each frame to avoid over-length gaps. + dst->reserve(data.nbits * 2u * data.repeats); + + for (uint8_t repeats = 0; repeats < data.repeats; repeats++) { + // Data bits (MSB first) + for (uint32_t mask = 1UL << (data.nbits - 1); mask != 0; mask >>= 1) { + const bool is_last_bit = (mask == 1); + const bool is_last_frame = (repeats == (data.repeats - 1)); + if (is_last_bit) { + // Emit last bit's mark; replace its space with the proper gap + if (data.data & mask) { + dst->mark(BIT_ONE_HIGH_US); + } else { + dst->mark(BIT_ZERO_HIGH_US); + } + dst->space(is_last_frame ? FOOTER_GAP_US : INTER_FRAME_GAP_US); + } else { + if (data.data & mask) { + dst->item(BIT_ONE_HIGH_US, BIT_ONE_LOW_US); + } else { + dst->item(BIT_ZERO_HIGH_US, BIT_ZERO_LOW_US); + } + } + } + } +} + +optional SymphonyProtocol::decode(RemoteReceiveData src) { + auto is_valid_len = [](uint8_t nbits) -> bool { return nbits == 8 || nbits == 12 || nbits == 16; }; + + RemoteReceiveData s = src; // copy + SymphonyData out{0, 0, 1}; + + for (; out.nbits < 32; out.nbits++) { + if (s.expect_mark(BIT_ONE_HIGH_US)) { + if (!s.expect_space(BIT_ONE_LOW_US)) { + // Allow footer gap immediately after the last mark + if (s.peek_space_at_least(FOOTER_GAP_US)) { + uint8_t bits_with_this = out.nbits + 1; + if (is_valid_len(bits_with_this)) { + out.data = (out.data << 1UL) | 1UL; + out.nbits = bits_with_this; + return out; + } + } + return {}; + } + // Successfully consumed a '1' bit (mark + space) + out.data = (out.data << 1UL) | 1UL; + continue; + } else if (s.expect_mark(BIT_ZERO_HIGH_US)) { + if (!s.expect_space(BIT_ZERO_LOW_US)) { + // Allow footer gap immediately after the last mark + if (s.peek_space_at_least(FOOTER_GAP_US)) { + uint8_t bits_with_this = out.nbits + 1; + if (is_valid_len(bits_with_this)) { + out.data = (out.data << 1UL) | 0UL; + out.nbits = bits_with_this; + return out; + } + } + return {}; + } + // Successfully consumed a '0' bit (mark + space) + out.data = (out.data << 1UL) | 0UL; + continue; + } else { + // Completed a valid-length frame followed by a footer gap + if (is_valid_len(out.nbits) && s.peek_space_at_least(FOOTER_GAP_US)) { + return out; + } + return {}; + } + } + + if (is_valid_len(out.nbits) && s.peek_space_at_least(FOOTER_GAP_US)) { + return out; + } + + return {}; +} + +void SymphonyProtocol::dump(const SymphonyData &data) { + const int32_t hex_width = (data.nbits + 3) / 4; // pad to nibble width + ESP_LOGI(TAG, "Received Symphony: data=0x%0*X, nbits=%d", hex_width, (uint32_t) data.data, data.nbits); +} + +} // namespace remote_base +} // namespace esphome diff --git a/esphome/components/remote_base/symphony_protocol.h b/esphome/components/remote_base/symphony_protocol.h new file mode 100644 index 0000000000..7e77a268ba --- /dev/null +++ b/esphome/components/remote_base/symphony_protocol.h @@ -0,0 +1,44 @@ +#pragma once + +#include "esphome/core/component.h" +#include "remote_base.h" + +#include + +namespace esphome { +namespace remote_base { + +struct SymphonyData { + uint32_t data; + uint8_t nbits; + uint8_t repeats{1}; + + bool operator==(const SymphonyData &rhs) const { return data == rhs.data && nbits == rhs.nbits; } +}; + +class SymphonyProtocol : public RemoteProtocol { + public: + void encode(RemoteTransmitData *dst, const SymphonyData &data) override; + optional decode(RemoteReceiveData src) override; + void dump(const SymphonyData &data) override; +}; + +DECLARE_REMOTE_PROTOCOL(Symphony) + +template class SymphonyAction : public RemoteTransmitterActionBase { + public: + TEMPLATABLE_VALUE(uint32_t, data) + TEMPLATABLE_VALUE(uint8_t, nbits) + TEMPLATABLE_VALUE(uint8_t, repeats) + + void encode(RemoteTransmitData *dst, Ts... x) override { + SymphonyData data{}; + data.data = this->data_.value(x...); + data.nbits = this->nbits_.value(x...); + data.repeats = this->repeats_.value(x...); + SymphonyProtocol().encode(dst, data); + } +}; + +} // namespace remote_base +} // namespace esphome diff --git a/tests/components/remote_receiver/common-actions.yaml b/tests/components/remote_receiver/common-actions.yaml index ca7713f58a..c2dc2f0c29 100644 --- a/tests/components/remote_receiver/common-actions.yaml +++ b/tests/components/remote_receiver/common-actions.yaml @@ -143,6 +143,11 @@ on_sony: - logger.log: format: "on_sony: %lu %u" args: ["long(x.data)", "x.nbits"] +on_symphony: + then: + - logger.log: + format: "on_symphony: 0x%lX %u" + args: ["long(x.data)", "x.nbits"] on_toshiba_ac: then: - logger.log: diff --git a/tests/components/remote_transmitter/common-buttons.yaml b/tests/components/remote_transmitter/common-buttons.yaml index 3be4bf3cca..58127d1ab4 100644 --- a/tests/components/remote_transmitter/common-buttons.yaml +++ b/tests/components/remote_transmitter/common-buttons.yaml @@ -53,6 +53,12 @@ button: remote_transmitter.transmit_sony: data: 0xABCDEF nbits: 12 + - platform: template + name: Symphony + on_press: + remote_transmitter.transmit_symphony: + data: 0xE88 + nbits: 12 - platform: template name: Panasonic on_press: From 85f1019d90a7e5d6eeafda94e6aec0b519fe4b21 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:21:38 -1000 Subject: [PATCH 02/22] [tests] Migrate remote_transmitter/receiver to common bus definitions (#11325) --- script/analyze_component_buses.py | 11 ++++++++++- tests/components/ballu/common.yaml | 5 +---- tests/components/ballu/test.esp8266-ard.yaml | 4 ++-- tests/components/climate_ir_lg/common.yaml | 5 +---- .../climate_ir_lg/test.esp32-c3-idf.yaml | 4 ++-- .../components/climate_ir_lg/test.esp32-idf.yaml | 4 ++-- .../climate_ir_lg/test.esp8266-ard.yaml | 4 ++-- tests/components/coolix/common.yaml | 5 +---- tests/components/coolix/test.esp32-c3-idf.yaml | 4 ++-- tests/components/coolix/test.esp32-idf.yaml | 4 ++-- tests/components/coolix/test.esp8266-ard.yaml | 4 ++-- tests/components/daikin/common.yaml | 5 +---- tests/components/daikin/test.esp8266-ard.yaml | 4 ++-- tests/components/daikin_arc/common.yaml | 15 --------------- .../components/daikin_arc/test.esp8266-ard.yaml | 6 +++--- tests/components/daikin_brc/common.yaml | 5 +---- .../components/daikin_brc/test.esp32-c3-idf.yaml | 4 ++-- tests/components/daikin_brc/test.esp32-idf.yaml | 4 ++-- .../components/daikin_brc/test.esp8266-ard.yaml | 4 ++-- tests/components/delonghi/common.yaml | 5 +---- tests/components/delonghi/test.esp32-c3-idf.yaml | 4 ++-- tests/components/delonghi/test.esp32-idf.yaml | 4 ++-- tests/components/delonghi/test.esp8266-ard.yaml | 4 ++-- tests/components/emmeti/common.yaml | 11 +---------- tests/components/emmeti/test.esp32-idf.yaml | 6 +++--- tests/components/emmeti/test.esp8266-ard.yaml | 6 +++--- tests/components/fujitsu_general/common.yaml | 5 +---- .../fujitsu_general/test.esp32-c3-idf.yaml | 4 ++-- .../fujitsu_general/test.esp32-idf.yaml | 4 ++-- .../fujitsu_general/test.esp8266-ard.yaml | 4 ++-- tests/components/gree/common.yaml | 5 +---- tests/components/gree/test.esp32-c3-idf.yaml | 4 ++-- tests/components/gree/test.esp32-idf.yaml | 4 ++-- tests/components/gree/test.esp8266-ard.yaml | 4 ++-- tests/components/heatpumpir/common.yaml | 7 +++---- tests/components/heatpumpir/test.bk72xx-ard.yaml | 4 ++-- tests/components/heatpumpir/test.esp32-ard.yaml | 4 ++-- .../components/heatpumpir/test.esp8266-ard.yaml | 4 ++-- tests/components/hitachi_ac344/common.yaml | 5 +---- .../hitachi_ac344/test.bk72xx-ard.yaml | 4 ++-- .../hitachi_ac344/test.esp32-c3-idf.yaml | 4 ++-- .../components/hitachi_ac344/test.esp32-idf.yaml | 4 ++-- .../hitachi_ac344/test.esp8266-ard.yaml | 4 ++-- tests/components/hitachi_ac424/common.yaml | 5 +---- .../hitachi_ac424/test.bk72xx-ard.yaml | 4 ++-- .../hitachi_ac424/test.esp32-c3-idf.yaml | 4 ++-- .../components/hitachi_ac424/test.esp32-idf.yaml | 4 ++-- .../hitachi_ac424/test.esp8266-ard.yaml | 4 ++-- tests/components/midea/common.yaml | 6 +----- tests/components/midea/test.esp32-ard.yaml | 4 +--- tests/components/midea/test.esp8266-ard.yaml | 4 +--- tests/components/midea_ir/common.yaml | 5 +---- tests/components/midea_ir/test.esp32-c3-idf.yaml | 3 +++ tests/components/midea_ir/test.esp32-idf.yaml | 3 +++ tests/components/midea_ir/test.esp8266-ard.yaml | 3 +++ tests/components/mitsubishi/common.yaml | 5 +---- .../components/mitsubishi/test.esp32-c3-idf.yaml | 3 +++ tests/components/mitsubishi/test.esp32-idf.yaml | 3 +++ .../components/mitsubishi/test.esp8266-ard.yaml | 3 +++ tests/components/noblex/common.yaml | 9 --------- tests/components/noblex/test.esp32-c3-idf.yaml | 4 ++++ tests/components/noblex/test.esp32-idf.yaml | 4 ++++ tests/components/noblex/test.esp8266-ard.yaml | 4 ++++ tests/components/prometheus/common.yaml | 5 +---- .../components/prometheus/test.esp32-c3-idf.yaml | 4 +++- tests/components/prometheus/test.esp32-idf.yaml | 2 +- .../components/prometheus/test.esp8266-ard.yaml | 4 +++- tests/components/tcl112/common.yaml | 5 +---- tests/components/tcl112/test.esp32-c3-idf.yaml | 4 ++-- tests/components/tcl112/test.esp32-idf.yaml | 4 ++-- tests/components/tcl112/test.esp8266-ard.yaml | 4 ++-- tests/components/toshiba/common.yaml | 5 +---- tests/components/toshiba/test.esp32-c3-idf.yaml | 4 ++-- tests/components/toshiba/test.esp32-idf.yaml | 4 ++-- tests/components/toshiba/test.esp8266-ard.yaml | 4 ++-- tests/components/whirlpool/common.yaml | 5 +---- .../components/whirlpool/test.esp32-c3-idf.yaml | 4 ++-- tests/components/whirlpool/test.esp32-idf.yaml | 4 ++-- tests/components/whirlpool/test.esp8266-ard.yaml | 4 ++-- tests/components/whynter/common.yaml | 5 +---- tests/components/whynter/test.esp32-c3-idf.yaml | 4 ++-- tests/components/whynter/test.esp32-idf.yaml | 4 ++-- tests/components/whynter/test.esp8266-ard.yaml | 4 ++-- tests/components/yashima/common.yaml | 5 +---- tests/components/yashima/test.esp32-c3-idf.yaml | 4 ++-- tests/components/yashima/test.esp32-idf.yaml | 4 ++-- tests/components/yashima/test.esp8266-ard.yaml | 4 ++-- tests/components/zhlt01/common.yaml | 5 +---- tests/components/zhlt01/test.esp32-c3-idf.yaml | 4 ++-- tests/components/zhlt01/test.esp32-idf.yaml | 4 ++-- tests/components/zhlt01/test.esp8266-ard.yaml | 4 ++-- .../common/remote_receiver/esp32-c3-idf.yaml | 16 ++++++++++++++++ .../common/remote_receiver/esp32-idf.yaml | 16 ++++++++++++++++ .../common/remote_receiver/esp8266-ard.yaml | 12 ++++++++++++ .../common/remote_transmitter/bk72xx-ard.yaml | 11 +++++++++++ .../common/remote_transmitter/esp32-ard.yaml | 11 +++++++++++ .../common/remote_transmitter/esp32-c3-idf.yaml | 13 +++++++++++++ .../common/remote_transmitter/esp32-idf.yaml | 13 +++++++++++++ .../common/remote_transmitter/esp8266-ard.yaml | 11 +++++++++++ 99 files changed, 283 insertions(+), 236 deletions(-) create mode 100644 tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml create mode 100644 tests/test_build_components/common/remote_receiver/esp32-idf.yaml create mode 100644 tests/test_build_components/common/remote_receiver/esp8266-ard.yaml create mode 100644 tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml create mode 100644 tests/test_build_components/common/remote_transmitter/esp32-ard.yaml create mode 100644 tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml create mode 100644 tests/test_build_components/common/remote_transmitter/esp32-idf.yaml create mode 100644 tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index 98edfef145..d0882e22e9 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -50,7 +50,14 @@ PACKAGE_DEPENDENCIES = { # Bus types that can be defined directly in config files # Components defining these directly cannot be grouped (they create unique bus IDs) -DIRECT_BUS_TYPES = ("i2c", "spi", "uart", "modbus") +DIRECT_BUS_TYPES = ( + "i2c", + "spi", + "uart", + "modbus", + "remote_transmitter", + "remote_receiver", +) # Signature for components with no bus requirements # These components can be merged with any other group @@ -68,6 +75,8 @@ BASE_BUS_COMPONENTS = { "uart", "modbus", "canbus", + "remote_transmitter", + "remote_receiver", } # Components that must be tested in isolation (not grouped or batched with others) diff --git a/tests/components/ballu/common.yaml b/tests/components/ballu/common.yaml index 52f86aa26a..178c39b8ce 100644 --- a/tests/components/ballu/common.yaml +++ b/tests/components/ballu/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: heatpumpir protocol: ballu @@ -10,3 +6,4 @@ climate: name: HeatpumpIR Climate min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr diff --git a/tests/components/ballu/test.esp8266-ard.yaml b/tests/components/ballu/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/ballu/test.esp8266-ard.yaml +++ b/tests/components/ballu/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/climate_ir_lg/common.yaml b/tests/components/climate_ir_lg/common.yaml index c8f84411c0..da0d656b21 100644 --- a/tests/components/climate_ir_lg/common.yaml +++ b/tests/components/climate_ir_lg/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: climate_ir_lg name: LG Climate + transmitter_id: xmitr diff --git a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml +++ b/tests/components/climate_ir_lg/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp32-idf.yaml b/tests/components/climate_ir_lg/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/climate_ir_lg/test.esp32-idf.yaml +++ b/tests/components/climate_ir_lg/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/climate_ir_lg/test.esp8266-ard.yaml b/tests/components/climate_ir_lg/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/climate_ir_lg/test.esp8266-ard.yaml +++ b/tests/components/climate_ir_lg/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/coolix/common.yaml b/tests/components/coolix/common.yaml index abe609c3ea..a1f68b8be0 100644 --- a/tests/components/coolix/common.yaml +++ b/tests/components/coolix/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: coolix name: Coolix Climate + transmitter_id: xmitr diff --git a/tests/components/coolix/test.esp32-c3-idf.yaml b/tests/components/coolix/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/coolix/test.esp32-c3-idf.yaml +++ b/tests/components/coolix/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/coolix/test.esp32-idf.yaml b/tests/components/coolix/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/coolix/test.esp32-idf.yaml +++ b/tests/components/coolix/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/coolix/test.esp8266-ard.yaml b/tests/components/coolix/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/coolix/test.esp8266-ard.yaml +++ b/tests/components/coolix/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/daikin/common.yaml b/tests/components/daikin/common.yaml index 27f381b422..fd73841686 100644 --- a/tests/components/daikin/common.yaml +++ b/tests/components/daikin/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: heatpumpir protocol: daikin @@ -10,3 +6,4 @@ climate: name: HeatpumpIR Climate min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr diff --git a/tests/components/daikin/test.esp8266-ard.yaml b/tests/components/daikin/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/daikin/test.esp8266-ard.yaml +++ b/tests/components/daikin/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/daikin_arc/common.yaml b/tests/components/daikin_arc/common.yaml index 5c0510f6df..53df3cf911 100644 --- a/tests/components/daikin_arc/common.yaml +++ b/tests/components/daikin_arc/common.yaml @@ -1,18 +1,3 @@ -remote_transmitter: - pin: ${tx_pin} - carrier_duty_percent: 50% - id: tsvr - -remote_receiver: - id: rcvr - pin: - number: ${rx_pin} - inverted: true - mode: - input: true - pullup: true - tolerance: 40% - climate: - platform: daikin_arc name: Daikin AC diff --git a/tests/components/daikin_arc/test.esp8266-ard.yaml b/tests/components/daikin_arc/test.esp8266-ard.yaml index 5698a7ef5f..aa8651e556 100644 --- a/tests/components/daikin_arc/test.esp8266-ard.yaml +++ b/tests/components/daikin_arc/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - tx_pin: GPIO0 - rx_pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/daikin_brc/common.yaml b/tests/components/daikin_brc/common.yaml index c9d7baa989..89786954ba 100644 --- a/tests/components/daikin_brc/common.yaml +++ b/tests/components/daikin_brc/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: daikin_brc name: Daikin_brc Climate + transmitter_id: xmitr diff --git a/tests/components/daikin_brc/test.esp32-c3-idf.yaml b/tests/components/daikin_brc/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/daikin_brc/test.esp32-c3-idf.yaml +++ b/tests/components/daikin_brc/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp32-idf.yaml b/tests/components/daikin_brc/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/daikin_brc/test.esp32-idf.yaml +++ b/tests/components/daikin_brc/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/daikin_brc/test.esp8266-ard.yaml b/tests/components/daikin_brc/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/daikin_brc/test.esp8266-ard.yaml +++ b/tests/components/daikin_brc/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/delonghi/common.yaml b/tests/components/delonghi/common.yaml index 8e9a1293d7..c3935adcbd 100644 --- a/tests/components/delonghi/common.yaml +++ b/tests/components/delonghi/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: delonghi name: Delonghi Climate + transmitter_id: xmitr diff --git a/tests/components/delonghi/test.esp32-c3-idf.yaml b/tests/components/delonghi/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/delonghi/test.esp32-c3-idf.yaml +++ b/tests/components/delonghi/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/delonghi/test.esp32-idf.yaml b/tests/components/delonghi/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/delonghi/test.esp32-idf.yaml +++ b/tests/components/delonghi/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/delonghi/test.esp8266-ard.yaml b/tests/components/delonghi/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/delonghi/test.esp8266-ard.yaml +++ b/tests/components/delonghi/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/emmeti/common.yaml b/tests/components/emmeti/common.yaml index ac4201e19b..77f381ecf9 100644 --- a/tests/components/emmeti/common.yaml +++ b/tests/components/emmeti/common.yaml @@ -1,14 +1,5 @@ -remote_transmitter: - id: tx - pin: ${remote_transmitter_pin} - carrier_duty_percent: 100% - -remote_receiver: - id: rcvr - pin: ${remote_receiver_pin} - climate: - platform: emmeti name: Emmeti receiver_id: rcvr - transmitter_id: tx + transmitter_id: xmitr diff --git a/tests/components/emmeti/test.esp32-idf.yaml b/tests/components/emmeti/test.esp32-idf.yaml index 2689ff279e..b241dbd159 100644 --- a/tests/components/emmeti/test.esp32-idf.yaml +++ b/tests/components/emmeti/test.esp32-idf.yaml @@ -1,5 +1,5 @@ -substitutions: - remote_transmitter_pin: GPIO33 - remote_receiver_pin: GPIO32 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/emmeti/test.esp8266-ard.yaml b/tests/components/emmeti/test.esp8266-ard.yaml index 1c9baa4ea3..aa8651e556 100644 --- a/tests/components/emmeti/test.esp8266-ard.yaml +++ b/tests/components/emmeti/test.esp8266-ard.yaml @@ -1,5 +1,5 @@ -substitutions: - remote_transmitter_pin: GPIO0 - remote_receiver_pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/fujitsu_general/common.yaml b/tests/components/fujitsu_general/common.yaml index 3359b89f2a..51bd1441c0 100644 --- a/tests/components/fujitsu_general/common.yaml +++ b/tests/components/fujitsu_general/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: fujitsu_general name: Fujitsu General Climate + transmitter_id: xmitr diff --git a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/fujitsu_general/test.esp32-c3-idf.yaml +++ b/tests/components/fujitsu_general/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp32-idf.yaml b/tests/components/fujitsu_general/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/fujitsu_general/test.esp32-idf.yaml +++ b/tests/components/fujitsu_general/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/fujitsu_general/test.esp8266-ard.yaml b/tests/components/fujitsu_general/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/fujitsu_general/test.esp8266-ard.yaml +++ b/tests/components/fujitsu_general/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/gree/common.yaml b/tests/components/gree/common.yaml index c221184bbf..e706076034 100644 --- a/tests/components/gree/common.yaml +++ b/tests/components/gree/common.yaml @@ -1,8 +1,5 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: gree name: GREE model: generic + transmitter_id: xmitr diff --git a/tests/components/gree/test.esp32-c3-idf.yaml b/tests/components/gree/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/gree/test.esp32-c3-idf.yaml +++ b/tests/components/gree/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/gree/test.esp32-idf.yaml b/tests/components/gree/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/gree/test.esp32-idf.yaml +++ b/tests/components/gree/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/gree/test.esp8266-ard.yaml b/tests/components/gree/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/gree/test.esp8266-ard.yaml +++ b/tests/components/gree/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/heatpumpir/common.yaml b/tests/components/heatpumpir/common.yaml index d740f31518..a2779f9803 100644 --- a/tests/components/heatpumpir/common.yaml +++ b/tests/components/heatpumpir/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: heatpumpir protocol: mitsubishi_heavy_zm @@ -10,6 +6,7 @@ climate: name: HeatpumpIR Climate Mitsubishi min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr - platform: heatpumpir protocol: daikin horizontal_default: mleft @@ -17,6 +14,7 @@ climate: name: HeatpumpIR Climate Daikin min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr - platform: heatpumpir protocol: panasonic_altdke horizontal_default: mright @@ -24,3 +22,4 @@ climate: name: HeatpumpIR Climate Panasonic min_temperature: 18 max_temperature: 30 + transmitter_id: xmitr diff --git a/tests/components/heatpumpir/test.bk72xx-ard.yaml b/tests/components/heatpumpir/test.bk72xx-ard.yaml index 06e1aea364..6cce191825 100644 --- a/tests/components/heatpumpir/test.bk72xx-ard.yaml +++ b/tests/components/heatpumpir/test.bk72xx-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO6 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/heatpumpir/test.esp32-ard.yaml b/tests/components/heatpumpir/test.esp32-ard.yaml index 7b012aa64c..01009de071 100644 --- a/tests/components/heatpumpir/test.esp32-ard.yaml +++ b/tests/components/heatpumpir/test.esp32-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/heatpumpir/test.esp8266-ard.yaml b/tests/components/heatpumpir/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/heatpumpir/test.esp8266-ard.yaml +++ b/tests/components/heatpumpir/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/common.yaml b/tests/components/hitachi_ac344/common.yaml index 960f032035..797e7875e5 100644 --- a/tests/components/hitachi_ac344/common.yaml +++ b/tests/components/hitachi_ac344/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: hitachi_ac344 name: Hitachi Climate + transmitter_id: xmitr diff --git a/tests/components/hitachi_ac344/test.bk72xx-ard.yaml b/tests/components/hitachi_ac344/test.bk72xx-ard.yaml index 06e1aea364..6cce191825 100644 --- a/tests/components/hitachi_ac344/test.bk72xx-ard.yaml +++ b/tests/components/hitachi_ac344/test.bk72xx-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO6 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml +++ b/tests/components/hitachi_ac344/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp32-idf.yaml b/tests/components/hitachi_ac344/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/hitachi_ac344/test.esp32-idf.yaml +++ b/tests/components/hitachi_ac344/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac344/test.esp8266-ard.yaml b/tests/components/hitachi_ac344/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/hitachi_ac344/test.esp8266-ard.yaml +++ b/tests/components/hitachi_ac344/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/common.yaml b/tests/components/hitachi_ac424/common.yaml index ad904c73a3..615bda4544 100644 --- a/tests/components/hitachi_ac424/common.yaml +++ b/tests/components/hitachi_ac424/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: hitachi_ac424 name: Hitachi Climate + transmitter_id: xmitr diff --git a/tests/components/hitachi_ac424/test.bk72xx-ard.yaml b/tests/components/hitachi_ac424/test.bk72xx-ard.yaml index 06e1aea364..6cce191825 100644 --- a/tests/components/hitachi_ac424/test.bk72xx-ard.yaml +++ b/tests/components/hitachi_ac424/test.bk72xx-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO6 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml +++ b/tests/components/hitachi_ac424/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp32-idf.yaml b/tests/components/hitachi_ac424/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/hitachi_ac424/test.esp32-idf.yaml +++ b/tests/components/hitachi_ac424/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/hitachi_ac424/test.esp8266-ard.yaml b/tests/components/hitachi_ac424/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/hitachi_ac424/test.esp8266-ard.yaml +++ b/tests/components/hitachi_ac424/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/midea/common.yaml b/tests/components/midea/common.yaml index a0909401ff..fec85aee96 100644 --- a/tests/components/midea/common.yaml +++ b/tests/components/midea/common.yaml @@ -2,10 +2,6 @@ wifi: ssid: MySSID password: password1 -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: midea id: midea_unit @@ -16,7 +12,7 @@ climate: x.set_mode(CLIMATE_MODE_FAN_ONLY); on_state: - logger.log: State changed! - transmitter_id: + transmitter_id: xmitr period: 1s num_attempts: 5 timeout: 2s diff --git a/tests/components/midea/test.esp32-ard.yaml b/tests/components/midea/test.esp32-ard.yaml index b78163199a..1e3fe0ff51 100644 --- a/tests/components/midea/test.esp32-ard.yaml +++ b/tests/components/midea/test.esp32-ard.yaml @@ -1,7 +1,5 @@ -substitutions: - pin: GPIO2 - packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml uart: !include ../../test_build_components/common/uart/esp32-ard.yaml <<: !include common.yaml diff --git a/tests/components/midea/test.esp8266-ard.yaml b/tests/components/midea/test.esp8266-ard.yaml index dc276e274c..9825ff85a1 100644 --- a/tests/components/midea/test.esp8266-ard.yaml +++ b/tests/components/midea/test.esp8266-ard.yaml @@ -1,7 +1,5 @@ -substitutions: - pin: GPIO15 - packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml uart: !include ../../test_build_components/common/uart/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/midea_ir/common.yaml b/tests/components/midea_ir/common.yaml index e8d89cecc2..e4cc4bb19c 100644 --- a/tests/components/midea_ir/common.yaml +++ b/tests/components/midea_ir/common.yaml @@ -1,8 +1,5 @@ -remote_transmitter: - pin: 4 - carrier_duty_percent: 50% - climate: - platform: midea_ir name: Midea IR use_fahrenheit: true + transmitter_id: xmitr diff --git a/tests/components/midea_ir/test.esp32-c3-idf.yaml b/tests/components/midea_ir/test.esp32-c3-idf.yaml index dade44d145..43d5343715 100644 --- a/tests/components/midea_ir/test.esp32-c3-idf.yaml +++ b/tests/components/midea_ir/test.esp32-c3-idf.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp32-idf.yaml b/tests/components/midea_ir/test.esp32-idf.yaml index dade44d145..e891f9dc85 100644 --- a/tests/components/midea_ir/test.esp32-idf.yaml +++ b/tests/components/midea_ir/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/midea_ir/test.esp8266-ard.yaml b/tests/components/midea_ir/test.esp8266-ard.yaml index dade44d145..4bed2f03e5 100644 --- a/tests/components/midea_ir/test.esp8266-ard.yaml +++ b/tests/components/midea_ir/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/mitsubishi/common.yaml b/tests/components/mitsubishi/common.yaml index c0fc959c5b..4a2deda163 100644 --- a/tests/components/mitsubishi/common.yaml +++ b/tests/components/mitsubishi/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: 4 - carrier_duty_percent: 50% - climate: - platform: mitsubishi name: Mitsubishi + transmitter_id: xmitr diff --git a/tests/components/mitsubishi/test.esp32-c3-idf.yaml b/tests/components/mitsubishi/test.esp32-c3-idf.yaml index dade44d145..43d5343715 100644 --- a/tests/components/mitsubishi/test.esp32-c3-idf.yaml +++ b/tests/components/mitsubishi/test.esp32-c3-idf.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp32-idf.yaml b/tests/components/mitsubishi/test.esp32-idf.yaml index dade44d145..e891f9dc85 100644 --- a/tests/components/mitsubishi/test.esp32-idf.yaml +++ b/tests/components/mitsubishi/test.esp32-idf.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/mitsubishi/test.esp8266-ard.yaml b/tests/components/mitsubishi/test.esp8266-ard.yaml index dade44d145..4bed2f03e5 100644 --- a/tests/components/mitsubishi/test.esp8266-ard.yaml +++ b/tests/components/mitsubishi/test.esp8266-ard.yaml @@ -1 +1,4 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/noblex/common.yaml b/tests/components/noblex/common.yaml index f5e471a9a7..8053d84d4b 100644 --- a/tests/components/noblex/common.yaml +++ b/tests/components/noblex/common.yaml @@ -1,12 +1,3 @@ -remote_receiver: - id: rcvr - pin: 4 - dump: all - -remote_transmitter: - pin: 2 - carrier_duty_percent: 50% - sensor: - platform: template id: noblex_ac_sensor diff --git a/tests/components/noblex/test.esp32-c3-idf.yaml b/tests/components/noblex/test.esp32-c3-idf.yaml index dade44d145..fe77c44eed 100644 --- a/tests/components/noblex/test.esp32-c3-idf.yaml +++ b/tests/components/noblex/test.esp32-c3-idf.yaml @@ -1 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-c3-idf.yaml + <<: !include common.yaml diff --git a/tests/components/noblex/test.esp32-idf.yaml b/tests/components/noblex/test.esp32-idf.yaml index dade44d145..b241dbd159 100644 --- a/tests/components/noblex/test.esp32-idf.yaml +++ b/tests/components/noblex/test.esp32-idf.yaml @@ -1 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml + <<: !include common.yaml diff --git a/tests/components/noblex/test.esp8266-ard.yaml b/tests/components/noblex/test.esp8266-ard.yaml index dade44d145..aa8651e556 100644 --- a/tests/components/noblex/test.esp8266-ard.yaml +++ b/tests/components/noblex/test.esp8266-ard.yaml @@ -1 +1,5 @@ +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml + remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml + <<: !include common.yaml diff --git a/tests/components/prometheus/common.yaml b/tests/components/prometheus/common.yaml index 9a16088ba0..a9354ebe3c 100644 --- a/tests/components/prometheus/common.yaml +++ b/tests/components/prometheus/common.yaml @@ -128,13 +128,10 @@ valve: optimistic: true has_position: true -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: climate_ir_lg name: LG Climate + transmitter_id: xmitr prometheus: include_internal: true diff --git a/tests/components/prometheus/test.esp32-c3-idf.yaml b/tests/components/prometheus/test.esp32-c3-idf.yaml index f00bca5947..fedeaf822a 100644 --- a/tests/components/prometheus/test.esp32-c3-idf.yaml +++ b/tests/components/prometheus/test.esp32-c3-idf.yaml @@ -1,5 +1,7 @@ substitutions: verify_ssl: "false" - pin: GPIO2 + +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/prometheus/test.esp32-idf.yaml b/tests/components/prometheus/test.esp32-idf.yaml index d60caadb05..e590417623 100644 --- a/tests/components/prometheus/test.esp32-idf.yaml +++ b/tests/components/prometheus/test.esp32-idf.yaml @@ -1,8 +1,8 @@ substitutions: verify_ssl: "false" - pin: GPIO2 packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml spi: !include ../../test_build_components/common/spi/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/prometheus/test.esp8266-ard.yaml b/tests/components/prometheus/test.esp8266-ard.yaml index 6ee1831769..bae76751e8 100644 --- a/tests/components/prometheus/test.esp8266-ard.yaml +++ b/tests/components/prometheus/test.esp8266-ard.yaml @@ -1,5 +1,7 @@ substitutions: verify_ssl: "false" - pin: GPIO5 + +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/tcl112/common.yaml b/tests/components/tcl112/common.yaml index 0e43de4a4a..1074712f94 100644 --- a/tests/components/tcl112/common.yaml +++ b/tests/components/tcl112/common.yaml @@ -1,7 +1,3 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - sensor: - platform: template id: tcl112_sensor @@ -13,3 +9,4 @@ climate: supports_heat: true supports_cool: true sensor: tcl112_sensor + transmitter_id: xmitr diff --git a/tests/components/tcl112/test.esp32-c3-idf.yaml b/tests/components/tcl112/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/tcl112/test.esp32-c3-idf.yaml +++ b/tests/components/tcl112/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/tcl112/test.esp32-idf.yaml b/tests/components/tcl112/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/tcl112/test.esp32-idf.yaml +++ b/tests/components/tcl112/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/tcl112/test.esp8266-ard.yaml b/tests/components/tcl112/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/tcl112/test.esp8266-ard.yaml +++ b/tests/components/tcl112/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/toshiba/common.yaml b/tests/components/toshiba/common.yaml index 79a833980e..ba96c0628d 100644 --- a/tests/components/toshiba/common.yaml +++ b/tests/components/toshiba/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: toshiba name: Toshiba Climate + transmitter_id: xmitr diff --git a/tests/components/toshiba/test.esp32-c3-idf.yaml b/tests/components/toshiba/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/toshiba/test.esp32-c3-idf.yaml +++ b/tests/components/toshiba/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/toshiba/test.esp32-idf.yaml b/tests/components/toshiba/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/toshiba/test.esp32-idf.yaml +++ b/tests/components/toshiba/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/toshiba/test.esp8266-ard.yaml b/tests/components/toshiba/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/toshiba/test.esp8266-ard.yaml +++ b/tests/components/toshiba/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/whirlpool/common.yaml b/tests/components/whirlpool/common.yaml index 804c1aac26..6d55db1f08 100644 --- a/tests/components/whirlpool/common.yaml +++ b/tests/components/whirlpool/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: whirlpool name: Whirlpool Climate + transmitter_id: xmitr diff --git a/tests/components/whirlpool/test.esp32-c3-idf.yaml b/tests/components/whirlpool/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/whirlpool/test.esp32-c3-idf.yaml +++ b/tests/components/whirlpool/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/whirlpool/test.esp32-idf.yaml b/tests/components/whirlpool/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/whirlpool/test.esp32-idf.yaml +++ b/tests/components/whirlpool/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/whirlpool/test.esp8266-ard.yaml b/tests/components/whirlpool/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/whirlpool/test.esp8266-ard.yaml +++ b/tests/components/whirlpool/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/whynter/common.yaml b/tests/components/whynter/common.yaml index 04ad6bed54..63df11dd91 100644 --- a/tests/components/whynter/common.yaml +++ b/tests/components/whynter/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: whynter name: Whynter Climate + transmitter_id: xmitr diff --git a/tests/components/whynter/test.esp32-c3-idf.yaml b/tests/components/whynter/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/whynter/test.esp32-c3-idf.yaml +++ b/tests/components/whynter/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/whynter/test.esp32-idf.yaml b/tests/components/whynter/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/whynter/test.esp32-idf.yaml +++ b/tests/components/whynter/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/whynter/test.esp8266-ard.yaml b/tests/components/whynter/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/whynter/test.esp8266-ard.yaml +++ b/tests/components/whynter/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/yashima/common.yaml b/tests/components/yashima/common.yaml index bfe181f1a6..431c27ebb3 100644 --- a/tests/components/yashima/common.yaml +++ b/tests/components/yashima/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: yashima name: Yashima Climate + transmitter_id: xmitr diff --git a/tests/components/yashima/test.esp32-c3-idf.yaml b/tests/components/yashima/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/yashima/test.esp32-c3-idf.yaml +++ b/tests/components/yashima/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/yashima/test.esp32-idf.yaml b/tests/components/yashima/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/yashima/test.esp32-idf.yaml +++ b/tests/components/yashima/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/yashima/test.esp8266-ard.yaml b/tests/components/yashima/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/yashima/test.esp8266-ard.yaml +++ b/tests/components/yashima/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/components/zhlt01/common.yaml b/tests/components/zhlt01/common.yaml index 0adbe77325..d0fd531c87 100644 --- a/tests/components/zhlt01/common.yaml +++ b/tests/components/zhlt01/common.yaml @@ -1,7 +1,4 @@ -remote_transmitter: - pin: ${pin} - carrier_duty_percent: 50% - climate: - platform: zhlt01 name: ZH/LT-01 Climate + transmitter_id: xmitr diff --git a/tests/components/zhlt01/test.esp32-c3-idf.yaml b/tests/components/zhlt01/test.esp32-c3-idf.yaml index 7b012aa64c..43d5343715 100644 --- a/tests/components/zhlt01/test.esp32-c3-idf.yaml +++ b/tests/components/zhlt01/test.esp32-c3-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml <<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp32-idf.yaml b/tests/components/zhlt01/test.esp32-idf.yaml index 7b012aa64c..e891f9dc85 100644 --- a/tests/components/zhlt01/test.esp32-idf.yaml +++ b/tests/components/zhlt01/test.esp32-idf.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO2 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml <<: !include common.yaml diff --git a/tests/components/zhlt01/test.esp8266-ard.yaml b/tests/components/zhlt01/test.esp8266-ard.yaml index f5097fcf5f..4bed2f03e5 100644 --- a/tests/components/zhlt01/test.esp8266-ard.yaml +++ b/tests/components/zhlt01/test.esp8266-ard.yaml @@ -1,4 +1,4 @@ -substitutions: - pin: GPIO5 +packages: + remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml <<: !include common.yaml diff --git a/tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml b/tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml new file mode 100644 index 0000000000..ad5acedf55 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-c3-idf.yaml @@ -0,0 +1,16 @@ +# Common remote_receiver configuration for ESP32-C3 IDF tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO5 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% + clock_resolution: 2000000 + filter_symbols: 2 + receive_symbols: 4 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_receiver/esp32-idf.yaml b/tests/test_build_components/common/remote_receiver/esp32-idf.yaml new file mode 100644 index 0000000000..2905e22233 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp32-idf.yaml @@ -0,0 +1,16 @@ +# Common remote_receiver configuration for ESP32 IDF tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO32 + +remote_receiver: + - id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% + clock_resolution: 2000000 + filter_symbols: 2 + receive_symbols: 4 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_receiver/esp8266-ard.yaml b/tests/test_build_components/common/remote_receiver/esp8266-ard.yaml new file mode 100644 index 0000000000..e2472d00c5 --- /dev/null +++ b/tests/test_build_components/common/remote_receiver/esp8266-ard.yaml @@ -0,0 +1,12 @@ +# Common remote_receiver configuration for ESP8266 Arduino tests +# Provides a shared remote receiver that all components can use +# Components will auto-use this receiver if they don't specify receiver_id + +substitutions: + remote_receiver_pin: GPIO5 + +remote_receiver: + id: rcvr + pin: ${remote_receiver_pin} + dump: all + tolerance: 25% diff --git a/tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml b/tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml new file mode 100644 index 0000000000..b951b8713f --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/bk72xx-ard.yaml @@ -0,0 +1,11 @@ +# Common remote_transmitter configuration for BK72XX Arduino tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO6 + +remote_transmitter: + id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% diff --git a/tests/test_build_components/common/remote_transmitter/esp32-ard.yaml b/tests/test_build_components/common/remote_transmitter/esp32-ard.yaml new file mode 100644 index 0000000000..4378e328af --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common remote_transmitter configuration for ESP32 Arduino tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% diff --git a/tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml b/tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml new file mode 100644 index 0000000000..b6b9a87fe1 --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp32-c3-idf.yaml @@ -0,0 +1,13 @@ +# Common remote_transmitter configuration for ESP32-C3 IDF tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + - id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% + clock_resolution: 2000000 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_transmitter/esp32-idf.yaml b/tests/test_build_components/common/remote_transmitter/esp32-idf.yaml new file mode 100644 index 0000000000..1d771b3edd --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp32-idf.yaml @@ -0,0 +1,13 @@ +# Common remote_transmitter configuration for ESP32 IDF tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + - id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% + clock_resolution: 2000000 + rmt_symbols: 64 diff --git a/tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml b/tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml new file mode 100644 index 0000000000..3be59c7997 --- /dev/null +++ b/tests/test_build_components/common/remote_transmitter/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common remote_transmitter configuration for ESP8266 Arduino tests +# Provides a shared remote transmitter that all components can use +# Components will auto-use this transmitter if they don't specify transmitter_id + +substitutions: + remote_transmitter_pin: GPIO2 + +remote_transmitter: + id: xmitr + pin: ${remote_transmitter_pin} + carrier_duty_percent: 50% From 931e3f80f0b1a70adf1264a75e4d568c90af9d1b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:25:03 -1000 Subject: [PATCH 03/22] no memory when tatget branch does not have --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efa9ce0bca..42f934de9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -534,6 +534,7 @@ jobs: ram_usage: ${{ steps.extract.outputs.ram_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }} cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }} + skip: ${{ steps.check-script.outputs.skip }} steps: - name: Check out target branch uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -735,7 +736,7 @@ jobs: - determine-jobs - memory-impact-target-branch - memory-impact-pr-branch - if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' + if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true' permissions: contents: read pull-requests: write From 5080698c3a7ed5b64429ce5d8b0fbfeddb635c9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:34:16 -1000 Subject: [PATCH 04/22] no memory when tatget branch does not have --- script/ci_memory_impact_comment.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index f381df0ff6..8b0dbb6f58 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -304,9 +304,9 @@ def create_detailed_breakdown_table( for comp, target_flash, pr_flash, delta in changed_components[:20]: target_str = format_bytes(target_flash) pr_str = format_bytes(pr_flash) - change_str = format_change( - target_flash, pr_flash, threshold=COMPONENT_CHANGE_THRESHOLD - ) + # Only apply threshold to ESPHome components, not framework/infrastructure + threshold = COMPONENT_CHANGE_THRESHOLD if comp.startswith("[esphome]") else None + change_str = format_change(target_flash, pr_flash, threshold=threshold) lines.append(f"| `{comp}` | {target_str} | {pr_str} | {change_str} |") if len(changed_components) > 20: From c70937ed01441c97f7c7d8132d05d635ac3a2534 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:55:05 -1000 Subject: [PATCH 05/22] dry --- script/analyze_component_buses.py | 14 +------ script/determine-jobs.py | 64 ++++++++++++++----------------- script/helpers.py | 62 +++++++++++++++++++++++++++--- script/list-components.py | 10 ++--- script/split_components_for_ci.py | 10 ++--- script/test_build_components.py | 9 +++-- 6 files changed, 100 insertions(+), 69 deletions(-) diff --git a/script/analyze_component_buses.py b/script/analyze_component_buses.py index d0882e22e9..78f5ca3344 100755 --- a/script/analyze_component_buses.py +++ b/script/analyze_component_buses.py @@ -34,6 +34,8 @@ from typing import Any # Add esphome to path sys.path.insert(0, str(Path(__file__).parent.parent)) +from helpers import BASE_BUS_COMPONENTS + from esphome import yaml_util from esphome.config_helpers import Extend, Remove @@ -67,18 +69,6 @@ NO_BUSES_SIGNATURE = "no_buses" # Isolated components have unique signatures and cannot be merged with others ISOLATED_SIGNATURE_PREFIX = "isolated_" -# Base bus components - these ARE the bus implementations and should not -# be flagged as needing migration since they are the platform/base components -BASE_BUS_COMPONENTS = { - "i2c", - "spi", - "uart", - "modbus", - "canbus", - "remote_transmitter", - "remote_receiver", -} - # Components that must be tested in isolation (not grouped or batched with others) # These have known build issues that prevent grouping # NOTE: This should be kept in sync with both test_build_components and split_components_for_ci.py diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 8e2c239fe2..5767ced859 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -38,6 +38,7 @@ Options: from __future__ import annotations import argparse +from collections import Counter from enum import StrEnum from functools import cache import json @@ -48,11 +49,13 @@ import sys from typing import Any from helpers import ( + BASE_BUS_COMPONENTS, CPP_FILE_EXTENSIONS, - ESPHOME_COMPONENTS_PATH, PYTHON_FILE_EXTENSIONS, changed_files, get_all_dependencies, + get_component_from_path, + get_component_test_files, get_components_from_integration_fixtures, parse_test_filename, root_path, @@ -142,12 +145,9 @@ def should_run_integration_tests(branch: str | None = None) -> bool: # Check if any required components changed for file in files: - if file.startswith(ESPHOME_COMPONENTS_PATH): - parts = file.split("/") - if len(parts) >= 3: - component = parts[2] - if component in all_required_components: - return True + component = get_component_from_path(file) + if component and component in all_required_components: + return True return False @@ -261,10 +261,7 @@ def _component_has_tests(component: str) -> bool: Returns: True if the component has test YAML files """ - tests_dir = Path(root_path) / "tests" / "components" / component - if not tests_dir.exists(): - return False - return any(tests_dir.glob("test.*.yaml")) + return bool(get_component_test_files(component)) def detect_memory_impact_config( @@ -291,17 +288,15 @@ def detect_memory_impact_config( files = changed_files(branch) # Find all changed components (excluding core and base bus components) - changed_component_set = set() + changed_component_set: set[str] = set() has_core_changes = False for file in files: - if file.startswith(ESPHOME_COMPONENTS_PATH): - parts = file.split("/") - if len(parts) >= 3: - component = parts[2] - # Skip base bus components as they're used across many builds - if component not in ["i2c", "spi", "uart", "modbus", "canbus"]: - changed_component_set.add(component) + component = get_component_from_path(file) + if component: + # Skip base bus components as they're used across many builds + if component not in BASE_BUS_COMPONENTS: + changed_component_set.add(component) elif file.startswith("esphome/"): # Core ESPHome files changed (not component-specific) has_core_changes = True @@ -321,25 +316,24 @@ def detect_memory_impact_config( return {"should_run": "false"} # Find components that have tests and collect their supported platforms - components_with_tests = [] - component_platforms_map = {} # Track which platforms each component supports + components_with_tests: list[str] = [] + component_platforms_map: dict[ + str, set[Platform] + ] = {} # Track which platforms each component supports for component in sorted(changed_component_set): - tests_dir = Path(root_path) / "tests" / "components" / component - if not tests_dir.exists(): - continue - # Look for test files on preferred platforms - test_files = list(tests_dir.glob("test.*.yaml")) + test_files = get_component_test_files(component) if not test_files: continue # Check if component has tests for any preferred platform - available_platforms = [] - for test_file in test_files: - _, platform = parse_test_filename(test_file) - if platform != "all" and platform in MEMORY_IMPACT_PLATFORM_PREFERENCE: - available_platforms.append(platform) + available_platforms = [ + platform + for test_file in test_files + if (platform := parse_test_filename(test_file)[1]) != "all" + and platform in MEMORY_IMPACT_PLATFORM_PREFERENCE + ] if not available_platforms: continue @@ -367,10 +361,10 @@ def detect_memory_impact_config( else: # No common platform - pick the most commonly supported platform # This allows testing components individually even if they can't be merged - platform_counts = {} - for platforms in component_platforms_map.values(): - for p in platforms: - platform_counts[p] = platform_counts.get(p, 0) + 1 + # Count how many components support each platform + platform_counts = Counter( + p for platforms in component_platforms_map.values() for p in platforms + ) # Pick the platform supported by most components, preferring earlier in MEMORY_IMPACT_PLATFORM_PREFERENCE platform = max( platform_counts.keys(), diff --git a/script/helpers.py b/script/helpers.py index 85e568dcf8..edde3d78af 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -29,6 +29,18 @@ YAML_FILE_EXTENSIONS = (".yaml", ".yml") # Component path prefix ESPHOME_COMPONENTS_PATH = "esphome/components/" +# Base bus components - these ARE the bus implementations and should not +# be flagged as needing migration since they are the platform/base components +BASE_BUS_COMPONENTS = { + "i2c", + "spi", + "uart", + "modbus", + "canbus", + "remote_transmitter", + "remote_receiver", +} + def parse_list_components_output(output: str) -> list[str]: """Parse the output from list-components.py script. @@ -63,6 +75,48 @@ def parse_test_filename(test_file: Path) -> tuple[str, str]: return parts[0], "all" +def get_component_from_path(file_path: str) -> str | None: + """Extract component name from a file path. + + Args: + file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp") + + Returns: + Component name if path is in components directory, None otherwise + """ + if not file_path.startswith(ESPHOME_COMPONENTS_PATH): + return None + parts = file_path.split("/") + if len(parts) >= 3: + return parts[2] + return None + + +def get_component_test_files( + component: str, *, all_variants: bool = False +) -> list[Path]: + """Get test files for a component. + + Args: + component: Component name (e.g., "wifi") + all_variants: If True, returns all test files including variants (test-*.yaml). + If False, returns only base test files (test.*.yaml). + Default is False. + + Returns: + List of test file paths for the component, or empty list if none exist + """ + tests_dir = Path(root_path) / "tests" / "components" / component + if not tests_dir.exists(): + return [] + + if all_variants: + # Match both test.*.yaml and test-*.yaml patterns + return list(tests_dir.glob("test[.-]*.yaml")) + # Match only test.*.yaml (base tests) + return list(tests_dir.glob("test.*.yaml")) + + def styled(color: str | tuple[str, ...], msg: str, reset: bool = True) -> str: prefix = "".join(color) if isinstance(color, tuple) else color suffix = colorama.Style.RESET_ALL if reset else "" @@ -331,11 +385,9 @@ def _filter_changed_ci(files: list[str]) -> list[str]: # because changes in one file can affect other files in the same component. filtered_files = [] for f in files: - if f.startswith(ESPHOME_COMPONENTS_PATH): - # Check if file belongs to any of the changed components - parts = f.split("/") - if len(parts) >= 3 and parts[2] in component_set: - filtered_files.append(f) + component = get_component_from_path(f) + if component and component in component_set: + filtered_files.append(f) return filtered_files diff --git a/script/list-components.py b/script/list-components.py index 9abb2bc345..11533ceb30 100755 --- a/script/list-components.py +++ b/script/list-components.py @@ -4,7 +4,7 @@ from collections.abc import Callable from pathlib import Path import sys -from helpers import changed_files, git_ls_files +from helpers import changed_files, get_component_from_path, git_ls_files from esphome.const import ( KEY_CORE, @@ -30,11 +30,9 @@ def get_all_component_files() -> list[str]: def extract_component_names_array_from_files_array(files): components = [] for file in files: - file_parts = file.split("/") - if len(file_parts) >= 4: - component_name = file_parts[2] - if component_name not in components: - components.append(component_name) + component_name = get_component_from_path(file) + if component_name and component_name not in components: + components.append(component_name) return components diff --git a/script/split_components_for_ci.py b/script/split_components_for_ci.py index dff46d3619..6ba2598eda 100755 --- a/script/split_components_for_ci.py +++ b/script/split_components_for_ci.py @@ -28,6 +28,7 @@ from script.analyze_component_buses import ( create_grouping_signature, merge_compatible_bus_groups, ) +from script.helpers import get_component_test_files # Weighting for batch creation # Isolated components can't be grouped/merged, so they count as 10x @@ -45,17 +46,12 @@ def has_test_files(component_name: str, tests_dir: Path) -> bool: Args: component_name: Name of the component - tests_dir: Path to tests/components directory + tests_dir: Path to tests/components directory (unused, kept for compatibility) Returns: True if the component has test.*.yaml files """ - component_dir = tests_dir / component_name - if not component_dir.exists() or not component_dir.is_dir(): - return False - - # Check for test.*.yaml files - return any(component_dir.glob("test.*.yaml")) + return bool(get_component_test_files(component_name)) def create_intelligent_batches( diff --git a/script/test_build_components.py b/script/test_build_components.py index 07f2680799..77c97a8773 100755 --- a/script/test_build_components.py +++ b/script/test_build_components.py @@ -39,6 +39,7 @@ from script.analyze_component_buses import ( merge_compatible_bus_groups, uses_local_file_references, ) +from script.helpers import get_component_test_files from script.merge_component_configs import merge_component_configs @@ -100,10 +101,10 @@ def find_component_tests( if not comp_dir.is_dir(): continue - # Find test files - either base only (test.*.yaml) or all (test[.-]*.yaml) - pattern = "test.*.yaml" if base_only else "test[.-]*.yaml" - for test_file in comp_dir.glob(pattern): - component_tests[comp_dir.name].append(test_file) + # Get test files using helper function + test_files = get_component_test_files(comp_dir.name, all_variants=not base_only) + if test_files: + component_tests[comp_dir.name] = test_files return dict(component_tests) From b95999aca7cc2d39ff6846627b8a0936f409465d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:55:37 -1000 Subject: [PATCH 06/22] Update esphome/analyze_memory/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/analyze_memory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 5ef9eab526..74299d4e95 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -295,7 +295,7 @@ class MemoryAnalyzer: cppfilt_cmd = "c++filt" _LOGGER.warning("Demangling %d symbols", len(symbols)) - _LOGGER.warning("objdump_path = %s", self.objdump_path) + _LOGGER.debug("objdump_path = %s", self.objdump_path) # Check if we have a toolchain-specific c++filt if self.objdump_path and self.objdump_path != "objdump": From 9a4288d81a02e7484e393f97a89fab856ae6e4e9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:56:41 -1000 Subject: [PATCH 07/22] Update script/determine-jobs.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- script/determine-jobs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 5767ced859..26e91edbe1 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -26,7 +26,7 @@ The CI workflow uses this information to: - Skip or run Python linters (ruff, flake8, pylint, pyupgrade) - Determine which components to test individually - Decide how to split component tests (if there are many) -- Run memory impact analysis when exactly one component changes +- Run memory impact analysis whenever there are changed components (merged config), and also for core-only changes Usage: python script/determine-jobs.py [-b BRANCH] From a96cc5e6f20a8b7205a48ea38836fb22ff012239 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:57:33 -1000 Subject: [PATCH 08/22] Update esphome/analyze_memory/__init__.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/analyze_memory/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 74299d4e95..3e85c4d869 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -294,24 +294,24 @@ class MemoryAnalyzer: # Try to find the appropriate c++filt for the platform cppfilt_cmd = "c++filt" - _LOGGER.warning("Demangling %d symbols", len(symbols)) + _LOGGER.info("Demangling %d symbols", len(symbols)) _LOGGER.debug("objdump_path = %s", self.objdump_path) # Check if we have a toolchain-specific c++filt if self.objdump_path and self.objdump_path != "objdump": # Replace objdump with c++filt in the path potential_cppfilt = self.objdump_path.replace("objdump", "c++filt") - _LOGGER.warning("Checking for toolchain c++filt at: %s", potential_cppfilt) + _LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt) if Path(potential_cppfilt).exists(): cppfilt_cmd = potential_cppfilt - _LOGGER.warning("✓ Using toolchain c++filt: %s", cppfilt_cmd) + _LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd) else: - _LOGGER.warning( + _LOGGER.info( "✗ Toolchain c++filt not found at %s, using system c++filt", potential_cppfilt, ) else: - _LOGGER.warning( + _LOGGER.info( "✗ Using system c++filt (objdump_path=%s)", self.objdump_path ) From 0b09e506854decd24b44a6ee77e2831f5a193859 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 17:57:42 -1000 Subject: [PATCH 09/22] preen --- esphome/analyze_memory/cli.py | 4 ++-- script/determine-jobs.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 7b004353ec..bcf9f45de9 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -183,9 +183,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" ) - # Top 10 largest core symbols + # Top 15 largest core symbols lines.append("") - lines.append("Top 10 Largest [esphome]core Symbols:") + lines.append("Top 15 Largest [esphome]core Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 5767ced859..bcc357d953 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -13,9 +13,9 @@ what files have changed. It outputs JSON with the following structure: "component_test_count": 5, "memory_impact": { "should_run": "true/false", - "component": "component_name", - "test_file": "test.esp32-idf.yaml", - "platform": "esp32-idf" + "components": ["component1", "component2", ...], + "platform": "esp32-idf", + "use_merged_config": "true" } } @@ -26,7 +26,7 @@ The CI workflow uses this information to: - Skip or run Python linters (ruff, flake8, pylint, pyupgrade) - Determine which components to test individually - Decide how to split component tests (if there are many) -- Run memory impact analysis when exactly one component changes +- Run memory impact analysis when components change Usage: python script/determine-jobs.py [-b BRANCH] From bbd636a8cc7fecd046ef16cc919a71bf37e3db97 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 18 Oct 2025 03:59:23 +0000 Subject: [PATCH 10/22] [pre-commit.ci lite] apply automatic fixes --- esphome/analyze_memory/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 3e85c4d869..b5d574807e 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -311,9 +311,7 @@ class MemoryAnalyzer: potential_cppfilt, ) else: - _LOGGER.info( - "✗ Using system c++filt (objdump_path=%s)", self.objdump_path - ) + _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path) # Strip GCC optimization suffixes and prefixes before demangling # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt From 9cf1fd24fd5e1a91bbc5fe49ed941b60dce5eb49 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:06:13 -1000 Subject: [PATCH 11/22] preen --- esphome/analyze_memory/__init__.py | 42 +++---- esphome/analyze_memory/cli.py | 2 +- script/ci_memory_impact_comment.py | 196 +++++++++++++---------------- script/ci_memory_impact_extract.py | 15 +-- 4 files changed, 116 insertions(+), 139 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 3e85c4d869..942caabe70 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -77,7 +77,7 @@ class MemoryAnalyzer: readelf_path: str | None = None, external_components: set[str] | None = None, idedata: "IDEData | None" = None, - ): + ) -> None: """Initialize memory analyzer. Args: @@ -311,15 +311,13 @@ class MemoryAnalyzer: potential_cppfilt, ) else: - _LOGGER.info( - "✗ Using system c++filt (objdump_path=%s)", self.objdump_path - ) + _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path) # Strip GCC optimization suffixes and prefixes before demangling # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt # Prefixes like _GLOBAL__sub_I_ need to be removed and tracked - symbols_stripped = [] - symbols_prefixes = [] # Track removed prefixes + symbols_stripped: list[str] = [] + symbols_prefixes: list[str] = [] # Track removed prefixes for symbol in symbols: # Remove GCC optimization markers stripped = re.sub(r"\$(?:isra|part|constprop)\$\d+", "", symbol) @@ -327,12 +325,11 @@ class MemoryAnalyzer: # Handle GCC global constructor/initializer prefixes # _GLOBAL__sub_I_ -> extract for demangling prefix = "" - if stripped.startswith("_GLOBAL__sub_I_"): - prefix = "_GLOBAL__sub_I_" - stripped = stripped[len(prefix) :] - elif stripped.startswith("_GLOBAL__sub_D_"): - prefix = "_GLOBAL__sub_D_" - stripped = stripped[len(prefix) :] + for gcc_prefix in _GCC_PREFIX_ANNOTATIONS: + if stripped.startswith(gcc_prefix): + prefix = gcc_prefix + stripped = stripped[len(prefix) :] + break symbols_stripped.append(stripped) symbols_prefixes.append(prefix) @@ -405,17 +402,18 @@ class MemoryAnalyzer: if stripped == demangled and stripped.startswith("_Z"): failed_count += 1 if failed_count <= 5: # Only log first 5 failures - _LOGGER.warning("Failed to demangle: %s", original[:100]) + _LOGGER.warning("Failed to demangle: %s", original) - if failed_count > 0: - _LOGGER.warning( - "Failed to demangle %d/%d symbols using %s", - failed_count, - len(symbols), - cppfilt_cmd, - ) - else: - _LOGGER.warning("Successfully demangled all %d symbols", len(symbols)) + if failed_count == 0: + _LOGGER.info("Successfully demangled all %d symbols", len(symbols)) + return + + _LOGGER.warning( + "Failed to demangle %d/%d symbols using %s", + failed_count, + len(symbols), + cppfilt_cmd, + ) @staticmethod def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str: diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index bcf9f45de9..a2366430dd 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -83,7 +83,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): total_ram = sum(c.ram_total for _, c in components) # Build report - lines = [] + lines: list[str] = [] lines.append("=" * self.TABLE_WIDTH) lines.append("Component Memory Analysis".center(self.TABLE_WIDTH)) diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index 8b0dbb6f58..d177b101a8 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -411,137 +411,115 @@ def find_existing_comment(pr_number: str) -> str | None: Returns: Comment numeric ID if found, None otherwise + + Raises: + subprocess.CalledProcessError: If gh command fails """ - try: - print( - f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr - ) + print(f"DEBUG: Looking for existing comment on PR #{pr_number}", file=sys.stderr) - # Use gh api to get comments directly - this returns the numeric id field - result = subprocess.run( - [ - "gh", - "api", - f"/repos/{{owner}}/{{repo}}/issues/{pr_number}/comments", - "--jq", - ".[] | {id, body}", - ], - capture_output=True, - text=True, - check=True, - ) + # Use gh api to get comments directly - this returns the numeric id field + result = subprocess.run( + [ + "gh", + "api", + f"/repos/{{owner}}/{{repo}}/issues/{pr_number}/comments", + "--jq", + ".[] | {id, body}", + ], + capture_output=True, + text=True, + check=True, + ) - print( - f"DEBUG: gh api comments output (first 500 chars):\n{result.stdout[:500]}", - file=sys.stderr, - ) + print( + f"DEBUG: gh api comments output (first 500 chars):\n{result.stdout[:500]}", + file=sys.stderr, + ) - # Parse comments and look for our marker - comment_count = 0 - for line in result.stdout.strip().split("\n"): - if not line: - continue + # Parse comments and look for our marker + comment_count = 0 + for line in result.stdout.strip().split("\n"): + if not line: + continue - try: - comment = json.loads(line) - comment_count += 1 - comment_id = comment.get("id") + try: + comment = json.loads(line) + comment_count += 1 + comment_id = comment.get("id") + print( + f"DEBUG: Checking comment {comment_count}: id={comment_id}", + file=sys.stderr, + ) + + body = comment.get("body", "") + if COMMENT_MARKER in body: print( - f"DEBUG: Checking comment {comment_count}: id={comment_id}", + f"DEBUG: Found existing comment with id={comment_id}", file=sys.stderr, ) + # Return the numeric id + return str(comment_id) + print("DEBUG: Comment does not contain marker", file=sys.stderr) + except json.JSONDecodeError as e: + print(f"DEBUG: JSON decode error: {e}", file=sys.stderr) + continue - body = comment.get("body", "") - if COMMENT_MARKER in body: - print( - f"DEBUG: Found existing comment with id={comment_id}", - file=sys.stderr, - ) - # Return the numeric id - return str(comment_id) - print("DEBUG: Comment does not contain marker", file=sys.stderr) - except json.JSONDecodeError as e: - print(f"DEBUG: JSON decode error: {e}", file=sys.stderr) - continue - - print( - f"DEBUG: No existing comment found (checked {comment_count} comments)", - file=sys.stderr, - ) - return None - - except subprocess.CalledProcessError as e: - print(f"Error finding existing comment: {e}", file=sys.stderr) - if e.stderr: - print(f"stderr: {e.stderr.decode()}", file=sys.stderr) - return None + print( + f"DEBUG: No existing comment found (checked {comment_count} comments)", + file=sys.stderr, + ) + return None -def post_or_update_comment(pr_number: str, comment_body: str) -> bool: +def post_or_update_comment(pr_number: str, comment_body: str) -> None: """Post a new comment or update existing one. Args: pr_number: PR number comment_body: Comment body text - Returns: - True if successful, False otherwise + Raises: + subprocess.CalledProcessError: If gh command fails """ # Look for existing comment existing_comment_id = find_existing_comment(pr_number) - try: - if existing_comment_id and existing_comment_id != "None": - # Update existing comment - print( - f"DEBUG: Updating existing comment {existing_comment_id}", - file=sys.stderr, - ) - result = subprocess.run( - [ - "gh", - "api", - f"/repos/{{owner}}/{{repo}}/issues/comments/{existing_comment_id}", - "-X", - "PATCH", - "-f", - f"body={comment_body}", - ], - check=True, - capture_output=True, - text=True, - ) - print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) - else: - # Post new comment - print( - f"DEBUG: Posting new comment (existing_comment_id={existing_comment_id})", - file=sys.stderr, - ) - result = subprocess.run( - ["gh", "pr", "comment", pr_number, "--body", comment_body], - check=True, - capture_output=True, - text=True, - ) - print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) + if existing_comment_id and existing_comment_id != "None": + # Update existing comment + print( + f"DEBUG: Updating existing comment {existing_comment_id}", + file=sys.stderr, + ) + result = subprocess.run( + [ + "gh", + "api", + f"/repos/{{owner}}/{{repo}}/issues/comments/{existing_comment_id}", + "-X", + "PATCH", + "-f", + f"body={comment_body}", + ], + check=True, + capture_output=True, + text=True, + ) + print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) + else: + # Post new comment + print( + f"DEBUG: Posting new comment (existing_comment_id={existing_comment_id})", + file=sys.stderr, + ) + result = subprocess.run( + ["gh", "pr", "comment", pr_number, "--body", comment_body], + check=True, + capture_output=True, + text=True, + ) + print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) - print("Comment posted/updated successfully", file=sys.stderr) - return True - - except subprocess.CalledProcessError as e: - print(f"Error posting/updating comment: {e}", file=sys.stderr) - if e.stderr: - print( - f"stderr: {e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr}", - file=sys.stderr, - ) - if e.stdout: - print( - f"stdout: {e.stdout.decode() if isinstance(e.stdout, bytes) else e.stdout}", - file=sys.stderr, - ) - return False + print("Comment posted/updated successfully", file=sys.stderr) def main() -> int: diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index 76632ebc33..5522d522f0 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -27,6 +27,11 @@ sys.path.insert(0, str(Path(__file__).parent.parent)) # pylint: disable=wrong-import-position from script.ci_helpers import write_github_output +# Regex patterns for extracting memory usage from PlatformIO output +_RAM_PATTERN = re.compile(r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes") +_FLASH_PATTERN = re.compile(r"Flash:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes") +_BUILD_PATH_PATTERN = re.compile(r"Build path: (.+)") + def extract_from_compile_output( output_text: str, @@ -42,7 +47,7 @@ def extract_from_compile_output( Flash: [=== ] 34.0% (used 348511 bytes from 1023984 bytes) Also extracts build directory from lines like: - INFO Deleting /path/to/build/.esphome/build/componenttestesp8266ard/.pioenvs + INFO Compiling app... Build path: /path/to/build Args: output_text: Compile output text (may contain multiple builds) @@ -51,12 +56,8 @@ def extract_from_compile_output( Tuple of (total_ram_bytes, total_flash_bytes, build_dir) or (None, None, None) if not found """ # Find all RAM and Flash matches (may be multiple builds) - ram_matches = re.findall( - r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", output_text - ) - flash_matches = re.findall( - r"Flash:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", output_text - ) + ram_matches = _RAM_PATTERN.findall(output_text) + flash_matches = _FLASH_PATTERN.findall(output_text) if not ram_matches or not flash_matches: return None, None, None From 0b077bdfc62c2d2923356ecec37ad27821b610aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:08:52 -1000 Subject: [PATCH 12/22] preen --- script/ci_memory_impact_extract.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index 5522d522f0..17ac788ae3 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -70,7 +70,7 @@ def extract_from_compile_output( # Look for: INFO Compiling app... Build path: /path/to/build # Note: Multiple builds reuse the same build path (each overwrites the previous) build_dir = None - if match := re.search(r"Build path: (.+)", output_text): + if match := _BUILD_PATH_PATTERN.search(output_text): build_dir = match.group(1).strip() return total_ram, total_flash, build_dir @@ -210,11 +210,7 @@ def main() -> int: return 1 # Count how many builds were found - num_builds = len( - re.findall( - r"RAM:\s+\[.*?\]\s+\d+\.\d+%\s+\(used\s+(\d+)\s+bytes", compile_output - ) - ) + num_builds = len(_RAM_PATTERN.findall(compile_output)) if num_builds > 1: print( From 07ad32968e585919374e9c7c891bdb355501a9f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:15:46 -1000 Subject: [PATCH 13/22] template all the things --- script/ci_memory_impact_comment.py | 296 +++++++----------- .../ci_memory_impact_comment_template.j2 | 27 ++ .../ci_memory_impact_component_breakdown.j2 | 15 + script/templates/ci_memory_impact_macros.j2 | 8 + .../ci_memory_impact_symbol_changes.j2 | 51 +++ 5 files changed, 216 insertions(+), 181 deletions(-) create mode 100644 script/templates/ci_memory_impact_comment_template.j2 create mode 100644 script/templates/ci_memory_impact_component_breakdown.j2 create mode 100644 script/templates/ci_memory_impact_macros.j2 create mode 100644 script/templates/ci_memory_impact_symbol_changes.j2 diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index d177b101a8..961c304e40 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -14,6 +14,8 @@ from pathlib import Path import subprocess import sys +from jinja2 import Environment, FileSystemLoader + # Add esphome to path for analyze_memory import sys.path.insert(0, str(Path(__file__).parent.parent)) @@ -26,6 +28,22 @@ COMMENT_MARKER = "" OVERALL_CHANGE_THRESHOLD = 1.0 # Overall RAM/Flash changes COMPONENT_CHANGE_THRESHOLD = 3.0 # Component breakdown changes +# Display limits for tables +MAX_COMPONENT_BREAKDOWN_ROWS = 20 # Maximum components to show in breakdown table +MAX_CHANGED_SYMBOLS_ROWS = 30 # Maximum changed symbols to show +MAX_NEW_SYMBOLS_ROWS = 15 # Maximum new symbols to show +MAX_REMOVED_SYMBOLS_ROWS = 15 # Maximum removed symbols to show + +# Symbol display formatting +SYMBOL_DISPLAY_MAX_LENGTH = 100 # Max length before using
tag +SYMBOL_DISPLAY_TRUNCATE_LENGTH = 97 # Length to truncate in summary + +# Component change noise threshold +COMPONENT_CHANGE_NOISE_THRESHOLD = 2 # Ignore component changes ≤ this many bytes + +# Template directory +TEMPLATE_DIR = Path(__file__).parent / "templates" + def load_analysis_json(json_path: str) -> dict | None: """Load memory analysis results from JSON file. @@ -111,35 +129,20 @@ def format_change(before: int, after: int, threshold: float | None = None) -> st return f"{emoji} {delta_str} ({pct_str})" -def format_symbol_for_display(symbol: str) -> str: - """Format a symbol name for display in markdown table. - - Args: - symbol: Symbol name to format - - Returns: - Formatted symbol with backticks or HTML details tag for long names - """ - if len(symbol) <= 100: - return f"`{symbol}`" - # Use HTML details for very long symbols (no backticks inside HTML) - return f"
{symbol[:97]}...{symbol}
" - - -def create_symbol_changes_table( +def prepare_symbol_changes_data( target_symbols: dict | None, pr_symbols: dict | None -) -> str: - """Create a markdown table showing symbols that changed size. +) -> dict | None: + """Prepare symbol changes data for template rendering. Args: target_symbols: Symbol name to size mapping for target branch pr_symbols: Symbol name to size mapping for PR branch Returns: - Formatted markdown table + Dictionary with changed, new, and removed symbols, or None if no changes """ if not target_symbols or not pr_symbols: - return "" + return None # Find all symbols that exist in both branches or only in one all_symbols = set(target_symbols.keys()) | set(pr_symbols.keys()) @@ -165,113 +168,39 @@ def create_symbol_changes_table( changed_symbols.append((symbol, target_size, pr_size, delta)) if not changed_symbols and not new_symbols and not removed_symbols: - return "" + return None - lines = [ - "", - "
", - "🔍 Symbol-Level Changes (click to expand)", - "", - ] + # Sort by size/delta + changed_symbols.sort(key=lambda x: abs(x[3]), reverse=True) + new_symbols.sort(key=lambda x: x[1], reverse=True) + removed_symbols.sort(key=lambda x: x[1], reverse=True) - # Show changed symbols (sorted by absolute delta) - if changed_symbols: - changed_symbols.sort(key=lambda x: abs(x[3]), reverse=True) - lines.extend( - [ - "### Changed Symbols", - "", - "| Symbol | Target Size | PR Size | Change |", - "|--------|-------------|---------|--------|", - ] - ) - - # Show top 30 changes - for symbol, target_size, pr_size, delta in changed_symbols[:30]: - target_str = format_bytes(target_size) - pr_str = format_bytes(pr_size) - change_str = format_change(target_size, pr_size) # Chart icons only - display_symbol = format_symbol_for_display(symbol) - lines.append( - f"| {display_symbol} | {target_str} | {pr_str} | {change_str} |" - ) - - if len(changed_symbols) > 30: - lines.append( - f"| ... | ... | ... | *({len(changed_symbols) - 30} more changed symbols not shown)* |" - ) - lines.append("") - - # Show new symbols - if new_symbols: - new_symbols.sort(key=lambda x: x[1], reverse=True) - lines.extend( - [ - "### New Symbols (top 15)", - "", - "| Symbol | Size |", - "|--------|------|", - ] - ) - - for symbol, size in new_symbols[:15]: - display_symbol = format_symbol_for_display(symbol) - lines.append(f"| {display_symbol} | {format_bytes(size)} |") - - if len(new_symbols) > 15: - total_new_size = sum(s[1] for s in new_symbols) - lines.append( - f"| *{len(new_symbols) - 15} more new symbols...* | *Total: {format_bytes(total_new_size)}* |" - ) - lines.append("") - - # Show removed symbols - if removed_symbols: - removed_symbols.sort(key=lambda x: x[1], reverse=True) - lines.extend( - [ - "### Removed Symbols (top 15)", - "", - "| Symbol | Size |", - "|--------|------|", - ] - ) - - for symbol, size in removed_symbols[:15]: - display_symbol = format_symbol_for_display(symbol) - lines.append(f"| {display_symbol} | {format_bytes(size)} |") - - if len(removed_symbols) > 15: - total_removed_size = sum(s[1] for s in removed_symbols) - lines.append( - f"| *{len(removed_symbols) - 15} more removed symbols...* | *Total: {format_bytes(total_removed_size)}* |" - ) - lines.append("") - - lines.extend(["
", ""]) - - return "\n".join(lines) + return { + "changed_symbols": changed_symbols, + "new_symbols": new_symbols, + "removed_symbols": removed_symbols, + } -def create_detailed_breakdown_table( +def prepare_component_breakdown_data( target_analysis: dict | None, pr_analysis: dict | None -) -> str: - """Create a markdown table showing detailed memory breakdown by component. +) -> list[tuple[str, int, int, int]] | None: + """Prepare component breakdown data for template rendering. Args: target_analysis: Component memory breakdown for target branch pr_analysis: Component memory breakdown for PR branch Returns: - Formatted markdown table + List of tuples (component, target_flash, pr_flash, delta), or None if no changes """ if not target_analysis or not pr_analysis: - return "" + return None # Combine all components from both analyses all_components = set(target_analysis.keys()) | set(pr_analysis.keys()) - # Filter to components that have changed (ignoring noise ≤2 bytes) + # Filter to components that have changed (ignoring noise) changed_components = [] for comp in all_components: target_mem = target_analysis.get(comp, {}) @@ -280,43 +209,18 @@ def create_detailed_breakdown_table( target_flash = target_mem.get("flash_total", 0) pr_flash = pr_mem.get("flash_total", 0) - # Only include if component has meaningful change (>2 bytes) + # Only include if component has meaningful change (above noise threshold) delta = pr_flash - target_flash - if abs(delta) > 2: + if abs(delta) > COMPONENT_CHANGE_NOISE_THRESHOLD: changed_components.append((comp, target_flash, pr_flash, delta)) if not changed_components: - return "" + return None # Sort by absolute delta (largest changes first) changed_components.sort(key=lambda x: abs(x[3]), reverse=True) - # Build table - limit to top 20 changes - lines = [ - "", - "
", - "📊 Component Memory Breakdown", - "", - "| Component | Target Flash | PR Flash | Change |", - "|-----------|--------------|----------|--------|", - ] - - for comp, target_flash, pr_flash, delta in changed_components[:20]: - target_str = format_bytes(target_flash) - pr_str = format_bytes(pr_flash) - # Only apply threshold to ESPHome components, not framework/infrastructure - threshold = COMPONENT_CHANGE_THRESHOLD if comp.startswith("[esphome]") else None - change_str = format_change(target_flash, pr_flash, threshold=threshold) - lines.append(f"| `{comp}` | {target_str} | {pr_str} | {change_str} |") - - if len(changed_components) > 20: - lines.append( - f"| ... | ... | ... | *({len(changed_components) - 20} more components not shown)* |" - ) - - lines.extend(["", "
", ""]) - - return "\n".join(lines) + return changed_components def create_comment_body( @@ -332,7 +236,7 @@ def create_comment_body( pr_symbols: dict | None = None, target_cache_hit: bool = False, ) -> str: - """Create the comment body with memory impact analysis. + """Create the comment body with memory impact analysis using Jinja2 templates. Args: components: List of component names (merged config) @@ -350,57 +254,87 @@ def create_comment_body( Returns: Formatted comment body """ - ram_change = format_change(target_ram, pr_ram, threshold=OVERALL_CHANGE_THRESHOLD) - flash_change = format_change( - target_flash, pr_flash, threshold=OVERALL_CHANGE_THRESHOLD + # Set up Jinja2 environment + env = Environment( + loader=FileSystemLoader(TEMPLATE_DIR), + trim_blocks=True, + lstrip_blocks=True, ) - # Use provided analysis data if available - component_breakdown = "" - symbol_changes = "" + # Register custom filters + env.filters["format_bytes"] = format_bytes + env.filters["format_change"] = format_change - if target_analysis and pr_analysis: - component_breakdown = create_detailed_breakdown_table( - target_analysis, pr_analysis - ) - - if target_symbols and pr_symbols: - symbol_changes = create_symbol_changes_table(target_symbols, pr_symbols) - else: - print("No ELF files provided, skipping detailed analysis", file=sys.stderr) + # Prepare template context + context = { + "comment_marker": COMMENT_MARKER, + "platform": platform, + "target_ram": format_bytes(target_ram), + "pr_ram": format_bytes(pr_ram), + "target_flash": format_bytes(target_flash), + "pr_flash": format_bytes(pr_flash), + "ram_change": format_change( + target_ram, pr_ram, threshold=OVERALL_CHANGE_THRESHOLD + ), + "flash_change": format_change( + target_flash, pr_flash, threshold=OVERALL_CHANGE_THRESHOLD + ), + "target_cache_hit": target_cache_hit, + "component_change_threshold": COMPONENT_CHANGE_THRESHOLD, + } # Format components list if len(components) == 1: - components_str = f"`{components[0]}`" - config_note = "a representative test configuration" + context["components_str"] = f"`{components[0]}`" + context["config_note"] = "a representative test configuration" else: - components_str = ", ".join(f"`{c}`" for c in sorted(components)) - config_note = f"a merged configuration with {len(components)} components" + context["components_str"] = ", ".join(f"`{c}`" for c in sorted(components)) + context["config_note"] = ( + f"a merged configuration with {len(components)} components" + ) - # Add cache info note if target was cached - cache_note = "" - if target_cache_hit: - cache_note = "\n\n> ⚡ Target branch analysis was loaded from cache (build skipped for faster CI)." + # Prepare component breakdown if available + component_breakdown = "" + if target_analysis and pr_analysis: + changed_components = prepare_component_breakdown_data( + target_analysis, pr_analysis + ) + if changed_components: + template = env.get_template("ci_memory_impact_component_breakdown.j2") + component_breakdown = template.render( + changed_components=changed_components, + format_bytes=format_bytes, + format_change=format_change, + component_change_threshold=COMPONENT_CHANGE_THRESHOLD, + max_rows=MAX_COMPONENT_BREAKDOWN_ROWS, + ) - return f"""{COMMENT_MARKER} -## Memory Impact Analysis + # Prepare symbol changes if available + symbol_changes = "" + if target_symbols and pr_symbols: + symbol_data = prepare_symbol_changes_data(target_symbols, pr_symbols) + if symbol_data: + template = env.get_template("ci_memory_impact_symbol_changes.j2") + symbol_changes = template.render( + **symbol_data, + format_bytes=format_bytes, + format_change=format_change, + max_changed_rows=MAX_CHANGED_SYMBOLS_ROWS, + max_new_rows=MAX_NEW_SYMBOLS_ROWS, + max_removed_rows=MAX_REMOVED_SYMBOLS_ROWS, + symbol_max_length=SYMBOL_DISPLAY_MAX_LENGTH, + symbol_truncate_length=SYMBOL_DISPLAY_TRUNCATE_LENGTH, + ) -**Components:** {components_str} -**Platform:** `{platform}` + if not target_analysis or not pr_analysis: + print("No ELF files provided, skipping detailed analysis", file=sys.stderr) -| Metric | Target Branch | This PR | Change | -|--------|--------------|---------|--------| -| **RAM** | {format_bytes(target_ram)} | {format_bytes(pr_ram)} | {ram_change} | -| **Flash** | {format_bytes(target_flash)} | {format_bytes(pr_flash)} | {flash_change} | -{component_breakdown}{symbol_changes}{cache_note} + context["component_breakdown"] = component_breakdown + context["symbol_changes"] = symbol_changes ---- -> **Note:** This analysis measures **static RAM and Flash usage** only (compile-time allocation). -> **Dynamic memory (heap)** cannot be measured automatically. -> **⚠️ You must test this PR on a real device** to measure free heap and ensure no runtime memory issues. - -*This analysis runs automatically when components change. Memory usage is measured from {config_note}.* -""" + # Render main template + template = env.get_template("ci_memory_impact_comment_template.j2") + return template.render(**context) def find_existing_comment(pr_number: str) -> str | None: @@ -605,9 +539,9 @@ def main() -> int: ) # Post or update comment - success = post_or_update_comment(args.pr_number, comment_body) + post_or_update_comment(args.pr_number, comment_body) - return 0 if success else 1 + return 0 if __name__ == "__main__": diff --git a/script/templates/ci_memory_impact_comment_template.j2 b/script/templates/ci_memory_impact_comment_template.j2 new file mode 100644 index 0000000000..4c8d7f4865 --- /dev/null +++ b/script/templates/ci_memory_impact_comment_template.j2 @@ -0,0 +1,27 @@ +{{ comment_marker }} +## Memory Impact Analysis + +**Components:** {{ components_str }} +**Platform:** `{{ platform }}` + +| Metric | Target Branch | This PR | Change | +|--------|--------------|---------|--------| +| **RAM** | {{ target_ram }} | {{ pr_ram }} | {{ ram_change }} | +| **Flash** | {{ target_flash }} | {{ pr_flash }} | {{ flash_change }} | +{% if component_breakdown %} +{{ component_breakdown }} +{%- endif %} +{%- if symbol_changes %} +{{ symbol_changes }} +{%- endif %} +{%- if target_cache_hit %} + +> ⚡ Target branch analysis was loaded from cache (build skipped for faster CI). +{%- endif %} + +--- +> **Note:** This analysis measures **static RAM and Flash usage** only (compile-time allocation). +> **Dynamic memory (heap)** cannot be measured automatically. +> **⚠️ You must test this PR on a real device** to measure free heap and ensure no runtime memory issues. + +*This analysis runs automatically when components change. Memory usage is measured from {{ config_note }}.* diff --git a/script/templates/ci_memory_impact_component_breakdown.j2 b/script/templates/ci_memory_impact_component_breakdown.j2 new file mode 100644 index 0000000000..a781e5c546 --- /dev/null +++ b/script/templates/ci_memory_impact_component_breakdown.j2 @@ -0,0 +1,15 @@ + +
+📊 Component Memory Breakdown + +| Component | Target Flash | PR Flash | Change | +|-----------|--------------|----------|--------| +{% for comp, target_flash, pr_flash, delta in changed_components[:max_rows] -%} +{% set threshold = component_change_threshold if comp.startswith("[esphome]") else none -%} +| `{{ comp }}` | {{ target_flash|format_bytes }} | {{ pr_flash|format_bytes }} | {{ format_change(target_flash, pr_flash, threshold=threshold) }} | +{% endfor -%} +{% if changed_components|length > max_rows -%} +| ... | ... | ... | *({{ changed_components|length - max_rows }} more components not shown)* | +{% endif -%} + +
diff --git a/script/templates/ci_memory_impact_macros.j2 b/script/templates/ci_memory_impact_macros.j2 new file mode 100644 index 0000000000..9fb346a7c5 --- /dev/null +++ b/script/templates/ci_memory_impact_macros.j2 @@ -0,0 +1,8 @@ +{#- Macro for formatting symbol names in tables -#} +{%- macro format_symbol(symbol, max_length, truncate_length) -%} +{%- if symbol|length <= max_length -%} +`{{ symbol }}` +{%- else -%} +
{{ symbol[:truncate_length] }}...{{ symbol }}
+{%- endif -%} +{%- endmacro -%} diff --git a/script/templates/ci_memory_impact_symbol_changes.j2 b/script/templates/ci_memory_impact_symbol_changes.j2 new file mode 100644 index 0000000000..bd540712f8 --- /dev/null +++ b/script/templates/ci_memory_impact_symbol_changes.j2 @@ -0,0 +1,51 @@ +{%- from 'ci_memory_impact_macros.j2' import format_symbol -%} + +
+🔍 Symbol-Level Changes (click to expand) + +{%- if changed_symbols %} + +### Changed Symbols + +| Symbol | Target Size | PR Size | Change | +|--------|-------------|---------|--------| +{% for symbol, target_size, pr_size, delta in changed_symbols[:max_changed_rows] -%} +| {{ format_symbol(symbol, symbol_max_length, symbol_truncate_length) }} | {{ target_size|format_bytes }} | {{ pr_size|format_bytes }} | {{ format_change(target_size, pr_size) }} | +{% endfor -%} +{% if changed_symbols|length > max_changed_rows -%} +| ... | ... | ... | *({{ changed_symbols|length - max_changed_rows }} more changed symbols not shown)* | +{% endif -%} + +{%- endif %} +{%- if new_symbols %} + +### New Symbols (top {{ max_new_rows }}) + +| Symbol | Size | +|--------|------| +{% for symbol, size in new_symbols[:max_new_rows] -%} +| {{ format_symbol(symbol, symbol_max_length, symbol_truncate_length) }} | {{ size|format_bytes }} | +{% endfor -%} +{% if new_symbols|length > max_new_rows -%} +{% set total_new_size = new_symbols|sum(attribute=1) -%} +| *{{ new_symbols|length - max_new_rows }} more new symbols...* | *Total: {{ total_new_size|format_bytes }}* | +{% endif -%} + +{%- endif %} +{%- if removed_symbols %} + +### Removed Symbols (top {{ max_removed_rows }}) + +| Symbol | Size | +|--------|------| +{% for symbol, size in removed_symbols[:max_removed_rows] -%} +| {{ format_symbol(symbol, symbol_max_length, symbol_truncate_length) }} | {{ size|format_bytes }} | +{% endfor -%} +{% if removed_symbols|length > max_removed_rows -%} +{% set total_removed_size = removed_symbols|sum(attribute=1) -%} +| *{{ removed_symbols|length - max_removed_rows }} more removed symbols...* | *Total: {{ total_removed_size|format_bytes }}* | +{% endif -%} + +{%- endif %} + +
From ba18bb6a4fedb7946c0a462957fdbfe960bb1eb3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:18:15 -1000 Subject: [PATCH 14/22] template all the things --- script/ci_memory_impact_comment.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index 961c304e40..5a399639f5 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -148,9 +148,11 @@ def prepare_symbol_changes_data( all_symbols = set(target_symbols.keys()) | set(pr_symbols.keys()) # Track changes - changed_symbols = [] - new_symbols = [] - removed_symbols = [] + changed_symbols: list[ + tuple[str, int, int, int] + ] = [] # (symbol, target_size, pr_size, delta) + new_symbols: list[tuple[str, int]] = [] # (symbol, size) + removed_symbols: list[tuple[str, int]] = [] # (symbol, size) for symbol in all_symbols: target_size = target_symbols.get(symbol, 0) @@ -201,7 +203,9 @@ def prepare_component_breakdown_data( all_components = set(target_analysis.keys()) | set(pr_analysis.keys()) # Filter to components that have changed (ignoring noise) - changed_components = [] + changed_components: list[ + tuple[str, int, int, int] + ] = [] # (comp, target_flash, pr_flash, delta) for comp in all_components: target_mem = target_analysis.get(comp, {}) pr_mem = pr_analysis.get(comp, {}) From a078486a878406a6fd85a8d995e9453bf1d52561 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:21:28 -1000 Subject: [PATCH 15/22] update test --- tests/script/test_determine_jobs.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index f8557ef6b6..24c77b6ae9 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -17,6 +17,9 @@ script_dir = os.path.abspath( ) sys.path.insert(0, script_dir) +# Import helpers module for patching +import helpers # noqa: E402 + spec = importlib.util.spec_from_file_location( "determine_jobs", os.path.join(script_dir, "determine-jobs.py") ) @@ -478,9 +481,10 @@ def test_main_filters_components_without_tests( airthings_dir = tests_dir / "airthings_ble" airthings_dir.mkdir(parents=True) - # Mock root_path to use tmp_path + # Mock root_path to use tmp_path (need to patch both determine_jobs and helpers) with ( patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), patch("sys.argv", ["determine-jobs.py"]), ): # Clear the cache since we're mocking root_path From 7e54803edea0b24f0892129e4a66d39dd44da5b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:25:41 -1000 Subject: [PATCH 16/22] update test --- esphome/analyze_memory/cli.py | 19 +++---- script/ci_memory_impact_comment.py | 82 ++++++++++++++++++------------ script/ci_memory_impact_extract.py | 30 +++++------ 3 files changed, 75 insertions(+), 56 deletions(-) diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index a2366430dd..5713eac94c 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -371,15 +371,16 @@ def main(): idedata = None for idedata_path in idedata_candidates: - if idedata_path.exists(): - try: - with open(idedata_path, encoding="utf-8") as f: - raw_data = json.load(f) - idedata = IDEData(raw_data) - print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) - break - except (json.JSONDecodeError, OSError) as e: - print(f"Warning: Failed to load idedata: {e}", file=sys.stderr) + if not idedata_path.exists(): + continue + try: + with open(idedata_path, encoding="utf-8") as f: + raw_data = json.load(f) + idedata = IDEData(raw_data) + print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) + break + except (json.JSONDecodeError, OSError) as e: + print(f"Warning: Failed to load idedata: {e}", file=sys.stderr) if not idedata: print( diff --git a/script/ci_memory_impact_comment.py b/script/ci_memory_impact_comment.py index 5a399639f5..4e3fbb9086 100755 --- a/script/ci_memory_impact_comment.py +++ b/script/ci_memory_impact_comment.py @@ -409,6 +409,54 @@ def find_existing_comment(pr_number: str) -> str | None: return None +def update_existing_comment(comment_id: str, comment_body: str) -> None: + """Update an existing comment. + + Args: + comment_id: Comment ID to update + comment_body: New comment body text + + Raises: + subprocess.CalledProcessError: If gh command fails + """ + print(f"DEBUG: Updating existing comment {comment_id}", file=sys.stderr) + result = subprocess.run( + [ + "gh", + "api", + f"/repos/{{owner}}/{{repo}}/issues/comments/{comment_id}", + "-X", + "PATCH", + "-f", + f"body={comment_body}", + ], + check=True, + capture_output=True, + text=True, + ) + print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) + + +def create_new_comment(pr_number: str, comment_body: str) -> None: + """Create a new PR comment. + + Args: + pr_number: PR number + comment_body: Comment body text + + Raises: + subprocess.CalledProcessError: If gh command fails + """ + print(f"DEBUG: Posting new comment on PR #{pr_number}", file=sys.stderr) + result = subprocess.run( + ["gh", "pr", "comment", pr_number, "--body", comment_body], + check=True, + capture_output=True, + text=True, + ) + print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) + + def post_or_update_comment(pr_number: str, comment_body: str) -> None: """Post a new comment or update existing one. @@ -423,39 +471,9 @@ def post_or_update_comment(pr_number: str, comment_body: str) -> None: existing_comment_id = find_existing_comment(pr_number) if existing_comment_id and existing_comment_id != "None": - # Update existing comment - print( - f"DEBUG: Updating existing comment {existing_comment_id}", - file=sys.stderr, - ) - result = subprocess.run( - [ - "gh", - "api", - f"/repos/{{owner}}/{{repo}}/issues/comments/{existing_comment_id}", - "-X", - "PATCH", - "-f", - f"body={comment_body}", - ], - check=True, - capture_output=True, - text=True, - ) - print(f"DEBUG: Update response: {result.stdout}", file=sys.stderr) + update_existing_comment(existing_comment_id, comment_body) else: - # Post new comment - print( - f"DEBUG: Posting new comment (existing_comment_id={existing_comment_id})", - file=sys.stderr, - ) - result = subprocess.run( - ["gh", "pr", "comment", pr_number, "--body", comment_body], - check=True, - capture_output=True, - text=True, - ) - print(f"DEBUG: Post response: {result.stdout}", file=sys.stderr) + create_new_comment(pr_number, comment_body) print("Comment posted/updated successfully", file=sys.stderr) diff --git a/script/ci_memory_impact_extract.py b/script/ci_memory_impact_extract.py index 17ac788ae3..77d59417e3 100755 --- a/script/ci_memory_impact_extract.py +++ b/script/ci_memory_impact_extract.py @@ -25,6 +25,8 @@ import sys sys.path.insert(0, str(Path(__file__).parent.parent)) # pylint: disable=wrong-import-position +from esphome.analyze_memory import MemoryAnalyzer +from esphome.platformio_api import IDEData from script.ci_helpers import write_github_output # Regex patterns for extracting memory usage from PlatformIO output @@ -85,9 +87,6 @@ def run_detailed_analysis(build_dir: str) -> dict | None: Returns: Dictionary with analysis results or None if analysis fails """ - from esphome.analyze_memory import MemoryAnalyzer - from esphome.platformio_api import IDEData - build_path = Path(build_dir) if not build_path.exists(): print(f"Build directory not found: {build_dir}", file=sys.stderr) @@ -120,18 +119,19 @@ def run_detailed_analysis(build_dir: str) -> dict | None: idedata = None for idedata_path in idedata_candidates: - if idedata_path.exists(): - try: - with open(idedata_path, encoding="utf-8") as f: - raw_data = json.load(f) - idedata = IDEData(raw_data) - print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) - break - except (json.JSONDecodeError, OSError) as e: - print( - f"Warning: Failed to load idedata from {idedata_path}: {e}", - file=sys.stderr, - ) + if not idedata_path.exists(): + continue + try: + with open(idedata_path, encoding="utf-8") as f: + raw_data = json.load(f) + idedata = IDEData(raw_data) + print(f"Loaded idedata from: {idedata_path}", file=sys.stderr) + break + except (json.JSONDecodeError, OSError) as e: + print( + f"Warning: Failed to load idedata from {idedata_path}: {e}", + file=sys.stderr, + ) analyzer = MemoryAnalyzer(elf_path, idedata=idedata) components = analyzer.analyze() From 85e0a4fbf9e966a096d61cfcf086640ee88c6be3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:29:36 -1000 Subject: [PATCH 17/22] update test --- esphome/platformio_api.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index cc48562b4c..c50bb2acff 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -378,19 +378,17 @@ class IDEData: @property def objdump_path(self) -> str: # replace gcc at end with objdump - - # Windows - if self.cc_path.endswith(".exe"): - return f"{self.cc_path[:-7]}objdump.exe" - - return f"{self.cc_path[:-3]}objdump" + return ( + f"{self.cc_path[:-7]}objdump.exe" + if self.cc_path.endswith(".exe") + else f"{self.cc_path[:-3]}objdump" + ) @property def readelf_path(self) -> str: # replace gcc at end with readelf - - # Windows - if self.cc_path.endswith(".exe"): - return f"{self.cc_path[:-7]}readelf.exe" - - return f"{self.cc_path[:-3]}readelf" + return ( + f"{self.cc_path[:-7]}readelf.exe" + if self.cc_path.endswith(".exe") + else f"{self.cc_path[:-3]}readelf" + ) From 541fb8b27c3cc302923fd41ad6c3f6bdb9b06ea9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:32:22 -1000 Subject: [PATCH 18/22] update test --- esphome/analyze_memory/__init__.py | 40 +++++++++++++++++++----------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 942caabe70..db16051b8a 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -33,6 +33,21 @@ _GCC_PREFIX_ANNOTATIONS = { "_GLOBAL__sub_D_": "global destructor for", } +# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2) +_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)") + +# C++ runtime patterns for categorization +_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) + +# libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.) +_LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"]) + +# Regex pattern for parsing readelf section headers +# Format: [ #] name type addr off size +_READELF_SECTION_PATTERN = re.compile( + r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)" +) + @dataclass class MemorySection: @@ -133,12 +148,7 @@ class MemoryAnalyzer: # Parse section headers for line in result.stdout.splitlines(): # Look for section entries - if not ( - match := re.match( - r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)", - line, - ) - ): + if not (match := _READELF_SECTION_PATTERN.match(line)): continue section_name = match.group(1) @@ -273,14 +283,14 @@ class MemoryAnalyzer: # Check if spi_flash vs spi_driver if "spi_" in symbol_name or "SPI" in symbol_name: - if "spi_flash" in symbol_name: - return "spi_flash" - return "spi_driver" + return "spi_flash" if "spi_flash" in symbol_name else "spi_driver" # libc special printf variants - if symbol_name.startswith("_") and symbol_name[1:].replace("_r", "").replace( - "v", "" - ).replace("s", "") in ["printf", "fprintf", "sprintf", "scanf"]: + if ( + symbol_name.startswith("_") + and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "") + in _LIBC_PRINTF_SCANF_FAMILY + ): return "libc" # Track uncategorized symbols for analysis @@ -320,7 +330,7 @@ class MemoryAnalyzer: symbols_prefixes: list[str] = [] # Track removed prefixes for symbol in symbols: # Remove GCC optimization markers - stripped = re.sub(r"\$(?:isra|part|constprop)\$\d+", "", symbol) + stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol) # Handle GCC global constructor/initializer prefixes # _GLOBAL__sub_I_ -> extract for demangling @@ -450,7 +460,7 @@ class MemoryAnalyzer: Returns: Demangled name with suffix annotation """ - suffix_match = re.search(r"(\$(?:isra|part|constprop)\$\d+)", original) + suffix_match = _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original) if suffix_match: return f"{demangled} [{suffix_match.group(1)}]" return demangled @@ -462,7 +472,7 @@ class MemoryAnalyzer: def _categorize_esphome_core_symbol(self, demangled: str) -> str: """Categorize ESPHome core symbols into subcategories.""" # Special patterns that need to be checked separately - if any(pattern in demangled for pattern in ["vtable", "typeinfo", "thunk"]): + if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS): return "C++ Runtime (vtables/RTTI)" if demangled.startswith("std::"): From f9807db08ab7218f5f14570814378fc95aba3ff1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:37:24 -1000 Subject: [PATCH 19/22] preen --- esphome/analyze_memory/__init__.py | 21 +++++++++++++-------- esphome/analyze_memory/cli.py | 22 ++++++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index db16051b8a..15cadaf859 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -48,6 +48,12 @@ _READELF_SECTION_PATTERN = re.compile( r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)" ) +# Component category prefixes +_COMPONENT_PREFIX_ESPHOME = "[esphome]" +_COMPONENT_PREFIX_EXTERNAL = "[external]" +_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" +_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" + @dataclass class MemorySection: @@ -222,7 +228,7 @@ class MemoryAnalyzer: self._uncategorized_symbols.append((symbol_name, demangled, size)) # Track ESPHome core symbols for detailed analysis - if component == "[esphome]core" and size > 0: + if component == _COMPONENT_CORE and size > 0: demangled = self._demangle_symbol(symbol_name) self._esphome_core_symbols.append((symbol_name, demangled, size)) @@ -246,7 +252,7 @@ class MemoryAnalyzer: for component_name in get_esphome_components(): patterns = get_component_class_patterns(component_name) if any(pattern in demangled for pattern in patterns): - return f"[esphome]{component_name}" + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" # Check for ESPHome component namespaces match = ESPHOME_COMPONENT_PATTERN.search(demangled) @@ -257,17 +263,17 @@ class MemoryAnalyzer: # Check if this is an actual component in the components directory if component_name in get_esphome_components(): - return f"[esphome]{component_name}" + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" # Check if this is a known external component from the config if component_name in self.external_components: - return f"[external]{component_name}" + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" # Everything else in esphome:: namespace is core - return "[esphome]core" + return _COMPONENT_CORE # Check for esphome core namespace (no component namespace) if "esphome::" in demangled: # If no component match found, it's core - return "[esphome]core" + return _COMPONENT_CORE # Check against symbol patterns for component, patterns in SYMBOL_PATTERNS.items(): @@ -460,8 +466,7 @@ class MemoryAnalyzer: Returns: Demangled name with suffix annotation """ - suffix_match = _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original) - if suffix_match: + if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original): return f"{demangled} [{suffix_match.group(1)}]" return demangled diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index 5713eac94c..1695a00c19 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -3,7 +3,13 @@ from collections import defaultdict import sys -from . import MemoryAnalyzer +from . import ( + _COMPONENT_API, + _COMPONENT_CORE, + _COMPONENT_PREFIX_ESPHOME, + _COMPONENT_PREFIX_EXTERNAL, + MemoryAnalyzer, +) class MemoryAnalyzerCLI(MemoryAnalyzer): @@ -144,7 +150,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): if self._esphome_core_symbols: lines.append("") lines.append("=" * self.TABLE_WIDTH) - lines.append("[esphome]core Detailed Analysis".center(self.TABLE_WIDTH)) + lines.append( + f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH) + ) lines.append("=" * self.TABLE_WIDTH) lines.append("") @@ -185,7 +193,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Top 15 largest core symbols lines.append("") - lines.append("Top 15 Largest [esphome]core Symbols:") + lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:") sorted_core_symbols = sorted( self._esphome_core_symbols, key=lambda x: x[2], reverse=True ) @@ -199,10 +207,12 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): esphome_components = [ (name, mem) for name, mem in components - if name.startswith("[esphome]") and name != "[esphome]core" + if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE ] external_components = [ - (name, mem) for name, mem in components if name.startswith("[external]") + (name, mem) + for name, mem in components + if name.startswith(_COMPONENT_PREFIX_EXTERNAL) ] top_esphome_components = sorted( @@ -217,7 +227,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): # Check if API component exists and ensure it's included api_component = None for name, mem in components: - if name == "[esphome]api": + if name == _COMPONENT_API: api_component = (name, mem) break From 4f4da1de22acb050c0641a98828f0f6e231c2487 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:41:12 -1000 Subject: [PATCH 20/22] preen --- esphome/analyze_memory/__init__.py | 17 +++++++++++------ esphome/analyze_memory/helpers.py | 13 +++++++++---- esphome/platformio_api.py | 14 ++++++++------ 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 15cadaf859..71e86e3788 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -54,15 +54,20 @@ _COMPONENT_PREFIX_EXTERNAL = "[external]" _COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core" _COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api" +# C++ namespace prefixes +_NAMESPACE_ESPHOME = "esphome::" +_NAMESPACE_STD = "std::" + +# Type alias for symbol information: (symbol_name, size, component) +SymbolInfoType = tuple[str, int, str] + @dataclass class MemorySection: """Represents a memory section with its symbols.""" name: str - symbols: list[tuple[str, int, str]] = field( - default_factory=list - ) # (symbol_name, size, component) + symbols: list[SymbolInfoType] = field(default_factory=list) total_size: int = 0 @@ -246,7 +251,7 @@ class MemoryAnalyzer: # Check for special component classes first (before namespace pattern) # This handles cases like esphome::ESPHomeOTAComponent which should map to ota - if "esphome::" in demangled: + if _NAMESPACE_ESPHOME in demangled: # Check for special component classes that include component name in the class # For example: esphome::ESPHomeOTAComponent -> ota component for component_name in get_esphome_components(): @@ -271,7 +276,7 @@ class MemoryAnalyzer: return _COMPONENT_CORE # Check for esphome core namespace (no component namespace) - if "esphome::" in demangled: + if _NAMESPACE_ESPHOME in demangled: # If no component match found, it's core return _COMPONENT_CORE @@ -480,7 +485,7 @@ class MemoryAnalyzer: if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS): return "C++ Runtime (vtables/RTTI)" - if demangled.startswith("std::"): + if demangled.startswith(_NAMESPACE_STD): return "C++ STL" # Check against patterns from const.py diff --git a/esphome/analyze_memory/helpers.py b/esphome/analyze_memory/helpers.py index 1b5a1c67c2..cb503b37c5 100644 --- a/esphome/analyze_memory/helpers.py +++ b/esphome/analyze_memory/helpers.py @@ -5,6 +5,11 @@ from pathlib import Path from .const import SECTION_MAPPING +# Import namespace constant from parent module +# Note: This would create a circular import if done at module level, +# so we'll define it locally here as well +_NAMESPACE_ESPHOME = "esphome::" + # Get the list of actual ESPHome components by scanning the components directory @cache @@ -40,10 +45,10 @@ def get_component_class_patterns(component_name: str) -> list[str]: component_upper = component_name.upper() component_camel = component_name.replace("_", "").title() return [ - f"esphome::{component_upper}Component", # e.g., esphome::OTAComponent - f"esphome::ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent - f"esphome::{component_camel}Component", # e.g., esphome::OtaComponent - f"esphome::ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent + f"{_NAMESPACE_ESPHOME}{component_upper}Component", # e.g., esphome::OTAComponent + f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component", # e.g., esphome::ESPHomeOTAComponent + f"{_NAMESPACE_ESPHOME}{component_camel}Component", # e.g., esphome::OtaComponent + f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component", # e.g., esphome::ESPHomeOtaComponent ] diff --git a/esphome/platformio_api.py b/esphome/platformio_api.py index c50bb2acff..d59523a74a 100644 --- a/esphome/platformio_api.py +++ b/esphome/platformio_api.py @@ -378,17 +378,19 @@ class IDEData: @property def objdump_path(self) -> str: # replace gcc at end with objdump + path = self.cc_path return ( - f"{self.cc_path[:-7]}objdump.exe" - if self.cc_path.endswith(".exe") - else f"{self.cc_path[:-3]}objdump" + f"{path[:-7]}objdump.exe" + if path.endswith(".exe") + else f"{path[:-3]}objdump" ) @property def readelf_path(self) -> str: # replace gcc at end with readelf + path = self.cc_path return ( - f"{self.cc_path[:-7]}readelf.exe" - if self.cc_path.endswith(".exe") - else f"{self.cc_path[:-3]}readelf" + f"{path[:-7]}readelf.exe" + if path.endswith(".exe") + else f"{path[:-3]}readelf" ) From 7f2d8a2c118da393b7758dee2ae215ddd50985fc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:46:41 -1000 Subject: [PATCH 21/22] whitespace --- .../ci_memory_impact_comment_template.j2 | 6 +- .../ci_memory_impact_symbol_changes.j2 | 12 +- tests/script/test_determine_jobs.py | 182 ++++++++++++++++++ 3 files changed, 191 insertions(+), 9 deletions(-) diff --git a/script/templates/ci_memory_impact_comment_template.j2 b/script/templates/ci_memory_impact_comment_template.j2 index 4c8d7f4865..9fbf78e99f 100644 --- a/script/templates/ci_memory_impact_comment_template.j2 +++ b/script/templates/ci_memory_impact_comment_template.j2 @@ -10,10 +10,10 @@ | **Flash** | {{ target_flash }} | {{ pr_flash }} | {{ flash_change }} | {% if component_breakdown %} {{ component_breakdown }} -{%- endif %} -{%- if symbol_changes %} +{% endif %} +{% if symbol_changes %} {{ symbol_changes }} -{%- endif %} +{% endif %} {%- if target_cache_hit %} > ⚡ Target branch analysis was loaded from cache (build skipped for faster CI). diff --git a/script/templates/ci_memory_impact_symbol_changes.j2 b/script/templates/ci_memory_impact_symbol_changes.j2 index bd540712f8..60f2f50e48 100644 --- a/script/templates/ci_memory_impact_symbol_changes.j2 +++ b/script/templates/ci_memory_impact_symbol_changes.j2 @@ -3,7 +3,7 @@
🔍 Symbol-Level Changes (click to expand) -{%- if changed_symbols %} +{% if changed_symbols %} ### Changed Symbols @@ -16,8 +16,8 @@ | ... | ... | ... | *({{ changed_symbols|length - max_changed_rows }} more changed symbols not shown)* | {% endif -%} -{%- endif %} -{%- if new_symbols %} +{% endif %} +{% if new_symbols %} ### New Symbols (top {{ max_new_rows }}) @@ -31,8 +31,8 @@ | *{{ new_symbols|length - max_new_rows }} more new symbols...* | *Total: {{ total_new_size|format_bytes }}* | {% endif -%} -{%- endif %} -{%- if removed_symbols %} +{% endif %} +{% if removed_symbols %} ### Removed Symbols (top {{ max_removed_rows }}) @@ -46,6 +46,6 @@ | *{{ removed_symbols|length - max_removed_rows }} more removed symbols...* | *Total: {{ total_removed_size|format_bytes }}* | {% endif -%} -{%- endif %} +{% endif %}
diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index 24c77b6ae9..b479fc03c5 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -504,3 +504,185 @@ def test_main_filters_components_without_tests( # memory_impact should be present assert "memory_impact" in output assert output["memory_impact"]["should_run"] == "false" + + +# Tests for detect_memory_impact_config function + + +def test_detect_memory_impact_config_with_common_platform(tmp_path: Path) -> None: + """Test memory impact detection when components share a common platform.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # wifi component with esp32-idf test + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + # api component with esp32-idf test + api_dir = tests_dir / "api" + api_dir.mkdir(parents=True) + (api_dir / "test.esp32-idf.yaml").write_text("test: api") + + # Mock changed_files to return wifi and api component changes + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/api/api.cpp", + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "true" + assert set(result["components"]) == {"wifi", "api"} + assert result["platform"] == "esp32-idf" # Common platform + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_core_only_changes(tmp_path: Path) -> None: + """Test memory impact detection with core-only changes (no component changes).""" + # Create test directory structure with fallback component + tests_dir = tmp_path / "tests" / "components" + + # api component (fallback component) with esp32-idf test + api_dir = tests_dir / "api" + api_dir.mkdir(parents=True) + (api_dir / "test.esp32-idf.yaml").write_text("test: api") + + # Mock changed_files to return only core files (no component files) + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/core/application.cpp", + "esphome/core/component.h", + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "true" + assert result["components"] == ["api"] # Fallback component + assert result["platform"] == "esp32-idf" # Fallback platform + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_no_common_platform(tmp_path: Path) -> None: + """Test memory impact detection when components have no common platform.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # wifi component only has esp32-idf test + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + # logger component only has esp8266-ard test + logger_dir = tests_dir / "logger" + logger_dir.mkdir(parents=True) + (logger_dir / "test.esp8266-ard.yaml").write_text("test: logger") + + # Mock changed_files to return both components + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/wifi/wifi.cpp", + "esphome/components/logger/logger.cpp", + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Should pick the most frequently supported platform + assert result["should_run"] == "true" + assert set(result["components"]) == {"wifi", "logger"} + # When no common platform, picks most commonly supported + # esp8266-ard is preferred over esp32-idf in the preference list + assert result["platform"] in ["esp32-idf", "esp8266-ard"] + assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_no_changes(tmp_path: Path) -> None: + """Test memory impact detection when no files changed.""" + # Mock changed_files to return empty list + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_no_components_with_tests(tmp_path: Path) -> None: + """Test memory impact detection when changed components have no tests.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # Create component directory but no test files + custom_component_dir = tests_dir / "my_custom_component" + custom_component_dir.mkdir(parents=True) + + # Mock changed_files to return component without tests + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/my_custom_component/component.cpp", + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_skips_base_bus_components(tmp_path: Path) -> None: + """Test that base bus components (i2c, spi, uart) are skipped.""" + # Create test directory structure + tests_dir = tmp_path / "tests" / "components" + + # i2c component (should be skipped as it's a base bus component) + i2c_dir = tests_dir / "i2c" + i2c_dir.mkdir(parents=True) + (i2c_dir / "test.esp32-idf.yaml").write_text("test: i2c") + + # wifi component (should not be skipped) + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + # Mock changed_files to return both i2c and wifi + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + ): + mock_changed_files.return_value = [ + "esphome/components/i2c/i2c.cpp", + "esphome/components/wifi/wifi.cpp", + ] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Should only include wifi, not i2c + assert result["should_run"] == "true" + assert result["components"] == ["wifi"] + assert "i2c" not in result["components"] From e70cb098ae25af4ea59a5b2a8d792d20212c9d50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 17 Oct 2025 18:50:07 -1000 Subject: [PATCH 22/22] whitespace --- tests/unit_tests/test_platformio_api.py | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/unit_tests/test_platformio_api.py b/tests/unit_tests/test_platformio_api.py index 07948cc6ad..13ef3516e4 100644 --- a/tests/unit_tests/test_platformio_api.py +++ b/tests/unit_tests/test_platformio_api.py @@ -387,6 +387,42 @@ def test_idedata_addr2line_path_unix(setup_core: Path) -> None: assert result == "/usr/bin/addr2line" +def test_idedata_objdump_path_windows(setup_core: Path) -> None: + """Test IDEData.objdump_path on Windows.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.objdump_path + assert result == "C:\\tools\\objdump.exe" + + +def test_idedata_objdump_path_unix(setup_core: Path) -> None: + """Test IDEData.objdump_path on Unix.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.objdump_path + assert result == "/usr/bin/objdump" + + +def test_idedata_readelf_path_windows(setup_core: Path) -> None: + """Test IDEData.readelf_path on Windows.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "C:\\tools\\gcc.exe"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.readelf_path + assert result == "C:\\tools\\readelf.exe" + + +def test_idedata_readelf_path_unix(setup_core: Path) -> None: + """Test IDEData.readelf_path on Unix.""" + raw_data = {"prog_path": "/path/to/firmware.elf", "cc_path": "/usr/bin/gcc"} + idedata = platformio_api.IDEData(raw_data) + + result = idedata.readelf_path + assert result == "/usr/bin/readelf" + + def test_patch_structhash(setup_core: Path) -> None: """Test patch_structhash monkey patches platformio functions.""" # Create simple namespace objects to act as modules