mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 07:31:51 +00:00
Merge branch 'ci_impact_analysis_sensor_base' into ci_impact_analysis_sensor_full
This commit is contained in:
@@ -33,15 +33,41 @@ _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]+)"
|
||||
)
|
||||
|
||||
# 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"
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -77,7 +103,7 @@ class MemoryAnalyzer:
|
||||
readelf_path: str | None = None,
|
||||
external_components: set[str] | None = None,
|
||||
idedata: "IDEData | None" = None,
|
||||
):
|
||||
) -> None:
|
||||
"""Initialize memory analyzer.
|
||||
|
||||
Args:
|
||||
@@ -133,12 +159,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)
|
||||
@@ -212,7 +233,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))
|
||||
|
||||
@@ -230,13 +251,13 @@ 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():
|
||||
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)
|
||||
@@ -247,17 +268,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 _NAMESPACE_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():
|
||||
@@ -273,14 +294,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
|
||||
@@ -294,45 +315,42 @@ class MemoryAnalyzer:
|
||||
# Try to find the appropriate c++filt for the platform
|
||||
cppfilt_cmd = "c++filt"
|
||||
|
||||
_LOGGER.warning("Demangling %d symbols", len(symbols))
|
||||
_LOGGER.warning("objdump_path = %s", self.objdump_path)
|
||||
_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(
|
||||
"✗ 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)
|
||||
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
|
||||
|
||||
# Handle GCC global constructor/initializer prefixes
|
||||
# _GLOBAL__sub_I_<mangled> -> extract <mangled> 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 +423,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:
|
||||
@@ -452,8 +471,7 @@ class MemoryAnalyzer:
|
||||
Returns:
|
||||
Demangled name with suffix annotation
|
||||
"""
|
||||
suffix_match = re.search(r"(\$(?:isra|part|constprop)\$\d+)", original)
|
||||
if suffix_match:
|
||||
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
|
||||
return f"{demangled} [{suffix_match.group(1)}]"
|
||||
return demangled
|
||||
|
||||
@@ -464,10 +482,10 @@ 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::"):
|
||||
if demangled.startswith(_NAMESPACE_STD):
|
||||
return "C++ STL"
|
||||
|
||||
# Check against patterns from const.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):
|
||||
@@ -83,7 +89,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))
|
||||
@@ -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("")
|
||||
|
||||
@@ -183,9 +191,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(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
|
||||
|
||||
@@ -371,15 +381,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(
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
120
esphome/components/remote_base/symphony_protocol.cpp
Normal file
120
esphome/components/remote_base/symphony_protocol.cpp
Normal file
@@ -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<SymphonyData> 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
|
||||
44
esphome/components/remote_base/symphony_protocol.h
Normal file
44
esphome/components/remote_base/symphony_protocol.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "remote_base.h"
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
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<SymphonyData> {
|
||||
public:
|
||||
void encode(RemoteTransmitData *dst, const SymphonyData &data) override;
|
||||
optional<SymphonyData> decode(RemoteReceiveData src) override;
|
||||
void dump(const SymphonyData &data) override;
|
||||
};
|
||||
|
||||
DECLARE_REMOTE_PROTOCOL(Symphony)
|
||||
|
||||
template<typename... Ts> class SymphonyAction : public RemoteTransmitterActionBase<Ts...> {
|
||||
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
|
||||
@@ -378,19 +378,19 @@ 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"
|
||||
path = self.cc_path
|
||||
return (
|
||||
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
|
||||
|
||||
# Windows
|
||||
if self.cc_path.endswith(".exe"):
|
||||
return f"{self.cc_path[:-7]}readelf.exe"
|
||||
|
||||
return f"{self.cc_path[:-3]}readelf"
|
||||
path = self.cc_path
|
||||
return (
|
||||
f"{path[:-7]}readelf.exe"
|
||||
if path.endswith(".exe")
|
||||
else f"{path[:-3]}readelf"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +52,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
|
||||
@@ -60,16 +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",
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
@@ -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 = "<!-- esphome-memory-impact-analysis -->"
|
||||
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 <details> 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,43 +129,30 @@ 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"<details><summary><code>{symbol[:97]}...</code></summary><code>{symbol}</code></details>"
|
||||
|
||||
|
||||
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())
|
||||
|
||||
# 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)
|
||||
@@ -165,114 +170,42 @@ 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 = [
|
||||
"",
|
||||
"<details>",
|
||||
"<summary>🔍 Symbol-Level Changes (click to expand)</summary>",
|
||||
"",
|
||||
]
|
||||
# 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(["</details>", ""])
|
||||
|
||||
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)
|
||||
changed_components = []
|
||||
# Filter to components that have changed (ignoring noise)
|
||||
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, {})
|
||||
@@ -280,43 +213,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 = [
|
||||
"",
|
||||
"<details open>",
|
||||
"<summary>📊 Component Memory Breakdown</summary>",
|
||||
"",
|
||||
"| 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)
|
||||
change_str = format_change(
|
||||
target_flash, pr_flash, threshold=COMPONENT_CHANGE_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(["", "</details>", ""])
|
||||
|
||||
return "\n".join(lines)
|
||||
return changed_components
|
||||
|
||||
|
||||
def create_comment_body(
|
||||
@@ -332,7 +240,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 +258,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:
|
||||
@@ -411,137 +349,133 @@ 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 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.
|
||||
|
||||
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(existing_comment_id, comment_body)
|
||||
else:
|
||||
create_new_comment(pr_number, comment_body)
|
||||
|
||||
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:
|
||||
@@ -627,9 +561,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__":
|
||||
|
||||
@@ -25,8 +25,15 @@ 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
|
||||
_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 +49,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 +58,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
|
||||
@@ -69,7 +72,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
|
||||
@@ -84,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)
|
||||
@@ -119,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()
|
||||
@@ -209,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(
|
||||
|
||||
@@ -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 whenever there are changed components (merged config), and also for core-only changes
|
||||
|
||||
Usage:
|
||||
python script/determine-jobs.py [-b BRANCH]
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
27
script/templates/ci_memory_impact_comment_template.j2
Normal file
27
script/templates/ci_memory_impact_comment_template.j2
Normal file
@@ -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 }}.*
|
||||
15
script/templates/ci_memory_impact_component_breakdown.j2
Normal file
15
script/templates/ci_memory_impact_component_breakdown.j2
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
<details open>
|
||||
<summary>📊 Component Memory Breakdown</summary>
|
||||
|
||||
| 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 -%}
|
||||
|
||||
</details>
|
||||
8
script/templates/ci_memory_impact_macros.j2
Normal file
8
script/templates/ci_memory_impact_macros.j2
Normal file
@@ -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 -%}
|
||||
<details><summary><code>{{ symbol[:truncate_length] }}...</code></summary><code>{{ symbol }}</code></details>
|
||||
{%- endif -%}
|
||||
{%- endmacro -%}
|
||||
51
script/templates/ci_memory_impact_symbol_changes.j2
Normal file
51
script/templates/ci_memory_impact_symbol_changes.j2
Normal file
@@ -0,0 +1,51 @@
|
||||
{%- from 'ci_memory_impact_macros.j2' import format_symbol -%}
|
||||
|
||||
<details>
|
||||
<summary>🔍 Symbol-Level Changes (click to expand)</summary>
|
||||
|
||||
{% 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 %}
|
||||
|
||||
</details>
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: coolix
|
||||
name: Coolix Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: daikin_brc
|
||||
name: Daikin_brc Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: delonghi
|
||||
name: Delonghi Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: fujitsu_general
|
||||
name: Fujitsu General Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: gree
|
||||
name: GREE
|
||||
model: generic
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO6
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: hitachi_ac344
|
||||
name: Hitachi Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO6
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml
|
||||
|
||||
<<: !include common.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: hitachi_ac424
|
||||
name: Hitachi Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO6
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml
|
||||
|
||||
<<: !include common.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: 4
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: mitsubishi
|
||||
name: Mitsubishi
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: toshiba
|
||||
name: Toshiba Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: whirlpool
|
||||
name: Whirlpool Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-c3-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO2
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
substitutions:
|
||||
pin: GPIO5
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
remote_transmitter:
|
||||
pin: ${pin}
|
||||
carrier_duty_percent: 50%
|
||||
|
||||
climate:
|
||||
- platform: whynter
|
||||
name: Whynter Climate
|
||||
transmitter_id: xmitr
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user