From 588fde91b252f55d5e19c95f634393ca3e423f5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Oct 2026 09:19:35 -1000 Subject: [PATCH] [template] Keep alarm codes in a shared flash table (#20084) --- .../template/alarm_control_panel/__init__.py | 9 ++++++-- .../template_alarm_control_panel.cpp | 16 ++++++++----- .../template_alarm_control_panel.h | 15 +++++------- .../template/config/alarm_codes.yaml | 23 +++++++++++++++++++ .../template/test_template_alarm_codes.py | 20 ++++++++++++++++ 5 files changed, 66 insertions(+), 17 deletions(-) create mode 100644 tests/component_tests/template/config/alarm_codes.yaml create mode 100644 tests/component_tests/template/test_template_alarm_codes.py diff --git a/esphome/components/template/alarm_control_panel/__init__.py b/esphome/components/template/alarm_control_panel/__init__.py index 59624a5f53..a708c83d09 100644 --- a/esphome/components/template/alarm_control_panel/__init__.py +++ b/esphome/components/template/alarm_control_panel/__init__.py @@ -117,8 +117,13 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = await alarm_control_panel.new_alarm_control_panel(config) await cg.register_component(var, config) - if CONF_CODES in config: - cg.add(var.set_codes(config[CONF_CODES])) + # Shared flash table ended by nullptr, so the panel stores only a pointer. + # requires_code_to_arm needs codes (validate_config) and defaults to false. + if codes := config.get(CONF_CODES): + table = cg.shared_progmem_array( + "alarm_codes", cg.const_char_ptr, [*codes, cg.nullptr] + ) + cg.add(var.set_codes(table)) if CONF_REQUIRES_CODE_TO_ARM in config: cg.add(var.set_requires_code_to_arm(config[CONF_REQUIRES_CODE_TO_ARM])) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp index 293f025527..12ff0b5a4b 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.cpp @@ -36,6 +36,11 @@ static const LogString *sensor_type_to_string(AlarmSensorType type) { #endif void TemplateAlarmControlPanel::dump_config() { + size_t num_codes = 0; + if (this->codes_ != nullptr) { + while (this->codes_[num_codes] != nullptr) + num_codes++; + } ESP_LOGCONFIG(TAG, "TemplateAlarmControlPanel:\n" " Current State: %s\n" @@ -47,8 +52,8 @@ void TemplateAlarmControlPanel::dump_config() { " Pending Time: %" PRIu32 "s\n" " Trigger Time: %" PRIu32 "s\n" " Supported Features: %" PRIu32, - LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), this->codes_.size(), - YESNO(!this->codes_.empty() && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), + LOG_STR_ARG(alarm_control_panel_state_to_string(this->current_state_)), num_codes, + YESNO(this->codes_ != nullptr && this->requires_code_to_arm_), (this->arming_away_time_ / 1000), (this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features()); #ifdef USE_BINARY_SENSOR @@ -194,13 +199,12 @@ void TemplateAlarmControlPanel::loop() { } bool TemplateAlarmControlPanel::is_code_valid_(optional code) { - if (!this->codes_.empty()) { + if (this->codes_ != nullptr) { if (code.has_value()) { ESP_LOGVV(TAG, "Checking code: %s", code.value().c_str()); - // Use strcmp for const char* comparison const char *code_cstr = code.value().c_str(); - for (const char *stored_code : this->codes_) { - if (strcmp(stored_code, code_cstr) == 0) + for (const char *const *stored = this->codes_; *stored != nullptr; stored++) { + if (strcmp(*stored, code_cstr) == 0) return true; } return false; diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 5888ce5e29..c977da10a9 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -61,7 +61,7 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl void setup() override; void loop() override; uint32_t get_supported_features() const override; - bool get_requires_code() const override { return !this->codes_.empty(); } + bool get_requires_code() const override { return this->codes_ != nullptr; } bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_all_sensors_ready() { return this->sensors_ready_; }; void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } @@ -83,14 +83,11 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED); #endif - /** Set the codes (from initializer list). + /** Set the codes. * - * @param codes The list of valid codes + * @param codes nullptr-terminated table of valid codes; must outlive the panel */ - void set_codes(std::initializer_list codes) { this->codes_ = codes; } - - // Deleted overload to catch incorrect std::string usage at compile time - void set_codes(std::initializer_list codes) = delete; + void set_codes(const char *const *codes) { this->codes_ = codes; } /** set requires a code to arm * @@ -150,8 +147,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl uint32_t pending_time_; // the time in trigger uint32_t trigger_time_; - // a list of codes (const char* pointers to string literals in flash) - FixedVector codes_; + // nullptr-terminated table of codes, shared in flash + const char *const *codes_{nullptr}; // requires a code to arm bool requires_code_to_arm_ = false; bool supports_arm_home_ = false; diff --git a/tests/component_tests/template/config/alarm_codes.yaml b/tests/component_tests/template/config/alarm_codes.yaml new file mode 100644 index 0000000000..afeb45e1d2 --- /dev/null +++ b/tests/component_tests/template/config/alarm_codes.yaml @@ -0,0 +1,23 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +alarm_control_panel: + - platform: template + id: panel_a + name: Panel A + codes: + - "1234" + - "5678" + - platform: template + id: panel_b + name: Panel B + codes: + - "1234" + - "5678" + - platform: template + id: panel_none + name: Panel None + codes: [] diff --git a/tests/component_tests/template/test_template_alarm_codes.py b/tests/component_tests/template/test_template_alarm_codes.py new file mode 100644 index 0000000000..1a8e94be90 --- /dev/null +++ b/tests/component_tests/template/test_template_alarm_codes.py @@ -0,0 +1,20 @@ +"""Tests for the template alarm control panel codes codegen.""" + +from collections.abc import Callable +from pathlib import Path + + +def test_codes_share_one_progmem_table( + generate_main: Callable[[str | Path], str], + component_config_path: Callable[[str], Path], +) -> None: + """Identical code lists share one nullptr-terminated flash table; empty lists set none.""" + main_cpp = generate_main(component_config_path("alarm_codes.yaml")) + + assert ( + 'static constexpr const char * alarm_codes[] PROGMEM = {"1234", "5678", nullptr};' + in main_cpp + ) + assert "panel_a->set_codes(alarm_codes);" in main_cpp + assert "panel_b->set_codes(alarm_codes);" in main_cpp + assert "panel_none->set_codes(" not in main_cpp