Compare commits

...

124 Commits

Author SHA1 Message Date
J. Nick Koston
c2968e64d1 Merge branch 'esp32_cam_init_order' into frame_helper_optimize_cleanup_api 2025-05-15 16:51:59 -05:00
J. Nick Koston
db9a070c42 Fix ESP32 Camera class inheritance
panic was introduced in bb1f24ab43

Fix ESP32 Camera crash by reordering parent class inheritance to match other entity components. The crash occurred because `EntityBase` methods were accessed before initialization when Component was the first parent. Changing ESP32Camera to inherit first from EntityBase ensures proper memory layout and initialization order, consistent with all other entities in the codebase.

```
[14:39:21][D][binary_sensor:032]: 'esp-cam status': Sending state ON
[14:39:21][D][api.connection:1527]: Home Assistant 2025.6.0.dev0 (192.168.209.64): Connected successfully
[14:39:21]Guru Meditation Error: Core  1 panic'ed (LoadProhibited). Exception was unhandled.
[14:39:21]
[14:39:21]Core  1 register dump:
[14:39:21]PC      : 0x4008aa94  PS      : 0x00060d30  A0      : 0x800d40eb  A1      : 0x3ffc06a0
WARNING Decoded 0x4008aa94: strlen at /Users/brnomac003/.gitlab-runner/builds/qR2TxTby/0/idf/crosstool-NG/.build/xtensa-esp32-elf/src/newlib/newlib/libc/machine/xtensa/strlen.S:46
[14:39:21]A2      : 0x00000027  A3      : 0x00000023  A4      : 0x000000ff  A5      : 0x0000ff00
[14:39:21]A6      : 0x00ff0000  A7      : 0xff000000  A8      : 0x800d4982  A9      : 0x3ffc0670
[14:39:21]A10     : 0x3ffc0728  A11     : 0x3ffc07f0  A12     : 0x00000006  A13     : 0x00000000
[14:39:21]A14     : 0x3ffb5e64  A15     : 0x00000000  SAR     : 0x00000019  EXCCAUSE: 0x0000001c
[14:39:21]EXCVADDR: 0x00000027  LBEG    : 0x4008a350  LEND    : 0x4008a36c  LCOUNT  : 0xffffffff
[14:39:21]
[14:39:21]
[14:39:21]Backtrace: 0x4008aa91:0x3ffc06a0 0x400d40e8:0x3ffc06b0 0x400e64ea:0x3ffc06d0 0x400d4b2d:0x3ffc0710 0x400d5e0e:0x3ffc0760 0x400d47df:0x3ffc0830 0x400d484a:0x3ffc0870 0x400da2cd:0x3ffc08a0 0x400e6319:0x3ffc08c0 0x400d5134:0x3ffc08e0
0x400da244:0x3ffc0980 0x401669e5:0x3ffc0a60 0x40166a9d:0x3ffc0a80 0x400e4c1f:0x3ffc0aa0 0x400e7446:0x3ffc0ad0 0x400da78a:0x3ffc0af0
WARNING Found stack trace! Trying to decode it
WARNING Decoded 0x4008aa91: strlen at /Users/brnomac003/.gitlab-runner/builds/qR2TxTby/0/idf/crosstool-NG/.build/xtensa-esp32-elf/src/newlib/newlib/libc/machine/xtensa/strlen.S:43
WARNING Decoded 0x400d40e8: std::char_traits<char>::length(char const*) at /Users/bdraco/.platformio/packages/toolchain-xtensa-esp32/xtensa-esp32-elf/include/c++/12.2.0/bits/char_traits.h:395
 (inlined by) std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string<std::allocator<char> >(char const*, std::allocator<char> const&) at
/Users/bdraco/.platformio/packages/toolchain-xtensa-esp32/xtensa-esp32-elf/include/c++/12.2.0/bits/basic_string.h:641
WARNING Decoded 0x400e64ea: esphome::EntityBase::get_object_id[abi:cxx11]() const at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/core/entity_base.cpp:53
WARNING Decoded 0x400d4b2d: esphome::api::get_default_unique_id(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, esphome::EntityBase*) at
/Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/api_connection.cpp:256
WARNING Decoded 0x400d5e0e: esphome::api::APIConnection::try_send_camera_info_(esphome::esp32_camera::ESP32Camera*) at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/api_connection.cpp:1139 (discriminator 1)
WARNING Decoded 0x400d47df: esphome::api::APIConnection::send_info_(esphome::EntityBase*, bool (esphome::api::APIConnection::*)(void*)) at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/api_connection.h:478
(discriminator 6)
WARNING Decoded 0x400d484a: esphome::api::APIConnection::send_camera_info(esphome::esp32_camera::ESP32Camera*) at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/api_connection.cpp:1134 (discriminator 4)
WARNING Decoded 0x400da2cd: esphome::api::ListEntitiesIterator::on_camera(esphome::esp32_camera::ESP32Camera*) at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/list_entities.cpp:81
WARNING Decoded 0x400e6319: esphome::ComponentIterator::advance() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/core/component_iterator.cpp:170
WARNING Decoded 0x400d5134: esphome::api::APIConnection::loop() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/api_connection.cpp:163
WARNING Decoded 0x400da244: esphome::api::APIServer::loop() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/api/api_server.cpp:148
WARNING Decoded 0x401669e5: esphome::Component::call_loop() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/core/component.cpp:80
WARNING Decoded 0x40166a9d: esphome::Component::call() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/core/component.cpp:107
WARNING Decoded 0x400e4c1f: esphome::Application::loop() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/core/application.cpp:75
WARNING Decoded 0x400e7446: loop() at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/cam-utilities-indoor.yaml:54
WARNING Decoded 0x400da78a: esphome::loop_task(void*) at /Users/bdraco/esphome/.esphome/build/cam-utilities-indoor/src/esphome/components/esp32/core.cpp:79 (discriminator 1)
```
2025-05-15 15:24:29 -05:00
J. Nick Koston
4bff8ef969 Revert "Bail quickly if there is no data to read"
This reverts commit e544f6711e.
2025-05-15 13:43:28 -05:00
J. Nick Koston
9ff93142fd Revert "FIONREAD"
This reverts commit 2201f67045.
2025-05-15 13:42:49 -05:00
J. Nick Koston
a4c5384b30 Revert "preen"
This reverts commit b5af2cb4ee.
2025-05-15 13:42:48 -05:00
J. Nick Koston
b5af2cb4ee preen 2025-05-15 13:32:12 -05:00
J. Nick Koston
29b3d7355c Merge branch 'bail_no_data' into frame_helper_optimize_cleanup_api 2025-05-15 13:27:19 -05:00
J. Nick Koston
2201f67045 FIONREAD 2025-05-15 13:26:59 -05:00
J. Nick Koston
7a364ff63a cleanup 2025-05-15 12:54:34 -05:00
J. Nick Koston
e544f6711e Bail quickly if there is no data to read 2025-05-15 12:54:34 -05:00
J. Nick Koston
31f5bbf623 cleanup 2025-05-15 12:52:25 -05:00
J. Nick Koston
488dc40f2e Bail quickly if there is no data to read 2025-05-15 12:48:33 -05:00
J. Nick Koston
e8e0e34702 Merge branch 'multi_task_logger_esp32' into frame_helper_optimize_cleanup_api 2025-05-15 11:02:49 -05:00
J. Nick Koston
eebdc9c38f Fix ESP32 console logging corruption and message loss in multi-task environments
These changes enhance ESPHome's logging system on ESP32 multi-task environments:

1. **Emergency Console Logging**:
   - Added fallback console logging when the task log buffer is full or disabled
   - Ensures critical messages are still visible even when the ring buffer fails

2. **Improved Console Output**:
   - Messages successfully sent to the ring buffer now also display on the console
   - Ensures consistent console output for all log messages regardless of source

3. **Optimized Resource Usage**:
   - Release ring buffer messages earlier after transferring to tx_buffer
   - Reduces contention for the shared log buffer in multi-task environments

1. **Stack Memory Efficiency**:
   - No longer need to allocate stack memory for console output when ring buffer is available
   - Only uses stack memory for emergency fallback cases, reducing stack usage in normal operation

2. **Console Output Integrity**:
   - Prevents console output corruption that could occur with concurrent writes from multiple tasks
   - Serializes all console output through the main loop when possible

3. **Message Ordering**:
   - Messages from different tasks may appear slightly out of order due to async delivery to main loop
   - This trade-off is preferable to corrupted console output from concurrent writes

