From 69ef58089c1f91b3c95ae3766031130584fcc24e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Oct 2026 07:56:49 -1000 Subject: [PATCH] Document session lessons in AGENTS.md --- AGENTS.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 8194cd50407..22e8a2ada9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -177,6 +177,8 @@ file does, and it is the authority when they disagree. The most useful starting Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use `set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms: https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb + * **Repeating work:** use `set_interval`, never a `set_timeout` that re-arms itself; every re-arm + allocates a new scheduler item. * **Scheduler ids:** name a timer only when it must be cancelled or replaced, and use a `static constexpr uint32_t` id, never a string. Ids are per component instance and cannot clash with other components, so number them from 0 and keep all of a component's ids together in one place. @@ -191,6 +193,10 @@ file does, and it is the authority when they disagree. The most useful starting // Bad - this is exactly what the base class already does float get_setup_priority() const override { return setup_priority::DATA; } ``` + * **Lookup tables and status strings in flash:** on ESP8266 a `const` table, `static const` array or + `std::array` member initializer lives in RAM. Mark such tables `PROGMEM` and read them with + `progmem_memcpy()` or `progmem_read_byte()` (`esphome/core/hal.h`). Pass status text as `LOG_STR`, + e.g. `this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL))`, not as a `const char *`. * **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they can be stored in flash rather than RAM. ```cpp @@ -534,6 +540,7 @@ file does, and it is the authority when they disagree. The most useful starting `cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples. * **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`. + An intentionally empty body (an unused `register_dumper` builder, for example) gets a one-line docstring, not `pass`. * **Configuration Validation:** * **Reuse existing validators:** Before writing a custom validator, check for an existing one in `config_validation.py` and compose it in `cv.All(...)` rather than duplicating logic across components. For example, rename a config key with `cv.rename_key(CONF_OLD, CONF_NEW, removed_in="2026.6.0")`, and reject mutually-exclusive keys with `cv.has_at_most_one_key(...)` / `cv.has_exactly_one_key(...)`. See how `api` composes `cv.has_exactly_one_key` + `cv.rename_key`. @@ -569,6 +576,13 @@ file does, and it is the authority when they disagree. The most useful starting * **Testing:** * **Python:** Run unit tests with `pytest`. * **C++:** Use `clang-tidy` for static analysis. + * **C++ unit tests:** gtests in `tests/components//*.cpp`, run with + `script/cpp_unit_test.py `; see `tests/components/README.md`. Set the loop clock in a test + with `LoopBlockingGuard{nullptr, nullptr, now_ms}`; the runner resets it before every test. + * **Integration tests:** `tests/integration/` runs real firmware on the host. Serial components prove + timeouts and recovery with the `uart_mock` fixture (`tests/integration/test_uart_mock_*.py`). + * **Keep build-test variants few:** one `test..yaml` per platform the code actually differs + on. A variant per board for platform-identical code only adds CI time. * **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows: ``` tests/