# ESPHome AI Collaboration Guide

This document provides essential context for AI models interacting with this project. Adhering to these guidelines will ensure consistency and maintain code quality.

## 1. Project Overview & Purpose

*   **Primary Goal:** ESPHome is a system to configure microcontrollers (like ESP32, ESP8266, RP2040, and LibreTiny-based chips) using simple yet powerful YAML configuration files. It generates C++ firmware that can be compiled and flashed to these devices, allowing users to control them remotely through home automation systems.
*   **Business Domain:** Internet of Things (IoT), Home Automation.

## 2. Core Technologies & Stack

*   **Languages:** Python (>=3.11), C++ (gnu++20)
*   **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
*   **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
*   **Configuration:** YAML.
*   **Key Libraries/Dependencies:**
    *   **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `tornado` (for the web server), `aioesphomeapi` (for the native API).
    *   **C++:** `ArduinoJson` (for JSON serialization/deserialization), `AsyncMqttClient-esphome` (for MQTT), `ESPAsyncWebServer` (for the web server).
*   **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
*   **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.

## 3. Architectural Patterns

*   **Overall Architecture:** The project follows a code-generation architecture. The Python code parses user-defined YAML configuration files and generates C++ source code. This C++ code is then compiled and flashed to the target microcontroller using PlatformIO.

*   **Directory Structure Philosophy:**
    *   `/esphome`: Contains the core Python source code for the ESPHome application.
    *   `/esphome/components`: Contains the individual components that can be used in ESPHome configurations. Each component is a self-contained unit with its own C++ and Python code.
    *   `/tests`: Contains all unit and integration tests for the Python code.
    *   `/docker`: Contains Docker-related files for building and running ESPHome in a container.
    *   `/script`: Contains helper scripts for development and maintenance.

*   **Core Architectural Components:**
    1.  **Configuration System** (`esphome/config*.py`): Handles YAML parsing and validation using Voluptuous, schema definitions, and multi-platform configurations.
    2.  **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
    3.  **Component System** (`esphome/components/`): Contains modular hardware and software components with platform-specific implementations and dependency management.
    4.  **Core Framework** (`esphome/core/`): Manages the application lifecycle, hardware abstraction, and component registration.
    5.  **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.

*   **Platform Support:**
    1.  **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3).
    2.  **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints.
    3.  **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support.
    4.  **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components.

## 4. Coding Conventions & Style Guide

*   **Formatting:**
    *   **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
    *   **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.

*   **Naming Conventions:**
    *   **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
    *   **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions):
        - Function, method, and variable names: `lower_snake_case`
        - Class/struct/enum names: `UpperCamelCase`
        - Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE`
        - Function-local constants: `lower_snake_case`
        - Protected/private fields: `lower_snake_case_with_trailing_underscore_`
        - Favor descriptive names over abbreviations

*   **C++ Field Visibility:**
    *   **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
    *   **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
        1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references.
           ```cpp
           // Helper to find matching string in vector and return its pointer
           inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
             for (const char *item : vec) {
               if (strcmp(item, value) == 0)
                 return item;
             }
             return nullptr;
           }

           class ClimateDevice {
            public:
             void set_custom_fan_modes(std::initializer_list<const char *> modes) {
               this->custom_fan_modes_ = modes;
               this->active_custom_fan_mode_ = nullptr;  // Reset when modes change
             }
             bool set_custom_fan_mode(const char *mode) {
               // Find mode in supported list and store that pointer (not the input pointer)
               const char *validated_mode = vector_find(this->custom_fan_modes_, mode);
               if (validated_mode != nullptr) {
                 this->active_custom_fan_mode_ = validated_mode;
                 return true;
               }
               return false;
             }
            private:
             std::vector<const char *> custom_fan_modes_;  // Pointers to string literals in flash
             const char *active_custom_fan_mode_{nullptr};  // Must point to entry in custom_fan_modes_
           };
           ```
        2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption.
           ```cpp
           class Buffer {
            public:
             void resize(size_t new_size) {
               auto new_data = std::make_unique<uint8_t[]>(new_size);
               if (this->data_) {
                 std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size));
               }
               this->data_ = std::move(new_data);
               this->size_ = new_size;  // Must stay in sync with data_
             }
            private:
             std::unique_ptr<uint8_t[]> data_;
             size_t size_{0};  // Must match allocated size of data_
           };
           ```
        3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip.
    *   **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members.

*   **C++ Preprocessor Directives:**
    *   **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
    *   **Use `#define` only for:**
        - Conditional compilation (`#ifdef`, `#ifndef`)
        - Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)