These improvements provide more reliable logging behavior, particularly under memory constraints or high logging volume, while maintaining thread safety and minimizing resource contention.
2025-05-15 10:52:58 -05:00
Thomas Rupprecht
4761ffe023 [gps] update lib, improve code/tests/config (#8768) 2025-05-15 22:07:41 +12:00
Thomas Rupprecht
88edddf07a [log] improve/refactor log (#8708) 2025-05-15 21:45:07 +12:00
J. Nick Koston
0b77cb1d16 Logger Recursion Guard per Task on ESP32 (#8765) 2025-05-15 21:36:28 +12:00
J. Nick Koston
65b6d256bc Merge branch 'implement_buffer_queue' into frame_helper_optimize_cleanup_api 2025-05-15 03:52:34 -05:00
J. Nick Koston
428371d685 preen 2025-05-15 03:51:08 -05:00
J. Nick Koston
d108219947 dry 2025-05-15 03:49:51 -05:00
J. Nick Koston
cdf3ed07ba dry 2025-05-15 03:47:10 -05:00
J. Nick Koston
061bbabd09 Merge branch 'implement_buffer_queue' into frame_helper_optimize_cleanup_api 2025-05-15 03:25:02 -05:00
J. Nick Koston
2646ec166b save some bytes 2025-05-15 03:24:47 -05:00
J. Nick Koston
bafc57f02e fix refactoring error 2025-05-15 03:06:52 -05:00
J. Nick Koston
549ed6178b fix refactoring error 2025-05-15 03:06:36 -05:00
J. Nick Koston
0c67e06573 Merge branch 'implement_buffer_queue' into frame_helper_optimize_cleanup_api 2025-05-15 03:02:16 -05:00
J. Nick Koston
592a95c565 dry 2025-05-15 02:58:15 -05:00
J. Nick Koston
f41ef68b41 dry 2025-05-15 02:55:00 -05:00
J. Nick Koston
26af1cf650 preen 2025-05-15 02:32:19 -05:00
J. Nick Koston
66b995cffe cleanups 2025-05-15 02:13:20 -05:00
J. Nick Koston
5d0b74db3d Merge branch 'implement_buffer_queue' into frame_helper_optimize_cleanup_api 2025-05-15 01:54:52 -05:00
J. Nick Koston
31e3065600 fixes 2025-05-15 01:49:38 -05:00
J. Nick Koston
872a70d235 fixes 2025-05-15 01:48:43 -05:00
J. Nick Koston
7b84eb2903 fixes 2025-05-15 01:47:18 -05:00
J. Nick Koston
7b4e7108c0 cleanup 2025-05-15 01:45:24 -05:00
J. Nick Koston
d4b42ebf20 Eliminate outbound buffer expensive O(n) with O(1) queue operations
- Replaced inefficient vector-based buffer with a queue of discrete message buffers

- Moved common code to base class to reduce duplication

- Removed unnecessary data copying after partial sends

- Added small-buffer optimization to allow writing with backlog <256 bytes

- Moved common code to base class to reduce duplication
2025-05-15 01:31:43 -05:00
J. Nick Koston
a8e9c79975 Eliminate outbound buffer expensive O(n) with O(1) queue operations
- Replaced inefficient vector-based buffer with a queue of discrete message buffers

- Moved common code to base class to reduce duplication

- Removed unnecessary data copying after partial sends

- Added small-buffer optimization to allow writing with backlog <256 bytes

- Moved common code to base class to reduce duplication
2025-05-15 01:31:22 -05:00
J. Nick Koston
efa6745a5e Optimize protobuf varint decoder for ESPHome use case (#8791) 2025-05-15 17:16:25 +12:00
J. Nick Koston
dd8d8ad952 Use fixed buffer for plaintext protocol like noise protocol (#8800) 2025-05-15 17:16:08 +12:00
dependabot[bot]
57284b1ac3 Bump cairosvg from 2.8.0 to 2.8.1 (#8799)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 23:26:28 -05:00
J. Nick Koston
47b9c3db1d adjust 2025-05-14 23:09:18 -05:00
J. Nick Koston
b32ed848b0 Merge remote-tracking branch 'upstream/dev' into frame_helper_optimize_cleanup_api 2025-05-14 23:08:09 -05:00
Jesse Hills
1a651ce66d Update some sensor schemas to be Optional (#8803) 2025-05-15 02:40:11 +00:00
Jesse Hills
730441c120 [api] Update api proto to add legacy value (#8802) 2025-05-14 21:26:21 -05:00
J. Nick Koston
bb1f24ab43 Avoid protobuf message construction when tx buffer is full (#8787)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-14 21:25:44 -05:00
J. Nick Koston
8fb68804e4 Merge branch 'cleanup_api' into frame_helper_optimize_cleanup_api 2025-05-14 17:28:29 -05:00
J. Nick Koston
e2453dd513 optimize 2025-05-14 17:27:05 -05:00
J. Nick Koston
ed563b0c83 Merge branch 'cleanup_api' into frame_helper_optimize_cleanup_api 2025-05-14 17:20:38 -05:00
J. Nick Koston
856d679ce2 Merge branch 'plaintext' into frame_helper_optimize 2025-05-14 16:58:59 -05:00
J. Nick Koston
f5ac77634b Use fixed buffer for plaintext protocol like noise protocol
This PR optimizes memory usage in the APIPlaintextFrameHelper by replacing the vector-based header buffer with a fixed-size buffer, similar to what's already done in the noise protocol implementation.

- Replaced `std::vector<uint8_t> rx_header_buf_` with a 5-byte fixed buffer `uint8_t rx_header_buf_[5]`
- Implemented a modified protocol parsing algorithm that validates the indicator byte without storing it
- Ensured compatibility with noise protocol by supporting message sizes up to at least 65535 (requires 3-byte varint)
- Improved readability with clearer conditional logic and better comments
- Added detailed notes about protocol limitations and variable length encoding

- Reduces memory fragmentation by eliminating dynamic allocations during header parsing
2025-05-14 16:48:12 -05:00
J. Nick Koston
b30d7fb0eb Use fixed buffer for plaintext protocol like noise protocol
This PR optimizes memory usage in the APIPlaintextFrameHelper by replacing the vector-based header buffer with a fixed-size buffer, similar to what's already done in the noise protocol implementation.

- Replaced `std::vector<uint8_t> rx_header_buf_` with a 5-byte fixed buffer `uint8_t rx_header_buf_[5]`
- Implemented a modified protocol parsing algorithm that validates the indicator byte without storing it
- Ensured compatibility with noise protocol by supporting message sizes up to at least 65535 (requires 3-byte varint)
- Improved readability with clearer conditional logic and better comments
- Added detailed notes about protocol limitations and variable length encoding

- Reduces memory fragmentation by eliminating dynamic allocations during header parsing
2025-05-14 16:47:26 -05:00
NP v/d Spek
edb8d187be add actions to the MAX7219Component (#6462)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-15 09:15:04 +12:00
J. Nick Koston
fc609f02f3 reset 2025-05-14 15:19:07 -05:00
J. Nick Koston
3fbbec81af frame helper opt 2025-05-14 14:26:05 -05:00
Jesse Hills
e7b6081c5c Merge branch 'beta' into dev 2025-05-15 06:51:16 +12:00
J. Nick Koston
7934618c9c debug 2025-05-14 12:40:40 -05:00
J. Nick Koston
c4aee545c3 cleanup 2025-05-14 12:38:07 -05:00
dependabot[bot]
5454500024 Bump cairosvg from 2.7.1 to 2.8.0 (#8780)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 11:20:22 -05:00
Jesse Hills
191afd3e69 Bump esphome-dashboard to 20250514.0 (#8790) 2025-05-14 02:08:16 -05:00
Jesse Hills
de27ce79dc [climate] Update components to use `climate_schema(...)` (#8788) 2025-05-14 05:36:21 +00:00
J. Nick Koston
e244b71802 Merge remote-tracking branch 'upstream/cleanup_api' into cleanup_api 2025-05-13 23:49:05 -05:00
J. Nick Koston
2f078d4edf no need to loop twice 2025-05-13 23:48:48 -05:00
Jesse Hills
a12bd78ceb Fix release to pypi (#8789) 2025-05-14 16:35:30 +12:00
J. Nick Koston
ddb986b4fa Improve batching of BLE advertisements for better airtime efficiency (#8778) 2025-05-14 04:34:33 +00:00
J. Nick Koston
4215cc5e6a Update esphome/components/api/api_connection.h 2025-05-13 23:20:12 -05:00
J. Nick Koston
b3911ef37c lint 2025-05-13 23:19:25 -05:00
J. Nick Koston
0d1dae175c Merge remote-tracking branch 'upstream/cleanup_api' into cleanup_api 2025-05-13 23:10:43 -05:00
J. Nick Koston
6e95ef06e0 preen 2025-05-13 23:10:29 -05:00
J. Nick Koston
d7311b048b Merge branch 'dev' into cleanup_api 2025-05-13 23:01:55 -05:00
J. Nick Koston
84a84e769b balance 2025-05-13 22:59:52 -05:00
J. Nick Koston
0db37ddf0a balance 2025-05-13 22:59:18 -05:00
Jesse Hills
c98c78e368 Merge branch 'beta' into dev 2025-05-14 15:55:25 +12:00
J. Nick Koston
71577cf6d4 reduce 2025-05-13 22:37:00 -05:00
J. Nick Koston
8c0546b535 reduce 2025-05-13 22:32:40 -05:00
J. Nick Koston
9bf527b0b6 reduce 2025-05-13 22:28:51 -05:00
J. Nick Koston
c7501911bf reduce 2025-05-13 22:26:47 -05:00
J. Nick Koston
4b82ed5b81 revert 2025-05-13 22:12:12 -05:00
J. Nick Koston
1f8ae120d4 revert 2025-05-13 22:10:45 -05:00
J. Nick Koston
8769ddcfa9 revert 2025-05-13 22:04:23 -05:00
J. Nick Koston
3987b98044 remove 2025-05-13 22:03:17 -05:00
J. Nick Koston
0edfa4746a revert 2025-05-13 22:00:48 -05:00
dependabot[bot]
5570a788fd Bump aioesphomeapi from 30.2.0 to 31.0.0 (#8779)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 21:23:32 -05:00
J. Nick Koston
c8dcebfb3f cleanup 2025-05-13 21:22:30 -05:00
J. Nick Koston
edd755323c true 2025-05-13 21:11:19 -05:00
J. Nick Koston
4096c943cc cleanup 2025-05-13 21:08:59 -05:00
J. Nick Koston
e4caef77fc cleanup 2025-05-13 21:07:55 -05:00
J. Nick Koston
0d81306481 cleanup 2025-05-13 21:07:10 -05:00
J. Nick Koston
39abbe609a cleanup 2025-05-13 21:05:26 -05:00
J. Nick Koston
326df5752f cleanup 2025-05-13 21:04:46 -05:00
J. Nick Koston
7cf1db1382 cleanup 2025-05-13 21:04:07 -05:00
J. Nick Koston
dcdc2a30c5 fixes 2025-05-13 20:50:49 -05:00
J. Nick Koston
c0b9f6407c reduce diff 2025-05-13 20:45:09 -05:00
J. Nick Koston
85457eeed0 preen 2025-05-13 20:43:03 -05:00
J. Nick Koston
3fb10037a9 Refactor api_connection to avoid constructing protobuf messages if the tx_buffer is full
Also do not try to dequeue if the buffer is full
2025-05-13 20:36:57 -05:00
Jesse Hills
42c355e6d7 [fan] Update components to use `fan_schema(...)` (#8786) 2025-05-13 20:30:11 -05:00
Jesse Hills
a835ab48bc [schema] Get component name if available for deprecation warning (#8785) 2025-05-13 20:25:21 -05:00
Jesse Hills
f28a373898 [media_player] Deprecate `MEDIA_PLAYER_SCHEMA` (#8784) 2025-05-13 23:48:54 +00:00
Jesse Hills
28e29efd98 Bump version to 2025.6.0-dev 2025-05-14 09:54:26 +12:00
J. Nick Koston
45e08ed584 Merge branch 'improve_ble_batching' into loop_runtime_stats_ble_batching 2025-05-13 11:41:30 -05:00
J. Nick Koston
a7449dce92 Improve batching of BLE advertisements for better airtime efficiency 2025-05-13 11:31:48 -05:00
J. Nick Koston
8067caf16f preen 2025-05-13 11:30:26 -05:00
J. Nick Koston
5fbb066ee7 preen 2025-05-13 11:30:06 -05:00
J. Nick Koston
c9680a1ccb preen 2025-05-13 11:29:22 -05:00
J. Nick Koston
03399e6dd6 preen 2025-05-13 11:22:33 -05:00
J. Nick Koston
7f838ece00 preen 2025-05-13 11:22:05 -05:00
J. Nick Koston
3f87010c0e preen 2025-05-13 11:21:32 -05:00
J. Nick Koston
a960d9966d preen 2025-05-13 03:55:08 -05:00
J. Nick Koston
02c390c6c3 tweak 2025-05-13 03:51:22 -05:00
J. Nick Koston
eebefdf026 preen 2025-05-13 03:38:42 -05:00
J. Nick Koston
cb748bbb02 preen 2025-05-13 03:32:57 -05:00
J. Nick Koston
c35db19995 preen 2025-05-13 03:30:19 -05:00
J. Nick Koston
71b493bd8b its too much 2025-05-13 03:25:49 -05:00
J. Nick Koston
f67e02c653 its too much 2025-05-13 03:24:48 -05:00
J. Nick Koston
9db52b17f2 its too much 2025-05-13 03:24:36 -05:00
J. Nick Koston
d728382542 its too much 2025-05-13 03:24:07 -05:00
J. Nick Koston
d95bbfc6c4 its too much 2025-05-13 03:00:38 -05:00
J. Nick Koston
83db3eddd9 revert ota 2025-05-13 01:07:43 -05:00
J. Nick Koston
cc2c5a544e revert ota 2025-05-13 01:07:38 -05:00
J. Nick Koston
8fba8c2800 revert ota 2025-05-13 01:05:37 -05:00
J. Nick Koston
51d1da8460 revert ota 2025-05-13 01:04:09 -05:00
J. Nick Koston
2f1257056d revert 2025-05-13 01:02:00 -05:00
J. Nick Koston
2f8f6967bf fix ota 2025-05-13 00:55:19 -05:00
J. Nick Koston
246527e618 runtime stats 2025-05-13 00:54:05 -05:00
J. Nick Koston
3857cc9c83 runtime stats 2025-05-13 00:51:14 -05:00
48 changed files with 1934 additions and 1251 deletions

View File

@@ -169,7 +169,7 @@ esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle
esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers

View File

@@ -43,7 +43,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 Fore, color, setup_log
from esphome.log import AnsiFore, color, setup_log
from esphome.util import (
get_serial_ports,
list_yaml_files,
@@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
raise ValueError
break
except ValueError:
safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
return options[opt - 1][1]
@@ -596,30 +596,30 @@ def command_update_all(args):
click.echo(f"{half_line}{middle_text}{half_line}")
for f in files:
print(f"Updating {color(Fore.CYAN, f)}")
print(f"Updating {color(AnsiFore.CYAN, f)}")
print("-" * twidth)
print()
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
if rc == 0:
print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
success[f] = True
else:
print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
success[f] = False
print()
print()
print()
print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if success[f]:
print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}")
print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
failed += 1
return failed
@@ -645,7 +645,7 @@ def command_rename(args, config):
if c not in ALLOWED_NAME_CHARS:
print(
color(
Fore.BOLD_RED,
AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
)
@@ -658,7 +658,9 @@ def command_rename(args, config):
yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print(
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
color(
AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
)
)
return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME]
@@ -681,7 +683,7 @@ def command_rename(args, config):
)
> 1
):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1
new_raw = re.sub(
@@ -693,7 +695,7 @@ def command_rename(args, config):
new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
print(
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
)
print()
@@ -702,7 +704,7 @@ def command_rename(args, config):
rc = run_external_process("esphome", "config", new_path)
if rc != 0:
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
os.remove(new_path)
return 1
@@ -728,7 +730,7 @@ def command_rename(args, config):
if CORE.config_path != new_path:
os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print()
return 0

View File

@@ -432,7 +432,8 @@ message FanCommandRequest {
enum ColorMode {
COLOR_MODE_UNKNOWN = 0;
COLOR_MODE_ON_OFF = 1;
COLOR_MODE_BRIGHTNESS = 2;
COLOR_MODE_LEGACY_BRIGHTNESS = 2;
COLOR_MODE_BRIGHTNESS = 3;
COLOR_MODE_WHITE = 7;
COLOR_MODE_COLOR_TEMPERATURE = 11;
COLOR_MODE_COLD_WARM_WHITE = 19;

File diff suppressed because it is too large Load Diff

View File

@@ -8,13 +8,18 @@
#include "api_server.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include <vector>
#include <map>
#include <string>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
namespace api {
using send_message_t = bool(APIConnection *, void *);
using send_message_t = bool (APIConnection::*)(void *);
/*
This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that
@@ -30,10 +35,10 @@ class DeferredMessageQueue {
protected:
void *source_;
send_message_t *send_message_;
send_message_t send_message_;
public:
DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {}
DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {}
bool operator==(const DeferredMessage &test) const {
return (source_ == test.source_ && send_message_ == test.send_message_);
}
@@ -46,12 +51,13 @@ class DeferredMessageQueue {
APIConnection *api_connection_;
// helper for allowing only unique entries in the queue
void dmq_push_back_with_dedup_(void *source, send_message_t *send_message);
void dmq_push_back_with_dedup_(void *source, send_message_t send_message);
public:
DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
void process_queue();
void defer(void *source, send_message_t *send_message);
void defer(void *source, send_message_t send_message);
bool empty() const { return deferred_queue_.empty(); }
};
class APIConnection : public APIServerConnection {
@@ -59,6 +65,70 @@ class APIConnection : public APIServerConnection {
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection();
// Performance statistics class for API loop sections
class APISectionStats {
public:
APISectionStats()
: period_count_(0),
total_count_(0),
period_time_ms_(0),
total_time_ms_(0),
period_max_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
// Log if this is the first record in this period
if (this->period_count_ == 1) {
ESP_LOGV("api.stats", "First time recording stats for this section: %u ms", duration_ms);
}
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Period stats (reset each logging interval)
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
}
// Total stats (persistent until reboot)
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
}
protected:
// Period stats (reset each logging interval)
uint32_t period_count_;
uint32_t period_time_ms_;
uint32_t period_max_time_ms_;
// Total stats (persistent until reboot)
uint32_t total_count_;
uint32_t total_time_ms_;
uint32_t total_max_time_ms_;
};
void start();
void loop();
@@ -69,137 +139,213 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state);
static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor);
protected:
bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor);
bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state);
bool try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor);
public:
#endif
#ifdef USE_COVER
bool send_cover_state(cover::Cover *cover);
void send_cover_info(cover::Cover *cover);
static bool try_send_cover_state(APIConnection *api, void *v_cover);
static bool try_send_cover_info(APIConnection *api, void *v_cover);
void cover_command(const CoverCommandRequest &msg) override;
protected:
bool try_send_cover_state_(cover::Cover *cover);
bool try_send_cover_info_(cover::Cover *cover);
public:
#endif
#ifdef USE_FAN
bool send_fan_state(fan::Fan *fan);
void send_fan_info(fan::Fan *fan);
static bool try_send_fan_state(APIConnection *api, void *v_fan);
static bool try_send_fan_info(APIConnection *api, void *v_fan);
void fan_command(const FanCommandRequest &msg) override;
protected:
bool try_send_fan_state_(fan::Fan *fan);
bool try_send_fan_info_(fan::Fan *fan);
public:
#endif
#ifdef USE_LIGHT
bool send_light_state(light::LightState *light);
void send_light_info(light::LightState *light);
static bool try_send_light_state(APIConnection *api, void *v_light);
static bool try_send_light_info(APIConnection *api, void *v_light);
void light_command(const LightCommandRequest &msg) override;
protected:
bool try_send_light_state_(light::LightState *light);
bool try_send_light_info_(light::LightState *light);
public:
#endif
#ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor, float state);
void send_sensor_info(sensor::Sensor *sensor);
static bool try_send_sensor_state(APIConnection *api, void *v_sensor);
static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state);
static bool try_send_sensor_info(APIConnection *api, void *v_sensor);
protected:
bool try_send_sensor_state_(sensor::Sensor *sensor);
bool try_send_sensor_state_(sensor::Sensor *sensor, float state);
bool try_send_sensor_info_(sensor::Sensor *sensor);
public:
#endif
#ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch, bool state);
void send_switch_info(switch_::Switch *a_switch);
static bool try_send_switch_state(APIConnection *api, void *v_a_switch);
static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state);
static bool try_send_switch_info(APIConnection *api, void *v_a_switch);
void switch_command(const SwitchCommandRequest &msg) override;
protected:
bool try_send_switch_state_(switch_::Switch *a_switch);
bool try_send_switch_state_(switch_::Switch *a_switch, bool state);
bool try_send_switch_info_(switch_::Switch *a_switch);
public:
#endif
#ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state);
static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor);
protected:
bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor);
bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state);
bool try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor);
public:
#endif
#ifdef USE_ESP32_CAMERA
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera);
static bool try_send_camera_info(APIConnection *api, void *v_camera);
void camera_image(const CameraImageRequest &msg) override;
protected:
bool try_send_camera_info_(esp32_camera::ESP32Camera *camera);
public:
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void send_climate_info(climate::Climate *climate);
static bool try_send_climate_state(APIConnection *api, void *v_climate);
static bool try_send_climate_info(APIConnection *api, void *v_climate);
void climate_command(const ClimateCommandRequest &msg) override;
protected:
bool try_send_climate_state_(climate::Climate *climate);
bool try_send_climate_info_(climate::Climate *climate);
public:
#endif
#ifdef USE_NUMBER
bool send_number_state(number::Number *number, float state);
void send_number_info(number::Number *number);
static bool try_send_number_state(APIConnection *api, void *v_number);
static bool try_send_number_state(APIConnection *api, number::Number *number, float state);
static bool try_send_number_info(APIConnection *api, void *v_number);
void number_command(const NumberCommandRequest &msg) override;
protected:
bool try_send_number_state_(number::Number *number);
bool try_send_number_state_(number::Number *number, float state);
bool try_send_number_info_(number::Number *number);
public:
#endif
#ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date);
static bool try_send_date_state(APIConnection *api, void *v_date);
static bool try_send_date_info(APIConnection *api, void *v_date);
void date_command(const DateCommandRequest &msg) override;
protected:
bool try_send_date_state_(datetime::DateEntity *date);
bool try_send_date_info_(datetime::DateEntity *date);
public:
#endif
#ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time);
static bool try_send_time_state(APIConnection *api, void *v_time);
static bool try_send_time_info(APIConnection *api, void *v_time);
void time_command(const TimeCommandRequest &msg) override;
protected:
bool try_send_time_state_(datetime::TimeEntity *time);
bool try_send_time_info_(datetime::TimeEntity *time);
public:
#endif
#ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime);
static bool try_send_datetime_state(APIConnection *api, void *v_datetime);
static bool try_send_datetime_info(APIConnection *api, void *v_datetime);
void datetime_command(const DateTimeCommandRequest &msg) override;
protected:
bool try_send_datetime_state_(datetime::DateTimeEntity *datetime);
bool try_send_datetime_info_(datetime::DateTimeEntity *datetime);
public:
#endif
#ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state);
void send_text_info(text::Text *text);
static bool try_send_text_state(APIConnection *api, void *v_text);
static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state);
static bool try_send_text_info(APIConnection *api, void *v_text);
void text_command(const TextCommandRequest &msg) override;
protected:
bool try_send_text_state_(text::Text *text);
bool try_send_text_state_(text::Text *text, std::string state);
bool try_send_text_info_(text::Text *text);
public:
#endif
#ifdef USE_SELECT
bool send_select_state(select::Select *select, std::string state);
void send_select_info(select::Select *select);
static bool try_send_select_state(APIConnection *api, void *v_select);
static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state);
static bool try_send_select_info(APIConnection *api, void *v_select);
void select_command(const SelectCommandRequest &msg) override;
protected:
bool try_send_select_state_(select::Select *select);
bool try_send_select_state_(select::Select *select, std::string state);
bool try_send_select_info_(select::Select *select);
public:
#endif
#ifdef USE_BUTTON
void send_button_info(button::Button *button);
static bool try_send_button_info(APIConnection *api, void *v_button);
void button_command(const ButtonCommandRequest &msg) override;
protected:
bool try_send_button_info_(button::Button *button);
public:
#endif
#ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
void send_lock_info(lock::Lock *a_lock);
static bool try_send_lock_state(APIConnection *api, void *v_a_lock);
static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state);
static bool try_send_lock_info(APIConnection *api, void *v_a_lock);
void lock_command(const LockCommandRequest &msg) override;
protected:
bool try_send_lock_state_(lock::Lock *a_lock);
bool try_send_lock_state_(lock::Lock *a_lock, lock::LockState state);
bool try_send_lock_info_(lock::Lock *a_lock);
public:
#endif
#ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve);
void send_valve_info(valve::Valve *valve);
static bool try_send_valve_state(APIConnection *api, void *v_valve);
static bool try_send_valve_info(APIConnection *api, void *v_valve);
void valve_command(const ValveCommandRequest &msg) override;
protected:
bool try_send_valve_state_(valve::Valve *valve);
bool try_send_valve_info_(valve::Valve *valve);
public:
#endif
#ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player);
static bool try_send_media_player_state(APIConnection *api, void *v_media_player);
static bool try_send_media_player_info(APIConnection *api, void *v_media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override;
protected:
bool try_send_media_player_state_(media_player::MediaPlayer *media_player);
bool try_send_media_player_info_(media_player::MediaPlayer *media_player);
public:
#endif
bool try_send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
@@ -246,25 +392,37 @@ class APIConnection : public APIServerConnection {
#ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel);
static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
protected:
bool try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
bool try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
public:
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, std::string event_type);
void send_event_info(event::Event *event);
static bool try_send_event(APIConnection *api, void *v_event);
static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type);
static bool try_send_event_info(APIConnection *api, void *v_event);
protected:
bool try_send_event_(event::Event *event);
bool try_send_event_(event::Event *event, std::string event_type);
bool try_send_event_info_(event::Event *event);
public:
#endif
#ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update);
static bool try_send_update_state(APIConnection *api, void *v_update);
static bool try_send_update_info(APIConnection *api, void *v_update);
void update_command(const UpdateCommandRequest &msg) override;
protected:
bool try_send_update_state_(update::UpdateEntity *update);
bool try_send_update_info_(update::UpdateEntity *update);
public:
#endif
void on_disconnect_response(const DisconnectResponse &value) override;
@@ -318,6 +476,7 @@ class APIConnection : public APIServerConnection {
this->proto_write_buffer_.reserve(reserve_size);
return {&this->proto_write_buffer_};
}
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; }
@@ -325,6 +484,99 @@ class APIConnection : public APIServerConnection {
protected:
friend APIServer;
/**
* Generic send entity state method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This is the base version for entities that use their current state.
*
* @param entity The entity to send state for
* @param try_send_func The function that tries to send the state
* @return True on success or message deferred, false if subscription check failed
*/
bool send_state_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return true;
}
this->deferred_message_queue_.defer(entity, try_send_func);
return true;
}
/**
* Send entity state method that handles explicit state values.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This method accepts a state parameter to be used instead of the entity's current state.
* It attempts to send the state with the provided value first, and if that fails due to buffer constraints,
* it defers the entity for later processing using the entity-only function.
*
* @tparam EntityT The entity type
* @tparam StateT Type of the state parameter
* @tparam Args Additional argument types (if any)
* @param entity The entity to send state for
* @param try_send_entity_func The function that tries to send the state with entity pointer only
* @param try_send_state_func The function that tries to send the state with entity and state parameters
* @param state The state value to send
* @param args Additional arguments to pass to the try_send_state_func
* @return True on success or message deferred, false if subscription check failed
*/
template<typename EntityT, typename StateT, typename... Args>
bool send_state_with_value_(EntityT *entity, bool (APIConnection::*try_send_entity_func)(EntityT *),
bool (APIConnection::*try_send_state_func)(EntityT *, StateT, Args...), StateT state,
Args... args) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_state_func)(entity, state, args...)) {
return true;
}
this->deferred_message_queue_.defer(entity, reinterpret_cast<send_message_t>(try_send_entity_func));
return true;
}
/**
* Generic send entity info method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* @param entity The entity to send info for
* @param try_send_func The function that tries to send the info
*/
void send_info_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return;
}
this->deferred_message_queue_.defer(entity, try_send_func);
}
/**
* Generic function for generating entity info response messages.
* This is used to reduce duplication in the try_send_*_info functions.
*
* @param entity The entity to generate info for
* @param response The response object
* @param send_response_func Function pointer to send the response
* @return True if the message was sent successfully
*/
template<typename ResponseT>
bool try_send_entity_info_(esphome::EntityBase *entity, ResponseT &response,
bool (APIServerConnectionBase::*send_response_func)(const ResponseT &)) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
// Send the response using the provided send method
return (this->*send_response_func)(response);
}
bool send_(const void *buf, size_t len, bool force);
enum class ConnectionState {
@@ -362,6 +614,17 @@ class APIConnection : public APIServerConnection {
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1;
// API loop section performance statistics
std::map<std::string, APISectionStats> section_stats_;
uint32_t stats_log_interval_{60000}; // 60 seconds default
uint32_t next_stats_log_{0};
bool stats_enabled_{true};
void log_section_stats_();
void reset_section_stats_();
// Method to enable/disable section stats
void set_stats_enabled(bool enabled) { this->stats_enabled_ = enabled; }
};
} // namespace api

View File

@@ -13,14 +13,6 @@ namespace api {
static const char *const TAG = "api.socket";
/// Is the given return value (from write syscalls) a wouldblock error?
bool is_would_block(ssize_t ret) {
if (ret == -1) {
return errno == EWOULDBLOCK || errno == EAGAIN;
}
return ret == 0;
}
const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) {
@@ -73,14 +65,21 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN";
}
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
StateEnum failed_state) {
// This method writes data to socket or buffers it
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, size_t total_write_len) {
SendBuffer buffer;
buffer.data.reserve(total_write_len);
for (int i = 0; i < iovcnt; i++) {
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len);
}
this->tx_buf_.push_back(std::move(buffer));
}
// This method writes data to socket or buffers it
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
@@ -94,71 +93,104 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket:
total_write_len += iov[i].iov_len;
}
if (!tx_buf.empty()) {
// try to empty tx_buf first
while (!tx_buf.empty()) {
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
// Try to send any existing buffered data first if there is any
if (!this->tx_buf_.empty()) {
APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
return send_result;
}
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (!this->tx_buf_.empty()) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
return APIError::OK; // Success, data buffered
}
}
if (!tx_buf.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK; // Success, data buffered
}
// Try to send directly if no buffered data
ssize_t sent = this->socket_->writev(iov, iovcnt);
ssize_t sent = socket->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
return APIError::OK; // Success, data buffered
}
return APIError::OK; // Success, data buffered
} else if (sent == -1) {
// an error occurred
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
// Socket error
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t remaining = total_write_len - sent;
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + remaining);
} else if (static_cast<size_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
SendBuffer buffer;
size_t to_consume = sent;
size_t remaining = total_write_len - sent;
buffer.data.reserve(remaining);
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
// This segment was fully sent
to_consume -= iov[i].iov_len;
} else {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
// This segment was partially sent or not sent at all
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume;
size_t len = iov[i].iov_len - to_consume;
buffer.data.insert(buffer.data.end(), data, data + len);
to_consume = 0;
}
}
return APIError::OK; // Success, data buffered
this->tx_buf_.push_back(std::move(buffer));
}
return APIError::OK; // Success, all data sent
return APIError::OK; // Success, all data sent or buffered
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
// Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
bool tx_buf_empty = false;
while (!tx_buf_empty) {
// Get the first buffer in the queue
SendBuffer &front_buffer = this->tx_buf_.front();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
if (sent == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
// Real socket error (not just would block)
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// Socket would block, we'll try again later
return APIError::WOULD_BLOCK;
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer.offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front();
// Update empty status for the loop condition
tx_buf_empty = this->tx_buf_.empty();
// Continue loop to try sending the next buffer
}
}
return APIError::OK; // All buffers sent successfully
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
@@ -206,11 +238,11 @@ std::string noise_err_to_str(int err) {
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
@@ -218,7 +250,7 @@ APIError APINoiseFrameHelper::init() {
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
@@ -238,13 +270,12 @@ APIError APINoiseFrameHelper::loop() {
return APIError::OK;
if (err != APIError::OK)
return err;
if (!tx_buf_.empty()) {
err = try_send_tx_buf_();
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
if (this->tx_buf_.empty())
return APIError::OK;
err = try_send_tx_buf_();
if (err == APIError::WOULD_BLOCK)
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
return err;
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@@ -271,7 +302,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_header_buf_len_ < 3) {
// no header information yet
size_t to_read = 3 - rx_header_buf_len_;
ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -318,7 +349,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_buf_len_ < msg_size) {
// more data to read
size_t to_read = msg_size - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -556,7 +587,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err;
APIError aerr;
@@ -610,27 +640,7 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
iov.iov_len = total_len;
// write raw to not have two packets sent if NAGLE disabled
return write_raw_(&iov, 1);
}
APIError APINoiseFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent == 0) {
break;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
return APIFrameHelper::write_raw_(&iov, 1);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
uint8_t header[3];
@@ -642,12 +652,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return write_raw_(iov, 1);
return APIFrameHelper::write_raw_(iov, 1);
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return write_raw_(iov, 2);
return APIFrameHelper::write_raw_(iov, 2);
}
/** Initiate the data structures for the handshake.
@@ -740,22 +750,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
}
}
APIError APINoiseFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APINoiseFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
@@ -766,28 +760,24 @@ void noise_rand_bytes(void *output, size_t len) {
}
}
// Explicit template instantiation for Noise
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
/// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
@@ -802,14 +792,12 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// try send pending TX data
if (!tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK) {
return err;
}
}
return APIError::OK;
if (this->tx_buf_.empty())
return APIError::OK;
APIError err = try_send_tx_buf_();
if (err == APIError::WOULD_BLOCK)
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
return err;
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@@ -830,6 +818,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header
while (!rx_header_parsed_) {
uint8_t data;
// Reading one byte at a time is fastest in practice for ESP32 when
// there is no data on the wire (which is the common case).
// This results in faster failure detection compared to
// attempting to read multiple bytes at once.
ssize_t received = socket_->read(&data, 1);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
@@ -843,27 +835,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
rx_header_buf_.push_back(data);
// try parse header
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
return APIError::BAD_INDICATOR;
// Successfully read a byte
// Process byte according to current buffer position
if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte)
if (data != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", data);
return APIError::BAD_INDICATOR;
}
// We don't store the indicator byte, just increment position
rx_header_buf_pos_ = 1; // Set to 1 directly
continue; // Need more bytes before we can parse
}
size_t i = 1;
// Check buffer overflow before storing
if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed)
state_ = State::FAILED;
HELPER_LOG("Header buffer overflow");
return APIError::BAD_DATA_PACKET;
}
// Store byte in buffer (adjust index to account for skipped indicator byte)
rx_header_buf_[rx_header_buf_pos_ - 1] = data;
// Increment position after storing
rx_header_buf_pos_++;
// Case 3: If we only have one varint byte, we need more
if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte
continue; // Need more bytes before we can parse
}
// At this point, we have at least 3 bytes total:
// - Validated indicator byte (0x00) but not stored
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// First 1-3 bytes: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's 65535
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// Remaining 1-2 bytes: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
// we'll continue reading more bytes.
uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
if (!msg_size_varint.has_value()) {
// not enough data there yet
continue;
}
i += consumed;
rx_header_parsed_len_ = msg_size_varint->as_uint32();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
@@ -881,7 +906,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -909,7 +934,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// consume msg
rx_buf_ = {};
rx_buf_len_ = 0;
rx_header_buf_.clear();
rx_header_buf_pos_ = 0;
rx_header_parsed_ = false;
return APIError::OK;
}
@@ -941,7 +966,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19;
write_raw_(iov, 1);
APIFrameHelper::write_raw_(iov, 1);
}
return aerr;
}
@@ -952,7 +977,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
@@ -969,53 +993,14 @@ APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *pay
iov[0].iov_base = &header[0];
iov[0].iov_len = header.size();
if (payload_len == 0) {
return write_raw_(iov, 1);
return APIFrameHelper::write_raw_(iov, 1);
}
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
return write_raw_(iov, 2);
}
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
return APIFrameHelper::write_raw_(iov, 2);
}
APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Explicit template instantiation for Plaintext
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
#endif // USE_API_PLAINTEXT
} // namespace api

View File

@@ -60,71 +60,142 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper {
public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
socket_ = socket_owned_.get();
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0;
bool can_write_without_blocking() {
// First check if we're in the DATA state
if (state_ != State::DATA) {
return false;
}
// Empty buffer can always accept more data
if (tx_buf_.empty()) {
return true;
}
// Optimization: Allow writing even with a small buffer backlog to reduce delays in message processing.
// This improves throughput for real-time data like sensor readings and prevents high-priority
// messages from being unnecessarily delayed by a small queue backlog.
// The 256-byte threshold is small enough to not impact memory usage significantly
// but large enough to improve overall system responsiveness.
if (tx_buf_.size() == 1 && tx_buf_.front().remaining() < 256) {
return true;
}
return false;
}
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
virtual std::string getpeername() = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0;
std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError shutdown(int how) {
int err = this->socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Give this helper a name for logging
virtual void set_log_info(std::string info) = 0;
void set_log_info(std::string info) { info_ = std::move(info); }
protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent
struct SendBuffer {
std::vector<uint8_t> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
// Using uint16_t reduces memory usage since ESPHome API messages are limited to 64KB max
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
const uint8_t *current_data() const { return data.data() + offset; }
};
// Queue of data buffers to be sent
std::deque<SendBuffer> tx_buf_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Current state of the frame helper
State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
APIError write_raw_(const struct iovec *iov, int iovcnt);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, size_t total_write_len);
};
#ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {}
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {}
~APINoiseFrameHelper() override;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen);
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
APIError write_frame_(const uint8_t *data, size_t len);
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
@@ -132,69 +203,37 @@ class APINoiseFrameHelper : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_;
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2,
SERVER_HELLO = 3,
HANDSHAKE = 4,
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8,
} state_ = State::INITIALIZE;
};
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {}
~APIPlaintextFrameHelper() override = default;
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen);
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
std::unique_ptr<socket::Socket> socket_;
std::string info_;
std::vector<uint8_t> rx_header_buf_;
// Fixed-size header buffer for plaintext protocol:
// We only need space for the two varints since we validate the indicator byte separately.
// To match noise protocol's maximum message size (65535), we need:
// 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
// ESP32 due to memory constraints.
uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
enum class State {
INITIALIZE = 1,
DATA = 2,
CLOSED = 3,
FAILED = 4,
} state_ = State::INITIALIZE;
};
#endif

View File

@@ -96,6 +96,8 @@ template<> const char *proto_enum_to_string<enums::ColorMode>(enums::ColorMode v
return "COLOR_MODE_UNKNOWN";
case enums::COLOR_MODE_ON_OFF:
return "COLOR_MODE_ON_OFF";
case enums::COLOR_MODE_LEGACY_BRIGHTNESS:
return "COLOR_MODE_LEGACY_BRIGHTNESS";
case enums::COLOR_MODE_BRIGHTNESS:
return "COLOR_MODE_BRIGHTNESS";
case enums::COLOR_MODE_WHITE:

View File

@@ -41,7 +41,8 @@ enum FanDirection : uint32_t {
enum ColorMode : uint32_t {
COLOR_MODE_UNKNOWN = 0,
COLOR_MODE_ON_OFF = 1,
COLOR_MODE_BRIGHTNESS = 2,
COLOR_MODE_LEGACY_BRIGHTNESS = 2,
COLOR_MODE_BRIGHTNESS = 3,
COLOR_MODE_WHITE = 7,
COLOR_MODE_COLOR_TEMPERATURE = 11,
COLOR_MODE_COLD_WARM_WHITE = 19,

View File

@@ -20,16 +20,26 @@ class ProtoVarInt {
explicit ProtoVarInt(uint64_t value) : value_(value) {}
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (consumed != nullptr)
*consumed = 0;
if (len == 0)
if (len == 0) {
if (consumed != nullptr)
*consumed = 0;
return {};
}
uint64_t result = 0;
uint8_t bitpos = 0;
// Most common case: single-byte varint (values 0-127)
if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
for (uint32_t i = 0; i < len; i++) {
// General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < len; i++) {
uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7;
@@ -40,7 +50,9 @@ class ProtoVarInt {
}
}
return {};
if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
}
uint32_t as_uint32() const { return this->value_; }

View File

@@ -32,14 +32,14 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CCS811Component),
cv.Required(CONF_ECO2): sensor.sensor_schema(
cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
@@ -64,10 +64,13 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2])
cg.add(var.set_co2(sens))
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
if eco2_config := config.get(CONF_ECO2):
sens = await sensor.new_sensor(eco2_config)
cg.add(var.set_co2(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config)

View File

@@ -27,14 +27,14 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DPS310Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
icon=ICON_GAUGE,
accuracy_decimals=1,
@@ -53,10 +53,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))

View File

@@ -26,19 +26,19 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EE895Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE,
@@ -56,14 +56,14 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))

View File

@@ -28,21 +28,21 @@ UNIT_INDEX = "index"
CONFIG_SCHEMA_BASE = cv.Schema(
{
cv.Required(CONF_ECO2): sensor.sensor_schema(
cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_AQI): sensor.sensor_schema(
cv.Optional(CONF_AQI): sensor.sensor_schema(
icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
@@ -62,12 +62,15 @@ async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2])
cg.add(var.set_co2(sens))
sens = await sensor.new_sensor(config[CONF_TVOC])
cg.add(var.set_tvoc(sens))
sens = await sensor.new_sensor(config[CONF_AQI])
cg.add(var.set_aqi(sens))
if eco2_config := config.get(CONF_ECO2):
sens = await sensor.new_sensor(eco2_config)
cg.add(var.set_co2(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if aqi_config := config.get(CONF_AQI):
sens = await sensor.new_sensor(aqi_config)
cg.add(var.set_aqi(sens))
if compensation_config := config.get(CONF_COMPENSATION):
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])

View File

@@ -106,7 +106,7 @@ class CameraImageReader {
};
/* ---------------- ESP32Camera class ---------------- */
class ESP32Camera : public Component, public EntityBase {
class ESP32Camera : public EntityBase, public Component {
public:
ESP32Camera();

View File

@@ -9,23 +9,32 @@ from esphome.const import (
CONF_LONGITUDE,
CONF_SATELLITES,
CONF_SPEED,
DEVICE_CLASS_SPEED,
STATE_CLASS_MEASUREMENT,
UNIT_DEGREES,
UNIT_KILOMETER_PER_HOUR,
UNIT_METER,
)
CONF_GPS_ID = "gps_id"
CONF_HDOP = "hdop"
ICON_ALTIMETER = "mdi:altimeter"
ICON_COMPASS = "mdi:compass"
ICON_LATITUDE = "mdi:latitude"
ICON_LONGITUDE = "mdi:longitude"
ICON_SATELLITE = "mdi:satellite-variant"
ICON_SPEEDOMETER = "mdi:speedometer"
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["sensor"]
CODEOWNERS = ["@coogle"]
CODEOWNERS = ["@coogle", "@ximex"]
gps_ns = cg.esphome_ns.namespace("gps")
GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
GPSListener = gps_ns.class_("GPSListener")
CONF_GPS_ID = "gps_id"
CONF_HDOP = "hdop"
MULTI_CONF = True
CONFIG_SCHEMA = cv.All(
cv.Schema(
@@ -33,25 +42,37 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(GPS),
cv.Optional(CONF_LATITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_LATITUDE,
accuracy_decimals=6,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_LONGITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_LONGITUDE,
accuracy_decimals=6,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOMETER_PER_HOUR,
icon=ICON_SPEEDOMETER,
accuracy_decimals=3,
device_class=DEVICE_CLASS_SPEED,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_COURSE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
icon=ICON_COMPASS,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
icon=ICON_ALTIMETER,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
icon=ICON_SATELLITE,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
@@ -73,28 +94,28 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_LATITUDE in config:
sens = await sensor.new_sensor(config[CONF_LATITUDE])
if latitude_config := config.get(CONF_LATITUDE):
sens = await sensor.new_sensor(latitude_config)
cg.add(var.set_latitude_sensor(sens))
if CONF_LONGITUDE in config:
sens = await sensor.new_sensor(config[CONF_LONGITUDE])
if longitude_config := config.get(CONF_LONGITUDE):
sens = await sensor.new_sensor(longitude_config)
cg.add(var.set_longitude_sensor(sens))
if CONF_SPEED in config:
sens = await sensor.new_sensor(config[CONF_SPEED])
if speed_config := config.get(CONF_SPEED):
sens = await sensor.new_sensor(speed_config)
cg.add(var.set_speed_sensor(sens))
if CONF_COURSE in config:
sens = await sensor.new_sensor(config[CONF_COURSE])
if course_config := config.get(CONF_COURSE):
sens = await sensor.new_sensor(course_config)
cg.add(var.set_course_sensor(sens))
if CONF_ALTITUDE in config:
sens = await sensor.new_sensor(config[CONF_ALTITUDE])
if altitude_config := config.get(CONF_ALTITUDE):
sens = await sensor.new_sensor(altitude_config)
cg.add(var.set_altitude_sensor(sens))
if CONF_SATELLITES in config:
sens = await sensor.new_sensor(config[CONF_SATELLITES])
if satellites_config := config.get(CONF_SATELLITES):
sens = await sensor.new_sensor(satellites_config)
cg.add(var.set_satellites_sensor(sens))
if hdop_config := config.get(CONF_HDOP):
@@ -102,4 +123,4 @@ async def to_code(config):
cg.add(var.set_hdop_sensor(sens))
# https://platformio.org/lib/show/1655/TinyGPSPlus
cg.add_library("mikalhart/TinyGPSPlus", "1.0.2")
cg.add_library("mikalhart/TinyGPSPlus", "1.1.0")

View File

@@ -10,6 +10,17 @@ static const char *const TAG = "gps";
TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); }
void GPS::dump_config() {
ESP_LOGCONFIG(TAG, "GPS:");
LOG_SENSOR(" ", "Latitude", this->latitude_sensor_);
LOG_SENSOR(" ", "Longitude", this->longitude_sensor_);
LOG_SENSOR(" ", "Speed", this->speed_sensor_);
LOG_SENSOR(" ", "Course", this->course_sensor_);
LOG_SENSOR(" ", "Altitude", this->altitude_sensor_);
LOG_SENSOR(" ", "Satellites", this->satellites_sensor_);
LOG_SENSOR(" ", "HDOP", this->hdop_sensor_);
}
void GPS::update() {
if (this->latitude_sensor_ != nullptr)
this->latitude_sensor_->publish_state(this->latitude_);
@@ -34,40 +45,45 @@ void GPS::update() {
}
void GPS::loop() {
while (this->available() && !this->has_time_) {
while (this->available() > 0 && !this->has_time_) {
if (this->tiny_gps_.encode(this->read())) {
if (tiny_gps_.location.isUpdated()) {
this->latitude_ = tiny_gps_.location.lat();
this->longitude_ = tiny_gps_.location.lng();
if (this->tiny_gps_.location.isUpdated()) {
this->latitude_ = this->tiny_gps_.location.lat();
this->longitude_ = this->tiny_gps_.location.lng();
ESP_LOGD(TAG, "Location:");
ESP_LOGD(TAG, " Lat: %f", this->latitude_);
ESP_LOGD(TAG, " Lon: %f", this->longitude_);
ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_);
ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_);
}
if (tiny_gps_.speed.isUpdated()) {
this->speed_ = tiny_gps_.speed.kmph();
if (this->tiny_gps_.speed.isUpdated()) {
this->speed_ = this->tiny_gps_.speed.kmph();
ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_);
}
if (tiny_gps_.course.isUpdated()) {
this->course_ = tiny_gps_.course.deg();
if (this->tiny_gps_.course.isUpdated()) {
this->course_ = this->tiny_gps_.course.deg();
ESP_LOGD(TAG, "Course: %.2f °", this->course_);
}
if (tiny_gps_.altitude.isUpdated()) {
this->altitude_ = tiny_gps_.altitude.meters();
if (this->tiny_gps_.altitude.isUpdated()) {
this->altitude_ = this->tiny_gps_.altitude.meters();
ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_);
}
if (tiny_gps_.satellites.isUpdated()) {
this->satellites_ = tiny_gps_.satellites.value();
if (this->tiny_gps_.satellites.isUpdated()) {
this->satellites_ = this->tiny_gps_.satellites.value();
ESP_LOGD(TAG, "Satellites: %d", this->satellites_);
}
if (tiny_gps_.hdop.isUpdated()) {
this->hdop_ = tiny_gps_.hdop.hdop();
if (this->tiny_gps_.hdop.isUpdated()) {
this->hdop_ = this->tiny_gps_.hdop.hdop();
ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_);
}
for (auto *listener : this->listeners_)
for (auto *listener : this->listeners_) {
listener->on_update(this->tiny_gps_);
}
}
}
}

View File

@@ -5,7 +5,7 @@
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include <TinyGPS++.h>
#include <TinyGPSPlus.h>
#include <vector>
@@ -27,13 +27,13 @@ class GPSListener {
class GPS : public PollingComponent, public uart::UARTDevice {
public:
void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; }
void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; }
void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; }
void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; }
void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; }
void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; }
void set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; }
void set_latitude_sensor(sensor::Sensor *latitude_sensor) { this->latitude_sensor_ = latitude_sensor; }
void set_longitude_sensor(sensor::Sensor *longitude_sensor) { this->longitude_sensor_ = longitude_sensor; }
void set_speed_sensor(sensor::Sensor *speed_sensor) { this->speed_sensor_ = speed_sensor; }
void set_course_sensor(sensor::Sensor *course_sensor) { this->course_sensor_ = course_sensor; }
void set_altitude_sensor(sensor::Sensor *altitude_sensor) { this->altitude_sensor_ = altitude_sensor; }
void set_satellites_sensor(sensor::Sensor *satellites_sensor) { this->satellites_sensor_ = satellites_sensor; }
void set_hdop_sensor(sensor::Sensor *hdop_sensor) { this->hdop_sensor_ = hdop_sensor; }
void register_listener(GPSListener *listener) {
listener->parent_ = this;
@@ -41,19 +41,20 @@ class GPS : public PollingComponent, public uart::UARTDevice {
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void dump_config() override;
void loop() override;
void update() override;
TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; }
protected:
float latitude_ = NAN;
float longitude_ = NAN;
float speed_ = NAN;
float course_ = NAN;
float altitude_ = NAN;
int satellites_ = 0;
double hdop_ = NAN;
float latitude_{NAN};
float longitude_{NAN};
float speed_{NAN};
float course_{NAN};
float altitude_{NAN};
uint16_t satellites_{0};
float hdop_{NAN};
sensor::Sensor *latitude_sensor_{nullptr};
sensor::Sensor *longitude_sensor_{nullptr};

View File

@@ -25,13 +25,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HTE501Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
@@ -49,10 +49,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens))

View File

@@ -23,13 +23,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(HYT271Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
@@ -47,10 +47,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity(sens))

View File

@@ -254,6 +254,7 @@ async def to_code(config):
config[CONF_TX_BUFFER_SIZE],
)
if CORE.is_esp32:
cg.add(log.create_pthread_key())
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")

View File

@@ -14,25 +14,47 @@ namespace logger {
static const char *const TAG = "logger";
#ifdef USE_ESP32
// Implementation for ESP32 (multi-core with atomic support)
// Main thread: synchronous logging with direct buffer access
// Other threads: console output with stack buffer, callbacks via async buffer
// Implementation for ESP32 (multi-task platform with task-specific tracking)
// Main task always uses direct buffer access for console output and callbacks
//
// For non-main tasks:
// - WITH task log buffer: Prefer sending to ring buffer for async processing
// - Avoids allocating stack memory for console output in normal operation
// - Prevents console corruption from concurrent writes by multiple tasks
// - Messages are serialized through main loop for proper console output
// - Fallback to emergency console logging only if ring buffer is full
// - WITHOUT task log buffer: Only emergency console output, no callbacks
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed))
if (level > this->level_for(tag))
return;
recursion_guard_.store(true, std::memory_order_relaxed);
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
bool is_main_task = (current_task == main_task_);
// For main task: call log_message_to_buffer_and_send_ which does console and callback logging
if (current_task == main_task_) {
// Check and set recursion guard - uses pthread TLS for per-task state
if (this->check_and_set_task_log_recursion_(is_main_task)) {
return; // Recursion detected
}
// Main task uses the shared buffer for efficiency
if (is_main_task) {
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
recursion_guard_.store(false, std::memory_order_release);
this->reset_task_log_recursion_(is_main_task);
return;
}
// For non-main tasks: use stack-allocated buffer only for console output
if (this->baud_rate_ > 0) { // If logging is enabled, write to console
bool message_sent = false;
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
message_sent = this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag,
static_cast<uint16_t>(line), current_task, format, args);
#endif // USE_ESPHOME_TASK_LOG_BUFFER
// Emergency console logging for non-main tasks when ring buffer is full or disabled
// This is a fallback mechanism to ensure critical log messages are visible
// Note: This may cause interleaved/corrupted console output if multiple tasks
// log simultaneously, but it's better than losing important messages entirely
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
// Maximum size for console log messages (includes null terminator)
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
@@ -42,32 +64,21 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
this->write_msg_(console_buffer);
}
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
if (this->log_callback_.size() > 0) {
// This will be processed in the main loop
this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag, static_cast<uint16_t>(line),
current_task, format, args);
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
recursion_guard_.store(false, std::memory_order_release);
// Reset the recursion guard for this task
this->reset_task_log_recursion_(is_main_task);
}
#endif // USE_ESP32
#ifndef USE_ESP32
// Implementation for platforms that do not support atomic operations
// or have to consider logging in other tasks
#else
// Implementation for all other platforms
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_)
if (level > this->level_for(tag) || global_recursion_guard_)
return;
recursion_guard_ = true;
global_recursion_guard_ = true;
// Format and send to both console and callbacks
this->log_message_to_buffer_and_send_(level, tag, line, format, args);
recursion_guard_ = false;
global_recursion_guard_ = false;
}
#endif // !USE_ESP32
@@ -76,10 +87,10 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_)
if (level > this->level_for(tag) || global_recursion_guard_)
return;
recursion_guard_ = true;
global_recursion_guard_ = true;
this->tx_buffer_at_ = 0;
// Copy format string from progmem
@@ -91,7 +102,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
// Buffer full from copying format
if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
recursion_guard_ = false; // Make sure to reset the recursion guard before returning
global_recursion_guard_ = false; // Make sure to reset the recursion guard before returning
return;
}
@@ -107,7 +118,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
}
this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start);
recursion_guard_ = false;
global_recursion_guard_ = false;
}
#endif // USE_STORE_LOG_STR_IN_FLASH
@@ -179,7 +190,17 @@ void Logger::loop() {
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
this->tx_buffer_[this->tx_buffer_at_] = '\0';
this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_);
// At this point all the data we need from message has been transferred to the tx_buffer
// so we can release the message to allow other tasks to use it as soon as possible.
this->log_buffer_->release_message_main_loop(received_token);
// Write to console from the main loop to prevent corruption from concurrent writes
// This ensures all log messages appear on the console in a clean, serialized manner
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
}
}
}
#endif

