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
107 changed files with 1192 additions and 3729 deletions

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@v6.17.0
uses: docker/build-push-action@v6.16.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@v6.17.0
uses: docker/build-push-action@v6.16.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

View File

@@ -18,7 +18,6 @@ jobs:
outputs:
tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@v4.1.7
- name: Get tag
@@ -28,11 +27,6 @@ jobs:
if [[ "${{ github.event_name }}" = "release" ]]; then
TAG="${{ github.event.release.tag_name}}"
BRANCH_BUILD="false"
if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
ENVIRONMENT="beta"
else
ENVIRONMENT="production"
fi
else
TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
today="$(date --utc '+%Y%m%d')"
@@ -41,15 +35,12 @@ jobs:
if [[ "$BRANCH" != "dev" ]]; then
TAG="${TAG}-${BRANCH}"
BRANCH_BUILD="true"
ENVIRONMENT=""
else
BRANCH_BUILD="false"
ENVIRONMENT="dev"
fi
fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length
deploy-pypi:
@@ -242,8 +233,9 @@ jobs:
deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
needs:
- init
- deploy-manifest
steps:
- name: Trigger Workflow
uses: actions/github-script@v7.0.1

1
.gitignore vendored
View File

@@ -143,4 +143,3 @@ sdkconfig.*
/components
/managed_components
api-docs/

2877
Doxyfile

File diff suppressed because it is too large Load Diff

View File

@@ -11,9 +11,7 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*"
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14
RUN pip install uv==0.6.14
COPY requirements.txt /

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;

View File

@@ -4,7 +4,6 @@
#include <cinttypes>
#include <utility>
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -27,6 +26,7 @@ namespace esphome {
namespace api {
static const char *const TAG = "api.connection";
static const char *const STATS_TAG = "api.stats";
static const int ESP32_CAMERA_STOP_STREAM = 5000;
// helper for allowing only unique entries in the queue
@@ -63,6 +63,11 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
: parent_(parent), deferred_message_queue_(this), initial_state_iterator_(this), list_entities_iterator_(this) {
this->proto_write_buffer_.reserve(64);
// Explicitly initialize stats
this->stats_enabled_ = true;
this->next_stats_log_ = 0;
ESP_LOGD(STATS_TAG, "API Connection created with stats_enabled_=true");
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx->has_psk()) {
@@ -79,11 +84,7 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
#endif
}
void APIConnection::start() {
this->last_traffic_ = App.get_loop_component_start_time();
// Set next_ping_retry_ to prevent immediate ping
// This ensures the first ping happens after the keepalive period
this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS;
this->last_traffic_ = millis();
APIError err = this->helper_->init();
if (err != APIError::OK) {
@@ -97,6 +98,64 @@ void APIConnection::start() {
this->helper_->set_log_info(this->client_info_);
}
void APIConnection::log_section_stats_() {
ESP_LOGI(STATS_TAG, "API Connection Section Runtime Statistics");
ESP_LOGI(STATS_TAG, "Period stats (last %" PRIu32 "ms):", this->stats_log_interval_);
if (this->section_stats_.empty()) {
ESP_LOGW(STATS_TAG, "No section stats collected yet");
return;
}
// First collect stats we want to display
std::vector<std::pair<std::string, const APISectionStats *>> stats_to_display;
for (const auto &it : this->section_stats_) {
const APISectionStats &stats = it.second;
if (stats.get_period_count() > 0) {
stats_to_display.push_back({it.first, &stats});
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), [](const auto &a, const auto &b) {
return a.second->get_period_time_ms() > b.second->get_period_time_ms();
});
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const std::string &section = it.first;
const APISectionStats *stats = it.second;
ESP_LOGI(STATS_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.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(STATS_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 auto &a, const auto &b) { return a.second->get_total_time_ms() > b.second->get_total_time_ms(); });
for (const auto &it : stats_to_display) {
const std::string &section = it.first;
const APISectionStats *stats = it.second;
ESP_LOGI(STATS_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.c_str(),
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
}
void APIConnection::reset_section_stats_() {
ESP_LOGD(STATS_TAG, "Resetting API section stats, sections count: %u", this->section_stats_.size());
for (auto &it : this->section_stats_) {
it.second.reset_period_stats();
}
}
APIConnection::~APIConnection() {
#ifdef USE_BLUETOOTH_PROXY
if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
@@ -111,6 +170,9 @@ APIConnection::~APIConnection() {
}
void APIConnection::loop() {
// Measure total time for entire loop function
const uint32_t loop_start_time = millis();
if (this->remove_)
return;
@@ -128,15 +190,30 @@ void APIConnection::loop() {
return;
}
const uint32_t now = millis();
uint32_t start_time;
uint32_t duration;
// Section: Helper Loop
start_time = millis();
APIError err = this->helper_->loop();
duration = millis() - start_time;
this->section_stats_["helper_loop"].record_time(duration);
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
api_error_to_str(err), errno);
return;
}
// Section: Read Packet
start_time = millis();
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
duration = millis() - start_time;
this->section_stats_["read_packet"].record_time(duration);
if (err == APIError::WOULD_BLOCK) {
// pass
} else if (err != APIError::OK) {
@@ -151,36 +228,48 @@ void APIConnection::loop() {
}
return;
} else {
this->last_traffic_ = App.get_loop_component_start_time();
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
this->last_traffic_ = now;
// Section: Process Message
start_time = millis();
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
duration = millis() - start_time;
this->section_stats_["process_message"].record_time(duration);
if (this->remove_)
return;
}
// Section: Process Queue
start_time = millis();
if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) {
this->deferred_message_queue_.process_queue();
}
duration = millis() - start_time;
this->section_stats_["process_queue"].record_time(duration);
// Section: Iterator Advance
start_time = millis();
if (!this->list_entities_iterator_.completed())
this->list_entities_iterator_.advance();
if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
this->initial_state_iterator_.advance();
duration = millis() - start_time;
this->section_stats_["iterator_advance"].record_time(duration);
// Section: Keepalive
start_time = millis();
static uint32_t keepalive = 60000;
static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000;
const uint32_t now = App.get_loop_component_start_time();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {
if (now - this->last_traffic_ > (keepalive * 5) / 2) {
on_fatal_error();
ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
}
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) {
} else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) {
ESP_LOGVV(TAG, "Sending keepalive PING...");
this->sent_ping_ = this->send_ping_request(PingRequest());
if (!this->sent_ping_) {
@@ -199,8 +288,12 @@ void APIConnection::loop() {
}
}
}
duration = millis() - start_time;
this->section_stats_["keepalive"].record_time(duration);
#ifdef USE_ESP32_CAMERA
// Section: Camera
start_time = millis();
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
// Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
@@ -239,8 +332,12 @@ void APIConnection::loop() {
this->image_reader_.return_image();
}
}
duration = millis() - start_time;
this->section_stats_["camera"].record_time(duration);
#endif
// Section: State Subscriptions
start_time = millis();
if (state_subs_at_ != -1) {
const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= (int) subs.size()) {
@@ -256,6 +353,43 @@ void APIConnection::loop() {
}
}
}
duration = millis() - start_time;
this->section_stats_["state_subs"].record_time(duration);
// Log stats periodically
if (this->stats_enabled_) {
// If next_stats_log_ is 0, initialize it
if (this->next_stats_log_ == 0) {
this->next_stats_log_ = now + this->stats_log_interval_;
ESP_LOGI(STATS_TAG, "API section stats logging enabled, next log at %u", this->next_stats_log_);
} else if (now >= this->next_stats_log_) {
ESP_LOGI(STATS_TAG, "Logging API section stats now (current time: %u, scheduled time: %u)", now,
this->next_stats_log_);
// Force logging even if no stats are collected yet
ESP_LOGI(STATS_TAG, "Stats collection status: enabled=%d, sections=%u", this->stats_enabled_,
this->section_stats_.size());
// Explicitly log some stats we know should exist
ESP_LOGI(STATS_TAG, "Record count for key sections: helper_loop=%u, read_packet=%u, total_loop=%u",
this->section_stats_["helper_loop"].get_period_count(),
this->section_stats_["read_packet"].get_period_count(),
this->section_stats_["total_loop"].get_period_count());
this->log_section_stats_();
this->reset_section_stats_();
this->next_stats_log_ = now + this->stats_log_interval_;
ESP_LOGI(STATS_TAG, "Next API section stats log scheduled for %u", this->next_stats_log_);
}
}
// Record total loop execution time
const uint32_t total_loop_duration = millis() - loop_start_time;
this->section_stats_["total_loop"].record_time(total_loop_duration);
// Log a warning if the loop takes longer than 30ms
if (total_loop_duration > 30) {
ESP_LOGW(STATS_TAG, "API loop took %ums, which exceeds the recommended 30ms limit", total_loop_duration);
}
}
std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) {
@@ -1648,11 +1782,24 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false;
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse
// Track send_buffer time
const uint32_t start_time = millis();
if (this->remove_)
return false;
uint32_t check_block_start = millis();
if (!this->try_to_clear_buffer(true)) {
return false;
}
uint32_t check_block_duration = millis() - check_block_start;
this->section_stats_["try_to_clear_buffer"].record_time(check_block_duration);
uint32_t write_start = millis();
APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
uint32_t write_duration = millis() - write_start;
this->section_stats_["write_packet"].record_time(write_duration);
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
if (err == APIError::WOULD_BLOCK)
return false;
if (err != APIError::OK) {
@@ -1665,6 +1812,17 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
}
return false;
}
// Measure total send_buffer function time
uint32_t total_duration = millis() - start_time;
this->section_stats_["send_buffer_total"].record_time(total_duration);
// Log a warning if send_buffer takes longer than 15ms
if (total_duration > 15) {
ESP_LOGW(STATS_TAG, "send_buffer took %ums (message_type=%u, size=%u)", total_duration, message_type,
buffer.get_buffer()->size());
}
// Do not set last_traffic_ on send
return true;
}