*   **C++ Additional Conventions:**
    *   **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`)
    *   **Indentation:** Use spaces (two per indentation level), not tabs
    *   **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
    *   **Line length:** Wrap lines at no more than 120 characters
    *   **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
        (never change after construction) should be constructor parameters rather than set via setter methods.
        This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
        In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments.
        ```cpp
        // Good - required invariant dependency as constructor parameter
        class SourceTextSensor : public text_sensor::TextSensor, public Component {
         public:
          explicit SourceTextSensor(text::Text *source) : source_(source) {}
         protected:
          text::Text *source_;
        };
        ```
        ```cpp
        // Bad - required invariant dependency as setter
        class SourceTextSensor : public text_sensor::TextSensor, public Component {
         public:
          void set_source(text::Text *source) { this->source_ = source; }
         protected:
          text::Text *source_{nullptr};
        };
        ```

*   **Component Structure:**
    *   **Standard Files:**
        ```
        components/[component_name]/
        ├── __init__.py          # Component configuration schema and code generation
        ├── [component].h        # C++ header file (if needed)
        ├── [component].cpp      # C++ implementation (if needed)
        └── [platform]/          # Platform-specific implementations
            ├── __init__.py      # Platform-specific configuration
            ├── [platform].h     # Platform C++ header
            └── [platform].cpp   # Platform C++ implementation
        ```

    *   **Component Metadata:**
        - `DEPENDENCIES`: List of required components
        - `AUTO_LOAD`: Components to automatically load
        - `CONFLICTS_WITH`: Incompatible components
        - `CODEOWNERS`: GitHub usernames responsible for maintenance
        - `MULTI_CONF`: Whether multiple instances are allowed

*   **Code Generation & Common Patterns:**
    *   **Configuration Schema Pattern:**
        ```python
        import esphome.codegen as cg
        import esphome.config_validation as cv
        from esphome.const import CONF_KEY, CONF_ID

        CONF_PARAM = "param"  # A constant that does not yet exist in esphome/const.py

        my_component_ns = cg.esphome_ns.namespace("my_component")
        MyComponent = my_component_ns.class_("MyComponent", cg.Component)

        CONFIG_SCHEMA = cv.Schema({
            cv.GenerateID(): cv.declare_id(MyComponent),
            cv.Required(CONF_KEY): cv.string,
            cv.Optional(CONF_PARAM, default=42): cv.int_,
        }).extend(cv.COMPONENT_SCHEMA)

        async def to_code(config):
            var = cg.new_Pvariable(config[CONF_ID])
            await cg.register_component(var, config)
            cg.add(var.set_key(config[CONF_KEY]))
            cg.add(var.set_param(config[CONF_PARAM]))
        ```

    *   **C++ Class Pattern:**
        ```cpp
        namespace esphome::my_component {

        class MyComponent : public Component {
         public:
          void setup() override;
          void loop() override;
          void dump_config() override;

          void set_key(const std::string &key) { this->key_ = key; }
          void set_param(int param) { this->param_ = param; }

         protected:
          std::string key_;
          int param_{0};
        };

        }  // namespace esphome::my_component
        ```

    *   **Common Component Examples:**
        - **Sensor:**
          ```python
          from esphome.components import sensor
          CONFIG_SCHEMA = sensor.sensor_schema(MySensor).extend(cv.polling_component_schema("60s"))
          async def to_code(config):
              var = await sensor.new_sensor(config)
              await cg.register_component(var, config)
          ```

        - **Binary Sensor:**
          ```python
          from esphome.components import binary_sensor
          CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend({ ... })
          async def to_code(config):
              var = await binary_sensor.new_binary_sensor(config)
          ```

        - **Switch:**
          ```python
          from esphome.components import switch
          CONFIG_SCHEMA = switch.switch_schema().extend({ ... })
          async def to_code(config):
              var = await switch.new_switch(config)
          ```

*   **Automations (Triggers, Actions, Conditions):**

    Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).

    *   **Triggers -- Callback method (preferred):**

        Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.

        **Python:**
        ```python
        from esphome import automation

        CONFIG_SCHEMA = cv.Schema({
            cv.GenerateID(): cv.declare_id(MyComponent),
            cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
        }).extend(cv.COMPONENT_SCHEMA)

        async def to_code(config):
            var = cg.new_Pvariable(config[CONF_ID])
            await cg.register_component(var, config)
            for conf in config.get(CONF_ON_STATE, []):
                await automation.build_callback_automation(
                    var, "add_on_state_callback", [(bool, "x")], conf
                )
        ```

        `build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).

        For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
        ```python
        for conf_key, forwarder in (
            (CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
            (CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
        ):
            for conf in config.get(conf_key, []):
                await automation.build_callback_automation(
                    var, "add_on_state_callback", [], conf, forwarder=forwarder
                )
        ```

        **C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
        ```cpp
        class MyComponent : public Component {
         public:
          // Must be a template -- accepts both std::function and pointer-sized forwarder structs
          template<typename F> void add_on_state_callback(F &&callback) {
            this->state_callback_.add(std::forward<F>(callback));
          }
         protected:
          // Use CallbackManager when callbacks are always registered (e.g. core components)
          CallbackManager<void(bool)> state_callback_;
          // Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
          // (nullptr vs empty std::vector) per instance when no callbacks are added
          // LazyCallbackManager<void(bool)> state_callback_;
        };
        ```

    *   **Triggers -- Trigger class method:**

        Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).

        **Python:**
        ```python
        TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())

        CONFIG_SCHEMA = cv.Schema({
            cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
                {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
            ),
        })

        async def to_code(config):
            for conf in config.get(CONF_ON_TURN_ON, []):
                trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
                await automation.build_automation(trigger, [], conf)
        ```

        **C++:**
        ```cpp
        class TurnOnTrigger : public Trigger<> {
         public:
          explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
            parent->add_on_state_callback([this](bool state) {
              if (state && !this->last_on_)
                this->trigger();
              this->last_on_ = state;
            });
          }
         protected:
          bool last_on_;
        };
        ```

    *   **Actions:**
        ```cpp
        template<typename... Ts> class MyAction : public Action<Ts...> {
         public:
          explicit MyAction(MyComponent *parent) : parent_(parent) {}
          void play(const Ts &...) override { this->parent_->do_something(); }
         protected:
          MyComponent *parent_;
        };
        ```
        Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.

    *   **Conditions:**
        ```cpp
        template<typename... Ts> class MyCondition : public Condition<Ts...> {
         public:
          explicit MyCondition(MyComponent *parent) : parent_(parent) {}
          bool check(const Ts &...) override { return this->parent_->is_active(); }
         protected:
          MyComponent *parent_;
        };
        ```
        Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.

