mirror of
https://github.com/esphome/esphome.git
synced 2026-09-09 14:28:46 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
637513694f | ||
|
|
176e9d43c9 | ||
|
|
c55330c27e | ||
|
|
9f7342dfaf | ||
|
|
4fbdf00bcb |
@@ -9,12 +9,12 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
|
|
||||||
## 2. Core Technologies & Stack
|
## 2. Core Technologies & Stack
|
||||||
|
|
||||||
* **Languages:** Python (>=3.12), C++ (gnu++20)
|
* **Languages:** Python (>=3.11), C++ (gnu++20)
|
||||||
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
||||||
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
||||||
* **Configuration:** YAML.
|
* **Configuration:** YAML.
|
||||||
* **Key Libraries/Dependencies:**
|
* **Key Libraries/Dependencies:**
|
||||||
* **Python:** `voluptuous` (for configuration validation), `PyYAML` (for parsing configuration files), `paho-mqtt` (for MQTT communication), `aioesphomeapi` (for the native API).
|
* **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).
|
* **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).
|
* **Package Manager(s):** `pip` (for Python dependencies), `platformio` (for C++/PlatformIO dependencies).
|
||||||
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
|
* **Communication Protocols:** Protobuf (for native API), MQTT, HTTP.
|
||||||
@@ -35,6 +35,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
2. **Code Generation** (`esphome/codegen.py`, `esphome/cpp_generator.py`): Manages Python to C++ code generation, template processing, and build flag management.
|
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.
|
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.
|
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:**
|
* **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).
|
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).
|
||||||
@@ -58,19 +59,6 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
|
||||||
- Favor descriptive names over abbreviations
|
- Favor descriptive names over abbreviations
|
||||||
|
|
||||||
* **Python Idioms:**
|
|
||||||
* **Assignment expressions (PEP 572):** Prefer the walrus operator (`:=`) wherever it removes a redundant lookup or a throwaway temporary. The most common case in component code is presence-checking a config key and then indexing it separately — fetch once with `.get()` and bind in the condition instead:
|
|
||||||
```python
|
|
||||||
# Bad - looks up CONF_BLAH twice
|
|
||||||
if CONF_BLAH in config:
|
|
||||||
cg.add(var.set_blah(config[CONF_BLAH]))
|
|
||||||
|
|
||||||
# Good - single lookup, value bound inline
|
|
||||||
if (blah := config.get(CONF_BLAH)) is not None:
|
|
||||||
cg.add(var.set_blah(blah))
|
|
||||||
```
|
|
||||||
The same applies to `while` loops and comprehensions where it avoids recomputing a value. Don't contort code to use it — reach for `:=` only when it genuinely cuts repetition or an extra assignment line.
|
|
||||||
|
|
||||||
* **C++ Field Visibility:**
|
* **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_`.
|
* **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:
|
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
|
||||||
@@ -410,31 +398,20 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
│ ├── i2c/ # I2C bus
|
│ ├── i2c/ # I2C bus
|
||||||
│ └── spi/ # SPI bus
|
│ └── spi/ # SPI bus
|
||||||
└── components/[component]/
|
└── components/[component]/
|
||||||
├── common.yaml # Component-only config (no bus definitions)
|
├── common.yaml # Component-only config (no bus definitions)
|
||||||
├── test.esp32-idf.yaml # config + compile
|
├── test.esp32-idf.yaml
|
||||||
├── test.esp8266-ard.yaml # config + compile
|
├── test.esp8266-ard.yaml
|
||||||
├── test-variant.esp32-idf.yaml # variant test, config + compile
|
└── test.rp2040-ard.yaml
|
||||||
├── 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.
|
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`:
|
* **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/`:
|
||||||
- `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/`.
|
|
||||||
|
|
||||||
All includes in test files must go through dict-style `packages:` so that batch grouping works correctly — the grouping scripts only understand dict-style packages. Never use list-style packages (`packages: [- !include ...]`) or top-level merge keys (`<<: !include common.yaml`). Bus packages are keyed by the bus name; the component's `common.yaml` is keyed by the component name (e.g. `cst328: !include common.yaml`):
|
|
||||||
```yaml
|
```yaml
|
||||||
# test.esp32-idf.yaml — everything included via named packages
|
# test.esp32-idf.yaml — use packages for buses
|
||||||
packages:
|
packages:
|
||||||
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||||
my_component: !include common.yaml
|
|
||||||
|
<<: !include common.yaml
|
||||||
```
|
```
|
||||||
```yaml
|
```yaml
|
||||||
# common.yaml — component config only, NO bus definitions
|
# common.yaml — component config only, NO bus definitions
|
||||||
@@ -456,6 +433,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
* **Debug Tools:**
|
* **Debug Tools:**
|
||||||
- `esphome config <file>.yaml` to validate configuration.
|
- `esphome config <file>.yaml` to validate configuration.
|
||||||
- `esphome compile <file>.yaml` to compile without uploading.
|
- `esphome compile <file>.yaml` to compile without uploading.
|
||||||
|
- Check the Dashboard for real-time logs.
|
||||||
- Use component-specific debug logging.
|
- Use component-specific debug logging.
|
||||||
* **Common Issues:**
|
* **Common Issues:**
|
||||||
- **Import Errors**: Check component dependencies and `PYTHONPATH`.
|
- **Import Errors**: Check component dependencies and `PYTHONPATH`.
|
||||||
@@ -474,7 +452,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
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.
|
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 Contributions:**
|
||||||
* Documentation is hosted in the separate `esphome/esphome.io` repository.
|
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
|
||||||
* The contribution workflow is the same as for the codebase.
|
* 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.
|
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
|
||||||
|
|
||||||
@@ -657,7 +635,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
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.
|
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:**
|
**Why this matters:**
|
||||||
- Module-level globals persist between compilation runs if the host process (e.g. device-builder) doesn't fork/exec
|
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
|
||||||
- `CORE.data` automatically clears between runs
|
- `CORE.data` automatically clears between runs
|
||||||
- Namespacing under `DOMAIN` prevents key collisions between components
|
- Namespacing under `DOMAIN` prevents key collisions between components
|
||||||
- `@dataclass` provides type safety and cleaner attribute access
|
- `@dataclass` provides type safety and cleaner attribute access
|
||||||
@@ -693,7 +671,7 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
- [ ] Explored non-breaking alternatives
|
- [ ] Explored non-breaking alternatives
|
||||||
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
|
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
|
||||||
- [ ] Documented migration path in PR description with before/after examples
|
- [ ] Documented migration path in PR description with before/after examples
|
||||||
- [ ] Updated all internal usage and esphome.io
|
- [ ] Updated all internal usage and esphome-docs
|
||||||
- [ ] Tested backward compatibility during deprecation period
|
- [ ] Tested backward compatibility during deprecation period
|
||||||
|
|
||||||
* **Deprecation Pattern (C++):**
|
* **Deprecation Pattern (C++):**
|
||||||
@@ -710,9 +688,3 @@ This document provides essential context for AI models interacting with this pro
|
|||||||
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
|
_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
|
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
|
||||||
```
|
```
|
||||||
## 9. English Language
|
|
||||||
|
|
||||||
The project uses English for non-code content. When drafting documentation, code comments, commit messages,
|
|
||||||
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
|
|
||||||
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
|
|
||||||
audience, including non-native English speakers.
|
|
||||||
+7
-29
@@ -5,30 +5,24 @@ Checks: >-
|
|||||||
-altera-*,
|
-altera-*,
|
||||||
-android-*,
|
-android-*,
|
||||||
-boost-*,
|
-boost-*,
|
||||||
-bugprone-derived-method-shadowing-base-method,
|
|
||||||
-bugprone-easily-swappable-parameters,
|
-bugprone-easily-swappable-parameters,
|
||||||
-bugprone-implicit-widening-of-multiplication-result,
|
-bugprone-implicit-widening-of-multiplication-result,
|
||||||
-bugprone-invalid-enum-default-initialization,
|
|
||||||
-bugprone-multi-level-implicit-pointer-conversion,
|
-bugprone-multi-level-implicit-pointer-conversion,
|
||||||
-bugprone-narrowing-conversions,
|
-bugprone-narrowing-conversions,
|
||||||
-bugprone-tagged-union-member-count,
|
|
||||||
-bugprone-signed-char-misuse,
|
-bugprone-signed-char-misuse,
|
||||||
-bugprone-switch-missing-default-case,
|
-bugprone-switch-missing-default-case,
|
||||||
-cert-dcl50-cpp,
|
-cert-dcl50-cpp,
|
||||||
-cert-err33-c,
|
-cert-err33-c,
|
||||||
-cert-err58-cpp,
|
-cert-err58-cpp,
|
||||||
-cert-int09-c,
|
|
||||||
-cert-oop57-cpp,
|
-cert-oop57-cpp,
|
||||||
-cert-str34-c,
|
-cert-str34-c,
|
||||||
-clang-analyzer-optin.core.EnumCastOutOfRange,
|
-clang-analyzer-optin.core.EnumCastOutOfRange,
|
||||||
-clang-analyzer-optin.cplusplus.UninitializedObject,
|
-clang-analyzer-optin.cplusplus.UninitializedObject,
|
||||||
-clang-analyzer-osx.*,
|
-clang-analyzer-osx.*,
|
||||||
-clang-analyzer-security.ArrayBound,
|
|
||||||
-clang-diagnostic-delete-abstract-non-virtual-dtor,
|
-clang-diagnostic-delete-abstract-non-virtual-dtor,
|
||||||
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
|
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
|
||||||
-clang-diagnostic-deprecated-declarations,
|
-clang-diagnostic-deprecated-declarations,
|
||||||
-clang-diagnostic-ignored-optimization-argument,
|
-clang-diagnostic-ignored-optimization-argument,
|
||||||
-clang-diagnostic-missing-designated-field-initializers,
|
|
||||||
-clang-diagnostic-missing-field-initializers,
|
-clang-diagnostic-missing-field-initializers,
|
||||||
-clang-diagnostic-shadow-field,
|
-clang-diagnostic-shadow-field,
|
||||||
-clang-diagnostic-unused-const-variable,
|
-clang-diagnostic-unused-const-variable,
|
||||||
@@ -48,7 +42,6 @@ Checks: >-
|
|||||||
-cppcoreguidelines-owning-memory,
|
-cppcoreguidelines-owning-memory,
|
||||||
-cppcoreguidelines-prefer-member-initializer,
|
-cppcoreguidelines-prefer-member-initializer,
|
||||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||||
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
|
|
||||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||||
-cppcoreguidelines-pro-type-const-cast,
|
-cppcoreguidelines-pro-type-const-cast,
|
||||||
@@ -61,13 +54,12 @@ Checks: >-
|
|||||||
-cppcoreguidelines-rvalue-reference-param-not-moved,
|
-cppcoreguidelines-rvalue-reference-param-not-moved,
|
||||||
-cppcoreguidelines-special-member-functions,
|
-cppcoreguidelines-special-member-functions,
|
||||||
-cppcoreguidelines-use-default-member-init,
|
-cppcoreguidelines-use-default-member-init,
|
||||||
-cppcoreguidelines-use-enum-class,
|
|
||||||
-cppcoreguidelines-virtual-class-destructor,
|
-cppcoreguidelines-virtual-class-destructor,
|
||||||
-fuchsia-default-arguments-calls,
|
|
||||||
-fuchsia-default-arguments-declarations,
|
|
||||||
-fuchsia-multiple-inheritance,
|
-fuchsia-multiple-inheritance,
|
||||||
-fuchsia-overloaded-operator,
|
-fuchsia-overloaded-operator,
|
||||||
-fuchsia-statically-constructed-objects,
|
-fuchsia-statically-constructed-objects,
|
||||||
|
-fuchsia-default-arguments-declarations,
|
||||||
|
-fuchsia-default-arguments-calls,
|
||||||
-google-build-using-namespace,
|
-google-build-using-namespace,
|
||||||
-google-explicit-constructor,
|
-google-explicit-constructor,
|
||||||
-google-readability-braces-around-statements,
|
-google-readability-braces-around-statements,
|
||||||
@@ -79,63 +71,49 @@ Checks: >-
|
|||||||
-llvm-else-after-return,
|
-llvm-else-after-return,
|
||||||
-llvm-header-guard,
|
-llvm-header-guard,
|
||||||
-llvm-include-order,
|
-llvm-include-order,
|
||||||
-llvm-prefer-static-over-anonymous-namespace,
|
|
||||||
-llvm-qualified-auto,
|
-llvm-qualified-auto,
|
||||||
-llvm-use-ranges,
|
|
||||||
-llvmlibc-*,
|
-llvmlibc-*,
|
||||||
-misc-const-correctness,
|
-misc-const-correctness,
|
||||||
-misc-include-cleaner,
|
-misc-include-cleaner,
|
||||||
-misc-multiple-inheritance,
|
|
||||||
-misc-no-recursion,
|
-misc-no-recursion,
|
||||||
-misc-non-private-member-variables-in-classes,
|
-misc-non-private-member-variables-in-classes,
|
||||||
-misc-override-with-different-visibility,
|
|
||||||
-misc-unused-parameters,
|
-misc-unused-parameters,
|
||||||
-misc-use-anonymous-namespace,
|
-misc-use-anonymous-namespace,
|
||||||
-misc-use-internal-linkage,
|
|
||||||
-modernize-avoid-bind,
|
-modernize-avoid-bind,
|
||||||
-modernize-avoid-variadic-functions,
|
|
||||||
-modernize-avoid-c-arrays,
|
-modernize-avoid-c-arrays,
|
||||||
-modernize-avoid-c-style-cast,
|
-modernize-concat-nested-namespaces,
|
||||||
-modernize-macro-to-enum,
|
-modernize-macro-to-enum,
|
||||||
-modernize-return-braced-init-list,
|
-modernize-return-braced-init-list,
|
||||||
-modernize-type-traits,
|
-modernize-type-traits,
|
||||||
-modernize-use-auto,
|
-modernize-use-auto,
|
||||||
-modernize-use-constraints,
|
-modernize-use-constraints,
|
||||||
-modernize-use-default-member-init,
|
-modernize-use-default-member-init,
|
||||||
-modernize-use-designated-initializers,
|
|
||||||
-modernize-use-equals-default,
|
-modernize-use-equals-default,
|
||||||
-modernize-use-integer-sign-comparison,
|
|
||||||
-modernize-use-nodiscard,
|
-modernize-use-nodiscard,
|
||||||
-modernize-use-nullptr,
|
-modernize-use-nullptr,
|
||||||
-modernize-use-ranges,
|
-modernize-use-nodiscard,
|
||||||
|
-modernize-use-nullptr,
|
||||||
-modernize-use-trailing-return-type,
|
-modernize-use-trailing-return-type,
|
||||||
-mpi-*,
|
-mpi-*,
|
||||||
-objc-*,
|
-objc-*,
|
||||||
-performance-enum-size,
|
-performance-enum-size,
|
||||||
-portability-avoid-pragma-once,
|
|
||||||
-portability-template-virtual-member-function,
|
|
||||||
-readability-ambiguous-smartptr-reset-call,
|
|
||||||
-readability-avoid-nested-conditional-operator,
|
-readability-avoid-nested-conditional-operator,
|
||||||
|
-readability-container-contains,
|
||||||
-readability-container-data-pointer,
|
-readability-container-data-pointer,
|
||||||
-readability-convert-member-functions-to-static,
|
-readability-convert-member-functions-to-static,
|
||||||
-readability-else-after-return,
|
-readability-else-after-return,
|
||||||
-readability-enum-initial-value,
|
|
||||||
-readability-function-cognitive-complexity,
|
-readability-function-cognitive-complexity,
|
||||||
-readability-implicit-bool-conversion,
|
-readability-implicit-bool-conversion,
|
||||||
-readability-isolate-declaration,
|
-readability-isolate-declaration,
|
||||||
-readability-magic-numbers,
|
-readability-magic-numbers,
|
||||||
-readability-make-member-function-const,
|
-readability-make-member-function-const,
|
||||||
-readability-math-missing-parentheses,
|
|
||||||
-readability-named-parameter,
|
-readability-named-parameter,
|
||||||
-readability-redundant-casting,
|
-readability-redundant-casting,
|
||||||
-readability-redundant-inline-specifier,
|
-readability-redundant-inline-specifier,
|
||||||
-readability-redundant-member-init,
|
-readability-redundant-member-init,
|
||||||
-readability-redundant-parentheses,
|
-readability-redundant-string-init,
|
||||||
-readability-redundant-typename,
|
|
||||||
-readability-uppercase-literal-suffix,
|
-readability-uppercase-literal-suffix,
|
||||||
-readability-use-anyofallof,
|
-readability-use-anyofallof,
|
||||||
-readability-use-std-min-max,
|
|
||||||
-readability-use-concise-preprocessor-directives,
|
|
||||||
WarningsAsErrors: '*'
|
WarningsAsErrors: '*'
|
||||||
FormatStyle: google
|
FormatStyle: google
|
||||||
CheckOptions:
|
CheckOptions:
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
f31f13994768b5b07e29624406c9b053bf4bb26e1623ac2bc1e9d4a9477502d6
|
||||||
@@ -29,7 +29,7 @@ Required fields:
|
|||||||
- **What does this implement/fix?**: Brief description of changes
|
- **What does this implement/fix?**: Brief description of changes
|
||||||
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
|
- **Types of changes**: Check ONE appropriate box (Bugfix, New feature, Breaking change, etc.)
|
||||||
- **Related issue**: Use `fixes <link>` syntax if applicable
|
- **Related issue**: Use `fixes <link>` syntax if applicable
|
||||||
- **Pull request in esphome.io**: Link if docs are needed
|
- **Pull request in esphome-docs**: Link if docs are needed
|
||||||
- **Test Environment**: Check platforms you tested on
|
- **Test Environment**: Check platforms you tested on
|
||||||
- **Example config.yaml**: Include working example YAML
|
- **Example config.yaml**: Include working example YAML
|
||||||
- **Checklist**: Verify code is tested and tests added
|
- **Checklist**: Verify code is tested and tests added
|
||||||
@@ -54,9 +54,9 @@ Required fields:
|
|||||||
|
|
||||||
- fixes https://github.com/esphome/esphome/issues/XXX
|
- fixes https://github.com/esphome/esphome/issues/XXX
|
||||||
|
|
||||||
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
|
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
||||||
|
|
||||||
- esphome/esphome.io#XXX
|
- esphome/esphome-docs#XXX
|
||||||
|
|
||||||
## Test Environment
|
## Test Environment
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ component_name:
|
|||||||
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
|
- [x] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
- [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
|
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Push and Create PR
|
## 5. Push and Create PR
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG BUILD_BASE_VERSION=2026.06.1
|
ARG BUILD_BASE_VERSION=2025.04.0
|
||||||
|
|
||||||
|
|
||||||
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
|
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
|
||||||
|
|||||||
@@ -12,9 +12,10 @@
|
|||||||
"--privileged",
|
"--privileged",
|
||||||
"-e",
|
"-e",
|
||||||
"GIT_EDITOR=code --wait"
|
"GIT_EDITOR=code --wait"
|
||||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
// uncomment and edit the path in order to pass though local USB serial to the conatiner
|
||||||
// , "--device=/dev/ttyACM0"
|
// , "--device=/dev/ttyACM0"
|
||||||
],
|
],
|
||||||
|
"appPort": 6052,
|
||||||
// if you are using avahi in the host device, uncomment these to allow the
|
// if you are using avahi in the host device, uncomment these to allow the
|
||||||
// devcontainer to find devices via mdns
|
// devcontainer to find devices via mdns
|
||||||
//"mounts": [
|
//"mounts": [
|
||||||
@@ -40,11 +41,7 @@
|
|||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"python.languageServer": "Pylance",
|
"python.languageServer": "Pylance",
|
||||||
// Use the container's pre-provisioned venv (built by the Dockerfile, outside the
|
"python.pythonPath": "/usr/bin/python3",
|
||||||
// bind-mounted workspace) rather than a ./venv that may leak in from the host and
|
|
||||||
// mismatch the container's Python. See .devcontainer/Dockerfile (esphome-venv).
|
|
||||||
"python.defaultInterpreterPath": "/home/esphome/.local/esphome-venv/bin/python",
|
|
||||||
"python.terminal.activateEnvironment": true,
|
|
||||||
"pylint.args": [
|
"pylint.args": [
|
||||||
"--rcfile=${workspaceFolder}/pyproject.toml"
|
"--rcfile=${workspaceFolder}/pyproject.toml"
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
@@ -115,4 +115,4 @@ examples/
|
|||||||
Dockerfile
|
Dockerfile
|
||||||
.git/
|
.git/
|
||||||
tests/
|
tests/
|
||||||
.?*
|
.*
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
# Normalize line endings to LF in the repository
|
# Normalize line endings to LF in the repository
|
||||||
* text eol=lf
|
* text eol=lf
|
||||||
*.png binary
|
*.png binary
|
||||||
*.gif binary
|
|
||||||
*.apng binary
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
blank_issues_enabled: false
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Report an issue with the ESPHome documentation
|
- name: Report an issue with the ESPHome documentation
|
||||||
url: https://github.com/esphome/esphome.io/issues/new/choose
|
url: https://github.com/esphome/esphome-docs/issues/new/choose
|
||||||
about: Report an issue with the ESPHome documentation.
|
about: Report an issue with the ESPHome documentation.
|
||||||
- name: Report an issue with the ESPHome web server
|
- name: Report an issue with the ESPHome web server
|
||||||
url: https://github.com/esphome/esphome-webserver/issues/new/choose
|
url: https://github.com/esphome/esphome-webserver/issues/new/choose
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
|
|
||||||
- fixes <link to issue>
|
- fixes <link to issue>
|
||||||
|
|
||||||
**Pull request in [esphome.io](https://github.com/esphome/esphome.io) with documentation (if applicable):**
|
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
||||||
|
|
||||||
- esphome/esphome.io#<esphome.io PR number goes here>
|
- esphome/esphome-docs#<esphome-docs PR number goes here>
|
||||||
|
|
||||||
## Test Environment
|
## Test Environment
|
||||||
|
|
||||||
@@ -43,4 +43,4 @@
|
|||||||
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
If user exposed functionality or configuration variables are added/changed:
|
||||||
- [ ] Documentation added/updated in [esphome.io](https://github.com/esphome/esphome.io).
|
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ inputs:
|
|||||||
description: "Version to build"
|
description: "Version to build"
|
||||||
required: true
|
required: true
|
||||||
example: "2023.12.0"
|
example: "2023.12.0"
|
||||||
|
base_os:
|
||||||
|
description: "Base OS to use"
|
||||||
|
required: false
|
||||||
|
default: "debian"
|
||||||
|
example: "debian"
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
@@ -42,7 +47,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to ghcr by digest
|
- name: Build and push to ghcr by digest
|
||||||
id: build-ghcr
|
id: build-ghcr
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -55,6 +60,7 @@ runs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
BUILD_TYPE=${{ inputs.build_type }}
|
BUILD_TYPE=${{ inputs.build_type }}
|
||||||
BUILD_VERSION=${{ inputs.version }}
|
BUILD_VERSION=${{ inputs.version }}
|
||||||
|
BUILD_OS=${{ inputs.base_os }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
@@ -67,7 +73,7 @@ runs:
|
|||||||
|
|
||||||
- name: Build and push to dockerhub by digest
|
- name: Build and push to dockerhub by digest
|
||||||
id: build-dockerhub
|
id: build-dockerhub
|
||||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILD_SUMMARY: false
|
DOCKER_BUILD_SUMMARY: false
|
||||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||||
@@ -80,6 +86,7 @@ runs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
BUILD_TYPE=${{ inputs.build_type }}
|
BUILD_TYPE=${{ inputs.build_type }}
|
||||||
BUILD_VERSION=${{ inputs.version }}
|
BUILD_VERSION=${{ inputs.version }}
|
||||||
|
BUILD_OS=${{ inputs.base_os }}
|
||||||
outputs: |
|
outputs: |
|
||||||
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
name: Cache ESP-IDF
|
|
||||||
description: >
|
|
||||||
Resolve the pinned ESP-IDF version and cache the native ESP-IDF install
|
|
||||||
(toolchains + source) at ~/.esphome-idf. Every job that installs ESP-IDF
|
|
||||||
natively (clang-tidy for IDF/Arduino and the component test batches) shares
|
|
||||||
one cache, since the install is identical (ESPHOME_IDF_DEFAULT_TARGETS
|
|
||||||
defaults to "all", so all toolchains are present regardless of the chip).
|
|
||||||
Callers must set env ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf and have the
|
|
||||||
Python venv already restored.
|
|
||||||
inputs:
|
|
||||||
framework:
|
|
||||||
description: 'Which pinned IDF version to key on: "espidf" (recommended) or "arduino".'
|
|
||||||
default: espidf
|
|
||||||
restore-only:
|
|
||||||
description: >
|
|
||||||
When "true", only restore -- never save the cache, even on dev. Use from
|
|
||||||
jobs that may not produce an ESP-IDF install (e.g. a component batch with
|
|
||||||
no esp32 target), so a partial/empty install is never written to the key.
|
|
||||||
default: "false"
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Resolve ESP-IDF version for cache key
|
|
||||||
# The native-IDF version is pinned in code, not in any file that feeds the
|
|
||||||
# other cache keys, so resolve it explicitly. Keying on it means the cache
|
|
||||||
# invalidates on a version bump (actions/cache never overwrites a key).
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
if [ "${{ inputs.framework }}" = "arduino" ]; then
|
|
||||||
version=$(python -c 'from esphome.components.esp32 import ARDUINO_FRAMEWORK_VERSION_LOOKUP as A, ARDUINO_IDF_VERSION_LOOKUP as L; print(L[A["recommended"]])')
|
|
||||||
else
|
|
||||||
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
|
|
||||||
fi
|
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
||||||
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
|
|
||||||
# shared cache (so it lives in the default-branch scope readable by all
|
|
||||||
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
|
|
||||||
# their own scope / the repo quota (e.g. on a version-bump PR).
|
|
||||||
- name: Cache ESP-IDF install (write on dev)
|
|
||||||
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
|
|
||||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-idf
|
|
||||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
|
||||||
- name: Cache ESP-IDF install (restore-only off dev)
|
|
||||||
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
|
|
||||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-idf
|
|
||||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: Cache nRF Connect SDK
|
|
||||||
description: >
|
|
||||||
Resolve the pinned sdk-nrf version and cache the native sdk-nrf install
|
|
||||||
(west workspace, Zephyr SDK toolchain, python env) at ~/.esphome-sdk-nrf.
|
|
||||||
Every job that installs sdk-nrf natively (the nrf52 clang-tidy job and,
|
|
||||||
once the component tests build natively, their batches) shares one cache.
|
|
||||||
Callers must set env ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf and have
|
|
||||||
the Python venv already restored.
|
|
||||||
inputs:
|
|
||||||
restore-only:
|
|
||||||
description: >
|
|
||||||
When "true", only restore -- never save the cache, even on dev. Use from
|
|
||||||
jobs that may not produce a complete install (e.g. a component batch
|
|
||||||
that fails mid-install), so a partial install is never written.
|
|
||||||
default: "false"
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Resolve sdk-nrf and toolchain versions for cache key
|
|
||||||
# Both versions are pinned in code, not in any file that feeds the
|
|
||||||
# other cache keys, so resolve them explicitly. Keying on them means
|
|
||||||
# the cache invalidates when either is bumped (actions/cache never
|
|
||||||
# overwrites a key).
|
|
||||||
id: version
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
version=$(python -c '
|
|
||||||
from esphome.components.nrf52 import RECOMMENDED_SDK_NRF_VERSION
|
|
||||||
from esphome.components.nrf52.framework import TOOLCHAIN_VERSION
|
|
||||||
print(f"{RECOMMENDED_SDK_NRF_VERSION}-{TOOLCHAIN_VERSION}")')
|
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
|
||||||
# Mirror cache-esp-idf: only dev-branch runs write the shared cache (so it
|
|
||||||
# lives in the default-branch scope readable by all PRs); PRs are
|
|
||||||
# restore-only and never push multi-GB artifacts into their own scope.
|
|
||||||
- name: Cache nRF Connect SDK install (write on dev)
|
|
||||||
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
|
|
||||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-sdk-nrf
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
|
|
||||||
- name: Cache nRF Connect SDK install (restore-only off dev)
|
|
||||||
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
|
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
||||||
with:
|
|
||||||
path: ~/.esphome-sdk-nrf
|
|
||||||
# yamllint disable-line rule:line-length
|
|
||||||
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
|
|
||||||
@@ -17,31 +17,16 @@ runs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
id: python
|
id: python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: ${{ inputs.python-version }}
|
python-version: ${{ inputs.python-version }}
|
||||||
- name: Restore Python virtual environment
|
- name: Restore Python virtual environment
|
||||||
id: cache-venv
|
id: cache-venv
|
||||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||||
with:
|
with:
|
||||||
path: venv
|
path: venv
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
|
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
|
||||||
- name: Set up uv
|
|
||||||
# Only needed on cache miss to populate the venv. ``uv pip install``
|
|
||||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
|
||||||
# downstream jobs rely on is preserved.
|
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
# Pull request saves land in per-PR scopes nothing else can
|
|
||||||
# reuse; dev pushes seed the shared copy instead.
|
|
||||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
|
||||||
# miss; that fetch flakes on Windows runners.
|
|
||||||
version: "0.11.15"
|
|
||||||
- name: Create Python virtual environment
|
- name: Create Python virtual environment
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
|
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -49,8 +34,8 @@ runs:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
python --version
|
python --version
|
||||||
uv pip install -r requirements.txt -r requirements_test.txt
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
uv pip install -e .
|
pip install -e .
|
||||||
- name: Create Python virtual environment
|
- name: Create Python virtual environment
|
||||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -58,5 +43,5 @@ runs:
|
|||||||
python -m venv venv
|
python -m venv venv
|
||||||
source ./venv/Scripts/activate
|
source ./venv/Scripts/activate
|
||||||
python --version
|
python --version
|
||||||
uv pip install -r requirements.txt -r requirements_test.txt
|
pip install -r requirements.txt -r requirements_test.txt
|
||||||
uv pip install -e .
|
pip install -e .
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
../AGENTS.md
|
../.ai/instructions.md
|
||||||
@@ -5,7 +5,6 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: daily
|
interval: daily
|
||||||
open-pull-requests-limit: 10
|
|
||||||
ignore:
|
ignore:
|
||||||
# Hypotehsis is only used for testing and is updated quite often
|
# Hypotehsis is only used for testing and is updated quite often
|
||||||
- dependency-name: hypothesis
|
- dependency-name: hypothesis
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ module.exports = {
|
|||||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||||
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
|
|
||||||
|
|
||||||
MANAGED_LABELS: [
|
MANAGED_LABELS: [
|
||||||
'new-component',
|
'new-component',
|
||||||
@@ -35,9 +34,6 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
DOCS_PR_PATTERNS: [
|
DOCS_PR_PATTERNS: [
|
||||||
/https:\/\/github\.com\/esphome\/esphome\.io\/pull\/\d+/,
|
|
||||||
/esphome\/esphome\.io#\d+/,
|
|
||||||
// Keep matching the old esphome-docs name during the transition period
|
|
||||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||||
/esphome\/esphome-docs#\d+/
|
/esphome\/esphome-docs#\d+/
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const fs = require('fs');
|
||||||
const { DOCS_PR_PATTERNS } = require('./constants');
|
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||||
const {
|
const {
|
||||||
COMPONENT_REGEX,
|
COMPONENT_REGEX,
|
||||||
@@ -8,31 +9,6 @@ const {
|
|||||||
} = require('../detect-tags');
|
} = require('../detect-tags');
|
||||||
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
|
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
|
||||||
|
|
||||||
// Top-level `CONFIG_SCHEMA = ...` (assignment) or `CONFIG_SCHEMA: ConfigType = ...` (annotation).
|
|
||||||
// Ruff/Black enforce exactly one space around `=` and no space before `:`,
|
|
||||||
// so we can match strictly: `CONFIG_SCHEMA ` or `CONFIG_SCHEMA:`.
|
|
||||||
const CONFIG_SCHEMA_REGEX = /^CONFIG_SCHEMA[ :]/m;
|
|
||||||
|
|
||||||
// Fetch a file's contents from the PR head SHA via the GitHub API.
|
|
||||||
// The auto-label workflow runs on `pull_request_target`, which checks out the
|
|
||||||
// base branch — files added by the PR don't exist in the workspace, so we have
|
|
||||||
// to fetch them from the head SHA. Returns null if the file can't be fetched.
|
|
||||||
async function fetchPrFileContent(github, context, path) {
|
|
||||||
try {
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
const { data } = await github.rest.repos.getContent({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
path,
|
|
||||||
ref: context.payload.pull_request.head.sha,
|
|
||||||
});
|
|
||||||
return Buffer.from(data.content, 'base64').toString('utf8');
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Failed to fetch ${path} from PR head:`, error.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Merge branch detection
|
// Strategy: Merge branch detection
|
||||||
async function detectMergeBranch(context) {
|
async function detectMergeBranch(context) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
@@ -69,72 +45,52 @@ async function detectComponentPlatforms(changedFiles, apiData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: New component detection
|
// Strategy: New component detection
|
||||||
async function detectNewComponents(github, context, prFiles) {
|
async function detectNewComponents(prFiles) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
let hasYamlLoadable = false;
|
|
||||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||||
|
|
||||||
for (const file of addedFiles) {
|
for (const file of addedFiles) {
|
||||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||||
if (!componentMatch) continue;
|
if (componentMatch) {
|
||||||
|
try {
|
||||||
labels.add('new-component');
|
const content = fs.readFileSync(file, 'utf8');
|
||||||
const content = await fetchPrFileContent(github, context, file);
|
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||||
if (content === null) {
|
labels.add('new-target-platform');
|
||||||
// Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure
|
}
|
||||||
hasYamlLoadable = true;
|
} catch (error) {
|
||||||
continue;
|
console.log(`Failed to read content of ${file}:`, error.message);
|
||||||
}
|
}
|
||||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
labels.add('new-component');
|
||||||
labels.add('new-target-platform');
|
|
||||||
}
|
|
||||||
if (CONFIG_SCHEMA_REGEX.test(content)) {
|
|
||||||
hasYamlLoadable = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { labels, hasYamlLoadable };
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: New platform detection
|
// Strategy: New platform detection
|
||||||
async function detectNewPlatforms(github, context, prFiles, apiData) {
|
async function detectNewPlatforms(prFiles, apiData) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
let hasYamlLoadable = false;
|
|
||||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||||
|
|
||||||
const platformPathPatterns = [
|
|
||||||
/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/,
|
|
||||||
/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/,
|
|
||||||
];
|
|
||||||
|
|
||||||
const removedFiles = new Set(prFiles.filter(file => file.status === 'removed').map(file => file.filename));
|
|
||||||
|
|
||||||
for (const file of addedFiles) {
|
for (const file of addedFiles) {
|
||||||
for (const re of platformPathPatterns) {
|
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||||
const match = file.match(re);
|
if (platformFileMatch) {
|
||||||
if (!match) continue;
|
const [, component, platform] = platformFileMatch;
|
||||||
const platform = match[2];
|
if (apiData.platformComponents.includes(platform)) {
|
||||||
if (!apiData.platformComponents.includes(platform)) break;
|
labels.add('new-platform');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if this is a restructure between flat and subdirectory forms (either direction):
|
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||||
// <component>/<platform>.py <-> <component>/<platform>/__init__.py
|
if (platformDirMatch) {
|
||||||
const flatEquivalent = `esphome/components/${match[1]}/${platform}.py`;
|
const [, component, platform] = platformDirMatch;
|
||||||
const subdirEquivalent = `esphome/components/${match[1]}/${platform}/__init__.py`;
|
if (apiData.platformComponents.includes(platform)) {
|
||||||
if (removedFiles.has(flatEquivalent) || removedFiles.has(subdirEquivalent)) break;
|
labels.add('new-platform');
|
||||||
|
|
||||||
labels.add('new-platform');
|
|
||||||
const content = await fetchPrFileContent(github, context, file);
|
|
||||||
if (content === null) {
|
|
||||||
// Safe default: assume YAML-loadable so needs-docs behaviour is unchanged on fetch failure
|
|
||||||
hasYamlLoadable = true;
|
|
||||||
} else if (CONFIG_SCHEMA_REGEX.test(content)) {
|
|
||||||
hasYamlLoadable = true;
|
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { labels, hasYamlLoadable };
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: Core files detection
|
// Strategy: Core files detection
|
||||||
@@ -147,9 +103,19 @@ async function detectCoreChanges(changedFiles) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: PR size detection
|
// Strategy: PR size detection
|
||||||
async function detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
|
|
||||||
|
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||||
|
labels.add('small-pr');
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalChanges <= MEDIUM_PR_THRESHOLD) {
|
||||||
|
labels.add('medium-pr');
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
const testAdditions = prFiles
|
const testAdditions = prFiles
|
||||||
.filter(file => file.filename.startsWith('tests/'))
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||||
@@ -157,24 +123,7 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, S
|
|||||||
.filter(file => file.filename.startsWith('tests/'))
|
.filter(file => file.filename.startsWith('tests/'))
|
||||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||||
|
|
||||||
const nonTestAdditions = totalAdditions - testAdditions;
|
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||||
const nonTestDeletions = totalDeletions - testDeletions;
|
|
||||||
|
|
||||||
// small/medium count churn (additions + deletions) so a balanced refactor isn't undersized.
|
|
||||||
const nonTestChurn = nonTestAdditions + nonTestDeletions;
|
|
||||||
|
|
||||||
if (nonTestChurn <= SMALL_PR_THRESHOLD) {
|
|
||||||
labels.add('small-pr');
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nonTestChurn <= MEDIUM_PR_THRESHOLD) {
|
|
||||||
labels.add('medium-pr');
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
// too-big uses net line delta (additions - deletions), matching the review message in reviews.js.
|
|
||||||
const nonTestChanges = nonTestAdditions - nonTestDeletions;
|
|
||||||
|
|
||||||
// Don't add too-big if mega-pr label is already present
|
// Don't add too-big if mega-pr label is already present
|
||||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||||
@@ -286,20 +235,19 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get base branch ref to check if deprecation already exists for the component
|
// Get PR head to fetch files from the PR branch
|
||||||
// This prevents flagging a PR that simply adds deprecation
|
const prNumber = context.payload.pull_request.number;
|
||||||
const baseRef = context.payload.pull_request.base.ref;
|
|
||||||
|
|
||||||
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
||||||
for (const component of components) {
|
for (const component of components) {
|
||||||
const initFile = `esphome/components/${component}/__init__.py`;
|
const initFile = `esphome/components/${component}/__init__.py`;
|
||||||
try {
|
try {
|
||||||
// Fetch file content from base branch using GitHub API
|
// Fetch file content from PR head using GitHub API
|
||||||
const { data: fileData } = await github.rest.repos.getContent({
|
const { data: fileData } = await github.rest.repos.getContent({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
path: initFile,
|
path: initFile,
|
||||||
ref: baseRef
|
ref: `refs/pull/${prNumber}/head`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Decode base64 content
|
// Decode base64 content
|
||||||
@@ -332,26 +280,8 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
|||||||
return { labels, deprecatedInfo };
|
return { labels, deprecatedInfo };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy: Detect when maintainers cannot modify the PR branch
|
|
||||||
function detectMaintainerAccess(context) {
|
|
||||||
const pr = context.payload.pull_request;
|
|
||||||
|
|
||||||
// Only relevant for cross-repo PRs (forks)
|
|
||||||
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pr.maintainer_can_modify) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOrgFork = pr.head.repo.owner.type === 'Organization';
|
|
||||||
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
|
|
||||||
return { isOrgFork, orgName: pr.head.repo.owner.login };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy: Requirements detection
|
// Strategy: Requirements detection
|
||||||
async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable) {
|
async function detectRequirements(allLabels, prFiles, context) {
|
||||||
const labels = new Set();
|
const labels = new Set();
|
||||||
|
|
||||||
// Check for missing tests
|
// Check for missing tests
|
||||||
@@ -359,15 +289,8 @@ async function detectRequirements(allLabels, prFiles, context, hasYamlLoadable)
|
|||||||
labels.add('needs-tests');
|
labels.add('needs-tests');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for missing docs.
|
// Check for missing docs
|
||||||
// `new-feature` (PR-body checkbox) always counts. `new-component` / `new-platform`
|
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
||||||
// only count when at least one newly added file defines a top-level CONFIG_SCHEMA,
|
|
||||||
// i.e. the new component/platform is actually loadable from YAML.
|
|
||||||
const docsEligible =
|
|
||||||
allLabels.has('new-feature') ||
|
|
||||||
((allLabels.has('new-component') || allLabels.has('new-platform')) && hasYamlLoadable);
|
|
||||||
|
|
||||||
if (docsEligible) {
|
|
||||||
const prBody = context.payload.pull_request.body || '';
|
const prBody = context.payload.pull_request.body || '';
|
||||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||||
|
|
||||||
@@ -405,6 +328,5 @@ module.exports = {
|
|||||||
detectTests,
|
detectTests,
|
||||||
detectPRTemplateCheckboxes,
|
detectPRTemplateCheckboxes,
|
||||||
detectDeprecatedComponents,
|
detectDeprecatedComponents,
|
||||||
detectMaintainerAccess,
|
|
||||||
detectRequirements
|
detectRequirements
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,10 +12,9 @@ const {
|
|||||||
detectTests,
|
detectTests,
|
||||||
detectPRTemplateCheckboxes,
|
detectPRTemplateCheckboxes,
|
||||||
detectDeprecatedComponents,
|
detectDeprecatedComponents,
|
||||||
detectMaintainerAccess,
|
|
||||||
detectRequirements
|
detectRequirements
|
||||||
} = require('./detectors');
|
} = require('./detectors');
|
||||||
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
|
const { handleReviews } = require('./reviews');
|
||||||
const { applyLabels, removeOldLabels } = require('./labels');
|
const { applyLabels, removeOldLabels } = require('./labels');
|
||||||
|
|
||||||
// Fetch API data
|
// Fetch API data
|
||||||
@@ -106,8 +105,8 @@ module.exports = async ({ github, context }) => {
|
|||||||
const [
|
const [
|
||||||
branchLabels,
|
branchLabels,
|
||||||
componentLabels,
|
componentLabels,
|
||||||
newComponentResult,
|
newComponentLabels,
|
||||||
newPlatformResult,
|
newPlatformLabels,
|
||||||
coreLabels,
|
coreLabels,
|
||||||
sizeLabels,
|
sizeLabels,
|
||||||
dashboardLabels,
|
dashboardLabels,
|
||||||
@@ -115,31 +114,22 @@ module.exports = async ({ github, context }) => {
|
|||||||
codeOwnerLabels,
|
codeOwnerLabels,
|
||||||
testLabels,
|
testLabels,
|
||||||
checkboxLabels,
|
checkboxLabels,
|
||||||
deprecatedResult,
|
deprecatedResult
|
||||||
maintainerAccess
|
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
detectMergeBranch(context),
|
detectMergeBranch(context),
|
||||||
detectComponentPlatforms(changedFiles, apiData),
|
detectComponentPlatforms(changedFiles, apiData),
|
||||||
detectNewComponents(github, context, prFiles),
|
detectNewComponents(prFiles),
|
||||||
detectNewPlatforms(github, context, prFiles, apiData),
|
detectNewPlatforms(prFiles, apiData),
|
||||||
detectCoreChanges(changedFiles),
|
detectCoreChanges(changedFiles),
|
||||||
detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
||||||
detectDashboardChanges(changedFiles),
|
detectDashboardChanges(changedFiles),
|
||||||
detectGitHubActionsChanges(changedFiles),
|
detectGitHubActionsChanges(changedFiles),
|
||||||
detectCodeOwner(github, context, changedFiles),
|
detectCodeOwner(github, context, changedFiles),
|
||||||
detectTests(changedFiles),
|
detectTests(changedFiles),
|
||||||
detectPRTemplateCheckboxes(context),
|
detectPRTemplateCheckboxes(context),
|
||||||
detectDeprecatedComponents(github, context, changedFiles),
|
detectDeprecatedComponents(github, context, changedFiles)
|
||||||
detectMaintainerAccess(context)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Extract new-component / new-platform results
|
|
||||||
const newComponentLabels = newComponentResult.labels;
|
|
||||||
const newPlatformLabels = newPlatformResult.labels;
|
|
||||||
// Eligible for needs-docs only if any newly added component or platform file
|
|
||||||
// defines a top-level CONFIG_SCHEMA (i.e. is actually loadable from YAML).
|
|
||||||
const hasYamlLoadable = newComponentResult.hasYamlLoadable || newPlatformResult.hasYamlLoadable;
|
|
||||||
|
|
||||||
// Extract deprecated component info
|
// Extract deprecated component info
|
||||||
const deprecatedLabels = deprecatedResult.labels;
|
const deprecatedLabels = deprecatedResult.labels;
|
||||||
const deprecatedInfo = deprecatedResult.deprecatedInfo;
|
const deprecatedInfo = deprecatedResult.deprecatedInfo;
|
||||||
@@ -161,7 +151,7 @@ module.exports = async ({ github, context }) => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Detect requirements based on all other labels
|
// Detect requirements based on all other labels
|
||||||
const requirementLabels = await detectRequirements(allLabels, prFiles, context, hasYamlLoadable);
|
const requirementLabels = await detectRequirements(allLabels, prFiles, context);
|
||||||
for (const label of requirementLabels) {
|
for (const label of requirementLabels) {
|
||||||
allLabels.add(label);
|
allLabels.add(label);
|
||||||
}
|
}
|
||||||
@@ -187,11 +177,8 @@ module.exports = async ({ github, context }) => {
|
|||||||
|
|
||||||
console.log('Computed labels:', finalLabels.join(', '));
|
console.log('Computed labels:', finalLabels.join(', '));
|
||||||
|
|
||||||
// Handle reviews and org fork comment
|
// Handle reviews
|
||||||
await Promise.all([
|
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||||
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
|
|
||||||
handleMaintainerAccessComment(github, context, maintainerAccess)
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Apply labels
|
// Apply labels
|
||||||
await applyLabels(github, context, finalLabels);
|
await applyLabels(github, context, finalLabels);
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "auto-label-pr",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"test": "node --test tests/*.test.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,7 @@ const {
|
|||||||
BOT_COMMENT_MARKER,
|
BOT_COMMENT_MARKER,
|
||||||
CODEOWNERS_MARKER,
|
CODEOWNERS_MARKER,
|
||||||
TOO_BIG_MARKER,
|
TOO_BIG_MARKER,
|
||||||
DEPRECATED_COMPONENT_MARKER,
|
DEPRECATED_COMPONENT_MARKER
|
||||||
ORG_FORK_MARKER
|
|
||||||
} = require('./constants');
|
} = require('./constants');
|
||||||
|
|
||||||
// Generate review messages
|
// Generate review messages
|
||||||
@@ -41,36 +40,16 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
|
|||||||
|
|
||||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||||
|
|
||||||
message +=
|
|
||||||
`Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
|
|
||||||
`this PR is on the large side `;
|
|
||||||
|
|
||||||
if (tooManyLabels && tooManyChanges) {
|
if (tooManyLabels && tooManyChanges) {
|
||||||
message +=
|
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||||
`(${nonTestChanges} line changes excluding tests, across ` +
|
|
||||||
`${originalLabelCount} different components/areas)`;
|
|
||||||
} else if (tooManyLabels) {
|
} else if (tooManyLabels) {
|
||||||
message +=
|
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||||
`(it touches ${originalLabelCount} different components/areas)`;
|
|
||||||
} else {
|
} else {
|
||||||
message += `(${nonTestChanges} line changes excluding tests)`;
|
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||||
}
|
}
|
||||||
|
|
||||||
message += `, which makes it harder for maintainers to review.\n\n`;
|
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
|
||||||
message +=
|
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
|
||||||
`Smaller, focused PRs tend to be reviewed much faster since they ` +
|
|
||||||
`fit into the short gaps between other maintainer work; large ones ` +
|
|
||||||
`often have to wait for a rare long uninterrupted block of time. ` +
|
|
||||||
`If you can break this up into smaller pieces that can be reviewed ` +
|
|
||||||
`independently, it will almost certainly land faster overall.\n\n`;
|
|
||||||
message +=
|
|
||||||
`Before putting more time in, it's also worth popping into ` +
|
|
||||||
`\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
|
|
||||||
`you scope things and flag anything already in flight.\n\n`;
|
|
||||||
message +=
|
|
||||||
`For more details (including how to split the work up), see: ` +
|
|
||||||
`https://developers.esphome.io/contributing/submitting-your-work/` +
|
|
||||||
`#how-to-approach-large-submissions`;
|
|
||||||
|
|
||||||
messages.push(message);
|
messages.push(message);
|
||||||
}
|
}
|
||||||
@@ -157,63 +136,6 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle maintainer access warning comment
|
|
||||||
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
|
|
||||||
if (!maintainerAccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
const pr_number = context.issue.number;
|
|
||||||
const prAuthor = context.payload.pull_request.user.login;
|
|
||||||
|
|
||||||
// Check if we already posted the warning (iterate pages to exit early)
|
|
||||||
let existingComment;
|
|
||||||
for await (const { data: comments } of github.paginate.iterator(
|
|
||||||
github.rest.issues.listComments,
|
|
||||||
{ owner, repo, issue_number: pr_number }
|
|
||||||
)) {
|
|
||||||
existingComment = comments.find(comment =>
|
|
||||||
comment.user.type === 'Bot' &&
|
|
||||||
comment.body && comment.body.includes(ORG_FORK_MARKER)
|
|
||||||
);
|
|
||||||
if (existingComment) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingComment) {
|
|
||||||
console.log('Maintainer access warning comment already exists, skipping');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let body;
|
|
||||||
if (maintainerAccess.isOrgFork) {
|
|
||||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
|
|
||||||
`Hey there @${prAuthor},\n` +
|
|
||||||
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
|
|
||||||
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
|
|
||||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
|
||||||
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
|
|
||||||
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
|
|
||||||
} else {
|
|
||||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
|
|
||||||
`Hey there @${prAuthor},\n` +
|
|
||||||
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
|
|
||||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
|
||||||
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: pr_number,
|
|
||||||
body
|
|
||||||
});
|
|
||||||
console.log('Created maintainer access warning comment');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
handleReviews,
|
handleReviews
|
||||||
handleMaintainerAccessComment
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
const { describe, it } = require('node:test');
|
|
||||||
const assert = require('node:assert/strict');
|
|
||||||
const { detectNewPlatforms, detectNewComponents, detectPRSize } = require('../detectors');
|
|
||||||
|
|
||||||
// Minimal GitHub API mock — only repos.getContent is called by detectNewPlatforms/detectNewComponents
|
|
||||||
// to check for CONFIG_SCHEMA in newly added files.
|
|
||||||
function makeGithub(content = '') {
|
|
||||||
return {
|
|
||||||
rest: {
|
|
||||||
repos: {
|
|
||||||
getContent: async () => ({
|
|
||||||
data: { content: Buffer.from(content).toString('base64') }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const CONTEXT = {
|
|
||||||
repo: { owner: 'esphome', repo: 'esphome' },
|
|
||||||
payload: { pull_request: { head: { sha: 'abc123' }, base: { ref: 'dev' } } }
|
|
||||||
};
|
|
||||||
|
|
||||||
const API_DATA = {
|
|
||||||
targetPlatforms: ['esp32', 'esp8266', 'rp2040'],
|
|
||||||
platformComponents: ['cover', 'sensor', 'binary_sensor', 'switch', 'light', 'fan', 'climate', 'valve']
|
|
||||||
};
|
|
||||||
|
|
||||||
const WITH_SCHEMA = 'CONFIG_SCHEMA = cv.Schema({})';
|
|
||||||
const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// detectNewPlatforms
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe('detectNewPlatforms', () => {
|
|
||||||
describe('restructure detection (no false positives)', () => {
|
|
||||||
it('flat .py -> subdir __init__.py is not a new platform', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/endstop/cover.py', status: 'removed' },
|
|
||||||
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
|
||||||
assert.equal(result.labels.size, 0);
|
|
||||||
assert.equal(result.hasYamlLoadable, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('subdir __init__.py -> flat .py is not a new platform', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'removed' },
|
|
||||||
{ filename: 'esphome/components/endstop/cover.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
|
||||||
assert.equal(result.labels.size, 0);
|
|
||||||
assert.equal(result.hasYamlLoadable, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('genuine new platforms', () => {
|
|
||||||
it('new subdir platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/my_sensor/cover/__init__.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
|
||||||
assert.ok(result.labels.has('new-platform'));
|
|
||||||
assert.equal(result.hasYamlLoadable, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new flat platform with CONFIG_SCHEMA sets new-platform and hasYamlLoadable', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, API_DATA);
|
|
||||||
assert.ok(result.labels.has('new-platform'));
|
|
||||||
assert.equal(result.hasYamlLoadable, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new platform without CONFIG_SCHEMA sets new-platform but not hasYamlLoadable', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/my_sensor/cover.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewPlatforms(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles, API_DATA);
|
|
||||||
assert.ok(result.labels.has('new-platform'));
|
|
||||||
assert.equal(result.hasYamlLoadable, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('non-platform file addition produces no labels', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/my_sensor/sensor.py', status: 'added' },
|
|
||||||
];
|
|
||||||
// Override platformComponents so 'sensor' is not a recognized platform -> no label expected.
|
|
||||||
const nonPlatformApiData = { ...API_DATA, platformComponents: ['cover'] };
|
|
||||||
const result = await detectNewPlatforms(makeGithub(WITH_SCHEMA), CONTEXT, prFiles, nonPlatformApiData);
|
|
||||||
assert.equal(result.labels.size, 0);
|
|
||||||
assert.equal(result.hasYamlLoadable, false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// detectNewComponents
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe('detectNewComponents', () => {
|
|
||||||
it('new top-level __init__.py sets new-component', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/actuator/__init__.py', status: 'added', },
|
|
||||||
];
|
|
||||||
const result = await detectNewComponents(makeGithub(WITHOUT_SCHEMA), CONTEXT, prFiles);
|
|
||||||
assert.ok(result.labels.has('new-component'));
|
|
||||||
assert.equal(result.hasYamlLoadable, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new top-level __init__.py with CONFIG_SCHEMA sets hasYamlLoadable', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/my_component/__init__.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
|
|
||||||
assert.ok(result.labels.has('new-component'));
|
|
||||||
assert.equal(result.hasYamlLoadable, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('new top-level __init__.py with IS_TARGET_PLATFORM sets new-target-platform', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/my_platform/__init__.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewComponents(makeGithub('IS_TARGET_PLATFORM = True'), CONTEXT, prFiles);
|
|
||||||
assert.ok(result.labels.has('new-component'));
|
|
||||||
assert.ok(result.labels.has('new-target-platform'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('modified __init__.py does not set new-component', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/existing/__init__.py', status: 'modified' },
|
|
||||||
];
|
|
||||||
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
|
|
||||||
assert.equal(result.labels.size, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('nested __init__.py does not set new-component', async () => {
|
|
||||||
const prFiles = [
|
|
||||||
{ filename: 'esphome/components/endstop/cover/__init__.py', status: 'added' },
|
|
||||||
];
|
|
||||||
const result = await detectNewComponents(makeGithub(WITH_SCHEMA), CONTEXT, prFiles);
|
|
||||||
assert.equal(result.labels.size, 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// detectPRSize
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe('detectPRSize', () => {
|
|
||||||
const SMALL = 30;
|
|
||||||
const MEDIUM = 100;
|
|
||||||
const TOO_BIG = 1000;
|
|
||||||
|
|
||||||
function size(prFiles, isMegaPR = false) {
|
|
||||||
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
|
||||||
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
|
||||||
return detectPRSize(prFiles, totalAdditions, totalDeletions, isMegaPR, SMALL, MEDIUM, TOO_BIG);
|
|
||||||
}
|
|
||||||
|
|
||||||
it('counts only non-test changes toward small-pr', async () => {
|
|
||||||
// 10 source + 5000 test lines -> non-test churn of 10 is still small.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 10, deletions: 0 },
|
|
||||||
{ filename: 'tests/components/foo/test.esp32-idf.yaml', additions: 5000, deletions: 0 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('small-pr'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('counts additions and deletions as churn (not net delta)', async () => {
|
|
||||||
// A balanced refactor (40 added, 40 removed) is 80 lines of churn -> medium, not small.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 40, deletions: 40 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('medium-pr'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('labels medium-pr when non-test changes exceed small threshold', async () => {
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 60, deletions: 0 },
|
|
||||||
{ filename: 'tests/components/foo/test.esp32-idf.yaml', additions: 5000, deletions: 0 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('medium-pr'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses net delta (not churn) for too-big', async () => {
|
|
||||||
// 600 added + 600 removed: 1200 churn (above too-big) but 0 net delta -> not too-big.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 600, deletions: 600 },
|
|
||||||
]);
|
|
||||||
assert.equal(labels.size, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('labels too-big when non-test changes exceed the big threshold', async () => {
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 2000, deletions: 0 },
|
|
||||||
{ filename: 'tests/components/foo/test.esp32-idf.yaml', additions: 5000, deletions: 0 },
|
|
||||||
]);
|
|
||||||
assert.ok(labels.has('too-big'));
|
|
||||||
assert.equal(labels.size, 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not label too-big when mega-pr is set', async () => {
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 2000, deletions: 0 },
|
|
||||||
], true);
|
|
||||||
assert.equal(labels.size, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('produces no size label for a large mega-pr in the gap above medium', async () => {
|
|
||||||
// Non-test changes land between MEDIUM and TOO_BIG: not small/medium, and mega-pr suppresses too-big.
|
|
||||||
const labels = await size([
|
|
||||||
{ filename: 'esphome/components/foo/foo.cpp', additions: 500, deletions: 0 },
|
|
||||||
], true);
|
|
||||||
assert.equal(labels.size, 0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -41,6 +41,7 @@ function hasCoreChanges(changedFiles) {
|
|||||||
*/
|
*/
|
||||||
function hasDashboardChanges(changedFiles) {
|
function hasDashboardChanges(changedFiles) {
|
||||||
return changedFiles.some(file =>
|
return changedFiles.some(file =>
|
||||||
|
file.startsWith('esphome/dashboard/') ||
|
||||||
file.startsWith('esphome/components/dashboard_import/')
|
file.startsWith('esphome/components/dashboard_import/')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [labeled, opened, reopened, synchronize, edited]
|
types: [labeled, opened, reopened, synchronize, edited]
|
||||||
|
|
||||||
# All PR/label/review writes are performed with the App token minted below,
|
|
||||||
# so the workflow's GITHUB_TOKEN only needs read access for checkout.
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout reads the workflow source
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
SMALL_PR_THRESHOLD: 30
|
SMALL_PR_THRESHOLD: 30
|
||||||
@@ -21,24 +20,20 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
label:
|
label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||||
with:
|
with:
|
||||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||||
# Scope the minted App token to the minimum needed by auto-label-pr/*.js.
|
|
||||||
permission-contents: read # repos.getContent for CODEOWNERS and file lookups in detectors.js
|
|
||||||
permission-issues: write # listLabelsOnIssue, addLabels, removeLabel, list/createComment
|
|
||||||
permission-pull-requests: write # pulls.listFiles, list/create/update/dismissReview
|
|
||||||
|
|
||||||
- name: Auto Label PR
|
- name: Auto Label PR
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ on:
|
|||||||
- ".github/workflows/ci-api-proto.yml"
|
- ".github/workflows/ci-api-proto.yml"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout for the PR head
|
contents: read
|
||||||
pull-requests: write # pulls.createReview / listReviews / dismissReview when generated proto files are stale
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
@@ -21,24 +21,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
- name: Set up uv
|
|
||||||
# ``--system`` (below) installs into the setup-python interpreter;
|
|
||||||
# no venv is created or restored by this workflow.
|
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
# Pull-request-only workflow: a save could never be shared and
|
|
||||||
# would only consume quota.
|
|
||||||
save-cache: "false"
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
|
||||||
# miss; that fetch flakes on Windows runners.
|
|
||||||
version: "0.11.15"
|
|
||||||
|
|
||||||
- name: Install apt dependencies
|
- name: Install apt dependencies
|
||||||
run: |
|
run: |
|
||||||
@@ -47,7 +34,7 @@ jobs:
|
|||||||
sudo apt install -y protobuf-compiler
|
sudo apt install -y protobuf-compiler
|
||||||
protoc --version
|
protoc --version
|
||||||
- name: Install python dependencies
|
- name: Install python dependencies
|
||||||
run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt
|
run: pip install aioesphomeapi -c requirements.txt -r requirements_dev.txt
|
||||||
- name: Generate files
|
- name: Generate files
|
||||||
run: script/api_protobuf/api_protobuf.py
|
run: script/api_protobuf/api_protobuf.py
|
||||||
- name: Check for changes
|
- name: Check for changes
|
||||||
@@ -60,7 +47,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
- if: failure()
|
- if: failure()
|
||||||
name: Review PR
|
name: Review PR
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
await github.rest.pulls.createReview({
|
await github.rest.pulls.createReview({
|
||||||
@@ -75,7 +62,7 @@ jobs:
|
|||||||
run: git diff
|
run: git diff
|
||||||
- if: failure()
|
- if: failure()
|
||||||
name: Archive artifacts
|
name: Archive artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: generated-proto-files
|
name: generated-proto-files
|
||||||
path: |
|
path: |
|
||||||
@@ -83,7 +70,7 @@ jobs:
|
|||||||
esphome/components/api/api_pb2_service.*
|
esphome/components/api/api_pb2_service.*
|
||||||
- if: success()
|
- if: success()
|
||||||
name: Dismiss review
|
name: Dismiss review
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
let reviews = await github.rest.pulls.listReviews({
|
let reviews = await github.rest.pulls.listReviews({
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Clang-tidy Hash CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".clang-tidy"
|
||||||
|
- "platformio.ini"
|
||||||
|
- "requirements_dev.txt"
|
||||||
|
- "sdkconfig.defaults"
|
||||||
|
- ".clang-tidy.hash"
|
||||||
|
- "script/clang_tidy_hash.py"
|
||||||
|
- ".github/workflows/ci-clang-tidy-hash.yml"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
verify-hash:
|
||||||
|
name: Verify clang-tidy hash
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
|
with:
|
||||||
|
python-version: "3.11"
|
||||||
|
|
||||||
|
- name: Verify hash
|
||||||
|
run: |
|
||||||
|
python script/clang_tidy_hash.py --verify
|
||||||
|
|
||||||
|
- if: failure()
|
||||||
|
name: Show hash details
|
||||||
|
run: |
|
||||||
|
python script/clang_tidy_hash.py
|
||||||
|
echo "## Job Failed" | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
name: Request changes
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
await github.rest.pulls.createReview({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
event: 'REQUEST_CHANGES',
|
||||||
|
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||||
|
})
|
||||||
|
|
||||||
|
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
name: Dismiss review
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
let reviews = await github.rest.pulls.listReviews({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo
|
||||||
|
});
|
||||||
|
for (let review of reviews.data) {
|
||||||
|
if (review.user.login === 'github-actions[bot]' && review.state === 'CHANGES_REQUESTED') {
|
||||||
|
await github.rest.pulls.dismissReview({
|
||||||
|
pull_number: context.issue.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
review_id: review.id,
|
||||||
|
message: 'Clang-tidy hash now matches configuration.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-173
@@ -1,41 +1,29 @@
|
|||||||
---
|
---
|
||||||
name: CI for docker images
|
name: CI for docker images
|
||||||
|
|
||||||
# Only run on PRs that touch the docker image, its build inputs, or any code
|
# Only run when docker paths change
|
||||||
# whose toolchain the compile smoke test exercises (core + target platforms).
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
|
branches: [dev, beta, release]
|
||||||
|
paths:
|
||||||
|
- "docker/**"
|
||||||
|
- ".github/workflows/ci-docker.yml"
|
||||||
|
- "requirements*.txt"
|
||||||
|
- "platformio.ini"
|
||||||
|
- "script/platformio_install_deps.py"
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
# Docker image and its build inputs.
|
|
||||||
- "docker/**"
|
- "docker/**"
|
||||||
- ".github/workflows/ci-docker.yml"
|
- ".github/workflows/ci-docker.yml"
|
||||||
- "requirements*.txt"
|
- "requirements*.txt"
|
||||||
- "pyproject.toml"
|
|
||||||
- "platformio.ini"
|
- "platformio.ini"
|
||||||
- "esphome/idf_component.yml"
|
|
||||||
- "script/platformio_install_deps.py"
|
- "script/platformio_install_deps.py"
|
||||||
# Core, build pipeline, toolchain, and target-platform changes can change
|
|
||||||
# how a toolchain is set up or built, so re-run the per-toolchain compile
|
|
||||||
# smoke test when they change.
|
|
||||||
- "esphome/core/**"
|
|
||||||
- "esphome/writer.py"
|
|
||||||
- "esphome/build_gen/**"
|
|
||||||
- "esphome/espidf/**"
|
|
||||||
- "esphome/platformio/**"
|
|
||||||
- "esphome/components/bk72xx/**"
|
|
||||||
- "esphome/components/esp32/**"
|
|
||||||
- "esphome/components/esp8266/**"
|
|
||||||
- "esphome/components/host/**"
|
|
||||||
- "esphome/components/libretiny/**"
|
|
||||||
- "esphome/components/ln882x/**"
|
|
||||||
- "esphome/components/nrf52/**"
|
|
||||||
- "esphome/components/rp2040/**"
|
|
||||||
- "esphome/components/rtl87xx/**"
|
|
||||||
- "esphome/components/zephyr/**"
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout only
|
contents: read
|
||||||
|
packages: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# yamllint disable-line rule:line-length
|
# yamllint disable-line rule:line-length
|
||||||
@@ -46,9 +34,6 @@ jobs:
|
|||||||
check-docker:
|
check-docker:
|
||||||
name: Build docker containers
|
name: Build docker containers
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
permissions:
|
|
||||||
contents: read # actions/checkout to load Dockerfile and build context
|
|
||||||
packages: write # push branch-tagged images to ghcr.io for local testing
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -57,161 +42,23 @@ jobs:
|
|||||||
- "ha-addon"
|
- "ha-addon"
|
||||||
- "docker"
|
- "docker"
|
||||||
# - "lint"
|
# - "lint"
|
||||||
outputs:
|
|
||||||
tag: ${{ steps.tag.outputs.tag }}
|
|
||||||
push: ${{ steps.tag.outputs.push }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Determine tag and whether to push
|
- name: Set TAG
|
||||||
id: tag
|
|
||||||
run: |
|
run: |
|
||||||
# Sanitize the branch name into a valid docker tag: replace invalid
|
echo "TAG=check" >> $GITHUB_ENV
|
||||||
# characters, ensure the first character is valid (tags must start
|
|
||||||
# with [A-Za-z0-9_]), and cap the length at 128 characters.
|
|
||||||
branch="${{ github.head_ref || github.ref_name }}"
|
|
||||||
tag="${branch//[^a-zA-Z0-9_.-]/-}"
|
|
||||||
case "$tag" in
|
|
||||||
[a-zA-Z0-9_]*) ;;
|
|
||||||
*) tag="pr-${tag}" ;;
|
|
||||||
esac
|
|
||||||
tag="${tag:0:128}"
|
|
||||||
echo "tag=${tag}" >> "$GITHUB_OUTPUT"
|
|
||||||
# Only push branch images for same-repo pull requests. Push events
|
|
||||||
# only fire for dev/beta/release, whose images are owned by the
|
|
||||||
# release pipeline -- never overwrite those from here.
|
|
||||||
if [ "${{ github.event_name }}" = "pull_request" ] \
|
|
||||||
&& [ "${{ github.repository }}" = "esphome/esphome" ] \
|
|
||||||
&& [ "${{ github.event.pull_request.head.repo.full_name }}" = "esphome/esphome" ]; then
|
|
||||||
echo "push=true" >> "$GITHUB_OUTPUT"
|
|
||||||
else
|
|
||||||
echo "push=false" >> "$GITHUB_OUTPUT"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Log in to the GitHub container registry
|
|
||||||
if: steps.tag.outputs.push == 'true'
|
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: |
|
run: |
|
||||||
docker/build.py \
|
docker/build.py \
|
||||||
--tag "${{ steps.tag.outputs.tag }}" \
|
--tag "${TAG}" \
|
||||||
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
|
--arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
|
||||||
--build-type "${{ matrix.build_type }}" \
|
--build-type "${{ matrix.build_type }}" \
|
||||||
--registry ghcr \
|
build
|
||||||
build ${{ steps.tag.outputs.push == 'true' && '--push --no-cache-to' || '' }} ${{ (matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker') && '--load' || '' }}
|
|
||||||
|
|
||||||
# The amd64 "docker" image is also loaded locally (above) and handed to
|
|
||||||
# compile-test as an artifact, so the smoke test reuses this build instead
|
|
||||||
# of building the image a second time. Using an artifact (rather than the
|
|
||||||
# pushed image) keeps it working for fork PRs, which never push to ghcr.io.
|
|
||||||
- name: Export image for compile-test
|
|
||||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
|
||||||
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz
|
|
||||||
|
|
||||||
- name: Upload compile-test image artifact
|
|
||||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
# The tar is already gzipped, so upload it as-is. archive: false skips
|
|
||||||
# the redundant zip and makes the file name the artifact name (the
|
|
||||||
# `name` input is ignored in that mode).
|
|
||||||
path: compile-test-image.tar.gz
|
|
||||||
retention-days: 1
|
|
||||||
archive: false
|
|
||||||
|
|
||||||
manifest:
|
|
||||||
name: Push ${{ matrix.build_type }} manifest to ghcr.io
|
|
||||||
needs: [check-docker]
|
|
||||||
if: needs.check-docker.outputs.push == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read # actions/checkout to run docker/build.py
|
|
||||||
packages: write # buildx imagetools writes the multi-arch tag to ghcr.io
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
build_type:
|
|
||||||
- "ha-addon"
|
|
||||||
- "docker"
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
- name: Set up Python
|
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
|
||||||
with:
|
|
||||||
python-version: "3.12"
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
|
||||||
|
|
||||||
- name: Log in to the GitHub container registry
|
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Create and push manifest
|
|
||||||
run: |
|
|
||||||
docker/build.py \
|
|
||||||
--tag "${{ needs.check-docker.outputs.tag }}" \
|
|
||||||
--build-type "${{ matrix.build_type }}" \
|
|
||||||
--registry ghcr \
|
|
||||||
manifest
|
|
||||||
|
|
||||||
# Smoke-test the built image by compiling one minimal config per target
|
|
||||||
# platform / toolchain. This catches missing system dependencies in the image
|
|
||||||
# that only surface when a given toolchain is downloaded and run. The image is
|
|
||||||
# the amd64 "docker" build produced by check-docker (shared as an artifact).
|
|
||||||
compile-test:
|
|
||||||
name: Compile ${{ matrix.id }}
|
|
||||||
needs: check-docker
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
contents: read # actions/checkout to load the test configs
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
# Cap concurrency so this smoke test doesn't hog all the shared runners.
|
|
||||||
max-parallel: 2
|
|
||||||
matrix:
|
|
||||||
# One entry per distinct toolchain. ESP32 variants (c3/c6/s2/s3/p4)
|
|
||||||
# share a toolchain bundle, so esp32 is exercised on the base variant
|
|
||||||
# across the full framework x toolchain cross-product (arduino/esp-idf
|
|
||||||
# framework, each built with the platformio and native esp-idf
|
|
||||||
# toolchains) so both toolchains stay covered regardless of which one is
|
|
||||||
# the default.
|
|
||||||
id:
|
|
||||||
- esp8266-arduino
|
|
||||||
- esp32-arduino-platformio
|
|
||||||
- esp32-arduino-esp-idf
|
|
||||||
- esp32-idf-platformio
|
|
||||||
- esp32-idf-esp-idf
|
|
||||||
- rp2040-arduino
|
|
||||||
- bk72xx-arduino
|
|
||||||
- rtl87xx-arduino
|
|
||||||
- ln882x-arduino
|
|
||||||
- nrf52
|
|
||||||
- host
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
- name: Download image artifact
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: compile-test-image.tar.gz
|
|
||||||
- name: Load image
|
|
||||||
run: docker load --input compile-test-image.tar.gz
|
|
||||||
- name: Compile ${{ matrix.id }}
|
|
||||||
run: |
|
|
||||||
docker run --rm \
|
|
||||||
-v "${{ github.workspace }}/docker/test_configs:/config" \
|
|
||||||
"ghcr.io/esphome/esphome-amd64:${{ needs.check-docker.outputs.tag }}" \
|
|
||||||
compile "${{ matrix.id }}.yaml"
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
name: CI - GitHub Scripts
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [dev, beta, release]
|
|
||||||
paths:
|
|
||||||
- ".github/scripts/**"
|
|
||||||
- ".github/workflows/ci-github-scripts.yml"
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- ".github/scripts/**"
|
|
||||||
- ".github/workflows/ci-github-scripts.yml"
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-auto-label-pr:
|
|
||||||
name: Test auto-label-pr scripts
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code from GitHub
|
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
working-directory: .github/scripts/auto-label-pr
|
|
||||||
run: npm test
|
|
||||||
@@ -7,9 +7,9 @@ on:
|
|||||||
types: [completed]
|
types: [completed]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout of the base repo at the PR's target branch
|
contents: read
|
||||||
pull-requests: write # gh api to look up the PR by head SHA and post/update the memory-impact comment
|
pull-requests: write
|
||||||
actions: read # gh run download for the memory-analysis artifacts produced by the CI workflow run
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
memory-impact-comment:
|
memory-impact-comment:
|
||||||
@@ -49,7 +49,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Check out code from base repository
|
- name: Check out code from base repository
|
||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
# Always check out from the base repository (esphome/esphome), never from forks
|
# Always check out from the base repository (esphome/esphome), never from forks
|
||||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
if: steps.pr.outputs.skip != 'true'
|
if: steps.pr.outputs.skip != 'true'
|
||||||
uses: ./.github/actions/restore-python
|
uses: ./.github/actions/restore-python
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
cache-key: ${{ hashFiles('.cache-key') }}
|
cache-key: ${{ hashFiles('.cache-key') }}
|
||||||
|
|
||||||
- name: Download memory analysis artifacts
|
- name: Download memory analysis artifacts
|
||||||
|
|||||||
+154
-536
File diff suppressed because it is too large
Load Diff
@@ -1,72 +0,0 @@
|
|||||||
name: Close PR From Fork Default Branch
|
|
||||||
|
|
||||||
on:
|
|
||||||
# pull_request_target is required so we have permission to comment and close PRs from forks.
|
|
||||||
pull_request_target:
|
|
||||||
types: [opened, reopened]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write # pulls.update to close the PR opened from a fork's default branch
|
|
||||||
issues: write # issues.createComment to explain to the contributor why the PR was closed
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close:
|
|
||||||
name: Close PR opened from fork's default branch
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: >-
|
|
||||||
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
|
|
||||||
&& github.event.pull_request.head.ref == github.event.repository.default_branch
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { owner, repo } = context.repo;
|
|
||||||
const prNumber = context.payload.pull_request.number;
|
|
||||||
const author = context.payload.pull_request.user.login;
|
|
||||||
const defaultBranch = context.payload.repository.default_branch;
|
|
||||||
const headRepo = context.payload.pull_request.head.repo.full_name;
|
|
||||||
|
|
||||||
const body = [
|
|
||||||
`Hi @${author}, thanks for opening a pull request! :tada:`,
|
|
||||||
``,
|
|
||||||
`It looks like this PR was opened from the \`${defaultBranch}\` branch of your fork (\`${headRepo}\`), which is the same name as this repository's default branch. Working directly on \`${defaultBranch}\` in your fork causes a few problems:`,
|
|
||||||
``,
|
|
||||||
`- Your fork's \`${defaultBranch}\` branch will permanently diverge from \`esphome/esphome:${defaultBranch}\`, making it hard to keep your fork up to date.`,
|
|
||||||
`- Any additional commits you push to \`${defaultBranch}\` will be added to this PR, so you can't easily work on multiple changes at once.`,
|
|
||||||
`- Pushing maintainer fixes to your branch is awkward, since it means committing directly to your fork's default branch.`,
|
|
||||||
`- It makes local collaboration painful — \`${defaultBranch}\` in a checkout becomes ambiguous between upstream and your fork, and maintainers end up with naming collisions when fetching your branch.`,
|
|
||||||
``,
|
|
||||||
`Please re-open this as a new PR from a dedicated feature branch. The usual flow looks like:`,
|
|
||||||
``,
|
|
||||||
`\`\`\`bash`,
|
|
||||||
`# Make sure your fork's ${defaultBranch} is up to date with upstream`,
|
|
||||||
`git remote add upstream https://github.com/${owner}/${repo}.git # if you haven't already`,
|
|
||||||
`git fetch upstream`,
|
|
||||||
`git checkout ${defaultBranch}`,
|
|
||||||
`git reset --hard upstream/${defaultBranch}`,
|
|
||||||
`git push --force-with-lease origin ${defaultBranch}`,
|
|
||||||
``,
|
|
||||||
`# Create a new branch for your change and cherry-pick / re-apply your commits there`,
|
|
||||||
`git checkout -b my-feature-branch upstream/${defaultBranch}`,
|
|
||||||
`# ...re-apply your changes, then:`,
|
|
||||||
`git push origin my-feature-branch`,
|
|
||||||
`\`\`\``,
|
|
||||||
``,
|
|
||||||
`Then open a new pull request from \`my-feature-branch\` into \`${owner}/${repo}:${defaultBranch}\`.`,
|
|
||||||
``,
|
|
||||||
`Closing this PR for now — sorry for the friction, and thanks again for contributing! :heart:`,
|
|
||||||
].join('\n');
|
|
||||||
|
|
||||||
await github.rest.issues.createComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: prNumber,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
|
|
||||||
await github.rest.pulls.update({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
pull_number: prNumber,
|
|
||||||
state: 'closed',
|
|
||||||
});
|
|
||||||
@@ -15,9 +15,9 @@ on:
|
|||||||
- beta
|
- beta
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write # issues.addLabels / removeLabel to manage the 'code-owner-approved' label on the PR
|
issues: write
|
||||||
pull-requests: read # listReviews to determine whether a codeowner has approved
|
pull-requests: read
|
||||||
contents: read # actions/checkout to read CODEOWNERS and the shared codeowners.js helper
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
codeowner-approved:
|
codeowner-approved:
|
||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
sparse-checkout: |
|
sparse-checkout: |
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
CODEOWNERS
|
CODEOWNERS
|
||||||
|
|
||||||
- name: Check codeowner approval and update label
|
- name: Check codeowner approval and update label
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -17,10 +17,9 @@ on:
|
|||||||
- release
|
- release
|
||||||
- beta
|
- beta
|
||||||
|
|
||||||
# PR/review writes (requestReviewers, issues.createComment) are performed with the App token minted below,
|
|
||||||
# so the workflow's GITHUB_TOKEN only needs read access for checkout.
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout to read CODEOWNERS and the shared codeowners.js helper
|
pull-requests: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
request-codeowner-reviews:
|
request-codeowner-reviews:
|
||||||
@@ -29,24 +28,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base branch
|
- name: Checkout base branch
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.base.sha }}
|
ref: ${{ github.event.pull_request.base.sha }}
|
||||||
|
|
||||||
- name: Generate a token
|
|
||||||
id: generate-token
|
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
||||||
with:
|
|
||||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
|
||||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
|
||||||
# Scope the minted App token to the minimum needed by the github-script step below.
|
|
||||||
permission-pull-requests: write # pulls.listFiles, pulls.get, pulls.listReviews, pulls.requestReviewers
|
|
||||||
permission-issues: write # issues.listComments and issues.createComment (PR comments use the issues API)
|
|
||||||
|
|
||||||
- name: Request reviews from component codeowners
|
- name: Request reviews from component codeowners
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
|
||||||
script: |
|
script: |
|
||||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "30 18 * * 4"
|
- cron: "30 18 * * 4"
|
||||||
|
|
||||||
# Deny by default; the analyze job opts in to exactly what it needs.
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze (${{ matrix.language }})
|
name: Analyze (${{ matrix.language }})
|
||||||
@@ -29,10 +26,15 @@ jobs:
|
|||||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||||
permissions:
|
permissions:
|
||||||
security-events: write # upload CodeQL SARIF results to the Code Scanning API
|
# required for all workflows
|
||||||
packages: read # fetch internal or private CodeQL query packs
|
security-events: write
|
||||||
actions: read # required by codeql-action when run from a private repo
|
|
||||||
contents: read # actions/checkout to scan the repository
|
# required to fetch internal or private CodeQL packs
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
# only required for workflows in private repositories
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
@@ -52,11 +54,11 @@ jobs:
|
|||||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -84,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
@@ -4,29 +4,20 @@ on:
|
|||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
# All API calls (pulls.listFiles + issues.{list,create,update}Comment) are performed with
|
permissions:
|
||||||
# the App token minted below, so the workflow's GITHUB_TOKEN does not need any scopes.
|
contents: read # Needed to fetch PR details
|
||||||
permissions: {}
|
issues: write # Needed to create and update comments (PR comments are managed via the issues REST API)
|
||||||
|
pull-requests: write # also needed?
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
external-comment:
|
external-comment:
|
||||||
name: External component comment
|
name: External component comment
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
|
||||||
id: generate-token
|
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
||||||
with:
|
|
||||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
|
||||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
|
||||||
# pulls.listFiles + issues.{list,create,update}Comment on PRs. For PR resources
|
|
||||||
# the issues.*Comment APIs require the pull-requests scope, not issues.
|
|
||||||
permission-pull-requests: write
|
|
||||||
|
|
||||||
- name: Add external component comment
|
- name: Add external component comment
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
// Generate external component usage instructions
|
// Generate external component usage instructions
|
||||||
function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) {
|
function generateExternalComponentInstructions(prNumber, componentNames, owner, repo) {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ on:
|
|||||||
types: [labeled]
|
types: [labeled]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write # issues.createComment to mention component codeowners on the newly labelled issue
|
issues: write
|
||||||
contents: read # repos.getContent to fetch CODEOWNERS from the default branch
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
notify-codeowners:
|
notify-codeowners:
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Notify codeowners for component issues
|
- name: Notify codeowners for component issues
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ on:
|
|||||||
- cron: "30 0 * * *" # Run daily at 00:30 UTC
|
- cron: "30 0 * * *" # Run daily at 00:30 UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
# Deny by default; the lock job opts in to exactly what the reusable workflow needs.
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lock:
|
lock:
|
||||||
permissions:
|
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||||
issues: write # issues.lock on closed issues
|
|
||||||
pull-requests: write # issues.lock on closed pull requests
|
|
||||||
uses: esphome/workflows/.github/workflows/lock.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0
|
|
||||||
|
|||||||
@@ -3,22 +3,19 @@ name: PR Title Check
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, edited, synchronize, reopened]
|
types: [opened, edited, synchronize, reopened]
|
||||||
branches-ignore:
|
|
||||||
- release
|
|
||||||
- beta
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout to load detect-tags.js
|
contents: read
|
||||||
pull-requests: read # pulls.listFiles to map changed files to component/core/dashboard/ci tags
|
pull-requests: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Validate PR title
|
name: Validate PR title
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const {
|
const {
|
||||||
@@ -29,11 +26,10 @@ jobs:
|
|||||||
} = require('./.github/scripts/detect-tags.js');
|
} = require('./.github/scripts/detect-tags.js');
|
||||||
|
|
||||||
const title = context.payload.pull_request.title;
|
const title = context.payload.pull_request.title;
|
||||||
const user = context.payload.pull_request.user;
|
const author = context.payload.pull_request.user.login;
|
||||||
|
|
||||||
// Skip bot PRs (e.g. dependabot, esphome[bot] device-class sync) -
|
// Skip bot PRs (e.g. dependabot) - they have their own title format
|
||||||
// they have their own title formats.
|
if (author === 'dependabot[bot]') {
|
||||||
if (user.type === 'Bot') {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,15 +65,14 @@ jobs:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for MDX syntax characters not wrapped in backticks.
|
// Check for angle brackets not wrapped in backticks.
|
||||||
// Astro docs MDX treats bare `<` as JSX component opening tags and
|
// Astro docs MDX treats bare < as JSX component opening tags.
|
||||||
// bare `{` as JS expressions, so both must be escaped in changelog entries.
|
|
||||||
const stripped = title.replace(/`[^`]*`/g, '');
|
const stripped = title.replace(/`[^`]*`/g, '');
|
||||||
if (/[<>{}]/.test(stripped)) {
|
if (/[<>]/.test(stripped)) {
|
||||||
core.setFailed(
|
core.setFailed(
|
||||||
'PR title contains `<`, `>`, `{`, or `}` not wrapped in backticks.\n' +
|
'PR title contains `<` or `>` not wrapped in backticks.\n' +
|
||||||
'Astro docs MDX interprets bare `<` as JSX components and bare `{` as JS expressions.\n' +
|
'Astro docs MDX interprets bare `<` as JSX components.\n' +
|
||||||
'Please wrap these characters with backticks, e.g.: [component] Add `<feature>` support'
|
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
- cron: "0 2 * * *"
|
- cron: "0 2 * * *"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout for all jobs; deploy jobs add their own scopes when they need to write
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
init:
|
init:
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Get tag
|
- name: Get tag
|
||||||
id: tag
|
id: tag
|
||||||
# yamllint disable rule:line-length
|
# yamllint disable rule:line-length
|
||||||
@@ -57,12 +57,12 @@ jobs:
|
|||||||
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
|
if: github.repository == 'esphome/esphome' && github.event_name == 'release'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout to build the sdist/wheel
|
contents: read
|
||||||
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.x"
|
python-version: "3.x"
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
pip3 install build
|
pip3 install build
|
||||||
python3 -m build
|
python3 -m build
|
||||||
- name: Publish
|
- name: Publish
|
||||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||||
with:
|
with:
|
||||||
skip-existing: true
|
skip-existing: true
|
||||||
|
|
||||||
@@ -78,8 +78,8 @@ jobs:
|
|||||||
name: Build ESPHome ${{ matrix.platform.arch }}
|
name: Build ESPHome ${{ matrix.platform.arch }}
|
||||||
if: github.repository == 'esphome/esphome'
|
if: github.repository == 'esphome/esphome'
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout to load Dockerfile and build context
|
contents: read
|
||||||
packages: write # docker/login-action + build-push-action push image digests to ghcr.io
|
packages: write
|
||||||
runs-on: ${{ matrix.platform.os }}
|
runs-on: ${{ matrix.platform.os }}
|
||||||
needs: [init]
|
needs: [init]
|
||||||
strategy:
|
strategy:
|
||||||
@@ -92,22 +92,22 @@ jobs:
|
|||||||
os: "ubuntu-24.04-arm"
|
os: "ubuntu-24.04-arm"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.11"
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Log in to docker hub
|
- name: Log in to docker hub
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -138,7 +138,7 @@ jobs:
|
|||||||
# version: ${{ needs.init.outputs.tag }}
|
# version: ${{ needs.init.outputs.tag }}
|
||||||
|
|
||||||
- name: Upload digests
|
- name: Upload digests
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.platform.arch }}
|
name: digests-${{ matrix.platform.arch }}
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
@@ -152,8 +152,8 @@ jobs:
|
|||||||
- deploy-docker
|
- deploy-docker
|
||||||
if: github.repository == 'esphome/esphome'
|
if: github.repository == 'esphome/esphome'
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # actions/checkout to load Dockerfile and build context
|
contents: read
|
||||||
packages: write # docker/login-action + build-push-action push image digests to ghcr.io
|
packages: write
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -168,7 +168,7 @@ jobs:
|
|||||||
- ghcr
|
- ghcr
|
||||||
- dockerhub
|
- dockerhub
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
@@ -178,17 +178,17 @@ jobs:
|
|||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Log in to docker hub
|
- name: Log in to docker hub
|
||||||
if: matrix.registry == 'dockerhub'
|
if: matrix.registry == 'dockerhub'
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||||
- name: Log in to the GitHub container registry
|
- name: Log in to the GitHub container registry
|
||||||
if: matrix.registry == 'ghcr'
|
if: matrix.registry == 'ghcr'
|
||||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -212,6 +212,72 @@ jobs:
|
|||||||
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
|
docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
|
||||||
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
|
$(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
|
||||||
|
|
||||||
|
deploy-ha-addon-repo:
|
||||||
|
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- init
|
||||||
|
- deploy-manifest
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||||
|
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||||
|
owner: esphome
|
||||||
|
repositories: home-assistant-addon
|
||||||
|
|
||||||
|
- name: Trigger Workflow
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
script: |
|
||||||
|
let description = "ESPHome";
|
||||||
|
if (context.eventName == "release") {
|
||||||
|
description = ${{ toJSON(github.event.release.body) }};
|
||||||
|
}
|
||||||
|
github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: "esphome",
|
||||||
|
repo: "home-assistant-addon",
|
||||||
|
workflow_id: "bump-version.yml",
|
||||||
|
ref: "main",
|
||||||
|
inputs: {
|
||||||
|
version: "${{ needs.init.outputs.tag }}",
|
||||||
|
content: description
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
deploy-esphome-schema:
|
||||||
|
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [init]
|
||||||
|
environment: ${{ needs.init.outputs.deploy_env }}
|
||||||
|
steps:
|
||||||
|
- name: Generate a token
|
||||||
|
id: generate-token
|
||||||
|
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||||
|
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||||
|
owner: esphome
|
||||||
|
repositories: esphome-schema
|
||||||
|
|
||||||
|
- name: Trigger Workflow
|
||||||
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
script: |
|
||||||
|
github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: "esphome",
|
||||||
|
repo: "esphome-schema",
|
||||||
|
workflow_id: "generate-schemas.yml",
|
||||||
|
ref: "main",
|
||||||
|
inputs: {
|
||||||
|
version: "${{ needs.init.outputs.tag }}",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
version-notifier:
|
version-notifier:
|
||||||
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -221,20 +287,19 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||||
with:
|
with:
|
||||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||||
owner: esphome
|
owner: esphome
|
||||||
repositories: version-notifier
|
repositories: version-notifier
|
||||||
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
|
|
||||||
|
|
||||||
- name: Trigger Workflow
|
- name: Trigger Workflow
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
await github.rest.actions.createWorkflowDispatch({
|
github.rest.actions.createWorkflowDispatch({
|
||||||
owner: "esphome",
|
owner: "esphome",
|
||||||
repo: "version-notifier",
|
repo: "version-notifier",
|
||||||
workflow_id: "notify.yml",
|
workflow_id: "notify.yml",
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
issues: write # actions/stale labels, comments on, and closes stale issues
|
issues: write
|
||||||
pull-requests: write # actions/stale labels, comments on, and closes stale pull requests
|
pull-requests: write
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: lock
|
group: lock
|
||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Stale
|
- name: Stale
|
||||||
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
with:
|
with:
|
||||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
|
|||||||
@@ -2,32 +2,30 @@ name: Status check labels
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
types: [labeled, unlabeled]
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: read # issues.listLabelsOnIssue to detect blocking labels (needs-docs, merge-after-release, chained-pr)
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check:
|
check:
|
||||||
name: Check blocking labels
|
name: Check ${{ matrix.label }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
label:
|
||||||
|
- needs-docs
|
||||||
|
- merge-after-release
|
||||||
|
- chained-pr
|
||||||
steps:
|
steps:
|
||||||
- name: Check for blocking labels
|
- name: Check for ${{ matrix.label }} label
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
|
||||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||||
owner: context.repo.owner,
|
owner: context.repo.owner,
|
||||||
repo: context.repo.repo,
|
repo: context.repo.repo,
|
||||||
issue_number: context.issue.number
|
issue_number: context.issue.number
|
||||||
});
|
});
|
||||||
const labelNames = labels.map(l => l.name);
|
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||||
const found = blockingLabels.filter(bl => labelNames.includes(bl));
|
if (hasLabel) {
|
||||||
if (found.length > 0) {
|
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||||
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,94 +6,42 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: "45 6 * * *"
|
- cron: "45 6 * * *"
|
||||||
|
|
||||||
# Repo writes (branch push, PR open) happen via the App token minted below,
|
|
||||||
# so the workflow's GITHUB_TOKEN does not need any write scopes.
|
|
||||||
permissions:
|
|
||||||
contents: read # actions/checkout for this repo and home-assistant/core
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync:
|
sync:
|
||||||
name: Sync Device Classes
|
name: Sync Device Classes
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.repository == 'esphome/esphome'
|
if: github.repository == 'esphome/esphome'
|
||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
|
||||||
id: generate-token
|
|
||||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
|
||||||
with:
|
|
||||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
|
||||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
|
||||||
# Scope the minted App token to the minimum needed by peter-evans/create-pull-request.
|
|
||||||
permission-contents: write # git.createCommit + refs.create/update to push the sync/device-classes branch
|
|
||||||
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
|
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- name: Checkout Home Assistant
|
- name: Checkout Home Assistant
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
repository: home-assistant/core
|
repository: home-assistant/core
|
||||||
path: lib/home-assistant
|
path: lib/home-assistant
|
||||||
|
|
||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.14"
|
python-version: "3.14"
|
||||||
|
|
||||||
- name: Set up uv
|
|
||||||
# An order of magnitude faster than pip on cold boots, with its
|
|
||||||
# own wheel cache. ``--system`` (below) installs into the
|
|
||||||
# setup-python interpreter so subsequent ``pre-commit`` /
|
|
||||||
# ``script/run-in-env.py`` steps find the deps without a
|
|
||||||
# ``uv run`` prefix.
|
|
||||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
|
||||||
with:
|
|
||||||
enable-cache: true
|
|
||||||
# Pin uv version so the action does not have to fetch the
|
|
||||||
# manifest from raw.githubusercontent.com on every cache
|
|
||||||
# miss; that fetch flakes on Windows runners.
|
|
||||||
version: "0.11.15"
|
|
||||||
|
|
||||||
- name: Install Home Assistant
|
- name: Install Home Assistant
|
||||||
run: |
|
run: |
|
||||||
uv pip install --system -e lib/home-assistant
|
python -m pip install --upgrade pip
|
||||||
uv pip install --system -r requirements.txt -r requirements_test.txt pre-commit
|
pip install -e lib/home-assistant
|
||||||
|
pip install -r requirements_test.txt pre-commit
|
||||||
|
|
||||||
- name: Sync
|
- name: Sync
|
||||||
run: |
|
run: |
|
||||||
python ./script/sync-device_class.py
|
python ./script/sync-device_class.py
|
||||||
|
|
||||||
- name: Apply pre-commit auto-fixes
|
- name: Run pre-commit hooks
|
||||||
# First pass: let formatters (ruff, end-of-file-fixer, etc.) modify
|
run: |
|
||||||
# files. pre-commit exits non-zero whenever a hook touches anything,
|
python script/run-in-env.py pre-commit run --all-files
|
||||||
# which would otherwise abort the workflow before the auto-fixes
|
|
||||||
# can flow into the sync PR.
|
|
||||||
#
|
|
||||||
# SKIP:
|
|
||||||
# - no-commit-to-branch is a local guard against committing on
|
|
||||||
# dev/release/beta; CI runs on dev by definition, and
|
|
||||||
# peter-evans/create-pull-request creates the branch itself.
|
|
||||||
# - pylint surfaces import-error / relative-beyond-top-level
|
|
||||||
# noise here because this workflow installs only a subset of
|
|
||||||
# the runtime deps (HA + requirements*.txt); main CI already
|
|
||||||
# gates pylint on real PRs.
|
|
||||||
env:
|
|
||||||
SKIP: pylint,no-commit-to-branch
|
|
||||||
run: python script/run-in-env.py pre-commit run --all-files || true
|
|
||||||
|
|
||||||
- name: Verify pre-commit clean
|
|
||||||
# Second pass: re-run all hooks against the now-fixed tree.
|
|
||||||
# Auto-fixers exit 0 (nothing to change); any remaining failure
|
|
||||||
# from a check-only hook (flake8 / yamllint / ci-custom) is a
|
|
||||||
# real issue and fails the workflow loudly. Same SKIP list as
|
|
||||||
# above for the same reasons.
|
|
||||||
env:
|
|
||||||
SKIP: pylint,no-commit-to-branch
|
|
||||||
run: python script/run-in-env.py pre-commit run --all-files
|
|
||||||
|
|
||||||
- name: Commit changes
|
- name: Commit changes
|
||||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||||
with:
|
with:
|
||||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||||
committer: esphomebot <esphome@openhomefoundation.org>
|
committer: esphomebot <esphome@openhomefoundation.org>
|
||||||
@@ -102,4 +50,4 @@ jobs:
|
|||||||
delete-branch: true
|
delete-branch: true
|
||||||
title: "Synchronise Device Classes from Home Assistant"
|
title: "Synchronise Device Classes from Home Assistant"
|
||||||
body-path: .github/PULL_REQUEST_TEMPLATE.md
|
body-path: .github/PULL_REQUEST_TEMPLATE.md
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }}
|
||||||
|
|||||||
@@ -141,12 +141,10 @@ tests/.esphome/
|
|||||||
|
|
||||||
sdkconfig.*
|
sdkconfig.*
|
||||||
!sdkconfig.defaults
|
!sdkconfig.defaults
|
||||||
!sdkconfig.defaults.*
|
|
||||||
|
|
||||||
.tests/
|
.tests/
|
||||||
|
|
||||||
/components
|
/components
|
||||||
/managed_components
|
/managed_components
|
||||||
/dependencies.lock
|
|
||||||
|
|
||||||
api-docs/
|
api-docs/
|
||||||
|
|||||||
+11
-9
@@ -6,12 +6,12 @@ ci:
|
|||||||
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
autoupdate_commit_msg: 'pre-commit: autoupdate'
|
||||||
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
autoupdate_schedule: off # Disabled until ruff versions are synced between deps and pre-commit
|
||||||
# Skip hooks that have issues in pre-commit CI environment
|
# Skip hooks that have issues in pre-commit CI environment
|
||||||
skip: [pylint]
|
skip: [pylint, clang-tidy-hash]
|
||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.15.15
|
rev: v0.15.8
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
@@ -40,7 +40,7 @@ repos:
|
|||||||
rev: v3.21.2
|
rev: v3.21.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py312-plus]
|
args: [--py311-plus]
|
||||||
- repo: https://github.com/adrienverge/yamllint.git
|
- repo: https://github.com/adrienverge/yamllint.git
|
||||||
rev: v1.37.1
|
rev: v1.37.1
|
||||||
hooks:
|
hooks:
|
||||||
@@ -55,11 +55,13 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: pylint
|
- id: pylint
|
||||||
name: pylint
|
name: pylint
|
||||||
entry: python script/run-in-env.py pylint
|
entry: python3 script/run-in-env.py pylint
|
||||||
language: system
|
language: system
|
||||||
types: [python]
|
types: [python]
|
||||||
files: ^esphome/.+\.py$
|
- id: clang-tidy-hash
|
||||||
- id: ci-custom
|
name: Update clang-tidy hash
|
||||||
name: ci-custom
|
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||||
entry: python script/run-in-env.py script/ci-custom.py
|
language: python
|
||||||
language: system
|
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
||||||
|
pass_filenames: false
|
||||||
|
additional_dependencies: []
|
||||||
|
|||||||
+7
-34
@@ -19,6 +19,7 @@ esphome/components/ac_dimmer/* @glmnet
|
|||||||
esphome/components/adc/* @esphome/core
|
esphome/components/adc/* @esphome/core
|
||||||
esphome/components/adc128s102/* @DeerMaximum
|
esphome/components/adc128s102/* @DeerMaximum
|
||||||
esphome/components/addressable_light/* @justfalter
|
esphome/components/addressable_light/* @justfalter
|
||||||
|
esphome/components/ade7880/* @kpfleming
|
||||||
esphome/components/ade7953/* @angelnu
|
esphome/components/ade7953/* @angelnu
|
||||||
esphome/components/ade7953_base/* @angelnu
|
esphome/components/ade7953_base/* @angelnu
|
||||||
esphome/components/ade7953_i2c/* @angelnu
|
esphome/components/ade7953_i2c/* @angelnu
|
||||||
@@ -27,7 +28,7 @@ esphome/components/ads1118/* @solomondg1
|
|||||||
esphome/components/ags10/* @mak-42
|
esphome/components/ags10/* @mak-42
|
||||||
esphome/components/aic3204/* @kbx81
|
esphome/components/aic3204/* @kbx81
|
||||||
esphome/components/airthings_ble/* @jeromelaban
|
esphome/components/airthings_ble/* @jeromelaban
|
||||||
esphome/components/airthings_wave_base/* @jeromelaban @ncareau
|
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
|
||||||
esphome/components/airthings_wave_mini/* @ncareau
|
esphome/components/airthings_wave_mini/* @ncareau
|
||||||
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
|
||||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
|
||||||
@@ -55,7 +56,6 @@ esphome/components/audio_adc/* @kbx81
|
|||||||
esphome/components/audio_dac/* @kbx81
|
esphome/components/audio_dac/* @kbx81
|
||||||
esphome/components/audio_file/* @kahrendt
|
esphome/components/audio_file/* @kahrendt
|
||||||
esphome/components/audio_file/media_source/* @kahrendt
|
esphome/components/audio_file/media_source/* @kahrendt
|
||||||
esphome/components/audio_http/* @kahrendt
|
|
||||||
esphome/components/axs15231/* @clydebarrow
|
esphome/components/axs15231/* @clydebarrow
|
||||||
esphome/components/b_parasite/* @rbaron
|
esphome/components/b_parasite/* @rbaron
|
||||||
esphome/components/ballu/* @bazuchan
|
esphome/components/ballu/* @bazuchan
|
||||||
@@ -83,7 +83,6 @@ esphome/components/bme680_bsec/* @trvrnrth
|
|||||||
esphome/components/bme68x_bsec2/* @kbx81 @neffs
|
esphome/components/bme68x_bsec2/* @kbx81 @neffs
|
||||||
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
esphome/components/bme68x_bsec2_i2c/* @kbx81 @neffs
|
||||||
esphome/components/bmi160/* @flaviut
|
esphome/components/bmi160/* @flaviut
|
||||||
esphome/components/bmi270/* @clydebarrow
|
|
||||||
esphome/components/bmp280_base/* @ademuri
|
esphome/components/bmp280_base/* @ademuri
|
||||||
esphome/components/bmp280_i2c/* @ademuri
|
esphome/components/bmp280_i2c/* @ademuri
|
||||||
esphome/components/bmp280_spi/* @ademuri
|
esphome/components/bmp280_spi/* @ademuri
|
||||||
@@ -122,9 +121,7 @@ esphome/components/cover/* @esphome/core
|
|||||||
esphome/components/cs5460a/* @balrog-kun
|
esphome/components/cs5460a/* @balrog-kun
|
||||||
esphome/components/cse7761/* @berfenger
|
esphome/components/cse7761/* @berfenger
|
||||||
esphome/components/cst226/* @clydebarrow
|
esphome/components/cst226/* @clydebarrow
|
||||||
esphome/components/cst328/* @latonita
|
|
||||||
esphome/components/cst816/* @clydebarrow
|
esphome/components/cst816/* @clydebarrow
|
||||||
esphome/components/cst9220/* @clydebarrow
|
|
||||||
esphome/components/ct_clamp/* @jesserockz
|
esphome/components/ct_clamp/* @jesserockz
|
||||||
esphome/components/current_based/* @djwmarcx
|
esphome/components/current_based/* @djwmarcx
|
||||||
esphome/components/dac7678/* @NickB1
|
esphome/components/dac7678/* @NickB1
|
||||||
@@ -141,18 +138,16 @@ esphome/components/dfplayer/* @glmnet
|
|||||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||||
esphome/components/dht/* @OttoWinter
|
esphome/components/dht/* @OttoWinter
|
||||||
esphome/components/display_menu_base/* @numo68
|
esphome/components/display_menu_base/* @numo68
|
||||||
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
esphome/components/dlms_meter/* @SimonFischer04
|
||||||
esphome/components/dps310/* @kbx81
|
esphome/components/dps310/* @kbx81
|
||||||
esphome/components/ds1307/* @badbadc0ffee
|
esphome/components/ds1307/* @badbadc0ffee
|
||||||
esphome/components/ds2484/* @mrk-its
|
esphome/components/ds2484/* @mrk-its
|
||||||
esphome/components/ds248x/* @tomwellnitz
|
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk
|
||||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
|
||||||
esphome/components/duty_time/* @dudanov
|
esphome/components/duty_time/* @dudanov
|
||||||
esphome/components/ee895/* @Stock-M
|
esphome/components/ee895/* @Stock-M
|
||||||
esphome/components/ektf2232/touchscreen/* @jesserockz
|
esphome/components/ektf2232/touchscreen/* @jesserockz
|
||||||
esphome/components/emc2101/* @ellull
|
esphome/components/emc2101/* @ellull
|
||||||
esphome/components/emmeti/* @E440QF
|
esphome/components/emmeti/* @E440QF
|
||||||
esphome/components/emontx/* @FredM67 @glynhudson @TrystanLea
|
|
||||||
esphome/components/ens160/* @latonita
|
esphome/components/ens160/* @latonita
|
||||||
esphome/components/ens160_base/* @latonita @vincentscode
|
esphome/components/ens160_base/* @latonita @vincentscode
|
||||||
esphome/components/ens160_i2c/* @latonita
|
esphome/components/ens160_i2c/* @latonita
|
||||||
@@ -188,7 +183,6 @@ esphome/components/ezo_pmp/* @carlos-sarmiento
|
|||||||
esphome/components/factory_reset/* @anatoly-savchenkov
|
esphome/components/factory_reset/* @anatoly-savchenkov
|
||||||
esphome/components/fastled_base/* @OttoWinter
|
esphome/components/fastled_base/* @OttoWinter
|
||||||
esphome/components/feedback/* @ianchi
|
esphome/components/feedback/* @ianchi
|
||||||
esphome/components/file/* @esphome/core
|
|
||||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||||
esphome/components/font/* @clydebarrow @esphome/core
|
esphome/components/font/* @clydebarrow @esphome/core
|
||||||
esphome/components/fs3000/* @kahrendt
|
esphome/components/fs3000/* @kahrendt
|
||||||
@@ -210,7 +204,6 @@ esphome/components/gree/switch/* @nagyrobi
|
|||||||
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
||||||
esphome/components/grove_tb6612fng/* @max246
|
esphome/components/grove_tb6612fng/* @max246
|
||||||
esphome/components/growatt_solar/* @leeuwte
|
esphome/components/growatt_solar/* @leeuwte
|
||||||
esphome/components/gsl3670/* @clydebarrow
|
|
||||||
esphome/components/gt911/* @clydebarrow @jesserockz
|
esphome/components/gt911/* @clydebarrow @jesserockz
|
||||||
esphome/components/haier/* @paveldn
|
esphome/components/haier/* @paveldn
|
||||||
esphome/components/haier/binary_sensor/* @paveldn
|
esphome/components/haier/binary_sensor/* @paveldn
|
||||||
@@ -271,7 +264,6 @@ esphome/components/integration/* @OttoWinter
|
|||||||
esphome/components/internal_temperature/* @Mat931
|
esphome/components/internal_temperature/* @Mat931
|
||||||
esphome/components/interval/* @esphome/core
|
esphome/components/interval/* @esphome/core
|
||||||
esphome/components/ir_rf_proxy/* @kbx81
|
esphome/components/ir_rf_proxy/* @kbx81
|
||||||
esphome/components/it8951/* @koosoli @limengdu @Passific
|
|
||||||
esphome/components/jsn_sr04t/* @Mafus1
|
esphome/components/jsn_sr04t/* @Mafus1
|
||||||
esphome/components/json/* @esphome/core
|
esphome/components/json/* @esphome/core
|
||||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||||
@@ -297,7 +289,6 @@ esphome/components/lock/* @esphome/core
|
|||||||
esphome/components/logger/* @esphome/core
|
esphome/components/logger/* @esphome/core
|
||||||
esphome/components/logger/select/* @clydebarrow
|
esphome/components/logger/select/* @clydebarrow
|
||||||
esphome/components/lps22/* @nagisa
|
esphome/components/lps22/* @nagisa
|
||||||
esphome/components/lsm6ds/* @clydebarrow
|
|
||||||
esphome/components/ltr390/* @latonita @sjtrny
|
esphome/components/ltr390/* @latonita @sjtrny
|
||||||
esphome/components/ltr501/* @latonita
|
esphome/components/ltr501/* @latonita
|
||||||
esphome/components/ltr_als_ps/* @latonita
|
esphome/components/ltr_als_ps/* @latonita
|
||||||
@@ -354,11 +345,9 @@ esphome/components/modbus_controller/select/* @martgras @stegm
|
|||||||
esphome/components/modbus_controller/sensor/* @martgras
|
esphome/components/modbus_controller/sensor/* @martgras
|
||||||
esphome/components/modbus_controller/switch/* @martgras
|
esphome/components/modbus_controller/switch/* @martgras
|
||||||
esphome/components/modbus_controller/text_sensor/* @martgras
|
esphome/components/modbus_controller/text_sensor/* @martgras
|
||||||
esphome/components/modbus_server/* @exciton
|
|
||||||
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
|
||||||
esphome/components/mopeka_pro_check/* @spbrogan
|
esphome/components/mopeka_pro_check/* @spbrogan
|
||||||
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
esphome/components/mopeka_std_check/* @Fabian-Schmidt
|
||||||
esphome/components/motion/* @esphome/core
|
|
||||||
esphome/components/mpl3115a2/* @kbickar
|
esphome/components/mpl3115a2/* @kbickar
|
||||||
esphome/components/mpu6886/* @fabaff
|
esphome/components/mpu6886/* @fabaff
|
||||||
esphome/components/ms8607/* @e28eta
|
esphome/components/ms8607/* @e28eta
|
||||||
@@ -387,11 +376,9 @@ esphome/components/pca6416a/* @Mat931
|
|||||||
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
|
||||||
esphome/components/pcf85063/* @brogon
|
esphome/components/pcf85063/* @brogon
|
||||||
esphome/components/pcf8563/* @KoenBreeman
|
esphome/components/pcf8563/* @KoenBreeman
|
||||||
esphome/components/pcm5122/* @remcom
|
|
||||||
esphome/components/pi4ioe5v6408/* @jesserockz
|
esphome/components/pi4ioe5v6408/* @jesserockz
|
||||||
esphome/components/pid/* @OttoWinter
|
esphome/components/pid/* @OttoWinter
|
||||||
esphome/components/pipsolar/* @andreashergert1984
|
esphome/components/pipsolar/* @andreashergert1984
|
||||||
esphome/components/pixoo/* @jesserockz
|
|
||||||
esphome/components/pm1006/* @habbie
|
esphome/components/pm1006/* @habbie
|
||||||
esphome/components/pm2005/* @andrewjswan
|
esphome/components/pm2005/* @andrewjswan
|
||||||
esphome/components/pmsa003i/* @sjtrny
|
esphome/components/pmsa003i/* @sjtrny
|
||||||
@@ -407,17 +394,14 @@ esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
|||||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||||
esphome/components/power_supply/* @esphome/core
|
esphome/components/power_supply/* @esphome/core
|
||||||
esphome/components/preferences/* @esphome/core
|
esphome/components/preferences/* @esphome/core
|
||||||
esphome/components/provisioning/* @esphome/core
|
|
||||||
esphome/components/psram/* @esphome/core
|
esphome/components/psram/* @esphome/core
|
||||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
||||||
esphome/components/pvvx_mithermometer/* @pasiz
|
esphome/components/pvvx_mithermometer/* @pasiz
|
||||||
esphome/components/pylontech/* @functionpointer
|
esphome/components/pylontech/* @functionpointer
|
||||||
esphome/components/qmi8658/* @clydebarrow
|
|
||||||
esphome/components/qmp6988/* @andrewpc
|
esphome/components/qmp6988/* @andrewpc
|
||||||
esphome/components/qr_code/* @wjtje
|
esphome/components/qr_code/* @wjtje
|
||||||
esphome/components/qspi_dbi/* @clydebarrow
|
esphome/components/qspi_dbi/* @clydebarrow
|
||||||
esphome/components/qwiic_pir/* @kahrendt
|
esphome/components/qwiic_pir/* @kahrendt
|
||||||
esphome/components/radio_frequency/* @kbx81
|
|
||||||
esphome/components/radon_eye_ble/* @jeffeb3
|
esphome/components/radon_eye_ble/* @jeffeb3
|
||||||
esphome/components/radon_eye_rd200/* @jeffeb3
|
esphome/components/radon_eye_rd200/* @jeffeb3
|
||||||
esphome/components/rc522/* @glmnet
|
esphome/components/rc522/* @glmnet
|
||||||
@@ -428,9 +412,7 @@ esphome/components/resampler/speaker/* @kahrendt
|
|||||||
esphome/components/restart/* @esphome/core
|
esphome/components/restart/* @esphome/core
|
||||||
esphome/components/rf_bridge/* @jesserockz
|
esphome/components/rf_bridge/* @jesserockz
|
||||||
esphome/components/rgbct/* @jesserockz
|
esphome/components/rgbct/* @jesserockz
|
||||||
esphome/components/ring_buffer/* @kahrendt
|
esphome/components/rp2040/* @jesserockz
|
||||||
esphome/components/router/speaker/* @kahrendt
|
|
||||||
esphome/components/rp2/* @jesserockz
|
|
||||||
esphome/components/rp2040_ble/* @bdraco
|
esphome/components/rp2040_ble/* @bdraco
|
||||||
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||||
esphome/components/rp2040_pwm/* @jesserockz
|
esphome/components/rp2040_pwm/* @jesserockz
|
||||||
@@ -454,12 +436,7 @@ esphome/components/select/* @esphome/core
|
|||||||
esphome/components/sen0321/* @notjj
|
esphome/components/sen0321/* @notjj
|
||||||
esphome/components/sen21231/* @shreyaskarnik
|
esphome/components/sen21231/* @shreyaskarnik
|
||||||
esphome/components/sen5x/* @martgras
|
esphome/components/sen5x/* @martgras
|
||||||
esphome/components/sen6x/* @martgras @mebner86 @tuct
|
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||||
esphome/components/sendspin/* @kahrendt
|
|
||||||
esphome/components/sendspin/media_player/* @kahrendt
|
|
||||||
esphome/components/sendspin/media_source/* @kahrendt
|
|
||||||
esphome/components/sendspin/sensor/* @kahrendt
|
|
||||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
|
||||||
esphome/components/sensirion_common/* @martgras
|
esphome/components/sensirion_common/* @martgras
|
||||||
esphome/components/sensor/* @esphome/core
|
esphome/components/sensor/* @esphome/core
|
||||||
esphome/components/serial_proxy/* @kbx81
|
esphome/components/serial_proxy/* @kbx81
|
||||||
@@ -506,7 +483,6 @@ esphome/components/ssd1331_base/* @kbx81
|
|||||||
esphome/components/ssd1331_spi/* @kbx81
|
esphome/components/ssd1331_spi/* @kbx81
|
||||||
esphome/components/ssd1351_base/* @kbx81
|
esphome/components/ssd1351_base/* @kbx81
|
||||||
esphome/components/ssd1351_spi/* @kbx81
|
esphome/components/ssd1351_spi/* @kbx81
|
||||||
esphome/components/st7123/* @miniskipper
|
|
||||||
esphome/components/st7567_base/* @latonita
|
esphome/components/st7567_base/* @latonita
|
||||||
esphome/components/st7567_i2c/* @latonita
|
esphome/components/st7567_i2c/* @latonita
|
||||||
esphome/components/st7567_spi/* @latonita
|
esphome/components/st7567_spi/* @latonita
|
||||||
@@ -571,7 +547,6 @@ esphome/components/uart/packet_transport/* @clydebarrow
|
|||||||
esphome/components/udp/* @clydebarrow
|
esphome/components/udp/* @clydebarrow
|
||||||
esphome/components/ufire_ec/* @pvizeli
|
esphome/components/ufire_ec/* @pvizeli
|
||||||
esphome/components/ufire_ise/* @pvizeli
|
esphome/components/ufire_ise/* @pvizeli
|
||||||
esphome/components/ufm01/* @ljungqvist
|
|
||||||
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
||||||
esphome/components/update/* @jesserockz
|
esphome/components/update/* @jesserockz
|
||||||
esphome/components/uponor_smatrix/* @kroimon
|
esphome/components/uponor_smatrix/* @kroimon
|
||||||
@@ -588,7 +563,6 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
|||||||
esphome/components/watchdog/* @oarcher
|
esphome/components/watchdog/* @oarcher
|
||||||
esphome/components/water_heater/* @dhoeben
|
esphome/components/water_heater/* @dhoeben
|
||||||
esphome/components/waveshare_epaper/* @clydebarrow
|
esphome/components/waveshare_epaper/* @clydebarrow
|
||||||
esphome/components/waveshare_io_ch32v003/* @latonita
|
|
||||||
esphome/components/web_server/ota/* @esphome/core
|
esphome/components/web_server/ota/* @esphome/core
|
||||||
esphome/components/web_server_base/* @esphome/core
|
esphome/components/web_server_base/* @esphome/core
|
||||||
esphome/components/web_server_idf/* @dentra
|
esphome/components/web_server_idf/* @dentra
|
||||||
@@ -610,7 +584,6 @@ esphome/components/wk2212_spi/* @DrCoolZic
|
|||||||
esphome/components/wl_134/* @hobbypunk90
|
esphome/components/wl_134/* @hobbypunk90
|
||||||
esphome/components/wts01/* @alepee
|
esphome/components/wts01/* @alepee
|
||||||
esphome/components/x9c/* @EtienneMD
|
esphome/components/x9c/* @EtienneMD
|
||||||
esphome/components/xdb401/* @RT530
|
|
||||||
esphome/components/xgzp68xx/* @gcormier
|
esphome/components/xgzp68xx/* @gcormier
|
||||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||||
@@ -625,6 +598,6 @@ esphome/components/xxtea/* @clydebarrow
|
|||||||
esphome/components/zephyr/* @tomaszduda23
|
esphome/components/zephyr/* @tomaszduda23
|
||||||
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
|
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
|
||||||
esphome/components/zhlt01/* @cfeenstra1024
|
esphome/components/zhlt01/* @cfeenstra1024
|
||||||
esphome/components/zigbee/* @luar123 @tomaszduda23
|
esphome/components/zigbee/* @tomaszduda23
|
||||||
esphome/components/zio_ultrasonic/* @kahrendt
|
esphome/components/zio_ultrasonic/* @kahrendt
|
||||||
esphome/components/zwave_proxy/* @kbx81
|
esphome/components/zwave_proxy/* @kbx81
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
|||||||
# could be handy for archiving the generated documentation or if some version
|
# could be handy for archiving the generated documentation or if some version
|
||||||
# control system is used.
|
# control system is used.
|
||||||
|
|
||||||
PROJECT_NUMBER = 2026.8.0-dev
|
PROJECT_NUMBER = 2026.4.0-dev
|
||||||
|
|
||||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||||
# for a project that appears at the top of each page and should give viewer a
|
# for a project that appears at the top of each page and should give viewer a
|
||||||
|
|||||||
@@ -4,6 +4,4 @@ include requirements.txt
|
|||||||
recursive-include esphome *.yaml
|
recursive-include esphome *.yaml
|
||||||
recursive-include esphome *.cpp *.h *.tcc *.c
|
recursive-include esphome *.cpp *.h *.tcc *.c
|
||||||
recursive-include esphome *.py.script
|
recursive-include esphome *.py.script
|
||||||
recursive-include esphome *.jinja
|
|
||||||
recursive-include esphome LICENSE.txt
|
recursive-include esphome LICENSE.txt
|
||||||
recursive-include esphome requirements.txt
|
|
||||||
|
|||||||
-148
@@ -1,148 +0,0 @@
|
|||||||
# ESPHome Threat Model
|
|
||||||
|
|
||||||
This document defines the trust boundary for the **ESPHome** repository — the
|
|
||||||
Python compiler/CLI and the device firmware it generates — so that real security
|
|
||||||
bugs can be told apart from defense-in-depth improvements. It gives contributors,
|
|
||||||
reviewers, and security researchers a clear answer to one question:
|
|
||||||
**does this issue let an _unauthenticated_ attacker do something they shouldn't?**
|
|
||||||
|
|
||||||
Related documents:
|
|
||||||
|
|
||||||
- Deployment guidance for operators:
|
|
||||||
https://esphome.io/guides/security_best_practices/
|
|
||||||
- The **Device Builder dashboard** (the web UI, its authentication, ingress,
|
|
||||||
Origin/Host gates, and peer-link pairing) lives in a separate repository and
|
|
||||||
has its own threat model. If your report concerns any of that, please read and
|
|
||||||
report there instead:
|
|
||||||
https://github.com/esphome/device-builder/blob/main/docs/THREAT_MODEL.md
|
|
||||||
|
|
||||||
## The trust boundary
|
|
||||||
|
|
||||||
For this repository there are two trusted inputs by design:
|
|
||||||
|
|
||||||
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
|
|
||||||
(see below).
|
|
||||||
2. **Authenticated peers of a running device** — clients holding the device's
|
|
||||||
API encryption key / password, OTA password, or web server credentials.
|
|
||||||
|
|
||||||
The security boundary is therefore **unauthenticated network traffic vs. those
|
|
||||||
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
|
|
||||||
security bug.
|
|
||||||
|
|
||||||
## Config authors are host-equivalent by design
|
|
||||||
|
|
||||||
Anyone who can supply or edit a configuration is **trusted with full code
|
|
||||||
execution on the host that runs `esphome`**, on purpose. This is what the product
|
|
||||||
does, not a flaw. A config author can already, through fully supported features:
|
|
||||||
|
|
||||||
- Run arbitrary **Python** at validation/compile time via `external_components:`
|
|
||||||
(and other component-import mechanisms) — ESPHome imports those packages as
|
|
||||||
ordinary Python.
|
|
||||||
- Run arbitrary **shell** commands through the compile/validate/flash toolchain
|
|
||||||
that ESPHome invokes as subprocesses.
|
|
||||||
- Read and write arbitrary files reachable by the process (e.g. via `!include`,
|
|
||||||
`packages:`, `dashboard_import:`, and generated build output).
|
|
||||||
|
|
||||||
Because of this, a malicious config author is equivalent to shell access on the
|
|
||||||
host running the build.
|
|
||||||
|
|
||||||
## What is *not* a security vulnerability
|
|
||||||
|
|
||||||
If exploiting an issue requires the ability to supply or edit configuration, it
|
|
||||||
is **not** a vulnerability in ESPHome, because that ability already grants host
|
|
||||||
code execution. This explicitly includes, among others:
|
|
||||||
|
|
||||||
- Template / expression injection in substitutions or any YAML string value
|
|
||||||
(e.g. Jinja `${...}` evaluation reaching Python internals). This grants no
|
|
||||||
capability a config author lacks.
|
|
||||||
- `!include` / `packages:` / `dashboard_import:` reading or fetching content
|
|
||||||
from surprising or remote locations.
|
|
||||||
- The validator or compiler crashing or behaving unexpectedly on adversarial
|
|
||||||
YAML.
|
|
||||||
- ESPHome running as root in the official container — that is the documented
|
|
||||||
deployment posture, reachable by the same caller through the features above.
|
|
||||||
|
|
||||||
These do not warrant a CVE or coordinated disclosure. Hardening in these areas
|
|
||||||
(for example, sandboxing template evaluation as least-surprise defense-in-depth)
|
|
||||||
is welcome as a normal enhancement PR, framed as cleanliness rather than a
|
|
||||||
security fix — not as a vulnerability remediation.
|
|
||||||
|
|
||||||
## What we do defend
|
|
||||||
|
|
||||||
These *are* security bugs in this repo, and we want to hear about them privately:
|
|
||||||
|
|
||||||
- Memory-safety or protocol bugs in the generated **device firmware** that are
|
|
||||||
remotely triggerable over the network (native API, web server, OTA, BLE,
|
|
||||||
captive portal, etc.) **without** valid credentials.
|
|
||||||
- Authentication or encryption bypass on the device — reaching API calls, OTA
|
|
||||||
updates, or the web server without the configured key/password.
|
|
||||||
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
|
|
||||||
below their documented guarantees.
|
|
||||||
|
|
||||||
## The web server is an open HTTP API by design
|
|
||||||
|
|
||||||
The `web_server` component exposes a plain HTTP interface for viewing and
|
|
||||||
controlling entities, and, when the `web_server` OTA platform is enabled, for
|
|
||||||
uploading firmware at `/update`. Its only access controls are the optional
|
|
||||||
`web_server` `auth:` credentials and the network the device sits on.
|
|
||||||
|
|
||||||
When `auth:` is not configured, every endpoint is reachable by any client that
|
|
||||||
can reach the device. This is intentional; enabling `web_server` without `auth:`
|
|
||||||
is choosing an open control surface, in the same way that running native OTA
|
|
||||||
without a password leaves OTA open. The API is documented and is meant to be
|
|
||||||
called by other devices, scripts, and pages.
|
|
||||||
|
|
||||||
As defense-in-depth, the web server checks the `Origin` header on browser requests
|
|
||||||
to its entity control and state endpoints: a request whose `Origin` does not match
|
|
||||||
the address the device is served on is rejected, and the `allowed_origins` option
|
|
||||||
widens that list. This blocks the common "confused deputy" (CSRF) case where a page
|
|
||||||
the operator visits drives the device through their browser. It is **not** an
|
|
||||||
authentication boundary: it only constrains browsers. Any client that omits the
|
|
||||||
`Origin` header — `curl`, scripts, or other non-browser callers on the same
|
|
||||||
network — reaches every endpoint exactly as before. The check also does not cover
|
|
||||||
the web OTA `/update` endpoint. The device performs no CSRF-token or `Referer`
|
|
||||||
validation. The following are therefore **not** vulnerabilities in this repository:
|
|
||||||
|
|
||||||
- Requests without an `Origin` header (for example `curl`) reaching the control
|
|
||||||
endpoints, whether or not `web_server` `auth:` is set.
|
|
||||||
- Requests from an origin the operator added to `allowed_origins`.
|
|
||||||
- Cross-origin or CSRF firmware upload through the web OTA endpoint (`/update`) when
|
|
||||||
web OTA is enabled without `web_server` `auth:`. The `/update` endpoint is not
|
|
||||||
covered by the `Origin` check; this is the same exposure as running OTA without a
|
|
||||||
password.
|
|
||||||
|
|
||||||
The supported defenses are `web_server` `auth:`, protecting OTA (a web password or
|
|
||||||
a native OTA password), and keeping devices on a trusted, segmented network. See
|
|
||||||
the security best practices guide linked above.
|
|
||||||
|
|
||||||
What remains in scope is bypassing `web_server` `auth:` when it *is* configured,
|
|
||||||
and any memory-safety or protocol bug in the server reachable without credentials.
|
|
||||||
|
|
||||||
This section documents the current design and scope; it is not a judgment that the
|
|
||||||
design is optimal or that it will not change.
|
|
||||||
|
|
||||||
## Explicitly out of scope
|
|
||||||
|
|
||||||
- Local attackers who already have shell access on the host that runs `esphome`.
|
|
||||||
- Supply-chain attacks against ESPHome or its dependencies.
|
|
||||||
- Operator-supplied hostile YAML (covered above — config authoring is trusted).
|
|
||||||
- Attacks that require an already-authenticated device peer (someone who already
|
|
||||||
holds the API key / OTA / web credentials).
|
|
||||||
- Access to the device web server or its web OTA endpoint by non-browser clients
|
|
||||||
(those that send no `Origin` header). The web server is an open HTTP API by
|
|
||||||
design (see above); browser cross-origin requests are blocked by default, but the
|
|
||||||
real controls are `web_server` `auth:` and network isolation.
|
|
||||||
- Anything in the dashboard / device-builder — report that in its own repository
|
|
||||||
(linked at the top).
|
|
||||||
- Deployments where the operator removed protections or exposed credentials. See
|
|
||||||
the security best practices guide:
|
|
||||||
https://esphome.io/guides/security_best_practices/
|
|
||||||
|
|
||||||
## Reporting a vulnerability
|
|
||||||
|
|
||||||
If you believe you've found an issue that crosses the unauthenticated boundary
|
|
||||||
above, please report it privately via GitHub Security Advisories rather than a
|
|
||||||
public issue. For issues that require config-write access, please review this
|
|
||||||
document first — they are very likely out of scope by design. For dashboard /
|
|
||||||
device-builder issues, report against that repository and consult its threat
|
|
||||||
model (linked at the top).
|
|
||||||
-18
@@ -1,18 +0,0 @@
|
|||||||
coverage:
|
|
||||||
status:
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: 100%
|
|
||||||
threshold: 0%
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
informational: true
|
|
||||||
|
|
||||||
ignore:
|
|
||||||
- "esphome/components/**/*"
|
|
||||||
- "esphome/analyze_memory/**/*"
|
|
||||||
- "tests/integration/**/*"
|
|
||||||
|
|
||||||
comment:
|
|
||||||
layout: "reach, diff, flags, files"
|
|
||||||
require_changes: true
|
|
||||||
+14
-6
@@ -1,9 +1,10 @@
|
|||||||
ARG BUILD_VERSION=dev
|
ARG BUILD_VERSION=dev
|
||||||
ARG BUILD_BASE_VERSION=2026.06.1
|
ARG BUILD_OS=alpine
|
||||||
|
ARG BUILD_BASE_VERSION=2025.04.0
|
||||||
ARG BUILD_TYPE=docker
|
ARG BUILD_TYPE=docker
|
||||||
|
|
||||||
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
|
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
|
||||||
FROM ghcr.io/esphome/docker-base:debian-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
|
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
|
||||||
|
|
||||||
ARG BUILD_TYPE
|
ARG BUILD_TYPE
|
||||||
FROM base-source-${BUILD_TYPE} AS base
|
FROM base-source-${BUILD_TYPE} AS base
|
||||||
@@ -11,6 +12,16 @@ FROM base-source-${BUILD_TYPE} AS base
|
|||||||
RUN git config --system --add safe.directory "*" \
|
RUN git config --system --add safe.directory "*" \
|
||||||
&& git config --system advice.detachedHead false
|
&& git config --system advice.detachedHead false
|
||||||
|
|
||||||
|
# Install build tools for Python packages that require compilation
|
||||||
|
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||||
|
RUN if command -v apk > /dev/null; then \
|
||||||
|
apk add --no-cache build-base; \
|
||||||
|
else \
|
||||||
|
apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*; \
|
||||||
|
fi
|
||||||
|
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||||
|
|
||||||
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
RUN pip install --no-cache-dir -U pip uv==0.10.1
|
||||||
@@ -21,9 +32,6 @@ RUN \
|
|||||||
uv pip install --no-cache-dir \
|
uv pip install --no-cache-dir \
|
||||||
-r /requirements.txt
|
-r /requirements.txt
|
||||||
|
|
||||||
# Install the ESPHome Device Builder dashboard.
|
|
||||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.6.4
|
|
||||||
|
|
||||||
RUN \
|
RUN \
|
||||||
platformio settings set enable_telemetry No \
|
platformio settings set enable_telemetry No \
|
||||||
&& platformio settings set check_platformio_interval 1000000 \
|
&& platformio settings set check_platformio_interval 1000000 \
|
||||||
|
|||||||
+13
-42
@@ -20,10 +20,6 @@ TYPE_HA_ADDON = "ha-addon"
|
|||||||
TYPE_LINT = "lint"
|
TYPE_LINT = "lint"
|
||||||
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
|
TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT]
|
||||||
|
|
||||||
REGISTRY_GHCR = "ghcr"
|
|
||||||
REGISTRY_DOCKERHUB = "dockerhub"
|
|
||||||
REGISTRIES = [REGISTRY_GHCR, REGISTRY_DOCKERHUB]
|
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
@@ -38,12 +34,6 @@ parser.add_argument(
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--build-type", choices=TYPES, required=True, help="The type of build to run"
|
"--build-type", choices=TYPES, required=True, help="The type of build to run"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--registry",
|
|
||||||
choices=REGISTRIES,
|
|
||||||
action="append",
|
|
||||||
help="Restrict to specific registries (default: all). May be passed multiple times.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--dry-run", action="store_true", help="Don't run any commands, just print them"
|
"--dry-run", action="store_true", help="Don't run any commands, just print them"
|
||||||
)
|
)
|
||||||
@@ -55,11 +45,6 @@ build_parser.add_argument("--push", help="Also push the images", action="store_t
|
|||||||
build_parser.add_argument(
|
build_parser.add_argument(
|
||||||
"--load", help="Load the docker image locally", action="store_true"
|
"--load", help="Load the docker image locally", action="store_true"
|
||||||
)
|
)
|
||||||
build_parser.add_argument(
|
|
||||||
"--no-cache-to",
|
|
||||||
help="Don't write the build cache (avoids polluting the shared cache)",
|
|
||||||
action="store_true",
|
|
||||||
)
|
|
||||||
manifest_parser = subparsers.add_parser(
|
manifest_parser = subparsers.add_parser(
|
||||||
"manifest", help="Create a manifest from already pushed images"
|
"manifest", help="Create a manifest from already pushed images"
|
||||||
)
|
)
|
||||||
@@ -110,14 +95,11 @@ def main():
|
|||||||
print("Command failed")
|
print("Command failed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
registries = args.registry or REGISTRIES
|
|
||||||
|
|
||||||
# detect channel from tag
|
# detect channel from tag
|
||||||
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
|
match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag)
|
||||||
major_minor_version = None
|
major_minor_version = None
|
||||||
if match is None:
|
if match is None:
|
||||||
# Custom tag (e.g. a branch name) -- push only the tag itself
|
channel = CHANNEL_DEV
|
||||||
channel = None
|
|
||||||
elif match.group(2) is None:
|
elif match.group(2) is None:
|
||||||
major_minor_version = match.group(1)
|
major_minor_version = match.group(1)
|
||||||
channel = CHANNEL_RELEASE
|
channel = CHANNEL_RELEASE
|
||||||
@@ -146,18 +128,11 @@ def main():
|
|||||||
CHANNEL_DEV: "cache-dev",
|
CHANNEL_DEV: "cache-dev",
|
||||||
CHANNEL_BETA: "cache-beta",
|
CHANNEL_BETA: "cache-beta",
|
||||||
CHANNEL_RELEASE: "cache-latest",
|
CHANNEL_RELEASE: "cache-latest",
|
||||||
}.get(channel, "cache-dev")
|
}[channel]
|
||||||
# Cache images live alongside the pushed images; prefer GHCR when it is
|
cache_img = f"ghcr.io/{params.build_to}:{cache_tag}"
|
||||||
# one of the selected registries, otherwise fall back to Docker Hub so a
|
|
||||||
# registry-restricted build doesn't need GHCR auth.
|
|
||||||
cache_prefix = "ghcr.io/" if REGISTRY_GHCR in registries else ""
|
|
||||||
cache_img = f"{cache_prefix}{params.build_to}:{cache_tag}"
|
|
||||||
|
|
||||||
imgs = []
|
imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push]
|
||||||
if REGISTRY_DOCKERHUB in registries:
|
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
|
||||||
imgs += [f"{params.build_to}:{tag}" for tag in tags_to_push]
|
|
||||||
if REGISTRY_GHCR in registries:
|
|
||||||
imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push]
|
|
||||||
|
|
||||||
# 3. build
|
# 3. build
|
||||||
cmd = [
|
cmd = [
|
||||||
@@ -180,9 +155,7 @@ def main():
|
|||||||
for img in imgs:
|
for img in imgs:
|
||||||
cmd += ["--tag", img]
|
cmd += ["--tag", img]
|
||||||
if args.push:
|
if args.push:
|
||||||
cmd += ["--push"]
|
cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"]
|
||||||
if not args.no_cache_to:
|
|
||||||
cmd += ["--cache-to", f"type=registry,ref={cache_img},mode=max"]
|
|
||||||
if args.load:
|
if args.load:
|
||||||
cmd += ["--load"]
|
cmd += ["--load"]
|
||||||
|
|
||||||
@@ -190,22 +163,20 @@ def main():
|
|||||||
elif args.command == "manifest":
|
elif args.command == "manifest":
|
||||||
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
|
manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to
|
||||||
|
|
||||||
targets = []
|
targets = [f"{manifest}:{tag}" for tag in tags_to_push]
|
||||||
if REGISTRY_DOCKERHUB in registries:
|
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
|
||||||
targets += [f"{manifest}:{tag}" for tag in tags_to_push]
|
# 1. Create manifests
|
||||||
if REGISTRY_GHCR in registries:
|
|
||||||
targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push]
|
|
||||||
# Use buildx imagetools (not `docker manifest`) so the per-arch sources,
|
|
||||||
# which buildx pushes as single-platform manifest lists, are combined
|
|
||||||
# and pushed correctly in one step.
|
|
||||||
for target in targets:
|
for target in targets:
|
||||||
cmd = ["docker", "buildx", "imagetools", "create", "--tag", target]
|
cmd = ["docker", "manifest", "create", target]
|
||||||
for arch in ARCHS:
|
for arch in ARCHS:
|
||||||
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
|
src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}"
|
||||||
if target.startswith("ghcr.io"):
|
if target.startswith("ghcr.io"):
|
||||||
src = f"ghcr.io/{src}"
|
src = f"ghcr.io/{src}"
|
||||||
cmd.append(src)
|
cmd.append(src)
|
||||||
run_command(*cmd)
|
run_command(*cmd)
|
||||||
|
# 2. Push manifests
|
||||||
|
for target in targets:
|
||||||
|
run_command("docker", "manifest", "push", target)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -21,23 +21,10 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
|
|||||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
||||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
||||||
|
|
||||||
# Keep the native toolchain installs on the persistent cache root, not the
|
|
||||||
# container's ephemeral user cache dir (re-downloaded on every restart).
|
|
||||||
export ESPHOME_ESP_IDF_PREFIX="$(dirname "${pio_cache_base}")/idf"
|
|
||||||
export ESPHOME_SDK_NRF_PREFIX="$(dirname "${pio_cache_base}")/sdk-nrf"
|
|
||||||
|
|
||||||
# If /build is mounted, use that as the build path
|
# If /build is mounted, use that as the build path
|
||||||
# otherwise use path in /config (so that builds aren't lost on container restart)
|
# otherwise use path in /config (so that builds aren't lost on container restart)
|
||||||
if [[ -d /build ]]; then
|
if [[ -d /build ]]; then
|
||||||
export ESPHOME_BUILD_PATH=/build
|
export ESPHOME_BUILD_PATH=/build
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# The default CMD is "dashboard /config". Route the dashboard to the new
|
|
||||||
# Device Builder, but pass every other subcommand (compile, run, config,
|
|
||||||
# logs, ...) straight through to the esphome CLI so direct CLI use keeps working.
|
|
||||||
if [[ "$1" == "dashboard" ]]; then
|
|
||||||
shift
|
|
||||||
exec esphome-device-builder "$@"
|
|
||||||
fi
|
|
||||||
|
|
||||||
exec esphome "$@"
|
exec esphome "$@"
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
types {
|
||||||
|
text/html html htm shtml;
|
||||||
|
text/css css;
|
||||||
|
text/xml xml;
|
||||||
|
image/gif gif;
|
||||||
|
image/jpeg jpeg jpg;
|
||||||
|
application/javascript js;
|
||||||
|
application/atom+xml atom;
|
||||||
|
application/rss+xml rss;
|
||||||
|
|
||||||
|
text/mathml mml;
|
||||||
|
text/plain txt;
|
||||||
|
text/vnd.sun.j2me.app-descriptor jad;
|
||||||
|
text/vnd.wap.wml wml;
|
||||||
|
text/x-component htc;
|
||||||
|
|
||||||
|
image/png png;
|
||||||
|
image/svg+xml svg svgz;
|
||||||
|
image/tiff tif tiff;
|
||||||
|
image/vnd.wap.wbmp wbmp;
|
||||||
|
image/webp webp;
|
||||||
|
image/x-icon ico;
|
||||||
|
image/x-jng jng;
|
||||||
|
image/x-ms-bmp bmp;
|
||||||
|
|
||||||
|
font/woff woff;
|
||||||
|
font/woff2 woff2;
|
||||||
|
|
||||||
|
application/java-archive jar war ear;
|
||||||
|
application/json json;
|
||||||
|
application/mac-binhex40 hqx;
|
||||||
|
application/msword doc;
|
||||||
|
application/pdf pdf;
|
||||||
|
application/postscript ps eps ai;
|
||||||
|
application/rtf rtf;
|
||||||
|
application/vnd.apple.mpegurl m3u8;
|
||||||
|
application/vnd.google-earth.kml+xml kml;
|
||||||
|
application/vnd.google-earth.kmz kmz;
|
||||||
|
application/vnd.ms-excel xls;
|
||||||
|
application/vnd.ms-fontobject eot;
|
||||||
|
application/vnd.ms-powerpoint ppt;
|
||||||
|
application/vnd.oasis.opendocument.graphics odg;
|
||||||
|
application/vnd.oasis.opendocument.presentation odp;
|
||||||
|
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||||
|
application/vnd.oasis.opendocument.text odt;
|
||||||
|
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
pptx;
|
||||||
|
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
xlsx;
|
||||||
|
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
docx;
|
||||||
|
application/vnd.wap.wmlc wmlc;
|
||||||
|
application/x-7z-compressed 7z;
|
||||||
|
application/x-cocoa cco;
|
||||||
|
application/x-java-archive-diff jardiff;
|
||||||
|
application/x-java-jnlp-file jnlp;
|
||||||
|
application/x-makeself run;
|
||||||
|
application/x-perl pl pm;
|
||||||
|
application/x-pilot prc pdb;
|
||||||
|
application/x-rar-compressed rar;
|
||||||
|
application/x-redhat-package-manager rpm;
|
||||||
|
application/x-sea sea;
|
||||||
|
application/x-shockwave-flash swf;
|
||||||
|
application/x-stuffit sit;
|
||||||
|
application/x-tcl tcl tk;
|
||||||
|
application/x-x509-ca-cert der pem crt;
|
||||||
|
application/x-xpinstall xpi;
|
||||||
|
application/xhtml+xml xhtml;
|
||||||
|
application/xspf+xml xspf;
|
||||||
|
application/zip zip;
|
||||||
|
|
||||||
|
application/octet-stream bin exe dll;
|
||||||
|
application/octet-stream deb;
|
||||||
|
application/octet-stream dmg;
|
||||||
|
application/octet-stream iso img;
|
||||||
|
application/octet-stream msi msp msm;
|
||||||
|
|
||||||
|
audio/midi mid midi kar;
|
||||||
|
audio/mpeg mp3;
|
||||||
|
audio/ogg ogg;
|
||||||
|
audio/x-m4a m4a;
|
||||||
|
audio/x-realaudio ra;
|
||||||
|
|
||||||
|
video/3gpp 3gpp 3gp;
|
||||||
|
video/mp2t ts;
|
||||||
|
video/mp4 mp4;
|
||||||
|
video/mpeg mpeg mpg;
|
||||||
|
video/quicktime mov;
|
||||||
|
video/webm webm;
|
||||||
|
video/x-flv flv;
|
||||||
|
video/x-m4v m4v;
|
||||||
|
video/x-mng mng;
|
||||||
|
video/x-ms-asf asx asf;
|
||||||
|
video/x-ms-wmv wmv;
|
||||||
|
video/x-msvideo avi;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_ignore_client_abort off;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_send_timeout 86400s;
|
||||||
|
proxy_max_temp_file_size 0;
|
||||||
|
|
||||||
|
proxy_set_header Accept-Encoding "";
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-NginX-Proxy true;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header Authorization "";
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
root /dev/null;
|
||||||
|
server_name $hostname;
|
||||||
|
|
||||||
|
client_max_body_size 512m;
|
||||||
|
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header X-Robots-Tag none;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers off;
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
upstream esphome {
|
||||||
|
server unix:/var/run/esphome.sock;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
daemon off;
|
||||||
|
user root;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
worker_processes 1;
|
||||||
|
error_log /proc/1/fd/1 error;
|
||||||
|
events {
|
||||||
|
worker_connections 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/includes/mime.types;
|
||||||
|
|
||||||
|
access_log off;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
gzip on;
|
||||||
|
keepalive_timeout 65;
|
||||||
|
sendfile on;
|
||||||
|
server_tokens off;
|
||||||
|
|
||||||
|
tcp_nodelay on;
|
||||||
|
tcp_nopush on;
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
include /etc/nginx/includes/upstream.conf;
|
||||||
|
include /etc/nginx/servers/*.conf;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
server {
|
||||||
|
{{ if not .ssl }}
|
||||||
|
listen 6052 default_server;
|
||||||
|
{{ else }}
|
||||||
|
listen 6052 default_server ssl http2;
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
include /etc/nginx/includes/server_params.conf;
|
||||||
|
include /etc/nginx/includes/proxy_params.conf;
|
||||||
|
|
||||||
|
{{ if .ssl }}
|
||||||
|
include /etc/nginx/includes/ssl_params.conf;
|
||||||
|
|
||||||
|
ssl_certificate /ssl/{{ .certfile }};
|
||||||
|
ssl_certificate_key /ssl/{{ .keyfile }};
|
||||||
|
|
||||||
|
# Redirect http requests to https on the same port.
|
||||||
|
# https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/
|
||||||
|
error_page 497 https://$http_host$request_uri;
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
# Clear Home Assistant Ingress header
|
||||||
|
proxy_set_header X-HA-Ingress "";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://esphome;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
server {
|
||||||
|
listen 127.0.0.1:{{ .port }} default_server;
|
||||||
|
listen {{ .interface }}:{{ .port }} default_server;
|
||||||
|
|
||||||
|
include /etc/nginx/includes/server_params.conf;
|
||||||
|
include /etc/nginx/includes/proxy_params.conf;
|
||||||
|
|
||||||
|
# Set Home Assistant Ingress header
|
||||||
|
proxy_set_header X-HA-Ingress "YES";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
allow 172.30.32.2;
|
||||||
|
allow 127.0.0.1;
|
||||||
|
deny all;
|
||||||
|
|
||||||
|
proxy_pass http://esphome;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ fi
|
|||||||
|
|
||||||
port=$(bashio::addon.ingress_port)
|
port=$(bashio::addon.ingress_port)
|
||||||
|
|
||||||
# Wait for the ESPHome Device Builder to become available
|
# Wait for NGINX to become available
|
||||||
bashio::net.wait_for "${port}" "127.0.0.1" 300
|
bashio::net.wait_for "${port}" "127.0.0.1" 300
|
||||||
|
|
||||||
config=$(\
|
config=$(\
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Home Assistant Community Add-on: ESPHome
|
# Home Assistant Community Add-on: ESPHome
|
||||||
# Take down the S6 supervision tree when ESPHome Device Builder fails
|
# Take down the S6 supervision tree when ESPHome dashboard fails
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
declare exit_code
|
declare exit_code
|
||||||
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
|
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
|
||||||
@@ -10,7 +10,7 @@ readonly exit_code_service="${1}"
|
|||||||
readonly exit_code_signal="${2}"
|
readonly exit_code_signal="${2}"
|
||||||
|
|
||||||
bashio::log.info \
|
bashio::log.info \
|
||||||
"Service ESPHome Device Builder exited with code ${exit_code_service}" \
|
"Service ESPHome dashboard exited with code ${exit_code_service}" \
|
||||||
"(by signal ${exit_code_signal})"
|
"(by signal ${exit_code_signal})"
|
||||||
|
|
||||||
if [[ "${exit_code_service}" -eq 256 ]]; then
|
if [[ "${exit_code_service}" -eq 256 ]]; then
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# shellcheck shell=bash
|
# shellcheck shell=bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Community Hass.io Add-ons: ESPHome
|
# Community Hass.io Add-ons: ESPHome
|
||||||
# Runs the ESPHome Device Builder
|
# Runs the ESPHome dashboard
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
readonly pio_cache_base=/data/cache/platformio
|
readonly pio_cache_base=/data/cache/platformio
|
||||||
|
|
||||||
@@ -15,15 +15,18 @@ export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms"
|
|||||||
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages"
|
||||||
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache"
|
||||||
|
|
||||||
# Keep the native toolchain installs on the persistent /data volume, not the
|
|
||||||
# container's ephemeral user cache dir (wiped on every add-on update/restart).
|
|
||||||
export ESPHOME_ESP_IDF_PREFIX=/data/cache/idf
|
|
||||||
export ESPHOME_SDK_NRF_PREFIX=/data/cache/sdk-nrf
|
|
||||||
|
|
||||||
if bashio::config.true 'leave_front_door_open'; then
|
if bashio::config.true 'leave_front_door_open'; then
|
||||||
export DISABLE_HA_AUTHENTICATION=true
|
export DISABLE_HA_AUTHENTICATION=true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if bashio::config.true 'streamer_mode'; then
|
||||||
|
export ESPHOME_STREAMER_MODE=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if bashio::config.has_value 'relative_url'; then
|
||||||
|
export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url')
|
||||||
|
fi
|
||||||
|
|
||||||
if bashio::config.has_value 'default_compile_process_limit'; then
|
if bashio::config.has_value 'default_compile_process_limit'; then
|
||||||
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit')
|
export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit')
|
||||||
else
|
else
|
||||||
@@ -46,21 +49,5 @@ if bashio::fs.directory_exists '/config/esphome/.esphome'; then
|
|||||||
rm -rf /config/esphome/.esphome
|
rm -rf /config/esphome/.esphome
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Only signal device-builder to expose the public LAN port when the operator
|
bashio::log.info "Starting ESPHome dashboard..."
|
||||||
# mapped port 6052, matching the legacy dashboard where nginx listened on the
|
exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon
|
||||||
# fixed port 6052 only when it was configured. We use the mapping purely as a
|
|
||||||
# presence check and don't forward the published value; device-builder binds
|
|
||||||
# its default port 6052 (the fixed container port, as the legacy
|
|
||||||
# "listen 6052" did). --ha-addon-allow-public is inert on its own: the no-auth
|
|
||||||
# gate is the DISABLE_HA_AUTHENTICATION env var set above, so both opt-ins are
|
|
||||||
# required to bind 6052 unauthenticated; either alone stays ingress-only.
|
|
||||||
set --
|
|
||||||
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
|
|
||||||
set -- --ha-addon-allow-public
|
|
||||||
fi
|
|
||||||
|
|
||||||
bashio::log.info "Starting ESPHome Device Builder..."
|
|
||||||
exec esphome-device-builder /config/esphome \
|
|
||||||
--ha-addon \
|
|
||||||
--ingress-port "$(bashio::addon.ingress_port)" \
|
|
||||||
"$@"
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/command/with-contenv bashio
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# ==============================================================================
|
||||||
|
# Community Hass.io Add-ons: ESPHome
|
||||||
|
# Configures NGINX for use with ESPHome
|
||||||
|
# ==============================================================================
|
||||||
|
mkdir -p /var/log/nginx
|
||||||
|
|
||||||
|
# Generate Ingress configuration
|
||||||
|
bashio::var.json \
|
||||||
|
interface "$(bashio::addon.ip_address)" \
|
||||||
|
port "^$(bashio::addon.ingress_port)" \
|
||||||
|
| tempio \
|
||||||
|
-template /etc/nginx/templates/ingress.gtpl \
|
||||||
|
-out /etc/nginx/servers/ingress.conf
|
||||||
|
|
||||||
|
# Generate direct access configuration, if enabled.
|
||||||
|
if bashio::var.has_value "$(bashio::addon.port 6052)"; then
|
||||||
|
bashio::config.require.ssl
|
||||||
|
bashio::var.json \
|
||||||
|
certfile "$(bashio::config 'certfile')" \
|
||||||
|
keyfile "$(bashio::config 'keyfile')" \
|
||||||
|
ssl "^$(bashio::config 'ssl')" \
|
||||||
|
| tempio \
|
||||||
|
-template /etc/nginx/templates/direct.gtpl \
|
||||||
|
-out /etc/nginx/servers/direct.conf
|
||||||
|
fi
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
oneshot
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/etc/s6-overlay/s6-rc.d/init-nginx/run
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/command/with-contenv bashio
|
||||||
|
# ==============================================================================
|
||||||
|
# Community Hass.io Add-ons: ESPHome
|
||||||
|
# Take down the S6 supervision tree when NGINX fails
|
||||||
|
# ==============================================================================
|
||||||
|
declare exit_code
|
||||||
|
readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode)
|
||||||
|
readonly exit_code_service="${1}"
|
||||||
|
readonly exit_code_signal="${2}"
|
||||||
|
|
||||||
|
bashio::log.info \
|
||||||
|
"Service NGINX exited with code ${exit_code_service}" \
|
||||||
|
"(by signal ${exit_code_signal})"
|
||||||
|
|
||||||
|
if [[ "${exit_code_service}" -eq 256 ]]; then
|
||||||
|
if [[ "${exit_code_container}" -eq 0 ]]; then
|
||||||
|
echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode
|
||||||
|
fi
|
||||||
|
[[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt
|
||||||
|
elif [[ "${exit_code_service}" -ne 0 ]]; then
|
||||||
|
if [[ "${exit_code_container}" -eq 0 ]]; then
|
||||||
|
echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode
|
||||||
|
fi
|
||||||
|
exec /run/s6/basedir/bin/halt
|
||||||
|
fi
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
#!/command/with-contenv bashio
|
||||||
|
# shellcheck shell=bash
|
||||||
|
# ==============================================================================
|
||||||
|
# Community Hass.io Add-ons: ESPHome
|
||||||
|
# Runs the NGINX proxy
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
bashio::log.info "Waiting for ESPHome dashboard to come up..."
|
||||||
|
|
||||||
|
while [[ ! -S /var/run/esphome.sock ]]; do
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
bashio::log.info "Starting NGINX..."
|
||||||
|
exec nginx
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
longrun
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-bk72xx-arduino
|
|
||||||
|
|
||||||
bk72xx:
|
|
||||||
board: generic-bk7231n-qfn32-tuya
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-ard-idf
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: arduino
|
|
||||||
toolchain: esp-idf
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-ard-pio
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: arduino
|
|
||||||
toolchain: platformio
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-idf-idf
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: esp-idf
|
|
||||||
toolchain: esp-idf
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp32-idf-pio
|
|
||||||
|
|
||||||
esp32:
|
|
||||||
variant: esp32
|
|
||||||
framework:
|
|
||||||
type: esp-idf
|
|
||||||
toolchain: platformio
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-esp8266-arduino
|
|
||||||
|
|
||||||
esp8266:
|
|
||||||
board: d1_mini
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-host
|
|
||||||
|
|
||||||
host:
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-ln882x-arduino
|
|
||||||
|
|
||||||
ln882x:
|
|
||||||
board: generic-ln882h
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-nrf52
|
|
||||||
|
|
||||||
nrf52:
|
|
||||||
board: adafruit_itsybitsy_nrf52840
|
|
||||||
bootloader: adafruit_nrf52_sd140_v6
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-rp2040-arduino
|
|
||||||
|
|
||||||
rp2040:
|
|
||||||
variant: rp2040
|
|
||||||
|
|
||||||
logger:
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
esphome:
|
|
||||||
name: docker-test-rtl87xx-arduino
|
|
||||||
|
|
||||||
rtl87xx:
|
|
||||||
board: generic-rtl8710bn-2mb-788k
|
|
||||||
|
|
||||||
logger:
|
|
||||||
+229
-954
File diff suppressed because it is too large
Load Diff
@@ -101,17 +101,6 @@ class AddressCache:
|
|||||||
"""Check if any cache entries exist."""
|
"""Check if any cache entries exist."""
|
||||||
return bool(self.mdns_cache or self.dns_cache)
|
return bool(self.mdns_cache or self.dns_cache)
|
||||||
|
|
||||||
def add_mdns_addresses(self, hostname: str, addresses: list[str]) -> None:
|
|
||||||
"""Store resolved mDNS addresses for ``hostname`` in the cache.
|
|
||||||
|
|
||||||
Callers that discover ``.local`` hosts (e.g. via mDNS browse) can use
|
|
||||||
this to avoid a second resolution round-trip during the upload path.
|
|
||||||
No-op when ``addresses`` is empty.
|
|
||||||
"""
|
|
||||||
if not addresses:
|
|
||||||
return
|
|
||||||
self.mdns_cache[normalize_hostname(hostname)] = addresses
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_cli_args(
|
def from_cli_args(
|
||||||
cls, mdns_args: Iterable[str], dns_args: Iterable[str]
|
cls, mdns_args: Iterable[str], dns_args: Iterable[str]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from .helpers import (
|
|||||||
from .toolchain import find_tool, resolve_tool_path, run_tool
|
from .toolchain import find_tool, resolve_tool_path, run_tool
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from esphome.platformio.toolchain import IDEData
|
from esphome.platformio_api import IDEData
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -793,11 +793,8 @@ class MemoryAnalyzer:
|
|||||||
"""Scan ESPHome source object files to map extern "C" symbols to components.
|
"""Scan ESPHome source object files to map extern "C" symbols to components.
|
||||||
|
|
||||||
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
|
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
|
||||||
under ``src/`` (including ``src/main.cpp.o`` and everything beneath
|
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
|
||||||
``src/esphome/``) and build a symbol-to-component mapping. This catches
|
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
|
||||||
``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()``
|
|
||||||
entry points in ``main.cpp``, and other symbols that lack C++ namespace
|
|
||||||
prefixes.
|
|
||||||
|
|
||||||
Skips scanning if ``_source_symbol_map`` was already populated by
|
Skips scanning if ``_source_symbol_map`` was already populated by
|
||||||
``_parse_map_file()``.
|
``_parse_map_file()``.
|
||||||
@@ -809,12 +806,12 @@ class MemoryAnalyzer:
|
|||||||
if obj_dir is None:
|
if obj_dir is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/...
|
# Find ESPHome source object files
|
||||||
src_dir = obj_dir / "src"
|
esphome_src_dir = obj_dir / "src" / "esphome"
|
||||||
if not src_dir.is_dir():
|
if not esphome_src_dir.is_dir():
|
||||||
return
|
return
|
||||||
|
|
||||||
obj_files = sorted(src_dir.rglob("*.o"))
|
obj_files = sorted(esphome_src_dir.rglob("*.o"))
|
||||||
if not obj_files:
|
if not obj_files:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1067,10 +1064,6 @@ class MemoryAnalyzer:
|
|||||||
if component_name in self.external_components:
|
if component_name in self.external_components:
|
||||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
||||||
|
|
||||||
# ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop())
|
|
||||||
if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"):
|
|
||||||
return _COMPONENT_CORE
|
|
||||||
|
|
||||||
# ESPHome core: src/esphome/core/... or src/esphome/...
|
# ESPHome core: src/esphome/core/... or src/esphome/...
|
||||||
if "core" in parts and "esphome" in parts:
|
if "core" in parts and "esphome" in parts:
|
||||||
return _COMPONENT_CORE
|
return _COMPONENT_CORE
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ from __future__ import annotations
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import heapq
|
import heapq
|
||||||
import json
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -21,7 +19,6 @@ from . import (
|
|||||||
RAM_SECTIONS,
|
RAM_SECTIONS,
|
||||||
MemoryAnalyzer,
|
MemoryAnalyzer,
|
||||||
)
|
)
|
||||||
from .toolchain import find_elf_path, find_idedata_path, idedata_candidates
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import ComponentMemory
|
from . import ComponentMemory
|
||||||
@@ -43,7 +40,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
|
|
||||||
# Symbol size threshold for detailed analysis
|
# Symbol size threshold for detailed analysis
|
||||||
SYMBOL_SIZE_THRESHOLD: int = (
|
SYMBOL_SIZE_THRESHOLD: int = (
|
||||||
10 # Show symbols larger than this in detailed analysis
|
100 # Show symbols larger than this in detailed analysis
|
||||||
)
|
)
|
||||||
# Lower threshold for RAM symbols (RAM is more constrained)
|
# Lower threshold for RAM symbols (RAM is more constrained)
|
||||||
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
||||||
@@ -512,7 +509,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
lines.append(
|
lines.append(
|
||||||
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
||||||
)
|
)
|
||||||
for i, (_symbol, demangled, size) in enumerate(large_core_symbols):
|
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
|
||||||
# Core symbols only track (symbol, demangled, size) without section info,
|
# Core symbols only track (symbol, demangled, size) without section info,
|
||||||
# so we don't show section labels here
|
# so we don't show section labels here
|
||||||
lines.append(
|
lines.append(
|
||||||
@@ -604,7 +601,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
lines.append(
|
lines.append(
|
||||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
|
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
|
||||||
)
|
)
|
||||||
for i, (_symbol, demangled, size, section) in enumerate(large_symbols):
|
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
|
||||||
lines.append(
|
lines.append(
|
||||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
||||||
)
|
)
|
||||||
@@ -643,7 +640,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
lines.append(
|
lines.append(
|
||||||
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
|
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
|
||||||
)
|
)
|
||||||
for _symbol, demangled, size, section in large_ram_syms[:10]:
|
for symbol, demangled, size, section in large_ram_syms[:10]:
|
||||||
# Format section label consistently by stripping leading dot
|
# Format section label consistently by stripping leading dot
|
||||||
section_label = section.lstrip(".") if section else ""
|
section_label = section.lstrip(".") if section else ""
|
||||||
display_name = _format_pstorage_name(demangled)
|
display_name = _format_pstorage_name(demangled)
|
||||||
@@ -702,7 +699,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
|||||||
content = "\n".join(lines)
|
content = "\n".join(lines)
|
||||||
|
|
||||||
if output_file:
|
if output_file:
|
||||||
with Path(output_file).open("w", encoding="utf-8") as f:
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
else:
|
else:
|
||||||
print(content)
|
print(content)
|
||||||
@@ -740,8 +737,9 @@ def main():
|
|||||||
|
|
||||||
# Load build directory
|
# Load build directory
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from esphome.platformio.toolchain import IDEData
|
from esphome.platformio_api import IDEData
|
||||||
|
|
||||||
build_path = Path(build_dir)
|
build_path = Path(build_dir)
|
||||||
|
|
||||||
@@ -761,25 +759,45 @@ def main():
|
|||||||
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elf_path = find_elf_path(build_path)
|
# Find firmware.elf
|
||||||
if not elf_path:
|
elf_file = None
|
||||||
print(f"Error: no firmware ELF found in {build_dir}", file=sys.stderr)
|
for elf_candidate in [
|
||||||
|
build_path / "firmware.elf",
|
||||||
|
build_path / ".pioenvs" / build_path.name / "firmware.elf",
|
||||||
|
]:
|
||||||
|
if elf_candidate.exists():
|
||||||
|
elf_file = str(elf_candidate)
|
||||||
|
break
|
||||||
|
|
||||||
|
if not elf_file:
|
||||||
|
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elf_file = str(elf_path)
|
|
||||||
|
# Find idedata.json - check current directory first, then home
|
||||||
|
device_name = build_path.name
|
||||||
|
idedata_candidates = [
|
||||||
|
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||||
|
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||||
|
]
|
||||||
|
|
||||||
idedata = None
|
idedata = None
|
||||||
if idedata_path := find_idedata_path(build_path):
|
for idedata_path in idedata_candidates:
|
||||||
|
if not idedata_path.exists():
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
with idedata_path.open(encoding="utf-8") as f:
|
with open(idedata_path, encoding="utf-8") as f:
|
||||||
raw_data = json.load(f)
|
raw_data = json.load(f)
|
||||||
idedata = IDEData(raw_data)
|
idedata = IDEData(raw_data)
|
||||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
||||||
|
break
|
||||||
except (json.JSONDecodeError, OSError) as e:
|
except (json.JSONDecodeError, OSError) as e:
|
||||||
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
||||||
|
|
||||||
if not idedata:
|
if not idedata:
|
||||||
searched = "\n ".join(str(p) for p in idedata_candidates(build_path))
|
print(
|
||||||
print(f"Warning: idedata not found, searched:\n {searched}", file=sys.stderr)
|
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
||||||
analyzer.analyze()
|
analyzer.analyze()
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ def batch_demangle(
|
|||||||
failed_count = 0
|
failed_count = 0
|
||||||
|
|
||||||
for original, stripped, prefix, demangled in zip(
|
for original, stripped, prefix, demangled in zip(
|
||||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines, strict=True
|
symbols, symbols_stripped, symbols_prefixes, demangled_lines
|
||||||
):
|
):
|
||||||
# Add back any prefix that was removed
|
# Add back any prefix that was removed
|
||||||
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ memory-constrained platforms like ESP8266.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
@@ -65,7 +65,6 @@ class RamSymbol:
|
|||||||
size: int
|
size: int
|
||||||
section: str
|
section: str
|
||||||
demangled: str = "" # Demangled name, set after batch demangling
|
demangled: str = "" # Demangled name, set after batch demangling
|
||||||
aliases: list[str] = field(default_factory=list) # Other names at same address
|
|
||||||
|
|
||||||
|
|
||||||
class RamStringsAnalyzer:
|
class RamStringsAnalyzer:
|
||||||
@@ -236,11 +235,6 @@ class RamStringsAnalyzer:
|
|||||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Track symbols by address so aliases (multiple names for the same
|
|
||||||
# object, e.g. the newlib __lock___* mutexes that all alias one
|
|
||||||
# StaticSemaphore_t) are reported once instead of once per name.
|
|
||||||
symbols_by_addr: dict[int, RamSymbol] = {}
|
|
||||||
|
|
||||||
for line in output.split("\n"):
|
for line in output.split("\n"):
|
||||||
parts = line.split()
|
parts = line.split()
|
||||||
if len(parts) < 4:
|
if len(parts) < 4:
|
||||||
@@ -259,18 +253,6 @@ class RamStringsAnalyzer:
|
|||||||
if sym_type not in DATA_SYMBOL_TYPES:
|
if sym_type not in DATA_SYMBOL_TYPES:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (existing := symbols_by_addr.get(addr)) is not None:
|
|
||||||
# Prefer a global (uppercase type) name as the primary so
|
|
||||||
# nm output order can't hide it behind a local alias.
|
|
||||||
if sym_type.isupper() and existing.sym_type.islower():
|
|
||||||
existing.aliases.append(existing.name)
|
|
||||||
existing.name = name
|
|
||||||
existing.sym_type = sym_type
|
|
||||||
else:
|
|
||||||
existing.aliases.append(name)
|
|
||||||
existing.size = max(existing.size, size)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Check if symbol is in a RAM section
|
# Check if symbol is in a RAM section
|
||||||
for section_name in self.ram_sections:
|
for section_name in self.ram_sections:
|
||||||
if section_name not in self.sections:
|
if section_name not in self.sections:
|
||||||
@@ -278,15 +260,15 @@ class RamStringsAnalyzer:
|
|||||||
|
|
||||||
section = self.sections[section_name]
|
section = self.sections[section_name]
|
||||||
if section.address <= addr < section.address + section.size:
|
if section.address <= addr < section.address + section.size:
|
||||||
symbol = RamSymbol(
|
self.ram_symbols.append(
|
||||||
name=name,
|
RamSymbol(
|
||||||
sym_type=sym_type,
|
name=name,
|
||||||
address=addr,
|
sym_type=sym_type,
|
||||||
size=size,
|
address=addr,
|
||||||
section=section_name,
|
size=size,
|
||||||
|
section=section_name,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
symbols_by_addr[addr] = symbol
|
|
||||||
self.ram_symbols.append(symbol)
|
|
||||||
break
|
break
|
||||||
|
|
||||||
def _demangle_symbols(self) -> None:
|
def _demangle_symbols(self) -> None:
|
||||||
@@ -454,13 +436,7 @@ class RamStringsAnalyzer:
|
|||||||
for symbol in largest_symbols:
|
for symbol in largest_symbols:
|
||||||
# Use demangled name if available, otherwise raw name
|
# Use demangled name if available, otherwise raw name
|
||||||
display_name = symbol.demangled or symbol.name
|
display_name = symbol.demangled or symbol.name
|
||||||
# Truncate the name, not the alias note, so merged aliases stay
|
name_display = display_name[:49] if len(display_name) > 49 else display_name
|
||||||
# visible even for long demangled C++ names.
|
|
||||||
alias_note = f" (+{len(symbol.aliases)} aliases)" if symbol.aliases else ""
|
|
||||||
max_name_len = 49 - len(alias_note)
|
|
||||||
if len(display_name) > max_name_len:
|
|
||||||
display_name = display_name[:max_name_len]
|
|
||||||
name_display = display_name + alias_note
|
|
||||||
lines.append(
|
lines.append(
|
||||||
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@@ -23,78 +24,6 @@ TOOLCHAIN_PREFIXES = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def find_elf_path(build_path: Path) -> Path | None:
|
|
||||||
"""Locate the firmware ELF inside an ESPHome build directory.
|
|
||||||
|
|
||||||
The layout depends on the toolchain that produced the build, so try each
|
|
||||||
known one in turn.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_path: Path to an ESPHome build directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the ELF file, or None if no known layout matches
|
|
||||||
"""
|
|
||||||
name = build_path.name
|
|
||||||
for candidate in (
|
|
||||||
# Native ESP-IDF: idf.py writes build/<name>.elf, which ESPHome copies
|
|
||||||
# to build/firmware.elf (see espidf.toolchain.create_elf_copy)
|
|
||||||
build_path / "build" / "firmware.elf",
|
|
||||||
# PlatformIO
|
|
||||||
build_path / "firmware.elf",
|
|
||||||
build_path / ".pioenvs" / name / "firmware.elf",
|
|
||||||
# LibreTiny uses raw_firmware.elf
|
|
||||||
build_path / "raw_firmware.elf",
|
|
||||||
build_path / ".pioenvs" / name / "raw_firmware.elf",
|
|
||||||
# Zephyr (nRF52); the SDK nests the artifacts one level deeper from 2.9.2
|
|
||||||
build_path / ".pioenvs" / name / "zephyr" / "zephyr" / "zephyr.elf",
|
|
||||||
build_path / ".pioenvs" / name / "zephyr" / "zephyr.elf",
|
|
||||||
):
|
|
||||||
if candidate.is_file():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def idedata_candidates(build_path: Path) -> list[Path]:
|
|
||||||
"""Return the idedata locations searched for a build directory, in order.
|
|
||||||
|
|
||||||
Exposed so a caller reporting "not found" can name the paths it tried
|
|
||||||
without keeping its own copy of the list.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_path: Path to an ESPHome build directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The candidate idedata JSON paths, most specific first
|
|
||||||
"""
|
|
||||||
name = build_path.name
|
|
||||||
return [
|
|
||||||
# In .pioenvs for test builds
|
|
||||||
build_path / ".pioenvs" / name / "idedata.json",
|
|
||||||
# Both toolchains cache it in the data dir, which holds this build dir:
|
|
||||||
# <data_dir>/idedata/<name>.json next to <data_dir>/build/<name>
|
|
||||||
build_path.parent.parent / "idedata" / f"{name}.json",
|
|
||||||
# Regular builds, invoked from the config dir or from anywhere
|
|
||||||
Path.cwd() / ".esphome" / "idedata" / f"{name}.json",
|
|
||||||
Path.home() / ".esphome" / "idedata" / f"{name}.json",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def find_idedata_path(build_path: Path) -> Path | None:
|
|
||||||
"""Locate the idedata JSON belonging to an ESPHome build directory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
build_path: Path to an ESPHome build directory
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the idedata JSON, or None if it was not found
|
|
||||||
"""
|
|
||||||
for candidate in idedata_candidates(build_path):
|
|
||||||
if candidate.is_file():
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
||||||
"""Search for a tool in PlatformIO package directories.
|
"""Search for a tool in PlatformIO package directories.
|
||||||
|
|
||||||
@@ -108,7 +37,7 @@ def _find_in_platformio_packages(tool_name: str) -> str | None:
|
|||||||
Full path to the tool or None if not found
|
Full path to the tool or None if not found
|
||||||
"""
|
"""
|
||||||
# Get PlatformIO packages directory
|
# Get PlatformIO packages directory
|
||||||
platformio_home = Path("~/.platformio/packages").expanduser()
|
platformio_home = Path(os.path.expanduser("~/.platformio/packages"))
|
||||||
if not platformio_home.exists():
|
if not platformio_home.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
"""Helpers for running an async coroutine from sync code via a daemon thread.
|
|
||||||
|
|
||||||
``asyncio.run(coro())`` in the main thread blocks until the loop's cleanup
|
|
||||||
cycle finishes, which can add hundreds of milliseconds before the caller
|
|
||||||
receives the result. Running the loop in a daemon thread lets the caller
|
|
||||||
observe the result as soon as the coroutine completes while cleanup finishes
|
|
||||||
in the background.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
from collections.abc import Awaitable, Callable
|
|
||||||
import threading
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncThreadRunner[T](threading.Thread):
|
|
||||||
"""Run an async coroutine in a daemon thread and expose its result.
|
|
||||||
|
|
||||||
The runner catches all exceptions from the coroutine and stores them in
|
|
||||||
``exception`` so ``event`` is always set — this prevents callers waiting
|
|
||||||
on ``event`` from hanging forever when the coroutine crashes.
|
|
||||||
|
|
||||||
Typical usage::
|
|
||||||
|
|
||||||
runner = AsyncThreadRunner(lambda: my_coro(arg))
|
|
||||||
runner.start()
|
|
||||||
if not runner.event.wait(timeout=5.0):
|
|
||||||
... # timed out
|
|
||||||
if runner.exception is not None:
|
|
||||||
raise runner.exception
|
|
||||||
result = runner.result
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, coro_factory: Callable[[], Awaitable[T]]) -> None:
|
|
||||||
super().__init__(daemon=True)
|
|
||||||
self._coro_factory = coro_factory
|
|
||||||
self.result: T | None = None
|
|
||||||
self.exception: BaseException | None = None
|
|
||||||
self.event = threading.Event()
|
|
||||||
|
|
||||||
async def _runner(self) -> None:
|
|
||||||
try:
|
|
||||||
self.result = await self._coro_factory()
|
|
||||||
except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except
|
|
||||||
# Capture all exceptions so ``event`` is always set — otherwise a
|
|
||||||
# crash would hang the waiter forever.
|
|
||||||
self.exception = exc
|
|
||||||
finally:
|
|
||||||
self.event.set()
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
asyncio.run(self._runner())
|
|
||||||
+7
-38
@@ -1,4 +1,3 @@
|
|||||||
from dataclasses import dataclass, field
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
@@ -127,7 +126,7 @@ def validate_potentially_or_condition(value):
|
|||||||
return validate_condition(value)
|
return validate_condition(value)
|
||||||
|
|
||||||
|
|
||||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action)
|
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
||||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
||||||
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
||||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||||
@@ -199,10 +198,11 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
|||||||
return cv.Schema([schema])(value)
|
return cv.Schema([schema])(value)
|
||||||
except cv.Invalid as err2:
|
except cv.Invalid as err2:
|
||||||
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
||||||
raise err from None
|
# pylint: disable=raise-missing-from
|
||||||
|
raise err
|
||||||
if "Unable to find action" in str(err):
|
if "Unable to find action" in str(err):
|
||||||
raise err2 from None
|
raise err2
|
||||||
raise cv.MultipleInvalid([err, err2]) from None
|
raise cv.MultipleInvalid([err, err2])
|
||||||
elif isinstance(value, dict):
|
elif isinstance(value, dict):
|
||||||
if CONF_THEN in value:
|
if CONF_THEN in value:
|
||||||
return [schema(value)]
|
return [schema(value)]
|
||||||
@@ -396,6 +396,7 @@ async def delay_action_to_code(
|
|||||||
args: TemplateArgsType,
|
args: TemplateArgsType,
|
||||||
) -> MockObj:
|
) -> MockObj:
|
||||||
var = cg.new_Pvariable(action_id, template_arg)
|
var = cg.new_Pvariable(action_id, template_arg)
|
||||||
|
await cg.register_component(var, {})
|
||||||
template_ = await cg.templatable(config, args, cg.uint32)
|
template_ = await cg.templatable(config, args, cg.uint32)
|
||||||
cg.add(var.set_delay(template_))
|
cg.add(var.set_delay(template_))
|
||||||
return var
|
return var
|
||||||
@@ -596,7 +597,7 @@ async def component_resume_action_to_code(
|
|||||||
comp = await cg.get_variable(config[CONF_ID])
|
comp = await cg.get_variable(config[CONF_ID])
|
||||||
var = cg.new_Pvariable(action_id, template_arg, comp)
|
var = cg.new_Pvariable(action_id, template_arg, comp)
|
||||||
if CONF_UPDATE_INTERVAL in config:
|
if CONF_UPDATE_INTERVAL in config:
|
||||||
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
|
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
|
||||||
cg.add(var.set_update_interval(template_))
|
cg.add(var.set_update_interval(template_))
|
||||||
return var
|
return var
|
||||||
|
|
||||||
@@ -714,35 +715,3 @@ async def build_callback_automation(
|
|||||||
# MockObjs (not user input), and there's no Expression type for positional
|
# MockObjs (not user input), and there's no Expression type for positional
|
||||||
# aggregate initialization (StructInitializer uses named fields).
|
# aggregate initialization (StructInitializer uses named fields).
|
||||||
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
|
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
|
||||||
class CallbackAutomation:
|
|
||||||
"""A single callback automation entry for build_callback_automations."""
|
|
||||||
|
|
||||||
conf_key: str
|
|
||||||
callback_method: str
|
|
||||||
args: TemplateArgsType = field(default_factory=list)
|
|
||||||
forwarder: MockObj | MockObjClass | None = None
|
|
||||||
|
|
||||||
|
|
||||||
async def build_callback_automations(
|
|
||||||
parent: MockObj,
|
|
||||||
config: ConfigType,
|
|
||||||
entries: tuple[CallbackAutomation, ...],
|
|
||||||
) -> None:
|
|
||||||
"""Build multiple callback automations from a tuple of entries.
|
|
||||||
|
|
||||||
:param parent: The component object (e.g., button, sensor).
|
|
||||||
:param config: The full component config dict.
|
|
||||||
:param entries: Tuple of CallbackAutomation entries to process.
|
|
||||||
"""
|
|
||||||
for entry in entries:
|
|
||||||
for conf in config.get(entry.conf_key, []):
|
|
||||||
await build_callback_automation(
|
|
||||||
parent,
|
|
||||||
entry.callback_method,
|
|
||||||
entry.args,
|
|
||||||
conf,
|
|
||||||
forwarder=entry.forwarder,
|
|
||||||
)
|
|
||||||
|
|||||||
+45
-159
@@ -3,43 +3,23 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from esphome.components.esp32 import get_esp32_variant, idf_version
|
from esphome.components.esp32 import get_esp32_variant
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.framework_helpers import (
|
|
||||||
get_project_compile_flags,
|
|
||||||
get_project_cxx_compile_flags,
|
|
||||||
get_project_link_flags,
|
|
||||||
)
|
|
||||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||||
|
|
||||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
|
||||||
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
|
||||||
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
|
||||||
# i.e. after IDF appends its default and before the options are consumed, and
|
|
||||||
# applies project-wide like PlatformIO build_unflags.
|
|
||||||
CPP_STANDARD_TEMPLATE = """\
|
|
||||||
idf_build_get_property(esphome_cxx_compile_options CXX_COMPILE_OPTIONS)
|
|
||||||
list(FILTER esphome_cxx_compile_options EXCLUDE REGEX "^-std=")
|
|
||||||
list(APPEND esphome_cxx_compile_options "-std={standard}")
|
|
||||||
idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")"""
|
|
||||||
|
|
||||||
|
|
||||||
def get_available_components() -> list[str] | None:
|
def get_available_components() -> list[str] | None:
|
||||||
"""Get list of built-in ESP-IDF components from project_description.json.
|
"""Get list of available ESP-IDF components from project_description.json.
|
||||||
|
|
||||||
Excludes ``src``, IDF-managed components (``managed_components/``), and
|
Returns only internal ESP-IDF components, excluding external/managed
|
||||||
converted PIO libs (``pio_components/``). Returns ``None`` if the build
|
components (from idf_component.yml).
|
||||||
dir or ``project_description.json`` isn't ready yet.
|
|
||||||
"""
|
"""
|
||||||
if CORE.build_path is None:
|
|
||||||
return None
|
|
||||||
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
|
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
|
||||||
if not project_desc.exists():
|
if not project_desc.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with project_desc.open(encoding="utf-8") as f:
|
with open(project_desc, encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
component_info = data.get("build_component_info", {})
|
component_info = data.get("build_component_info", {})
|
||||||
@@ -50,9 +30,9 @@ def get_available_components() -> list[str] | None:
|
|||||||
if name == "src":
|
if name == "src":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Exclude IDF-managed and converted-PIO components (external).
|
# Exclude managed/external components
|
||||||
comp_dir = info.get("dir", "")
|
comp_dir = info.get("dir", "")
|
||||||
if "managed_components" in comp_dir or "pio_components" in comp_dir:
|
if "managed_components" in comp_dir:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
result.append(name)
|
result.append(name)
|
||||||
@@ -67,166 +47,72 @@ def has_discovered_components() -> bool:
|
|||||||
return get_available_components() is not None
|
return get_available_components() is not None
|
||||||
|
|
||||||
|
|
||||||
def get_project_cmakelists(minimal: bool = False) -> str:
|
def get_project_cmakelists() -> str:
|
||||||
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
|
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
|
||||||
|
|
||||||
When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS``
|
|
||||||
since ``project_description.json`` may be stale on the first write.
|
|
||||||
"""
|
|
||||||
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
||||||
variant = get_esp32_variant()
|
variant = get_esp32_variant()
|
||||||
idf_target = variant.lower().replace("-", "")
|
idf_target = variant.lower().replace("-", "")
|
||||||
|
|
||||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")]
|
||||||
# --format=raw because the legacy mode doesn't support it.
|
|
||||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
|
||||||
|
|
||||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
|
||||||
# -Wl, linker flags — those go on the src component via
|
|
||||||
# target_link_options below). Emitted via idf_build_set_property so the
|
|
||||||
# flags propagate to every IDF component (including managed ones like
|
|
||||||
# esphome__micro-mp3) rather than just src/. Required so suppressions
|
|
||||||
# like ``-Wno-error=maybe-uninitialized`` actually silence warnings in
|
|
||||||
# third-party components we don't author.
|
|
||||||
project_compile_opts = get_project_compile_flags()
|
|
||||||
extra_compile_options = "\n".join(
|
extra_compile_options = "\n".join(
|
||||||
f'idf_build_set_property(COMPILE_OPTIONS "{flag}" APPEND)'
|
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
|
||||||
for flag in project_compile_opts
|
for compile_def in compile_defs
|
||||||
)
|
|
||||||
|
|
||||||
# Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS
|
|
||||||
# (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as
|
|
||||||
# -Wno-volatile is passed on a C compile.
|
|
||||||
cxx_compile_options = "\n".join(
|
|
||||||
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
|
|
||||||
for flag in get_project_cxx_compile_flags()
|
|
||||||
)
|
|
||||||
|
|
||||||
cpp_standard_options = (
|
|
||||||
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
|
|
||||||
if CORE.cpp_standard
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
# Per-project list exposed as a CMake variable so converted PIO libs
|
|
||||||
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
|
||||||
# project-specific names into their cached CMakeLists.
|
|
||||||
#
|
|
||||||
# Emit via idf_build_set_property (not plain set()) so the value is
|
|
||||||
# serialised into build_properties.temp.cmake and visible to IDF's
|
|
||||||
# early requirements-expansion pass (component_get_requirements.cmake
|
|
||||||
# runs as a separate CMake script invocation that doesn't load the
|
|
||||||
# project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_
|
|
||||||
# MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty).
|
|
||||||
from esphome.components.esp32 import get_managed_component_require_names
|
|
||||||
|
|
||||||
managed_components_property = "\n".join(
|
|
||||||
f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)"
|
|
||||||
for name in get_managed_component_require_names()
|
|
||||||
)
|
|
||||||
|
|
||||||
# Built-in IDF components exposed via our own property (not IDF's
|
|
||||||
# __COMPONENT_REQUIRES_COMMON, which would append them to every
|
|
||||||
# component's REQUIRES including real IDF components). Referenced by
|
|
||||||
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
|
|
||||||
# on minimal writes because project_description.json may be stale.
|
|
||||||
builtin_components_property = (
|
|
||||||
""
|
|
||||||
if minimal
|
|
||||||
else "\n".join(
|
|
||||||
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
|
|
||||||
for name in sorted(get_available_components() or [])
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
# Auto-generated by ESPHome
|
# Auto-generated by ESPHome
|
||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
# On Windows, Ninja can fail with:
|
|
||||||
# "CreateProcess: The parameter is incorrect (is the command line too long?)"
|
|
||||||
# when compiler/linker command lines exceed the OS length limit.
|
|
||||||
#
|
|
||||||
# The following settings force CMake/Ninja to use *response files* (@file.rsp)
|
|
||||||
# to pass long lists of includes, objects, and other arguments indirectly,
|
|
||||||
# avoiding command-line length limits and fixing the build failure.
|
|
||||||
#
|
|
||||||
# This is especially useful for large ESP-IDF / ESPHome projects with many
|
|
||||||
# source files or include directories.
|
|
||||||
set(CMAKE_C_USE_RESPONSE_FILE_FOR_INCLUDES 1)
|
|
||||||
set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_INCLUDES 1)
|
|
||||||
set(CMAKE_C_USE_RESPONSE_FILE_FOR_OBJECTS 1)
|
|
||||||
set(CMAKE_CXX_USE_RESPONSE_FILE_FOR_OBJECTS 1)
|
|
||||||
set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
|
|
||||||
|
|
||||||
set(IDF_TARGET {idf_target})
|
set(IDF_TARGET {idf_target})
|
||||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||||
|
|
||||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||||
|
|
||||||
{cpp_standard_options}
|
|
||||||
|
|
||||||
{cxx_compile_options}
|
|
||||||
|
|
||||||
{extra_compile_options}
|
{extra_compile_options}
|
||||||
|
|
||||||
{managed_components_property}
|
|
||||||
|
|
||||||
{builtin_components_property}
|
|
||||||
|
|
||||||
project({CORE.name})
|
project({CORE.name})
|
||||||
|
|
||||||
# Emit raw JSON size data for ESPHome to read post-build.
|
|
||||||
add_custom_command(
|
|
||||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
|
||||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
|
||||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
|
||||||
${{CMAKE_PROJECT_NAME}}.map
|
|
||||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
|
||||||
VERBATIM
|
|
||||||
)
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_component_cmakelists() -> str:
|
def get_component_cmakelists(minimal: bool = False) -> str:
|
||||||
"""Generate the main component CMakeLists.txt.
|
"""Generate the main component CMakeLists.txt."""
|
||||||
|
idf_requires = [] if minimal else (get_available_components() or [])
|
||||||
|
requires_str = " ".join(idf_requires)
|
||||||
|
|
||||||
REQUIRES pulls in the discovered built-in IDF components via the
|
# Extract compile options (-W flags, excluding linker flags)
|
||||||
project-level variables set in the top-level CMakeLists.
|
compile_opts = [
|
||||||
"""
|
flag
|
||||||
# Extract linker options (-Wl, flags). Compile flags (-D, -W) are
|
for flag in CORE.build_flags
|
||||||
# emitted project-wide via idf_build_set_property in
|
if flag.startswith("-W") and not flag.startswith("-Wl,")
|
||||||
# get_project_cmakelists so they reach every component, not just src/.
|
]
|
||||||
link_opts = get_project_link_flags()
|
compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
|
||||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
|
||||||
|
# Extract linker options (-Wl, flags)
|
||||||
|
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
|
||||||
|
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
|
||||||
|
|
||||||
return f"""\
|
return f"""\
|
||||||
# Auto-generated by ESPHome
|
# Auto-generated by ESPHome
|
||||||
# CONFIGURE_DEPENDS asks CMake to re-check the glob each build so test
|
file(GLOB_RECURSE app_sources
|
||||||
# runs that reuse the build dir don't compile stale source paths. It's
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
||||||
# invalid in script mode (cmake -P), which is how IDF's
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
||||||
# component_get_requirements.cmake includes us, so skip it there.
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
||||||
if(CMAKE_SCRIPT_MODE_FILE)
|
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
||||||
file(GLOB_RECURSE app_sources
|
)
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
|
||||||
)
|
|
||||||
else()
|
|
||||||
file(GLOB_RECURSE app_sources CONFIGURE_DEPENDS
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
|
||||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
|
||||||
)
|
|
||||||
endif()
|
|
||||||
|
|
||||||
idf_component_register(
|
idf_component_register(
|
||||||
SRCS ${{app_sources}}
|
SRCS ${{app_sources}}
|
||||||
INCLUDE_DIRS "." "esphome"
|
INCLUDE_DIRS "." "esphome"
|
||||||
REQUIRES ${{ESPHOME_PROJECT_BUILTIN_COMPONENTS}}
|
REQUIRES {requires_str}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Apply C++ standard
|
||||||
|
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
# ESPHome compile options
|
||||||
|
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||||
|
{compile_opts_str}
|
||||||
)
|
)
|
||||||
|
|
||||||
# ESPHome linker options
|
# ESPHome linker options
|
||||||
@@ -244,11 +130,11 @@ def write_project(minimal: bool = False) -> None:
|
|||||||
# Write top-level CMakeLists.txt
|
# Write top-level CMakeLists.txt
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_build_path("CMakeLists.txt"),
|
CORE.relative_build_path("CMakeLists.txt"),
|
||||||
get_project_cmakelists(minimal=minimal),
|
get_project_cmakelists(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Write component CMakeLists.txt in src/
|
# Write component CMakeLists.txt in src/
|
||||||
write_file_if_changed(
|
write_file_if_changed(
|
||||||
CORE.relative_src_path("CMakeLists.txt"),
|
CORE.relative_src_path("CMakeLists.txt"),
|
||||||
get_component_cmakelists(),
|
get_component_cmakelists(minimal=minimal),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from esphome.const import __version__
|
from esphome.const import __version__
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
from esphome.helpers import mkdir_p, read_file, write_file_if_changed
|
||||||
from esphome.writer import find_begin_end
|
from esphome.writer import find_begin_end, update_storage_json
|
||||||
|
|
||||||
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
INI_AUTO_GENERATE_BEGIN = "; ========== AUTO GENERATED CODE BEGIN ==========="
|
||||||
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
INI_AUTO_GENERATE_END = "; =========== AUTO GENERATED CODE END ============"
|
||||||
@@ -33,27 +33,12 @@ def format_ini(data: dict[str, str | list[str]]) -> str:
|
|||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
# All -std= variants a platform/framework may set by default, in both the GNU
|
|
||||||
# and strict dialects; unflagged so the cg.set_cpp_standard() value is the
|
|
||||||
# only standard left in the build.
|
|
||||||
CPP_STD_VARIANTS = [
|
|
||||||
f"{prefix}{year}"
|
|
||||||
for year in ("11", "14", "17", "20", "23", "26", "2a", "2b", "2c")
|
|
||||||
for prefix in ("gnu++", "c++")
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def get_ini_content():
|
def get_ini_content():
|
||||||
CORE.add_platformio_option(
|
CORE.add_platformio_option(
|
||||||
"lib_deps",
|
"lib_deps",
|
||||||
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
[x.as_lib_dep for x in CORE.platformio_libraries.values()]
|
||||||
+ ["${common.lib_deps}"],
|
+ ["${common.lib_deps}"],
|
||||||
)
|
)
|
||||||
if CORE.cpp_standard:
|
|
||||||
for variant in CPP_STD_VARIANTS:
|
|
||||||
if variant != CORE.cpp_standard:
|
|
||||||
CORE.add_build_unflag(f"-std={variant}")
|
|
||||||
CORE.add_build_flag(f"-std={CORE.cpp_standard}")
|
|
||||||
# Sort to avoid changing build flags order
|
# Sort to avoid changing build flags order
|
||||||
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
CORE.add_platformio_option("build_flags", sorted(CORE.build_flags))
|
||||||
|
|
||||||
@@ -73,6 +58,7 @@ def get_ini_content():
|
|||||||
|
|
||||||
|
|
||||||
def write_ini(content):
|
def write_ini(content):
|
||||||
|
update_storage_json()
|
||||||
path = CORE.relative_build_path("platformio.ini")
|
path = CORE.relative_build_path("platformio.ini")
|
||||||
|
|
||||||
if path.is_file():
|
if path.is_file():
|
||||||
@@ -108,6 +94,7 @@ Import("env")
|
|||||||
def write_cxx_flags_script() -> None:
|
def write_cxx_flags_script() -> None:
|
||||||
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
|
||||||
contents = CXX_FLAGS_FILE_CONTENTS
|
contents = CXX_FLAGS_FILE_CONTENTS
|
||||||
for flag in sorted(CORE.cxx_build_flags):
|
if not CORE.is_host:
|
||||||
contents += f'env.Append(CXXFLAGS=["{flag}"])\n'
|
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
|
||||||
|
contents += "\n"
|
||||||
write_file_if_changed(path, contents)
|
write_file_if_changed(path, contents)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user