View File

@@ -11,13 +11,14 @@
#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 {
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
using send_message_t = bool (APIConnection::*)(void *);
/*
@@ -64,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();
@@ -408,14 +473,7 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear();
// Get header padding size - used for both reserve and insert
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
this->proto_write_buffer_.reserve(reserve_size);
return {&this->proto_write_buffer_};
}
bool try_to_clear_buffer(bool log_out_of_space);
@@ -556,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;
@@ -493,12 +524,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
std::vector<uint8_t> data;
data.resize(reason.length() + 1);
data[0] = 0x01; // failure
// Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i];
}
// temporarily remove failed state
auto orig_state = state_;
state_ = State::EXPLICIT_REJECT;
@@ -559,8 +587,7 @@ 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_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err;
APIError aerr;
aerr = state_action_();
@@ -572,36 +599,31 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
return APIError::WOULD_BLOCK;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding
size_t payload_len = raw_buffer->size() - frame_header_padding_;
size_t padding = 0;
size_t msg_len = 4 + payload_len + padding;
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
if (tmpbuf == nullptr) {
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
// We need to resize to include MAC space, but we already reserved it in create_buffer
raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
// Write the noise header in the padded area
// Buffer layout:
// [0] - 0x01 indicator byte
// [1-2] - Size of encrypted payload (filled after encryption)
// [3-4] - Message type (encrypted)
// [5-6] - Payload length (encrypted)
// [7...] - Actual payload data (encrypted)
uint8_t *buf_start = raw_buffer->data();
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set later after encryption
tmpbuf[0] = 0x01; // indicator
// tmpbuf[1], tmpbuf[2] to be set later
const uint8_t msg_offset = 3;
buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
// payload data is already in the buffer starting at position 7
const uint8_t payload_offset = msg_offset + 4;
tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type
tmpbuf[msg_offset + 1] = (uint8_t) type;
tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len
tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
// The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) {
state_ = State::FAILED;
@@ -610,37 +632,15 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
}
size_t total_len = 3 + mbuf.size;
buf_start[1] = (uint8_t) (mbuf.size >> 8);
buf_start[2] = (uint8_t) mbuf.size;
tmpbuf[1] = (uint8_t) (mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size;
struct iovec iov;
// Point iov_base to the beginning of the buffer (no unused padding in Noise)
// We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
iov.iov_base = buf_start;
iov.iov_base = &tmpbuf[0];
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];
@@ -652,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.
@@ -728,8 +728,6 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
return APIError::HANDSHAKESTATE_SPLIT_FAILED;
}
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_);
handshake_ = nullptr;
@@ -752,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) {
@@ -778,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);
@@ -814,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
@@ -930,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;
@@ -990,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;
}
@@ -1001,108 +977,30 @@ 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_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding (frame_header_padding_ = 6)
size_t payload_len = raw_buffer->size() - frame_header_padding_;
std::vector<uint8_t> header;
header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) +
api::ProtoSize::varint(static_cast<uint32_t>(type)));
header.push_back(0x00);
ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header);
// Calculate varint sizes for header components
size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
size_t total_header_len = 1 + size_varint_len + type_varint_len;
if (total_header_len > frame_header_padding_) {
// Header is too large to fit in the padding
return APIError::BAD_ARG;
struct iovec iov[2];
iov[0].iov_base = &header[0];
iov[0].iov_len = header.size();
if (payload_len == 0) {
return APIFrameHelper::write_raw_(iov, 1);
}
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
// Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
uint8_t *buf_start = raw_buffer->data();
size_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode size varint directly into buffer
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
// Encode type varint directly into buffer
ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
struct iovec iov;
// Point iov_base to the beginning of our header (skip unused padding)
// This ensures we only send the actual header and payload, not the empty padding bytes
iov.iov_base = buf_start + header_offset;
iov.iov_len = total_header_len + payload_len;
return write_raw_(&iov, 1);
}
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

@@ -16,8 +16,6 @@
namespace esphome {
namespace api {
class ProtoWriteBuffer;
struct ReadPacketBuffer {
std::vector<uint8_t> container;
uint16_t type;
@@ -62,83 +60,134 @@ 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;
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 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;
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;
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;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
void set_log_info(std::string info) { info_ = std::move(info); }
protected:
// 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);
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
// 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
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(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
: 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_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) 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); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
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
@@ -147,7 +196,6 @@ class APINoiseFrameHelper : public APIFrameHelper {
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_;
@@ -155,64 +203,21 @@ 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)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
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_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) 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); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
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_;
// 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:
@@ -229,15 +234,6 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
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

@@ -83,34 +83,6 @@ class ProtoVarInt {
return static_cast<int64_t>(this->value_ >> 1);
}
}
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {

View File

@@ -14,8 +14,11 @@ namespace esphome {
namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice {
public:
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public:
void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s;
s->turn_on();
@@ -45,9 +48,6 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected:
#ifdef USE_SWITCH
switch_::Switch *rf_power_switch_{nullptr};
#endif
int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */

View File

@@ -7,7 +7,7 @@ CODEOWNERS = ["@bazuchan"]
ballu_ns = cg.esphome_ns.namespace("ballu")
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate)
async def to_code(config):

View File

@@ -3,7 +3,6 @@
#include "bedjet_hub.h"
#include "bedjet_child.h"
#include "bedjet_const.h"
#include "esphome/core/application.h"
#include <cinttypes>
namespace esphome {

View File

@@ -15,21 +15,17 @@ void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, false);
this->send_state_internal(state);
} else {
this->filter_list_->input(state, false);
this->filter_list_->input(state);
}
}
void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
this->has_state_ = false;
this->publish_state(state);
}
void BinarySensor::send_state_internal(bool state, bool is_initial) {
void BinarySensor::send_state_internal(bool state) {
bool is_initial = !this->has_state_;
if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else {

View File

@@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool state, bool is_initial);
void send_state_internal(bool state);
/// Return whether this binary sensor has outputted a state.
virtual bool has_state() const;

View File

@@ -9,37 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter";
void Filter::output(bool value, bool is_initial) {
void Filter::output(bool value) {
if (!this->dedup_.next(value))
return;
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial);
this->parent_->send_state_internal(value);
} else {
this->next_->input(value, is_initial);
this->next_->input(value);
}
}
void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value, is_initial);
void Filter::input(bool value) {
auto b = this->new_value(value);
if (b.has_value()) {
this->output(*b, is_initial);
this->output(*b);
}
}
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOnOffFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
} else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
}
return {};
}
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOnFilter::new_value(bool value) {
if (value) {
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
return {};
} else {
this->cancel_timeout("ON");
@@ -49,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
optional<bool> DelayedOffFilter::new_value(bool value) {
if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
return {};
} else {
this->cancel_timeout("OFF");
@@ -61,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) {
// Ignore if already running
if (this->active_timing_ != 0)
@@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val, false); // This is at least the second one so not initial
this->output(val);
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
}
@@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
optional<bool> SettleFilter::new_value(bool value) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->steady_ = true;
this->output(value, is_initial);
this->output(value);
});
return {};
} else {
this->steady_ = false;
this->output(value, is_initial);
this->output(value);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}

View File

@@ -14,11 +14,11 @@ class BinarySensor;
class Filter {
public:
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
virtual optional<bool> new_value(bool value) = 0;
void input(bool value, bool is_initial);
void input(bool value);
void output(bool value, bool is_initial);
void output(bool value);
protected:
friend BinarySensor;
@@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
};
struct AutorepeatFilterTiming {
@@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;
@@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
protected:
std::function<optional<bool>(bool)> f_;
@@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value, bool is_initial) override;
optional<bool> new_value(bool value) override;
float get_setup_priority() const override;

View File