*   **Configuration Validation:**
    *   **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
    *   **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
    *   **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
    *   **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`.
    *   **Schema Extensions:**
        ```python
        CONFIG_SCHEMA = cv.Schema({ ... })
         .extend(cv.COMPONENT_SCHEMA)
         .extend(uart.UART_DEVICE_SCHEMA)
         .extend(i2c.i2c_device_schema(0x48))
         .extend(spi.spi_device_schema(cs_pin_required=True))
        ```

## 5. Key Files & Entrypoints

*   **Main Entrypoint(s):** `esphome/__main__.py` is the main entrypoint for the ESPHome command-line interface.
*   **Configuration:**
    *   `pyproject.toml`: Defines the Python project metadata and dependencies.
    *   `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
    *   `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
*   **CI/CD Pipeline:** Defined in `.github/workflows`.
*   **Static Analysis & Development:**
    *   `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.

## 6. Development & Testing Workflow

*   **Local Development Environment:** Use the provided Docker container or create a Python virtual environment and install dependencies from `requirements_dev.txt`.
*   **Running Commands:** Use the `script/run-in-env.py` script to execute commands within the project's virtual environment. For example, to run the linter: `python3 script/run-in-env.py pre-commit run`.
*   **Testing:**
    *   **Python:** Run unit tests with `pytest`.
    *   **C++:** Use `clang-tidy` for static analysis.
    *   **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
        ```
        tests/
        ├── test_build_components/
        │   └── common/          # Shared bus packages (uart, i2c, spi, etc.)
        │       ├── uart/        # UART at default baud rate
        │       ├── uart_115200/ # UART at 115200 baud
        │       ├── i2c/         # I2C bus
        │       └── spi/         # SPI bus
        └── components/[component]/
            ├── common.yaml          # Component-only config (no bus definitions)
            ├── test.esp32-idf.yaml      # config + compile
            ├── test.esp8266-ard.yaml    # config + compile
            ├── test-variant.esp32-idf.yaml  # variant test, config + compile
            ├── validate.esp32-idf.yaml      # config-only (never compiled)
            └── validate-legacy.esp32-idf.yaml  # config-only variant
        ```
        Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.

    *   **Config-only test files (`validate.*.yaml`):** Use this prefix when a YAML file only needs to exercise schema/validation paths and does not need to be compiled. CI runs `validate.*.yaml` files with `esphome config` only and skips them during compile. The grammar mirrors `test.*.yaml`:
        - `validate.<platform>.yaml` — base config-only test
        - `validate-<variant>.<platform>.yaml` — config-only variant

        Use this for things like deprecated-syntax migration tests, schema edge cases, or platform-specific validation branches where building firmware adds no signal. A component may have any mix of `test.*.yaml` and `validate.*.yaml` files. Validate files never participate in bus-grouping; each one runs as its own `esphome config` invocation.

        When a PR's only edits to a component are `validate.*.yaml` files (no source changes, no `test.*.yaml` changes, and the component isn't pulled in as a dependency of another changed component), CI skips the compile stage for that component entirely and only runs config validation. This is decided in `script/determine-jobs.py` via `_component_change_is_validate_only` and surfaced as the `validate_only_components` output that the `test-build-components-split` job consumes.

    *   **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`:
        ```yaml
        # test.esp32-idf.yaml — use packages for buses
        packages:
          uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml

        <<: !include common.yaml
        ```
        ```yaml
        # common.yaml — component config only, NO bus definitions
        my_component:
          id: my_instance

        sensor:
          - platform: my_component
            name: My Sensor
        ```
        Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.

    *   **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
        ```bash
        ./script/test_component_grouping.py -e config --all
        ```
        This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
*   **Debugging and Troubleshooting:**
    *   **Debug Tools:**
        - `esphome config <file>.yaml` to validate configuration.
        - `esphome compile <file>.yaml` to compile without uploading.
        - Check the Dashboard for real-time logs.
        - Use component-specific debug logging.
    *   **Common Issues:**
        - **Import Errors**: Check component dependencies and `PYTHONPATH`.
        - **Validation Errors**: Review configuration schema definitions.
        - **Build Errors**: Check platform compatibility and library versions.
        - **Runtime Errors**: Review generated C++ code and component logic.

## 7. Specific Instructions for AI Collaboration

*   **Contribution Workflow (Pull Request Process):**
    1.  **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
    2.  **Make Changes:** Adhere to all coding conventions and patterns.
    3.  **Test:** Create component tests for all supported platforms and run the full test suite locally.
    4.  **Lint:** Run `pre-commit` to ensure code is compliant.
    5.  **Commit:** Commit your changes. There is no strict format for commit messages.
    6.  **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.

*   **Documentation Contributions:**
    *   Documentation is hosted in the separate `esphome/esphome-docs` repository.
    *   The contribution workflow is the same as for the codebase.
    *   When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.

*   **Best Practices:**
    *   **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
    *   **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
    *   **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
    *   **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.

        **Why Heap Allocation Matters:**

        ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this.

        **Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs.

        **STL Container Guidelines:**

        ESPHome runs on embedded systems with limited resources. Choose containers carefully:

        1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
           ```cpp
           // Bad - generates STL realloc code
           std::vector<int> values;

           // Good - no dynamic allocation
           std::array<int, MAX_VALUES> values;
           ```
           Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.

           **For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.

           > **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.

           ```cpp
           // Bad - STL overhead for simple byte buffer
           std::vector<uint8_t> buffer;
           buffer.resize(256);

           // Good - minimal overhead, single allocation
           std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
           // Or if size is constant:
           std::array<uint8_t, 256> buffer;
           ```

        2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for compile-time fixed size with `push_back()` interface (no dynamic allocation).
           ```cpp
           // Bad - generates STL realloc code (_M_realloc_insert)
           std::vector<ServiceRecord> services;
           services.reserve(5);  // Still includes reallocation machinery

           // Good - compile-time fixed size, no dynamic allocation
           StaticVector<ServiceRecord, MAX_SERVICES> services;
           services.push_back(record1);
           ```
           Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
           Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.

        3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
           ```cpp
           // Bad - generates STL realloc code (_M_realloc_insert)
           std::vector<TxtRecord> txt_records;
           txt_records.reserve(5);  // Still includes reallocation machinery

           // Good - runtime size, single allocation, no reallocation machinery
           FixedVector<TxtRecord> txt_records;
           txt_records.init(record_count);  // Initialize with exact size at runtime
           ```
           **Benefits:**
           - Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
           - Single allocation, no upper bound needed
           - No reallocation overhead
           - Compatible with protobuf code generation when using `[(fixed_vector) = true]` option

        4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
           ```cpp
           // Bad - 2KB+ overhead for red-black tree/hash table
           std::map<std::string, int> small_lookup;
           std::unordered_map<int, std::string> tiny_map;

           // Good - simple struct with linear search (std::vector is fine)
           struct LookupEntry {
             const char *key;
             int value;
           };
           std::vector<LookupEntry> small_lookup = {
             {"key1", 10},
             {"key2", 20},
             {"key3", 30},
           };
           // Or std::array if size is compile-time constant:
           // std::array<LookupEntry, 3> small_lookup = {{ ... }};
           ```
           Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.

        5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.

        6. **Detection:** Look for these patterns in compiler output:
           - Large code sections with STL symbols (vector, map, set)
           - `alloc`, `realloc`, `dealloc` in symbol names
           - `_M_realloc_insert`, `_M_default_append` (vector reallocation)
           - Red-black tree code (`rb_tree`, `_Rb_tree`)
           - Hash table infrastructure (`unordered_map`, `hash`)

        **Prioritize optimization effort for:**
        - Core components (API, network, logger)
        - Widely-used components (mdns, wifi, ble)
        - Components causing flash size complaints

        Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).

        **Callback Managers:**

        ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method.

        | Type | Idle overhead (32-bit) | When to use |
        |------|----------------------|-------------|
        | `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
        | `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |

        `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.

        **Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature:
        ```cpp
        // Bad -- forces heap allocation for forwarder structs
        void add_on_state_callback(std::function<void(bool)> &&callback) {
          this->state_callback_.add(std::move(callback));
        }

        // Good -- accepts any callable without forcing std::function wrapping
        template<typename F> void add_on_state_callback(F &&callback) {
          this->state_callback_.add(std::forward<F>(callback));
        }
        ```

    *   **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.

        **Bad Pattern (Module-Level Globals):**
        ```python
        # Don't do this - state persists between compilation runs
        _component_state = []
        _use_feature = None

        def enable_feature():
            global _use_feature
            _use_feature = True
        ```

        **Bad Pattern (Flat Keys):**
        ```python
        # Don't do this - keys should be namespaced under component domain
        MY_FEATURE_KEY = "my_component_feature"
        CORE.data[MY_FEATURE_KEY] = True
        ```

        **Good Pattern (dataclass):**
        ```python
        from dataclasses import dataclass, field
        from esphome.core import CORE

        DOMAIN = "my_component"

        @dataclass
        class MyComponentData:
            feature_enabled: bool = False
            item_count: int = 0
            items: list[str] = field(default_factory=list)

        def _get_data() -> MyComponentData:
            if DOMAIN not in CORE.data:
                CORE.data[DOMAIN] = MyComponentData()
            return CORE.data[DOMAIN]

        def request_feature() -> None:
            _get_data().feature_enabled = True

        def add_item(item: str) -> None:
            _get_data().items.append(item)
        ```

        If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.

        **Why this matters:**
        - Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
        - `CORE.data` automatically clears between runs
        - Namespacing under `DOMAIN` prevents key collisions between components
        - `@dataclass` provides type safety and cleaner attribute access