View File

@@ -3,7 +3,7 @@
#include <cstdarg>
#include <map>
#ifdef USE_ESP32
#include <atomic>
#include <pthread.h>
#endif
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
@@ -84,6 +84,23 @@ enum UARTSelection {
};
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY
/**
* @brief Logger component for all ESPHome logging.
*
* This class implements a multi-platform logging system with protection against recursion.
*
* Recursion Protection Strategy:
* - On ESP32: Uses task-specific recursion guards
* * Main task: Uses a dedicated boolean member variable for efficiency
* * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state
* - On other platforms: Uses a simple global recursion guard
*
* We use pthread TLS via pthread_key_create to create a unique key for storing
* task-specific recursion state, which:
* 1. Efficiently handles multiple tasks without locks or mutexes
* 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables
* 3. Avoids the limitations of the fixed FreeRTOS task local storage slots
*/
class Logger : public Component {
public:
explicit Logger(uint32_t baud_rate, size_t tx_buffer_size);
@@ -102,6 +119,9 @@ class Logger : public Component {
#ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; }
#endif
#ifdef USE_ESP32
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger.
@@ -222,18 +242,22 @@ class Logger : public Component {
std::map<std::string, int> log_levels_{};
CallbackManager<void(int, const char *, const char *)> log_callback_{};
int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
#ifdef USE_ESP32
std::atomic<bool> recursion_guard_{false};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
#endif
#ifdef USE_ESP32
// Task-specific recursion guards:
// - Main task uses a dedicated member variable for efficiency
// - Other tasks use pthread TLS with a dynamically created key via pthread_key_create
bool main_task_recursion_guard_{false};
pthread_key_t log_recursion_key_;
#else
bool recursion_guard_{false};
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif
void *main_task_ = nullptr;
CallbackManager<void(int)> level_callback_{};
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
void *main_task_ = nullptr; // Only used for thread name identification
const char *HOT get_thread_name_() {
TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
if (current_task == main_task_) {
@@ -248,6 +272,32 @@ class Logger : public Component {
}
#endif
#ifdef USE_ESP32
inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) {
if (is_main_task) {
const bool was_recursive = main_task_recursion_guard_;
main_task_recursion_guard_ = true;
return was_recursive;
}
intptr_t current = (intptr_t) pthread_getspecific(log_recursion_key_);
if (current != 0)
return true;
pthread_setspecific(log_recursion_key_, (void *) 1);
return false;
}
inline void HOT reset_task_log_recursion_(bool is_main_task) {
if (is_main_task) {
main_task_recursion_guard_ = false;
return;
}
pthread_setspecific(log_recursion_key_, (void *) 0);
}
#endif
inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer,
int *buffer_at, int buffer_size) {
// Format header

View File

@@ -0,0 +1,52 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "max7219digit.h"
namespace esphome {
namespace max7219digit {
template<typename... Ts> class DisplayInvertAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
bool state = this->state_.value(x...);
this->parent_->invert_on_off(state);
}
};
template<typename... Ts> class DisplayVisibilityAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
bool state = this->state_.value(x...);
this->parent_->turn_on_off(state);
}
};
template<typename... Ts> class DisplayReverseAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
bool state = this->state_.value(x...);
this->parent_->set_reverse(state);
}
};
template<typename... Ts> class DisplayIntensityAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(uint8_t, state)
void play(Ts... x) override {
uint8_t state = this->state_.value(x...);
this->parent_->set_intensity(state);
}
};
} // namespace max7219digit
} // namespace esphome