@@ -2,7 +2,6 @@
#include "esphome/core/log.h"
#include "esphome/core/macros.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32
@@ -178,7 +177,7 @@ void BluetoothProxy::loop() {
// Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0;
uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {

View File

@@ -40,7 +40,7 @@ def climate_ir_schema(
)
def climate_ir_with_receiver_schema(
def climare_ir_with_receiver_schema(
class_: MockObjClass,
) -> cv.Schema:
return climate_ir_schema(class_).extend(
@@ -59,7 +59,7 @@ def deprecated_schema_constant(config):
type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"Please use `climate_ir.climare_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
@@ -68,7 +68,7 @@ def deprecated_schema_constant(config):
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climare_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)

View File

@@ -13,7 +13,7 @@ CONF_BIT_HIGH = "bit_high"
CONF_BIT_ONE_LOW = "bit_one_low"
CONF_BIT_ZERO_LOW = "bit_zero_low"
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(LgIrClimate).extend(
{
cv.Optional(
CONF_HEADER_HIGH, default="8000us"

View File

@@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"]
coolix_ns = cg.esphome_ns.namespace("coolix")
CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(CoolixClimate)
async def to_code(config):

View File

@@ -1,6 +1,5 @@
#include "cse7766.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace cse7766 {
@@ -8,7 +7,7 @@ namespace cse7766 {
static const char *const TAG = "cse7766";
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index.
this->raw_data_index_ = 0;

View File

@@ -1,7 +1,6 @@
#include "current_based_cover.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cfloat>
namespace esphome {
@@ -61,7 +60,7 @@ void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction

View File

@@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
daikin_ns = cg.esphome_ns.namespace("daikin")
DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinClimate)
async def to_code(config):

View File

@@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc")
DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinArcClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinArcClimate)
async def to_code(config):

View File

@@ -9,7 +9,7 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc")
DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinBrcClimate).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DaikinBrcClimate).extend(
{
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}

View File

@@ -1,7 +1,6 @@
#include "daly_bms.h"
#include <vector>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace daly_bms {
@@ -33,7 +32,7 @@ void DalyBmsComponent::update() {
}
void DalyBmsComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (this->receiving_ && (now - this->last_transmission_ >= 200)) {
// last transmission too long ago. Reset RX index.
ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index.");

View File

@@ -70,7 +70,7 @@ void DebugComponent::loop() {
#ifdef USE_SENSOR
// calculate loop time - from last call to this one
if (this->loop_time_sensor_ != nullptr) {
uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
uint32_t loop_time = now - this->last_loop_timetag_;
this->max_loop_time_ = std::max(this->max_loop_time_, loop_time);
this->last_loop_timetag_ = now;

View File

@@ -34,15 +34,13 @@ class DebugComponent : public PollingComponent {
#endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor;
}
#endif // USE_SENSOR
#ifdef USE_ESP32
void on_shutdown() override;
#endif // USE_ESP32
protected:
uint32_t free_heap_{};

View File

@@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
delonghi_ns = cg.esphome_ns.namespace("delonghi")
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DelonghiClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(DelonghiClimate)
async def to_code(config):

View File

@@ -7,7 +7,7 @@ AUTO_LOAD = ["climate_ir"]
emmeti_ns = cg.esphome_ns.namespace("emmeti")
EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(EmmetiClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(EmmetiClimate)
async def to_code(config):

View File

@@ -1,7 +1,6 @@
#include "endstop_cover.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
namespace esphome {
namespace endstop {
@@ -66,7 +65,7 @@ void EndstopCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) {
float dur = (now - this->start_dir_time_) / 1e3f;

View File

@@ -15,9 +15,8 @@
#ifdef USE_ARDUINO
#include <Esp.h>
#else
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_clk_tree.h>
#endif
void setup();
void loop();
#endif
@@ -64,13 +63,7 @@ uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
#ifdef USE_ESP_IDF
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#else
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
freq = config.freq_mhz * 1000000U;
#endif
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif

View File

@@ -6,7 +6,6 @@
#include <cstring>
#include "ble_uuid.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace esp32_ble {
@@ -144,7 +143,7 @@ void BLEAdvertising::loop() {
if (this->raw_advertisements_callbacks_.empty()) {
return;
}
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop();
this->current_adv_index_ += 1;

View File

@@ -296,7 +296,7 @@ async def to_code(config):
add_idf_component(
name="esp32-camera",
repo="https://github.com/espressif/esp32-camera.git",
ref="v2.0.15",
ref="v2.0.9",
)
for conf in config.get(CONF_ON_STREAM_START, []):

View File

@@ -3,7 +3,6 @@
#include "esp32_camera.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include <freertos/task.h>
@@ -55,7 +54,11 @@ void ESP32Camera::dump_config() {
ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href);
ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk);
ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz);
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl);
#else
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl);
#endif
ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset);
switch (this->config_.frame_size) {
case FRAMESIZE_QQVGA:
@@ -159,7 +162,7 @@ void ESP32Camera::loop() {
}
// request idle image every idle_update_interval
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now;
this->request_image(IDLE);
@@ -235,8 +238,13 @@ void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) {
this->config_.xclk_freq_hz = frequency;
}
void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) {
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
this->config_.pin_sscb_sda = sda;
this->config_.pin_sscb_scl = scl;
#else
this->config_.pin_sccb_sda = sda;
this->config_.pin_sccb_scl = scl;
#endif
}
void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@@ -92,7 +92,7 @@ void ESP32ImprovComponent::loop() {
if (!this->incoming_data_.empty())
this->process_incoming_data_();
uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
switch (this->state_) {
case improv::STATE_STOPPED:

View File

@@ -288,7 +288,7 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) {
}
void ESP32TouchComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
for (auto *child : this->children_) {
child->value_ = this->component_touch_pad_read(child->get_touch_pad());

View File

@@ -240,7 +240,7 @@ void EthernetComponent::setup() {
}
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
switch (this->state_) {
case EthernetComponentState::STOPPED:

View File

@@ -1,7 +1,6 @@
#include "feedback_cover.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace feedback {
@@ -221,7 +220,7 @@ void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_o
void FeedbackCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
// Recompute position every loop cycle
this->recompute_position_();

View File

@@ -8,7 +8,7 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_(
"FujitsuGeneralClimate", climate_ir.ClimateIR
)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FujitsuGeneralClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(FujitsuGeneralClimate)
async def to_code(config):

View File

@@ -6,7 +6,6 @@
*/
#include "gcja5.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cstring>
namespace esphome {
@@ -17,7 +16,7 @@ static const char *const TAG = "gcja5";
void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); }
void GCJA5Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index.
this->rx_message_.clear();

View File

@@ -21,7 +21,7 @@ MODELS = {
"yag": Model.GREE_YAG,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(GreeClimate).extend(
{
cv.Required(CONF_MODEL): cv.enum(MODELS),
}

View File

@@ -1,6 +1,5 @@
#include "growatt_solar.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace growatt_solar {
@@ -19,7 +18,7 @@ void GrowattSolar::loop() {
void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
const uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
if (now - this->last_send_ < this->get_update_interval() / 2) {
return;
}

View File

@@ -97,7 +97,7 @@ VERTICAL_DIRECTIONS = {
}
CONFIG_SCHEMA = cv.All(
climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend(
climate_ir.climare_ir_with_receiver_schema(HeatpumpIRClimate).extend(
{
cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS),
cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS),

View File

@@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344")
HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate)
async def to_code(config):

View File

@@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424")
HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(HitachiClimate)
async def to_code(config):

View File

@@ -1,6 +1,5 @@
#include "kuntze.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace kuntze {
@@ -61,7 +60,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
}
void Kuntze::loop() {
uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
// timeout after 15 seconds
if (this->waiting_ && (now - this->last_send_ > 15000)) {
ESP_LOGW(TAG, "timed out waiting for response");

View File

@@ -24,7 +24,6 @@ from esphome.const import (
CONF_HARDWARE_UART,
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
CONF_LOGS,
CONF_ON_MESSAGE,
CONF_TAG,
@@ -248,7 +247,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
baud_rate = config[CONF_BAUD_RATE]
level = config[CONF_LEVEL]
CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
log = cg.new_Pvariable(
config[CONF_ID],

View File

@@ -5,7 +5,7 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_
from esphome.core import CORE
from esphome.cpp_helpers import register_component, register_parented
from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns
from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns
CODEOWNERS = ["@clydebarrow"]
@@ -21,10 +21,9 @@ CONFIG_SCHEMA = select.select_schema(
async def to_code(config):
parent = await cg.get_variable(config[CONF_LOGGER_ID])
levels = list(LOG_LEVELS)
index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL])
levels = LOG_LEVEL_SEVERITY
index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL])
levels = levels[: index + 1]
var = await select.new_select(config, options=levels)
await register_parented(var, parent)
await register_parented(var, config[CONF_LOGGER_ID])
await register_component(var, config)

View File

@@ -36,43 +36,29 @@ from .types import (
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
TIME_TEXT_SCHEMA = cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
)
PRINTF_TEXT_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
},
),
validate_printf,
)
def _validate_text(value):
"""
Do some sanity checking of the format to get better error messages
than using cv.Any
"""
if value is None:
raise cv.Invalid("No text specified")
if isinstance(value, dict):
if CONF_TIME_FORMAT in value:
return TIME_TEXT_SCHEMA(value)
return PRINTF_TEXT_SCHEMA(value)
return cv.templatable(cv.string)(value)
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT): _validate_text,
cv.Optional(CONF_TEXT): cv.Any(
cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
cv.lambda_
),
},
),
validate_printf,
),
cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
),
cv.templatable(cv.string),
)
}
)

View File

@@ -1,6 +1,5 @@
#include "matrix_keypad.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace matrix_keypad {
@@ -29,7 +28,7 @@ void MatrixKeypad::setup() {
void MatrixKeypad::loop() {
static uint32_t active_start = 0;
static int active_key = -1;
uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
int key = -1;
bool error = false;
int pos = 0, row, col;

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

@@ -2,7 +2,6 @@
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include "max7219font.h"
#include <algorithm>
@@ -64,7 +63,7 @@ void MAX7219Component::dump_config() {
}
void MAX7219Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
const uint32_t millis_since_last_scroll = now - this->last_scroll_;
const size_t first_line_size = this->max_displaybuffer_[0].size();
// check if the buffer has shrunk past the current position since last update

View File

@@ -147,11 +147,7 @@ bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCES
this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability;
this->unprocessed_probability_status_ = true;
}
if (this->recent_streaming_probabilities_[this->last_n_index_] < this->probability_cutoff_) {
// Only increment ignore windows if less than the probability cutoff; this forces the model to "cool-off" from a
// previous detection and calling ``reset_probabilities`` so it avoids duplicate detections
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
}
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
}
return true;
}

