mirror of
https://github.com/esphome/esphome.git
synced 2025-10-26 03:58:40 +00:00
Compare commits
2 Commits
select_fix
...
fix_clang_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
326405ae8f | ||
|
|
c9e166905f |
@@ -231,22 +231,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
api_component = (name, mem)
|
||||
break
|
||||
|
||||
# Also include wifi_stack and other important system components if they exist
|
||||
system_components_to_include = [
|
||||
# Empty list - we've finished debugging symbol categorization
|
||||
# Add component names here if you need to debug their symbols
|
||||
]
|
||||
system_components = [
|
||||
(name, mem)
|
||||
for name, mem in components
|
||||
if name in system_components_to_include
|
||||
]
|
||||
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
||||
components_to_analyze = (
|
||||
list(top_esphome_components)
|
||||
+ list(top_external_components)
|
||||
+ system_components
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included
|
||||
components_to_analyze = list(top_esphome_components) + list(
|
||||
top_external_components
|
||||
)
|
||||
if api_component and api_component not in components_to_analyze:
|
||||
components_to_analyze.append(api_component)
|
||||
|
||||
@@ -127,39 +127,40 @@ SYMBOL_PATTERNS = {
|
||||
"tryget_socket_unconn",
|
||||
"cs_create_ctrl_sock",
|
||||
"netbuf_alloc",
|
||||
"tcp_", # TCP protocol functions
|
||||
"udp_", # UDP protocol functions
|
||||
"lwip_", # LwIP stack functions
|
||||
"eagle_lwip", # ESP-specific LwIP functions
|
||||
"new_linkoutput", # Link output function
|
||||
"acd_", # Address Conflict Detection (ACD)
|
||||
"eth_", # Ethernet functions
|
||||
"mac_enable_bb", # MAC baseband enable
|
||||
"reassemble_and_dispatch", # Packet reassembly
|
||||
],
|
||||
# dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
|
||||
"dhcp": ["dhcp", "handle_dhcp"],
|
||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||
# Order matters! More specific categories must come before general ones.
|
||||
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||
"mdns_lib": ["mdns"],
|
||||
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
"memory_",
|
||||
"tlsf_",
|
||||
"memp_",
|
||||
"pbuf_",
|
||||
"pbuf_alloc",
|
||||
"pbuf_copy_partial_pbuf",
|
||||
"esp_mmu_map",
|
||||
"mmu_hal_",
|
||||
"s_do_mapping", # Memory mapping function, not WiFi
|
||||
"hash_map_", # Hash map data structure
|
||||
"umm_assimilate", # UMM malloc assimilation
|
||||
"wifi_stack": [
|
||||
"ieee80211",
|
||||
"hostap",
|
||||
"sta_",
|
||||
"ap_",
|
||||
"scan_",
|
||||
"wifi_",
|
||||
"wpa_",
|
||||
"wps_",
|
||||
"esp_wifi",
|
||||
"cnx_",
|
||||
"wpa3_",
|
||||
"sae_",
|
||||
"wDev_",
|
||||
"ic_",
|
||||
"mac_",
|
||||
"esf_buf",
|
||||
"gWpaSm",
|
||||
"sm_WPA",
|
||||
"eapol_",
|
||||
"owe_",
|
||||
"wifiLowLevelInit",
|
||||
"s_do_mapping",
|
||||
"gScanStruct",
|
||||
"ppSearchTxframe",
|
||||
"ppMapWaitTxq",
|
||||
"ppFillAMPDUBar",
|
||||
"ppCheckTxConnTrafficIdle",
|
||||
"ppCalTkipMic",
|
||||
],
|
||||
# Bluetooth categories must come BEFORE wifi_stack to avoid misclassification
|
||||
# Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns
|
||||
"bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
|
||||
"wifi_bt_coex": ["coex"],
|
||||
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
|
||||
"bluedroid_bt": [
|
||||
"bluedroid",
|
||||
@@ -206,61 +207,6 @@ SYMBOL_PATTERNS = {
|
||||
"copy_extra_byte_in_db",
|
||||
"parse_read_local_supported_commands_response",
|
||||
],
|
||||
"bluetooth": [
|
||||
"bt_",
|
||||
"_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
|
||||
"l2c_",
|
||||
"l2ble_", # L2CAP for BLE
|
||||
"gatt_",
|
||||
"gap_",
|
||||
"hci_",
|
||||
"btsnd_hcic_", # Bluetooth HCI command send functions
|
||||
"BT_init",
|
||||
"BT_tx_", # Bluetooth transmit functions
|
||||
"esp_ble_", # Catch esp_ble_* functions
|
||||
],
|
||||
"bluetooth_ll": [
|
||||
"llm_", # Link layer manager
|
||||
"llc_", # Link layer control
|
||||
"lld_", # Link layer driver
|
||||
"ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented)
|
||||
"llcp_", # Link layer control protocol
|
||||
"lmp_", # Link manager protocol
|
||||
],
|
||||
"wifi_bt_coex": ["coex"],
|
||||
"wifi_stack": [
|
||||
"ieee80211",
|
||||
"hostap",
|
||||
"sta_",
|
||||
"wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_"
|
||||
"wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts
|
||||
"wifi_",
|
||||
"wpa_",
|
||||
"wps_",
|
||||
"esp_wifi",
|
||||
"cnx_",
|
||||
"wpa3_",
|
||||
"sae_",
|
||||
"wDev_",
|
||||
"ic_mac_", # More specific than "mac_" to avoid matching emac_
|
||||
"esf_buf",
|
||||
"gWpaSm",
|
||||
"sm_WPA",
|
||||
"eapol_",
|
||||
"owe_",
|
||||
"wifiLowLevelInit",
|
||||
# Removed "s_do_mapping" - this is memory management, not WiFi
|
||||
"gScanStruct",
|
||||
"ppSearchTxframe",
|
||||
"ppMapWaitTxq",
|
||||
"ppFillAMPDUBar",
|
||||
"ppCheckTxConnTrafficIdle",
|
||||
"ppCalTkipMic",
|
||||
"phy_force_wifi",
|
||||
"phy_unforce_wifi",
|
||||
"write_wifi_chan",
|
||||
"wifi_track_pll",
|
||||
],
|
||||
"crypto_math": [
|
||||
"ecp_",
|
||||
"bignum_",
|
||||
@@ -285,36 +231,13 @@ SYMBOL_PATTERNS = {
|
||||
"p_256_init_curve",
|
||||
"shift_sub_rows",
|
||||
"rshift",
|
||||
"rijndaelEncrypt", # AES Rijndael encryption
|
||||
],
|
||||
# System and Arduino core functions must come before libc
|
||||
"esp_system": [
|
||||
"system_", # ESP system functions
|
||||
"postmortem_", # Postmortem reporting
|
||||
],
|
||||
"arduino_core": [
|
||||
"pinMode",
|
||||
"resetPins",
|
||||
"millis",
|
||||
"micros",
|
||||
"delay(", # More specific - Arduino delay function with parenthesis
|
||||
"delayMicroseconds",
|
||||
"digitalWrite",
|
||||
"digitalRead",
|
||||
],
|
||||
"sntp": ["sntp_", "sntp_recv"],
|
||||
"scheduler": [
|
||||
"run_scheduled_",
|
||||
"compute_scheduled_",
|
||||
"event_TaskQueue",
|
||||
],
|
||||
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
|
||||
"libc": [
|
||||
"printf",
|
||||
"scanf",
|
||||
"malloc",
|
||||
"_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring
|
||||
"umm_free", # UMM malloc free function
|
||||
"free",
|
||||
"memcpy",
|
||||
"memset",
|
||||
"strcpy",
|
||||
@@ -336,7 +259,7 @@ SYMBOL_PATTERNS = {
|
||||
"_setenv_r",
|
||||
"_tzset_unlocked_r",
|
||||
"__tzcalc_limits",
|
||||
"_select", # More specific than "select" to avoid matching "dhcp_select", etc.
|
||||
"select",
|
||||
"scalbnf",
|
||||
"strtof",
|
||||
"strtof_l",
|
||||
@@ -393,24 +316,8 @@ SYMBOL_PATTERNS = {
|
||||
"CSWTCH$",
|
||||
"dst$",
|
||||
"sulp",
|
||||
"_strtol_l", # String to long with locale
|
||||
"__cvt", # Convert
|
||||
"__utoa", # Unsigned to ASCII
|
||||
"__global_locale", # Global locale
|
||||
"_ctype_", # Character type
|
||||
"impure_data", # Impure data
|
||||
],
|
||||
"string_ops": [
|
||||
"strcmp",
|
||||
"strncmp",
|
||||
"strchr",
|
||||
"strstr",
|
||||
"strtok",
|
||||
"strdup",
|
||||
"strncasecmp_P", # String compare (case insensitive, from program memory)
|
||||
"strnlen_P", # String length (from program memory)
|
||||
"strncat_P", # String concatenate (from program memory)
|
||||
],
|
||||
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
|
||||
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
|
||||
"file_io": [
|
||||
"fread",
|
||||
@@ -431,26 +338,10 @@ SYMBOL_PATTERNS = {
|
||||
"vsscanf",
|
||||
],
|
||||
"cpp_anonymous": ["_GLOBAL__N_", "n$"],
|
||||
# Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS
|
||||
"nvs": ["nvs_"], # Plain C NVS functions
|
||||
"ota": ["ota_", "OTA", "esp_ota", "app_desc"],
|
||||
# cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
|
||||
# Only keep patterns that are truly runtime-specific and not categorizable by namespace
|
||||
"cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
|
||||
"exception_handling": [
|
||||
"__cxa_",
|
||||
"_Unwind_",
|
||||
"__gcc_personality",
|
||||
"uw_frame_state",
|
||||
"search_object", # Search for exception handling object
|
||||
"get_cie_encoding", # Get CIE encoding
|
||||
"add_fdes", # Add frame description entries
|
||||
"fde_unencoded_compare", # Compare FDEs
|
||||
"fde_mixed_encoding_compare", # Compare mixed encoding FDEs
|
||||
"frame_downheap", # Frame heap operations
|
||||
"frame_heapsort", # Frame heap sorting
|
||||
],
|
||||
"cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
|
||||
"exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
|
||||
"static_init": ["_GLOBAL__sub_I_"],
|
||||
"mdns_lib": ["mdns"],
|
||||
"phy_radio": [
|
||||
"phy_",
|
||||
"rf_",
|
||||
@@ -503,47 +394,10 @@ SYMBOL_PATTERNS = {
|
||||
"txcal_debuge_mode",
|
||||
"ant_wifitx_cfg",
|
||||
"reg_init_begin",
|
||||
"tx_cap_init", # TX capacitance init
|
||||
"ram_set_txcap", # RAM TX capacitance setting
|
||||
"tx_atten_", # TX attenuation
|
||||
"txiq_", # TX I/Q calibration
|
||||
"ram_cal_", # RAM calibration
|
||||
"ram_rxiq_", # RAM RX I/Q
|
||||
"readvdd33", # Read VDD33
|
||||
"test_tout", # Test timeout
|
||||
"tsen_meas", # Temperature sensor measurement
|
||||
"bbpll_cal", # Baseband PLL calibration
|
||||
"set_cal_", # Set calibration
|
||||
"set_rfanagain_", # Set RF analog gain
|
||||
"set_txdc_", # Set TX DC
|
||||
"get_vdd33_", # Get VDD33
|
||||
"gen_rx_gain_table", # Generate RX gain table
|
||||
"ram_ana_inf_gating_en", # RAM analog interface gating enable
|
||||
"tx_cont_en", # TX continuous enable
|
||||
"tx_delay_cfg", # TX delay configuration
|
||||
"tx_gain_table_set", # TX gain table set
|
||||
"check_and_reset_hw_deadlock", # Hardware deadlock check
|
||||
"s_config", # System/hardware config
|
||||
"chan14_mic_cfg", # Channel 14 MIC config
|
||||
],
|
||||
"wifi_phy_pp": [
|
||||
"pp_",
|
||||
"ppT",
|
||||
"ppR",
|
||||
"ppP",
|
||||
"ppInstall",
|
||||
"ppCalTxAMPDULength",
|
||||
"ppCheckTx", # Packet processor TX check
|
||||
"ppCal", # Packet processor calibration
|
||||
"HdlAllBuffedEb", # Handle buffered EB
|
||||
],
|
||||
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
|
||||
"wifi_lmac": ["lmac"],
|
||||
"wifi_device": [
|
||||
"wdev",
|
||||
"wDev_",
|
||||
"ic_set_sta", # Set station mode
|
||||
"ic_set_vif", # Set virtual interface
|
||||
],
|
||||
"wifi_device": ["wdev", "wDev_"],
|
||||
"power_mgmt": [
|
||||
"pm_",
|
||||
"sleep",
|
||||
@@ -552,7 +406,15 @@ SYMBOL_PATTERNS = {
|
||||
"deep_sleep",
|
||||
"power_down",
|
||||
"g_pm",
|
||||
"pmc", # Power Management Controller
|
||||
],
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
"memory_",
|
||||
"tlsf_",
|
||||
"memp_",
|
||||
"pbuf_",
|
||||
"pbuf_alloc",
|
||||
"pbuf_copy_partial_pbuf",
|
||||
],
|
||||
"hal_layer": ["hal_"],
|
||||
"clock_mgmt": [
|
||||
@@ -577,6 +439,7 @@ SYMBOL_PATTERNS = {
|
||||
"error_handling": ["panic", "abort", "assert", "error_", "fault"],
|
||||
"authentication": ["auth"],
|
||||
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
|
||||
"dhcp": ["dhcp", "handle_dhcp"],
|
||||
"ethernet_phy": [
|
||||
"emac_",
|
||||
"eth_phy_",
|
||||
@@ -755,15 +618,7 @@ SYMBOL_PATTERNS = {
|
||||
"ampdu_dispatch_upto",
|
||||
],
|
||||
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
|
||||
"rate_control": [
|
||||
"rssi_margin",
|
||||
"rcGetSched",
|
||||
"get_rate_fcc_index",
|
||||
"rcGetRate", # Get rate
|
||||
"rc_get_", # Rate control getters
|
||||
"rc_set_", # Rate control setters
|
||||
"rc_enable_", # Rate control enable functions
|
||||
],
|
||||
"rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
|
||||
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
|
||||
"channel_mgmt": ["chm_init", "chm_set_current_channel"],
|
||||
"trace": ["trc_init", "trc_onAmpduOp"],
|
||||
@@ -944,18 +799,31 @@ SYMBOL_PATTERNS = {
|
||||
"supports_interlaced_inquiry_scan",
|
||||
"supports_reading_remote_extended_features",
|
||||
],
|
||||
"bluetooth_ll": [
|
||||
"lld_pdu_",
|
||||
"ld_acl_",
|
||||
"lld_stop_ind_handler",
|
||||
"lld_evt_winsize_change",
|
||||
"config_lld_evt_funcs_reset",
|
||||
"config_lld_funcs_reset",
|
||||
"config_llm_funcs_reset",
|
||||
"llm_set_long_adv_data",
|
||||
"lld_retry_tx_prog",
|
||||
"llc_link_sup_to_ind_handler",
|
||||
"config_llc_funcs_reset",
|
||||
"lld_evt_rxwin_compute",
|
||||
"config_btdm_funcs_reset",
|
||||
"config_ea_funcs_reset",
|
||||
"llc_defalut_state_tab_reset",
|
||||
"config_rwip_funcs_reset",
|
||||
"ke_lmp_rx_flooding_detect",
|
||||
],
|
||||
}
|
||||
|
||||
# Demangled patterns: patterns found in demangled C++ names
|
||||
DEMANGLED_PATTERNS = {
|
||||
"gpio_driver": ["GPIO"],
|
||||
"uart_driver": ["UART"],
|
||||
# mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
|
||||
"mdns_lib": [
|
||||
"MDNSResponder",
|
||||
"MDNSImplementation",
|
||||
"MDNS",
|
||||
],
|
||||
"network_stack": [
|
||||
"lwip",
|
||||
"tcp",
|
||||
@@ -968,24 +836,6 @@ DEMANGLED_PATTERNS = {
|
||||
"ethernet",
|
||||
"ppp",
|
||||
"slip",
|
||||
"UdpContext", # UDP context class
|
||||
"DhcpServer", # DHCP server class
|
||||
],
|
||||
"arduino_core": [
|
||||
"String::", # Arduino String class
|
||||
"Print::", # Arduino Print class
|
||||
"HardwareSerial::", # Serial class
|
||||
"IPAddress::", # IP address class
|
||||
"EspClass::", # ESP class
|
||||
"experimental::_SPI", # Experimental SPI
|
||||
],
|
||||
"ota": [
|
||||
"UpdaterClass",
|
||||
"Updater::",
|
||||
],
|
||||
"wifi": [
|
||||
"ESP8266WiFi",
|
||||
"WiFi::",
|
||||
],
|
||||
"wifi_stack": ["NetworkInterface"],
|
||||
"nimble_bt": [
|
||||
@@ -1004,6 +854,7 @@ DEMANGLED_PATTERNS = {
|
||||
"rtti": ["__type_info", "__class_type_info"],
|
||||
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
|
||||
"async_tcp": ["AsyncClient", "AsyncServer"],
|
||||
"mdns_lib": ["mdns"],
|
||||
"json_lib": [
|
||||
"ArduinoJson",
|
||||
"JsonDocument",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
|
||||
// test
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ enum SaturationVaporPressureEquation {
|
||||
};
|
||||
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
// Test change for clang-tidy split logic
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
repeated string options = 6 [(container_pointer) = "FixedVector"];
|
||||
repeated string options = 6 [(container_pointer) = "std::vector"];
|
||||
bool disabled_by_default = 7;
|
||||
EntityCategory entity_category = 8;
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
|
||||
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_select_response"; }
|
||||
#endif
|
||||
const FixedVector<std::string> *options{};
|
||||
const std::vector<std::string> *options{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
|
||||
@@ -264,31 +264,20 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
),
|
||||
)
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
timings = []
|
||||
if len(config) > 0:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", conf[CONF_DELAY]),
|
||||
("time_off", conf[CONF_TIME_OFF]),
|
||||
("time_on", conf[CONF_TIME_ON]),
|
||||
)
|
||||
timings.extend(
|
||||
(conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
|
||||
for conf in config
|
||||
]
|
||||
)
|
||||
else:
|
||||
timings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
|
||||
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
|
||||
(
|
||||
"time_off",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
),
|
||||
(
|
||||
"time_on",
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
),
|
||||
timings.append(
|
||||
(
|
||||
cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
|
||||
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
|
||||
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
|
||||
)
|
||||
]
|
||||
)
|
||||
var = cg.new_Pvariable(filter_id, timings)
|
||||
await cg.register_component(var, {})
|
||||
return var
|
||||
|
||||
@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
|
||||
last_filter->next_ = filter;
|
||||
}
|
||||
}
|
||||
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
for (Filter *filter : filters) {
|
||||
this->add_filter(filter);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/filter.h"
|
||||
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
|
||||
void publish_initial_state(bool new_state);
|
||||
|
||||
void add_filter(Filter *filter);
|
||||
void add_filters(std::initializer_list<Filter *> filters);
|
||||
void add_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "filter.h"
|
||||
|
||||
#include "binary_sensor.h"
|
||||
#include <utility>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -67,7 +68,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
|
||||
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
|
||||
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
if (value) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
@@ -80,6 +82,11 @@ class InvertFilter : public Filter {
|
||||
};
|
||||
|
||||
struct AutorepeatFilterTiming {
|
||||
AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
|
||||
this->delay = delay;
|
||||
this->time_off = off;
|
||||
this->time_on = on;
|
||||
}
|
||||
uint32_t delay;
|
||||
uint32_t time_off;
|
||||
uint32_t time_on;
|
||||
@@ -87,7 +94,7 @@ struct AutorepeatFilterTiming {
|
||||
|
||||
class AutorepeatFilter : public Filter, public Component {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
|
||||
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
@@ -97,7 +104,7 @@ class AutorepeatFilter : public Filter, public Component {
|
||||
void next_timing_();
|
||||
void next_value_(bool val);
|
||||
|
||||
FixedVector<AutorepeatFilterTiming> timings_;
|
||||
std::vector<AutorepeatFilterTiming> timings_;
|
||||
uint8_t active_timing_{0};
|
||||
};
|
||||
|
||||
|
||||
@@ -385,14 +385,12 @@ void Climate::save_state_() {
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
|
||||
state.uses_custom_fan_mode = true;
|
||||
const auto &supported = traits.get_supported_custom_fan_modes();
|
||||
// std::set has consistent order (lexicographic for strings)
|
||||
size_t i = 0;
|
||||
for (const auto &mode : supported) {
|
||||
if (mode == custom_fan_mode) {
|
||||
std::vector<std::string> vec{supported.begin(), supported.end()};
|
||||
for (size_t i = 0; i < vec.size(); i++) {
|
||||
if (vec[i] == custom_fan_mode) {
|
||||
state.custom_fan_mode = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_presets() && preset.has_value()) {
|
||||
@@ -402,14 +400,12 @@ void Climate::save_state_() {
|
||||
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
|
||||
state.uses_custom_preset = true;
|
||||
const auto &supported = traits.get_supported_custom_presets();
|
||||
// std::set has consistent order (lexicographic for strings)
|
||||
size_t i = 0;
|
||||
for (const auto &preset : supported) {
|
||||
if (preset == custom_preset) {
|
||||
std::vector<std::string> vec{supported.begin(), supported.end()};
|
||||
for (size_t i = 0; i < vec.size(); i++) {
|
||||
if (vec[i] == custom_preset) {
|
||||
state.custom_preset = i;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
@@ -553,34 +549,22 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
climate->fan_mode = this->fan_mode;
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
|
||||
// std::set has consistent order (lexicographic for strings)
|
||||
// std::set has consistent order (lexicographic for strings), so this is ok
|
||||
const auto &modes = traits.get_supported_custom_fan_modes();
|
||||
if (custom_fan_mode < modes.size()) {
|
||||
size_t i = 0;
|
||||
for (const auto &mode : modes) {
|
||||
if (i == this->custom_fan_mode) {
|
||||
climate->custom_fan_mode = mode;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
std::vector<std::string> modes_vec{modes.begin(), modes.end()};
|
||||
if (custom_fan_mode < modes_vec.size()) {
|
||||
climate->custom_fan_mode = modes_vec[this->custom_fan_mode];
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_presets() && !this->uses_custom_preset) {
|
||||
climate->preset = this->preset;
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
|
||||
// std::set has consistent order (lexicographic for strings)
|
||||
// std::set has consistent order (lexicographic for strings), so this is ok
|
||||
const auto &presets = traits.get_supported_custom_presets();
|
||||
if (custom_preset < presets.size()) {
|
||||
size_t i = 0;
|
||||
for (const auto &preset : presets) {
|
||||
if (i == this->custom_preset) {
|
||||
climate->custom_preset = preset;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
std::vector<std::string> presets_vec{presets.begin(), presets.end()};
|
||||
if (custom_preset < presets_vec.size()) {
|
||||
climate->custom_preset = presets_vec[this->custom_preset];
|
||||
}
|
||||
}
|
||||
if (traits.get_supports_swing_modes()) {
|
||||
|
||||
@@ -9,8 +9,7 @@ static const char *const TAG = "copy.select";
|
||||
void CopySelect::setup() {
|
||||
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
|
||||
|
||||
// Copy options from source select
|
||||
this->traits.copy_options(source_->traits.get_options());
|
||||
traits.set_options(source_->traits.get_options());
|
||||
|
||||
if (source_->has_state())
|
||||
this->publish_state(source_->state);
|
||||
|
||||
@@ -112,7 +112,7 @@ async def to_code(config):
|
||||
|
||||
cg.add_define("USE_IMPROV")
|
||||
|
||||
await improv_base.setup_improv_core(var, config, "esp32_improv")
|
||||
await improv_base.setup_improv_core(var, config)
|
||||
|
||||
cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
|
||||
cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
|
||||
|
||||
@@ -389,13 +389,11 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
std::string url_strings[3];
|
||||
size_t url_count = 0;
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_NEXT_URL
|
||||
// Add next_url if configured (should be first per Improv BLE spec)
|
||||
std::string next_url = this->get_formatted_next_url_();
|
||||
if (!next_url.empty()) {
|
||||
url_strings[url_count++] = std::move(next_url);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Add default URLs for backward compatibility
|
||||
url_strings[url_count++] = ESPHOME_MY_LINK;
|
||||
|
||||
@@ -190,9 +190,7 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_VARIANT", "ESP8266")
|
||||
cg.add_define(ThreadModel.SINGLE)
|
||||
|
||||
cg.add_platformio_option(
|
||||
"extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
|
||||
)
|
||||
cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("framework", "arduino")
|
||||
@@ -232,9 +230,9 @@ async def to_code(config):
|
||||
# For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
|
||||
cg.add_build_flag("-DNEW_OOM_ABORT")
|
||||
|
||||
# In testing mode, fake larger memory to allow linking grouped component tests
|
||||
# Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
|
||||
# we pretend it has much larger memory to test that components compile together
|
||||
# In testing mode, fake a larger IRAM to allow linking grouped component tests
|
||||
# Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
|
||||
# This is done via a pre-build script that generates a custom linker script
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
|
||||
@@ -273,8 +271,8 @@ def copy_files():
|
||||
post_build_file,
|
||||
CORE.relative_build_path("post_build.py"),
|
||||
)
|
||||
testing_mode_file = dir / "testing_mode.py.script"
|
||||
iram_fix_file = dir / "iram_fix.py.script"
|
||||
copy_file_if_changed(
|
||||
testing_mode_file,
|
||||
CORE.relative_build_path("testing_mode.py"),
|
||||
iram_fix_file,
|
||||
CORE.relative_build_path("iram_fix.py"),
|
||||
)
|
||||
|
||||
44
esphome/components/esp8266/iram_fix.py.script
Normal file
44
esphome/components/esp8266/iram_fix.py.script
Normal file
@@ -0,0 +1,44 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
|
||||
def patch_linker_script_after_preprocess(source, target, env):
|
||||
"""Patch the local linker script after PlatformIO preprocesses it."""
|
||||
# Check if we're in testing mode by looking for the define
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if not testing_mode:
|
||||
return
|
||||
|
||||
# Get the local linker script path
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
|
||||
|
||||
if not os.path.exists(local_ld):
|
||||
return
|
||||
|
||||
# Read the linker script
|
||||
with open(local_ld, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
|
||||
# The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
|
||||
updated = re.sub(
|
||||
r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
|
||||
r"\g<1>0x200000",
|
||||
content,
|
||||
)
|
||||
|
||||
if updated != content:
|
||||
with open(local_ld, "w") as f:
|
||||
f.write(updated)
|
||||
print("ESPHome: Patched IRAM size to 2MB for testing mode")
|
||||
|
||||
|
||||
# Hook into the build process right before linking
|
||||
# This runs after PlatformIO has already preprocessed the linker scripts
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
|
||||
@@ -1,166 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# pylint: disable=E0602
|
||||
Import("env") # noqa
|
||||
|
||||
|
||||
# Memory sizes for testing mode (allow larger builds for CI component grouping)
|
||||
TESTING_IRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_DRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_FLASH_SIZE = "0x2000000" # 32MB
|
||||
|
||||
|
||||
def patch_segment_size(content, segment_name, new_size, label):
|
||||
"""Patch a memory segment's length in linker script.
|
||||
|
||||
Args:
|
||||
content: Linker script content
|
||||
segment_name: Name of the segment (e.g., 'iram1_0_seg')
|
||||
new_size: New size as hex string (e.g., '0x200000')
|
||||
label: Human-readable label for logging (e.g., 'IRAM')
|
||||
|
||||
Returns:
|
||||
Tuple of (patched_content, was_patched)
|
||||
"""
|
||||
# Match: segment_name : org = 0x..., len = 0x...
|
||||
pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+"
|
||||
new_content = re.sub(pattern, rf"\g<1>{new_size}", content)
|
||||
return new_content, new_content != content
|
||||
|
||||
|
||||
def apply_memory_patches(content):
|
||||
"""Apply IRAM, DRAM, and Flash patches to linker script content.
|
||||
|
||||
Args:
|
||||
content: Linker script content as string
|
||||
|
||||
Returns:
|
||||
Patched content as string
|
||||
"""
|
||||
patches_applied = []
|
||||
|
||||
# Patch IRAM (for larger code in IRAM)
|
||||
content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM")
|
||||
if patched:
|
||||
patches_applied.append("IRAM")
|
||||
|
||||
# Patch DRAM (for larger BSS/data sections)
|
||||
content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM")
|
||||
if patched:
|
||||
patches_applied.append("DRAM")
|
||||
|
||||
# Patch Flash (for larger code sections)
|
||||
content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash")
|
||||
if patched:
|
||||
patches_applied.append("Flash")
|
||||
|
||||
if patches_applied:
|
||||
iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024)
|
||||
dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024)
|
||||
flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024)
|
||||
print(f" Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)")
|
||||
|
||||
return content
|
||||
|
||||
|
||||
def patch_linker_script_file(filepath, description):
|
||||
"""Patch a linker script file in the build directory with enlarged memory segments.
|
||||
|
||||
This function modifies linker scripts in the build directory only (never SDK files).
|
||||
It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode.
|
||||
|
||||
Args:
|
||||
filepath: Path to the linker script file in the build directory
|
||||
description: Human-readable description for logging
|
||||
|
||||
Returns:
|
||||
True if the file was patched, False if already patched or not found
|
||||
"""
|
||||
if not os.path.exists(filepath):
|
||||
print(f"ESPHome: {description} not found at {filepath}")
|
||||
return False
|
||||
|
||||
print(f"ESPHome: Patching {description}...")
|
||||
with open(filepath, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
patched_content = apply_memory_patches(content)
|
||||
|
||||
if patched_content != content:
|
||||
with open(filepath, "w") as f:
|
||||
f.write(patched_content)
|
||||
print(f"ESPHome: Successfully patched {description}")
|
||||
return True
|
||||
else:
|
||||
print(f"ESPHome: {description} already patched or no changes needed")
|
||||
return False
|
||||
|
||||
|
||||
def patch_local_linker_script(source, target, env):
|
||||
"""Patch the local.eagle.app.v6.common.ld in build directory.
|
||||
|
||||
This patches the preprocessed linker script that PlatformIO creates in the build
|
||||
directory, enlarging IRAM, DRAM, and Flash segments for testing mode.
|
||||
|
||||
Args:
|
||||
source: SCons source nodes
|
||||
target: SCons target nodes
|
||||
env: SCons environment
|
||||
"""
|
||||
# Check if we're in testing mode
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if not testing_mode:
|
||||
return
|
||||
|
||||
# Patch the local linker script if it exists
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ld_dir = os.path.join(build_dir, "ld")
|
||||
if os.path.exists(ld_dir):
|
||||
local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld")
|
||||
if os.path.exists(local_ld):
|
||||
patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld")
|
||||
|
||||
|
||||
# Check if we're in testing mode
|
||||
build_flags = env.get("BUILD_FLAGS", [])
|
||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
|
||||
|
||||
if testing_mode:
|
||||
# Create a custom linker script in the build directory with patched memory limits
|
||||
# This allows larger IRAM/DRAM/Flash for CI component grouping tests
|
||||
build_dir = env.subst("$BUILD_DIR")
|
||||
ldscript = env.GetProjectOption("board_build.ldscript", "")
|
||||
assert ldscript, "No linker script configured in board_build.ldscript"
|
||||
|
||||
framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266")
|
||||
assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package"
|
||||
|
||||
# Read the original SDK linker script (read-only, SDK is never modified)
|
||||
sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript)
|
||||
# Create a custom version in the build directory (isolated, temporary)
|
||||
custom_ld = os.path.join(build_dir, f"testing_{ldscript}")
|
||||
|
||||
if os.path.exists(sdk_ld) and not os.path.exists(custom_ld):
|
||||
# Read the SDK linker script
|
||||
with open(sdk_ld, "r") as f:
|
||||
content = f.read()
|
||||
|
||||
# Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB)
|
||||
patched_content = apply_memory_patches(content)
|
||||
|
||||
# Write the patched linker script to the build directory
|
||||
with open(custom_ld, "w") as f:
|
||||
f.write(patched_content)
|
||||
|
||||
print(f"ESPHome: Created custom linker script: {custom_ld}")
|
||||
|
||||
# Tell the linker to use our custom script from the build directory
|
||||
assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}"
|
||||
env.Replace(LDSCRIPT_PATH=custom_ld)
|
||||
print(f"ESPHome: Using custom linker script with patched memory limits")
|
||||
|
||||
# Also patch local.eagle.app.v6.common.ld after PlatformIO creates it
|
||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script)
|
||||
@@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
|
||||
|
||||
public:
|
||||
void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->sent_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||
}
|
||||
}
|
||||
void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->error_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
||||
|
||||
@@ -3,8 +3,6 @@ import re
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import __version__
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
@@ -37,9 +35,7 @@ def _process_next_url(url: str):
|
||||
return url
|
||||
|
||||
|
||||
async def setup_improv_core(var: MockObj, config: ConfigType, component: str):
|
||||
if next_url := config.get(CONF_NEXT_URL):
|
||||
cg.add(var.set_next_url(_process_next_url(next_url)))
|
||||
cg.add_define(f"USE_{component.upper()}_NEXT_URL")
|
||||
|
||||
async def setup_improv_core(var, config):
|
||||
if CONF_NEXT_URL in config:
|
||||
cg.add(var.set_next_url(_process_next_url(config[CONF_NEXT_URL])))
|
||||
cg.add_library("improv/Improv", "1.2.4")
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace improv_base {
|
||||
|
||||
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
|
||||
static constexpr const char DEVICE_NAME_PLACEHOLDER[] = "{{device_name}}";
|
||||
static constexpr size_t DEVICE_NAME_PLACEHOLDER_LEN = sizeof(DEVICE_NAME_PLACEHOLDER) - 1;
|
||||
static constexpr const char IP_ADDRESS_PLACEHOLDER[] = "{{ip_address}}";
|
||||
@@ -45,7 +43,6 @@ std::string ImprovBase::get_formatted_next_url_() {
|
||||
|
||||
return formatted_url;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace improv_base
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace improv_base {
|
||||
|
||||
class ImprovBase {
|
||||
public:
|
||||
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
|
||||
void set_next_url(const std::string &next_url) { this->next_url_ = next_url; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
#if defined(USE_ESP32_IMPROV_NEXT_URL) || defined(USE_IMPROV_SERIAL_NEXT_URL)
|
||||
std::string get_formatted_next_url_();
|
||||
std::string next_url_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace improv_base
|
||||
|
||||
@@ -43,4 +43,4 @@ FINAL_VALIDATE_SCHEMA = validate_logger
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await improv_base.setup_improv_core(var, config, "improv_serial")
|
||||
await improv_base.setup_improv_core(var, config)
|
||||
|
||||
@@ -146,11 +146,9 @@ void ImprovSerialComponent::loop() {
|
||||
|
||||
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
|
||||
std::vector<std::string> urls;
|
||||
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
||||
if (!this->next_url_.empty()) {
|
||||
urls.push_back(this->get_formatted_next_url_());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
|
||||
@@ -62,7 +62,7 @@ void AddressableLightTransformer::start() {
|
||||
}
|
||||
|
||||
optional<LightColorValues> AddressableLightTransformer::apply() {
|
||||
float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_());
|
||||
float smoothed_progress = LightTransitionTransformer::smoothed_progress(this->get_progress_());
|
||||
|
||||
// When running an output-buffer modifying effect, don't try to transition individual LEDs, but instead just fade the
|
||||
// LightColorValues. write_state() then picks up the change in brightness, and the color change is picked up by the
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "light_output.h"
|
||||
#include "light_state.h"
|
||||
#include "light_transformer.h"
|
||||
#include "transformers.h"
|
||||
|
||||
#ifdef USE_POWER_SUPPLY
|
||||
#include "esphome/components/power_supply/power_supply.h"
|
||||
@@ -103,7 +103,7 @@ class AddressableLight : public LightOutput, public Component {
|
||||
bool effect_active_{false};
|
||||
};
|
||||
|
||||
class AddressableLightTransformer : public LightTransformer {
|
||||
class AddressableLightTransformer : public LightTransitionTransformer {
|
||||
public:
|
||||
AddressableLightTransformer(AddressableLight &light) : light_(light) {}
|
||||
|
||||
|
||||
@@ -38,10 +38,6 @@ class LightTransformer {
|
||||
const LightColorValues &get_target_values() const { return this->target_values_; }
|
||||
|
||||
protected:
|
||||
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
|
||||
// transition from 0 to 1 on x = [0, 1]
|
||||
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
|
||||
|
||||
/// The progress of this transition, on a scale of 0 to 1.
|
||||
float get_progress_() {
|
||||
uint32_t now = esphome::millis();
|
||||
|
||||
@@ -50,11 +50,15 @@ class LightTransitionTransformer : public LightTransformer {
|
||||
if (this->changing_color_mode_)
|
||||
p = p < 0.5f ? p * 2 : (p - 0.5) * 2;
|
||||
|
||||
float v = LightTransformer::smoothed_progress(p);
|
||||
float v = LightTransitionTransformer::smoothed_progress(p);
|
||||
return LightColorValues::lerp(start, end, v);
|
||||
}
|
||||
|
||||
protected:
|
||||
// This looks crazy, but it reduces to 6x^5 - 15x^4 + 10x^3 which is just a smooth sigmoid-like
|
||||
// transition from 0 to 1 on x = [0, 1]
|
||||
static float smoothed_progress(float x) { return x * x * x * (x * (x * 6.0f - 15.0f) + 10.0f); }
|
||||
|
||||
LightColorValues end_values_{};
|
||||
LightColorValues intermediate_values_{};
|
||||
bool changing_color_mode_{false};
|
||||
|
||||
@@ -300,11 +300,11 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a
|
||||
}
|
||||
}
|
||||
|
||||
void LvSelectable::set_options(std::initializer_list<std::string> options) {
|
||||
void LvSelectable::set_options(std::vector<std::string> options) {
|
||||
auto index = this->get_selected_index();
|
||||
if (index >= options.size())
|
||||
index = options.size() - 1;
|
||||
this->options_ = options;
|
||||
this->options_ = std::move(options);
|
||||
this->set_option_string(join_string(this->options_).c_str());
|
||||
lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr);
|
||||
this->set_selected_index(index, LV_ANIM_OFF);
|
||||
|
||||
@@ -358,12 +358,12 @@ class LvSelectable : public LvCompound {
|
||||
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
|
||||
void set_selected_text(const std::string &text, lv_anim_enable_t anim);
|
||||
std::string get_selected_text();
|
||||
const FixedVector<std::string> &get_options() { return this->options_; }
|
||||
void set_options(std::initializer_list<std::string> options);
|
||||
std::vector<std::string> get_options() { return this->options_; }
|
||||
void set_options(std::vector<std::string> options);
|
||||
|
||||
protected:
|
||||
virtual void set_option_string(const char *options) = 0;
|
||||
FixedVector<std::string> options_{};
|
||||
std::vector<std::string> options_{};
|
||||
};
|
||||
|
||||
#ifdef USE_LVGL_DROPDOWN
|
||||
|
||||
@@ -53,10 +53,7 @@ class LVGLSelect : public select::Select, public Component {
|
||||
this->widget_->set_selected_text(value, this->anim_);
|
||||
this->publish();
|
||||
}
|
||||
void set_options_() {
|
||||
// Copy options from lvgl widget to select traits
|
||||
this->traits.copy_options(this->widget_->get_options());
|
||||
}
|
||||
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
|
||||
|
||||
LvSelectable *widget_;
|
||||
lv_anim_enable_t anim_;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
void SelectTraits::set_options(std::initializer_list<std::string> options) { this->options_ = options; }
|
||||
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
|
||||
|
||||
const FixedVector<std::string> &SelectTraits::get_options() const { return this->options_; }
|
||||
const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; }
|
||||
|
||||
} // namespace select
|
||||
} // namespace esphome
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <initializer_list>
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace select {
|
||||
|
||||
class SelectTraits {
|
||||
public:
|
||||
void set_options(std::initializer_list<std::string> options);
|
||||
const FixedVector<std::string> &get_options() const;
|
||||
/// Copy options from another SelectTraits (for copy_select, lvgl)
|
||||
void copy_options(const FixedVector<std::string> &other) { this->options_.copy_from(other); }
|
||||
void set_options(std::vector<std::string> options);
|
||||
const std::vector<std::string> &get_options() const;
|
||||
|
||||
protected:
|
||||
FixedVector<std::string> options_;
|
||||
std::vector<std::string> options_;
|
||||
};
|
||||
|
||||
} // namespace select
|
||||
|
||||
@@ -28,8 +28,6 @@ from esphome.const import (
|
||||
CONF_ON_RAW_VALUE,
|
||||
CONF_ON_VALUE,
|
||||
CONF_ON_VALUE_RANGE,
|
||||
CONF_OPTIMISTIC,
|
||||
CONF_PERIOD,
|
||||
CONF_QUANTILE,
|
||||
CONF_SEND_EVERY,
|
||||
CONF_SEND_FIRST_AT,
|
||||
@@ -646,29 +644,10 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
|
||||
|
||||
|
||||
HEARTBEAT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PERIOD): cv.positive_time_period_milliseconds,
|
||||
cv.Optional(CONF_OPTIMISTIC, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"heartbeat",
|
||||
HeartbeatFilter,
|
||||
cv.Any(
|
||||
cv.positive_time_period_milliseconds,
|
||||
HEARTBEAT_SCHEMA,
|
||||
),
|
||||
"heartbeat", HeartbeatFilter, cv.positive_time_period_milliseconds
|
||||
)
|
||||
async def heartbeat_filter_to_code(config, filter_id):
|
||||
if isinstance(config, dict):
|
||||
var = cg.new_Pvariable(filter_id, config[CONF_PERIOD])
|
||||
await cg.register_component(var, {})
|
||||
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
|
||||
return var
|
||||
|
||||
var = cg.new_Pvariable(filter_id, config)
|
||||
await cg.register_component(var, {})
|
||||
return var
|
||||
|
||||
@@ -313,7 +313,7 @@ optional<float> DeltaFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// OrFilter
|
||||
OrFilter::OrFilter(std::initializer_list<Filter *> filters) : filters_(filters), phi_(this) {}
|
||||
OrFilter::OrFilter(std::vector<Filter *> filters) : filters_(std::move(filters)), phi_(this) {}
|
||||
OrFilter::PhiNode::PhiNode(OrFilter *or_parent) : or_parent_(or_parent) {}
|
||||
|
||||
optional<float> OrFilter::PhiNode::new_value(float value) {
|
||||
@@ -326,14 +326,14 @@ optional<float> OrFilter::PhiNode::new_value(float value) {
|
||||
}
|
||||
optional<float> OrFilter::new_value(float value) {
|
||||
this->has_value_ = false;
|
||||
for (auto *filter : this->filters_)
|
||||
for (Filter *filter : this->filters_)
|
||||
filter->input(value);
|
||||
|
||||
return {};
|
||||
}
|
||||
void OrFilter::initialize(Sensor *parent, Filter *next) {
|
||||
Filter::initialize(parent, next);
|
||||
for (auto *filter : this->filters_) {
|
||||
for (Filter *filter : this->filters_) {
|
||||
filter->initialize(parent, &this->phi_);
|
||||
}
|
||||
this->phi_.initialize(parent, nullptr);
|
||||
@@ -372,12 +372,8 @@ optional<float> HeartbeatFilter::new_value(float value) {
|
||||
this->last_input_ = value;
|
||||
this->has_value_ = true;
|
||||
|
||||
if (this->optimistic_) {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void HeartbeatFilter::setup() {
|
||||
this->set_interval("heartbeat", this->time_period_, [this]() {
|
||||
ESP_LOGVV(TAG, "HeartbeatFilter(%p)::interval(has_value=%s, last_input=%f)", this, YESNO(this->has_value_),
|
||||
@@ -388,27 +384,20 @@ void HeartbeatFilter::setup() {
|
||||
this->output(this->last_input_);
|
||||
});
|
||||
}
|
||||
|
||||
float HeartbeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
CalibrateLinearFilter::CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions)
|
||||
: linear_functions_(linear_functions) {}
|
||||
|
||||
optional<float> CalibrateLinearFilter::new_value(float value) {
|
||||
for (const auto &f : this->linear_functions_) {
|
||||
for (std::array<float, 3> f : this->linear_functions_) {
|
||||
if (!std::isfinite(f[2]) || value < f[2])
|
||||
return (value * f[0]) + f[1];
|
||||
}
|
||||
return NAN;
|
||||
}
|
||||
|
||||
CalibratePolynomialFilter::CalibratePolynomialFilter(std::initializer_list<float> coefficients)
|
||||
: coefficients_(coefficients) {}
|
||||
|
||||
optional<float> CalibratePolynomialFilter::new_value(float value) {
|
||||
float res = 0.0f;
|
||||
float x = 1.0f;
|
||||
for (const auto &coefficient : this->coefficients_) {
|
||||
for (float coefficient : this->coefficients_) {
|
||||
res += x * coefficient;
|
||||
x *= value;
|
||||
}
|
||||
|
||||
@@ -396,16 +396,15 @@ class HeartbeatFilter : public Filter, public Component {
|
||||
explicit HeartbeatFilter(uint32_t time_period);
|
||||
|
||||
void setup() override;
|
||||
optional<float> new_value(float value) override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
uint32_t time_period_;
|
||||
float last_input_;
|
||||
bool has_value_{false};
|
||||
bool optimistic_{false};
|
||||
};
|
||||
|
||||
class DeltaFilter : public Filter {
|
||||
@@ -423,7 +422,7 @@ class DeltaFilter : public Filter {
|
||||
|
||||
class OrFilter : public Filter {
|
||||
public:
|
||||
explicit OrFilter(std::initializer_list<Filter *> filters);
|
||||
explicit OrFilter(std::vector<Filter *> filters);
|
||||
|
||||
void initialize(Sensor *parent, Filter *next) override;
|
||||
|
||||
@@ -439,27 +438,28 @@ class OrFilter : public Filter {
|
||||
OrFilter *or_parent_;
|
||||
};
|
||||
|
||||
FixedVector<Filter *> filters_;
|
||||
std::vector<Filter *> filters_;
|
||||
PhiNode phi_;
|
||||
bool has_value_{false};
|
||||
};
|
||||
|
||||
class CalibrateLinearFilter : public Filter {
|
||||
public:
|
||||
explicit CalibrateLinearFilter(std::initializer_list<std::array<float, 3>> linear_functions);
|
||||
CalibrateLinearFilter(std::vector<std::array<float, 3>> linear_functions)
|
||||
: linear_functions_(std::move(linear_functions)) {}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
FixedVector<std::array<float, 3>> linear_functions_;
|
||||
std::vector<std::array<float, 3>> linear_functions_;
|
||||
};
|
||||
|
||||
class CalibratePolynomialFilter : public Filter {
|
||||
public:
|
||||
explicit CalibratePolynomialFilter(std::initializer_list<float> coefficients);
|
||||
CalibratePolynomialFilter(std::vector<float> coefficients) : coefficients_(std::move(coefficients)) {}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
FixedVector<float> coefficients_;
|
||||
std::vector<float> coefficients_;
|
||||
};
|
||||
|
||||
class ClampFilter : public Filter {
|
||||
|
||||
@@ -107,12 +107,12 @@ void Sensor::add_filter(Filter *filter) {
|
||||
}
|
||||
filter->initialize(this, nullptr);
|
||||
}
|
||||
void Sensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
void Sensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
for (Filter *filter : filters) {
|
||||
this->add_filter(filter);
|
||||
}
|
||||
}
|
||||
void Sensor::set_filters(std::initializer_list<Filter *> filters) {
|
||||
void Sensor::set_filters(const std::vector<Filter *> &filters) {
|
||||
this->clear_filters();
|
||||
this->add_filters(filters);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/components/sensor/filter.h"
|
||||
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome {
|
||||
@@ -77,10 +77,10 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
|
||||
* SlidingWindowMovingAverageFilter(15, 15), // average over last 15 values
|
||||
* });
|
||||
*/
|
||||
void add_filters(std::initializer_list<Filter *> filters);
|
||||
void add_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
/// Clear the filters and replace them by filters.
|
||||
void set_filters(std::initializer_list<Filter *> filters);
|
||||
void set_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
/// Clear the entire filter chain.
|
||||
void clear_filters();
|
||||
|
||||
@@ -110,28 +110,17 @@ def validate_mapping(value):
|
||||
"substitute", SubstituteFilter, cv.ensure_list(validate_mapping)
|
||||
)
|
||||
async def substitute_filter_to_code(config, filter_id):
|
||||
substitutions = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("Substitution", "esphome::text_sensor::"),
|
||||
("from", conf[CONF_FROM]),
|
||||
("to", conf[CONF_TO]),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
return cg.new_Pvariable(filter_id, substitutions)
|
||||
from_strings = [conf[CONF_FROM] for conf in config]
|
||||
to_strings = [conf[CONF_TO] for conf in config]
|
||||
return cg.new_Pvariable(filter_id, from_strings, to_strings)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("map", MapFilter, cv.ensure_list(validate_mapping))
|
||||
async def map_filter_to_code(config, filter_id):
|
||||
mappings = [
|
||||
cg.StructInitializer(
|
||||
cg.MockObj("Substitution", "esphome::text_sensor::"),
|
||||
("from", conf[CONF_FROM]),
|
||||
("to", conf[CONF_TO]),
|
||||
)
|
||||
for conf in config
|
||||
]
|
||||
return cg.new_Pvariable(filter_id, mappings)
|
||||
map_ = cg.std_ns.class_("map").template(cg.std_string, cg.std_string)
|
||||
return cg.new_Pvariable(
|
||||
filter_id, map_([(item[CONF_FROM], item[CONF_TO]) for item in config])
|
||||
)
|
||||
|
||||
|
||||
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
|
||||
@@ -62,27 +62,19 @@ optional<std::string> AppendFilter::new_value(std::string value) { return value
|
||||
optional<std::string> PrependFilter::new_value(std::string value) { return this->prefix_ + value; }
|
||||
|
||||
// Substitute
|
||||
SubstituteFilter::SubstituteFilter(const std::initializer_list<Substitution> &substitutions)
|
||||
: substitutions_(substitutions) {}
|
||||
|
||||
optional<std::string> SubstituteFilter::new_value(std::string value) {
|
||||
std::size_t pos;
|
||||
for (const auto &sub : this->substitutions_) {
|
||||
while ((pos = value.find(sub.from)) != std::string::npos)
|
||||
value.replace(pos, sub.from.size(), sub.to);
|
||||
for (size_t i = 0; i < this->from_strings_.size(); i++) {
|
||||
while ((pos = value.find(this->from_strings_[i])) != std::string::npos)
|
||||
value.replace(pos, this->from_strings_[i].size(), this->to_strings_[i]);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Map
|
||||
MapFilter::MapFilter(const std::initializer_list<Substitution> &mappings) : mappings_(mappings) {}
|
||||
|
||||
optional<std::string> MapFilter::new_value(std::string value) {
|
||||
for (const auto &mapping : this->mappings_) {
|
||||
if (mapping.from == value)
|
||||
return mapping.to;
|
||||
}
|
||||
return value; // Pass through if no match
|
||||
auto item = mappings_.find(value);
|
||||
return item == mappings_.end() ? value : item->second;
|
||||
}
|
||||
|
||||
} // namespace text_sensor
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <queue>
|
||||
#include <utility>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace text_sensor {
|
||||
@@ -94,52 +98,26 @@ class PrependFilter : public Filter {
|
||||
std::string prefix_;
|
||||
};
|
||||
|
||||
struct Substitution {
|
||||
std::string from;
|
||||
std::string to;
|
||||
};
|
||||
|
||||
/// A simple filter that replaces a substring with another substring
|
||||
class SubstituteFilter : public Filter {
|
||||
public:
|
||||
explicit SubstituteFilter(const std::initializer_list<Substitution> &substitutions);
|
||||
SubstituteFilter(std::vector<std::string> from_strings, std::vector<std::string> to_strings)
|
||||
: from_strings_(std::move(from_strings)), to_strings_(std::move(to_strings)) {}
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
|
||||
protected:
|
||||
FixedVector<Substitution> substitutions_;
|
||||
std::vector<std::string> from_strings_;
|
||||
std::vector<std::string> to_strings_;
|
||||
};
|
||||
|
||||
/** A filter that maps values from one set to another
|
||||
*
|
||||
* Uses linear search instead of std::map for typical small datasets (2-20 mappings).
|
||||
* Linear search on contiguous memory is faster than red-black tree lookups when:
|
||||
* - Dataset is small (< ~30 items)
|
||||
* - Memory is contiguous (cache-friendly, better CPU cache utilization)
|
||||
* - No pointer chasing overhead (tree node traversal)
|
||||
* - String comparison cost dominates lookup time
|
||||
*
|
||||
* Benchmark results (see benchmark_map_filter.cpp):
|
||||
* - 2 mappings: Linear 1.26x faster than std::map
|
||||
* - 5 mappings: Linear 2.25x faster than std::map
|
||||
* - 10 mappings: Linear 1.83x faster than std::map
|
||||
* - 20 mappings: Linear 1.59x faster than std::map
|
||||
* - 30 mappings: Linear 1.09x faster than std::map
|
||||
* - 40 mappings: std::map 1.27x faster than Linear (break-even)
|
||||
*
|
||||
* Benefits over std::map:
|
||||
* - ~2KB smaller flash (no red-black tree code)
|
||||
* - ~24-32 bytes less RAM per mapping (no tree node overhead)
|
||||
* - Faster for typical ESPHome usage (2-10 mappings common, 20+ rare)
|
||||
*
|
||||
* Break-even point: ~35-40 mappings, but ESPHome configs rarely exceed 20
|
||||
*/
|
||||
/// A filter that maps values from one set to another
|
||||
class MapFilter : public Filter {
|
||||
public:
|
||||
explicit MapFilter(const std::initializer_list<Substitution> &mappings);
|
||||
MapFilter(std::map<std::string, std::string> mappings) : mappings_(std::move(mappings)) {}
|
||||
optional<std::string> new_value(std::string value) override;
|
||||
|
||||
protected:
|
||||
FixedVector<Substitution> mappings_;
|
||||
std::map<std::string, std::string> mappings_;
|
||||
};
|
||||
|
||||
} // namespace text_sensor
|
||||
|
||||
@@ -51,12 +51,12 @@ void TextSensor::add_filter(Filter *filter) {
|
||||
}
|
||||
filter->initialize(this, nullptr);
|
||||
}
|
||||
void TextSensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
void TextSensor::add_filters(const std::vector<Filter *> &filters) {
|
||||
for (Filter *filter : filters) {
|
||||
this->add_filter(filter);
|
||||
}
|
||||
}
|
||||
void TextSensor::set_filters(std::initializer_list<Filter *> filters) {
|
||||
void TextSensor::set_filters(const std::vector<Filter *> &filters) {
|
||||
this->clear_filters();
|
||||
this->add_filters(filters);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/text_sensor/filter.h"
|
||||
|
||||
#include <initializer_list>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome {
|
||||
@@ -37,10 +37,10 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
|
||||
void add_filter(Filter *filter);
|
||||
|
||||
/// Add a list of vectors to the back of the filter chain.
|
||||
void add_filters(std::initializer_list<Filter *> filters);
|
||||
void add_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
/// Clear the filters and replace them by filters.
|
||||
void set_filters(std::initializer_list<Filter *> filters);
|
||||
void set_filters(const std::vector<Filter *> &filters);
|
||||
|
||||
/// Clear the entire filter chain.
|
||||
void clear_filters();
|
||||
|
||||
@@ -378,18 +378,14 @@ async def to_code(config):
|
||||
# Track if any network uses Enterprise authentication
|
||||
has_eap = False
|
||||
|
||||
# Build all WiFiAP objects
|
||||
networks = config.get(CONF_NETWORKS, [])
|
||||
if networks:
|
||||
wifi_aps = []
|
||||
for network in networks:
|
||||
if CONF_EAP in network:
|
||||
has_eap = True
|
||||
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
|
||||
wifi_aps.append(wifi_network(network, WiFiAP(), ip_config))
|
||||
def add_sta(ap, network):
|
||||
ip_config = network.get(CONF_MANUAL_IP, config.get(CONF_MANUAL_IP))
|
||||
cg.add(var.add_sta(wifi_network(network, ap, ip_config)))
|
||||
|
||||
# Set all WiFi networks at once
|
||||
cg.add(var.set_stas(wifi_aps))
|
||||
for network in config.get(CONF_NETWORKS, []):
|
||||
if CONF_EAP in network:
|
||||
has_eap = True
|
||||
cg.with_local_variable(network[CONF_ID], WiFiAP(), add_sta, network)
|
||||
|
||||
if CONF_AP in config:
|
||||
conf = config[CONF_AP]
|
||||
|
||||
@@ -330,8 +330,11 @@ float WiFiComponent::get_loop_priority() const {
|
||||
return 10.0f; // before other loop components
|
||||
}
|
||||
|
||||
void WiFiComponent::set_stas(const std::initializer_list<WiFiAP> &aps) { this->sta_ = aps; }
|
||||
void WiFiComponent::set_sta(const WiFiAP &ap) { this->set_stas({ap}); }
|
||||
void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
|
||||
void WiFiComponent::set_sta(const WiFiAP &ap) {
|
||||
this->clear_sta();
|
||||
this->add_sta(ap);
|
||||
}
|
||||
void WiFiComponent::clear_sta() { this->sta_.clear(); }
|
||||
void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
|
||||
SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
|
||||
|
||||
@@ -219,7 +219,7 @@ class WiFiComponent : public Component {
|
||||
|
||||
void set_sta(const WiFiAP &ap);
|
||||
WiFiAP get_sta() { return this->selected_ap_; }
|
||||
void set_stas(const std::initializer_list<WiFiAP> &aps);
|
||||
void add_sta(const WiFiAP &ap);
|
||||
void clear_sta();
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
@@ -393,7 +393,7 @@ class WiFiComponent : public Component {
|
||||
#endif
|
||||
|
||||
std::string use_address_;
|
||||
FixedVector<WiFiAP> sta_;
|
||||
std::vector<WiFiAP> sta_;
|
||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||
WiFiAP selected_ap_;
|
||||
|
||||
@@ -471,7 +471,6 @@ CONF_IMPORT_REACTIVE_ENERGY = "import_reactive_energy"
|
||||
CONF_INC_PIN = "inc_pin"
|
||||
CONF_INCLUDE_INTERNAL = "include_internal"
|
||||
CONF_INCLUDES = "includes"
|
||||
CONF_INCLUDES_C = "includes_c"
|
||||
CONF_INDEX = "index"
|
||||
CONF_INDOOR = "indoor"
|
||||
CONF_INFRARED = "infrared"
|
||||
|
||||
@@ -243,7 +243,7 @@ template<typename... Ts> class ActionList {
|
||||
}
|
||||
this->actions_end_ = action;
|
||||
}
|
||||
void add_actions(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
void add_actions(const std::vector<Action<Ts...> *> &actions) {
|
||||
for (auto *action : actions) {
|
||||
this->add_action(action);
|
||||
}
|
||||
@@ -286,7 +286,7 @@ template<typename... Ts> class Automation {
|
||||
explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); }
|
||||
|
||||
void add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
|
||||
void add_actions(const std::initializer_list<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); }
|
||||
void add_actions(const std::vector<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); }
|
||||
|
||||
void stop() { this->actions_.stop(); }
|
||||
|
||||
|
||||
@@ -194,12 +194,12 @@ template<typename... Ts> class IfAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}
|
||||
|
||||
void add_then(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
void add_then(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->then_.add_actions(actions);
|
||||
this->then_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||
}
|
||||
|
||||
void add_else(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
void add_else(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->else_.add_actions(actions);
|
||||
this->else_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||
}
|
||||
@@ -240,7 +240,7 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
|
||||
public:
|
||||
WhileAction(Condition<Ts...> *condition) : condition_(condition) {}
|
||||
|
||||
void add_then(const std::initializer_list<Action<Ts...> *> &actions) {
|
||||
void add_then(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->then_.add_actions(actions);
|
||||
this->then_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
||||
if (this->num_running_ > 0 && this->condition_->check_tuple(this->var_)) {
|
||||
@@ -287,7 +287,7 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, count)
|
||||
|
||||
void add_then(const std::initializer_list<Action<uint32_t, Ts...> *> &actions) {
|
||||
void add_then(const std::vector<Action<uint32_t, Ts...> *> &actions) {
|
||||
this->then_.add_actions(actions);
|
||||
this->then_.add_action(new LambdaAction<uint32_t, Ts...>([this](uint32_t iteration, Ts... x) {
|
||||
iteration++;
|
||||
|
||||
@@ -21,7 +21,6 @@ from esphome.const import (
|
||||
CONF_FRIENDLY_NAME,
|
||||
CONF_ID,
|
||||
CONF_INCLUDES,
|
||||
CONF_INCLUDES_C,
|
||||
CONF_LIBRARIES,
|
||||
CONF_MIN_VERSION,
|
||||
CONF_NAME,
|
||||
@@ -228,7 +227,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include),
|
||||
cv.Optional(CONF_INCLUDES_C, default=[]): cv.ensure_list(valid_include),
|
||||
cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict),
|
||||
cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean,
|
||||
cv.Optional(CONF_DEBUG_SCHEDULER, default=False): cv.boolean,
|
||||
@@ -304,17 +302,6 @@ def _list_target_platforms():
|
||||
return target_platforms
|
||||
|
||||
|
||||
def _sort_includes_by_type(includes: list[str]) -> tuple[list[str], list[str]]:
|
||||
system_includes = []
|
||||
other_includes = []
|
||||
for include in includes:
|
||||
if include.startswith("<") and include.endswith(">"):
|
||||
system_includes.append(include)
|
||||
else:
|
||||
other_includes.append(include)
|
||||
return system_includes, other_includes
|
||||
|
||||
|
||||
def preload_core_config(config, result) -> str:
|
||||
with cv.prepend_path(CONF_ESPHOME):
|
||||
conf = PRELOAD_CONFIG_SCHEMA(config[CONF_ESPHOME])
|
||||
@@ -352,7 +339,7 @@ def preload_core_config(config, result) -> str:
|
||||
return target_platforms[0]
|
||||
|
||||
|
||||
def include_file(path: Path, basename: Path, is_c_header: bool = False):
|
||||
def include_file(path: Path, basename: Path):
|
||||
parts = basename.parts
|
||||
dst = CORE.relative_src_path(*parts)
|
||||
copy_file_if_changed(path, dst)
|
||||
@@ -360,14 +347,7 @@ def include_file(path: Path, basename: Path, is_c_header: bool = False):
|
||||
ext = path.suffix
|
||||
if ext in [".h", ".hpp", ".tcc"]:
|
||||
# Header, add include statement
|
||||
if is_c_header:
|
||||
# Wrap in extern "C" block for C headers
|
||||
cg.add_global(
|
||||
cg.RawStatement(f'extern "C" {{\n #include "{basename}"\n}}')
|
||||
)
|
||||
else:
|
||||
# Regular include
|
||||
cg.add_global(cg.RawStatement(f'#include "{basename}"'))
|
||||
cg.add_global(cg.RawStatement(f'#include "{basename}"'))
|
||||
|
||||
|
||||
ARDUINO_GLUE_CODE = """\
|
||||
@@ -397,7 +377,7 @@ async def add_arduino_global_workaround():
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def add_includes(includes: list[str], is_c_header: bool = False) -> None:
|
||||
async def add_includes(includes: list[str]) -> None:
|
||||
# Add includes at the very end, so that the included files can access global variables
|
||||
for include in includes:
|
||||
path = CORE.relative_config_path(include)
|
||||
@@ -405,11 +385,11 @@ async def add_includes(includes: list[str], is_c_header: bool = False) -> None:
|
||||
# Directory, copy tree
|
||||
for p in walk_files(path):
|
||||
basename = p.relative_to(path.parent)
|
||||
include_file(p, basename, is_c_header)
|
||||
include_file(p, basename)
|
||||
else:
|
||||
# Copy file
|
||||
basename = Path(path.name)
|
||||
include_file(path, basename, is_c_header)
|
||||
include_file(path, basename)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
@@ -514,25 +494,19 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(add_arduino_global_workaround)
|
||||
|
||||
if config[CONF_INCLUDES]:
|
||||
system_includes, other_includes = _sort_includes_by_type(config[CONF_INCLUDES])
|
||||
# Get the <...> includes
|
||||
system_includes = []
|
||||
other_includes = []
|
||||
for include in config[CONF_INCLUDES]:
|
||||
if include.startswith("<") and include.endswith(">"):
|
||||
system_includes.append(include)
|
||||
else:
|
||||
other_includes.append(include)
|
||||
# <...> includes should be at the start
|
||||
for include in system_includes:
|
||||
cg.add_global(cg.RawStatement(f"#include {include}"), prepend=True)
|
||||
# Other includes should be at the end
|
||||
CORE.add_job(add_includes, other_includes, False)
|
||||
|
||||
if config[CONF_INCLUDES_C]:
|
||||
system_includes, other_includes = _sort_includes_by_type(
|
||||
config[CONF_INCLUDES_C]
|
||||
)
|
||||
# <...> includes should be at the start
|
||||
for include in system_includes:
|
||||
cg.add_global(
|
||||
cg.RawStatement(f'extern "C" {{\n #include {include}\n}}'),
|
||||
prepend=True,
|
||||
)
|
||||
# Other includes should be at the end
|
||||
CORE.add_job(add_includes, other_includes, True)
|
||||
CORE.add_job(add_includes, other_includes)
|
||||
|
||||
if project_conf := config.get(CONF_PROJECT):
|
||||
cg.add_define("ESPHOME_PROJECT_NAME", project_conf[CONF_NAME])
|
||||
|
||||
@@ -44,7 +44,6 @@
|
||||
#define USE_GRAPHICAL_DISPLAY_MENU
|
||||
#define USE_HOMEASSISTANT_TIME
|
||||
#define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT
|
||||
#define USE_IMPROV_SERIAL_NEXT_URL
|
||||
#define USE_JSON
|
||||
#define USE_LIGHT
|
||||
#define USE_LOCK
|
||||
@@ -187,7 +186,6 @@
|
||||
#define USE_ESP32_CAMERA_JPEG_ENCODER
|
||||
#define USE_I2C
|
||||
#define USE_IMPROV
|
||||
#define USE_ESP32_IMPROV_NEXT_URL
|
||||
#define USE_MICROPHONE
|
||||
#define USE_PSRAM
|
||||
#define USE_SOCKET_IMPL_BSD_SOCKETS
|
||||
|
||||
@@ -194,8 +194,12 @@ template<typename T> class FixedVector {
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
// Helper to assign from initializer list (shared by constructor and assignment operator)
|
||||
void assign_from_initializer_list_(std::initializer_list<T> init_list) {
|
||||
public:
|
||||
FixedVector() = default;
|
||||
|
||||
/// Constructor from initializer list - allocates exact size needed
|
||||
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||
FixedVector(std::initializer_list<T> init_list) {
|
||||
init(init_list.size());
|
||||
size_t idx = 0;
|
||||
for (const auto &item : init_list) {
|
||||
@@ -205,17 +209,9 @@ template<typename T> class FixedVector {
|
||||
size_ = init_list.size();
|
||||
}
|
||||
|
||||
public:
|
||||
FixedVector() = default;
|
||||
|
||||
/// Constructor from initializer list - allocates exact size needed
|
||||
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||
FixedVector(std::initializer_list<T> init_list) { assign_from_initializer_list_(init_list); }
|
||||
|
||||
~FixedVector() { cleanup_(); }
|
||||
|
||||
// Disable copy operations (avoid accidental expensive copies)
|
||||
// Use copy_from() for explicit copying when needed (e.g., copy_select)
|
||||
FixedVector(const FixedVector &) = delete;
|
||||
FixedVector &operator=(const FixedVector &) = delete;
|
||||
|
||||
@@ -238,28 +234,6 @@ template<typename T> class FixedVector {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Assignment from initializer list - avoids temporary and move overhead
|
||||
/// This enables: FixedVector<int> v; v = {1, 2, 3};
|
||||
FixedVector &operator=(std::initializer_list<T> init_list) {
|
||||
cleanup_();
|
||||
reset_();
|
||||
assign_from_initializer_list_(init_list);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Explicitly copy another FixedVector
|
||||
/// This method exists instead of operator= to make copying intentional and visible.
|
||||
/// Copying is expensive on embedded systems, so we require explicit opt-in.
|
||||
/// Use cases: copy_select (copying source options), lvgl (copying widget options)
|
||||
void copy_from(const FixedVector &other) {
|
||||
cleanup_();
|
||||
reset_();
|
||||
init(other.size());
|
||||
for (const auto &item : other) {
|
||||
push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate capacity - can be called multiple times to reinit
|
||||
void init(size_t n) {
|
||||
cleanup_();
|
||||
@@ -318,11 +292,6 @@ template<typename T> class FixedVector {
|
||||
return data_[size_ - 1];
|
||||
}
|
||||
|
||||
/// Access first element (no bounds checking - matches std::vector behavior)
|
||||
/// Caller must ensure vector is not empty (size() > 0)
|
||||
T &front() { return data_[0]; }
|
||||
const T &front() const { return data_[0]; }
|
||||
|
||||
/// Access last element (no bounds checking - matches std::vector behavior)
|
||||
/// Caller must ensure vector is not empty (size() > 0)
|
||||
T &back() { return data_[size_ - 1]; }
|
||||
@@ -336,12 +305,6 @@ template<typename T> class FixedVector {
|
||||
T &operator[](size_t i) { return data_[i]; }
|
||||
const T &operator[](size_t i) const { return data_[i]; }
|
||||
|
||||
/// Access element with bounds checking (matches std::vector behavior)
|
||||
/// Returns reference to element at index i
|
||||
/// Behavior for out of bounds access matches std::vector::at() (undefined on embedded)
|
||||
T &at(size_t i) { return data_[i]; }
|
||||
const T &at(size_t i) const { return data_[i]; }
|
||||
|
||||
// Iterator support for range-based for loops
|
||||
T *begin() { return data_; }
|
||||
T *end() { return data_ + size_; }
|
||||
|
||||
@@ -43,6 +43,7 @@ from enum import StrEnum
|
||||
from functools import cache
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
@@ -52,13 +53,10 @@ from helpers import (
|
||||
CPP_FILE_EXTENSIONS,
|
||||
PYTHON_FILE_EXTENSIONS,
|
||||
changed_files,
|
||||
filter_component_files,
|
||||
get_all_dependencies,
|
||||
get_changed_components,
|
||||
get_component_from_path,
|
||||
get_component_test_files,
|
||||
get_components_from_integration_fixtures,
|
||||
get_components_with_dependencies,
|
||||
git_ls_files,
|
||||
parse_test_filename,
|
||||
root_path,
|
||||
@@ -563,29 +561,16 @@ def main() -> None:
|
||||
run_python_linters = should_run_python_linters(args.branch)
|
||||
changed_cpp_file_count = count_changed_cpp_files(args.branch)
|
||||
|
||||
# Get changed components
|
||||
# get_changed_components() returns:
|
||||
# None: Core files changed (need full scan)
|
||||
# []: No components changed
|
||||
# [list]: Changed components (already includes dependencies)
|
||||
changed_components_result = get_changed_components()
|
||||
# Get both directly changed and all changed components (with dependencies) in one call
|
||||
script_path = Path(__file__).parent / "list-components.py"
|
||||
cmd = [sys.executable, str(script_path), "--changed-with-deps"]
|
||||
if args.branch:
|
||||
cmd.extend(["-b", args.branch])
|
||||
|
||||
if changed_components_result is None:
|
||||
# Core files changed - will trigger full clang-tidy scan
|
||||
# No specific components to test
|
||||
changed_components = []
|
||||
directly_changed_components = []
|
||||
is_core_change = True
|
||||
else:
|
||||
# Get both directly changed and all changed (with dependencies)
|
||||
changed = changed_files(args.branch)
|
||||
component_files = [f for f in changed if filter_component_files(f)]
|
||||
|
||||
directly_changed_components = get_components_with_dependencies(
|
||||
component_files, False
|
||||
)
|
||||
changed_components = get_components_with_dependencies(component_files, True)
|
||||
is_core_change = False
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
component_data = json.loads(result.stdout)
|
||||
directly_changed_components = component_data["directly_changed"]
|
||||
changed_components = component_data["all_changed"]
|
||||
|
||||
# Filter to only components that have test files
|
||||
# Components without tests shouldn't generate CI test jobs
|
||||
@@ -596,11 +581,11 @@ def main() -> None:
|
||||
# Get directly changed components with tests (for isolated testing)
|
||||
# These will be tested WITHOUT --testing-mode in CI to enable full validation
|
||||
# (pin conflicts, etc.) since they contain the actual changes being reviewed
|
||||
directly_changed_with_tests = {
|
||||
directly_changed_with_tests = [
|
||||
component
|
||||
for component in directly_changed_components
|
||||
if _component_has_tests(component)
|
||||
}
|
||||
]
|
||||
|
||||
# Get dependency-only components (for grouped testing)
|
||||
dependency_only_components = [
|
||||
@@ -614,8 +599,7 @@ def main() -> None:
|
||||
|
||||
# Determine clang-tidy mode based on actual files that will be checked
|
||||
if run_clang_tidy:
|
||||
# Full scan needed if: hash changed OR core files changed
|
||||
is_full_scan = _is_clang_tidy_full_scan() or is_core_change
|
||||
is_full_scan = _is_clang_tidy_full_scan()
|
||||
|
||||
if is_full_scan:
|
||||
# Full scan checks all files - always use split mode for efficiency
|
||||
@@ -654,7 +638,7 @@ def main() -> None:
|
||||
"python_linters": run_python_linters,
|
||||
"changed_components": changed_components,
|
||||
"changed_components_with_tests": changed_components_with_tests,
|
||||
"directly_changed_components_with_tests": list(directly_changed_with_tests),
|
||||
"directly_changed_components_with_tests": directly_changed_with_tests,
|
||||
"dependency_only_components_with_tests": dependency_only_components,
|
||||
"component_test_count": len(changed_components_with_tests),
|
||||
"directly_changed_count": len(directly_changed_with_tests),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from functools import cache
|
||||
import json
|
||||
import os
|
||||
@@ -8,7 +7,6 @@ import os.path
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
@@ -306,10 +304,7 @@ def get_changed_components() -> list[str] | None:
|
||||
for f in changed
|
||||
)
|
||||
if core_cpp_changed:
|
||||
print(
|
||||
"Core C++/header files changed - will run full clang-tidy scan",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Core C++/header files changed - will run full clang-tidy scan")
|
||||
return None
|
||||
|
||||
# Use list-components.py to get changed components
|
||||
@@ -323,10 +318,7 @@ def get_changed_components() -> list[str] | None:
|
||||
return parse_list_components_output(result.stdout)
|
||||
except subprocess.CalledProcessError:
|
||||
# If the script fails, fall back to full scan
|
||||
print(
|
||||
"Could not determine changed components - will run full clang-tidy scan",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("Could not determine changed components - will run full clang-tidy scan")
|
||||
return None
|
||||
|
||||
|
||||
@@ -378,14 +370,14 @@ def _filter_changed_ci(files: list[str]) -> list[str]:
|
||||
if f in changed and not f.startswith(ESPHOME_COMPONENTS_PATH)
|
||||
]
|
||||
if not files:
|
||||
print("No files changed", file=sys.stderr)
|
||||
print("No files changed")
|
||||
return files
|
||||
|
||||
# Scenario 3: Specific components changed
|
||||
# Action: Check ALL files in each changed component
|
||||
# Convert component list to set for O(1) lookups
|
||||
component_set = set(components)
|
||||
print(f"Changed components: {', '.join(sorted(components))}", file=sys.stderr)
|
||||
print(f"Changed components: {', '.join(sorted(components))}")
|
||||
|
||||
# The 'files' parameter contains ALL files in the codebase that clang-tidy would check.
|
||||
# We filter this down to only files in the changed components.
|
||||
@@ -656,220 +648,3 @@ def get_components_from_integration_fixtures() -> set[str]:
|
||||
components.add(item["platform"])
|
||||
|
||||
return components
|
||||
|
||||
|
||||
def filter_component_files(file_path: str) -> bool:
|
||||
"""Check if a file path is a component file.
|
||||
|
||||
Args:
|
||||
file_path: Path to check
|
||||
|
||||
Returns:
|
||||
True if the file is in a component directory
|
||||
"""
|
||||
return file_path.startswith("esphome/components/") or file_path.startswith(
|
||||
"tests/components/"
|
||||
)
|
||||
|
||||
|
||||
def extract_component_names_from_files(files: list[str]) -> list[str]:
|
||||
"""Extract unique component names from a list of file paths.
|
||||
|
||||
Args:
|
||||
files: List of file paths
|
||||
|
||||
Returns:
|
||||
List of unique component names (preserves order)
|
||||
"""
|
||||
return list(
|
||||
dict.fromkeys(comp for file in files if (comp := get_component_from_path(file)))
|
||||
)
|
||||
|
||||
|
||||
def add_item_to_components_graph(
|
||||
components_graph: dict[str, list[str]], parent: str, child: str
|
||||
) -> None:
|
||||
"""Add a dependency relationship to the components graph.
|
||||
|
||||
Args:
|
||||
components_graph: Graph mapping parent components to their children
|
||||
parent: Parent component name
|
||||
child: Child component name (dependent)
|
||||
"""
|
||||
if not parent.startswith("__") and parent != child:
|
||||
if parent not in components_graph:
|
||||
components_graph[parent] = []
|
||||
if child not in components_graph[parent]:
|
||||
components_graph[parent].append(child)
|
||||
|
||||
|
||||
def resolve_auto_load(
|
||||
auto_load: list[str] | Callable[[], list[str]] | Callable[[dict | None], list[str]],
|
||||
config: dict | None = None,
|
||||
) -> list[str]:
|
||||
"""Resolve AUTO_LOAD to a list, handling callables with or without config parameter.
|
||||
|
||||
Args:
|
||||
auto_load: The AUTO_LOAD value (list or callable)
|
||||
config: Optional config to pass to callable AUTO_LOAD functions
|
||||
|
||||
Returns:
|
||||
List of component names to auto-load
|
||||
"""
|
||||
if not callable(auto_load):
|
||||
return auto_load
|
||||
|
||||
import inspect
|
||||
|
||||
if inspect.signature(auto_load).parameters:
|
||||
return auto_load(config)
|
||||
return auto_load()
|
||||
|
||||
|
||||
def create_components_graph() -> dict[str, list[str]]:
|
||||
"""Create a graph of component dependencies.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping parent components to their children (dependencies)
|
||||
"""
|
||||
from pathlib import Path
|
||||
|
||||
from esphome import const
|
||||
from esphome.core import CORE
|
||||
from esphome.loader import ComponentManifest, get_component, get_platform
|
||||
|
||||
# The root directory of the repo
|
||||
root = Path(__file__).parent.parent
|
||||
components_dir = root / "esphome" / "components"
|
||||
# Fake some directory so that get_component works
|
||||
CORE.config_path = root
|
||||
# Various configuration to capture different outcomes used by `AUTO_LOAD` function.
|
||||
KEY_CORE = const.KEY_CORE
|
||||
KEY_TARGET_FRAMEWORK = const.KEY_TARGET_FRAMEWORK
|
||||
KEY_TARGET_PLATFORM = const.KEY_TARGET_PLATFORM
|
||||
PLATFORM_ESP32 = const.PLATFORM_ESP32
|
||||
PLATFORM_ESP8266 = const.PLATFORM_ESP8266
|
||||
|
||||
TARGET_CONFIGURATIONS = [
|
||||
{KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None},
|
||||
{KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None},
|
||||
{KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None},
|
||||
{KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32},
|
||||
{KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP8266},
|
||||
]
|
||||
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
|
||||
|
||||
components_graph = {}
|
||||
platforms = []
|
||||
components: list[tuple[ComponentManifest, str, Path]] = []
|
||||
|
||||
for path in components_dir.iterdir():
|
||||
if not path.is_dir():
|
||||
continue
|
||||
if not (path / "__init__.py").is_file():
|
||||
continue
|
||||
name = path.name
|
||||
comp = get_component(name)
|
||||
if comp is None:
|
||||
raise RuntimeError(
|
||||
f"Cannot find component {name}. Make sure current path is pip installed ESPHome"
|
||||
)
|
||||
|
||||
components.append((comp, name, path))
|
||||
if comp.is_platform_component:
|
||||
platforms.append(name)
|
||||
|
||||
platforms = set(platforms)
|
||||
|
||||
for comp, name, path in components:
|
||||
for dependency in comp.dependencies:
|
||||
add_item_to_components_graph(
|
||||
components_graph, dependency.split(".")[0], name
|
||||
)
|
||||
|
||||
for target_config in TARGET_CONFIGURATIONS:
|
||||
CORE.data[KEY_CORE] = target_config
|
||||
for item in resolve_auto_load(comp.auto_load, config=None):
|
||||
add_item_to_components_graph(components_graph, item, name)
|
||||
# restore config
|
||||
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
|
||||
|
||||
for platform_path in path.iterdir():
|
||||
platform_name = platform_path.stem
|
||||
if platform_name == name or platform_name not in platforms:
|
||||
continue
|
||||
platform = get_platform(platform_name, name)
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
add_item_to_components_graph(components_graph, platform_name, name)
|
||||
|
||||
for dependency in platform.dependencies:
|
||||
add_item_to_components_graph(
|
||||
components_graph, dependency.split(".")[0], name
|
||||
)
|
||||
|
||||
for target_config in TARGET_CONFIGURATIONS:
|
||||
CORE.data[KEY_CORE] = target_config
|
||||
for item in resolve_auto_load(platform.auto_load, config={}):
|
||||
add_item_to_components_graph(components_graph, item, name)
|
||||
# restore config
|
||||
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
|
||||
|
||||
return components_graph
|
||||
|
||||
|
||||
def find_children_of_component(
|
||||
components_graph: dict[str, list[str]], component_name: str, depth: int = 0
|
||||
) -> list[str]:
|
||||
"""Find all components that depend on the given component (recursively).
|
||||
|
||||
Args:
|
||||
components_graph: Graph mapping parent components to their children
|
||||
component_name: Component name to find children for
|
||||
depth: Current recursion depth (max 10)
|
||||
|
||||
Returns:
|
||||
List of all dependent component names (may contain duplicates removed at end)
|
||||
"""
|
||||
if component_name not in components_graph:
|
||||
return []
|
||||
|
||||
children = []
|
||||
|
||||
for child in components_graph[component_name]:
|
||||
children.append(child)
|
||||
if depth < 10:
|
||||
children.extend(
|
||||
find_children_of_component(components_graph, child, depth + 1)
|
||||
)
|
||||
# Remove duplicate values
|
||||
return list(set(children))
|
||||
|
||||
|
||||
def get_components_with_dependencies(
|
||||
files: list[str], get_dependencies: bool = False
|
||||
) -> list[str]:
|
||||
"""Get component names from files, optionally including their dependencies.
|
||||
|
||||
Args:
|
||||
files: List of file paths
|
||||
get_dependencies: If True, include all dependent components
|
||||
|
||||
Returns:
|
||||
Sorted list of component names
|
||||
"""
|
||||
components = extract_component_names_from_files(files)
|
||||
|
||||
if get_dependencies:
|
||||
components_graph = create_components_graph()
|
||||
|
||||
all_components = components.copy()
|
||||
for c in components:
|
||||
all_components.extend(find_children_of_component(components_graph, c))
|
||||
# Remove duplicate values
|
||||
all_changed_components = list(set(all_components))
|
||||
|
||||
return sorted(all_changed_components)
|
||||
|
||||
return sorted(components)
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
from helpers import (
|
||||
changed_files,
|
||||
filter_component_files,
|
||||
get_components_with_dependencies,
|
||||
git_ls_files,
|
||||
from helpers import changed_files, get_component_from_path, git_ls_files
|
||||
|
||||
from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.loader import ComponentManifest, get_component, get_platform
|
||||
|
||||
|
||||
def filter_component_files(str):
|
||||
return str.startswith("esphome/components/") | str.startswith("tests/components/")
|
||||
|
||||
|
||||
def get_all_component_files() -> list[str]:
|
||||
@@ -15,6 +27,156 @@ def get_all_component_files() -> list[str]:
|
||||
return list(filter(filter_component_files, files))
|
||||
|
||||
|
||||
def extract_component_names_array_from_files_array(files):
|
||||
components = []
|
||||
for file in files:
|
||||
component_name = get_component_from_path(file)
|
||||
if component_name and component_name not in components:
|
||||
components.append(component_name)
|
||||
return components
|
||||
|
||||
|
||||
def add_item_to_components_graph(components_graph, parent, child):
|
||||
if not parent.startswith("__") and parent != child:
|
||||
if parent not in components_graph:
|
||||
components_graph[parent] = []
|
||||
if child not in components_graph[parent]:
|
||||
components_graph[parent].append(child)
|
||||
|
||||
|
||||
def resolve_auto_load(
|
||||
auto_load: list[str] | Callable[[], list[str]] | Callable[[dict | None], list[str]],
|
||||
config: dict | None = None,
|
||||
) -> list[str]:
|
||||
"""Resolve AUTO_LOAD to a list, handling callables with or without config parameter.
|
||||
|
||||
Args:
|
||||
auto_load: The AUTO_LOAD value (list or callable)
|
||||
config: Optional config to pass to callable AUTO_LOAD functions
|
||||
|
||||
Returns:
|
||||
List of component names to auto-load
|
||||
"""
|
||||
if not callable(auto_load):
|
||||
return auto_load
|
||||
|
||||
import inspect
|
||||
|
||||
if inspect.signature(auto_load).parameters:
|
||||
return auto_load(config)
|
||||
return auto_load()
|
||||
|
||||
|
||||
def create_components_graph():
|
||||
# The root directory of the repo
|
||||
root = Path(__file__).parent.parent
|
||||
components_dir = root / "esphome" / "components"
|
||||
# Fake some directory so that get_component works
|
||||
CORE.config_path = root
|
||||
# Various configuration to capture different outcomes used by `AUTO_LOAD` function.
|
||||
TARGET_CONFIGURATIONS = [
|
||||
{KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: None},
|
||||
{KEY_TARGET_FRAMEWORK: "arduino", KEY_TARGET_PLATFORM: None},
|
||||
{KEY_TARGET_FRAMEWORK: "esp-idf", KEY_TARGET_PLATFORM: None},
|
||||
{KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP32},
|
||||
{KEY_TARGET_FRAMEWORK: None, KEY_TARGET_PLATFORM: PLATFORM_ESP8266},
|
||||
]
|
||||
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
|
||||
|
||||
components_graph = {}
|
||||
platforms = []
|
||||
components: list[tuple[ComponentManifest, str, Path]] = []
|
||||
|
||||
for path in components_dir.iterdir():
|
||||
if not path.is_dir():
|
||||
continue
|
||||
if not (path / "__init__.py").is_file():
|
||||
continue
|
||||
name = path.name
|
||||
comp = get_component(name)
|
||||
if comp is None:
|
||||
print(
|
||||
f"Cannot find component {name}. Make sure current path is pip installed ESPHome"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
components.append((comp, name, path))
|
||||
if comp.is_platform_component:
|
||||
platforms.append(name)
|
||||
|
||||
platforms = set(platforms)
|
||||
|
||||
for comp, name, path in components:
|
||||
for dependency in comp.dependencies:
|
||||
add_item_to_components_graph(
|
||||
components_graph, dependency.split(".")[0], name
|
||||
)
|
||||
|
||||
for target_config in TARGET_CONFIGURATIONS:
|
||||
CORE.data[KEY_CORE] = target_config
|
||||
for item in resolve_auto_load(comp.auto_load, config=None):
|
||||
add_item_to_components_graph(components_graph, item, name)
|
||||
# restore config
|
||||
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
|
||||
|
||||
for platform_path in path.iterdir():
|
||||
platform_name = platform_path.stem
|
||||
if platform_name == name or platform_name not in platforms:
|
||||
continue
|
||||
platform = get_platform(platform_name, name)
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
add_item_to_components_graph(components_graph, platform_name, name)
|
||||
|
||||
for dependency in platform.dependencies:
|
||||
add_item_to_components_graph(
|
||||
components_graph, dependency.split(".")[0], name
|
||||
)
|
||||
|
||||
for target_config in TARGET_CONFIGURATIONS:
|
||||
CORE.data[KEY_CORE] = target_config
|
||||
for item in resolve_auto_load(platform.auto_load, config={}):
|
||||
add_item_to_components_graph(components_graph, item, name)
|
||||
# restore config
|
||||
CORE.data[KEY_CORE] = TARGET_CONFIGURATIONS[0]
|
||||
|
||||
return components_graph
|
||||
|
||||
|
||||
def find_children_of_component(components_graph, component_name, depth=0):
|
||||
if component_name not in components_graph:
|
||||
return []
|
||||
|
||||
children = []
|
||||
|
||||
for child in components_graph[component_name]:
|
||||
children.append(child)
|
||||
if depth < 10:
|
||||
children.extend(
|
||||
find_children_of_component(components_graph, child, depth + 1)
|
||||
)
|
||||
# Remove duplicate values
|
||||
return list(set(children))
|
||||
|
||||
|
||||
def get_components(files: list[str], get_dependencies: bool = False):
|
||||
components = extract_component_names_array_from_files_array(files)
|
||||
|
||||
if get_dependencies:
|
||||
components_graph = create_components_graph()
|
||||
|
||||
all_components = components.copy()
|
||||
for c in components:
|
||||
all_components.extend(find_children_of_component(components_graph, c))
|
||||
# Remove duplicate values
|
||||
all_changed_components = list(set(all_components))
|
||||
|
||||
return sorted(all_changed_components)
|
||||
|
||||
return sorted(components)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
@@ -89,8 +251,8 @@ def main():
|
||||
# Return JSON with both directly changed and all changed components
|
||||
import json
|
||||
|
||||
directly_changed = get_components_with_dependencies(files, False)
|
||||
all_changed = get_components_with_dependencies(files, True)
|
||||
directly_changed = get_components(files, False)
|
||||
all_changed = get_components(files, True)
|
||||
output = {
|
||||
"directly_changed": directly_changed,
|
||||
"all_changed": all_changed,
|
||||
@@ -98,11 +260,11 @@ def main():
|
||||
print(json.dumps(output))
|
||||
elif args.changed_direct:
|
||||
# Return only directly changed components (without dependencies)
|
||||
for c in get_components_with_dependencies(files, False):
|
||||
for c in get_components(files, False):
|
||||
print(c)
|
||||
else:
|
||||
# Return all changed components (with dependencies) - default behavior
|
||||
for c in get_components_with_dependencies(files, args.changed):
|
||||
for c in get_components(files, args.changed):
|
||||
print(c)
|
||||
|
||||
|
||||
|
||||
@@ -966,33 +966,11 @@ def test_components(
|
||||
# Find all component tests
|
||||
all_tests = {}
|
||||
for pattern in component_patterns:
|
||||
# Skip empty patterns (happens when components list is empty string)
|
||||
if not pattern:
|
||||
continue
|
||||
all_tests.update(find_component_tests(tests_dir, pattern, base_only))
|
||||
|
||||
# If no components found, build a reference configuration for baseline comparison
|
||||
# Create a synthetic "empty" component test that will build just the base config
|
||||
if not all_tests:
|
||||
print(f"No components found matching: {component_patterns}")
|
||||
print(
|
||||
"Building reference configuration with no components for baseline comparison..."
|
||||
)
|
||||
|
||||
# Create empty test files for each platform (or filtered platform)
|
||||
reference_tests: list[Path] = []
|
||||
for platform_name, base_file in platform_bases.items():
|
||||
if platform_filter and not platform_name.startswith(platform_filter):
|
||||
continue
|
||||
# Create an empty test file named to match the platform
|
||||
empty_test_file = build_dir / f"reference.{platform_name}.yaml"
|
||||
empty_test_file.write_text(
|
||||
"# Empty component test for baseline reference\n"
|
||||
)
|
||||
reference_tests.append(empty_test_file)
|
||||
|
||||
# Add to all_tests dict with component name "reference"
|
||||
all_tests["reference"] = reference_tests
|
||||
return 1
|
||||
|
||||
print(f"Found {len(all_tests)} components to test")
|
||||
|
||||
|
||||
@@ -37,36 +37,3 @@ binary_sensor:
|
||||
format: "New state is %s"
|
||||
args: ['x.has_value() ? ONOFF(x) : "Unknown"']
|
||||
- binary_sensor.invalidate_state: some_binary_sensor
|
||||
|
||||
# Test autorepeat with default configuration (no timings)
|
||||
- platform: template
|
||||
id: autorepeat_default
|
||||
name: "Autorepeat Default"
|
||||
filters:
|
||||
- autorepeat:
|
||||
|
||||
# Test autorepeat with single timing entry
|
||||
- platform: template
|
||||
id: autorepeat_single
|
||||
name: "Autorepeat Single"
|
||||
filters:
|
||||
- autorepeat:
|
||||
- delay: 2s
|
||||
time_off: 200ms
|
||||
time_on: 800ms
|
||||
|
||||
# Test autorepeat with three timing entries
|
||||
- platform: template
|
||||
id: autorepeat_multiple
|
||||
name: "Autorepeat Multiple"
|
||||
filters:
|
||||
- autorepeat:
|
||||
- delay: 500ms
|
||||
time_off: 50ms
|
||||
time_on: 950ms
|
||||
- delay: 2s
|
||||
time_off: 100ms
|
||||
time_on: 900ms
|
||||
- delay: 10s
|
||||
time_off: 200ms
|
||||
time_on: 800ms
|
||||
|
||||
@@ -173,66 +173,3 @@ sensor:
|
||||
timeout: 1000ms
|
||||
value: [42.0]
|
||||
- multiply: 2.0
|
||||
|
||||
# CalibrateLinearFilter - piecewise linear calibration
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Linear Two Points"
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 0.0 -> 0.0
|
||||
- 100.0 -> 100.0
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Linear Multiple Segments"
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
- 0.0 -> 0.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Linear Least Squares"
|
||||
filters:
|
||||
- calibrate_linear:
|
||||
method: least_squares
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
# CalibratePolynomialFilter - polynomial calibration
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Polynomial Degree 2"
|
||||
filters:
|
||||
- calibrate_polynomial:
|
||||
degree: 2
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Calibrate Polynomial Degree 3"
|
||||
filters:
|
||||
- calibrate_polynomial:
|
||||
degree: 3
|
||||
datapoints:
|
||||
- 0.0 -> 0.0
|
||||
- 25.0 -> 26.0
|
||||
- 50.0 -> 55.0
|
||||
- 100.0 -> 102.5
|
||||
|
||||
# OrFilter - filter branching
|
||||
- platform: copy
|
||||
source_id: source_sensor
|
||||
name: "Or Filter with Multiple Branches"
|
||||
filters:
|
||||
- or:
|
||||
- multiply: 2.0
|
||||
- offset: 10.0
|
||||
- lambda: return x * 3.0;
|
||||
|
||||
@@ -101,9 +101,6 @@ sensor:
|
||||
- filter_out: 10
|
||||
- filter_out: !lambda return NAN;
|
||||
- heartbeat: 5s
|
||||
- heartbeat:
|
||||
period: 5s
|
||||
optimistic: true
|
||||
- lambda: return x * (9.0/5.0) + 32.0;
|
||||
- max:
|
||||
window_size: 10
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "Test Substitute Single"
|
||||
id: test_substitute_single
|
||||
filters:
|
||||
- substitute:
|
||||
- ERROR -> Error
|
||||
|
||||
- platform: template
|
||||
name: "Test Substitute Multiple"
|
||||
id: test_substitute_multiple
|
||||
filters:
|
||||
- substitute:
|
||||
- ERROR -> Error
|
||||
- WARN -> Warning
|
||||
- INFO -> Information
|
||||
- DEBUG -> Debug
|
||||
|
||||
- platform: template
|
||||
name: "Test Substitute Chained"
|
||||
id: test_substitute_chained
|
||||
filters:
|
||||
- substitute:
|
||||
- foo -> bar
|
||||
- to_upper
|
||||
- substitute:
|
||||
- BAR -> baz
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Single"
|
||||
id: test_map_single
|
||||
filters:
|
||||
- map:
|
||||
- ON -> Active
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Multiple"
|
||||
id: test_map_multiple
|
||||
filters:
|
||||
- map:
|
||||
- ON -> Active
|
||||
- OFF -> Inactive
|
||||
- UNKNOWN -> Error
|
||||
- IDLE -> Standby
|
||||
|
||||
- platform: template
|
||||
name: "Test Map Passthrough"
|
||||
id: test_map_passthrough
|
||||
filters:
|
||||
- map:
|
||||
- Good -> Excellent
|
||||
- Bad -> Poor
|
||||
|
||||
- platform: template
|
||||
name: "Test All Filters"
|
||||
id: test_all_filters
|
||||
filters:
|
||||
- to_upper
|
||||
- to_lower
|
||||
- append: " suffix"
|
||||
- prepend: "prefix "
|
||||
- substitute:
|
||||
- prefix -> PREFIX
|
||||
- suffix -> SUFFIX
|
||||
- map:
|
||||
- PREFIX text SUFFIX -> mapped
|
||||
@@ -1 +0,0 @@
|
||||
<<: !include common.yaml
|
||||
@@ -12,8 +12,5 @@ esphome:
|
||||
- logger.log: "Failed to connect to WiFi!"
|
||||
|
||||
wifi:
|
||||
networks:
|
||||
- ssid: MySSID
|
||||
password: password1
|
||||
- ssid: MySSID2
|
||||
password: password2
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
esphome:
|
||||
name: host-climate-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
id: dual_mode_thermostat
|
||||
name: Dual-mode Thermostat
|
||||
sensor: host_thermostat_temperature_sensor
|
||||
humidity_sensor: host_thermostat_humidity_sensor
|
||||
humidity_hysteresis: 1.0
|
||||
min_cooling_off_time: 20s
|
||||
min_cooling_run_time: 20s
|
||||
max_cooling_run_time: 30s
|
||||
supplemental_cooling_delta: 3.0
|
||||
min_heating_off_time: 20s
|
||||
min_heating_run_time: 20s
|
||||
max_heating_run_time: 30s
|
||||
supplemental_heating_delta: 3.0
|
||||
min_fanning_off_time: 20s
|
||||
min_fanning_run_time: 20s
|
||||
min_idle_time: 10s
|
||||
visual:
|
||||
min_humidity: 20%
|
||||
max_humidity: 70%
|
||||
min_temperature: 15.0
|
||||
max_temperature: 32.0
|
||||
temperature_step: 0.1
|
||||
default_preset: home
|
||||
preset:
|
||||
- name: "away"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
- name: "home"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
auto_mode:
|
||||
- logger.log: "AUTO mode set"
|
||||
heat_cool_mode:
|
||||
- logger.log: "HEAT_COOL mode set"
|
||||
cool_action:
|
||||
- switch.turn_on: air_cond
|
||||
supplemental_cooling_action:
|
||||
- switch.turn_on: air_cond_2
|
||||
heat_action:
|
||||
- switch.turn_on: heater
|
||||
supplemental_heating_action:
|
||||
- switch.turn_on: heater_2
|
||||
dry_action:
|
||||
- switch.turn_on: air_cond
|
||||
fan_only_action:
|
||||
- switch.turn_on: fan_only
|
||||
idle_action:
|
||||
- switch.turn_off: air_cond
|
||||
- switch.turn_off: air_cond_2
|
||||
- switch.turn_off: heater
|
||||
- switch.turn_off: heater_2
|
||||
- switch.turn_off: fan_only
|
||||
humidity_control_humidify_action:
|
||||
- switch.turn_on: humidifier
|
||||
humidity_control_off_action:
|
||||
- switch.turn_off: humidifier
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: host_thermostat_humidity_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s
|
||||
- platform: template
|
||||
id: host_thermostat_temperature_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 22.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
id: air_cond
|
||||
name: Air Conditioner
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: air_cond_2
|
||||
name: Air Conditioner 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: fan_only
|
||||
name: Fan
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater
|
||||
name: Heater
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater_2
|
||||
name: Heater 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: dehumidifier
|
||||
name: Dehumidifier
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: humidifier
|
||||
name: Humidifier
|
||||
optimistic: true
|
||||
@@ -1,108 +0,0 @@
|
||||
esphome:
|
||||
name: host-climate-test
|
||||
host:
|
||||
api:
|
||||
logger:
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
id: dual_mode_thermostat
|
||||
name: Dual-mode Thermostat
|
||||
sensor: host_thermostat_temperature_sensor
|
||||
humidity_sensor: host_thermostat_humidity_sensor
|
||||
humidity_hysteresis: 1.0
|
||||
min_cooling_off_time: 20s
|
||||
min_cooling_run_time: 20s
|
||||
max_cooling_run_time: 30s
|
||||
supplemental_cooling_delta: 3.0
|
||||
min_heating_off_time: 20s
|
||||
min_heating_run_time: 20s
|
||||
max_heating_run_time: 30s
|
||||
supplemental_heating_delta: 3.0
|
||||
min_fanning_off_time: 20s
|
||||
min_fanning_run_time: 20s
|
||||
min_idle_time: 10s
|
||||
visual:
|
||||
min_humidity: 20%
|
||||
max_humidity: 70%
|
||||
min_temperature: 15.0
|
||||
max_temperature: 32.0
|
||||
temperature_step: 0.1
|
||||
default_preset: home
|
||||
preset:
|
||||
- name: "away"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
- name: "home"
|
||||
default_target_temperature_low: 18.0
|
||||
default_target_temperature_high: 24.0
|
||||
auto_mode:
|
||||
- logger.log: "AUTO mode set"
|
||||
heat_cool_mode:
|
||||
- logger.log: "HEAT_COOL mode set"
|
||||
cool_action:
|
||||
- switch.turn_on: air_cond
|
||||
supplemental_cooling_action:
|
||||
- switch.turn_on: air_cond_2
|
||||
heat_action:
|
||||
- switch.turn_on: heater
|
||||
supplemental_heating_action:
|
||||
- switch.turn_on: heater_2
|
||||
dry_action:
|
||||
- switch.turn_on: air_cond
|
||||
fan_only_action:
|
||||
- switch.turn_on: fan_only
|
||||
idle_action:
|
||||
- switch.turn_off: air_cond
|
||||
- switch.turn_off: air_cond_2
|
||||
- switch.turn_off: heater
|
||||
- switch.turn_off: heater_2
|
||||
- switch.turn_off: fan_only
|
||||
humidity_control_humidify_action:
|
||||
- switch.turn_on: humidifier
|
||||
humidity_control_off_action:
|
||||
- switch.turn_off: humidifier
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: host_thermostat_humidity_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s
|
||||
- platform: template
|
||||
id: host_thermostat_temperature_sensor
|
||||
unit_of_measurement: °C
|
||||
accuracy_decimals: 2
|
||||
state_class: measurement
|
||||
force_update: true
|
||||
lambda: return 22.0;
|
||||
update_interval: 0.1s
|
||||
|
||||
switch:
|
||||
- platform: template
|
||||
id: air_cond
|
||||
name: Air Conditioner
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: air_cond_2
|
||||
name: Air Conditioner 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: fan_only
|
||||
name: Fan
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater
|
||||
name: Heater
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: heater_2
|
||||
name: Heater 2
|
||||
optimistic: true
|
||||
- platform: template
|
||||
id: humidifier
|
||||
name: Humidifier
|
||||
optimistic: true
|
||||
@@ -210,15 +210,7 @@ sensor:
|
||||
name: "Test Sensor 50"
|
||||
lambda: return 50.0;
|
||||
update_interval: 0.1s
|
||||
# Sensors for the thermostat
|
||||
- platform: template
|
||||
name: "Humidity Sensor"
|
||||
id: humidity_sensor
|
||||
lambda: return 35.0;
|
||||
unit_of_measurement: "%"
|
||||
device_class: humidity
|
||||
state_class: measurement
|
||||
update_interval: 5s
|
||||
# Temperature sensor for the thermostat
|
||||
- platform: template
|
||||
name: "Temperature Sensor"
|
||||
id: temp_sensor
|
||||
@@ -303,11 +295,6 @@ valve:
|
||||
- logger.log: "Valve stopping"
|
||||
|
||||
output:
|
||||
- platform: template
|
||||
id: humidifier_output
|
||||
type: binary
|
||||
write_action:
|
||||
- logger.log: "Humidifier output changed"
|
||||
- platform: template
|
||||
id: heater_output
|
||||
type: binary
|
||||
@@ -318,31 +305,18 @@ output:
|
||||
type: binary
|
||||
write_action:
|
||||
- logger.log: "Cooler output changed"
|
||||
- platform: template
|
||||
id: fan_output
|
||||
type: binary
|
||||
write_action:
|
||||
- logger.log: "Fan output changed"
|
||||
|
||||
climate:
|
||||
- platform: thermostat
|
||||
name: "Test Thermostat"
|
||||
sensor: temp_sensor
|
||||
humidity_sensor: humidity_sensor
|
||||
default_preset: Home
|
||||
on_boot_restore_from: default_preset
|
||||
min_heating_off_time: 1s
|
||||
min_heating_run_time: 1s
|
||||
min_cooling_off_time: 1s
|
||||
min_cooling_run_time: 1s
|
||||
min_fan_mode_switching_time: 1s
|
||||
min_idle_time: 1s
|
||||
visual:
|
||||
min_humidity: 20%
|
||||
max_humidity: 70%
|
||||
min_temperature: 15.0
|
||||
max_temperature: 32.0
|
||||
temperature_step: 0.1
|
||||
heat_action:
|
||||
- output.turn_on: heater_output
|
||||
cool_action:
|
||||
@@ -350,14 +324,6 @@ climate:
|
||||
idle_action:
|
||||
- output.turn_off: heater_output
|
||||
- output.turn_off: cooler_output
|
||||
humidity_control_humidify_action:
|
||||
- output.turn_on: humidifier_output
|
||||
humidity_control_off_action:
|
||||
- output.turn_off: humidifier_output
|
||||
fan_mode_auto_action:
|
||||
- output.turn_off: fan_output
|
||||
fan_mode_on_action:
|
||||
- output.turn_on: fan_output
|
||||
preset:
|
||||
- name: Home
|
||||
default_target_temperature_low: 20
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
"""Integration test for Host mode with climate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_mode_climate_basic_state(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test basic climate state reporting."""
|
||||
loop = asyncio.get_running_loop()
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
states: dict[int, EntityState] = {}
|
||||
climate_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and not climate_future.done()
|
||||
):
|
||||
climate_future.set_result(state)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
try:
|
||||
climate_state = await asyncio.wait_for(climate_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Climate state not received within 5 seconds")
|
||||
|
||||
assert isinstance(climate_state, aioesphomeapi.ClimateState)
|
||||
assert climate_state.mode == ClimateMode.OFF
|
||||
assert climate_state.action == ClimateAction.OFF
|
||||
assert climate_state.current_temperature == 22.0
|
||||
assert climate_state.target_temperature_low == 18.0
|
||||
assert climate_state.target_temperature_high == 24.0
|
||||
assert climate_state.preset == ClimatePreset.HOME
|
||||
assert climate_state.current_humidity == 42.0
|
||||
assert climate_state.target_humidity == 20.0
|
||||
@@ -1,76 +0,0 @@
|
||||
"""Integration test for Host mode with climate."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import aioesphomeapi
|
||||
from aioesphomeapi import ClimateInfo, ClimateMode, EntityState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_host_mode_climate_control(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test climate mode control."""
|
||||
loop = asyncio.get_running_loop()
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
states: dict[int, EntityState] = {}
|
||||
climate_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.ClimateState)
|
||||
and state.mode == ClimateMode.HEAT
|
||||
and state.target_temperature_low == 21.5
|
||||
and state.target_temperature_high == 26.5
|
||||
and not climate_future.done()
|
||||
):
|
||||
climate_future.set_result(state)
|
||||
|
||||
# Get entities and set up state synchronization
|
||||
entities, services = await client.list_entities_services()
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
climate_infos = [e for e in entities if isinstance(e, ClimateInfo)]
|
||||
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
|
||||
|
||||
# Subscribe with the wrapper that filters initial states
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for all initial states to be broadcast
|
||||
try:
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
except TimeoutError:
|
||||
pytest.fail("Timeout waiting for initial states")
|
||||
|
||||
test_climate = next(
|
||||
(c for c in climate_infos if c.name == "Dual-mode Thermostat"), None
|
||||
)
|
||||
assert test_climate is not None, (
|
||||
"Dual-mode Thermostat thermostat climate not found"
|
||||
)
|
||||
|
||||
# Adjust setpoints
|
||||
client.climate_command(
|
||||
test_climate.key,
|
||||
mode=ClimateMode.HEAT,
|
||||
target_temperature_low=21.5,
|
||||
target_temperature_high=26.5,
|
||||
)
|
||||
|
||||
try:
|
||||
climate_state = await asyncio.wait_for(climate_future, timeout=5.0)
|
||||
except TimeoutError:
|
||||
pytest.fail("Climate state not received within 5 seconds")
|
||||
|
||||
assert isinstance(climate_state, aioesphomeapi.ClimateState)
|
||||
assert climate_state.mode == ClimateMode.HEAT
|
||||
assert climate_state.target_temperature_low == 21.5
|
||||
assert climate_state.target_temperature_high == 26.5
|
||||
@@ -5,10 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import (
|
||||
ClimateFanMode,
|
||||
ClimateFeature,
|
||||
ClimateInfo,
|
||||
ClimateMode,
|
||||
DateInfo,
|
||||
DateState,
|
||||
DateTimeInfo,
|
||||
@@ -124,46 +121,6 @@ async def test_host_mode_many_entities(
|
||||
assert len(climate_infos) >= 1, "Expected at least 1 climate entity"
|
||||
|
||||
climate_info = climate_infos[0]
|
||||
|
||||
# Verify feature flags set as expected
|
||||
assert climate_info.feature_flags == (
|
||||
ClimateFeature.SUPPORTS_ACTION
|
||||
| ClimateFeature.SUPPORTS_CURRENT_HUMIDITY
|
||||
| ClimateFeature.SUPPORTS_CURRENT_TEMPERATURE
|
||||
| ClimateFeature.SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
|
||||
| ClimateFeature.SUPPORTS_TARGET_HUMIDITY
|
||||
)
|
||||
|
||||
# Verify modes
|
||||
assert climate_info.supported_modes == [
|
||||
ClimateMode.OFF,
|
||||
ClimateMode.COOL,
|
||||
ClimateMode.HEAT,
|
||||
], f"Expected modes [OFF, COOL, HEAT], got {climate_info.supported_modes}"
|
||||
|
||||
# Verify visual parameters
|
||||
assert climate_info.visual_min_temperature == 15.0, (
|
||||
f"Expected min_temperature=15.0, got {climate_info.visual_min_temperature}"
|
||||
)
|
||||
assert climate_info.visual_max_temperature == 32.0, (
|
||||
f"Expected max_temperature=32.0, got {climate_info.visual_max_temperature}"
|
||||
)
|
||||
assert climate_info.visual_target_temperature_step == 0.1, (
|
||||
f"Expected temperature_step=0.1, got {climate_info.visual_target_temperature_step}"
|
||||
)
|
||||
assert climate_info.visual_min_humidity == 20.0, (
|
||||
f"Expected min_humidity=20.0, got {climate_info.visual_min_humidity}"
|
||||
)
|
||||
assert climate_info.visual_max_humidity == 70.0, (
|
||||
f"Expected max_humidity=70.0, got {climate_info.visual_max_humidity}"
|
||||
)
|
||||
|
||||
# Verify fan modes
|
||||
assert climate_info.supported_fan_modes == [
|
||||
ClimateFanMode.ON,
|
||||
ClimateFanMode.AUTO,
|
||||
], f"Expected fan modes [ON, AUTO], got {climate_info.supported_fan_modes}"
|
||||
|
||||
# Verify the thermostat has presets
|
||||
assert len(climate_info.supported_presets) > 0, (
|
||||
"Expected climate to have presets"
|
||||
|
||||
@@ -96,34 +96,17 @@ def test_main_all_tests_should_run(
|
||||
mock_should_run_clang_format.return_value = True
|
||||
mock_should_run_python_linters.return_value = True
|
||||
|
||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||
# Memory impact only runs when component C++ files change
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/config.py",
|
||||
"esphome/helpers.py",
|
||||
]
|
||||
# Mock list-components.py output (now returns JSON with --changed-with-deps)
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": ["wifi", "api"], "all_changed": ["wifi", "api", "sensor"]}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Run main function with mocked argv
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_changed_components",
|
||||
return_value=["wifi", "api", "sensor"],
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: ["wifi", "api"]
|
||||
if not deps
|
||||
else ["wifi", "api", "sensor"],
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
@@ -147,9 +130,9 @@ def test_main_all_tests_should_run(
|
||||
# changed_cpp_file_count should be present
|
||||
assert "changed_cpp_file_count" in output
|
||||
assert isinstance(output["changed_cpp_file_count"], int)
|
||||
# memory_impact should be false (no component C++ files changed)
|
||||
# memory_impact should be present
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
assert output["memory_impact"]["should_run"] == "false" # No files changed
|
||||
|
||||
|
||||
def test_main_no_tests_should_run(
|
||||
@@ -171,18 +154,13 @@ def test_main_no_tests_should_run(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock changed_files to return no component files
|
||||
mock_changed_files.return_value = []
|
||||
# Mock empty list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Run main function with mocked argv
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=[]),
|
||||
patch.object(determine_jobs, "filter_component_files", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=[]
|
||||
),
|
||||
):
|
||||
with patch("sys.argv", ["determine-jobs.py"]):
|
||||
determine_jobs.main()
|
||||
|
||||
# Check output
|
||||
@@ -248,22 +226,16 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = True
|
||||
|
||||
# Mock changed_files to return non-component files (to avoid memory impact)
|
||||
# Memory impact only runs when component C++ files change
|
||||
mock_changed_files.return_value = ["esphome/config.py"]
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": ["mqtt"], "all_changed": ["mqtt"]}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["script.py", "-b", "main"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=["mqtt"]),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=["mqtt"]
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
@@ -273,6 +245,13 @@ def test_main_with_branch_argument(
|
||||
mock_should_run_clang_format.assert_called_once_with("main")
|
||||
mock_should_run_python_linters.assert_called_once_with("main")
|
||||
|
||||
# Check that list-components.py was called with branch
|
||||
mock_subprocess_run.assert_called_once()
|
||||
call_args = mock_subprocess_run.call_args[0][0]
|
||||
assert "--changed-with-deps" in call_args
|
||||
assert "-b" in call_args
|
||||
assert "main" in call_args
|
||||
|
||||
# Check output
|
||||
captured = capsys.readouterr()
|
||||
output = json.loads(captured.out)
|
||||
@@ -293,7 +272,7 @@ def test_main_with_branch_argument(
|
||||
# changed_cpp_file_count should be present
|
||||
assert "changed_cpp_file_count" in output
|
||||
assert isinstance(output["changed_cpp_file_count"], int)
|
||||
# memory_impact should be false (no component C++ files changed)
|
||||
# memory_impact should be present
|
||||
assert "memory_impact" in output
|
||||
assert output["memory_impact"]["should_run"] == "false"
|
||||
|
||||
@@ -521,11 +500,16 @@ def test_main_filters_components_without_tests(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock changed_files to return component files
|
||||
mock_changed_files.return_value = [
|
||||
"esphome/components/wifi/wifi.cpp",
|
||||
"esphome/components/sensor/sensor.h",
|
||||
]
|
||||
# Mock list-components.py output with 3 components
|
||||
# wifi: has tests, sensor: has tests, airthings_ble: no tests
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{
|
||||
"directly_changed": ["wifi", "sensor"],
|
||||
"all_changed": ["wifi", "sensor", "airthings_ble"],
|
||||
}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Create test directory structure
|
||||
tests_dir = tmp_path / "tests" / "components"
|
||||
@@ -549,23 +533,6 @@ def test_main_filters_components_without_tests(
|
||||
patch.object(determine_jobs, "root_path", str(tmp_path)),
|
||||
patch.object(helpers, "root_path", str(tmp_path)),
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_changed_components",
|
||||
return_value=["wifi", "sensor", "airthings_ble"],
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"get_components_with_dependencies",
|
||||
side_effect=lambda files, deps: ["wifi", "sensor"]
|
||||
if not deps
|
||||
else ["wifi", "sensor", "airthings_ble"],
|
||||
),
|
||||
):
|
||||
# Clear the cache since we're mocking root_path
|
||||
determine_jobs._component_has_tests.cache_clear()
|
||||
@@ -821,18 +788,15 @@ def test_clang_tidy_mode_full_scan(
|
||||
mock_should_run_clang_format.return_value = False
|
||||
mock_should_run_python_linters.return_value = False
|
||||
|
||||
# Mock changed_files to return no component files
|
||||
mock_changed_files.return_value = []
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps({"directly_changed": [], "all_changed": []})
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Mock full scan (hash changed)
|
||||
with (
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=True),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=[]),
|
||||
patch.object(determine_jobs, "filter_component_files", return_value=False),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=[]
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
@@ -889,10 +853,12 @@ def test_clang_tidy_mode_targeted_scan(
|
||||
# Create component names
|
||||
components = [f"comp{i}" for i in range(component_count)]
|
||||
|
||||
# Mock changed_files to return component files
|
||||
mock_changed_files.return_value = [
|
||||
f"esphome/components/{comp}/file.cpp" for comp in components
|
||||
]
|
||||
# Mock list-components.py output
|
||||
mock_result = Mock()
|
||||
mock_result.stdout = json.dumps(
|
||||
{"directly_changed": components, "all_changed": components}
|
||||
)
|
||||
mock_subprocess_run.return_value = mock_result
|
||||
|
||||
# Mock git_ls_files to return files for each component
|
||||
cpp_files = {
|
||||
@@ -909,15 +875,6 @@ def test_clang_tidy_mode_targeted_scan(
|
||||
patch("sys.argv", ["determine-jobs.py"]),
|
||||
patch.object(determine_jobs, "_is_clang_tidy_full_scan", return_value=False),
|
||||
patch.object(determine_jobs, "git_ls_files", side_effect=mock_git_ls_files),
|
||||
patch.object(determine_jobs, "get_changed_components", return_value=components),
|
||||
patch.object(
|
||||
determine_jobs,
|
||||
"filter_component_files",
|
||||
side_effect=lambda f: f.startswith("esphome/components/"),
|
||||
),
|
||||
patch.object(
|
||||
determine_jobs, "get_components_with_dependencies", return_value=components
|
||||
),
|
||||
):
|
||||
determine_jobs.main()
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ esphome:
|
||||
friendly_name: $component_name
|
||||
|
||||
esp8266:
|
||||
board: d1_mini_pro
|
||||
board: d1_mini
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
@@ -517,35 +517,6 @@ def test_include_file_cpp(tmp_path: Path, mock_copy_file_if_changed: Mock) -> No
|
||||
mock_cg.add_global.assert_not_called()
|
||||
|
||||
|
||||
def test_include_file_with_c_header(
|
||||
tmp_path: Path, mock_copy_file_if_changed: Mock
|
||||
) -> None:
|
||||
"""Test include_file wraps header in extern C block when is_c_header is True."""
|
||||
src_file = tmp_path / "c_library.h"
|
||||
src_file.write_text("// C library header")
|
||||
|
||||
CORE.build_path = tmp_path / "build"
|
||||
|
||||
with patch("esphome.core.config.cg") as mock_cg:
|
||||
# Mock RawStatement to capture the text
|
||||
mock_raw_statement = MagicMock()
|
||||
mock_raw_statement.text = ""
|
||||
|
||||
def raw_statement_side_effect(text):
|
||||
mock_raw_statement.text = text
|
||||
return mock_raw_statement
|
||||
|
||||
mock_cg.RawStatement.side_effect = raw_statement_side_effect
|
||||
|
||||
config.include_file(src_file, Path("c_library.h"), is_c_header=True)
|
||||
|
||||
mock_copy_file_if_changed.assert_called_once()
|
||||
mock_cg.add_global.assert_called_once()
|
||||
# Check that include statement is wrapped in extern "C" block
|
||||
assert 'extern "C"' in mock_raw_statement.text
|
||||
assert '#include "c_library.h"' in mock_raw_statement.text
|
||||
|
||||
|
||||
def test_get_usable_cpu_count() -> None:
|
||||
"""Test get_usable_cpu_count returns CPU count."""
|
||||
count = config.get_usable_cpu_count()
|
||||
|
||||
Reference in New Issue
Block a user