View File

@@ -1,7 +1,14 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS
from esphome.const import (
CONF_ID,
CONF_INTENSITY,
CONF_LAMBDA,
CONF_NUM_CHIPS,
CONF_STATE,
)
CODEOWNERS = ["@rspaargaren"]
DEPENDENCIES = ["spi"]
@@ -17,6 +24,7 @@ CONF_REVERSE_ENABLE = "reverse_enable"
CONF_NUM_CHIP_LINES = "num_chip_lines"
CONF_CHIP_LINES_STYLE = "chip_lines_style"
integration_ns = cg.esphome_ns.namespace("max7219digit")
ChipLinesStyle = integration_ns.enum("ChipLinesStyle")
CHIP_LINES_STYLE = {
@@ -99,3 +107,87 @@ async def to_code(config):
config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
DisplayInvertAction = max7219_ns.class_("DisplayInvertAction", automation.Action)
DisplayVisibilityAction = max7219_ns.class_(
"DisplayVisibilityAction", automation.Action
)
DisplayReverseAction = max7219_ns.class_("DisplayReverseAction", automation.Action)
DisplayIntensityAction = max7219_ns.class_("DisplayIntensityAction", automation.Action)
MAX7219_OFF_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(MAX7219Component),
cv.Optional(CONF_STATE, default=False): False,
}
)
MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(MAX7219Component),
cv.Optional(CONF_STATE, default=True): True,
}
)
@automation.register_action(
"max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA
)
@automation.register_action(
"max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA
)
async def max7129digit_invert_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
return var
@automation.register_action(
"max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA
)
@automation.register_action(
"max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA
)
async def max7129digit_visible_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
return var
@automation.register_action(
"max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA
)
@automation.register_action(
"max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA
)
async def max7129digit_reverse_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
return var
MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MAX7219Component),
cv.Optional(CONF_INTENSITY, default=15): cv.templatable(
cv.int_range(min=0, max=15)
),
},
key=CONF_INTENSITY,
)
@automation.register_action(
"max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA
)
async def max7129digit_intensity_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8)
cg.add(var.set_state(template_))
return var