View File

@@ -10,7 +10,7 @@ midea_ir_ns = cg.esphome_ns.namespace("midea_ir")
MideaIR = midea_ir_ns.class_("MideaIR", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MideaIR).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MideaIR).extend(
{
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}

View File

@@ -43,7 +43,7 @@ VERTICAL_DIRECTIONS = {
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(MitsubishiClimate).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(MitsubishiClimate).extend(
{
cv.Optional(CONF_SET_FAN_MODE, default="3levels"): cv.enum(SETFANMODE),
cv.Optional(CONF_SUPPORTS_DRY, default=False): cv.boolean,

View File

@@ -1,7 +1,6 @@
#include "modbus.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "esphome/core/application.h"
namespace esphome {
namespace modbus {
@@ -14,7 +13,7 @@ void Modbus::setup() {
}
}
void Modbus::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
while (this->available()) {
uint8_t byte;

View File

@@ -345,7 +345,7 @@ void MQTTClientComponent::loop() {
this->disconnect_reason_.reset();
}
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
switch (this->state_) {
case MQTT_CLIENT_DISABLED:

View File

@@ -6,7 +6,7 @@ AUTO_LOAD = ["climate_ir"]
noblex_ns = cg.esphome_ns.namespace("noblex")
NoblexClimate = noblex_ns.class_("NoblexClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(NoblexClimate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(NoblexClimate)
async def to_code(config):

View File

@@ -75,7 +75,7 @@ class PNGFormat(Format):
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.1.0")
cg.add_library("pngle", "1.0.2")
IMAGE_FORMATS = {

View File

@@ -34,32 +34,12 @@ static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) {
* @param h The height of the rectangle to draw.
* @param rgba The color to paint the rectangle in.
*/
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color);
}
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
{
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
if (!pngle) {
ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!");
return;
}
memset(pngle, 0, PNGLE_T_SIZE);
pngle_reset(pngle);
this->pngle_ = pngle;
}
}
PngDecoder::~PngDecoder() {
if (this->pngle_) {
pngle_reset(this->pngle_);
this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE);
}
}
int PngDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size);
if (!this->pngle_) {

View File

@@ -1,8 +1,7 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "image_decoder.h"
#include "esphome/core/defines.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h>
@@ -19,14 +18,13 @@ class PngDecoder : public ImageDecoder {
*
* @param display The image to decode the stream into.
*/
PngDecoder(OnlineImage *image);
~PngDecoder() override;
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {}
~PngDecoder() override { pngle_destroy(this->pngle_); }
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
protected:
RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_;
};

View File

@@ -1,6 +1,5 @@
#include "pmsx003.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace pmsx003 {
@@ -43,7 +42,7 @@ void PMSX003Component::dump_config() {
}
void PMSX003Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
// If we update less often than it takes the device to stabilise, spin the fan down
// rather than running it constantly. It does take some time to stabilise, so we

View File

@@ -1,6 +1,5 @@
#include "pzem004t.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cinttypes>
namespace esphome {
@@ -17,7 +16,7 @@ void PZEM004T::setup() {
}
void PZEM004T::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (now - this->last_read_ > 500 && this->available() < 7) {
while (this->available())
this->read();

View File

@@ -1,6 +1,5 @@
#include "rf_bridge.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cinttypes>
#include <cstring>
@@ -129,7 +128,7 @@ void RFBridgeComponent::write_byte_str_(const std::string &codes) {
}
void RFBridgeComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (now - this->last_bridge_byte_ > 50) {
this->rx_buffer_.clear();
this->last_bridge_byte_ = now;

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

@@ -1,6 +1,5 @@
#include "sds011.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace sds011 {
@@ -76,7 +75,7 @@ void SDS011Component::dump_config() {
}
void SDS011Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if ((now - this->last_transmission_ >= 500) && this->data_index_) {
// last transmission too long ago. Reset RX index.
ESP_LOGV(TAG, "Last transmission too long ago. Reset RX index.");

View File

@@ -25,10 +25,6 @@ static const uint16_t SEN5X_CMD_TEMPERATURE_COMPENSATION = 0x60B2;
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_STATE = 0x6181;
static const uint16_t SEN5X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
static const int8_t SEN5X_INDEX_SCALE_FACTOR = 10; // used for VOC and NOx index values
static const int8_t SEN5X_MIN_INDEX_VALUE = 1 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
static const int16_t SEN5X_MAX_INDEX_VALUE = 500 * SEN5X_INDEX_SCALE_FACTOR; // must be adjusted by the scale factor
void SEN5XComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up sen5x...");
@@ -92,9 +88,8 @@ void SEN5XComponent::setup() {
product_name_.push_back(current_char);
// second char
current_char = *current_int & 0xFF;
if (current_char) {
if (current_char)
product_name_.push_back(current_char);
}
}
current_int++;
} while (current_char && --max);
@@ -276,10 +271,10 @@ void SEN5XComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Low RH/T acceleration mode");
break;
case MEDIUM_ACCELERATION:
ESP_LOGCONFIG(TAG, " Medium RH/T acceleration mode");
ESP_LOGCONFIG(TAG, " Medium RH/T accelertion mode");
break;
case HIGH_ACCELERATION:
ESP_LOGCONFIG(TAG, " High RH/T acceleration mode");
ESP_LOGCONFIG(TAG, " High RH/T accelertion mode");
break;
}
}
@@ -342,61 +337,47 @@ void SEN5XComponent::update() {
ESP_LOGD(TAG, "read data error (%d)", this->last_error_);
return;
}
float pm_1_0 = measurements[0] / 10.0;
if (measurements[0] == 0xFFFF)
pm_1_0 = NAN;
float pm_2_5 = measurements[1] / 10.0;
if (measurements[1] == 0xFFFF)
pm_2_5 = NAN;
float pm_4_0 = measurements[2] / 10.0;
if (measurements[2] == 0xFFFF)
pm_4_0 = NAN;
float pm_10_0 = measurements[3] / 10.0;
if (measurements[3] == 0xFFFF)
pm_10_0 = NAN;
float humidity = measurements[4] / 100.0;
if (measurements[4] == 0xFFFF)
humidity = NAN;
float temperature = (int16_t) measurements[5] / 200.0;
if (measurements[5] == 0xFFFF)
temperature = NAN;
float voc = measurements[6] / 10.0;
if (measurements[6] == 0xFFFF)
voc = NAN;
float nox = measurements[7] / 10.0;
if (measurements[7] == 0xFFFF)
nox = NAN;
ESP_LOGVV(TAG, "pm_1_0 = 0x%.4x", measurements[0]);
float pm_1_0 = measurements[0] == UINT16_MAX ? NAN : measurements[0] / 10.0f;
ESP_LOGVV(TAG, "pm_2_5 = 0x%.4x", measurements[1]);
float pm_2_5 = measurements[1] == UINT16_MAX ? NAN : measurements[1] / 10.0f;
ESP_LOGVV(TAG, "pm_4_0 = 0x%.4x", measurements[2]);
float pm_4_0 = measurements[2] == UINT16_MAX ? NAN : measurements[2] / 10.0f;
ESP_LOGVV(TAG, "pm_10_0 = 0x%.4x", measurements[3]);
float pm_10_0 = measurements[3] == UINT16_MAX ? NAN : measurements[3] / 10.0f;
ESP_LOGVV(TAG, "humidity = 0x%.4x", measurements[4]);
float humidity = measurements[4] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[4]) / 100.0f;
ESP_LOGVV(TAG, "temperature = 0x%.4x", measurements[5]);
float temperature = measurements[5] == INT16_MAX ? NAN : static_cast<int16_t>(measurements[5]) / 200.0f;
ESP_LOGVV(TAG, "voc = 0x%.4x", measurements[6]);
int16_t voc_idx = static_cast<int16_t>(measurements[6]);
float voc = (voc_idx < SEN5X_MIN_INDEX_VALUE || voc_idx > SEN5X_MAX_INDEX_VALUE)
? NAN
: static_cast<float>(voc_idx) / 10.0f;
ESP_LOGVV(TAG, "nox = 0x%.4x", measurements[7]);
int16_t nox_idx = static_cast<int16_t>(measurements[7]);
float nox = (nox_idx < SEN5X_MIN_INDEX_VALUE || nox_idx > SEN5X_MAX_INDEX_VALUE)
? NAN
: static_cast<float>(nox_idx) / 10.0f;
if (this->pm_1_0_sensor_ != nullptr) {
if (this->pm_1_0_sensor_ != nullptr)
this->pm_1_0_sensor_->publish_state(pm_1_0);
}
if (this->pm_2_5_sensor_ != nullptr) {
if (this->pm_2_5_sensor_ != nullptr)
this->pm_2_5_sensor_->publish_state(pm_2_5);
}
if (this->pm_4_0_sensor_ != nullptr) {
if (this->pm_4_0_sensor_ != nullptr)
this->pm_4_0_sensor_->publish_state(pm_4_0);
}
if (this->pm_10_0_sensor_ != nullptr) {
if (this->pm_10_0_sensor_ != nullptr)
this->pm_10_0_sensor_->publish_state(pm_10_0);
}
if (this->temperature_sensor_ != nullptr) {
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
}
if (this->humidity_sensor_ != nullptr) {
if (this->humidity_sensor_ != nullptr)
this->humidity_sensor_->publish_state(humidity);
}
if (this->voc_sensor_ != nullptr) {
if (this->voc_sensor_ != nullptr)
this->voc_sensor_->publish_state(voc);
}
if (this->nox_sensor_ != nullptr) {
if (this->nox_sensor_ != nullptr)
this->nox_sensor_->publish_state(nox);
}
this->status_clear_warning();
});
}

View File

@@ -1,6 +1,5 @@
#include "slow_pwm_output.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace slow_pwm {
@@ -40,7 +39,7 @@ void SlowPWMOutput::set_output_state_(bool new_state) {
}
void SlowPWMOutput::loop() {
uint32_t now = App.get_loop_component_start_time();
uint32_t now = millis();
float scaled_state = this->state_ * this->period_;
if (now - this->period_start_time_ >= this->period_) {

View File

@@ -174,16 +174,6 @@ AudioPipelineState AudioPipeline::process_state() {
}
}
if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_READING;
}
if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_DECODING;
}
if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) &&
(!(event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) &&
(event_bits & EventGroupBits::DECODER_MESSAGE_FINISHED))) {
@@ -213,6 +203,16 @@ AudioPipelineState AudioPipeline::process_state() {
return AudioPipelineState::STOPPED;
}
if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_READING;
}
if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_DECODING;
}
if (this->pause_state_) {
return AudioPipelineState::PAUSED;
}

