mirror of
https://github.com/esphome/esphome.git
synced 2025-09-06 19:55:35 +00:00
Compare commits
413 Commits
memory_api
...
ota_esp826
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2e7ebc6258 | ||
![]() |
c7ee727af4 | ||
![]() |
c5b2a9e24b | ||
![]() |
101d553df9 | ||
![]() |
8fb6420b1c | ||
![]() |
c03d978b46 | ||
![]() |
2d3cdf60ba | ||
![]() |
a29fef166b | ||
![]() |
9fe94f1201 | ||
![]() |
1b8978a89a | ||
![]() |
6f188d1284 | ||
![]() |
a1a336783e | ||
![]() |
c55bc93f70 | ||
![]() |
de998f2f39 | ||
![]() |
950299e52b | ||
![]() |
23c6650902 | ||
![]() |
5759692627 | ||
![]() |
0ab65c225e | ||
![]() |
8aeb6d3ba2 | ||
![]() |
c3359edb33 | ||
![]() |
4d681ffe3d | ||
![]() |
68628a85b1 | ||
![]() |
086f1982fa | ||
![]() |
5ba1c32242 | ||
![]() |
d2b23ba3a7 | ||
![]() |
83fbd77c4a | ||
![]() |
1a054299d4 | ||
![]() |
e3fb9c2a78 | ||
![]() |
d1276dc6df | ||
![]() |
f286bc57f3 | ||
![]() |
ed48282d09 | ||
![]() |
2ddd8c72d6 | ||
![]() |
d0b4bc48e4 | ||
![]() |
77dbe77117 | ||
![]() |
6daeffcefd | ||
![]() |
6d834c019d | ||
![]() |
905e2906fe | ||
![]() |
a25b544c3b | ||
![]() |
da21174c6d | ||
![]() |
e29f0ee7f8 | ||
![]() |
983b3cb879 | ||
![]() |
fd568d9af3 | ||
![]() |
ca72286386 | ||
![]() |
dea68bebd8 | ||
![]() |
ef98f67b41 | ||
![]() |
6a92b691a0 | ||
![]() |
bc960cf6d2 | ||
![]() |
461ce69296 | ||
![]() |
6a20e6f9ad | ||
![]() |
cde00a1f4c | ||
![]() |
5dc691874b | ||
![]() |
c526ab9a3f | ||
![]() |
07875a8b1e | ||
![]() |
ba4789970c | ||
![]() |
015977cfdf | ||
![]() |
e513c0f004 | ||
![]() |
a11970aee0 | ||
![]() |
4ab37b069b | ||
![]() |
b6bb6699d1 | ||
![]() |
078eaff9a8 | ||
![]() |
a7786b75a0 | ||
![]() |
d4c11dac8c | ||
![]() |
2f2f2f7d15 | ||
![]() |
a92a08c2de | ||
![]() |
75595b08be | ||
![]() |
3c7aba0681 | ||
![]() |
e5d1c30797 | ||
![]() |
c171d13c8c | ||
![]() |
65d63de9b6 | ||
![]() |
9e712e4127 | ||
![]() |
9007621fd7 | ||
![]() |
c01a26607e | ||
![]() |
f6ca70970f | ||
![]() |
4dc11f05a7 | ||
![]() |
5e508f7461 | ||
![]() |
2aceb56606 | ||
![]() |
d071a074ef | ||
![]() |
7a459c8c20 | ||
![]() |
aebd21958a | ||
![]() |
c542db8bfe | ||
![]() |
d9dcfe66ec | ||
![]() |
8517c2e903 | ||
![]() |
684384892a | ||
![]() |
d560831d79 | ||
![]() |
fcc3c8e1b6 | ||
![]() |
959ffde60e | ||
![]() |
07715dd50f | ||
![]() |
03836ee2d2 | ||
![]() |
50408d9abb | ||
![]() |
0de7259428 | ||
![]() |
d054709c2d | ||
![]() |
da16887915 | ||
![]() |
6da8ec8d55 | ||
![]() |
d2752b38c9 | ||
![]() |
6004367ee2 | ||
![]() |
ecfeb8e4d3 | ||
![]() |
456c31262d | ||
![]() |
9f02575287 | ||
![]() |
07bca6103f | ||
![]() |
a58c3950bc | ||
![]() |
8fe582309e | ||
![]() |
b41a61c76e | ||
![]() |
61a5023888 | ||
![]() |
4396bc0d1a | ||
![]() |
acfce581fa | ||
![]() |
88303f39fa | ||
![]() |
ca19959d7c | ||
![]() |
9737b35579 | ||
![]() |
be9c20c357 | ||
![]() |
12ba4b142e | ||
![]() |
c096c6934d | ||
![]() |
17f787fc36 | ||
![]() |
5cd9a86dcb | ||
![]() |
83fe4b4ff3 | ||
![]() |
94accd5abe | ||
![]() |
3ca0015284 | ||
![]() |
33eddb6035 | ||
![]() |
72c58ae36d | ||
![]() |
35411d199f | ||
![]() |
d45944a9e2 | ||
![]() |
86f306ba9e | ||
![]() |
1b3b2f6e6f | ||
![]() |
2adb993242 | ||
![]() |
3ff5b4773b | ||
![]() |
2cbf4f30f9 | ||
![]() |
56b6dd31f1 | ||
![]() |
fc1b49e87d | ||
![]() |
0089619518 | ||
![]() |
5a6db28f1d | ||
![]() |
6819bbd8f8 | ||
![]() |
634f687c3e | ||
![]() |
e2a9b85924 | ||
![]() |
4ccc6aee09 | ||
![]() |
0eab908b0e | ||
![]() |
3964f9794b | ||
![]() |
a45137434b | ||
![]() |
9b1ebdb6da | ||
![]() |
5a1533bea9 | ||
![]() |
0b50ef227b | ||
![]() |
0e31bc1a67 | ||
![]() |
8e67df8059 | ||
![]() |
e1a0949ddb | ||
![]() |
c5b2c8d971 | ||
![]() |
a8775ba60b | ||
![]() |
104906ca11 | ||
![]() |
ad5f6f0cfe | ||
![]() |
8356f7fcd3 | ||
![]() |
225de226b0 | ||
![]() |
2aaf951357 | ||
![]() |
82718e62e7 | ||
![]() |
fd07e1d979 | ||
![]() |
4dab9c4400 | ||
![]() |
7e23d865e6 | ||
![]() |
8f118232e4 | ||
![]() |
23554cda06 | ||
![]() |
064385eac6 | ||
![]() |
6502ed70de | ||
![]() |
bb894c3e32 | ||
![]() |
c5858b7032 | ||
![]() |
99f57ecb73 | ||
![]() |
cc6c892678 | ||
![]() |
07a98d2525 | ||
![]() |
e80f616366 | ||
![]() |
46be877594 | ||
![]() |
ac8b48a53c | ||
![]() |
7fdbd8528a | ||
![]() |
80970f972b | ||
![]() |
3c7865cd6f | ||
![]() |
3a6a66537c | ||
![]() |
7118bea031 | ||
![]() |
44bd8e5b54 | ||
![]() |
efaeb91803 | ||
![]() |
761c6c6685 | ||
![]() |
1f55486896 | ||
![]() |
6818439109 | ||
![]() |
0a77423073 | ||
![]() |
c29f8d0187 | ||
![]() |
2a3f80a82c | ||
![]() |
75f3adcd95 | ||
![]() |
daf8ec36ab | ||
![]() |
6c5632a0b3 | ||
![]() |
abecc0e8d8 | ||
![]() |
af9ecf3429 | ||
![]() |
5fa84439c2 | ||
![]() |
5d18afcd99 | ||
![]() |
117cffd2b0 | ||
![]() |
8ea1a3ed64 | ||
![]() |
4f29b3c7aa | ||
![]() |
3325592d67 | ||
![]() |
0a3ee7d84e | ||
![]() |
882237120e | ||
![]() |
71efaf097b | ||
![]() |
bd60dbb746 | ||
![]() |
6b5e43ca72 | ||
![]() |
8d61b1e8df | ||
![]() |
9c897993bb | ||
![]() |
93f9475105 | ||
![]() |
95cd224e3e | ||
![]() |
b7afeafda9 | ||
![]() |
7922462bcf | ||
![]() |
46d433775b | ||
![]() |
7c4a54de90 | ||
![]() |
c3f1596498 | ||
![]() |
0d1949a61b | ||
![]() |
6a8722f33e | ||
![]() |
fff66072d4 | ||
![]() |
1c2e1ab3e5 | ||
![]() |
68ddd98f5f | ||
![]() |
0dda3faed5 | ||
![]() |
40c0c36179 | ||
![]() |
6b7ced1970 | ||
![]() |
ed2b76050b | ||
![]() |
113813617d | ||
![]() |
c3a209d3f4 | ||
![]() |
7ffdaa1f06 | ||
![]() |
3a857950bf | ||
![]() |
0256e0005e | ||
![]() |
c65af68e63 | ||
![]() |
ef2121a215 | ||
![]() |
bb40b7702d | ||
![]() |
6c48f3d719 | ||
![]() |
ff52869b4c | ||
![]() |
82b7c1224c | ||
![]() |
c14c4fb658 | ||
![]() |
42aee53dde | ||
![]() |
9aa21956c8 | ||
![]() |
4c2874a32b | ||
![]() |
45b88f2da9 | ||
![]() |
8f53961496 | ||
![]() |
5cf0e4d9dd | ||
![]() |
b70983ed09 | ||
![]() |
ffa89eb2d3 | ||
![]() |
8b67d6dfec | ||
![]() |
581b4ef5a1 | ||
![]() |
da02f970d4 | ||
![]() |
2fc0a11596 | ||
![]() |
5a8f722316 | ||
![]() |
279f56141e | ||
![]() |
6bfe281d18 | ||
![]() |
a1371aea37 | ||
![]() |
d5c9c10b3b | ||
![]() |
cef39e7c59 | ||
![]() |
2b9e1ce315 | ||
![]() |
ff9ddb9d68 | ||
![]() |
676c51ffa0 | ||
![]() |
7e4d09dbd8 | ||
![]() |
58504662d8 | ||
![]() |
83b69519dd | ||
![]() |
d4d1a96f9b | ||
![]() |
76fd104fb6 | ||
![]() |
c4d1b1317a | ||
![]() |
14bc83342f | ||
![]() |
a1461c5293 | ||
![]() |
73b2db8af5 | ||
![]() |
a7a119f576 | ||
![]() |
1ba76f5f2e | ||
![]() |
37a9ad6a0d | ||
![]() |
c0a62c0be1 | ||
![]() |
bfb14e1cf9 | ||
![]() |
1415e02e40 | ||
![]() |
81f907e994 | ||
![]() |
61008bc8a9 | ||
![]() |
6d66ddd68d | ||
![]() |
fc180251be | ||
![]() |
ee1d4f27ef | ||
![]() |
325ec0a0ae | ||
![]() |
6071f4b02c | ||
![]() |
083ac8ce8e | ||
![]() |
4ceda31f32 | ||
![]() |
5021cc6d5f | ||
![]() |
2b3e546203 | ||
![]() |
1642d34d29 | ||
![]() |
8ceb1b9d60 | ||
![]() |
d872c8a999 | ||
![]() |
99125c045f | ||
![]() |
860a5ef5c0 | ||
![]() |
b01f03cc24 | ||
![]() |
cfb22e33c9 | ||
![]() |
96bbb58f34 | ||
![]() |
3edd746c6c | ||
![]() |
c308e03e92 | ||
![]() |
bd2b3b9da5 | ||
![]() |
d443a97dd8 | ||
![]() |
58a088e06b | ||
![]() |
49a46883ed | ||
![]() |
bc03538e25 | ||
![]() |
969034b61a | ||
![]() |
06eb1b6014 | ||
![]() |
589d00f17f | ||
![]() |
68c0aa4d6d | ||
![]() |
2fddb061e1 | ||
![]() |
c85eb448e4 | ||
![]() |
396c02c6de | ||
![]() |
52c4509208 | ||
![]() |
d29cae9c3b | ||
![]() |
532e3e370f | ||
![]() |
da573a217d | ||
![]() |
a9b27d1966 | ||
![]() |
0aa3c9685e | ||
![]() |
93b28447ee | ||
![]() |
52634dac2a | ||
![]() |
64c94c1440 | ||
![]() |
f7bf1ef52c | ||
![]() |
fa8c5e880c | ||
![]() |
27ba90ea95 | ||
![]() |
469246b8d8 | ||
![]() |
50f15735dc | ||
![]() |
83d9c02a1b | ||
![]() |
701e6099aa | ||
![]() |
d59476d0e1 | ||
![]() |
fbbb791b0d | ||
![]() |
36c4430317 | ||
![]() |
6be22a5ea9 | ||
![]() |
989058e6a9 | ||
![]() |
7c297366c7 | ||
![]() |
bb3ebaf955 | ||
![]() |
3007ca4d57 | ||
![]() |
a5f1661643 | ||
![]() |
4d683d5a69 | ||
![]() |
c0c0a42362 | ||
![]() |
6a5eb460ef | ||
![]() |
ef372eeeb7 | ||
![]() |
9aad0733ef | ||
![]() |
494a1a216c | ||
![]() |
a75f73dbf0 | ||
![]() |
c9d865a061 | ||
![]() |
3fbbdb4589 | ||
![]() |
cd6cf074d9 | ||
![]() |
d86e1e29a9 | ||
![]() |
dbaf2cdd50 | ||
![]() |
b44d2183aa | ||
![]() |
0f13af0076 | ||
![]() |
339c26c815 | ||
![]() |
d69e98e15d | ||
![]() |
b1b0638fab | ||
![]() |
296442d8f1 | ||
![]() |
fd442cc485 | ||
![]() |
4f58e1c8b9 | ||
![]() |
00d9baed11 | ||
![]() |
f1877ca084 | ||
![]() |
1f7c59f88d | ||
![]() |
b5f42bc493 | ||
![]() |
d8a46c7482 | ||
![]() |
20ad1ab4eb | ||
![]() |
940a8b43fa | ||
![]() |
f761404bf6 | ||
![]() |
e4dc62ea74 | ||
![]() |
c42c5dd946 | ||
![]() |
291215909a | ||
![]() |
0954a6185c | ||
![]() |
f13e742bd5 | ||
![]() |
7a4738ec4e | ||
![]() |
549b0d12b6 | ||
![]() |
412f4ac341 | ||
![]() |
d4ff1bcf5c | ||
![]() |
161f51e1f4 | ||
![]() |
da0c47629a | ||
![]() |
28b277c1c4 | ||
![]() |
936a090aaa | ||
![]() |
1be6d27012 | ||
![]() |
71557c9f58 | ||
![]() |
88cfcc1967 | ||
![]() |
fb379bbb88 | ||
![]() |
88d8cfe6a2 | ||
![]() |
f25abc3248 | ||
![]() |
5b6e152d6c | ||
![]() |
1d0a38446f | ||
![]() |
853dca6c5c | ||
![]() |
97560fd9ef | ||
![]() |
4b7f3355ea | ||
![]() |
110eac4f09 | ||
![]() |
79533cb0d7 | ||
![]() |
f4f69e827b | ||
![]() |
48a4dde824 | ||
![]() |
9b4fe54f45 | ||
![]() |
913c58cd2c | ||
![]() |
374858efeb | ||
![]() |
14dd48f9c3 | ||
![]() |
76d33308d9 | ||
![]() |
daccaf36a7 | ||
![]() |
56c88807ee | ||
![]() |
9c6dbbd8ea | ||
![]() |
a7dd849a8e | ||
![]() |
1f0c606be4 | ||
![]() |
ace375944c | ||
![]() |
5f7c2f771f | ||
![]() |
3d5b602288 | ||
![]() |
6d30269565 | ||
![]() |
4ff3137c0d | ||
![]() |
9d43ddd6f1 | ||
![]() |
f733c43dec | ||
![]() |
f5f0a01a85 | ||
![]() |
908891a096 | ||
![]() |
7657316a92 | ||
![]() |
4f425c700a | ||
![]() |
2c9987869e | ||
![]() |
68f388f78e | ||
![]() |
189d20a822 | ||
![]() |
08defd7360 | ||
![]() |
59d466a6c8 | ||
![]() |
85435e6b5f | ||
![]() |
f9453f9642 | ||
![]() |
f6cdbe37f9 | ||
![]() |
d6b222c370 | ||
![]() |
eecdaa5163 | ||
![]() |
4933ef780b | ||
![]() |
573dad1736 | ||
![]() |
3a6cc0ea3d | ||
![]() |
2f9475a927 | ||
![]() |
8dce7b0905 | ||
![]() |
8b0ad3072f | ||
![]() |
93028a4d90 | ||
![]() |
c9793f3741 |
@@ -9,7 +9,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
## 2. Core Technologies & Stack
|
||||
|
||||
* **Languages:** Python (>=3.10), C++ (gnu++20)
|
||||
* **Languages:** Python (>=3.11), C++ (gnu++20)
|
||||
* **Frameworks & Runtimes:** PlatformIO, Arduino, ESP-IDF.
|
||||
* **Build Systems:** PlatformIO is the primary build system. CMake is used as an alternative.
|
||||
* **Configuration:** YAML.
|
||||
@@ -38,7 +38,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
5. **Dashboard** (`esphome/dashboard/`): A web-based interface for device configuration, management, and OTA updates.
|
||||
|
||||
* **Platform Support:**
|
||||
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (S2, S3, C3, etc.) and both IDF and Arduino frameworks.
|
||||
1. **ESP32** (`components/esp32/`): Espressif ESP32 family. Supports multiple variants (Original, C2, C3, C5, C6, H2, P4, S2, S3) with ESP-IDF framework. Arduino framework supports only a subset of the variants (Original, C3, S2, S3).
|
||||
2. **ESP8266** (`components/esp8266/`): Espressif ESP8266. Arduino framework only, with memory constraints.
|
||||
3. **RP2040** (`components/rp2040/`): Raspberry Pi Pico/RP2040. Arduino framework with PIO (Programmable I/O) support.
|
||||
4. **LibreTiny** (`components/libretiny/`): Realtek and Beken chips. Supports multiple chip families and auto-generated components.
|
||||
@@ -60,7 +60,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
├── __init__.py # Component configuration schema and code generation
|
||||
├── [component].h # C++ header file (if needed)
|
||||
├── [component].cpp # C++ implementation (if needed)
|
||||
└── [platform]/ # Platform-specific implementations
|
||||
└── [platform]/ # Platform-specific implementations
|
||||
├── __init__.py # Platform-specific configuration
|
||||
├── [platform].h # Platform C++ header
|
||||
└── [platform].cpp # Platform C++ implementation
|
||||
@@ -150,7 +150,8 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Configuration Validation:**
|
||||
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
|
||||
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
|
||||
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `cv.only_with_arduino`.
|
||||
* **Platform-Specific:** `cv.only_on(["esp32", "esp8266"])`, `esp32.only_on_variant(...)`, `cv.only_on_esp32`, `cv.only_on_esp8266`, `cv.only_on_rp2040`.
|
||||
* **Framework-Specific:** `cv.only_with_framework(...)`, `cv.only_with_arduino`, `cv.only_with_esp_idf`.
|
||||
* **Schema Extensions:**
|
||||
```python
|
||||
CONFIG_SCHEMA = cv.Schema({ ... })
|
||||
@@ -168,6 +169,8 @@ This document provides essential context for AI models interacting with this pro
|
||||
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
|
||||
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
|
||||
* **CI/CD Pipeline:** Defined in `.github/workflows`.
|
||||
* **Static Analysis & Development:**
|
||||
* `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.
|
||||
|
||||
## 6. Development & Testing Workflow
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0
|
||||
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9
|
||||
|
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
74
.github/workflows/auto-label-pr.yml
vendored
74
.github/workflows/auto-label-pr.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
@@ -63,7 +63,11 @@ jobs:
|
||||
'needs-docs',
|
||||
'needs-codeowners',
|
||||
'too-big',
|
||||
'labeller-recheck'
|
||||
'labeller-recheck',
|
||||
'bugfix',
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'code-quality'
|
||||
];
|
||||
|
||||
const DOCS_PR_PATTERNS = [
|
||||
@@ -101,7 +105,9 @@ jobs:
|
||||
|
||||
// Calculate data from PR files
|
||||
const changedFiles = prFiles.map(file => file.filename);
|
||||
const totalChanges = prFiles.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
|
||||
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const totalChanges = totalAdditions + totalDeletions;
|
||||
|
||||
console.log('Current labels:', currentLabels.join(', '));
|
||||
console.log('Changed files:', changedFiles.length);
|
||||
@@ -227,16 +233,21 @@ jobs:
|
||||
// Strategy: PR size detection
|
||||
async function detectPRSize() {
|
||||
const labels = new Set();
|
||||
const testChanges = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
|
||||
|
||||
const nonTestChanges = totalChanges - testChanges;
|
||||
|
||||
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||
labels.add('small-pr');
|
||||
return labels;
|
||||
}
|
||||
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const testDeletions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
// Don't add too-big if mega-pr label is already present
|
||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||
labels.add('too-big');
|
||||
@@ -341,17 +352,42 @@ jobs:
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR Template Checkbox detection
|
||||
async function detectPRTemplateCheckboxes() {
|
||||
const labels = new Set();
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
|
||||
console.log('Checking PR template checkboxes...');
|
||||
|
||||
// Check for checked checkboxes in the "Types of changes" section
|
||||
const checkboxPatterns = [
|
||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
for (const { pattern, label } of checkboxPatterns) {
|
||||
if (pattern.test(prBody)) {
|
||||
console.log(`Found checked checkbox for: ${label}`);
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels) {
|
||||
const labels = new Set();
|
||||
|
||||
// Check for missing tests
|
||||
if ((allLabels.has('new-component') || allLabels.has('new-platform')) && !allLabels.has('has-tests')) {
|
||||
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
|
||||
labels.add('needs-tests');
|
||||
}
|
||||
|
||||
// Check for missing docs
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform')) {
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||
|
||||
@@ -383,10 +419,13 @@ jobs:
|
||||
|
||||
// Too big message
|
||||
if (finalLabels.includes('too-big')) {
|
||||
const testChanges = prFiles
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0) + (file.deletions || 0), 0);
|
||||
const nonTestChanges = totalChanges - testChanges;
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const testDeletions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||
@@ -535,7 +574,8 @@ jobs:
|
||||
dashboardLabels,
|
||||
actionsLabels,
|
||||
codeOwnerLabels,
|
||||
testLabels
|
||||
testLabels,
|
||||
checkboxLabels
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(),
|
||||
detectComponentPlatforms(apiData),
|
||||
@@ -546,7 +586,8 @@ jobs:
|
||||
detectDashboardChanges(),
|
||||
detectGitHubActionsChanges(),
|
||||
detectCodeOwner(),
|
||||
detectTests()
|
||||
detectTests(),
|
||||
detectPRTemplateCheckboxes()
|
||||
]);
|
||||
|
||||
// Combine all labels
|
||||
@@ -560,7 +601,8 @@ jobs:
|
||||
...dashboardLabels,
|
||||
...actionsLabels,
|
||||
...codeOwnerLabels,
|
||||
...testLabels
|
||||
...testLabels,
|
||||
...checkboxLabels
|
||||
]);
|
||||
|
||||
// Detect requirements based on all other labels
|
||||
|
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
|
2
.github/workflows/ci-clang-tidy-hash.yml
vendored
2
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
|
2
.github/workflows/ci-docker.yml
vendored
2
.github/workflows/ci-docker.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
|
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -136,7 +136,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -156,12 +156,12 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
uses: codecov/codecov-action@v5.5.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@v4.2.3
|
||||
uses: actions/cache/save@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -179,7 +179,7 @@ jobs:
|
||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -281,13 +281,13 @@ jobs:
|
||||
pio_cache_key: tidyesp32-idf
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
||||
pio_cache_key: tidy-zephyr
|
||||
ignore_errors: false
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -300,14 +300,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -374,7 +374,7 @@ jobs:
|
||||
sudo apt-get install libsdl2-dev
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
matrix: ${{ steps.split.outputs.components }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Split components into 20 groups
|
||||
id: split
|
||||
run: |
|
||||
@@ -430,7 +430,7 @@ jobs:
|
||||
sudo apt-get install libsdl2-dev
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -459,7 +459,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
|
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -54,7 +54,7 @@ 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
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
|
24
.github/workflows/needs-docs.yml
vendored
24
.github/workflows/needs-docs.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Needs Docs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check for needs-docs label
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const needsDocs = labels.find(label => label.name === 'needs-docs');
|
||||
if (needsDocs) {
|
||||
core.setFailed('Pull request needs docs');
|
||||
}
|
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v5.0.0
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v5.0.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
@@ -102,12 +102,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -168,10 +168,10 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
- uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
30
.github/workflows/status-check-labels.yml
vendored
Normal file
30
.github/workflows/status-check-labels.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Status check labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check ${{ matrix.label }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||
if (hasLabel) {
|
||||
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||
}
|
4
.github/workflows/sync-device-classes.yml
vendored
4
.github/workflows/sync-device-classes.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@v4.2.2
|
||||
uses: actions/checkout@v5.0.0
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.5
|
||||
rev: v0.12.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
26
CODEOWNERS
26
CODEOWNERS
@@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/api/* @OttoWinter
|
||||
esphome/components/api/* @esphome/core
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/async_tcp/* @OttoWinter
|
||||
esphome/components/async_tcp/* @esphome/core
|
||||
esphome/components/at581x/* @X-Ryl669
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
esphome/components/atm90e26/* @danieltwagner
|
||||
@@ -69,7 +69,7 @@ esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @tobias-
|
||||
esphome/components/bl0942/* @dbuezas @dwmw2
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/bluetooth_proxy/* @jesserockz
|
||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -89,9 +89,10 @@ esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/button/* @esphome/core
|
||||
esphome/components/bytebuffer/* @clydebarrow
|
||||
esphome/components/camera/* @DT-art1 @bdraco
|
||||
esphome/components/camera_encoder/* @DT-art1
|
||||
esphome/components/canbus/* @danielschramm @mvturnho
|
||||
esphome/components/cap1188/* @mreditor97
|
||||
esphome/components/captive_portal/* @OttoWinter
|
||||
esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
@@ -118,7 +119,7 @@ esphome/components/dallas_temp/* @ssieb
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/datetime/* @jesserockz @rfdarter
|
||||
esphome/components/debug/* @OttoWinter
|
||||
esphome/components/debug/* @esphome/core
|
||||
esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
@@ -144,9 +145,10 @@ esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/es8388/* @P4uLT
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
|
||||
esphome/components/esp32_ble_client/* @jesserockz
|
||||
esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
|
||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
|
||||
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
|
||||
esphome/components/esp32_ble_tracker/* @bdraco
|
||||
esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
@@ -155,6 +157,7 @@ esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
esphome/components/esp_ldo/* @clydebarrow
|
||||
esphome/components/espnow/* @jesserockz
|
||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||
esphome/components/event/* @nohat
|
||||
esphome/components/event_emitter/* @Rapsssito
|
||||
@@ -236,7 +239,7 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
esphome/components/json/* @OttoWinter
|
||||
esphome/components/json/* @esphome/core
|
||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||
esphome/components/key_collector/* @ssieb
|
||||
esphome/components/key_provider/* @ssieb
|
||||
@@ -244,6 +247,7 @@ esphome/components/kuntze/* @ssieb
|
||||
esphome/components/lc709203f/* @ilikecake
|
||||
esphome/components/lcd_menu/* @numo68
|
||||
esphome/components/ld2410/* @regevbr @sebcaps
|
||||
esphome/components/ld2412/* @Rihan9
|
||||
esphome/components/ld2420/* @descipher
|
||||
esphome/components/ld2450/* @hareeshmu
|
||||
esphome/components/ld24xx/* @kbx81
|
||||
@@ -466,13 +470,13 @@ esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @OttoWinter
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
esphome/components/tlc5971/* @IJIJI
|
||||
esphome/components/tm1621/* @Philippe12
|
||||
esphome/components/tm1637/* @glmnet
|
||||
esphome/components/tm1638/* @skykingjwc
|
||||
esphome/components/tm1651/* @freekode
|
||||
esphome/components/tm1651/* @mrtoy-me
|
||||
esphome/components/tmp102/* @timsavage
|
||||
esphome/components/tmp1075/* @sybrenstuvel
|
||||
esphome/components/tmp117/* @Azimath
|
||||
@@ -510,7 +514,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
esphome/components/web_server_base/* @OttoWinter
|
||||
esphome/components/web_server_base/* @esphome/core
|
||||
esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
esphome/components/weikai_i2c/* @DrCoolZic
|
||||
|
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.8.0-dev
|
||||
PROJECT_NUMBER = 2025.9.0-dev
|
||||
|
||||
# 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
|
||||
|
@@ -90,7 +90,7 @@ def main():
|
||||
def run_command(*cmd, ignore_error: bool = False):
|
||||
print(f"$ {shlex.join(list(cmd))}")
|
||||
if not args.dry_run:
|
||||
rc = subprocess.call(list(cmd))
|
||||
rc = subprocess.call(list(cmd), close_fds=False)
|
||||
if rc != 0 and not ignore_error:
|
||||
print("Command failed")
|
||||
sys.exit(1)
|
||||
|
@@ -9,6 +9,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Protocol
|
||||
|
||||
import argcomplete
|
||||
|
||||
@@ -44,6 +45,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
from esphome.helpers import get_bool_env, indent, is_ip_address
|
||||
from esphome.log import AnsiFore, color, setup_log
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import (
|
||||
get_serial_ports,
|
||||
list_yaml_files,
|
||||
@@ -55,6 +57,23 @@ from esphome.util import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ArgsProtocol(Protocol):
|
||||
device: list[str] | None
|
||||
reset: bool
|
||||
username: str | None
|
||||
password: str | None
|
||||
client_id: str | None
|
||||
topic: str | None
|
||||
file: str | None
|
||||
no_logs: bool
|
||||
only_generate: bool
|
||||
show_secrets: bool
|
||||
dashboard: bool
|
||||
configuration: str
|
||||
name: str
|
||||
upload_speed: str | None
|
||||
|
||||
|
||||
def choose_prompt(options, purpose: str = None):
|
||||
if not options:
|
||||
raise EsphomeError(
|
||||
@@ -88,30 +107,57 @@ def choose_prompt(options, purpose: str = None):
|
||||
|
||||
|
||||
def choose_upload_log_host(
|
||||
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||
):
|
||||
default: list[str] | str | None,
|
||||
check_default: str | None,
|
||||
show_ota: bool,
|
||||
show_mqtt: bool,
|
||||
show_api: bool,
|
||||
purpose: str | None = None,
|
||||
) -> list[str]:
|
||||
# Convert to list for uniform handling
|
||||
defaults = [default] if isinstance(default, str) else default or []
|
||||
|
||||
# If devices specified, resolve them
|
||||
if defaults:
|
||||
resolved: list[str] = []
|
||||
for device in defaults:
|
||||
if device == "SERIAL":
|
||||
serial_ports = get_serial_ports()
|
||||
if not serial_ports:
|
||||
_LOGGER.warning("No serial ports found, skipping SERIAL device")
|
||||
continue
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path)
|
||||
for port in serial_ports
|
||||
]
|
||||
resolved.append(choose_prompt(options, purpose=purpose))
|
||||
elif device == "OTA":
|
||||
if CORE.address and (
|
||||
(show_ota and "ota" in CORE.config)
|
||||
or (show_api and "api" in CORE.config)
|
||||
):
|
||||
resolved.append(CORE.address)
|
||||
elif show_mqtt and has_mqtt_logging():
|
||||
resolved.append("MQTT")
|
||||
else:
|
||||
resolved.append(device)
|
||||
if not resolved:
|
||||
_LOGGER.error("All specified devices: %s could not be resolved.", defaults)
|
||||
return resolved
|
||||
|
||||
# No devices specified, show interactive chooser
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
|
||||
]
|
||||
if default == "SERIAL":
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if default == "OTA":
|
||||
return CORE.address
|
||||
if (
|
||||
show_mqtt
|
||||
and (mqtt_config := CORE.config.get(CONF_MQTT))
|
||||
and mqtt_logging_enabled(mqtt_config)
|
||||
):
|
||||
if show_mqtt and has_mqtt_logging():
|
||||
mqtt_config = CORE.config[CONF_MQTT]
|
||||
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
|
||||
if default == "OTA":
|
||||
return "MQTT"
|
||||
if default is not None:
|
||||
return default
|
||||
|
||||
if check_default is not None and check_default in [opt[1] for opt in options]:
|
||||
return check_default
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
return [check_default]
|
||||
return [choose_prompt(options, purpose=purpose)]
|
||||
|
||||
|
||||
def mqtt_logging_enabled(mqtt_config):
|
||||
@@ -123,7 +169,14 @@ def mqtt_logging_enabled(mqtt_config):
|
||||
return log_topic.get(CONF_LEVEL, None) != "NONE"
|
||||
|
||||
|
||||
def get_port_type(port):
|
||||
def has_mqtt_logging() -> bool:
|
||||
"""Check if MQTT logging is available."""
|
||||
return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
|
||||
mqtt_config
|
||||
)
|
||||
|
||||
|
||||
def get_port_type(port: str) -> str:
|
||||
if port.startswith("/") or port.startswith("COM"):
|
||||
return "SERIAL"
|
||||
if port == "MQTT":
|
||||
@@ -131,7 +184,7 @@ def get_port_type(port):
|
||||
return "NETWORK"
|
||||
|
||||
|
||||
def run_miniterm(config, port, args):
|
||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
from aioesphomeapi import LogParser
|
||||
import serial
|
||||
|
||||
@@ -208,7 +261,7 @@ def wrap_to_code(name, comp):
|
||||
return wrapped
|
||||
|
||||
|
||||
def write_cpp(config):
|
||||
def write_cpp(config: ConfigType) -> int:
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
@@ -216,7 +269,7 @@ def write_cpp(config):
|
||||
return write_cpp_file()
|
||||
|
||||
|
||||
def generate_cpp_contents(config):
|
||||
def generate_cpp_contents(config: ConfigType) -> None:
|
||||
_LOGGER.info("Generating C++ source...")
|
||||
|
||||
for name, component, conf in iter_component_configs(CORE.config):
|
||||
@@ -227,7 +280,7 @@ def generate_cpp_contents(config):
|
||||
CORE.flush_tasks()
|
||||
|
||||
|
||||
def write_cpp_file():
|
||||
def write_cpp_file() -> int:
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
@@ -238,7 +291,7 @@ def write_cpp_file():
|
||||
return 0
|
||||
|
||||
|
||||
def compile_program(args, config):
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome import platformio_api
|
||||
|
||||
_LOGGER.info("Compiling app...")
|
||||
@@ -249,7 +302,9 @@ def compile_program(args, config):
|
||||
return 0 if idedata is not None else 1
|
||||
|
||||
|
||||
def upload_using_esptool(config, port, file, speed):
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
from esphome import platformio_api
|
||||
|
||||
first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
|
||||
@@ -277,20 +332,20 @@ def upload_using_esptool(config, port, file, speed):
|
||||
|
||||
def run_esptool(baud_rate):
|
||||
cmd = [
|
||||
"esptool.py",
|
||||
"esptool",
|
||||
"--before",
|
||||
"default_reset",
|
||||
"default-reset",
|
||||
"--after",
|
||||
"hard_reset",
|
||||
"hard-reset",
|
||||
"--baud",
|
||||
str(baud_rate),
|
||||
"--port",
|
||||
port,
|
||||
"--chip",
|
||||
mcu,
|
||||
"write_flash",
|
||||
"write-flash",
|
||||
"-z",
|
||||
"--flash_size",
|
||||
"--flash-size",
|
||||
"detect",
|
||||
]
|
||||
for img in flash_images:
|
||||
@@ -314,7 +369,7 @@ def upload_using_esptool(config, port, file, speed):
|
||||
return run_esptool(115200)
|
||||
|
||||
|
||||
def upload_using_platformio(config, port):
|
||||
def upload_using_platformio(config: ConfigType, port: str):
|
||||
from esphome import platformio_api
|
||||
|
||||
upload_args = ["-t", "upload", "-t", "nobuild"]
|
||||
@@ -323,7 +378,7 @@ def upload_using_platformio(config, port):
|
||||
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
|
||||
|
||||
|
||||
def check_permissions(port):
|
||||
def check_permissions(port: str):
|
||||
if os.name == "posix" and get_port_type(port) == "SERIAL":
|
||||
# Check if we can open selected serial port
|
||||
if not os.access(port, os.F_OK):
|
||||
@@ -341,7 +396,7 @@ def check_permissions(port):
|
||||
)
|
||||
|
||||
|
||||
def upload_program(config, args, host):
|
||||
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
if getattr(module, "upload_program")(config, args, host):
|
||||
@@ -356,7 +411,7 @@ def upload_program(config, args, host):
|
||||
return upload_using_esptool(config, host, file, args.upload_speed)
|
||||
|
||||
if CORE.target_platform in (PLATFORM_RP2040):
|
||||
return upload_using_platformio(config, args.device)
|
||||
return upload_using_platformio(config, host)
|
||||
|
||||
if CORE.is_libretiny:
|
||||
return upload_using_platformio(config, host)
|
||||
@@ -379,9 +434,12 @@ def upload_program(config, args, host):
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD, "")
|
||||
|
||||
# Check if we should use MQTT for address resolution
|
||||
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
|
||||
devices: list[str] = args.device or []
|
||||
if (
|
||||
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
|
||||
and (not args.device or args.device in ("MQTT", "OTA"))
|
||||
and (not devices or host in ("MQTT", "OTA"))
|
||||
and (
|
||||
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
|
||||
or get_port_type(host) == "MQTT"
|
||||
@@ -399,24 +457,29 @@ def upload_program(config, args, host):
|
||||
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
|
||||
|
||||
|
||||
def show_logs(config, args, port):
|
||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||
if "logger" not in config:
|
||||
raise EsphomeError("Logger is not configured!")
|
||||
|
||||
port = devices[0]
|
||||
|
||||
if get_port_type(port) == "SERIAL":
|
||||
check_permissions(port)
|
||||
return run_miniterm(config, port, args)
|
||||
if get_port_type(port) == "NETWORK" and "api" in config:
|
||||
addresses_to_use = devices
|
||||
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
|
||||
from esphome import mqtt
|
||||
|
||||
port = mqtt.get_esphome_device_ip(
|
||||
mqtt_address = mqtt.get_esphome_device_ip(
|
||||
config, args.username, args.password, args.client_id
|
||||
)[0]
|
||||
addresses_to_use = [mqtt_address]
|
||||
|
||||
from esphome.components.api.client import run_logs
|
||||
|
||||
return run_logs(config, port)
|
||||
if get_port_type(port) == "MQTT" and "mqtt" in config:
|
||||
return run_logs(config, addresses_to_use)
|
||||
if get_port_type(port) in ("NETWORK", "MQTT") and "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.show_logs(
|
||||
@@ -426,7 +489,7 @@ def show_logs(config, args, port):
|
||||
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
|
||||
|
||||
|
||||
def clean_mqtt(config, args):
|
||||
def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.clear_topic(
|
||||
@@ -434,13 +497,13 @@ def clean_mqtt(config, args):
|
||||
)
|
||||
|
||||
|
||||
def command_wizard(args):
|
||||
def command_wizard(args: ArgsProtocol) -> int | None:
|
||||
from esphome import wizard
|
||||
|
||||
return wizard.wizard(args.configuration)
|
||||
|
||||
|
||||
def command_config(args, config):
|
||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if not CORE.verbose:
|
||||
config = strip_default_ids(config)
|
||||
output = yaml_util.dump(config, args.show_secrets)
|
||||
@@ -455,7 +518,7 @@ def command_config(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_vscode(args):
|
||||
def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
from esphome import vscode
|
||||
|
||||
logging.disable(logging.INFO)
|
||||
@@ -463,14 +526,7 @@ def command_vscode(args):
|
||||
vscode.read_config(args)
|
||||
|
||||
|
||||
def command_compile(args, config):
|
||||
# Set memory analysis options in config
|
||||
if args.analyze_memory:
|
||||
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
|
||||
|
||||
if args.memory_report:
|
||||
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
|
||||
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -484,8 +540,9 @@ def command_compile(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_upload(args, config):
|
||||
port = choose_upload_log_host(
|
||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
@@ -493,14 +550,22 @@ def command_upload(args, config):
|
||||
show_api=False,
|
||||
purpose="uploading",
|
||||
)
|
||||
exit_code = upload_program(config, args, port)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
|
||||
# Try each device until one succeeds
|
||||
exit_code = 1
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def command_discover(args, config):
|
||||
def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -509,8 +574,9 @@ def command_discover(args, config):
|
||||
raise EsphomeError("No discover method configured (mqtt)")
|
||||
|
||||
|
||||
def command_logs(args, config):
|
||||
port = choose_upload_log_host(
|
||||
def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
@@ -518,10 +584,10 @@ def command_logs(args, config):
|
||||
show_api=True,
|
||||
purpose="logging",
|
||||
)
|
||||
return show_logs(config, args, port)
|
||||
return show_logs(config, args, devices)
|
||||
|
||||
|
||||
def command_run(args, config):
|
||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -538,7 +604,8 @@ def command_run(args, config):
|
||||
program_path = idedata.raw["prog_path"]
|
||||
return run_external_process(program_path)
|
||||
|
||||
port = choose_upload_log_host(
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
@@ -546,39 +613,53 @@ def command_run(args, config):
|
||||
show_api=True,
|
||||
purpose="uploading",
|
||||
)
|
||||
exit_code = upload_program(config, args, port)
|
||||
if exit_code != 0:
|
||||
|
||||
# Try each device for upload until one succeeds
|
||||
successful_device: str | None = None
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
successful_device = device
|
||||
break
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
if successful_device is None:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
|
||||
if args.no_logs:
|
||||
return 0
|
||||
port = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=port,
|
||||
|
||||
# For logs, prefer the device we successfully uploaded to
|
||||
devices = choose_upload_log_host(
|
||||
default=successful_device,
|
||||
check_default=successful_device,
|
||||
show_ota=False,
|
||||
show_mqtt=True,
|
||||
show_api=True,
|
||||
purpose="logging",
|
||||
)
|
||||
return show_logs(config, args, port)
|
||||
return show_logs(config, args, devices)
|
||||
|
||||
|
||||
def command_clean_mqtt(args, config):
|
||||
def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return clean_mqtt(config, args)
|
||||
|
||||
|
||||
def command_mqtt_fingerprint(args, config):
|
||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.get_fingerprint(config)
|
||||
|
||||
|
||||
def command_version(args):
|
||||
def command_version(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"Version: {const.__version__}")
|
||||
return 0
|
||||
|
||||
|
||||
def command_clean(args, config):
|
||||
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
try:
|
||||
writer.clean_build()
|
||||
except OSError as err:
|
||||
@@ -588,13 +669,13 @@ def command_clean(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_dashboard(args):
|
||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def command_update_all(args):
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
import click
|
||||
|
||||
success = {}
|
||||
@@ -641,7 +722,7 @@ def command_update_all(args):
|
||||
return failed
|
||||
|
||||
|
||||
def command_idedata(args, config):
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
from esphome import platformio_api
|
||||
@@ -657,7 +738,7 @@ def command_idedata(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args, config):
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
for c in args.name:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
print(
|
||||
@@ -774,6 +855,12 @@ POST_CONFIG_ACTIONS = {
|
||||
"discover": command_discover,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
"clean",
|
||||
"clean-mqtt",
|
||||
"config",
|
||||
]
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
options_parser = argparse.ArgumentParser(add_help=False)
|
||||
@@ -850,17 +937,6 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--analyze-memory",
|
||||
help="Analyze and display memory usage by component after compilation.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--memory-report",
|
||||
help="Save memory analysis report to a file (supports .json or .txt).",
|
||||
type=str,
|
||||
metavar="FILE",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
@@ -872,7 +948,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -894,7 +971,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--reset",
|
||||
@@ -923,7 +1001,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -1050,6 +1129,13 @@ def parse_args(argv):
|
||||
arguments = argv[1:]
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
|
||||
args, unknown_args = parser.parse_known_args(arguments)
|
||||
if unknown_args:
|
||||
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
|
||||
return args
|
||||
|
||||
return parser.parse_args(arguments)
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -391,8 +391,7 @@ async def build_action(full_config, template_arg, args):
|
||||
)
|
||||
action_id = full_config[CONF_TYPE_ID]
|
||||
builder = registry_entry.coroutine_fun
|
||||
ret = await builder(config, action_id, template_arg, args)
|
||||
return ret
|
||||
return await builder(config, action_id, template_arg, args)
|
||||
|
||||
|
||||
async def build_action_list(config, templ, arg_type):
|
||||
@@ -409,8 +408,7 @@ async def build_condition(full_config, template_arg, args):
|
||||
)
|
||||
action_id = full_config[CONF_TYPE_ID]
|
||||
builder = registry_entry.coroutine_fun
|
||||
ret = await builder(config, action_id, template_arg, args)
|
||||
return ret
|
||||
return await builder(config, action_id, template_arg, args)
|
||||
|
||||
|
||||
async def build_condition_list(config, templ, args):
|
||||
|
@@ -61,11 +61,10 @@ void AbsoluteHumidityComponent::loop() {
|
||||
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||
}
|
||||
if (no_humidity) {
|
||||
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||
ESP_LOGW(TAG, "No valid state from humidity sensor!");
|
||||
}
|
||||
ESP_LOGW(TAG, "Unable to calculate absolute humidity.");
|
||||
this->publish_state(NAN);
|
||||
this->status_set_warning();
|
||||
this->status_set_warning("Unable to calculate absolute humidity.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,9 +86,8 @@ void AbsoluteHumidityComponent::loop() {
|
||||
es = es_wobus(temperature_c);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!");
|
||||
this->publish_state(NAN);
|
||||
this->status_set_error();
|
||||
this->status_set_error("Invalid saturation vapor pressure equation selection!");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
|
||||
|
@@ -5,7 +5,7 @@ from esphome.const import (
|
||||
CONF_EQUATION,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
ICON_WATER,
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_GRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
@@ -27,8 +27,8 @@ EQUATION = {
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_WATER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
@@ -140,6 +140,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
9: adc_channel_t.ADC_CHANNEL_8,
|
||||
10: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
16: adc_channel_t.ADC_CHANNEL_0,
|
||||
17: adc_channel_t.ADC_CHANNEL_1,
|
||||
18: adc_channel_t.ADC_CHANNEL_2,
|
||||
19: adc_channel_t.ADC_CHANNEL_3,
|
||||
20: adc_channel_t.ADC_CHANNEL_4,
|
||||
21: adc_channel_t.ADC_CHANNEL_5,
|
||||
22: adc_channel_t.ADC_CHANNEL_6,
|
||||
23: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
}
|
||||
|
||||
# pin to adc2 channel mapping
|
||||
@@ -198,6 +208,14 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
19: adc_channel_t.ADC_CHANNEL_8,
|
||||
20: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
49: adc_channel_t.ADC_CHANNEL_0,
|
||||
50: adc_channel_t.ADC_CHANNEL_1,
|
||||
51: adc_channel_t.ADC_CHANNEL_2,
|
||||
52: adc_channel_t.ADC_CHANNEL_3,
|
||||
53: adc_channel_t.ADC_CHANNEL_4,
|
||||
54: adc_channel_t.ADC_CHANNEL_5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +267,11 @@ def validate_adc_pin(value):
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
|
||||
if CORE.is_nrf52:
|
||||
return pins.gpio_pin_schema(
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -265,5 +288,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
@@ -13,6 +13,10 @@
|
||||
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
@@ -38,15 +42,15 @@ enum class SamplingMode : uint8_t {
|
||||
|
||||
const LogString *sampling_mode_to_str(SamplingMode mode);
|
||||
|
||||
class Aggregator {
|
||||
template<typename T> class Aggregator {
|
||||
public:
|
||||
Aggregator(SamplingMode mode);
|
||||
void add_sample(uint32_t value);
|
||||
uint32_t aggregate();
|
||||
void add_sample(T value);
|
||||
T aggregate();
|
||||
|
||||
protected:
|
||||
uint32_t aggr_{0};
|
||||
uint32_t samples_{0};
|
||||
T aggr_{0};
|
||||
uint8_t samples_{0};
|
||||
SamplingMode mode_{SamplingMode::AVG};
|
||||
};
|
||||
|
||||
@@ -69,6 +73,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
/// @return A float representing the setup priority.
|
||||
float get_setup_priority() const override;
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
/// Set the ADC channel to be used by the ADC sensor.
|
||||
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
|
||||
void set_adc_channel(const adc_dt_spec *channel) { this->channel_ = channel; }
|
||||
#endif
|
||||
/// Set the GPIO pin to be used by the ADC sensor.
|
||||
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||
@@ -136,8 +145,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
adc_oneshot_unit_handle_t adc_handle_{nullptr};
|
||||
adc_cali_handle_t calibration_handle_{nullptr};
|
||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
||||
adc_channel_t channel_;
|
||||
adc_unit_t adc_unit_;
|
||||
adc_channel_t channel_{};
|
||||
adc_unit_t adc_unit_{};
|
||||
struct SetupFlags {
|
||||
uint8_t init_complete : 1;
|
||||
uint8_t config_complete : 1;
|
||||
@@ -151,6 +160,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
#ifdef USE_RP2040
|
||||
bool is_temperature_{false};
|
||||
#endif // USE_RP2040
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
const struct adc_dt_spec *channel_ = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace adc
|
||||
|
@@ -18,15 +18,15 @@ const LogString *sampling_mode_to_str(SamplingMode mode) {
|
||||
return LOG_STR("unknown");
|
||||
}
|
||||
|
||||
Aggregator::Aggregator(SamplingMode mode) {
|
||||
template<typename T> Aggregator<T>::Aggregator(SamplingMode mode) {
|
||||
this->mode_ = mode;
|
||||
// set to max uint if mode is "min"
|
||||
if (mode == SamplingMode::MIN) {
|
||||
this->aggr_ = UINT32_MAX;
|
||||
this->aggr_ = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
|
||||
void Aggregator::add_sample(uint32_t value) {
|
||||
template<typename T> void Aggregator<T>::add_sample(T value) {
|
||||
this->samples_ += 1;
|
||||
|
||||
switch (this->mode_) {
|
||||
@@ -47,7 +47,7 @@ void Aggregator::add_sample(uint32_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Aggregator::aggregate() {
|
||||
template<typename T> T Aggregator<T>::aggregate() {
|
||||
if (this->mode_ == SamplingMode::AVG) {
|
||||
if (this->samples_ == 0) {
|
||||
return this->aggr_;
|
||||
@@ -59,6 +59,12 @@ uint32_t Aggregator::aggregate() {
|
||||
return this->aggr_;
|
||||
}
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
template class Aggregator<int32_t>;
|
||||
#else
|
||||
template class Aggregator<uint32_t>;
|
||||
#endif
|
||||
|
||||
void ADCSensor::update() {
|
||||
float value_v = this->sample();
|
||||
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
|
@@ -72,10 +72,9 @@ void ADCSensor::setup() {
|
||||
// Initialize ADC calibration
|
||||
if (this->calibration_handle_ == nullptr) {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
esp_err_t err;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
// RISC-V variants and S3 use curve fitting calibration
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
@@ -153,7 +152,7 @@ float ADCSensor::sample() {
|
||||
}
|
||||
|
||||
float ADCSensor::sample_fixed_attenuation_() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
int raw;
|
||||
@@ -187,7 +186,7 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@@ -220,7 +219,7 @@ float ADCSensor::sample_autorange_() {
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@@ -232,7 +231,7 @@ float ADCSensor::sample_autorange_() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -261,7 +260,7 @@ float ADCSensor::sample_autorange_() {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
@@ -281,7 +280,7 @@ float ADCSensor::sample_autorange_() {
|
||||
}
|
||||
// Clean up calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
|
@@ -37,7 +37,7 @@ void ADCSensor::dump_config() {
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
uint32_t raw = 0;
|
||||
|
@@ -30,7 +30,7 @@ void ADCSensor::dump_config() {
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
if (this->output_raw_) {
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
|
@@ -41,7 +41,7 @@ void ADCSensor::dump_config() {
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
if (this->is_temperature_) {
|
||||
adc_set_temp_sensor_enabled(true);
|
||||
|
207
esphome/components/adc/adc_sensor_zephyr.cpp
Normal file
207
esphome/components/adc/adc_sensor_zephyr.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "hal/nrf_saadc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.zephyr";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
if (!adc_is_ready_dt(this->channel_)) {
|
||||
ESP_LOGE(TAG, "ADC controller device %s not ready", this->channel_->dev->name);
|
||||
return;
|
||||
}
|
||||
|
||||
auto err = adc_channel_setup_dt(this->channel_);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could not setup channel %s (%d)", this->channel_->dev->name, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
static const LogString *gain_to_str(enum adc_gain gain) {
|
||||
switch (gain) {
|
||||
case ADC_GAIN_1_6:
|
||||
return LOG_STR("1/6");
|
||||
case ADC_GAIN_1_5:
|
||||
return LOG_STR("1/5");
|
||||
case ADC_GAIN_1_4:
|
||||
return LOG_STR("1/4");
|
||||
case ADC_GAIN_1_3:
|
||||
return LOG_STR("1/3");
|
||||
case ADC_GAIN_2_5:
|
||||
return LOG_STR("2/5");
|
||||
case ADC_GAIN_1_2:
|
||||
return LOG_STR("1/2");
|
||||
case ADC_GAIN_2_3:
|
||||
return LOG_STR("2/3");
|
||||
case ADC_GAIN_4_5:
|
||||
return LOG_STR("4/5");
|
||||
case ADC_GAIN_1:
|
||||
return LOG_STR("1");
|
||||
case ADC_GAIN_2:
|
||||
return LOG_STR("2");
|
||||
case ADC_GAIN_3:
|
||||
return LOG_STR("3");
|
||||
case ADC_GAIN_4:
|
||||
return LOG_STR("4");
|
||||
case ADC_GAIN_6:
|
||||
return LOG_STR("6");
|
||||
case ADC_GAIN_8:
|
||||
return LOG_STR("8");
|
||||
case ADC_GAIN_12:
|
||||
return LOG_STR("12");
|
||||
case ADC_GAIN_16:
|
||||
return LOG_STR("16");
|
||||
case ADC_GAIN_24:
|
||||
return LOG_STR("24");
|
||||
case ADC_GAIN_32:
|
||||
return LOG_STR("32");
|
||||
case ADC_GAIN_64:
|
||||
return LOG_STR("64");
|
||||
case ADC_GAIN_128:
|
||||
return LOG_STR("128");
|
||||
}
|
||||
return LOG_STR("undefined gain");
|
||||
}
|
||||
|
||||
static const LogString *reference_to_str(enum adc_reference reference) {
|
||||
switch (reference) {
|
||||
case ADC_REF_VDD_1:
|
||||
return LOG_STR("VDD");
|
||||
case ADC_REF_VDD_1_2:
|
||||
return LOG_STR("VDD/2");
|
||||
case ADC_REF_VDD_1_3:
|
||||
return LOG_STR("VDD/3");
|
||||
case ADC_REF_VDD_1_4:
|
||||
return LOG_STR("VDD/4");
|
||||
case ADC_REF_INTERNAL:
|
||||
return LOG_STR("INTERNAL");
|
||||
case ADC_REF_EXTERNAL0:
|
||||
return LOG_STR("External, input 0");
|
||||
case ADC_REF_EXTERNAL1:
|
||||
return LOG_STR("External, input 1");
|
||||
}
|
||||
return LOG_STR("undefined reference");
|
||||
}
|
||||
|
||||
static const LogString *input_to_str(uint8_t input) {
|
||||
switch (input) {
|
||||
case NRF_SAADC_INPUT_AIN0:
|
||||
return LOG_STR("AIN0");
|
||||
case NRF_SAADC_INPUT_AIN1:
|
||||
return LOG_STR("AIN1");
|
||||
case NRF_SAADC_INPUT_AIN2:
|
||||
return LOG_STR("AIN2");
|
||||
case NRF_SAADC_INPUT_AIN3:
|
||||
return LOG_STR("AIN3");
|
||||
case NRF_SAADC_INPUT_AIN4:
|
||||
return LOG_STR("AIN4");
|
||||
case NRF_SAADC_INPUT_AIN5:
|
||||
return LOG_STR("AIN5");
|
||||
case NRF_SAADC_INPUT_AIN6:
|
||||
return LOG_STR("AIN6");
|
||||
case NRF_SAADC_INPUT_AIN7:
|
||||
return LOG_STR("AIN7");
|
||||
case NRF_SAADC_INPUT_VDD:
|
||||
return LOG_STR("VDD");
|
||||
case NRF_SAADC_INPUT_VDDHDIV5:
|
||||
return LOG_STR("VDDHDIV5");
|
||||
}
|
||||
return LOG_STR("undefined input");
|
||||
}
|
||||
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG,
|
||||
" Name: %s\n"
|
||||
" Channel: %d\n"
|
||||
" vref_mv: %d\n"
|
||||
" Resolution %d\n"
|
||||
" Oversampling %d",
|
||||
this->channel_->dev->name, this->channel_->channel_id, this->channel_->vref_mv, this->channel_->resolution,
|
||||
this->channel_->oversampling);
|
||||
|
||||
ESP_LOGV(TAG,
|
||||
" Gain: %s\n"
|
||||
" reference: %s\n"
|
||||
" acquisition_time: %d\n"
|
||||
" differential %s",
|
||||
LOG_STR_ARG(gain_to_str(this->channel_->channel_cfg.gain)),
|
||||
LOG_STR_ARG(reference_to_str(this->channel_->channel_cfg.reference)),
|
||||
this->channel_->channel_cfg.acquisition_time, YESNO(this->channel_->channel_cfg.differential));
|
||||
if (this->channel_->channel_cfg.differential) {
|
||||
ESP_LOGV(TAG,
|
||||
" Positive: %s\n"
|
||||
" Negative: %s",
|
||||
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)),
|
||||
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_negative)));
|
||||
} else {
|
||||
ESP_LOGV(TAG, " Positive: %s", LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)));
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
auto aggr = Aggregator<int32_t>(this->sampling_mode_);
|
||||
int err;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
int16_t buf = 0;
|
||||
struct adc_sequence sequence = {
|
||||
.buffer = &buf,
|
||||
/* buffer size in bytes, not number of samples */
|
||||
.buffer_size = sizeof(buf),
|
||||
};
|
||||
int32_t val_raw;
|
||||
|
||||
err = adc_sequence_init_dt(this->channel_, &sequence);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could sequence init %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
err = adc_read(this->channel_->dev, &sequence);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could not read %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
val_raw = (int32_t) buf;
|
||||
if (!this->channel_->channel_cfg.differential) {
|
||||
// https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/0ed4d9ffc674ae407be7cacf5696a02f5e789861/cores/nRF5/wiring_analog_nRF52.c#L222
|
||||
if (val_raw < 0) {
|
||||
val_raw = 0;
|
||||
}
|
||||
}
|
||||
aggr.add_sample(val_raw);
|
||||
}
|
||||
|
||||
int32_t val_mv = aggr.aggregate();
|
||||
|
||||
if (this->output_raw_) {
|
||||
return val_mv;
|
||||
}
|
||||
|
||||
err = adc_raw_to_millivolts_dt(this->channel_, &val_mv);
|
||||
/* conversion to mV may not be supported, skip if not */
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Value in mV not available %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return val_mv / 1000.0f;
|
||||
}
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
#endif
|
@@ -3,6 +3,12 @@ import logging
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
zephyr_add_prj_conf,
|
||||
zephyr_add_user,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ATTENUATION,
|
||||
@@ -11,6 +17,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
CONF_RAW,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
PLATFORM_NRF52,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
@@ -60,6 +67,10 @@ ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
|
||||
CONF_NRF_SAADC = "nrf_saadc"
|
||||
|
||||
adc_dt_spec = cg.global_ns.class_("adc_dt_spec")
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
ADCSensor,
|
||||
@@ -75,6 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
|
||||
cv.only_on_esp32, _attenuation
|
||||
),
|
||||
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
|
||||
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
|
||||
}
|
||||
@@ -83,6 +95,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_config,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -93,7 +107,7 @@ async def to_code(config):
|
||||
cg.add_define("USE_ADC_SENSOR_VCC")
|
||||
elif config[CONF_PIN] == "TEMPERATURE":
|
||||
cg.add(var.set_is_temperature())
|
||||
else:
|
||||
elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
@@ -122,3 +136,41 @@ async def to_code(config):
|
||||
):
|
||||
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))
|
||||
|
||||
elif CORE.is_nrf52:
|
||||
CORE.data.setdefault(CONF_ADC_CHANNEL_ID, 0)
|
||||
channel_id = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
CORE.data[CONF_ADC_CHANNEL_ID] = channel_id + 1
|
||||
zephyr_add_prj_conf("ADC", True)
|
||||
nrf_saadc = config[CONF_NRF_SAADC]
|
||||
rhs = cg.RawExpression(
|
||||
f"ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {channel_id})"
|
||||
)
|
||||
adc = cg.new_Pvariable(nrf_saadc, rhs)
|
||||
cg.add(var.set_adc_channel(adc))
|
||||
gain = "ADC_GAIN_1_6"
|
||||
pin_number = config[CONF_PIN][CONF_NUMBER]
|
||||
if pin_number == "VDDHDIV5":
|
||||
gain = "ADC_GAIN_1_2"
|
||||
if isinstance(pin_number, int):
|
||||
GPIO_TO_AIN = {v: k for k, v in AIN_TO_GPIO.items()}
|
||||
pin_number = GPIO_TO_AIN[pin_number]
|
||||
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&adc {{
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
channel@{channel_id} {{
|
||||
reg = <{channel_id}>;
|
||||
zephyr,gain = "{gain}";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
||||
zephyr,resolution = <14>;
|
||||
zephyr,oversampling = <8>;
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
@@ -36,6 +36,7 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
@@ -51,6 +52,20 @@ CONF_POWER_GAIN = "power_gain"
|
||||
|
||||
CONF_NEUTRAL = "neutral"
|
||||
|
||||
# Tuple of power channel phases
|
||||
POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
|
||||
|
||||
# Tuple of sensor types that can be configured for power channels
|
||||
POWER_SENSOR_TYPES = (
|
||||
CONF_CURRENT,
|
||||
CONF_VOLTAGE,
|
||||
CONF_ACTIVE_POWER,
|
||||
CONF_APPARENT_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_FORWARD_ACTIVE_ENERGY,
|
||||
CONF_REVERSE_ACTIVE_ENERGY,
|
||||
)
|
||||
|
||||
NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(NeutralChannel),
|
||||
@@ -150,7 +165,64 @@ POWER_CHANNEL_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
def prefix_sensor_name(
|
||||
sensor_conf: ConfigType,
|
||||
channel_name: str,
|
||||
channel_config: ConfigType,
|
||||
sensor_type: str,
|
||||
) -> None:
|
||||
"""Helper to prefix sensor name with channel name.
|
||||
|
||||
Args:
|
||||
sensor_conf: The sensor configuration (dict or string)
|
||||
channel_name: The channel name to prefix with
|
||||
channel_config: The channel configuration to update
|
||||
sensor_type: The sensor type key in the channel config
|
||||
"""
|
||||
if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf:
|
||||
sensor_name = sensor_conf[CONF_NAME]
|
||||
if sensor_name and not sensor_name.startswith(channel_name):
|
||||
sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}"
|
||||
elif isinstance(sensor_conf, str):
|
||||
# Simple value case - convert to dict with prefixed name
|
||||
channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"}
|
||||
|
||||
|
||||
def process_channel_sensors(
|
||||
config: ConfigType, channel_key: str, sensor_types: tuple
|
||||
) -> None:
|
||||
"""Process sensors for a channel and prefix their names.
|
||||
|
||||
Args:
|
||||
config: The main configuration
|
||||
channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL)
|
||||
sensor_types: Tuple of sensor types to process for this channel
|
||||
"""
|
||||
if not (channel_config := config.get(channel_key)) or not (
|
||||
channel_name := channel_config.get(CONF_NAME)
|
||||
):
|
||||
return
|
||||
|
||||
for sensor_type in sensor_types:
|
||||
if sensor_conf := channel_config.get(sensor_type):
|
||||
prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type)
|
||||
|
||||
|
||||
def preprocess_channels(config: ConfigType) -> ConfigType:
|
||||
"""Preprocess channel configurations to add channel name prefix to sensor names."""
|
||||
# Process power channels
|
||||
for channel in POWER_PHASES:
|
||||
process_channel_sensors(config, channel, POWER_SENSOR_TYPES)
|
||||
|
||||
# Process neutral channel
|
||||
process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
preprocess_channels,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ADE7880),
|
||||
@@ -167,7 +239,7 @@ CONFIG_SCHEMA = (
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x38))
|
||||
.extend(i2c.i2c_device_schema(0x38)),
|
||||
)
|
||||
|
||||
|
||||
@@ -188,15 +260,7 @@ async def neutral_channel(config):
|
||||
async def power_channel(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
for sensor_type in [
|
||||
CONF_CURRENT,
|
||||
CONF_VOLTAGE,
|
||||
CONF_ACTIVE_POWER,
|
||||
CONF_APPARENT_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_FORWARD_ACTIVE_ENERGY,
|
||||
CONF_REVERSE_ACTIVE_ENERGY,
|
||||
]:
|
||||
for sensor_type in POWER_SENSOR_TYPES:
|
||||
if conf := config.get(sensor_type):
|
||||
sens = await sensor.new_sensor(conf)
|
||||
cg.add(getattr(var, f"set_{sensor_type}")(sens))
|
||||
@@ -216,44 +280,6 @@ async def power_channel(config):
|
||||
return var
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]:
|
||||
if channel := config.get(channel):
|
||||
channel_name = channel.get(CONF_NAME)
|
||||
|
||||
for sensor_type in [
|
||||
CONF_CURRENT,
|
||||
CONF_VOLTAGE,
|
||||
CONF_ACTIVE_POWER,
|
||||
CONF_APPARENT_POWER,
|
||||
CONF_POWER_FACTOR,
|
||||
CONF_FORWARD_ACTIVE_ENERGY,
|
||||
CONF_REVERSE_ACTIVE_ENERGY,
|
||||
]:
|
||||
if conf := channel.get(sensor_type):
|
||||
sensor_name = conf.get(CONF_NAME)
|
||||
if (
|
||||
sensor_name
|
||||
and channel_name
|
||||
and not sensor_name.startswith(channel_name)
|
||||
):
|
||||
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
|
||||
|
||||
if channel := config.get(CONF_NEUTRAL):
|
||||
channel_name = channel.get(CONF_NAME)
|
||||
if conf := channel.get(CONF_CURRENT):
|
||||
sensor_name = conf.get(CONF_NAME)
|
||||
if (
|
||||
sensor_name
|
||||
and channel_name
|
||||
and not sensor_name.startswith(channel_name)
|
||||
):
|
||||
conf[CONF_NAME] = f"{channel_name} {sensor_name}"
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
@@ -89,7 +89,7 @@ void AGS10Component::dump_config() {
|
||||
bool AGS10Component::new_i2c_address(uint8_t newaddress) {
|
||||
uint8_t rev_newaddress = ~newaddress;
|
||||
std::array<uint8_t, 5> data{newaddress, rev_newaddress, newaddress, rev_newaddress, 0};
|
||||
data[4] = calc_crc8_(data, 4);
|
||||
data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
|
||||
if (!this->write_bytes(REG_ADDRESS, data)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->status_set_warning();
|
||||
@@ -109,7 +109,7 @@ bool AGS10Component::set_zero_point_with_current_resistance() { return this->set
|
||||
|
||||
bool AGS10Component::set_zero_point_with(uint16_t value) {
|
||||
std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0};
|
||||
data[4] = calc_crc8_(data, 4);
|
||||
data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
|
||||
if (!this->write_bytes(REG_CALIBRATION, data)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->status_set_warning();
|
||||
@@ -184,7 +184,7 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che
|
||||
auto res = *data;
|
||||
auto crc_byte = res[len];
|
||||
|
||||
if (crc_byte != calc_crc8_(res, len)) {
|
||||
if (crc_byte != crc8(res.data(), len, 0xFF, 0x31, true)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
ESP_LOGE(TAG, "Reading AGS10 version failed: crc error!");
|
||||
return optional<std::array<uint8_t, N>>();
|
||||
@@ -192,20 +192,5 @@ template<size_t N> optional<std::array<uint8_t, N>> AGS10Component::read_and_che
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
template<size_t N> uint8_t AGS10Component::calc_crc8_(std::array<uint8_t, N> dat, uint8_t num) {
|
||||
uint8_t i, byte1, crc = 0xFF;
|
||||
for (byte1 = 0; byte1 < num; byte1++) {
|
||||
crc ^= (dat[byte1]);
|
||||
for (i = 0; i < 8; i++) {
|
||||
if (crc & 0x80) {
|
||||
crc = (crc << 1) ^ 0x31;
|
||||
} else {
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
} // namespace ags10
|
||||
} // namespace esphome
|
||||
|
@@ -1,9 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ags10 {
|
||||
@@ -99,16 +99,6 @@ class AGS10Component : public PollingComponent, public i2c::I2CDevice {
|
||||
* Read, checks and returns data from the sensor.
|
||||
*/
|
||||
template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register);
|
||||
|
||||
/**
|
||||
* Calculates CRC8 value.
|
||||
*
|
||||
* CRC8 calculation, initial value: 0xFF, polynomial: 0x31 (x8+ x5+ x4+1)
|
||||
*
|
||||
* @param[in] dat the data buffer
|
||||
* @param num number of bytes in the buffer
|
||||
*/
|
||||
template<size_t N> uint8_t calc_crc8_(std::array<uint8_t, N> dat, uint8_t num);
|
||||
};
|
||||
|
||||
template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>, public Parented<AGS10Component> {
|
||||
|
@@ -18,6 +18,6 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
yield esp32_ble_tracker.register_ble_device(var, config)
|
||||
await esp32_ble_tracker.register_ble_device(var, config)
|
||||
|
@@ -13,7 +13,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
@@ -301,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -310,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -319,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -333,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
@@ -349,7 +345,6 @@ async def alarm_control_panel_is_armed_to_code(
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_global(alarm_control_panel_ns.using)
|
||||
cg.add_define("USE_ALARM_CONTROL_PANEL")
|
||||
|
@@ -29,22 +29,6 @@ namespace am2315c {
|
||||
|
||||
static const char *const TAG = "am2315c";
|
||||
|
||||
uint8_t AM2315C::crc8_(uint8_t *data, uint8_t len) {
|
||||
uint8_t crc = 0xFF;
|
||||
while (len--) {
|
||||
crc ^= *data++;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
if (crc & 0x80) {
|
||||
crc <<= 1;
|
||||
crc ^= 0x31;
|
||||
} else {
|
||||
crc <<= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
bool AM2315C::reset_register_(uint8_t reg) {
|
||||
// code based on demo code sent by www.aosong.com
|
||||
// no further documentation.
|
||||
@@ -86,7 +70,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
|
||||
humidity = raw * 9.5367431640625e-5;
|
||||
raw = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
|
||||
temperature = raw * 1.9073486328125e-4 - 50;
|
||||
return this->crc8_(data, 6) == data[6];
|
||||
return crc8(data, 6, 0xFF, 0x31, true) == data[6];
|
||||
}
|
||||
|
||||
void AM2315C::setup() {
|
||||
|
@@ -21,9 +21,9 @@
|
||||
// SOFTWARE.
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace am2315c {
|
||||
@@ -39,7 +39,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
protected:
|
||||
uint8_t crc8_(uint8_t *data, uint8_t len);
|
||||
bool convert_(uint8_t *data, float &humidity, float &temperature);
|
||||
bool reset_register_(uint8_t reg);
|
||||
|
||||
|
@@ -34,17 +34,20 @@ SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
),
|
||||
espImage.validate_settings,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -24,12 +24,12 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
|
||||
DOMAIN = "api"
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
api_ns = cg.esphome_ns.namespace("api")
|
||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||
@@ -122,8 +122,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
),
|
||||
@@ -136,7 +134,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(40.0)
|
||||
@coroutine_with_priority(CoroPriority.WEB)
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
@@ -323,6 +321,7 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
)
|
||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
cg.add(var.set_service("esphome.tag_scanned"))
|
||||
|
@@ -250,8 +250,8 @@ message DeviceInfoResponse {
|
||||
// Supports receiving and saving api encryption key
|
||||
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
|
||||
|
||||
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
|
||||
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
|
||||
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"];
|
||||
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"];
|
||||
|
||||
// Top-level area info to phase out suggested_area
|
||||
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
|
||||
@@ -1297,6 +1297,9 @@ enum MediaPlayerState {
|
||||
MEDIA_PLAYER_STATE_IDLE = 1;
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2;
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3;
|
||||
MEDIA_PLAYER_STATE_ANNOUNCING = 4;
|
||||
MEDIA_PLAYER_STATE_OFF = 5;
|
||||
MEDIA_PLAYER_STATE_ON = 6;
|
||||
}
|
||||
enum MediaPlayerCommand {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0;
|
||||
@@ -1304,6 +1307,15 @@ enum MediaPlayerCommand {
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2;
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3;
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
|
||||
MEDIA_PLAYER_COMMAND_TOGGLE = 5;
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6;
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7;
|
||||
MEDIA_PLAYER_COMMAND_ENQUEUE = 8;
|
||||
MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9;
|
||||
MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10;
|
||||
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11;
|
||||
MEDIA_PLAYER_COMMAND_TURN_ON = 12;
|
||||
MEDIA_PLAYER_COMMAND_TURN_OFF = 13;
|
||||
}
|
||||
enum MediaPlayerFormatPurpose {
|
||||
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0;
|
||||
@@ -1338,6 +1350,8 @@ message ListEntitiesMediaPlayerResponse {
|
||||
repeated MediaPlayerSupportedFormat supported_formats = 9;
|
||||
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
|
||||
uint32 feature_flags = 11;
|
||||
}
|
||||
message MediaPlayerStateResponse {
|
||||
option (id) = 64;
|
||||
@@ -1424,11 +1438,11 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1;
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
|
||||
}
|
||||
|
||||
enum BluetoothDeviceRequestType {
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0;
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0 [deprecated = true]; // V1 removed, use V3 variants
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1;
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2;
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3;
|
||||
@@ -1468,21 +1482,39 @@ message BluetoothGATTGetServicesRequest {
|
||||
}
|
||||
|
||||
message BluetoothGATTDescriptor {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
|
||||
// 128-bit UUIDs always use the uuid field for backwards compatibility.
|
||||
uint32 short_uuid = 3; // 16-bit or 32-bit UUID
|
||||
}
|
||||
|
||||
message BluetoothGATTCharacteristic {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
uint32 properties = 3;
|
||||
repeated BluetoothGATTDescriptor descriptors = 4;
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
|
||||
// 128-bit UUIDs always use the uuid field for backwards compatibility.
|
||||
uint32 short_uuid = 5; // 16-bit or 32-bit UUID
|
||||
}
|
||||
|
||||
message BluetoothGATTService {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
repeated BluetoothGATTCharacteristic characteristics = 3;
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
|
||||
// 128-bit UUIDs always use the uuid field for backwards compatibility.
|
||||
uint32 short_uuid = 4; // 16-bit or 32-bit UUID
|
||||
}
|
||||
|
||||
message BluetoothGATTGetServicesResponse {
|
||||
@@ -1491,7 +1523,7 @@ message BluetoothGATTGetServicesResponse {
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
|
||||
uint64 address = 1;
|
||||
repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1];
|
||||
repeated BluetoothGATTService services = 2;
|
||||
}
|
||||
|
||||
message BluetoothGATTGetServicesDoneResponse {
|
||||
@@ -1589,7 +1621,10 @@ message BluetoothConnectionsFreeResponse {
|
||||
|
||||
uint32 free = 1;
|
||||
uint32 limit = 2;
|
||||
repeated uint64 allocated = 3;
|
||||
repeated uint64 allocated = 3 [
|
||||
(fixed_array_size_define) = "BLUETOOTH_PROXY_MAX_CONNECTIONS",
|
||||
(fixed_array_skip_zero) = true
|
||||
];
|
||||
}
|
||||
|
||||
message BluetoothGATTErrorResponse {
|
||||
@@ -1677,6 +1712,7 @@ message BluetoothScannerStateResponse {
|
||||
|
||||
BluetoothScannerState state = 1;
|
||||
BluetoothScannerMode mode = 2;
|
||||
BluetoothScannerMode configured_mode = 3;
|
||||
}
|
||||
|
||||
message BluetoothScannerSetModeRequest {
|
||||
|
@@ -112,8 +112,7 @@ void APIConnection::start() {
|
||||
APIError err = this->helper_->init();
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
errno);
|
||||
this->log_warning_("Helper init failed", err);
|
||||
return;
|
||||
}
|
||||
this->client_info_.peername = helper_->getpeername();
|
||||
@@ -144,8 +143,7 @@ void APIConnection::loop() {
|
||||
APIError err = this->helper_->loop();
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||
api_error_to_str(err), errno);
|
||||
this->log_socket_operation_failed_(err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,8 +159,7 @@ void APIConnection::loop() {
|
||||
break;
|
||||
} else if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
errno);
|
||||
this->log_warning_("Reading failed", err);
|
||||
return;
|
||||
} else {
|
||||
this->last_traffic_ = now;
|
||||
@@ -292,16 +289,26 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
|
||||
return 0; // Doesn't fit
|
||||
}
|
||||
|
||||
// Allocate buffer space - pass payload size, allocation functions add header/footer space
|
||||
ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size)
|
||||
: conn->allocate_batch_message_buffer(calculated_size);
|
||||
|
||||
// Get buffer size after allocation (which includes header padding)
|
||||
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
size_t size_before_encode = shared_buf.size();
|
||||
|
||||
if (is_single || conn->flags_.batch_first_message) {
|
||||
// Single message or first batch message
|
||||
conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size);
|
||||
if (conn->flags_.batch_first_message) {
|
||||
conn->flags_.batch_first_message = false;
|
||||
}
|
||||
} else {
|
||||
// Batch message second or later
|
||||
// Add padding for previous message footer + this message header
|
||||
size_t current_size = shared_buf.size();
|
||||
shared_buf.reserve(current_size + total_calculated_size);
|
||||
shared_buf.resize(current_size + footer_size + header_padding);
|
||||
}
|
||||
|
||||
// Encode directly into buffer
|
||||
msg.encode(buffer);
|
||||
size_t size_before_encode = shared_buf.size();
|
||||
msg.encode({&shared_buf});
|
||||
|
||||
// Calculate actual encoded size (not including header that was already added)
|
||||
size_t actual_payload_size = shared_buf.size() - size_before_encode;
|
||||
@@ -413,7 +420,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
||||
msg.supports_speed = traits.supports_speed();
|
||||
msg.supports_direction = traits.supports_direction();
|
||||
msg.supported_speed_count = traits.supported_speed_count();
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
|
||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
@@ -458,9 +465,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
||||
resp.cold_white = values.get_cold_white();
|
||||
resp.warm_white = values.get_warm_white();
|
||||
if (light->supports_effects()) {
|
||||
// get_effect_name() returns temporary std::string - must store it
|
||||
std::string effect_name = light->get_effect_name();
|
||||
resp.set_effect(StringRef(effect_name));
|
||||
resp.set_effect(light->get_effect_name_ref());
|
||||
}
|
||||
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -469,7 +474,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
ListEntitiesLightResponse msg;
|
||||
auto traits = light->get_traits();
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes();
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
|
||||
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
@@ -655,7 +660,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.supports_current_humidity = traits.get_supports_current_humidity();
|
||||
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
|
||||
msg.supports_target_humidity = traits.get_supports_target_humidity();
|
||||
msg.supported_modes = &traits.get_supported_modes();
|
||||
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
||||
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
||||
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
||||
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
||||
@@ -663,11 +668,11 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||
msg.supports_action = traits.get_supports_action();
|
||||
msg.supported_fan_modes = &traits.get_supported_fan_modes();
|
||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
|
||||
msg.supported_presets = &traits.get_supported_presets();
|
||||
msg.supported_custom_presets = &traits.get_supported_custom_presets();
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
||||
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
||||
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
||||
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
|
||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -999,6 +1004,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
ListEntitiesMediaPlayerResponse msg;
|
||||
auto traits = media_player->get_traits();
|
||||
msg.supports_pause = traits.get_supports_pause();
|
||||
msg.feature_flags = traits.get_feature_flags();
|
||||
for (auto &supported_format : traits.get_supported_formats()) {
|
||||
msg.supported_formats.emplace_back();
|
||||
auto &media_format = msg.supported_formats.back();
|
||||
@@ -1107,10 +1113,8 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
|
||||
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
BluetoothConnectionsFreeResponse resp;
|
||||
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free();
|
||||
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
|
||||
return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||
@@ -1365,7 +1369,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 10;
|
||||
resp.api_version_minor = 12;
|
||||
// Temporary string for concatenation - will be valid during send_message call
|
||||
std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
||||
resp.set_server_info(StringRef(server_info));
|
||||
@@ -1419,9 +1423,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
|
||||
// get_compilation_time() returns temporary std::string - must store it
|
||||
std::string compilation_time = App.get_compilation_time();
|
||||
resp.set_compilation_time(StringRef(compilation_time));
|
||||
resp.set_compilation_time(App.get_compilation_time_ref());
|
||||
|
||||
// Compile-time StringRef constants for manufacturers
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
@@ -1466,18 +1468,22 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
size_t device_index = 0;
|
||||
for (auto const &device : App.get_devices()) {
|
||||
resp.devices.emplace_back();
|
||||
auto &device_info = resp.devices.back();
|
||||
if (device_index >= ESPHOME_DEVICE_COUNT)
|
||||
break;
|
||||
auto &device_info = resp.devices[device_index++];
|
||||
device_info.device_id = device->get_device_id();
|
||||
device_info.set_name(StringRef(device->get_name()));
|
||||
device_info.area_id = device->get_area_id();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
size_t area_index = 0;
|
||||
for (auto const &area : App.get_areas()) {
|
||||
resp.areas.emplace_back();
|
||||
auto &area_info = resp.areas.back();
|
||||
if (area_index >= ESPHOME_AREA_COUNT)
|
||||
break;
|
||||
auto &area_info = resp.areas[area_index++];
|
||||
area_info.area_id = area->get_area_id();
|
||||
area_info.set_name(StringRef(area->get_name()));
|
||||
}
|
||||
@@ -1539,8 +1545,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
APIError err = this->helper_->loop();
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||
api_error_to_str(err), errno);
|
||||
this->log_socket_operation_failed_(err);
|
||||
return false;
|
||||
}
|
||||
if (this->helper_->can_write_without_blocking())
|
||||
@@ -1560,8 +1565,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||
api_error_to_str(err), errno);
|
||||
this->log_warning_("Packet write failed", err);
|
||||
return false;
|
||||
}
|
||||
// Do not set last_traffic_ on send
|
||||
@@ -1622,14 +1626,6 @@ bool APIConnection::schedule_batch_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
ProtoWriteBuffer APIConnection::allocate_single_message_buffer(uint16_t size) { return this->create_buffer(size); }
|
||||
|
||||
ProtoWriteBuffer APIConnection::allocate_batch_message_buffer(uint16_t size) {
|
||||
ProtoWriteBuffer result = this->prepare_message_buffer(size, this->flags_.batch_first_message);
|
||||
this->flags_.batch_first_message = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure PacketInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<PacketInfo>::value,
|
||||
@@ -1646,6 +1642,8 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get shared buffer reference once to avoid multiple calls
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
size_t num_items = this->deferred_batch_.size();
|
||||
|
||||
// Fast path for single message - allocate exact size needed
|
||||
@@ -1656,8 +1654,7 @@ void APIConnection::process_batch_() {
|
||||
uint16_t payload_size =
|
||||
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
|
||||
|
||||
if (payload_size > 0 &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
|
||||
if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
@@ -1684,20 +1681,18 @@ void APIConnection::process_batch_() {
|
||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
|
||||
// Initialize buffer and tracking variables
|
||||
this->parent_->get_shared_buffer_ref().clear();
|
||||
shared_buf.clear();
|
||||
|
||||
// Pre-calculate exact buffer size needed based on message types
|
||||
uint32_t total_estimated_size = 0;
|
||||
uint32_t total_estimated_size = num_items * (header_padding + footer_size);
|
||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
total_estimated_size += item.estimated_size;
|
||||
}
|
||||
|
||||
// Calculate total overhead for all messages
|
||||
uint32_t total_overhead = (header_padding + footer_size) * num_items;
|
||||
|
||||
// Reserve based on estimated size (much more accurate than 24-byte worst-case)
|
||||
this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead);
|
||||
shared_buf.reserve(total_estimated_size);
|
||||
this->flags_.batch_first_message = true;
|
||||
|
||||
size_t items_processed = 0;
|
||||
@@ -1738,8 +1733,8 @@ void APIConnection::process_batch_() {
|
||||
}
|
||||
remaining_size -= payload_size;
|
||||
// Calculate where the next message's header padding will start
|
||||
// Current buffer size + footer space (that prepare_message_buffer will add for this message)
|
||||
current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size;
|
||||
// Current buffer size + footer space for this message
|
||||
current_offset = shared_buf.size() + footer_size;
|
||||
}
|
||||
|
||||
if (items_processed == 0) {
|
||||
@@ -1749,17 +1744,15 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected packets
|
||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()},
|
||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const PacketInfo>(packet_info, packet_count));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
errno);
|
||||
this->log_warning_("Batch write failed", err);
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1837,5 +1830,11 @@ void APIConnection::process_state_subscriptions_() {
|
||||
}
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
|
||||
void APIConnection::log_warning_(const char *message, APIError err) {
|
||||
ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno);
|
||||
}
|
||||
|
||||
void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); }
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@@ -44,7 +44,7 @@ static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HO
|
||||
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
|
||||
#endif
|
||||
|
||||
class APIConnection : public APIServerConnection {
|
||||
class APIConnection final : public APIServerConnection {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
@@ -235,6 +235,13 @@ class APIConnection : public APIServerConnection {
|
||||
this->is_authenticated();
|
||||
}
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection
|
||||
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
|
||||
return this->client_api_version_major_ > major ||
|
||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||
}
|
||||
|
||||
void on_fatal_error() override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
void on_unauthenticated_access() override;
|
||||
@@ -245,44 +252,21 @@ class APIConnection : public APIServerConnection {
|
||||
|
||||
// Get header padding size - used for both reserve and insert
|
||||
uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
|
||||
// Get shared buffer from parent server
|
||||
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding,
|
||||
reserve_size + header_padding + this->helper_->frame_footer_size());
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
|
||||
shared_buf.clear();
|
||||
// Reserve space for header padding + message + footer
|
||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
|
||||
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
|
||||
shared_buf.reserve(total_size);
|
||||
// Resize to add header padding so message encoding starts at the correct position
|
||||
shared_buf.resize(header_padding);
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
// Prepare buffer for next message in batch
|
||||
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
|
||||
// Get reference to shared buffer (it maintains state between batch messages)
|
||||
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
|
||||
if (is_first_message) {
|
||||
shared_buf.clear();
|
||||
}
|
||||
|
||||
size_t current_size = shared_buf.size();
|
||||
|
||||
// Calculate padding to add:
|
||||
// - First message: just header padding
|
||||
// - Subsequent messages: footer for previous message + header padding for this message
|
||||
size_t padding_to_add = is_first_message
|
||||
? this->helper_->frame_header_padding()
|
||||
: this->helper_->frame_header_padding() + this->helper_->frame_footer_size();
|
||||
|
||||
// Reserve space for padding + message
|
||||
shared_buf.reserve(current_size + padding_to_add + message_size);
|
||||
|
||||
// Resize to add the padding bytes
|
||||
shared_buf.resize(current_size + padding_to_add);
|
||||
|
||||
return {&shared_buf};
|
||||
}
|
||||
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
@@ -290,17 +274,13 @@ class APIConnection : public APIServerConnection {
|
||||
|
||||
std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
|
||||
|
||||
// Buffer allocator methods for batch processing
|
||||
ProtoWriteBuffer allocate_single_message_buffer(uint16_t size);
|
||||
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void process_state_subscriptions_();
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
#endif
|
||||
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||
@@ -321,9 +301,17 @@ class APIConnection : public APIServerConnection {
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
// IMPORTANT: get_object_id() may return a temporary std::string
|
||||
std::string object_id = entity->get_object_id();
|
||||
msg.set_object_id(StringRef(object_id));
|
||||
// Try to use static reference first to avoid allocation
|
||||
StringRef static_ref = entity->get_object_id_ref_for_api_();
|
||||
// Store dynamic string outside the if-else to maintain lifetime
|
||||
std::string object_id;
|
||||
if (!static_ref.empty()) {
|
||||
msg.set_object_id(static_ref);
|
||||
} else {
|
||||
// Dynamic case - need to allocate
|
||||
object_id = entity->get_object_id();
|
||||
msg.set_object_id(StringRef(object_id));
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.set_name(entity->get_name());
|
||||
@@ -696,10 +684,16 @@ class APIConnection : public APIServerConnection {
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
// Try to send immediately if:
|
||||
// 1. We should try to send immediately (should_try_send_immediately = true)
|
||||
// 2. Batch delay is 0 (user has opted in to immediate sending)
|
||||
// 3. Buffer has space available
|
||||
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
|
||||
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
|
||||
// the main loop is blocked, e.g., during OTA updates)
|
||||
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
|
||||
// AND Batch delay is 0 (user has opted in to immediate sending)
|
||||
// 3. AND: Buffer has space available
|
||||
if ((
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
|
||||
this->helper_->can_write_without_blocking()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
@@ -736,6 +730,11 @@ class APIConnection : public APIServerConnection {
|
||||
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
// Helper function to log API errors with errno
|
||||
void log_warning_(const char *message, APIError err);
|
||||
// Specific helper for duplicated error message
|
||||
void log_socket_operation_failed_(APIError err);
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
@@ -156,7 +156,9 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
|
||||
}
|
||||
|
||||
// Try to send directly if no buffered data
|
||||
ssize_t sent = this->socket_->writev(iov, iovcnt);
|
||||
// Optimize for single iovec case (common for plaintext API)
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) {
|
||||
APIError err = this->handle_socket_write_error_();
|
||||
|
@@ -104,9 +104,9 @@ class APIFrameHelper {
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
|
||||
// Get the frame header padding required by this protocol
|
||||
virtual uint8_t frame_header_padding() = 0;
|
||||
uint8_t frame_header_padding() const { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
virtual uint8_t frame_footer_size() = 0;
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper : public APIFrameHelper {
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
|
||||
const ClientInfo *client_info)
|
||||
@@ -25,10 +25,6 @@ class APINoiseFrameHelper : public APIFrameHelper {
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
// Get the frame header padding required by this protocol
|
||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||
|
||||
protected:
|
||||
APIError state_action_();
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info) {
|
||||
@@ -22,9 +22,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
uint8_t frame_header_padding() override { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() override { return frame_footer_size_; }
|
||||
|
||||
protected:
|
||||
APIError try_read_frame_(std::vector<uint8_t> *frame);
|
||||
|
@@ -28,5 +28,33 @@ extend google.protobuf.FieldOptions {
|
||||
optional string field_ifdef = 1042;
|
||||
optional uint32 fixed_array_size = 50007;
|
||||
optional bool no_zero_copy = 50008 [default=false];
|
||||
optional bool fixed_array_skip_zero = 50009 [default=false];
|
||||
optional string fixed_array_size_define = 50010;
|
||||
optional string fixed_array_with_length_define = 50011;
|
||||
|
||||
// container_pointer: Zero-copy optimization for repeated fields.
|
||||
//
|
||||
// When container_pointer is set on a repeated field, the generated message will
|
||||
// store a pointer to an existing container instead of copying the data into the
|
||||
// message's own repeated field. This eliminates heap allocations and improves performance.
|
||||
//
|
||||
// Requirements for safe usage:
|
||||
// 1. The source container must remain valid until the message is encoded
|
||||
// 2. Messages must be encoded immediately (which ESPHome does by default)
|
||||
// 3. The container type must match the field type exactly
|
||||
//
|
||||
// Supported container types:
|
||||
// - "std::vector<T>" for most repeated fields
|
||||
// - "std::set<T>" for unique/sorted data
|
||||
// - Full type specification required for enums (e.g., "std::set<climate::ClimateMode>")
|
||||
//
|
||||
// Example usage in .proto file:
|
||||
// repeated string supported_modes = 12 [(container_pointer) = "std::set"];
|
||||
// repeated ColorMode color_modes = 13 [(container_pointer) = "std::set<light::ColorMode>"];
|
||||
//
|
||||
// The corresponding C++ code must provide const reference access to a container
|
||||
// that matches the specified type and remains valid during message encoding.
|
||||
// This is typically done through methods returning const T& or special accessor
|
||||
// methods like get_options() or supported_modes_for_api_().
|
||||
optional string container_pointer = 50001;
|
||||
}
|
||||
|
@@ -115,12 +115,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_bool(19, this->api_encryption_supported);
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
for (auto &it : this->devices) {
|
||||
for (const auto &it : this->devices) {
|
||||
buffer.encode_message(20, it, true);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
for (auto &it : this->areas) {
|
||||
for (const auto &it : this->areas) {
|
||||
buffer.encode_message(21, it, true);
|
||||
}
|
||||
#endif
|
||||
@@ -167,10 +167,14 @@ void DeviceInfoResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_bool(2, this->api_encryption_supported);
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
size.add_repeated_message(2, this->devices);
|
||||
for (const auto &it : this->devices) {
|
||||
size.add_message_object_force(2, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
size.add_repeated_message(2, this->areas);
|
||||
for (const auto &it : this->areas) {
|
||||
size.add_message_object_force(2, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
size.add_message_object(2, this->area);
|
||||
@@ -1725,6 +1729,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
#ifdef USE_DEVICES
|
||||
buffer.encode_uint32(10, this->device_id);
|
||||
#endif
|
||||
buffer.encode_uint32(11, this->feature_flags);
|
||||
}
|
||||
void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->object_id_ref_.size());
|
||||
@@ -1740,6 +1745,7 @@ void ListEntitiesMediaPlayerResponse::calculate_size(ProtoSize &size) const {
|
||||
#ifdef USE_DEVICES
|
||||
size.add_uint32(1, this->device_id);
|
||||
#endif
|
||||
size.add_uint32(1, this->feature_flags);
|
||||
}
|
||||
void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_fixed32(1, this->key);
|
||||
@@ -1837,12 +1843,14 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->data_len);
|
||||
}
|
||||
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (auto &it : this->advertisements) {
|
||||
buffer.encode_message(1, it, true);
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
buffer.encode_message(1, this->advertisements[i], true);
|
||||
}
|
||||
}
|
||||
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_repeated_message(1, this->advertisements);
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
size.add_message_object_force(1, this->advertisements[i]);
|
||||
}
|
||||
}
|
||||
bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
@@ -1886,52 +1894,72 @@ bool BluetoothGATTGetServicesRequest::decode_varint(uint32_t field_id, ProtoVarI
|
||||
return true;
|
||||
}
|
||||
void BluetoothGATTDescriptor::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint64(1, this->uuid[0], true);
|
||||
buffer.encode_uint64(1, this->uuid[1], true);
|
||||
if (this->uuid[0] != 0 || this->uuid[1] != 0) {
|
||||
buffer.encode_uint64(1, this->uuid[0], true);
|
||||
buffer.encode_uint64(1, this->uuid[1], true);
|
||||
}
|
||||
buffer.encode_uint32(2, this->handle);
|
||||
buffer.encode_uint32(3, this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTDescriptor::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint64_force(1, this->uuid[0]);
|
||||
size.add_uint64_force(1, this->uuid[1]);
|
||||
if (this->uuid[0] != 0 || this->uuid[1] != 0) {
|
||||
size.add_uint64_force(1, this->uuid[0]);
|
||||
size.add_uint64_force(1, this->uuid[1]);
|
||||
}
|
||||
size.add_uint32(1, this->handle);
|
||||
size.add_uint32(1, this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint64(1, this->uuid[0], true);
|
||||
buffer.encode_uint64(1, this->uuid[1], true);
|
||||
if (this->uuid[0] != 0 || this->uuid[1] != 0) {
|
||||
buffer.encode_uint64(1, this->uuid[0], true);
|
||||
buffer.encode_uint64(1, this->uuid[1], true);
|
||||
}
|
||||
buffer.encode_uint32(2, this->handle);
|
||||
buffer.encode_uint32(3, this->properties);
|
||||
for (auto &it : this->descriptors) {
|
||||
buffer.encode_message(4, it, true);
|
||||
}
|
||||
buffer.encode_uint32(5, this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTCharacteristic::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint64_force(1, this->uuid[0]);
|
||||
size.add_uint64_force(1, this->uuid[1]);
|
||||
if (this->uuid[0] != 0 || this->uuid[1] != 0) {
|
||||
size.add_uint64_force(1, this->uuid[0]);
|
||||
size.add_uint64_force(1, this->uuid[1]);
|
||||
}
|
||||
size.add_uint32(1, this->handle);
|
||||
size.add_uint32(1, this->properties);
|
||||
size.add_repeated_message(1, this->descriptors);
|
||||
size.add_uint32(1, this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint64(1, this->uuid[0], true);
|
||||
buffer.encode_uint64(1, this->uuid[1], true);
|
||||
if (this->uuid[0] != 0 || this->uuid[1] != 0) {
|
||||
buffer.encode_uint64(1, this->uuid[0], true);
|
||||
buffer.encode_uint64(1, this->uuid[1], true);
|
||||
}
|
||||
buffer.encode_uint32(2, this->handle);
|
||||
for (auto &it : this->characteristics) {
|
||||
buffer.encode_message(3, it, true);
|
||||
}
|
||||
buffer.encode_uint32(4, this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTService::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint64_force(1, this->uuid[0]);
|
||||
size.add_uint64_force(1, this->uuid[1]);
|
||||
if (this->uuid[0] != 0 || this->uuid[1] != 0) {
|
||||
size.add_uint64_force(1, this->uuid[0]);
|
||||
size.add_uint64_force(1, this->uuid[1]);
|
||||
}
|
||||
size.add_uint32(1, this->handle);
|
||||
size.add_repeated_message(1, this->characteristics);
|
||||
size.add_uint32(1, this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint64(1, this->address);
|
||||
buffer.encode_message(2, this->services[0], true);
|
||||
for (auto &it : this->services) {
|
||||
buffer.encode_message(2, it, true);
|
||||
}
|
||||
}
|
||||
void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint64(1, this->address);
|
||||
size.add_message_object_force(1, this->services[0]);
|
||||
size.add_repeated_message(1, this->services);
|
||||
}
|
||||
void BluetoothGATTGetServicesDoneResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint64(1, this->address);
|
||||
@@ -2051,15 +2079,17 @@ void BluetoothGATTNotifyDataResponse::calculate_size(ProtoSize &size) const {
|
||||
void BluetoothConnectionsFreeResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, this->free);
|
||||
buffer.encode_uint32(2, this->limit);
|
||||
for (auto &it : this->allocated) {
|
||||
buffer.encode_uint64(3, it, true);
|
||||
for (const auto &it : this->allocated) {
|
||||
if (it != 0) {
|
||||
buffer.encode_uint64(3, it, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
void BluetoothConnectionsFreeResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, this->free);
|
||||
size.add_uint32(1, this->limit);
|
||||
if (!this->allocated.empty()) {
|
||||
for (const auto &it : this->allocated) {
|
||||
for (const auto &it : this->allocated) {
|
||||
if (it != 0) {
|
||||
size.add_uint64_force(1, it);
|
||||
}
|
||||
}
|
||||
@@ -2123,10 +2153,12 @@ void BluetoothDeviceClearCacheResponse::calculate_size(ProtoSize &size) const {
|
||||
void BluetoothScannerStateResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(this->state));
|
||||
buffer.encode_uint32(2, static_cast<uint32_t>(this->mode));
|
||||
buffer.encode_uint32(3, static_cast<uint32_t>(this->configured_mode));
|
||||
}
|
||||
void BluetoothScannerStateResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->state));
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->mode));
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->configured_mode));
|
||||
}
|
||||
bool BluetoothScannerSetModeRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -383,6 +383,12 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::Medi
|
||||
return "MEDIA_PLAYER_STATE_PLAYING";
|
||||
case enums::MEDIA_PLAYER_STATE_PAUSED:
|
||||
return "MEDIA_PLAYER_STATE_PAUSED";
|
||||
case enums::MEDIA_PLAYER_STATE_ANNOUNCING:
|
||||
return "MEDIA_PLAYER_STATE_ANNOUNCING";
|
||||
case enums::MEDIA_PLAYER_STATE_OFF:
|
||||
return "MEDIA_PLAYER_STATE_OFF";
|
||||
case enums::MEDIA_PLAYER_STATE_ON:
|
||||
return "MEDIA_PLAYER_STATE_ON";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -399,6 +405,24 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::Me
|
||||
return "MEDIA_PLAYER_COMMAND_MUTE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
return "MEDIA_PLAYER_COMMAND_UNMUTE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
return "MEDIA_PLAYER_COMMAND_TOGGLE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP:
|
||||
return "MEDIA_PLAYER_COMMAND_VOLUME_UP";
|
||||
case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
|
||||
return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN";
|
||||
case enums::MEDIA_PLAYER_COMMAND_ENQUEUE:
|
||||
return "MEDIA_PLAYER_COMMAND_ENQUEUE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE:
|
||||
return "MEDIA_PLAYER_COMMAND_REPEAT_ONE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF:
|
||||
return "MEDIA_PLAYER_COMMAND_REPEAT_OFF";
|
||||
case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST:
|
||||
return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST";
|
||||
case enums::MEDIA_PLAYER_COMMAND_TURN_ON:
|
||||
return "MEDIA_PLAYER_COMMAND_TURN_ON";
|
||||
case enums::MEDIA_PLAYER_COMMAND_TURN_OFF:
|
||||
return "MEDIA_PLAYER_COMMAND_TURN_OFF";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -1111,7 +1135,7 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
|
||||
dump_field(out, "string_", this->string_);
|
||||
dump_field(out, "int_", this->int_);
|
||||
for (const auto it : this->bool_array) {
|
||||
dump_field(out, "bool_array", it, 4);
|
||||
dump_field(out, "bool_array", static_cast<bool>(it), 4);
|
||||
}
|
||||
for (const auto &it : this->int_array) {
|
||||
dump_field(out, "int_array", it, 4);
|
||||
@@ -1466,6 +1490,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
dump_field(out, "feature_flags", this->feature_flags);
|
||||
}
|
||||
void MediaPlayerStateResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "MediaPlayerStateResponse");
|
||||
@@ -1509,9 +1534,9 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
|
||||
}
|
||||
void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse");
|
||||
for (const auto &it : this->advertisements) {
|
||||
for (uint16_t i = 0; i < this->advertisements_len; i++) {
|
||||
out.append(" advertisements: ");
|
||||
it.dump_to(out);
|
||||
this->advertisements[i].dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
}
|
||||
@@ -1536,6 +1561,7 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const {
|
||||
dump_field(out, "uuid", it, 4);
|
||||
}
|
||||
dump_field(out, "handle", this->handle);
|
||||
dump_field(out, "short_uuid", this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothGATTCharacteristic");
|
||||
@@ -1549,6 +1575,7 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "short_uuid", this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTService::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothGATTService");
|
||||
@@ -1561,6 +1588,7 @@ void BluetoothGATTService::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "short_uuid", this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse");
|
||||
@@ -1676,6 +1704,7 @@ void BluetoothScannerStateResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothScannerStateResponse");
|
||||
dump_field(out, "state", static_cast<enums::BluetoothScannerState>(this->state));
|
||||
dump_field(out, "mode", static_cast<enums::BluetoothScannerMode>(this->mode));
|
||||
dump_field(out, "configured_mode", static_cast<enums::BluetoothScannerMode>(this->configured_mode));
|
||||
}
|
||||
void BluetoothScannerSetModeRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothScannerSetModeRequest");
|
||||
|
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
// This file provides includes needed by the generated protobuf code
|
||||
// when using pointer optimizations for component-specific types
|
||||
|
||||
@@ -20,11 +22,13 @@
|
||||
#include "esphome/components/select/select_traits.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
#include "esphome/components/media_player/media_player_traits.h"
|
||||
#endif
|
||||
|
||||
// Standard library includes that might be needed
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// This file only provides includes, no actual code
|
||||
|
||||
} // namespace esphome::api
|
||||
|
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
"""Run the logs command in the event loop."""
|
||||
conf = config["api"]
|
||||
name = config["esphome"]["name"]
|
||||
@@ -39,13 +39,21 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
noise_psk: str | None = None
|
||||
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
|
||||
noise_psk = key
|
||||
_LOGGER.info("Starting log output from %s using esphome API", address)
|
||||
|
||||
if len(addresses) == 1:
|
||||
_LOGGER.info("Starting log output from %s using esphome API", addresses[0])
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Starting log output from %s using esphome API", " or ".join(addresses)
|
||||
)
|
||||
|
||||
cli = APIClient(
|
||||
address,
|
||||
addresses[0], # Primary address for compatibility
|
||||
port,
|
||||
password,
|
||||
client_info=f"ESPHome Logs {__version__}",
|
||||
noise_psk=noise_psk,
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
)
|
||||
dashboard = CORE.dashboard
|
||||
|
||||
@@ -66,7 +74,7 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
await stop()
|
||||
|
||||
|
||||
def run_logs(config: dict[str, Any], address: str) -> None:
|
||||
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
"""Run the logs command."""
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(async_run_logs(config, address))
|
||||
asyncio.run(async_run_logs(config, addresses))
|
||||
|
@@ -56,6 +56,14 @@ class CustomAPIDevice {
|
||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#else
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Register a custom native API service that will show up in Home Assistant.
|
||||
@@ -81,6 +89,12 @@ class CustomAPIDevice {
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#else
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
@@ -135,6 +149,22 @@ class CustomAPIDevice {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -222,6 +252,28 @@ class CustomAPIDevice {
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
#else
|
||||
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
||||
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void>
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
|
||||
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void>
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
|
@@ -8,74 +8,70 @@ namespace esphome::api {
|
||||
static const char *const TAG = "api.proto";
|
||||
|
||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
uint32_t i = 0;
|
||||
bool error = false;
|
||||
while (i < length) {
|
||||
const uint8_t *ptr = buffer;
|
||||
const uint8_t *end = buffer + length;
|
||||
|
||||
while (ptr < end) {
|
||||
uint32_t consumed;
|
||||
auto res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
|
||||
|
||||
// Parse field header
|
||||
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid field start at %" PRIu32, i);
|
||||
break;
|
||||
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t field_type = (res->as_uint32()) & 0b111;
|
||||
uint32_t field_id = (res->as_uint32()) >> 3;
|
||||
i += consumed;
|
||||
uint32_t tag = res->as_uint32();
|
||||
uint32_t field_type = tag & 0b111;
|
||||
uint32_t field_id = tag >> 3;
|
||||
ptr += consumed;
|
||||
|
||||
switch (field_type) {
|
||||
case 0: { // VarInt
|
||||
res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
|
||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid VarInt at %" PRIu32, i);
|
||||
error = true;
|
||||
break;
|
||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_varint(field_id, *res)) {
|
||||
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
|
||||
}
|
||||
i += consumed;
|
||||
ptr += consumed;
|
||||
break;
|
||||
}
|
||||
case 2: { // Length-delimited
|
||||
res = ProtoVarInt::parse(&buffer[i], length - i, &consumed);
|
||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at %" PRIu32, i);
|
||||
error = true;
|
||||
break;
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t field_length = res->as_uint32();
|
||||
i += consumed;
|
||||
if (field_length > length - i) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at %" PRIu32, i);
|
||||
error = true;
|
||||
break;
|
||||
ptr += consumed;
|
||||
if (ptr + field_length > end) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_length(field_id, ProtoLengthDelimited(&buffer[i], field_length))) {
|
||||
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
|
||||
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
|
||||
}
|
||||
i += field_length;
|
||||
ptr += field_length;
|
||||
break;
|
||||
}
|
||||
case 5: { // 32-bit
|
||||
if (length - i < 4) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at %" PRIu32, i);
|
||||
error = true;
|
||||
break;
|
||||
if (ptr + 4 > end) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t val = encode_uint32(buffer[i + 3], buffer[i + 2], buffer[i + 1], buffer[i]);
|
||||
uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
|
||||
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
|
||||
}
|
||||
i += 4;
|
||||
ptr += 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type at %" PRIu32, i);
|
||||
error = true;
|
||||
break;
|
||||
}
|
||||
if (error) {
|
||||
break;
|
||||
ESP_LOGV(TAG, "Invalid field type %u at offset %ld", field_type, (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,23 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Helper functions for ZigZag encoding/decoding
|
||||
inline constexpr uint32_t encode_zigzag32(int32_t value) {
|
||||
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
}
|
||||
|
||||
inline constexpr uint64_t encode_zigzag64(int64_t value) {
|
||||
return (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
|
||||
}
|
||||
|
||||
inline constexpr int32_t decode_zigzag32(uint32_t value) {
|
||||
return (value & 1) ? static_cast<int32_t>(~(value >> 1)) : static_cast<int32_t>(value >> 1);
|
||||
}
|
||||
|
||||
inline constexpr int64_t decode_zigzag64(uint64_t value) {
|
||||
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
@@ -87,33 +104,25 @@ class ProtoVarInt {
|
||||
return {}; // Incomplete or invalid varint
|
||||
}
|
||||
|
||||
uint16_t as_uint16() const { return this->value_; }
|
||||
uint32_t as_uint32() const { return this->value_; }
|
||||
uint64_t as_uint64() const { return this->value_; }
|
||||
bool as_bool() const { return this->value_; }
|
||||
int32_t as_int32() const {
|
||||
constexpr uint16_t as_uint16() const { return this->value_; }
|
||||
constexpr uint32_t as_uint32() const { return this->value_; }
|
||||
constexpr uint64_t as_uint64() const { return this->value_; }
|
||||
constexpr bool as_bool() const { return this->value_; }
|
||||
constexpr int32_t as_int32() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int32_t>(this->as_int64());
|
||||
}
|
||||
int64_t as_int64() const {
|
||||
constexpr int64_t as_int64() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int64_t>(this->value_);
|
||||
}
|
||||
int32_t as_sint32() const {
|
||||
constexpr int32_t as_sint32() const {
|
||||
// with ZigZag encoding
|
||||
if (this->value_ & 1) {
|
||||
return static_cast<int32_t>(~(this->value_ >> 1));
|
||||
} else {
|
||||
return static_cast<int32_t>(this->value_ >> 1);
|
||||
}
|
||||
return decode_zigzag32(static_cast<uint32_t>(this->value_));
|
||||
}
|
||||
int64_t as_sint64() const {
|
||||
constexpr int64_t as_sint64() const {
|
||||
// with ZigZag encoding
|
||||
if (this->value_ & 1) {
|
||||
return static_cast<int64_t>(~(this->value_ >> 1));
|
||||
} else {
|
||||
return static_cast<int64_t>(this->value_ >> 1);
|
||||
}
|
||||
return decode_zigzag64(this->value_);
|
||||
}
|
||||
/**
|
||||
* Encode the varint value to a pre-allocated buffer without bounds checking.
|
||||
@@ -309,22 +318,10 @@ class ProtoWriteBuffer {
|
||||
this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
|
||||
}
|
||||
void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
|
||||
uint32_t uvalue;
|
||||
if (value < 0) {
|
||||
uvalue = ~(value << 1);
|
||||
} else {
|
||||
uvalue = value << 1;
|
||||
}
|
||||
this->encode_uint32(field_id, uvalue, force);
|
||||
this->encode_uint32(field_id, encode_zigzag32(value), force);
|
||||
}
|
||||
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
uint64_t uvalue;
|
||||
if (value < 0) {
|
||||
uvalue = ~(value << 1);
|
||||
} else {
|
||||
uvalue = value << 1;
|
||||
}
|
||||
this->encode_uint64(field_id, uvalue, force);
|
||||
this->encode_uint64(field_id, encode_zigzag64(value), force);
|
||||
}
|
||||
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
|
||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
||||
@@ -395,7 +392,7 @@ class ProtoSize {
|
||||
* @param value The uint32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint32_t value) {
|
||||
static constexpr uint32_t varint(uint32_t value) {
|
||||
// Optimized varint size calculation using leading zeros
|
||||
// Each 7 bits requires one byte in the varint encoding
|
||||
if (value < 128)
|
||||
@@ -419,7 +416,7 @@ class ProtoSize {
|
||||
* @param value The uint64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(uint64_t value) {
|
||||
static constexpr uint32_t varint(uint64_t value) {
|
||||
// Handle common case of values fitting in uint32_t (vast majority of use cases)
|
||||
if (value <= UINT32_MAX) {
|
||||
return varint(static_cast<uint32_t>(value));
|
||||
@@ -450,7 +447,7 @@ class ProtoSize {
|
||||
* @param value The int32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int32_t value) {
|
||||
static constexpr uint32_t varint(int32_t value) {
|
||||
// Negative values are sign-extended to 64 bits in protocol buffers,
|
||||
// which always results in a 10-byte varint for negative int32
|
||||
if (value < 0) {
|
||||
@@ -466,7 +463,7 @@ class ProtoSize {
|
||||
* @param value The int64_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static inline uint32_t varint(int64_t value) {
|
||||
static constexpr uint32_t varint(int64_t value) {
|
||||
// For int64_t, we convert to uint64_t and calculate the size
|
||||
// This works because the bit pattern determines the encoding size,
|
||||
// and we've handled negative int32 values as a special case above
|
||||
@@ -480,7 +477,7 @@ class ProtoSize {
|
||||
* @param type The wire type value (from the WireType enum in the protobuf spec)
|
||||
* @return The number of bytes needed to encode the field ID and wire type
|
||||
*/
|
||||
static inline uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
uint32_t tag = (field_id << 3) | (type & 0b111);
|
||||
return varint(tag);
|
||||
}
|
||||
@@ -607,9 +604,8 @@ class ProtoSize {
|
||||
*/
|
||||
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size when force is true
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size_ += field_id_size + varint(zigzag);
|
||||
// ZigZag encoding for sint32
|
||||
total_size_ += field_id_size + varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
CONF_DIRECTION,
|
||||
CONF_HYSTERESIS,
|
||||
CONF_ID,
|
||||
CONF_POWER_MODE,
|
||||
CONF_RANGE,
|
||||
)
|
||||
|
||||
@@ -57,7 +58,6 @@ FAST_FILTER = {
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_POWER_MODE = "power_mode"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_START_POSITION = "start_position"
|
||||
|
@@ -24,7 +24,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_POWER_MODE = "power_mode"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_PWM_FREQUENCY = "pwm_frequency"
|
||||
|
@@ -8,9 +8,9 @@ from esphome.const import (
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(200.0)
|
||||
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
|
@@ -110,6 +110,8 @@ void ATM90E32Component::update() {
|
||||
|
||||
void ATM90E32Component::setup() {
|
||||
this->spi_setup();
|
||||
this->cs_summary_ = this->cs_->dump_summary();
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
|
||||
uint16_t mmode0 = 0x87; // 3P4W 50Hz
|
||||
uint16_t high_thresh = 0;
|
||||
@@ -130,9 +132,9 @@ void ATM90E32Component::setup() {
|
||||
mmode0 |= 0 << 1; // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S)
|
||||
}
|
||||
|
||||
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
|
||||
delay(6); // Wait for the minimum 5ms + 1ms
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
|
||||
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A, false); // Perform soft reset
|
||||
delay(6); // Wait for the minimum 5ms + 1ms
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
|
||||
if (!this->validate_spi_read_(0x55AA, "setup()")) {
|
||||
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
|
||||
this->mark_failed();
|
||||
@@ -156,16 +158,17 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_offset_calibration_) {
|
||||
// Initialize flash storage for offset calibrations
|
||||
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary());
|
||||
uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
|
||||
this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
|
||||
this->restore_offset_calibrations_();
|
||||
|
||||
// Initialize flash storage for power offset calibrations
|
||||
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary());
|
||||
uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
|
||||
this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
|
||||
this->restore_power_offset_calibrations_();
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values.");
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
|
||||
cs);
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
this->write16_(this->voltage_offset_registers[phase],
|
||||
static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
|
||||
@@ -180,21 +183,18 @@ void ATM90E32Component::setup() {
|
||||
|
||||
if (this->enable_gain_calibration_) {
|
||||
// Initialize flash storage for gain calibration
|
||||
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary());
|
||||
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
|
||||
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
|
||||
this->restore_gain_calibrations_();
|
||||
|
||||
if (this->using_saved_calibrations_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
|
||||
} else {
|
||||
if (!this->using_saved_calibrations_) {
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
|
||||
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values.");
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
|
||||
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
|
||||
@@ -213,6 +213,122 @@ void ATM90E32Component::setup() {
|
||||
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
|
||||
}
|
||||
|
||||
void ATM90E32Component::log_calibration_status_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
|
||||
bool offset_mismatch = false;
|
||||
bool power_mismatch = false;
|
||||
bool gain_mismatch = false;
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
offset_mismatch |= this->offset_calibration_mismatch_[phase];
|
||||
power_mismatch |= this->power_offset_calibration_mismatch_[phase];
|
||||
gain_mismatch |= this->gain_calibration_mismatch_[phase];
|
||||
}
|
||||
|
||||
if (offset_mismatch) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] ===================== Offset mismatch: using flash values =====================", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
|
||||
cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
|
||||
cs);
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase,
|
||||
this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_,
|
||||
this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_);
|
||||
}
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] ===============================================================================", cs);
|
||||
}
|
||||
if (power_mismatch) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] ================= Power offset mismatch: using flash values =================", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
|
||||
cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_active_power|offset_reactive_power|", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
|
||||
cs);
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6d | %6d | %6d | %6d |", cs, 'A' + phase,
|
||||
this->config_power_offset_phase_[phase].active_power_offset,
|
||||
this->power_offset_phase_[phase].active_power_offset,
|
||||
this->config_power_offset_phase_[phase].reactive_power_offset,
|
||||
this->power_offset_phase_[phase].reactive_power_offset);
|
||||
}
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] ===============================================================================", cs);
|
||||
}
|
||||
if (gain_mismatch) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] ====================== Gain mismatch: using flash values =====================", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
|
||||
cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | | config | flash | config | flash |", cs);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
|
||||
cs);
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] | %c | %6u | %6u | %6u | %6u |", cs, 'A' + phase,
|
||||
this->config_gain_phase_[phase].voltage_gain, this->gain_phase_[phase].voltage_gain,
|
||||
this->config_gain_phase_[phase].current_gain, this->gain_phase_[phase].current_gain);
|
||||
}
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] ===============================================================================", cs);
|
||||
}
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
|
||||
cs);
|
||||
} else if (this->restored_offset_calibration_ && !offset_mismatch) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ============== Restored offset calibration from memory ==============", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
|
||||
this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs);
|
||||
}
|
||||
|
||||
if (this->restored_power_offset_calibration_ && !power_mismatch) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restored power offset calibration from memory ============", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
|
||||
this->power_offset_phase_[phase].active_power_offset,
|
||||
this->power_offset_phase_[phase].reactive_power_offset);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
|
||||
}
|
||||
if (!this->enable_gain_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
|
||||
} else if (this->restored_gain_calibration_ && !gain_mismatch) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restoring saved gain calibrations to registers ============", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase,
|
||||
this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\\n", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration loaded and verified successfully.\n", cs);
|
||||
}
|
||||
this->calibration_message_printed_ = true;
|
||||
}
|
||||
|
||||
void ATM90E32Component::dump_config() {
|
||||
ESP_LOGCONFIG("", "ATM90E32:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
@@ -255,6 +371,10 @@ void ATM90E32Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
|
||||
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
|
||||
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
|
||||
if (this->restored_offset_calibration_ || this->restored_power_offset_calibration_ ||
|
||||
this->restored_gain_calibration_ || !this->enable_offset_calibration_ || !this->enable_gain_calibration_) {
|
||||
this->log_calibration_status_();
|
||||
}
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; }
|
||||
@@ -263,19 +383,17 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
|
||||
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
|
||||
// Default is 143FH (20ms, 63ms)
|
||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
|
||||
this->enable();
|
||||
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1us is plenty
|
||||
uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
|
||||
uint8_t addrl = (a_register & 0xFF);
|
||||
uint8_t data[2];
|
||||
uint16_t output;
|
||||
this->enable();
|
||||
delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty
|
||||
this->write_byte(addrh);
|
||||
this->write_byte(addrl);
|
||||
this->read_array(data, 2);
|
||||
this->disable();
|
||||
|
||||
output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
|
||||
uint8_t data[4] = {addrh, addrl, 0x00, 0x00};
|
||||
this->transfer_array(data, 4);
|
||||
uint16_t output = encode_uint16(data[2], data[3]);
|
||||
ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output);
|
||||
delay_microseconds_safe(1); // allow the last clock to propagate before releasing CS
|
||||
this->disable();
|
||||
delay_microseconds_safe(1); // meet minimum CS high time before next transaction
|
||||
return output;
|
||||
}
|
||||
|
||||
@@ -292,13 +410,19 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
|
||||
return val;
|
||||
}
|
||||
|
||||
void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
|
||||
void ATM90E32Component::write16_(uint16_t a_register, uint16_t val, bool validate) {
|
||||
ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val);
|
||||
uint8_t addrh = ((a_register >> 8) & 0x03);
|
||||
uint8_t addrl = (a_register & 0xFF);
|
||||
uint8_t data[4] = {addrh, addrl, uint8_t((val >> 8) & 0xFF), uint8_t(val & 0xFF)};
|
||||
this->enable();
|
||||
this->write_byte16(a_register);
|
||||
this->write_byte16(val);
|
||||
delay_microseconds_safe(1); // ensure CS setup time
|
||||
this->write_array(data, 4);
|
||||
delay_microseconds_safe(1); // allow clock to settle before raising CS
|
||||
this->disable();
|
||||
this->validate_spi_read_(val, "write16()");
|
||||
delay_microseconds_safe(1); // ensure minimum CS high time
|
||||
if (validate)
|
||||
this->validate_spi_read_(val, "write16()");
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
|
||||
@@ -441,8 +565,10 @@ float ATM90E32Component::get_chip_temperature_() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_gain_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->enable_gain_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true");
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
|
||||
cs);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -454,12 +580,14 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
|
||||
this->get_reference_current(2)};
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration =========================");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
|
||||
ESP_LOGI(TAG,
|
||||
"[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |");
|
||||
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ========================= Gain Calibration =========================", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(
|
||||
TAG,
|
||||
"[CALIBRATION][%s] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |",
|
||||
cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
float measured_voltage = this->get_phase_voltage_avg_(phase);
|
||||
@@ -476,22 +604,22 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
|
||||
// Voltage calibration
|
||||
if (ref_voltage <= 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.",
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: reference voltage is 0.", cs,
|
||||
phase_labels[phase]);
|
||||
} else if (measured_voltage == 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.",
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
|
||||
if (new_voltage_gain == 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.",
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
if (new_voltage_gain >= 65535) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
"[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.",
|
||||
phase_labels[phase]);
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage "
|
||||
"transformer.",
|
||||
cs, phase_labels[phase]);
|
||||
new_voltage_gain = 65535;
|
||||
}
|
||||
this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
|
||||
@@ -501,20 +629,20 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
|
||||
// Current calibration
|
||||
if (ref_current == 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.",
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: reference current is 0.", cs,
|
||||
phase_labels[phase]);
|
||||
} else if (measured_current == 0.0f) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.",
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
|
||||
if (new_current_gain == 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.",
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
if (new_current_gain >= 65535) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
|
||||
phase_labels[phase]);
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
|
||||
cs, phase_labels[phase]);
|
||||
new_current_gain = 65535;
|
||||
}
|
||||
this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
|
||||
@@ -523,13 +651,13 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
}
|
||||
|
||||
// Final row output
|
||||
ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |",
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |", cs,
|
||||
'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
|
||||
did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
|
||||
did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n");
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
|
||||
|
||||
this->save_gain_calibration_to_memory_();
|
||||
this->write_gains_to_registers_();
|
||||
@@ -537,54 +665,108 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory.");
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs);
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!");
|
||||
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs);
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_offset_calibration_to_memory_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = this->offset_pref_.save(&this->offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
this->restored_offset_calibration_ = true;
|
||||
for (bool &phase : this->offset_calibration_mismatch_)
|
||||
phase = false;
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs);
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs);
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||
global_preferences->sync();
|
||||
if (success) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
this->restored_power_offset_calibration_ = true;
|
||||
for (bool &phase : this->power_offset_calibration_mismatch_)
|
||||
phase = false;
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs);
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs);
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_offset_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true");
|
||||
ESP_LOGW(TAG,
|
||||
"[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
|
||||
cs);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ======================== Offset Calibration ========================", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
int16_t voltage_offset = calibrate_offset(phase, true);
|
||||
int16_t current_offset = calibrate_offset(phase, false);
|
||||
|
||||
this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset,
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset,
|
||||
current_offset);
|
||||
}
|
||||
|
||||
this->offset_pref_.save(&this->offset_phase_); // Save to flash
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs);
|
||||
|
||||
this->save_offset_calibration_to_memory_();
|
||||
}
|
||||
|
||||
void ATM90E32Component::run_power_offset_calibrations() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->enable_offset_calibration_) {
|
||||
ESP_LOGW(
|
||||
TAG,
|
||||
"[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true");
|
||||
"[CALIBRATION][%s] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true",
|
||||
cs);
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ===================== Power Offset Calibration =====================", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
int16_t active_offset = calibrate_power_offset(phase, false);
|
||||
int16_t reactive_offset = calibrate_power_offset(phase, true);
|
||||
|
||||
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
|
||||
active_offset, reactive_offset);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset,
|
||||
reactive_offset);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
|
||||
|
||||
this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash
|
||||
this->save_power_offset_calibration_to_memory_();
|
||||
}
|
||||
|
||||
void ATM90E32Component::write_gains_to_registers_() {
|
||||
@@ -631,102 +813,276 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_gain_calibrations_() {
|
||||
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:");
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t v_gain = this->gain_phase_[phase].voltage_gain;
|
||||
uint16_t i_gain = this->gain_phase_[phase].current_gain;
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
|
||||
}
|
||||
|
||||
this->write_gains_to_registers_();
|
||||
|
||||
if (this->verify_gain_writes_()) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
|
||||
}
|
||||
} else {
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
for (uint8_t i = 0; i < 3; ++i) {
|
||||
this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
|
||||
this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
|
||||
this->gain_phase_[i] = this->config_gain_phase_[i];
|
||||
}
|
||||
|
||||
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
|
||||
bool all_zero = true;
|
||||
bool same_as_config = true;
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
const auto &cfg = this->config_gain_phase_[phase];
|
||||
const auto &saved = this->gain_phase_[phase];
|
||||
if (saved.voltage_gain != 0 || saved.current_gain != 0)
|
||||
all_zero = false;
|
||||
if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain)
|
||||
same_as_config = false;
|
||||
}
|
||||
|
||||
if (!all_zero && !same_as_config) {
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
bool mismatch = false;
|
||||
if (this->has_config_voltage_gain_[phase] &&
|
||||
this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain)
|
||||
mismatch = true;
|
||||
if (this->has_config_current_gain_[phase] &&
|
||||
this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain)
|
||||
mismatch = true;
|
||||
if (mismatch)
|
||||
this->gain_calibration_mismatch_[phase] = true;
|
||||
}
|
||||
|
||||
this->write_gains_to_registers_();
|
||||
|
||||
if (this->verify_gain_writes_()) {
|
||||
this->using_saved_calibrations_ = true;
|
||||
this->restored_gain_calibration_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs);
|
||||
}
|
||||
}
|
||||
|
||||
this->using_saved_calibrations_ = false;
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->gain_phase_[i] = this->config_gain_phase_[i];
|
||||
this->write_gains_to_registers_();
|
||||
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs);
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_offset_calibrations_() {
|
||||
if (this->offset_pref_.load(&this->offset_phase_)) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory.");
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->config_offset_phase_[i] = this->offset_phase_[i];
|
||||
|
||||
bool have_data = this->offset_pref_.load(&this->offset_phase_);
|
||||
bool all_zero = true;
|
||||
if (have_data) {
|
||||
for (auto &phase : this->offset_phase_) {
|
||||
if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) {
|
||||
all_zero = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (have_data && !all_zero) {
|
||||
this->restored_offset_calibration_ = true;
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
auto &offset = this->offset_phase_[phase];
|
||||
write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_);
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase,
|
||||
offset.voltage_offset_, offset.current_offset_);
|
||||
bool mismatch = false;
|
||||
if (this->has_config_voltage_offset_[phase] &&
|
||||
offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_)
|
||||
mismatch = true;
|
||||
if (this->has_config_current_offset_[phase] &&
|
||||
offset.current_offset_ != this->config_offset_phase_[phase].current_offset_)
|
||||
mismatch = true;
|
||||
if (mismatch)
|
||||
this->offset_calibration_mismatch_[phase] = true;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values.");
|
||||
for (uint8_t phase = 0; phase < 3; phase++)
|
||||
this->offset_phase_[phase] = this->config_offset_phase_[phase];
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs);
|
||||
}
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_,
|
||||
this->offset_phase_[phase].current_offset_);
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::restore_power_offset_calibrations_() {
|
||||
if (this->power_offset_pref_.load(&this->power_offset_phase_)) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory.");
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
for (uint8_t i = 0; i < 3; ++i)
|
||||
this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
|
||||
|
||||
bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_);
|
||||
bool all_zero = true;
|
||||
if (have_data) {
|
||||
for (auto &phase : this->power_offset_phase_) {
|
||||
if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) {
|
||||
all_zero = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (have_data && !all_zero) {
|
||||
this->restored_power_offset_calibration_ = true;
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
auto &offset = this->power_offset_phase_[phase];
|
||||
write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset);
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
|
||||
offset.active_power_offset, offset.reactive_power_offset);
|
||||
bool mismatch = false;
|
||||
if (this->has_config_active_power_offset_[phase] &&
|
||||
offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset)
|
||||
mismatch = true;
|
||||
if (this->has_config_reactive_power_offset_[phase] &&
|
||||
offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset)
|
||||
mismatch = true;
|
||||
if (mismatch)
|
||||
this->power_offset_calibration_mismatch_[phase] = true;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values.");
|
||||
for (uint8_t phase = 0; phase < 3; ++phase)
|
||||
this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase];
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs);
|
||||
}
|
||||
|
||||
for (uint8_t phase = 0; phase < 3; ++phase) {
|
||||
write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset,
|
||||
this->power_offset_phase_[phase].reactive_power_offset);
|
||||
}
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_gain_calibrations() {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values");
|
||||
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
|
||||
gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->using_saved_calibrations_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase,
|
||||
this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
|
||||
return;
|
||||
}
|
||||
|
||||
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
|
||||
this->using_saved_calibrations_ = false;
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored gain calibrations and restoring config-defined values", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
|
||||
|
||||
if (success) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:");
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
|
||||
gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain);
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!");
|
||||
for (int phase = 0; phase < 3; phase++) {
|
||||
uint16_t voltage_gain = this->phase_[phase].voltage_gain_;
|
||||
uint16_t current_gain = this->phase_[phase].ct_gain_;
|
||||
|
||||
this->config_gain_phase_[phase].voltage_gain = voltage_gain;
|
||||
this->config_gain_phase_[phase].current_gain = current_gain;
|
||||
this->gain_phase_[phase].voltage_gain = voltage_gain;
|
||||
this->gain_phase_[phase].current_gain = current_gain;
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6u | %6u |", cs, 'A' + phase, voltage_gain, current_gain);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
|
||||
|
||||
GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}};
|
||||
bool success = this->gain_calibration_pref_.save(&zero_gains);
|
||||
global_preferences->sync();
|
||||
|
||||
this->using_saved_calibrations_ = false;
|
||||
this->restored_gain_calibration_ = false;
|
||||
for (bool &phase : this->gain_calibration_mismatch_)
|
||||
phase = false;
|
||||
|
||||
if (!success) {
|
||||
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs);
|
||||
}
|
||||
|
||||
this->write_gains_to_registers_(); // Apply them to the chip immediately
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_offset_calibrations() {
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
this->write_offsets_to_registers_(phase, 0, 0);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->restored_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
|
||||
this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
|
||||
return;
|
||||
}
|
||||
|
||||
this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored offset calibrations and restoring config-defined values", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared.");
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
int16_t voltage_offset =
|
||||
this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0;
|
||||
int16_t current_offset =
|
||||
this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0;
|
||||
this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, voltage_offset,
|
||||
current_offset);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
|
||||
|
||||
OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
|
||||
this->offset_pref_.save(&zero_offsets); // Clear stored values in flash
|
||||
global_preferences->sync();
|
||||
|
||||
this->restored_offset_calibration_ = false;
|
||||
for (bool &phase : this->offset_calibration_mismatch_)
|
||||
phase = false;
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs);
|
||||
}
|
||||
|
||||
void ATM90E32Component::clear_power_offset_calibrations() {
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
this->write_power_offsets_to_registers_(phase, 0, 0);
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
if (!this->restored_power_offset_calibration_) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase,
|
||||
this->power_offset_phase_[phase].active_power_offset,
|
||||
this->power_offset_phase_[phase].reactive_power_offset);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
|
||||
return;
|
||||
}
|
||||
|
||||
this->power_offset_pref_.save(&this->power_offset_phase_);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored power offsets and restoring config-defined values", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared.");
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
int16_t active_offset =
|
||||
this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0;
|
||||
int16_t reactive_offset = this->has_config_reactive_power_offset_[phase]
|
||||
? this->config_power_offset_phase_[phase].reactive_power_offset
|
||||
: 0;
|
||||
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] | %c | %6d | %6d |", cs, 'A' + phase, active_offset,
|
||||
reactive_offset);
|
||||
}
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
|
||||
|
||||
PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
|
||||
this->power_offset_pref_.save(&zero_power_offsets);
|
||||
global_preferences->sync();
|
||||
|
||||
this->restored_power_offset_calibration_ = false;
|
||||
for (bool &phase : this->power_offset_calibration_mismatch_)
|
||||
phase = false;
|
||||
|
||||
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs);
|
||||
}
|
||||
|
||||
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
|
||||
@@ -747,20 +1103,21 @@ int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
|
||||
|
||||
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
|
||||
const uint8_t num_reads = 5;
|
||||
uint64_t total_value = 0;
|
||||
int64_t total_value = 0;
|
||||
|
||||
for (uint8_t i = 0; i < num_reads; ++i) {
|
||||
uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
|
||||
: this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
|
||||
int32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
|
||||
: this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
|
||||
total_value += reading;
|
||||
}
|
||||
|
||||
const uint32_t average_value = total_value / num_reads;
|
||||
const uint32_t power_offset = ~average_value + 1;
|
||||
int32_t average_value = total_value / num_reads;
|
||||
int32_t power_offset = -average_value;
|
||||
return static_cast<int16_t>(power_offset); // Takes the lower 16 bits
|
||||
}
|
||||
|
||||
bool ATM90E32Component::verify_gain_writes_() {
|
||||
const char *cs = this->cs_summary_.c_str();
|
||||
bool success = true;
|
||||
for (uint8_t phase = 0; phase < 3; phase++) {
|
||||
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
|
||||
@@ -768,7 +1125,7 @@ bool ATM90E32Component::verify_gain_writes_() {
|
||||
|
||||
if (read_voltage != this->gain_phase_[phase].voltage_gain ||
|
||||
read_current != this->gain_phase_[phase].current_gain) {
|
||||
ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]);
|
||||
ESP_LOGE(TAG, "[CALIBRATION][%s] Mismatch detected for Phase %s!", cs, phase_labels[phase]);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
@@ -791,16 +1148,16 @@ void ATM90E32Component::check_phase_status() {
|
||||
status += "Phase Loss; ";
|
||||
|
||||
auto *sensor = this->phase_status_text_sensor_[phase];
|
||||
const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase";
|
||||
if (sensor == nullptr)
|
||||
continue;
|
||||
|
||||
if (!status.empty()) {
|
||||
status.pop_back(); // remove space
|
||||
status.pop_back(); // remove semicolon
|
||||
ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str());
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(status);
|
||||
ESP_LOGW(TAG, "%s: %s", sensor->get_name().c_str(), status.c_str());
|
||||
sensor->publish_state(status);
|
||||
} else {
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state("Okay");
|
||||
sensor->publish_state("Okay");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -817,9 +1174,12 @@ void ATM90E32Component::check_freq_status() {
|
||||
} else {
|
||||
freq_status = "Normal";
|
||||
}
|
||||
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
|
||||
|
||||
if (this->freq_status_text_sensor_ != nullptr) {
|
||||
if (freq_status == "Normal") {
|
||||
ESP_LOGD(TAG, "Frequency status: %s", freq_status.c_str());
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
|
||||
}
|
||||
this->freq_status_text_sensor_->publish_state(freq_status);
|
||||
}
|
||||
}
|
||||
|
@@ -61,15 +61,29 @@ class ATM90E32Component : public PollingComponent,
|
||||
this->phase_[phase].harmonic_active_power_sensor_ = obj;
|
||||
}
|
||||
void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
|
||||
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
|
||||
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
|
||||
void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; }
|
||||
void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; }
|
||||
void set_volt_gain(int phase, uint16_t gain) {
|
||||
this->phase_[phase].voltage_gain_ = gain;
|
||||
this->has_config_voltage_gain_[phase] = true;
|
||||
}
|
||||
void set_ct_gain(int phase, uint16_t gain) {
|
||||
this->phase_[phase].ct_gain_ = gain;
|
||||
this->has_config_current_gain_[phase] = true;
|
||||
}
|
||||
void set_voltage_offset(uint8_t phase, int16_t offset) {
|
||||
this->offset_phase_[phase].voltage_offset_ = offset;
|
||||
this->has_config_voltage_offset_[phase] = true;
|
||||
}
|
||||
void set_current_offset(uint8_t phase, int16_t offset) {
|
||||
this->offset_phase_[phase].current_offset_ = offset;
|
||||
this->has_config_current_offset_[phase] = true;
|
||||
}
|
||||
void set_active_power_offset(uint8_t phase, int16_t offset) {
|
||||
this->power_offset_phase_[phase].active_power_offset = offset;
|
||||
this->has_config_active_power_offset_[phase] = true;
|
||||
}
|
||||
void set_reactive_power_offset(uint8_t phase, int16_t offset) {
|
||||
this->power_offset_phase_[phase].reactive_power_offset = offset;
|
||||
this->has_config_reactive_power_offset_[phase] = true;
|
||||
}
|
||||
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
|
||||
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
|
||||
@@ -127,7 +141,7 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
uint16_t read16_(uint16_t a_register);
|
||||
int read32_(uint16_t addr_h, uint16_t addr_l);
|
||||
void write16_(uint16_t a_register, uint16_t val);
|
||||
void write16_(uint16_t a_register, uint16_t val, bool validate = true);
|
||||
float get_local_phase_voltage_(uint8_t phase);
|
||||
float get_local_phase_current_(uint8_t phase);
|
||||
float get_local_phase_active_power_(uint8_t phase);
|
||||
@@ -159,12 +173,15 @@ class ATM90E32Component : public PollingComponent,
|
||||
void restore_offset_calibrations_();
|
||||
void restore_power_offset_calibrations_();
|
||||
void restore_gain_calibrations_();
|
||||
void save_offset_calibration_to_memory_();
|
||||
void save_gain_calibration_to_memory_();
|
||||
void save_power_offset_calibration_to_memory_();
|
||||
void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
|
||||
void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
|
||||
void write_gains_to_registers_();
|
||||
bool verify_gain_writes_();
|
||||
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
|
||||
void log_calibration_status_();
|
||||
|
||||
struct ATM90E32Phase {
|
||||
uint16_t voltage_gain_{0};
|
||||
@@ -204,19 +221,33 @@ class ATM90E32Component : public PollingComponent,
|
||||
int16_t current_offset_{0};
|
||||
} offset_phase_[3];
|
||||
|
||||
OffsetCalibration config_offset_phase_[3];
|
||||
|
||||
struct PowerOffsetCalibration {
|
||||
int16_t active_power_offset{0};
|
||||
int16_t reactive_power_offset{0};
|
||||
} power_offset_phase_[3];
|
||||
|
||||
PowerOffsetCalibration config_power_offset_phase_[3];
|
||||
|
||||
struct GainCalibration {
|
||||
uint16_t voltage_gain{1};
|
||||
uint16_t current_gain{1};
|
||||
} gain_phase_[3];
|
||||
|
||||
GainCalibration config_gain_phase_[3];
|
||||
|
||||
bool has_config_voltage_offset_[3]{false, false, false};
|
||||
bool has_config_current_offset_[3]{false, false, false};
|
||||
bool has_config_active_power_offset_[3]{false, false, false};
|
||||
bool has_config_reactive_power_offset_[3]{false, false, false};
|
||||
bool has_config_voltage_gain_[3]{false, false, false};
|
||||
bool has_config_current_gain_[3]{false, false, false};
|
||||
|
||||
ESPPreferenceObject offset_pref_;
|
||||
ESPPreferenceObject power_offset_pref_;
|
||||
ESPPreferenceObject gain_calibration_pref_;
|
||||
std::string cs_summary_;
|
||||
|
||||
sensor::Sensor *freq_sensor_{nullptr};
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
@@ -231,6 +262,13 @@ class ATM90E32Component : public PollingComponent,
|
||||
bool peak_current_signed_{false};
|
||||
bool enable_offset_calibration_{false};
|
||||
bool enable_gain_calibration_{false};
|
||||
bool restored_offset_calibration_{false};
|
||||
bool restored_power_offset_calibration_{false};
|
||||
bool restored_gain_calibration_{false};
|
||||
bool calibration_message_printed_{false};
|
||||
bool offset_calibration_mismatch_[3]{false, false, false};
|
||||
bool power_offset_calibration_mismatch_[3]{false, false, false};
|
||||
bool gain_calibration_mismatch_[3]{false, false, false};
|
||||
};
|
||||
|
||||
} // namespace atm90e32
|
||||
|
@@ -2,7 +2,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MIC_GAIN
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -35,7 +35,7 @@ async def audio_adc_set_mic_gain_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_AUDIO_ADC")
|
||||
cg.add_global(audio_adc_ns.using)
|
||||
|
@@ -3,7 +3,7 @@ from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_VOLUME
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -51,7 +51,7 @@ async def audio_dac_set_volume_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_AUDIO_DAC")
|
||||
cg.add_global(audio_dac_ns.using)
|
||||
|
@@ -41,7 +41,7 @@ void AXS15231Touchscreen::update_touches() {
|
||||
i2c::ErrorCode err;
|
||||
uint8_t data[8]{};
|
||||
|
||||
err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD), false);
|
||||
err = this->write(AXS_READ_TOUCHPAD, sizeof(AXS_READ_TOUCHPAD));
|
||||
ERROR_CHECK(err);
|
||||
err = this->read(data, sizeof(data));
|
||||
ERROR_CHECK(err);
|
||||
|
@@ -59,7 +59,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_VIBRATION,
|
||||
DEVICE_CLASS_WINDOW,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.util import Registry
|
||||
@@ -516,6 +516,7 @@ def binary_sensor_schema(
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
device_class: str = cv.UNDEFINED,
|
||||
filters: list = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {}
|
||||
|
||||
@@ -527,6 +528,7 @@ def binary_sensor_schema(
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
(CONF_FILTERS, filters, validate_filters),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
@@ -650,9 +652,8 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_BINARY_SENSOR")
|
||||
cg.add_global(binary_sensor_ns.using)
|
||||
|
||||
|
||||
|
@@ -7,6 +7,19 @@ namespace binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
// Function implementation of LOG_BINARY_SENSOR macro to reduce code size
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
|
||||
if (!obj->get_device_class_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void BinarySensor::publish_state(bool new_state) {
|
||||
if (this->filter_list_ == nullptr) {
|
||||
this->send_state_internal(new_state);
|
||||
|
@@ -10,13 +10,10 @@ namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
|
||||
#define LOG_BINARY_SENSOR(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_device_class().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Device Class: '%s'", prefix, (obj)->get_device_class().c_str()); \
|
||||
} \
|
||||
}
|
||||
class BinarySensor;
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
|
||||
|
||||
#define LOG_BINARY_SENSOR(prefix, type, obj) log_binary_sensor(TAG, prefix, LOG_STR_LITERAL(type), obj)
|
||||
|
||||
#define SUB_BINARY_SENSOR(name) \
|
||||
protected: \
|
||||
|
@@ -175,8 +175,7 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -184,8 +183,7 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def ble_connect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -282,14 +280,13 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def remove_bond_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Register the loggers this component needs
|
||||
esp32_ble.register_bt_logger(BTLoggers.GATT, BTLoggers.SMP)
|
||||
cg.add_define("USE_ESP32_BLE_UUID")
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def to_code(config):
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
@@ -63,6 +63,6 @@ def to_code(config):
|
||||
)
|
||||
cg.add(var.set_char_uuid128(uuid128))
|
||||
cg.add(var.set_require_response(config[CONF_REQUIRE_RESPONSE]))
|
||||
yield output.register_output(var, config)
|
||||
yield ble_client.register_ble_node(var, config)
|
||||
yield cg.register_component(var, config)
|
||||
await output.register_output(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
@@ -1,13 +1,19 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ACTIVE, CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.log import AnsiFore, color
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
||||
DEPENDENCIES = ["api", "esp32"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CONNECTION_SLOTS = "connection_slots"
|
||||
CONF_CACHE_SERVICES = "cache_services"
|
||||
@@ -41,6 +47,27 @@ def validate_connections(config):
|
||||
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
||||
config
|
||||
)
|
||||
|
||||
# Warn about connection slot waste when using Arduino framework
|
||||
if CORE.using_arduino and connection_slots:
|
||||
_LOGGER.warning(
|
||||
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
|
||||
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
|
||||
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
|
||||
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
|
||||
"\n"
|
||||
"To switch to ESP-IDF, add this to your YAML:\n"
|
||||
" esp32:\n"
|
||||
" framework:\n"
|
||||
" type: esp-idf\n"
|
||||
"\n"
|
||||
"For detailed migration instructions, see:\n"
|
||||
"%s",
|
||||
color(
|
||||
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
**config,
|
||||
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
||||
@@ -53,7 +80,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BluetoothProxy),
|
||||
cv.Optional(CONF_ACTIVE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
|
||||
cv.SplitDefault(CONF_CACHE_SERVICES, esp32_idf=True): cv.All(
|
||||
cv.only_with_esp_idf, cv.boolean
|
||||
),
|
||||
@@ -87,6 +114,16 @@ async def to_code(config):
|
||||
cg.add(var.set_active(config[CONF_ACTIVE]))
|
||||
await esp32_ble_tracker.register_raw_ble_device(var, config)
|
||||
|
||||
# Define max connections for protobuf fixed array
|
||||
connection_count = len(config.get(CONF_CONNECTIONS, []))
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
|
||||
|
||||
# Define batch size for BLE advertisements
|
||||
# Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||||
# 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
|
||||
# This achieves ~97% WiFi MTU utilization while staying under the limit
|
||||
cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
|
||||
|
||||
for connection_conf in config.get(CONF_CONNECTIONS, []):
|
||||
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||
await cg.register_component(connection_var, connection_conf)
|
||||
|
@@ -12,16 +12,79 @@ namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const char *const TAG = "bluetooth_proxy.connection";
|
||||
|
||||
// This function is allocation-free and directly packs UUIDs into the output array
|
||||
// using precalculated constants for the Bluetooth base UUID
|
||||
static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t uuid_source) {
|
||||
esp_bt_uuid_t uuid = espbt::ESPBTUUID::from_uuid(uuid_source).as_128bit().get_uuid();
|
||||
out[0] = ((uint64_t) uuid.uuid.uuid128[15] << 56) | ((uint64_t) uuid.uuid.uuid128[14] << 48) |
|
||||
((uint64_t) uuid.uuid.uuid128[13] << 40) | ((uint64_t) uuid.uuid.uuid128[12] << 32) |
|
||||
((uint64_t) uuid.uuid.uuid128[11] << 24) | ((uint64_t) uuid.uuid.uuid128[10] << 16) |
|
||||
((uint64_t) uuid.uuid.uuid128[9] << 8) | ((uint64_t) uuid.uuid.uuid128[8]);
|
||||
out[1] = ((uint64_t) uuid.uuid.uuid128[7] << 56) | ((uint64_t) uuid.uuid.uuid128[6] << 48) |
|
||||
((uint64_t) uuid.uuid.uuid128[5] << 40) | ((uint64_t) uuid.uuid.uuid128[4] << 32) |
|
||||
((uint64_t) uuid.uuid.uuid128[3] << 24) | ((uint64_t) uuid.uuid.uuid128[2] << 16) |
|
||||
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
|
||||
// Bluetooth base UUID: 00000000-0000-1000-8000-00805F9B34FB
|
||||
// out[0] = bytes 8-15 (big-endian)
|
||||
// - For 128-bit UUIDs: use bytes 8-15 as-is
|
||||
// - For 16/32-bit UUIDs: insert into bytes 12-15, use 0x00001000 for bytes 8-11
|
||||
out[0] = uuid_source.len == ESP_UUID_LEN_128
|
||||
? (((uint64_t) uuid_source.uuid.uuid128[15] << 56) | ((uint64_t) uuid_source.uuid.uuid128[14] << 48) |
|
||||
((uint64_t) uuid_source.uuid.uuid128[13] << 40) | ((uint64_t) uuid_source.uuid.uuid128[12] << 32) |
|
||||
((uint64_t) uuid_source.uuid.uuid128[11] << 24) | ((uint64_t) uuid_source.uuid.uuid128[10] << 16) |
|
||||
((uint64_t) uuid_source.uuid.uuid128[9] << 8) | ((uint64_t) uuid_source.uuid.uuid128[8]))
|
||||
: (((uint64_t) (uuid_source.len == ESP_UUID_LEN_16 ? uuid_source.uuid.uuid16 : uuid_source.uuid.uuid32)
|
||||
<< 32) |
|
||||
0x00001000ULL); // Base UUID bytes 8-11
|
||||
// out[1] = bytes 0-7 (big-endian)
|
||||
// - For 128-bit UUIDs: use bytes 0-7 as-is
|
||||
// - For 16/32-bit UUIDs: use precalculated base UUID constant
|
||||
out[1] = uuid_source.len == ESP_UUID_LEN_128
|
||||
? ((uint64_t) uuid_source.uuid.uuid128[7] << 56) | ((uint64_t) uuid_source.uuid.uuid128[6] << 48) |
|
||||
((uint64_t) uuid_source.uuid.uuid128[5] << 40) | ((uint64_t) uuid_source.uuid.uuid128[4] << 32) |
|
||||
((uint64_t) uuid_source.uuid.uuid128[3] << 24) | ((uint64_t) uuid_source.uuid.uuid128[2] << 16) |
|
||||
((uint64_t) uuid_source.uuid.uuid128[1] << 8) | ((uint64_t) uuid_source.uuid.uuid128[0])
|
||||
: 0x800000805F9B34FBULL; // Base UUID bytes 0-7: 80-00-00-80-5F-9B-34-FB
|
||||
}
|
||||
|
||||
// Helper to fill UUID in the appropriate format based on client support and UUID type
|
||||
static void fill_gatt_uuid(std::array<uint64_t, 2> &uuid_128, uint32_t &short_uuid, const esp_bt_uuid_t &uuid,
|
||||
bool use_efficient_uuids) {
|
||||
if (!use_efficient_uuids || uuid.len == ESP_UUID_LEN_128) {
|
||||
// Use 128-bit format for old clients or when UUID is already 128-bit
|
||||
fill_128bit_uuid_array(uuid_128, uuid);
|
||||
} else if (uuid.len == ESP_UUID_LEN_16) {
|
||||
short_uuid = uuid.uuid.uuid16;
|
||||
} else if (uuid.len == ESP_UUID_LEN_32) {
|
||||
short_uuid = uuid.uuid.uuid32;
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for size estimation
|
||||
static constexpr uint8_t SERVICE_OVERHEAD_LEGACY = 25; // UUID(20) + handle(4) + overhead(1)
|
||||
static constexpr uint8_t SERVICE_OVERHEAD_EFFICIENT = 10; // UUID(6) + handle(4)
|
||||
static constexpr uint8_t CHAR_SIZE_128BIT = 35; // UUID(20) + handle(4) + props(4) + overhead(7)
|
||||
static constexpr uint8_t DESC_SIZE_128BIT = 25; // UUID(20) + handle(4) + overhead(1)
|
||||
static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4)
|
||||
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
|
||||
|
||||
// Helper to estimate service size before fetching all data
|
||||
/**
|
||||
* Estimate the size of a Bluetooth service based on the number of characteristics and UUID format.
|
||||
*
|
||||
* @param char_count The number of characteristics in the service.
|
||||
* @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions.
|
||||
* @return The estimated size of the service in bytes.
|
||||
*
|
||||
* This function calculates the size of a Bluetooth service by considering:
|
||||
* - A service overhead, which depends on whether efficient UUIDs are used.
|
||||
* - The size of each characteristic, assuming 128-bit UUIDs for safety.
|
||||
* - The size of descriptors, assuming one 128-bit descriptor per characteristic.
|
||||
*/
|
||||
static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) {
|
||||
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
|
||||
// Always assume 128-bit UUIDs for characteristics to be safe
|
||||
size_t char_size = CHAR_SIZE_128BIT;
|
||||
// Assume one 128-bit descriptor per characteristic
|
||||
size_t desc_size = DESC_SIZE_128BIT * DESC_PER_CHAR;
|
||||
|
||||
return service_overhead + (char_size + desc_size) * char_count;
|
||||
}
|
||||
|
||||
bool BluetoothConnection::supports_efficient_uuids_() const {
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
return api_conn && api_conn->client_supports_api_version(1, 12);
|
||||
}
|
||||
|
||||
void BluetoothConnection::dump_config() {
|
||||
@@ -29,16 +92,53 @@ void BluetoothConnection::dump_config() {
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
|
||||
auto &allocated = this->proxy_->connections_free_response_.allocated;
|
||||
for (auto &slot : allocated) {
|
||||
if (slot == find_value) {
|
||||
slot = set_value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::set_address(uint64_t address) {
|
||||
// If we're clearing an address (disconnecting), update the pre-allocated message
|
||||
if (address == 0 && this->address_ != 0) {
|
||||
this->proxy_->connections_free_response_.free++;
|
||||
this->update_allocated_slot_(this->address_, 0);
|
||||
}
|
||||
// If we're setting a new address (connecting), update the pre-allocated message
|
||||
else if (address != 0 && this->address_ == 0) {
|
||||
this->proxy_->connections_free_response_.free--;
|
||||
this->update_allocated_slot_(0, address);
|
||||
}
|
||||
|
||||
// Call parent implementation to actually set the address
|
||||
BLEClientBase::set_address(address);
|
||||
}
|
||||
|
||||
void BluetoothConnection::loop() {
|
||||
BLEClientBase::loop();
|
||||
|
||||
// Early return if no active connection or not in service discovery phase
|
||||
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
|
||||
// Early return if no active connection
|
||||
if (this->address_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle service discovery
|
||||
this->send_service_for_discovery_();
|
||||
// Handle service discovery if in valid range
|
||||
if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) {
|
||||
this->send_service_for_discovery_();
|
||||
}
|
||||
|
||||
// Check if we should disable the loop
|
||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||
@@ -52,140 +152,222 @@ void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||
// to detect incomplete service discovery rather than relying on us to
|
||||
// tell them about a partial list.
|
||||
this->set_address(0);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->send_service_ = INIT_SENDING_SERVICES;
|
||||
this->proxy_->send_connections_free();
|
||||
}
|
||||
|
||||
void BluetoothConnection::send_service_for_discovery_() {
|
||||
if (this->send_service_ == this->service_count_) {
|
||||
if (this->send_service_ >= this->service_count_) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->proxy_->send_gatt_services_done(this->address_);
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
this->release_services();
|
||||
}
|
||||
this->release_services();
|
||||
return;
|
||||
}
|
||||
|
||||
// Early return if no API connection
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// Send next service
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||
&service_result, &service_count, this->send_service_);
|
||||
this->send_service_++;
|
||||
|
||||
if (service_status != ESP_GATT_OK || service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
|
||||
this->connection_index_, this->address_str().c_str(), service_status != ESP_GATT_OK ? "error" : "missing",
|
||||
service_status, service_count, this->send_service_ - 1);
|
||||
return;
|
||||
}
|
||||
// Check if client supports efficient UUIDs
|
||||
bool use_efficient_uuids = this->supports_efficient_uuids_();
|
||||
|
||||
// Prepare response
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = this->address_;
|
||||
auto &service_resp = resp.services[0];
|
||||
fill_128bit_uuid_array(service_resp.uuid, service_result.uuid);
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
// Get the number of characteristics directly with one call
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
// Dynamic batching based on actual size
|
||||
// Conservative MTU limit for API messages (accounts for WPA3 overhead)
|
||||
static constexpr size_t MAX_PACKET_SIZE = 1360;
|
||||
|
||||
if (char_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_count_status);
|
||||
return;
|
||||
}
|
||||
// Keep running total of actual message size
|
||||
size_t current_size = 0;
|
||||
api::ProtoSize size;
|
||||
resp.calculate_size(size);
|
||||
current_size = size.get_size();
|
||||
|
||||
if (total_char_count == 0) {
|
||||
// No characteristics, just send the service response
|
||||
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||
return;
|
||||
}
|
||||
while (this->send_service_ < this->service_count_) {
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||
&service_result, &service_count, this->send_service_);
|
||||
|
||||
// Reserve space and process characteristics
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_status);
|
||||
if (service_status != ESP_GATT_OK || service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
|
||||
this->connection_index_, this->address_str().c_str(),
|
||||
service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
|
||||
// Get the number of characteristics BEFORE adding to response
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
|
||||
if (char_count_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// If this service likely won't fit, send current batch (unless it's the first)
|
||||
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
|
||||
if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
|
||||
// This service likely won't fit, send current batch
|
||||
break;
|
||||
}
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid);
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
// Now add the service since we know it will likely fit
|
||||
resp.services.emplace_back();
|
||||
auto &service_resp = resp.services.back();
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
||||
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
||||
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids);
|
||||
|
||||
if (desc_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
||||
return;
|
||||
}
|
||||
if (total_desc_count == 0) {
|
||||
// No descriptors, continue to next characteristic
|
||||
continue;
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
if (total_char_count > 0) {
|
||||
// Reserve space and process characteristics
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
|
||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
||||
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
||||
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
||||
|
||||
if (desc_count_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (total_desc_count == 0) {
|
||||
// No descriptors, continue to next characteristic
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserve space and process descriptors
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break; // No more descriptors
|
||||
}
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
|
||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
||||
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
}
|
||||
} // end if (total_char_count > 0)
|
||||
|
||||
// Calculate the actual size of just this service
|
||||
api::ProtoSize service_sizer;
|
||||
service_resp.calculate_size(service_sizer);
|
||||
size_t service_size = service_sizer.get_size() + 1; // +1 for field tag
|
||||
|
||||
// Check if adding this service would exceed the limit
|
||||
if (current_size + service_size > MAX_PACKET_SIZE) {
|
||||
// We would go over - pop the last service if we have more than one
|
||||
if (resp.services.size() > 1) {
|
||||
resp.services.pop_back();
|
||||
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
|
||||
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
|
||||
MAX_PACKET_SIZE);
|
||||
// Don't increment send_service_ - we'll retry this service in next batch
|
||||
} else {
|
||||
// This single service is too large, but we have to send it anyway
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
|
||||
this->address_str().c_str(), this->send_service_, service_size);
|
||||
// Increment so we don't get stuck
|
||||
this->send_service_++;
|
||||
}
|
||||
// Send what we have
|
||||
break;
|
||||
}
|
||||
|
||||
// Reserve space and process descriptors
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), desc_status);
|
||||
return;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break; // No more descriptors
|
||||
}
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid);
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
// Now we know we're keeping this service, add its size
|
||||
current_size += service_size;
|
||||
// Successfully added this service, increment counter
|
||||
this->send_service_++;
|
||||
}
|
||||
|
||||
// Send the message (we already checked api_conn is not null at the beginning)
|
||||
// Send the message with dynamically batched services
|
||||
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
|
||||
status);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
|
||||
action, type);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
|
||||
operation, handle, status);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
|
||||
if (err != ESP_OK) {
|
||||
this->log_connection_warning_(operation, err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||
@@ -193,10 +375,19 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
|
||||
switch (event) {
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->reset_connection_(param->disconnect.reason);
|
||||
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
|
||||
// This prevents race condition where we mark slot as free before controller cleanup is complete
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
|
||||
param->disconnect.reason);
|
||||
// Send disconnection notification but don't free the slot yet
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
|
||||
param->close.reason);
|
||||
// Now the GATT connection is fully closed and controller resources are freed
|
||||
// Safe to mark the connection slot as available
|
||||
this->reset_connection_(param->close.reason);
|
||||
break;
|
||||
}
|
||||
@@ -226,8 +417,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
case ESP_GATTC_READ_DESCR_EVT:
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->read.handle, param->read.status);
|
||||
this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
@@ -241,8 +431,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->write.handle, param->write.status);
|
||||
this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
|
||||
break;
|
||||
}
|
||||
@@ -254,9 +443,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
}
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
if (param->unreg_for_notify.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d",
|
||||
this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle,
|
||||
param->unreg_for_notify.status);
|
||||
this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
|
||||
param->unreg_for_notify.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
@@ -268,8 +456,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
if (param->reg_for_notify.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status);
|
||||
this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
|
||||
param->reg_for_notify.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
@@ -315,8 +503,7 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
|
||||
|
||||
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("read", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
@@ -324,18 +511,12 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("write", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
@@ -344,36 +525,24 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("read", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("write", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
@@ -382,18 +551,12 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
|
||||
esp_err_t err = esp_ble_gattc_write_char_descr(
|
||||
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("notify", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
@@ -401,22 +564,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
|
||||
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
|
||||
}
|
||||
return ESP_OK;
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
|
||||
}
|
||||
|
||||
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {
|
||||
|
@@ -8,7 +8,7 @@ namespace esphome::bluetooth_proxy {
|
||||
|
||||
class BluetoothProxy;
|
||||
|
||||
class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
class BluetoothConnection final : public esp32_ble_client::BLEClientBase {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
@@ -24,18 +24,27 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
|
||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||
|
||||
void set_address(uint64_t address) override;
|
||||
|
||||
protected:
|
||||
friend class BluetoothProxy;
|
||||
|
||||
bool supports_efficient_uuids_() const;
|
||||
void send_service_for_discovery_();
|
||||
void reset_connection_(esp_err_t reason);
|
||||
void update_allocated_slot_(uint64_t find_value, uint64_t set_value);
|
||||
void log_connection_error_(const char *operation, esp_gatt_status_t status);
|
||||
void log_connection_warning_(const char *operation, esp_err_t err);
|
||||
void log_gatt_not_connected_(const char *action, const char *type);
|
||||
void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
|
||||
esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
BluetoothProxy *proxy_;
|
||||
|
||||
// Group 2: 2-byte types
|
||||
int16_t send_service_{-2}; // Needs to handle negative values and service count
|
||||
int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index
|
||||
|
||||
// Group 3: 1-byte types
|
||||
bool seen_mtu_or_services_{false};
|
||||
|
@@ -11,12 +11,8 @@ namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const char *const TAG = "bluetooth_proxy";
|
||||
|
||||
// Batch size for BLE advertisements to maximize WiFi efficiency
|
||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
|
||||
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
|
||||
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
|
||||
// This achieves ~97% WiFi MTU utilization while staying under the limit
|
||||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
|
||||
// BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation
|
||||
// It sets the batch size for BLE advertisements to maximize WiFi efficiency
|
||||
|
||||
// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
|
||||
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
|
||||
@@ -25,15 +21,11 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62
|
||||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
|
||||
|
||||
void BluetoothProxy::setup() {
|
||||
// Pre-allocate response object
|
||||
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
|
||||
this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||
this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||
|
||||
// Reserve capacity but start with size 0
|
||||
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
|
||||
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
|
||||
|
||||
// Don't pre-allocate pool - let it grow only if needed in busy environments
|
||||
// Many devices in quiet areas will never need the overflow pool
|
||||
// Capture the configured scan mode from YAML before any API changes
|
||||
this->configured_scan_active_ = this->parent_->get_scan_active();
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
@@ -47,9 +39,32 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
|
||||
resp.state = static_cast<api::enums::BluetoothScannerState>(state);
|
||||
resp.mode = this->parent_->get_scan_active() ? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
|
||||
resp.configured_mode = this->configured_scan_active_
|
||||
? api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE
|
||||
: api::enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_PASSIVE;
|
||||
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), espbt::client_state_to_string(state));
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
|
||||
message);
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
|
||||
ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type);
|
||||
}
|
||||
|
||||
void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
|
||||
const char *type) {
|
||||
this->log_not_connected_gatt_(action, type);
|
||||
this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
// This method should never be called since bluetooth_proxy always uses raw advertisements
|
||||
@@ -62,39 +77,27 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return false;
|
||||
|
||||
auto &advertisements = this->response_->advertisements;
|
||||
auto &advertisements = this->response_.advertisements;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
auto &result = scan_results[i];
|
||||
uint8_t length = result.adv_data_len + result.scan_rsp_len;
|
||||
|
||||
// Check if we need to expand the vector
|
||||
if (this->advertisement_count_ >= advertisements.size()) {
|
||||
if (this->advertisement_pool_.empty()) {
|
||||
// No room in pool, need to allocate
|
||||
advertisements.emplace_back();
|
||||
} else {
|
||||
// Pull from pool
|
||||
advertisements.push_back(std::move(this->advertisement_pool_.back()));
|
||||
this->advertisement_pool_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in the data directly at current position
|
||||
auto &adv = advertisements[this->advertisement_count_];
|
||||
auto &adv = advertisements[this->response_.advertisements_len];
|
||||
adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
|
||||
adv.rssi = result.rssi;
|
||||
adv.address_type = result.ble_addr_type;
|
||||
adv.data_len = length;
|
||||
std::memcpy(adv.data, result.ble_adv, length);
|
||||
|
||||
this->advertisement_count_++;
|
||||
this->response_.advertisements_len++;
|
||||
|
||||
ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
|
||||
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
|
||||
|
||||
// Flush if we have reached FLUSH_BATCH_SIZE
|
||||
if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) {
|
||||
// Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE
|
||||
if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) {
|
||||
this->flush_pending_advertisements();
|
||||
}
|
||||
}
|
||||
@@ -103,54 +106,31 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
|
||||
}
|
||||
|
||||
void BluetoothProxy::flush_pending_advertisements() {
|
||||
if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() ||
|
||||
this->api_connection_ == nullptr)
|
||||
return;
|
||||
|
||||
auto &advertisements = this->response_->advertisements;
|
||||
|
||||
// Return any items beyond advertisement_count_ to the pool
|
||||
if (advertisements.size() > this->advertisement_count_) {
|
||||
// Move unused items back to pool
|
||||
this->advertisement_pool_.insert(this->advertisement_pool_.end(),
|
||||
std::make_move_iterator(advertisements.begin() + this->advertisement_count_),
|
||||
std::make_move_iterator(advertisements.end()));
|
||||
|
||||
// Resize to actual count
|
||||
advertisements.resize(this->advertisement_count_);
|
||||
}
|
||||
|
||||
// Send the message
|
||||
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||||
this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||||
|
||||
// Reset count - existing items will be overwritten in next batch
|
||||
this->advertisement_count_ = 0;
|
||||
ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len);
|
||||
|
||||
// Reset the length for the next batch
|
||||
this->response_.advertisements_len = 0;
|
||||
}
|
||||
|
||||
void BluetoothProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Bluetooth Proxy:\n"
|
||||
" Active: %s\n"
|
||||
" Connections: %d",
|
||||
YESNO(this->active_), this->connections_.size());
|
||||
}
|
||||
|
||||
int BluetoothProxy::get_bluetooth_connections_free() {
|
||||
int free = 0;
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->address_ == 0) {
|
||||
free++;
|
||||
ESP_LOGV(TAG, "[%d] Free connection", connection->get_connection_index());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "[%d] Used connection by [%s]", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
}
|
||||
}
|
||||
return free;
|
||||
YESNO(this->active_), this->connection_count_);
|
||||
}
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
@@ -173,7 +153,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
|
||||
}
|
||||
|
||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == address)
|
||||
return connection;
|
||||
}
|
||||
@@ -181,9 +162,10 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
if (!reserve)
|
||||
return nullptr;
|
||||
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == 0) {
|
||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
||||
connection->send_service_ = INIT_SENDING_SERVICES;
|
||||
connection->set_address(address);
|
||||
// All connections must start at INIT
|
||||
// We only set the state if we allocate the connection
|
||||
@@ -200,33 +182,25 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {
|
||||
switch (msg.request_type) {
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: {
|
||||
auto *connection = this->get_connection_(msg.address, true);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "No free connections available");
|
||||
this->send_device_connection(msg.address, false);
|
||||
return;
|
||||
}
|
||||
if (!msg.has_address_type) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
this->send_device_connection(msg.address, false);
|
||||
return;
|
||||
}
|
||||
if (connection->state() == espbt::ClientState::CONNECTED ||
|
||||
connection->state() == espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
this->send_device_connection(msg.address, true);
|
||||
this->send_connections_free();
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::SEARCHING) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::DISCOVERED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::CONNECTING) {
|
||||
if (connection->disconnect_pending()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||||
@@ -234,37 +208,22 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
connection->cancel_pending_disconnect();
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
return;
|
||||
} else if (connection->state() != espbt::ClientState::INIT) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
return;
|
||||
}
|
||||
if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) {
|
||||
connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
} else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) {
|
||||
this->log_connection_info_(connection, "v3 with cache");
|
||||
} else { // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE
|
||||
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
} else {
|
||||
connection->set_connection_type(espbt::ConnectionType::V1);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str());
|
||||
}
|
||||
if (msg.has_address_type) {
|
||||
uint64_to_bd_addr(msg.address, connection->remote_bda_);
|
||||
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
|
||||
connection->set_state(espbt::ClientState::DISCOVERED);
|
||||
} else {
|
||||
connection->set_state(espbt::ClientState::SEARCHING);
|
||||
this->log_connection_info_(connection, "v3 without cache");
|
||||
}
|
||||
uint64_to_bd_addr(msg.address, connection->remote_bda_);
|
||||
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
|
||||
connection->set_state(espbt::ClientState::DISCOVERED);
|
||||
this->send_connections_free();
|
||||
break;
|
||||
}
|
||||
@@ -318,14 +277,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
|
||||
break;
|
||||
}
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
|
||||
ESP_LOGE(TAG, "V1 connections removed");
|
||||
this->send_device_connection(msg.address, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,8 +301,7 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms
|
||||
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -352,8 +314,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
|
||||
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,8 +327,7 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead
|
||||
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -380,8 +340,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
|
||||
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr || !connection->connected()) {
|
||||
ESP_LOGW(TAG, "Cannot get GATT services, not connected");
|
||||
this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, 0, "get", "services");
|
||||
return;
|
||||
}
|
||||
if (!connection->service_count_) {
|
||||
@@ -389,16 +348,14 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
|
||||
this->send_gatt_services_done(msg.address);
|
||||
return;
|
||||
}
|
||||
if (connection->send_service_ ==
|
||||
DONE_SENDING_SERVICES) // Only start sending services if we're not already sending them
|
||||
if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet
|
||||
connection->send_service_ = 0;
|
||||
}
|
||||
|
||||
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -439,17 +396,13 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
|
||||
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void BluetoothProxy::send_connections_free() {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return;
|
||||
api::BluetoothConnectionsFreeResponse call;
|
||||
call.free = this->get_bluetooth_connections_free();
|
||||
call.limit = this->get_bluetooth_connections_limit();
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->address_ != 0) {
|
||||
call.allocated.push_back(connection->address_);
|
||||
}
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_connections_free(this->api_connection_);
|
||||
}
|
||||
this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) {
|
||||
api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
@@ -22,6 +23,7 @@ namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
||||
static const int DONE_SENDING_SERVICES = -2;
|
||||
static const int INIT_SENDING_SERVICES = -3;
|
||||
|
||||
using namespace esp32_ble_client;
|
||||
|
||||
@@ -48,7 +50,8 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
|
||||
};
|
||||
|
||||
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
friend class BluetoothConnection; // Allow connection to update connections_free_response_
|
||||
public:
|
||||
BluetoothProxy();
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
@@ -62,8 +65,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
this->connections_.push_back(connection);
|
||||
connection->proxy_ = this;
|
||||
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
|
||||
this->connections_[this->connection_count_++] = connection;
|
||||
connection->proxy_ = this;
|
||||
}
|
||||
}
|
||||
|
||||
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
|
||||
@@ -74,15 +79,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg);
|
||||
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg);
|
||||
|
||||
int get_bluetooth_connections_free();
|
||||
int get_bluetooth_connections_limit() { return this->connections_.size(); }
|
||||
|
||||
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags);
|
||||
void unsubscribe_api_connection(api::APIConnection *api_connection);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
|
||||
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
|
||||
void send_connections_free();
|
||||
void send_connections_free(api::APIConnection *api_connection);
|
||||
void send_gatt_services_done(uint64_t address);
|
||||
void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
|
||||
void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
|
||||
@@ -134,25 +137,32 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
|
||||
|
||||
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
|
||||
void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state);
|
||||
void log_connection_info_(BluetoothConnection *connection, const char *message);
|
||||
void log_not_connected_gatt_(const char *action, const char *type);
|
||||
void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
api::APIConnection *api_connection_{nullptr};
|
||||
|
||||
// Group 2: Container types (typically 12 bytes on 32-bit)
|
||||
std::vector<BluetoothConnection *> connections_{};
|
||||
// Group 2: Fixed-size array of connection pointers
|
||||
std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
|
||||
|
||||
// BLE advertisement batching
|
||||
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
// Pre-allocated response message - always ready to send
|
||||
api::BluetoothConnectionsFreeResponse connections_free_response_;
|
||||
|
||||
// Group 4: 1-byte types grouped together
|
||||
bool active_;
|
||||
uint8_t advertisement_count_{0};
|
||||
// 2 bytes used, 2 bytes padding
|
||||
uint8_t connection_count_{0};
|
||||
bool configured_scan_active_{false}; // Configured scan mode from YAML
|
||||
// 3 bytes used, 1 byte padding
|
||||
};
|
||||
|
||||
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@@ -7,6 +7,8 @@
|
||||
#include <esphome/components/sensor/sensor.h>
|
||||
#include <esphome/core/component.h>
|
||||
|
||||
#define BME280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
|
||||
|
||||
namespace esphome {
|
||||
namespace bme280_base {
|
||||
|
||||
@@ -98,18 +100,18 @@ void BME280Component::setup() {
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x60) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed();
|
||||
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed("Reset failed");
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -118,14 +120,12 @@ void BME280Component::setup() {
|
||||
do { // NOLINT
|
||||
delay(2);
|
||||
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
||||
ESP_LOGW(TAG, "Error reading status register.");
|
||||
this->mark_failed();
|
||||
this->mark_failed("Error reading status register");
|
||||
return;
|
||||
}
|
||||
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BME280_STATUS_IM_UPDATE) {
|
||||
ESP_LOGW(TAG, "Timeout loading NVM.");
|
||||
this->mark_failed();
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,26 +153,26 @@ void BME280Component::setup() {
|
||||
|
||||
uint8_t humid_control_val = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed("Read humidity control");
|
||||
return;
|
||||
}
|
||||
humid_control_val &= ~0b00000111;
|
||||
humid_control_val |= this->humidity_oversampling_ & 0b111;
|
||||
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed("Write humidity control");
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed("Read config");
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b101 << 5; // 1000 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed();
|
||||
this->mark_failed("Write config");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ void BME280Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_CHIP_ID:
|
||||
ESP_LOGE(TAG, "BME280 has wrong chip ID! Is it a BME280?");
|
||||
ESP_LOGE(TAG, BME280_ERROR_WRONG_CHIP_ID);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
@@ -223,21 +223,21 @@ void BME280Component::update() {
|
||||
this->set_timeout("data", uint32_t(ceilf(meas_time)), [this]() {
|
||||
uint8_t data[8];
|
||||
if (!this->read_bytes(BME280_REGISTER_MEASUREMENTS, data, 8)) {
|
||||
ESP_LOGW(TAG, "Error reading registers.");
|
||||
ESP_LOGW(TAG, "Error reading registers");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
int32_t t_fine = 0;
|
||||
float const temperature = this->read_temperature_(data, &t_fine);
|
||||
if (std::isnan(temperature)) {
|
||||
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
|
||||
ESP_LOGW(TAG, "Invalid temperature");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
float const pressure = this->read_pressure_(data, t_fine);
|
||||
float const humidity = this->read_humidity_(data, t_fine);
|
||||
|
||||
ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
|
||||
ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa Humidity=%.1f%%", temperature, pressure, humidity);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->pressure_sensor_ != nullptr)
|
||||
|
@@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8,
|
||||
-0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||
|
||||
static const char *oversampling_to_str(BME680Oversampling oversampling) {
|
||||
[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case BME680_OVERSAMPLING_NONE:
|
||||
return "None";
|
||||
@@ -47,7 +47,7 @@ static const char *oversampling_to_str(BME680Oversampling oversampling) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *iir_filter_to_str(BME680IIRFilter filter) {
|
||||
[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case BME680_IIR_FILTER_OFF:
|
||||
return "OFF";
|
||||
|
@@ -203,7 +203,7 @@ void BMI160Component::dump_config() {
|
||||
i2c::ErrorCode BMI160Component::read_le_int16_(uint8_t reg, int16_t *value, uint8_t len) {
|
||||
uint8_t raw_data[len * 2];
|
||||
// read using read_register because we have little-endian data, and read_bytes_16 will swap it
|
||||
i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2, true);
|
||||
i2c::ErrorCode err = this->read_register(reg, raw_data, len * 2);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
return err;
|
||||
}
|
||||
|
@@ -2,6 +2,8 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#define BMP280_ERROR_WRONG_CHIP_ID "Wrong chip ID"
|
||||
|
||||
namespace esphome {
|
||||
namespace bmp280_base {
|
||||
|
||||
@@ -61,25 +63,25 @@ void BMP280Component::setup() {
|
||||
|
||||
// Read the chip id twice, to work around a bug where the first read is 0.
|
||||
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
|
||||
if (!this->read_byte(0xD0, &chip_id)) {
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
if (!this->read_byte(0xD0, &chip_id)) {
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed();
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x58) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed();
|
||||
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
|
||||
this->mark_failed();
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -87,15 +89,13 @@ void BMP280Component::setup() {
|
||||
uint8_t retry = 5;
|
||||
do {
|
||||
delay(2);
|
||||
if (!this->read_byte(BMP280_REGISTER_STATUS, &status)) {
|
||||
ESP_LOGW(TAG, "Error reading status register.");
|
||||
this->mark_failed();
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
return;
|
||||
}
|
||||
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BMP280_STATUS_IM_UPDATE) {
|
||||
ESP_LOGW(TAG, "Timeout loading NVM.");
|
||||
this->mark_failed();
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -115,15 +115,15 @@ void BMP280Component::setup() {
|
||||
this->calibration_.p9 = this->read_s16_le_(0x9E);
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed();
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b000 << 5; // 0.5 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->write_byte(BMP280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed();
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +134,7 @@ void BMP280Component::dump_config() {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
break;
|
||||
case WRONG_CHIP_ID:
|
||||
ESP_LOGE(TAG, "BMP280 has wrong chip ID! Is it a BME280?");
|
||||
ESP_LOGE(TAG, BMP280_ERROR_WRONG_CHIP_ID);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
@@ -159,7 +159,7 @@ void BMP280Component::update() {
|
||||
meas_value |= (this->temperature_oversampling_ & 0b111) << 5;
|
||||
meas_value |= (this->pressure_oversampling_ & 0b111) << 2;
|
||||
meas_value |= 0b01; // Forced mode
|
||||
if (!this->write_byte(BMP280_REGISTER_CONTROL, meas_value)) {
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_CONTROL, meas_value)) {
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
@@ -172,13 +172,13 @@ void BMP280Component::update() {
|
||||
int32_t t_fine = 0;
|
||||
float temperature = this->read_temperature_(&t_fine);
|
||||
if (std::isnan(temperature)) {
|
||||
ESP_LOGW(TAG, "Invalid temperature, cannot read pressure values.");
|
||||
ESP_LOGW(TAG, "Invalid temperature");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
float pressure = this->read_pressure_(t_fine);
|
||||
|
||||
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa", temperature, pressure);
|
||||
ESP_LOGV(TAG, "Temperature=%.1f°C Pressure=%.1fhPa", temperature, pressure);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
if (this->pressure_sensor_ != nullptr)
|
||||
@@ -188,9 +188,10 @@ void BMP280Component::update() {
|
||||
}
|
||||
|
||||
float BMP280Component::read_temperature_(int32_t *t_fine) {
|
||||
uint8_t data[3];
|
||||
if (!this->read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
|
||||
uint8_t data[3]{};
|
||||
if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
|
||||
return NAN;
|
||||
ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]);
|
||||
int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
|
||||
adc >>= 4;
|
||||
if (adc == 0x80000) {
|
||||
@@ -212,7 +213,7 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
|
||||
|
||||
float BMP280Component::read_pressure_(int32_t t_fine) {
|
||||
uint8_t data[3];
|
||||
if (!this->read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
|
||||
if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
|
||||
return NAN;
|
||||
int32_t adc = ((data[0] & 0xFF) << 16) | ((data[1] & 0xFF) << 8) | (data[2] & 0xFF);
|
||||
adc >>= 4;
|
||||
@@ -258,12 +259,12 @@ void BMP280Component::set_pressure_oversampling(BMP280Oversampling pressure_over
|
||||
void BMP280Component::set_iir_filter(BMP280IIRFilter iir_filter) { this->iir_filter_ = iir_filter; }
|
||||
uint8_t BMP280Component::read_u8_(uint8_t a_register) {
|
||||
uint8_t data = 0;
|
||||
this->read_byte(a_register, &data);
|
||||
this->bmp_read_byte(a_register, &data);
|
||||
return data;
|
||||
}
|
||||
uint16_t BMP280Component::read_u16_le_(uint8_t a_register) {
|
||||
uint16_t data = 0;
|
||||
this->read_byte_16(a_register, &data);
|
||||
this->bmp_read_byte_16(a_register, &data);
|
||||
return (data >> 8) | (data << 8);
|
||||
}
|
||||
int16_t BMP280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
|
||||
|
@@ -67,12 +67,12 @@ class BMP280Component : public PollingComponent {
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
|
||||
virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
|
||||
virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||
virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
|
||||
|
||||
protected:
|
||||
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
|
||||
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
|
||||
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||
virtual bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) = 0;
|
||||
|
||||
/// Read the temperature value and store the calculated ambient temperature in t_fine.
|
||||
float read_temperature_(int32_t *t_fine);
|
||||
/// Read the pressure value in hPa using the provided t_fine value.
|
||||
|
@@ -5,19 +5,6 @@
|
||||
namespace esphome {
|
||||
namespace bmp280_i2c {
|
||||
|
||||
bool BMP280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
|
||||
return I2CDevice::read_byte(a_register, data);
|
||||
};
|
||||
bool BMP280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
|
||||
return I2CDevice::write_byte(a_register, data);
|
||||
};
|
||||
bool BMP280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
return I2CDevice::read_bytes(a_register, data, len);
|
||||
};
|
||||
bool BMP280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
|
||||
return I2CDevice::read_byte_16(a_register, data);
|
||||
};
|
||||
|
||||
void BMP280I2CComponent::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
BMP280Component::dump_config();
|
||||
|
@@ -11,10 +11,12 @@ static const char *const TAG = "bmp280_i2c.sensor";
|
||||
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
|
||||
class BMP280I2CComponent : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
|
||||
public:
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
bool write_byte(uint8_t a_register, uint8_t data) override;
|
||||
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
|
||||
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
|
||||
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
|
||||
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
|
||||
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
|
||||
return read_bytes(a_register, data, len);
|
||||
}
|
||||
bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override { return read_byte_16(a_register, data); }
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
|
@@ -28,7 +28,7 @@ void BMP280SPIComponent::setup() {
|
||||
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
|
||||
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf
|
||||
|
||||
bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
|
||||
bool BMP280SPIComponent::bmp_read_byte(uint8_t a_register, uint8_t *data) {
|
||||
this->enable();
|
||||
this->transfer_byte(set_bit(a_register, 7));
|
||||
*data = this->transfer_byte(0);
|
||||
@@ -36,7 +36,7 @@ bool BMP280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
|
||||
bool BMP280SPIComponent::bmp_write_byte(uint8_t a_register, uint8_t data) {
|
||||
this->enable();
|
||||
this->transfer_byte(clear_bit(a_register, 7));
|
||||
this->transfer_byte(data);
|
||||
@@ -44,7 +44,7 @@ bool BMP280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
bool BMP280SPIComponent::bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
|
||||
this->enable();
|
||||
this->transfer_byte(set_bit(a_register, 7));
|
||||
this->read_array(data, len);
|
||||
@@ -52,7 +52,7 @@ bool BMP280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t le
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BMP280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
|
||||
bool BMP280SPIComponent::bmp_read_byte_16(uint8_t a_register, uint16_t *data) {
|
||||
this->enable();
|
||||
this->transfer_byte(set_bit(a_register, 7));
|
||||
((uint8_t *) data)[1] = this->transfer_byte(0);
|
||||
|
@@ -10,10 +10,10 @@ class BMP280SPIComponent : public esphome::bmp280_base::BMP280Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
|
||||
void setup() override;
|
||||
bool read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
bool write_byte(uint8_t a_register, uint8_t data) override;
|
||||
bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
|
||||
bool read_byte_16(uint8_t a_register, uint16_t *data) override;
|
||||
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override;
|
||||
bool bmp_write_byte(uint8_t a_register, uint8_t data) override;
|
||||
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
|
||||
bool bmp_read_byte_16(uint8_t a_register, uint16_t *data) override;
|
||||
};
|
||||
|
||||
} // namespace bmp280_spi
|
||||
|
@@ -17,7 +17,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
@@ -134,7 +134,6 @@ async def button_press_to_code(config, action_id, template_arg, args):
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_global(button_ns.using)
|
||||
cg.add_define("USE_BUTTON")
|
||||
|
@@ -6,6 +6,19 @@ namespace button {
|
||||
|
||||
static const char *const TAG = "button";
|
||||
|
||||
// Function implementation of LOG_BUTTON macro to reduce code size
|
||||
void log_button(const char *tag, const char *prefix, const char *type, Button *obj) {
|
||||
if (obj == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
|
||||
if (!obj->get_icon_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
void Button::press() {
|
||||
ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str());
|
||||
this->press_action();
|
||||
|
@@ -7,13 +7,10 @@
|
||||
namespace esphome {
|
||||
namespace button {
|
||||
|
||||
#define LOG_BUTTON(prefix, type, obj) \
|
||||
if ((obj) != nullptr) { \
|
||||
ESP_LOGCONFIG(TAG, "%s%s '%s'", prefix, LOG_STR_LITERAL(type), (obj)->get_name().c_str()); \
|
||||
if (!(obj)->get_icon().empty()) { \
|
||||
ESP_LOGCONFIG(TAG, "%s Icon: '%s'", prefix, (obj)->get_icon().c_str()); \
|
||||
} \
|
||||
}
|
||||
class Button;
|
||||
void log_button(const char *tag, const char *prefix, const char *type, Button *obj);
|
||||
|
||||
#define LOG_BUTTON(prefix, type, obj) log_button(TAG, prefix, LOG_STR_LITERAL(type), obj)
|
||||
|
||||
#define SUB_BUTTON(name) \
|
||||
protected: \
|
||||
|
18
esphome/components/camera/buffer.h
Normal file
18
esphome/components/camera/buffer.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstddef>
|
||||
|
||||
namespace esphome::camera {
|
||||
|
||||
/// Interface for a generic buffer that stores image data.
|
||||
class Buffer {
|
||||
public:
|
||||
/// Returns a pointer to the buffer's data.
|
||||
virtual uint8_t *get_data_buffer() = 0;
|
||||
/// Returns the length of the buffer in bytes.
|
||||
virtual size_t get_data_length() = 0;
|
||||
virtual ~Buffer() = default;
|
||||
};
|
||||
|
||||
} // namespace esphome::camera
|
20
esphome/components/camera/buffer_impl.cpp
Normal file
20
esphome/components/camera/buffer_impl.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "buffer_impl.h"
|
||||
|
||||
namespace esphome::camera {
|
||||
|
||||
BufferImpl::BufferImpl(size_t size) {
|
||||
this->data_ = this->allocator_.allocate(size);
|
||||
this->size_ = size;
|
||||
}
|
||||
|
||||
BufferImpl::BufferImpl(CameraImageSpec *spec) {
|
||||
this->data_ = this->allocator_.allocate(spec->bytes_per_image());
|
||||
this->size_ = spec->bytes_per_image();
|
||||
}
|
||||
|
||||
BufferImpl::~BufferImpl() {
|
||||
if (this->data_ != nullptr)
|
||||
this->allocator_.deallocate(this->data_, this->size_);
|
||||
}
|
||||
|
||||
} // namespace esphome::camera
|
26
esphome/components/camera/buffer_impl.h
Normal file
26
esphome/components/camera/buffer_impl.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "buffer.h"
|
||||
#include "camera.h"
|
||||
|
||||
namespace esphome::camera {
|
||||
|
||||
/// Default implementation of Buffer Interface.
|
||||
/// Uses a RAMAllocator for memory reservation.
|
||||
class BufferImpl : public Buffer {
|
||||
public:
|
||||
explicit BufferImpl(size_t size);
|
||||
explicit BufferImpl(CameraImageSpec *spec);
|
||||
// -------- Buffer --------
|
||||
uint8_t *get_data_buffer() override { return data_; }
|
||||
size_t get_data_length() override { return size_; }
|
||||
// ------------------------
|
||||
~BufferImpl() override;
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_;
|
||||
size_t size_{};
|
||||
uint8_t *data_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::camera
|
@@ -15,6 +15,26 @@ namespace camera {
|
||||
*/
|
||||
enum CameraRequester : uint8_t { IDLE, API_REQUESTER, WEB_REQUESTER };
|
||||
|
||||
/// Enumeration of different pixel formats.
|
||||
enum PixelFormat : uint8_t {
|
||||
PIXEL_FORMAT_GRAYSCALE = 0, ///< 8-bit grayscale.
|
||||
PIXEL_FORMAT_RGB565, ///< 16-bit RGB (5-6-5).
|
||||
PIXEL_FORMAT_BGR888, ///< RGB pixel data in 8-bit format, stored as B, G, R (1 byte each).
|
||||
};
|
||||
|
||||
/// Returns string name for a given PixelFormat.
|
||||
inline const char *to_string(PixelFormat format) {
|
||||
switch (format) {
|
||||
case PIXEL_FORMAT_GRAYSCALE:
|
||||
return "PIXEL_FORMAT_GRAYSCALE";
|
||||
case PIXEL_FORMAT_RGB565:
|
||||
return "PIXEL_FORMAT_RGB565";
|
||||
case PIXEL_FORMAT_BGR888:
|
||||
return "PIXEL_FORMAT_BGR888";
|
||||
}
|
||||
return "PIXEL_FORMAT_UNKNOWN";
|
||||
}
|
||||
|
||||
/** Abstract camera image base class.
|
||||
* Encapsulates the JPEG encoded data and it is shared among
|
||||
* all connected clients.
|
||||
@@ -43,6 +63,29 @@ class CameraImageReader {
|
||||
virtual ~CameraImageReader() {}
|
||||
};
|
||||
|
||||
/// Specification of a caputured camera image.
|
||||
/// This struct defines the format and size details for images captured
|
||||
/// or processed by a camera component.
|
||||
struct CameraImageSpec {
|
||||
uint16_t width;
|
||||
uint16_t height;
|
||||
PixelFormat format;
|
||||
size_t bytes_per_pixel() {
|
||||
switch (format) {
|
||||
case PIXEL_FORMAT_GRAYSCALE:
|
||||
return 1;
|
||||
case PIXEL_FORMAT_RGB565:
|
||||
return 2;
|
||||
case PIXEL_FORMAT_BGR888:
|
||||
return 3;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
size_t bytes_per_row() { return bytes_per_pixel() * width; }
|
||||
size_t bytes_per_image() { return bytes_per_pixel() * width * height; }
|
||||
};
|
||||
|
||||
/** Abstract camera base class. Collaborates with API.
|
||||
* 1) API server starts and installs callback (add_image_callback)
|
||||
* which is called by the camera when a new image is available.
|
||||
|
69
esphome/components/camera/encoder.h
Normal file
69
esphome/components/camera/encoder.h
Normal file
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include "buffer.h"
|
||||
#include "camera.h"
|
||||
|
||||
namespace esphome::camera {
|
||||
|
||||
/// Result codes from the encoder used to control camera pipeline flow.
|
||||
enum EncoderError : uint8_t {
|
||||
ENCODER_ERROR_SUCCESS = 0, ///< Encoding succeeded, continue pipeline normally.
|
||||
ENCODER_ERROR_SKIP_FRAME, ///< Skip current frame, try again on next frame.
|
||||
ENCODER_ERROR_RETRY_FRAME, ///< Retry current frame, after buffer growth or for incremental encoding.
|
||||
ENCODER_ERROR_CONFIGURATION ///< Fatal config error, shut down pipeline.
|
||||
};
|
||||
|
||||
/// Converts EncoderError to string.
|
||||
inline const char *to_string(EncoderError error) {
|
||||
switch (error) {
|
||||
case ENCODER_ERROR_SUCCESS:
|
||||
return "ENCODER_ERROR_SUCCESS";
|
||||
case ENCODER_ERROR_SKIP_FRAME:
|
||||
return "ENCODER_ERROR_SKIP_FRAME";
|
||||
case ENCODER_ERROR_RETRY_FRAME:
|
||||
return "ENCODER_ERROR_RETRY_FRAME";
|
||||
case ENCODER_ERROR_CONFIGURATION:
|
||||
return "ENCODER_ERROR_CONFIGURATION";
|
||||
}
|
||||
return "ENCODER_ERROR_INVALID";
|
||||
}
|
||||
|
||||
/// Interface for an encoder buffer supporting resizing and variable-length data.
|
||||
class EncoderBuffer {
|
||||
public:
|
||||
/// Sets logical buffer size, reallocates if needed.
|
||||
/// @param size Required size in bytes.
|
||||
/// @return true on success, false on allocation failure.
|
||||
virtual bool set_buffer_size(size_t size) = 0;
|
||||
|
||||
/// Returns a pointer to the buffer data.
|
||||
virtual uint8_t *get_data() const = 0;
|
||||
|
||||
/// Returns number of bytes currently used.
|
||||
virtual size_t get_size() const = 0;
|
||||
|
||||
/// Returns total allocated buffer size.
|
||||
virtual size_t get_max_size() const = 0;
|
||||
|
||||
virtual ~EncoderBuffer() = default;
|
||||
};
|
||||
|
||||
/// Interface for image encoders used in a camera pipeline.
|
||||
class Encoder {
|
||||
public:
|
||||
/// Encodes pixel data from a previous camera pipeline stage.
|
||||
/// @param spec Specification of the input pixel data.
|
||||
/// @param pixels Image pixels in RGB or grayscale format, as specified in @p spec.
|
||||
/// @return EncoderError Indicating the result of the encoding operation.
|
||||
virtual EncoderError encode_pixels(CameraImageSpec *spec, Buffer *pixels) = 0;
|
||||
|
||||
/// Returns the encoder's output buffer.
|
||||
/// @return Pointer to an EncoderBuffer containing encoded data.
|
||||
virtual EncoderBuffer *get_output_buffer() = 0;
|
||||
|
||||
/// Prints the encoder's configuration to the log.
|
||||
virtual void dump_config() = 0;
|
||||
virtual ~Encoder() = default;
|
||||
};
|
||||
|
||||
} // namespace esphome::camera
|
62
esphome/components/camera_encoder/__init__.py
Normal file
62
esphome/components/camera_encoder/__init__.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TYPE
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@DT-art1"]
|
||||
|
||||
AUTO_LOAD = ["camera"]
|
||||
|
||||
CONF_BUFFER_EXPAND_SIZE = "buffer_expand_size"
|
||||
CONF_ENCODER_BUFFER_ID = "encoder_buffer_id"
|
||||
CONF_QUALITY = "quality"
|
||||
|
||||
ESP32_CAMERA_ENCODER = "esp32_camera"
|
||||
|
||||
camera_ns = cg.esphome_ns.namespace("camera")
|
||||
camera_encoder_ns = cg.esphome_ns.namespace("camera_encoder")
|
||||
|
||||
Encoder = camera_ns.class_("Encoder")
|
||||
EncoderBufferImpl = camera_encoder_ns.class_("EncoderBufferImpl")
|
||||
|
||||
ESP32CameraJPEGEncoder = camera_encoder_ns.class_("ESP32CameraJPEGEncoder", Encoder)
|
||||
|
||||
MAX_JPEG_BUFFER_SIZE_2MB = 2 * 1024 * 1024
|
||||
|
||||
ESP32_CAMERA_ENCODER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESP32CameraJPEGEncoder),
|
||||
cv.Optional(CONF_QUALITY, default=80): cv.int_range(1, 100),
|
||||
cv.Optional(CONF_BUFFER_SIZE, default=4096): cv.int_range(
|
||||
1024, MAX_JPEG_BUFFER_SIZE_2MB
|
||||
),
|
||||
cv.Optional(CONF_BUFFER_EXPAND_SIZE, default=1024): cv.int_range(
|
||||
0, MAX_JPEG_BUFFER_SIZE_2MB
|
||||
),
|
||||
cv.GenerateID(CONF_ENCODER_BUFFER_ID): cv.declare_id(EncoderBufferImpl),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
ESP32_CAMERA_ENCODER: ESP32_CAMERA_ENCODER_SCHEMA,
|
||||
},
|
||||
default_type=ESP32_CAMERA_ENCODER,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
||||
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.0")
|
||||
cg.add_build_flag("-DUSE_ESP32_CAMERA_JPEG_ENCODER")
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_QUALITY],
|
||||
buffer,
|
||||
)
|
||||
cg.add(var.set_buffer_expand_size(config[CONF_BUFFER_EXPAND_SIZE]))
|
23
esphome/components/camera_encoder/encoder_buffer_impl.cpp
Normal file
23
esphome/components/camera_encoder/encoder_buffer_impl.cpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "encoder_buffer_impl.h"
|
||||
|
||||
namespace esphome::camera_encoder {
|
||||
|
||||
bool EncoderBufferImpl::set_buffer_size(size_t size) {
|
||||
if (size > this->capacity_) {
|
||||
uint8_t *p = this->allocator_.reallocate(this->data_, size);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
|
||||
this->data_ = p;
|
||||
this->capacity_ = size;
|
||||
}
|
||||
this->size_ = size;
|
||||
return true;
|
||||
}
|
||||
|
||||
EncoderBufferImpl::~EncoderBufferImpl() {
|
||||
if (this->data_ != nullptr)
|
||||
this->allocator_.deallocate(this->data_, this->capacity_);
|
||||
}
|
||||
|
||||
} // namespace esphome::camera_encoder
|
25
esphome/components/camera_encoder/encoder_buffer_impl.h
Normal file
25
esphome/components/camera_encoder/encoder_buffer_impl.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/camera/encoder.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::camera_encoder {
|
||||
|
||||
class EncoderBufferImpl : public camera::EncoderBuffer {
|
||||
public:
|
||||
// --- EncoderBuffer ---
|
||||
bool set_buffer_size(size_t size) override;
|
||||
uint8_t *get_data() const override { return this->data_; }
|
||||
size_t get_size() const override { return this->size_; }
|
||||
size_t get_max_size() const override { return this->capacity_; }
|
||||
// ----------------------
|
||||
~EncoderBufferImpl() override;
|
||||
|
||||
protected:
|
||||
RAMAllocator<uint8_t> allocator_;
|
||||
size_t capacity_{};
|
||||
size_t size_{};
|
||||
uint8_t *data_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::camera_encoder
|
@@ -0,0 +1,82 @@
|
||||
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
|
||||
|
||||
#include "esp32_camera_jpeg_encoder.h"
|
||||
|
||||
namespace esphome::camera_encoder {
|
||||
|
||||
static const char *const TAG = "camera_encoder";
|
||||
|
||||
ESP32CameraJPEGEncoder::ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output) {
|
||||
this->quality_ = quality;
|
||||
this->output_ = output;
|
||||
}
|
||||
|
||||
camera::EncoderError ESP32CameraJPEGEncoder::encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) {
|
||||
this->bytes_written_ = 0;
|
||||
this->out_of_output_memory_ = false;
|
||||
bool success = fmt2jpg_cb(pixels->get_data_buffer(), pixels->get_data_length(), spec->width, spec->height,
|
||||
to_internal_(spec->format), this->quality_, callback_, this);
|
||||
|
||||
if (!success)
|
||||
return camera::ENCODER_ERROR_CONFIGURATION;
|
||||
|
||||
if (this->out_of_output_memory_) {
|
||||
if (this->buffer_expand_size_ <= 0)
|
||||
return camera::ENCODER_ERROR_SKIP_FRAME;
|
||||
|
||||
size_t current_size = this->output_->get_max_size();
|
||||
size_t new_size = this->output_->get_max_size() + this->buffer_expand_size_;
|
||||
if (!this->output_->set_buffer_size(new_size)) {
|
||||
ESP_LOGE(TAG, "Failed to expand output buffer.");
|
||||
this->buffer_expand_size_ = 0;
|
||||
return camera::ENCODER_ERROR_SKIP_FRAME;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Output buffer expanded (%u -> %u).", current_size, this->output_->get_max_size());
|
||||
return camera::ENCODER_ERROR_RETRY_FRAME;
|
||||
}
|
||||
|
||||
this->output_->set_buffer_size(this->bytes_written_);
|
||||
return camera::ENCODER_ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
void ESP32CameraJPEGEncoder::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ESP32 Camera JPEG Encoder:\n"
|
||||
" Size: %zu\n"
|
||||
" Quality: %d\n"
|
||||
" Expand: %d\n",
|
||||
this->output_->get_max_size(), this->quality_, this->buffer_expand_size_);
|
||||
}
|
||||
|
||||
size_t ESP32CameraJPEGEncoder::callback_(void *arg, size_t index, const void *data, size_t len) {
|
||||
ESP32CameraJPEGEncoder *that = reinterpret_cast<ESP32CameraJPEGEncoder *>(arg);
|
||||
uint8_t *buffer = that->output_->get_data();
|
||||
size_t buffer_length = that->output_->get_max_size();
|
||||
if (index + len > buffer_length) {
|
||||
that->out_of_output_memory_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::memcpy(&buffer[index], data, len);
|
||||
that->bytes_written_ += len;
|
||||
return len;
|
||||
}
|
||||
|
||||
pixformat_t ESP32CameraJPEGEncoder::to_internal_(camera::PixelFormat format) {
|
||||
switch (format) {
|
||||
case camera::PIXEL_FORMAT_GRAYSCALE:
|
||||
return PIXFORMAT_GRAYSCALE;
|
||||
case camera::PIXEL_FORMAT_RGB565:
|
||||
return PIXFORMAT_RGB565;
|
||||
// Internal representation for RGB is in byte order: B, G, R
|
||||
case camera::PIXEL_FORMAT_BGR888:
|
||||
return PIXFORMAT_RGB888;
|
||||
}
|
||||
|
||||
return PIXFORMAT_GRAYSCALE;
|
||||
}
|
||||
|
||||
} // namespace esphome::camera_encoder
|
||||
|
||||
#endif
|
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32_CAMERA_JPEG_ENCODER
|
||||
|
||||
#include <esp_camera.h>
|
||||
|
||||
#include "esphome/components/camera/encoder.h"
|
||||
|
||||
namespace esphome::camera_encoder {
|
||||
|
||||
/// Encoder that uses the software-based JPEG implementation from Espressif's esp32-camera component.
|
||||
class ESP32CameraJPEGEncoder : public camera::Encoder {
|
||||
public:
|
||||
/// Constructs a ESP32CameraJPEGEncoder instance.
|
||||
/// @param quality Sets the quality of the encoded image (1-100).
|
||||
/// @param output Pointer to preallocated output buffer.
|
||||
ESP32CameraJPEGEncoder(uint8_t quality, camera::EncoderBuffer *output);
|
||||
/// Sets the number of bytes to expand the output buffer on underflow during encoding.
|
||||
/// @param buffer_expand_size Number of bytes to expand the buffer.
|
||||
void set_buffer_expand_size(size_t buffer_expand_size) { this->buffer_expand_size_ = buffer_expand_size; }
|
||||
// -------- Encoder --------
|
||||
camera::EncoderError encode_pixels(camera::CameraImageSpec *spec, camera::Buffer *pixels) override;
|
||||
camera::EncoderBuffer *get_output_buffer() override { return output_; }
|
||||
void dump_config() override;
|
||||
// -------------------------
|
||||
protected:
|
||||
static size_t callback_(void *arg, size_t index, const void *data, size_t len);
|
||||
pixformat_t to_internal_(camera::PixelFormat format);
|
||||
|
||||
camera::EncoderBuffer *output_{};
|
||||
size_t buffer_expand_size_{};
|
||||
size_t bytes_written_{};
|
||||
uint8_t quality_{};
|
||||
bool out_of_output_memory_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::camera_encoder
|
||||
|
||||
#endif
|
@@ -10,11 +10,11 @@ from esphome.const import (
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
|
||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
|
||||
DEPENDENCIES = ["wifi"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
captive_portal_ns = cg.esphome_ns.namespace("captive_portal")
|
||||
CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component)
|
||||
@@ -40,7 +40,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(64.0)
|
||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
|
||||
async def to_code(config):
|
||||
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
|
||||
|
||||
|
@@ -153,8 +153,8 @@ void CCS811Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CCS811");
|
||||
LOG_I2C_DEVICE(this)
|
||||
LOG_UPDATE_INTERVAL(this)
|
||||
LOG_SENSOR(" ", "CO2 Sensor", this->co2_)
|
||||
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_)
|
||||
LOG_SENSOR(" ", "CO2 Sensor", this->co2_);
|
||||
LOG_SENSOR(" ", "TVOC Sensor", this->tvoc_);
|
||||
LOG_TEXT_SENSOR(" ", "Firmware Version Sensor", this->version_)
|
||||
if (this->baseline_) {
|
||||
ESP_LOGCONFIG(TAG, " Baseline: %04X", *this->baseline_);
|
||||
|
@@ -91,7 +91,7 @@ bool CH422GComponent::read_inputs_() {
|
||||
|
||||
// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address.
|
||||
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
|
||||
auto err = this->bus_->write(reg, &value, 1);
|
||||
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
|
||||
return false;
|
||||
@@ -102,7 +102,7 @@ bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
|
||||
|
||||
uint8_t CH422GComponent::read_reg_(uint8_t reg) {
|
||||
uint8_t value;
|
||||
auto err = this->bus_->read(reg, &value, 1);
|
||||
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
|
||||
return 0;
|
||||
|
@@ -47,7 +47,7 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
|
||||
@@ -517,7 +517,6 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_CLIMATE")
|
||||
cg.add_global(climate_ns.using)
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user