*   **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.

*   **Dependencies & Build System Integration:**
    *   **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`.
    *   **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`.
    *   **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags.

## 8. Public API and Breaking Changes

*   **Public C++ API:**
    *   **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal.
    *   **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API.
    *   **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters).

*   **Public Python API:**
    *   All documented configuration options at [esphome.io](https://esphome.io) are public API.
    *   Python code in `esphome/core/` actively used by existing core components is considered stable API.
    *   Other Python code is internal unless explicitly documented for external component use.

*   **Breaking Changes Policy:**
    *   Aim for **6-month deprecation window** when possible
    *   Clean breaks allowed for: signature changes, deep refactorings, resource constraints
    *   Must document migration path in PR description (generates release notes)
    *   Blog post required for core/base class changes or significant architectural changes
    *   Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes

*   **Breaking Change Checklist:**
    - [ ] Clear justification (RAM/flash savings, architectural improvement)
    - [ ] Explored non-breaking alternatives
    - [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
    - [ ] Documented migration path in PR description with before/after examples
    - [ ] Updated all internal usage and esphome-docs
    - [ ] Tested backward compatibility during deprecation period

*   **Deprecation Pattern (C++):**
    ```cpp
    // Remove before 2026.6.0
    ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
    void old_method() { this->new_method(); }
    ```

*   **Deprecation Pattern (Python):**
    ```python
    # Remove before 2026.6.0
    if CONF_OLD_KEY in config:
        _LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
        config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY)  # Auto-migrate
    ```