View File

@@ -20,7 +20,7 @@ SprinklerSwitch::SprinklerSwitch(switch_::Switch *off_switch, switch_::Switch *o
bool SprinklerSwitch::is_latching_valve() { return (this->off_switch_ != nullptr) && (this->on_switch_ != nullptr); }
void SprinklerSwitch::loop() {
if ((this->pinned_millis_) && (App.get_loop_component_start_time() > this->pinned_millis_ + this->pulse_duration_)) {
if ((this->pinned_millis_) && (millis() > this->pinned_millis_ + this->pulse_duration_)) {
this->pinned_millis_ = 0; // reset tracker
if (this->off_switch_->state) {
this->off_switch_->turn_off();
@@ -148,23 +148,22 @@ SprinklerValveOperator::SprinklerValveOperator(SprinklerValve *valve, Sprinkler
: controller_(controller), valve_(valve) {}
void SprinklerValveOperator::loop() {
uint32_t now = App.get_loop_component_start_time();
if (now >= this->start_millis_) { // dummy check
if (millis() >= this->start_millis_) { // dummy check
switch (this->state_) {
case STARTING:
if (now > (this->start_millis_ + this->start_delay_)) {
if (millis() > (this->start_millis_ + this->start_delay_)) {
this->run_(); // start_delay_ has been exceeded, so ensure both valves are on and update the state
}
break;
case ACTIVE:
if (now > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
if (millis() > (this->start_millis_ + this->start_delay_ + this->run_duration_)) {
this->stop(); // start_delay_ + run_duration_ has been exceeded, start shutting down
}
break;
case STOPPING:
if (now > (this->stop_millis_ + this->stop_delay_)) {
if (millis() > (this->stop_millis_ + this->stop_delay_)) {
this->kill_(); // stop_delay_has been exceeded, ensure all valves are off
}
break;

View File

@@ -7,7 +7,7 @@ CODEOWNERS = ["@glmnet"]
tcl112_ns = cg.esphome_ns.namespace("tcl112")
Tcl112Climate = tcl112_ns.class_("Tcl112Climate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(Tcl112Climate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Tcl112Climate)
async def to_code(config):

View File

@@ -1,7 +1,6 @@
#include "time_based_cover.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/application.h"
namespace esphome {
namespace time_based {
@@ -27,7 +26,7 @@ void TimeBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
// Recompute position every loop cycle
this->recompute_position_();

View File

@@ -16,7 +16,7 @@ MODELS = {
"RAC-PT1411HWRU-F": Model.MODEL_RAC_PT1411HWRU_F,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ToshibaClimate).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ToshibaClimate).extend(
{
cv.Optional(CONF_MODEL, default="generic"): cv.enum(MODELS, upper=True),
}

View File

@@ -54,8 +54,8 @@ async def to_code(config):
cg.add(var.set_select_mappings(list(options_map.keys())))
parent = await cg.get_variable(config[CONF_TUYA_ID])
cg.add(var.set_tuya_parent(parent))
if (enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None)) is not None:
if enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None) is not None:
cg.add(var.set_select_id(enum_datapoint, False))
if (int_datapoint := config.get(CONF_INT_DATAPOINT, None)) is not None:
if int_datapoint := config.get(CONF_INT_DATAPOINT, None) is not None:
cg.add(var.set_select_id(int_datapoint, True))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))

View File

@@ -1,6 +1,5 @@
#include "uart_switch.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace uart {
@@ -9,7 +8,7 @@ static const char *const TAG = "uart.switch";
void UARTSwitch::loop() {
if (this->send_every_) {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
if (now - this->last_transmission_ > this->send_every_) {
this->write_command_(this->state);
this->last_transmission_ = now;

View File

@@ -1,7 +1,6 @@
#include "uponor_smatrix_climate.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace uponor_smatrix {
@@ -14,7 +13,7 @@ void UponorSmatrixClimate::dump_config() {
}
void UponorSmatrixClimate::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
// Publish state after all update packets are processed
if (this->last_data_ != 0 && (now - this->last_data_ > 100) && this->target_temperature_raw_ != 0) {

View File

@@ -1,6 +1,5 @@
#include "uponor_smatrix.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace uponor_smatrix {
@@ -36,7 +35,7 @@ void UponorSmatrixComponent::dump_config() {
}
void UponorSmatrixComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
const uint32_t now = millis();
// Discard stale data
if (!this->rx_buffer_.empty() && (now - this->last_rx_ > 50)) {

View File

@@ -8,6 +8,58 @@
namespace esphome {
namespace weikai {
/*! @mainpage Weikai source code documentation
This documentation provides information about the implementation of the family of WeiKai Components in ESPHome.
Here is the class diagram related to Weikai family of components:
@image html weikai_class.png
@section WKRingBuffer_ The WKRingBuffer template class
The WKRingBuffer template class has it names implies implement a simple ring buffer helper class. This straightforward
container implements FIFO functionality, enabling bytes to be pushed into one side and popped from the other in the
order of entry. Implementation is classic and therefore not described in any details.
@section WeikaiRegister_ The WeikaiRegister class
The WeikaiRegister helper class creates objects that act as proxies to the device registers.
@details This is an abstract virtual class (interface) that provides all the necessary access to registers while hiding
the actual implementation. The access to the registers can be made through an I²C bus in for example for wk2168_i2c
component or through a SPI bus for example in the case of the wk2168_spi component. Derived classes will actually
performs the specific bus operations.
@section WeikaiRegisterI2C_ WeikaiRegisterI2C
The weikai_i2c::WeikaiRegisterI2C class implements the virtual methods of the WeikaiRegister class for an I2C bus.
@section WeikaiRegisterSPI_ WeikaiRegisterSPI
The weikai_spi::WeikaiRegisterSPI class implements the virtual methods of the WeikaiRegister class for an SPI bus.
@section WeikaiComponent_ The WeikaiComponent class
The WeikaiComponent class stores the information global to a WeiKai family component and provides methods to set/access
this information. It also serves as a container for WeikaiChannel instances. This is done by maintaining an array of
references these WeikaiChannel instances. This class derives from the esphome::Component classes. This class override
esphome::Component::loop() method to facilitate the seamless transfer of accumulated bytes from the receive
FIFO into the ring buffer. This process ensures quick access to the stored bytes, enhancing the overall efficiency of
the component.
@section WeikaiComponentI2C_ WeikaiComponentI2C
The weikai_i2c::WeikaiComponentI2C class implements the virtual methods of the WeikaiComponent class for an I2C bus.
@section WeikaiComponentSPI_ WeikaiComponentSPI
The weikai_spi::WeikaiComponentSPI class implements the virtual methods of the WeikaiComponent class for an SPI bus.
@section WeikaiGPIOPin_ WeikaiGPIOPin class
The WeikaiGPIOPin class is an helper class to expose the GPIO pins of WK family components as if they were internal
GPIO pins. It also provides the setup() and dump_summary() methods.
@section WeikaiChannel_ The WeikaiChannel class
The WeikaiChannel class is used to implement all the virtual methods of the ESPHome uart::UARTComponent class. An
individual instance of this class is created for each UART channel. It has a link back to the WeikaiComponent object it
belongs to. This class derives from the uart::UARTComponent class. It collaborates through an aggregation with
WeikaiComponent. This implies that WeikaiComponent acts as a container, housing several WeikaiChannel instances.
Furthermore, the WeikaiChannel class derives from the ESPHome uart::UARTComponent class, it also has an association
relationship with the WKRingBuffer and WeikaiRegister helper classes. Consequently, when a WeikaiChannel instance is
destroyed, the associated WKRingBuffer instance is also destroyed.
*/
static const char *const TAG = "weikai";
/// @brief convert an int to binary representation as C++ std::string

View File

@@ -15,7 +15,7 @@ MODELS = {
"DG11J1-91": Model.MODEL_DG11J1_91,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(WhirlpoolClimate).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(WhirlpoolClimate).extend(
{
cv.Optional(CONF_MODEL, default="DG11J1-3A"): cv.enum(MODELS, upper=True),
}

View File

@@ -9,7 +9,7 @@ whynter_ns = cg.esphome_ns.namespace("whynter")
Whynter = whynter_ns.class_("Whynter", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(Whynter).extend(
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(Whynter).extend(
{
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
}

View File

@@ -7,7 +7,7 @@ CODEOWNERS = ["@cfeenstra1024"]
zhlt01_ns = cg.esphome_ns.namespace("zhlt01")
ZHLT01Climate = zhlt01_ns.class_("ZHLT01Climate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(ZHLT01Climate)
CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(ZHLT01Climate)
async def to_code(config):

View File

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

View File

@@ -35,8 +35,6 @@ void Application::setup() {
for (uint32_t i = 0; i < this->components_.size(); i++) {
Component *component = this->components_[i];
// Update loop_component_start_time_ before calling each component during setup
this->loop_component_start_time_ = millis();
component->call();
this->scheduler.process_to_add();
this->feed_wdt();
@@ -51,8 +49,6 @@ void Application::setup() {
this->scheduler.call();
this->feed_wdt();
for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = millis();
this->components_[j]->call();
new_app_state |= this->components_[j]->get_component_state();
this->app_state_ |= new_app_state;
@@ -71,32 +67,22 @@ void Application::loop() {
uint32_t new_app_state = 0;
this->scheduler.call();
// Get the initial loop time at the start
uint32_t last_op_end_time = millis();
// Feed WDT with time
this->feed_wdt(last_op_end_time);
this->feed_wdt();
for (Component *component : this->looping_components_) {
// Update the cached time before each component runs
this->loop_component_start_time_ = last_op_end_time;
{
this->set_current_component(component);
WarnIfComponentBlockingGuard guard{component, last_op_end_time};
WarnIfComponentBlockingGuard guard{component};
component->call();
// Use the finish method to get the current time as the end time
last_op_end_time = guard.finish();
}
new_app_state |= component->get_component_state();
this->app_state_ |= new_app_state;
this->feed_wdt(last_op_end_time);
this->feed_wdt();
}
this->app_state_ = new_app_state;
// Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_;
const uint32_t now = millis();
auto elapsed = now - this->last_loop_;
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
yield();
} else {
@@ -108,7 +94,7 @@ void Application::loop() {
delay_time = std::min(next_schedule, delay_time);
delay(delay_time);
}
this->last_loop_ = last_op_end_time;
this->last_loop_ = now;
if (this->dump_config_at_ < this->components_.size()) {
if (this->dump_config_at_ == 0) {
@@ -123,12 +109,10 @@ void Application::loop() {
}
}
void IRAM_ATTR HOT Application::feed_wdt(uint32_t time) {
void IRAM_ATTR HOT Application::feed_wdt() {
static uint32_t last_feed = 0;
// Use provided time if available, otherwise get current time
uint32_t now = time ? time : millis();
// Compare in milliseconds (3ms threshold)
if (now - last_feed > 3) {
uint32_t now = micros();
if (now - last_feed > 3000) {
arch_feed_wdt();
last_feed = now;
#ifdef USE_STATUS_LED

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
@@ -217,9 +218,6 @@ class Application {
std::string get_compilation_time() const { return this->compilation_time_; }
/// Get the cached time in milliseconds from when the current component started its loop execution
inline uint32_t IRAM_ATTR HOT get_loop_component_start_time() const { return this->loop_component_start_time_; }
/** Set the target interval with which to run the loop() calls.
* If the loop() method takes longer than the target interval, ESPHome won't
* sleep in loop(), but if the time spent in loop() is small than the target, ESPHome
@@ -237,9 +235,21 @@ 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(uint32_t time = 0);
void feed_wdt();
void reboot();
@@ -554,7 +564,6 @@ class Application {
size_t dump_config_at_{SIZE_MAX};
uint32_t app_state_{0};
Component *current_component_{nullptr};
uint32_t loop_component_start_time_{0};
};
/// Global storage of Application pointer - only one Application can exist.

View File

@@ -240,12 +240,16 @@ void PollingComponent::stop_poller() {
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component, uint32_t start_time)
: started_(start_time), component_(component) {}
uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
WarnIfComponentBlockingGuard::WarnIfComponentBlockingGuard(Component *component)
: started_(millis()), component_(component) {}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {
uint32_t current_time = millis();
uint32_t blocking_time = current_time - this->started_;
uint32_t blocking_time = curr_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);
@@ -257,10 +261,6 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
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.");
}
return curr_time;
}
WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
} // namespace esphome

View File

@@ -6,6 +6,7 @@
#include <string>
#include "esphome/core/optional.h"
#include "esphome/core/runtime_stats.h"
namespace esphome {
@@ -339,11 +340,7 @@ class PollingComponent : public Component {
class WarnIfComponentBlockingGuard {
public:
WarnIfComponentBlockingGuard(Component *component, uint32_t start_time);
// Finish the timing operation and return the current time
uint32_t finish();
WarnIfComponentBlockingGuard(Component *component);
~WarnIfComponentBlockingGuard();
protected:

View File

@@ -1,13 +0,0 @@
#pragma once
namespace esphome {
/*! @mainpage ESPHome source code documentation
This documentation provides references to the ESPHome source code classes and methods.
@details This documentation site is purely for reference and does not contain any user documentation.
If you are contributing to ESPHome or building an ESPHome component, then you should be starting at
https://developers.esphome.io.
*/
} // namespace esphome

View File

@@ -1,7 +1,6 @@
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <functional>
#include <limits>

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

@@ -229,11 +229,8 @@ void HOT Scheduler::call() {
// - timeouts/intervals get added, potentially invalidating vector pointers
// - timeouts/intervals get cancelled
{
uint32_t now_ms = millis();
WarnIfComponentBlockingGuard guard{item->component, now_ms};
WarnIfComponentBlockingGuard guard{item->component};
item->callback();
// Call finish to ensure blocking time is properly calculated and reported
guard.finish();
}
}

View File

@@ -1,6 +1,5 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <iterator>
#include <memory>

View File

@@ -601,12 +601,10 @@ class DownloadListRequestHandler(BaseHandler):
loop = asyncio.get_running_loop()
try:
downloads_json = await loop.run_in_executor(None, self._get, configuration)
except vol.Invalid as exc:
_LOGGER.exception("Error while fetching downloads", exc_info=exc)
except vol.Invalid:
self.send_error(404)
return
if downloads_json is None:
_LOGGER.error("Configuration %s not found", configuration)
self.send_error(404)
return
self.set_status(200)
@@ -620,17 +618,14 @@ class DownloadListRequestHandler(BaseHandler):
if storage_json is None:
return None
try:
config = yaml_util.load_yaml(settings.rel_path(configuration))
config = yaml_util.load_yaml(settings.rel_path(configuration))
if const.CONF_EXTERNAL_COMPONENTS in config:
from esphome.components.external_components import (
do_external_components_pass,
)
if const.CONF_EXTERNAL_COMPONENTS in config:
from esphome.components.external_components import (
do_external_components_pass,
)
do_external_components_pass(config)
except vol.Invalid:
_LOGGER.info("Could not parse `external_components`, skipping")
do_external_components_pass(config)
from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS

View File

@@ -4,7 +4,7 @@ dependencies:
version: v1.3.1
esp32_camera:
git: https://github.com/espressif/esp32-camera.git
version: v2.0.15
version: v2.0.9
mdns:
git: https://github.com/espressif/esp-protocols.git
version: mdns-v1.8.2

Some files were not shown because too many files have changed in this diff Show More