View File

@@ -32,7 +32,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MHZ19Component),
cv.Required(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
@@ -61,16 +61,20 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_AUTOMATIC_BASELINE_CALIBRATION in config:
cg.add(var.set_abc_enabled(config[CONF_AUTOMATIC_BASELINE_CALIBRATION]))
if (
automatic_baseline_calibration := config.get(
CONF_AUTOMATIC_BASELINE_CALIBRATION
)
) is not None:
cg.add(var.set_abc_enabled(automatic_baseline_calibration))
cg.add(var.set_warmup_seconds(config[CONF_WARMUP_TIME]))

View File

@@ -24,13 +24,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MS5611Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
icon=ICON_GAUGE,
accuracy_decimals=1,
@@ -49,10 +49,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_PRESSURE in config:
sens = await sensor.new_sensor(config[CONF_PRESSURE])
if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens))

View File

@@ -29,19 +29,19 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(MS8607Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=2, # Resolution: 0.01
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_PRESSURE): sensor.sensor_schema(
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=2, # Resolution: 0.016
device_class=DEVICE_CLASS_PRESSURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=2, # Resolution: 0.04
device_class=DEVICE_CLASS_HUMIDITY,

View File

@@ -0,0 +1,26 @@
"""
Runtime statistics component for ESPHome.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
DEPENDENCIES = []
CONF_ENABLED = "enabled"
CONF_LOG_INTERVAL = "log_interval"
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
cv.Optional(
CONF_LOG_INTERVAL, default=60000
): cv.positive_time_period_milliseconds,
}
)
async def to_code(config):
"""Generate code for the runtime statistics component."""
cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))

View File

@@ -38,7 +38,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SenseAirComponent),
cv.Required(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
@@ -57,8 +57,8 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))

View File

@@ -37,14 +37,14 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SGP30Component),
cv.Required(CONF_ECO2): sensor.sensor_schema(
cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_TVOC): sensor.sensor_schema(
cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
@@ -86,32 +86,30 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_ECO2 in config:
sens = await sensor.new_sensor(config[CONF_ECO2])
if eco2_config := config.get(CONF_ECO2):
sens = await sensor.new_sensor(eco2_config)
cg.add(var.set_eco2_sensor(sens))
if CONF_TVOC in config:
sens = await sensor.new_sensor(config[CONF_TVOC])
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc_sensor(sens))
if CONF_ECO2_BASELINE in config:
sens = await sensor.new_sensor(config[CONF_ECO2_BASELINE])
if eco2_baseline_config := config.get(CONF_ECO2_BASELINE):
sens = await sensor.new_sensor(eco2_baseline_config)
cg.add(var.set_eco2_baseline_sensor(sens))
if CONF_TVOC_BASELINE in config:
sens = await sensor.new_sensor(config[CONF_TVOC_BASELINE])
if tvoc_baseline_config := config.get(CONF_TVOC_BASELINE):
sens = await sensor.new_sensor(tvoc_baseline_config)
cg.add(var.set_tvoc_baseline_sensor(sens))
if CONF_STORE_BASELINE in config:
cg.add(var.set_store_baseline(config[CONF_STORE_BASELINE]))
if (store_baseline := config.get(CONF_STORE_BASELINE)) is not None:
cg.add(var.set_store_baseline(store_baseline))
if CONF_BASELINE in config:
baseline_config = config[CONF_BASELINE]
if baseline_config := config.get(CONF_BASELINE):
cg.add(var.set_eco2_baseline(baseline_config[CONF_ECO2_BASELINE]))
cg.add(var.set_tvoc_baseline(baseline_config[CONF_TVOC_BASELINE]))
if CONF_COMPENSATION in config:
compensation_config = config[CONF_COMPENSATION]
if compensation_config := config.get(CONF_COMPENSATION):
sens = await cg.get_variable(compensation_config[CONF_HUMIDITY_SOURCE])
cg.add(var.set_humidity_sensor(sens))
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE_SOURCE])

View File

@@ -26,13 +26,13 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(SHTCXComponent),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema(
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema(
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY,
@@ -50,10 +50,10 @@ async def to_code(config):
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config:
sens = await sensor.new_sensor(config[CONF_TEMPERATURE])
if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config:
sens = await sensor.new_sensor(config[CONF_HUMIDITY])
if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens))

View File

@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(T6615Component),
cv.Required(CONF_CO2): sensor.sensor_schema(
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
@@ -41,6 +41,6 @@ async def to_code(config):
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if CONF_CO2 in config:
sens = await sensor.new_sensor(config[CONF_CO2])
if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens))

View File

@@ -63,7 +63,8 @@ void T6615Component::loop() {
case T6615Command::GET_PPM: {
const uint16_t ppm = encode_uint16(response_buffer[3], response_buffer[4]);
ESP_LOGD(TAG, "T6615 Received CO₂=%uppm", ppm);
this->co2_sensor_->publish_state(ppm);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
break;
}
default:

View File

@@ -28,7 +28,7 @@ import esphome.core.config as core_config
import esphome.final_validate as fv
from esphome.helpers import indent
from esphome.loader import ComponentManifest, get_component, get_platform
from esphome.log import Fore, color
from esphome.log import AnsiFore, color
from esphome.types import ConfigFragmentType, ConfigType
from esphome.util import OrderedDict, safe_print
from esphome.voluptuous_schema import ExtraKeysInvalid
@@ -959,7 +959,7 @@ def line_info(config, path, highlight=True):
if obj:
mark = obj.start_mark
source = f"[source {mark.document}:{mark.line + 1}]"
return color(Fore.CYAN, source)
return color(AnsiFore.CYAN, source)
return "None"
@@ -983,7 +983,7 @@ def dump_dict(
if at_root:
error = config.get_error_for_path(path)
if error is not None:
ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n"
if isinstance(conf, (list, tuple)):
multiline = True
@@ -995,11 +995,11 @@ def dump_dict(
path_ = path + [i]
error = config.get_error_for_path(path_)
if error is not None:
ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n"
sep = "- "
if config.is_in_error_path(path_):
sep = color(Fore.RED, sep)
sep = color(AnsiFore.RED, sep)
msg, _ = dump_dict(config, path_, at_root=False)
msg = indent(msg)
inf = line_info(config, path_, highlight=config.is_in_error_path(path_))
@@ -1018,11 +1018,11 @@ def dump_dict(
path_ = path + [k]
error = config.get_error_for_path(path_)
if error is not None:
ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n"
st = f"{k}: "
if config.is_in_error_path(path_):
st = color(Fore.RED, st)
st = color(AnsiFore.RED, st)
msg, m = dump_dict(config, path_, at_root=False)
inf = line_info(config, path_, highlight=config.is_in_error_path(path_))
@@ -1044,7 +1044,7 @@ def dump_dict(
if len(conf) > 80:
conf = f"|-\n{indent(conf)}"
error = config.get_error_for_path(path)
col = Fore.BOLD_RED if error else Fore.KEEP
col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP
ret += color(col, str(conf))
elif isinstance(conf, core.Lambda):
if is_secret(conf):
@@ -1052,13 +1052,13 @@ def dump_dict(
conf = f"!lambda |-\n{indent(str(conf.value))}"
error = config.get_error_for_path(path)
col = Fore.BOLD_RED if error else Fore.KEEP
col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP
ret += color(col, conf)
elif conf is None:
pass
else:
error = config.get_error_for_path(path)
col = Fore.BOLD_RED if error else Fore.KEEP
col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP
ret += color(col, str(conf))
multiline = "\n" in ret
@@ -1100,13 +1100,13 @@ def read_config(command_line_substitutions):
if not CORE.verbose:
res = strip_default_ids(res)
safe_print(color(Fore.BOLD_RED, "Failed config"))
safe_print(color(AnsiFore.BOLD_RED, "Failed config"))
safe_print("")
for path, domain in res.output_paths:
if not res.is_in_error_path(path):
continue
errstr = color(Fore.BOLD_RED, f"{domain}:")
errstr = color(AnsiFore.BOLD_RED, f"{domain}:")
errline = line_info(res, path)
if errline:
errstr += f" {errline}"
@@ -1121,7 +1121,7 @@ def read_config(command_line_substitutions):
safe_print(indent("\n".join(split_dump[:i])))
for err in res.errors:
safe_print(color(Fore.BOLD_RED, err.msg))
safe_print(color(AnsiFore.BOLD_RED, err.msg))
safe_print("")
return None

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.5.0b2"
__version__ = "2025.6.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -7,6 +7,7 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#include "esphome/core/runtime_stats.h"
#include "esphome/core/scheduler.h"
#ifdef USE_BINARY_SENSOR
@@ -234,6 +235,18 @@ class Application {
uint32_t get_loop_interval() const { return this->loop_interval_; }
/** Enable or disable runtime statistics collection.
*
* @param enable Whether to enable runtime statistics collection.
*/
void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
/** Set the interval at which runtime statistics are logged.
*
* @param interval The interval in milliseconds between logging of runtime statistics.
*/
void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
void schedule_dump_config() { this->dump_config_at_ = 0; }
void feed_wdt();

View File

@@ -243,7 +243,13 @@ void PollingComponent::set_update_interval(uint32_t update_interval) { this->upd
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component)
: started_(millis()), component_(component) {}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
uint32_t blocking_time = millis() - this->started_;
uint32_t current_time = millis();
uint32_t blocking_time = current_time - this->started_;
// Record component runtime stats
runtime_stats.record_component_time(this->component_, blocking_time, current_time);
// Original blocking check logic
bool should_warn;
if (this->component_ != nullptr) {
should_warn = this->component_->should_warn_of_blocking(blocking_time);
@@ -254,7 +260,6 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
const char *src = component_ == nullptr ? "<null>" : component_->get_component_source();
ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms).", src, blocking_time);
ESP_LOGW(TAG, "Components should block for at most 30 ms.");
;
}
}

View File

@@ -6,6 +6,7 @@
#include <string>
#include "esphome/core/optional.h"
#include "esphome/core/runtime_stats.h"
namespace esphome {

View File

@@ -0,0 +1,28 @@
#include "esphome/core/runtime_stats.h"
#include "esphome/core/component.h"
namespace esphome {
RuntimeStatsCollector runtime_stats;
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
if (!this->enabled_ || component == nullptr)
return;
const char *component_source = component->get_component_source();
this->component_stats_[component_source].record_time(duration_ms);
// If next_log_time_ is 0, initialize it
if (this->next_log_time_ == 0) {
this->next_log_time_ = current_time + this->log_interval_;
return;
}
if (current_time >= this->next_log_time_) {
this->log_stats_();
this->reset_stats_();
this->next_log_time_ = current_time + this->log_interval_;
}
}
} // namespace esphome

View File

@@ -0,0 +1,161 @@
#pragma once
#include <map>
#include <string>
#include <vector>
#include <cstdint>
#include <algorithm>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome {
static const char *const RUNTIME_TAG = "runtime";
class Component; // Forward declaration
class ComponentRuntimeStats {
public:
ComponentRuntimeStats()
: period_count_(0),
total_count_(0),
period_time_ms_(0),
total_time_ms_(0),
period_max_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Period stats (reset each logging interval)
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
}
// Total stats (persistent until reboot)
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
}
protected:
// Period stats (reset each logging interval)
uint32_t period_count_;
uint32_t period_time_ms_;
uint32_t period_max_time_ms_;
// Total stats (persistent until reboot)
uint32_t total_count_;
uint32_t total_time_ms_;
uint32_t total_max_time_ms_;
};
// For sorting components by run time
struct ComponentStatPair {
std::string name;
const ComponentRuntimeStats *stats;
bool operator>(const ComponentStatPair &other) const {
// Sort by period time as that's what we're displaying in the logs
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
}
};
class RuntimeStatsCollector {
public:
RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
uint32_t get_log_interval() const { return this->log_interval_; }
void set_enabled(bool enabled) { this->enabled_ = enabled; }
bool is_enabled() const { return this->enabled_; }
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
protected:
void log_stats_() {
ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
ESP_LOGI(RUNTIME_TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
// First collect stats we want to display
std::vector<ComponentStatPair> stats_to_display;
for (const auto &it : this->component_stats_) {
const ComponentRuntimeStats &stats = it.second;
if (stats.get_period_count() > 0) {
ComponentStatPair pair = {it.first, &stats};
stats_to_display.push_back(pair);
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
source.c_str(), stats->get_period_count(), stats->get_period_avg_time_ms(),
stats->get_period_max_time_ms(), stats->get_period_time_ms());
}
// Log total stats since boot
ESP_LOGI(RUNTIME_TAG, "Total stats (since boot):");
// Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(),
[](const ComponentStatPair &a, const ComponentStatPair &b) {
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
});
for (const auto &it : stats_to_display) {
const std::string &source = it.name;
const ComponentRuntimeStats *stats = it.stats;
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms",
source.c_str(), stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
}
void reset_stats_() {
for (auto &it : this->component_stats_) {
it.second.reset_period_stats();
}
}
std::map<std::string, ComponentRuntimeStats> component_stats_;
uint32_t log_interval_;
uint32_t next_log_time_;
bool enabled_;
};
// Global instance for runtime stats collection
extern RuntimeStatsCollector runtime_stats;
} // namespace esphome

View File

@@ -1,9 +1,10 @@
from enum import Enum
import logging
from esphome.core import CORE
class AnsiFore:
class AnsiFore(Enum):
KEEP = ""
BLACK = "\033[30m"
RED = "\033[31m"
@@ -26,7 +27,7 @@ class AnsiFore:
BOLD_RESET = "\033[1;39m"
class AnsiStyle:
class AnsiStyle(Enum):
BRIGHT = "\033[1m"
BOLD = "\033[1m"
DIM = "\033[2m"
@@ -35,16 +36,10 @@ class AnsiStyle:
RESET_ALL = "\033[0m"
Fore = AnsiFore()
Style = AnsiStyle()
def color(col: str, msg: str, reset: bool = True) -> bool:
if col and not col.startswith("\033["):
raise ValueError("Color must be value from esphome.log.Fore")
s = str(col) + msg
def color(col: AnsiFore, msg: str, reset: bool = True) -> str:
s = col.value + msg
if reset and col:
s += str(Style.RESET_ALL)
s += AnsiStyle.RESET_ALL.value
return s
@@ -54,20 +49,21 @@ class ESPHomeLogFormatter(logging.Formatter):
fmt += "%(levelname)s %(message)s"
super().__init__(fmt=fmt, style="%")
def format(self, record):
# @override
def format(self, record: logging.LogRecord) -> str:
formatted = super().format(record)
prefix = {
"DEBUG": Fore.CYAN,
"INFO": Fore.GREEN,
"WARNING": Fore.YELLOW,
"ERROR": Fore.RED,
"CRITICAL": Fore.RED,
"DEBUG": AnsiFore.CYAN.value,
"INFO": AnsiFore.GREEN.value,
"WARNING": AnsiFore.YELLOW.value,
"ERROR": AnsiFore.RED.value,
"CRITICAL": AnsiFore.RED.value,
}.get(record.levelname, "")
return f"{prefix}{formatted}{Style.RESET_ALL}"
return f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}"
def setup_log(
log_level=logging.INFO,
log_level: int = logging.INFO,
include_timestamp: bool = False,
) -> None:
import colorama

View File

@@ -28,7 +28,7 @@ from esphome.const import (
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import get_int_env, get_str_env
from esphome.log import Fore, color
from esphome.log import AnsiFore, color
from esphome.util import safe_print
_LOGGER = logging.getLogger(__name__)
@@ -291,7 +291,7 @@ def get_fingerprint(config):
sha1 = hashlib.sha1(cert_der).hexdigest()
safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}")
safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}")
safe_print(
f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}"
)

View File

@@ -9,7 +9,7 @@ import esphome.config_validation as cv
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
from esphome.core import CORE
from esphome.helpers import get_bool_env, write_file
from esphome.log import Fore, color
from esphome.log import AnsiFore, color
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.util import safe_input, safe_print
@@ -219,7 +219,7 @@ def wizard_write(path, **kwargs):
elif board in rtl87xx_boards.BOARDS:
platform = "RTL87XX"
else:
safe_print(color(Fore.RED, f'The board "{board}" is unknown.'))
safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.'))
return False
kwargs["platform"] = platform
hardware = kwargs["platform"]
@@ -274,12 +274,12 @@ def wizard(path):
if not path.endswith(".yaml") and not path.endswith(".yml"):
safe_print(
f"Please make your configuration file {color(Fore.CYAN, path)} have the extension .yaml or .yml"
f"Please make your configuration file {color(AnsiFore.CYAN, path)} have the extension .yaml or .yml"
)
return 1
if os.path.exists(path):
safe_print(
f"Uh oh, it seems like {color(Fore.CYAN, path)} already exists, please delete that file first or chose another configuration file."
f"Uh oh, it seems like {color(AnsiFore.CYAN, path)} already exists, please delete that file first or chose another configuration file."
)
return 2
@@ -298,17 +298,19 @@ def wizard(path):
sleep(3.0)
safe_print()
safe_print_step(1, CORE_BIG)
safe_print(f"First up, please choose a {color(Fore.GREEN, 'name')} for your node.")
safe_print(
f"First up, please choose a {color(AnsiFore.GREEN, 'name')} for your node."
)
safe_print(
"It should be a unique name that can be used to identify the device later."
)
sleep(1)
safe_print(
f"For example, I like calling the node in my living room {color(Fore.BOLD_WHITE, 'livingroom')}."
f"For example, I like calling the node in my living room {color(AnsiFore.BOLD_WHITE, 'livingroom')}."
)
safe_print()
sleep(1)
name = safe_input(color(Fore.BOLD_WHITE, "(name): "))
name = safe_input(color(AnsiFore.BOLD_WHITE, "(name): "))
while True:
try:
@@ -317,7 +319,7 @@ def wizard(path):
except vol.Invalid:
safe_print(
color(
Fore.RED,
AnsiFore.RED,
f'Oh noes, "{name}" isn\'t a valid name. Names can only '
f"include numbers, lower-case letters and hyphens. ",
)
@@ -325,11 +327,13 @@ def wizard(path):
name = strip_accents(name).lower().replace(" ", "-")
name = strip_accents(name).lower().replace("_", "-")
name = "".join(c for c in name if c in ALLOWED_NAME_CHARS)
safe_print(f'Shall I use "{color(Fore.CYAN, name)}" as the name instead?')
safe_print(
f'Shall I use "{color(AnsiFore.CYAN, name)}" as the name instead?'
)
sleep(0.5)
name = default_input("(name [{}]): ", name)
safe_print(f'Great! Your node is now called "{color(Fore.CYAN, name)}".')
safe_print(f'Great! Your node is now called "{color(AnsiFore.CYAN, name)}".')
sleep(1)
safe_print_step(2, ESP_BIG)
safe_print(
@@ -346,7 +350,7 @@ def wizard(path):
sleep(0.5)
safe_print()
platform = safe_input(
color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")
color(AnsiFore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")
)
try:
platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper())
@@ -355,7 +359,9 @@ def wizard(path):
safe_print(
f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.'
)
safe_print(f"Thanks! You've chosen {color(Fore.CYAN, platform)} as your platform.")
safe_print(
f"Thanks! You've chosen {color(AnsiFore.CYAN, platform)} as your platform."
)
safe_print()
sleep(1)
@@ -376,27 +382,29 @@ def wizard(path):
else:
raise NotImplementedError("Unknown platform!")
safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.")
safe_print(
f"Next, I need to know what {color(AnsiFore.GREEN, 'board')} you're using."
)
sleep(0.5)
safe_print(f"Please go to {color(Fore.GREEN, board_link)} and choose a board.")
safe_print(f"Please go to {color(AnsiFore.GREEN, board_link)} and choose a board.")
if platform == "ESP32":
safe_print(f"(Type {color(Fore.GREEN, 'esp01_1m')} for Sonoff devices)")
safe_print(f"(Type {color(AnsiFore.GREEN, 'esp01_1m')} for Sonoff devices)")
safe_print()
# Don't sleep because user needs to copy link
if platform == "ESP32":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcu-32s")}".')
boards_list = esp32_boards.BOARDS.items()
elif platform == "ESP8266":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcuv2")}".')
boards_list = esp8266_boards.BOARDS.items()
elif platform == "BK72XX":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".')
boards_list = bk72xx_boards.BOARDS.items()
elif platform == "RTL87XX":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".')
boards_list = rtl87xx_boards.BOARDS.items()
elif platform == "RP2040":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "rpipicow")}".')
boards_list = rp2040_boards.BOARDS.items()
else:
@@ -409,19 +417,21 @@ def wizard(path):
boards.append(board_id)
while True:
board = safe_input(color(Fore.BOLD_WHITE, "(board): "))
board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): "))
try:
board = vol.All(vol.Lower, vol.Any(*boards))(board)
break
except vol.Invalid:
safe_print(
color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.')
color(
AnsiFore.RED, f'Sorry, I don\'t think the board "{board}" exists.'
)
)
safe_print()
sleep(0.25)
safe_print()
safe_print(f"Way to go! You've chosen {color(Fore.CYAN, board)} as your board.")
safe_print(f"Way to go! You've chosen {color(AnsiFore.CYAN, board)} as your board.")
safe_print()
sleep(1)
@@ -432,19 +442,19 @@ def wizard(path):
safe_print()
sleep(1)
safe_print(
f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
f"First, what's the {color(AnsiFore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
)
sleep(1.5)
safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "Abraham Linksys")}".')
while True:
ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): "))
ssid = safe_input(color(AnsiFore.BOLD_WHITE, "(ssid): "))
try:
ssid = cv.ssid(ssid)
break
except vol.Invalid:
safe_print(
color(
Fore.RED,
AnsiFore.RED,
f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.',
)
)
@@ -452,18 +462,18 @@ def wizard(path):
sleep(1)
safe_print(
f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.'
f'Thank you very much! You\'ve just chosen "{color(AnsiFore.CYAN, ssid)}" as your SSID.'
)
safe_print()
sleep(0.75)
safe_print(
f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
f"Now please state the {color(AnsiFore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
)
safe_print()
safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "PASSWORD42")}"')
sleep(0.5)
psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): "))
psk = safe_input(color(AnsiFore.BOLD_WHITE, "(PSK): "))
safe_print(
"Perfect! WiFi is now set up (you can create static IPs and so on later)."
)
@@ -475,12 +485,12 @@ def wizard(path):
"(over the air) and integrates into Home Assistant with a native API."
)
safe_print(
f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?"
f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?"
)
safe_print()
sleep(0.25)
safe_print("Press ENTER for no password")
password = safe_input(color(Fore.BOLD_WHITE, "(password): "))
password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): "))
else:
ssid, password, psk = "", "", ""
@@ -497,8 +507,8 @@ def wizard(path):
safe_print()
safe_print(
color(Fore.CYAN, "DONE! I've now written a new configuration file to ")
+ color(Fore.BOLD_CYAN, path)
color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ")
+ color(AnsiFore.BOLD_CYAN, path)
)
safe_print()
safe_print("Next steps:")

View File

@@ -64,7 +64,7 @@ lib_deps =
heman/AsyncMqttClient-esphome@1.0.0 ; mqtt
esphome/ESPAsyncWebServer-esphome@3.3.0 ; web_server_base
fastled/FastLED@3.9.16 ; fastled_base
mikalhart/TinyGPSPlus@1.0.2 ; gps
mikalhart/TinyGPSPlus@1.1.0 ; gps
freekode/TM1651@1.0.1 ; tm1651
glmnet/Dsmr@0.7 ; dsmr
rweather/Crypto@0.4.0 ; dsmr

View File

@@ -13,13 +13,13 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.8.1
click==8.1.7
esphome-dashboard==20250514.0
aioesphomeapi==30.2.0
aioesphomeapi==31.0.0
zeroconf==0.147.0
puremagic==1.29
ruamel.yaml==0.18.10 # dashboard_import
esphome-glyphsets==0.2.0
pillow==10.4.0
cairosvg==2.7.1
cairosvg==2.8.1
freetype-py==2.5.1
# esp-idf requires this, but doesn't bundle it by default

View File

@@ -6,6 +6,20 @@ uart:
parity: EVEN
gps:
latitude:
name: "Latitude"
longitude:
name: "Longitude"
altitude:
name: "Altitude"
speed:
name: "Speed"
course:
name: "Course"
satellites:
name: "Satellites"
hdop:
name: "HDOP"
time:
- platform: gps

View File

@@ -14,3 +14,15 @@ display:
id: my_matrix
lambda: |-
it.printdigit("hello");
esphome:
on_boot:
- priority: 100
then:
- max7129digit.invert_off:
- max7129digit.invert_on:
- max7129digit.turn_on:
- max7129digit.turn_off:
- max7129digit.reverse_on:
- max7129digit.reverse_off:
- max7129digit.intensity: 10