esphome 2025.5.0b2__py3-none-any.whl → 2025.5.0b3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. esphome/__main__.py +16 -14
  2. esphome/components/api/api_connection.cpp +4 -3
  3. esphome/components/api/api_connection.h +8 -1
  4. esphome/components/api/api_frame_helper.cpp +136 -49
  5. esphome/components/api/api_frame_helper.h +49 -6
  6. esphome/components/api/proto.h +48 -8
  7. esphome/components/ballu/climate.py +1 -1
  8. esphome/components/bedjet/bedjet_hub.cpp +1 -0
  9. esphome/components/binary_sensor/binary_sensor.cpp +10 -6
  10. esphome/components/binary_sensor/binary_sensor.h +1 -1
  11. esphome/components/binary_sensor/filter.cpp +21 -21
  12. esphome/components/binary_sensor/filter.h +10 -10
  13. esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +2 -1
  14. esphome/components/ccs811/sensor.py +9 -6
  15. esphome/components/climate_ir/__init__.py +3 -3
  16. esphome/components/climate_ir_lg/climate.py +1 -1
  17. esphome/components/coolix/climate.py +1 -1
  18. esphome/components/cse7766/cse7766.cpp +2 -1
  19. esphome/components/current_based/current_based_cover.cpp +2 -1
  20. esphome/components/daikin/climate.py +1 -1
  21. esphome/components/daikin_arc/climate.py +1 -1
  22. esphome/components/daikin_brc/climate.py +1 -1
  23. esphome/components/daly_bms/daly_bms.cpp +2 -1
  24. esphome/components/debug/debug_component.cpp +1 -1
  25. esphome/components/delonghi/climate.py +1 -1
  26. esphome/components/dps310/sensor.py +6 -6
  27. esphome/components/ee895/sensor.py +9 -9
  28. esphome/components/emmeti/climate.py +1 -1
  29. esphome/components/endstop/endstop_cover.cpp +2 -1
  30. esphome/components/ens160_base/__init__.py +12 -9
  31. esphome/components/esp32_ble/ble_advertising.cpp +2 -1
  32. esphome/components/esp32_camera/esp32_camera.cpp +2 -1
  33. esphome/components/esp32_camera/esp32_camera.h +1 -1
  34. esphome/components/esp32_improv/esp32_improv_component.cpp +1 -1
  35. esphome/components/esp32_touch/esp32_touch.cpp +1 -1
  36. esphome/components/ethernet/ethernet_component.cpp +1 -1
  37. esphome/components/feedback/feedback_cover.cpp +2 -1
  38. esphome/components/fujitsu_general/climate.py +1 -1
  39. esphome/components/gcja5/gcja5.cpp +2 -1
  40. esphome/components/gps/__init__.py +37 -16
  41. esphome/components/gps/gps.cpp +33 -17
  42. esphome/components/gps/gps.h +16 -15
  43. esphome/components/gree/climate.py +1 -1
  44. esphome/components/growatt_solar/growatt_solar.cpp +2 -1
  45. esphome/components/heatpumpir/climate.py +1 -1
  46. esphome/components/hitachi_ac344/climate.py +1 -1
  47. esphome/components/hitachi_ac424/climate.py +1 -1
  48. esphome/components/hte501/sensor.py +6 -6
  49. esphome/components/hyt271/sensor.py +6 -6
  50. esphome/components/kuntze/kuntze.cpp +2 -1
  51. esphome/components/logger/__init__.py +1 -0
  52. esphome/components/logger/logger.cpp +53 -32
  53. esphome/components/logger/logger.h +55 -5
  54. esphome/components/matrix_keypad/matrix_keypad.cpp +2 -1
  55. esphome/components/max7219digit/max7219digit.cpp +2 -1
  56. esphome/components/mhz19/sensor.py +11 -7
  57. esphome/components/midea_ir/climate.py +1 -1
  58. esphome/components/mitsubishi/climate.py +1 -1
  59. esphome/components/modbus/modbus.cpp +2 -1
  60. esphome/components/mqtt/mqtt_client.cpp +1 -1
  61. esphome/components/ms5611/sensor.py +6 -6
  62. esphome/components/ms8607/sensor.py +3 -3
  63. esphome/components/noblex/climate.py +1 -1
  64. esphome/components/pmsx003/pmsx003.cpp +2 -1
  65. esphome/components/pzem004t/pzem004t.cpp +2 -1
  66. esphome/components/rf_bridge/rf_bridge.cpp +2 -1
  67. esphome/components/sds011/sds011.cpp +2 -1
  68. esphome/components/sen5x/sen5x.cpp +55 -36
  69. esphome/components/senseair/sensor.py +3 -3
  70. esphome/components/sgp30/sensor.py +14 -16
  71. esphome/components/shtcx/sensor.py +6 -6
  72. esphome/components/slow_pwm/slow_pwm_output.cpp +2 -1
  73. esphome/components/sprinkler/sprinkler.cpp +6 -5
  74. esphome/components/t6615/sensor.py +3 -3
  75. esphome/components/t6615/t6615.cpp +2 -1
  76. esphome/components/tcl112/climate.py +1 -1
  77. esphome/components/time_based/time_based_cover.cpp +2 -1
  78. esphome/components/toshiba/climate.py +1 -1
  79. esphome/components/uart/switch/uart_switch.cpp +2 -1
  80. esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +2 -1
  81. esphome/components/uponor_smatrix/uponor_smatrix.cpp +2 -1
  82. esphome/components/whirlpool/climate.py +1 -1
  83. esphome/components/whynter/climate.py +1 -1
  84. esphome/components/zhlt01/climate.py +1 -1
  85. esphome/config.py +13 -13
  86. esphome/const.py +1 -1
  87. esphome/core/application.cpp +22 -10
  88. esphome/core/application.h +5 -1
  89. esphome/core/component.cpp +10 -5
  90. esphome/core/component.h +5 -1
  91. esphome/core/scheduler.cpp +4 -1
  92. esphome/log.py +15 -19
  93. esphome/mqtt.py +2 -2
  94. esphome/voluptuous_schema.py +3 -1
  95. esphome/wizard.py +45 -35
  96. {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/METADATA +1 -1
  97. {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/RECORD +101 -101
  98. {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/WHEEL +0 -0
  99. {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/entry_points.txt +0 -0
  100. {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/licenses/LICENSE +0 -0
  101. {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/top_level.txt +0 -0
esphome/__main__.py CHANGED
@@ -43,7 +43,7 @@ from esphome.const import (
43
43
  )
44
44
  from esphome.core import CORE, EsphomeError, coroutine
45
45
  from esphome.helpers import get_bool_env, indent, is_ip_address
46
- from esphome.log import Fore, color, setup_log
46
+ from esphome.log import AnsiFore, color, setup_log
47
47
  from esphome.util import (
48
48
  get_serial_ports,
49
49
  list_yaml_files,
@@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
83
83
  raise ValueError
84
84
  break
85
85
  except ValueError:
86
- safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
86
+ safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
87
87
  return options[opt - 1][1]
88
88
 
89
89
 
@@ -596,30 +596,30 @@ def command_update_all(args):
596
596
  click.echo(f"{half_line}{middle_text}{half_line}")
597
597
 
598
598
  for f in files:
599
- print(f"Updating {color(Fore.CYAN, f)}")
599
+ print(f"Updating {color(AnsiFore.CYAN, f)}")
600
600
  print("-" * twidth)
601
601
  print()
602
602
  rc = run_external_process(
603
603
  "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
604
604
  )
605
605
  if rc == 0:
606
- print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
606
+ print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
607
607
  success[f] = True
608
608
  else:
609
- print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
609
+ print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
610
610
  success[f] = False
611
611
 
612
612
  print()
613
613
  print()
614
614
  print()
615
615
 
616
- print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
616
+ print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
617
617
  failed = 0
618
618
  for f in files:
619
619
  if success[f]:
620
- print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}")
620
+ print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
621
621
  else:
622
- print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
622
+ print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
623
623
  failed += 1
624
624
  return failed
625
625
 
@@ -645,7 +645,7 @@ def command_rename(args, config):
645
645
  if c not in ALLOWED_NAME_CHARS:
646
646
  print(
647
647
  color(
648
- Fore.BOLD_RED,
648
+ AnsiFore.BOLD_RED,
649
649
  f"'{c}' is an invalid character for names. Valid characters are: "
650
650
  f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
651
651
  )
@@ -658,7 +658,9 @@ def command_rename(args, config):
658
658
  yaml = yaml_util.load_yaml(CORE.config_path)
659
659
  if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
660
660
  print(
661
- color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
661
+ color(
662
+ AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
663
+ )
662
664
  )
663
665
  return 1
664
666
  old_name = yaml[CONF_ESPHOME][CONF_NAME]
@@ -681,7 +683,7 @@ def command_rename(args, config):
681
683
  )
682
684
  > 1
683
685
  ):
684
- print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
686
+ print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
685
687
  return 1
686
688
 
687
689
  new_raw = re.sub(
@@ -693,7 +695,7 @@ def command_rename(args, config):
693
695
 
694
696
  new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
695
697
  print(
696
- f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
698
+ f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
697
699
  )
698
700
  print()
699
701
 
@@ -702,7 +704,7 @@ def command_rename(args, config):
702
704
 
703
705
  rc = run_external_process("esphome", "config", new_path)
704
706
  if rc != 0:
705
- print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
707
+ print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
706
708
  os.remove(new_path)
707
709
  return 1
708
710
 
@@ -728,7 +730,7 @@ def command_rename(args, config):
728
730
  if CORE.config_path != new_path:
729
731
  os.remove(CORE.config_path)
730
732
 
731
- print(color(Fore.BOLD_GREEN, "SUCCESS"))
733
+ print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
732
734
  print()
733
735
  return 0
734
736
 
@@ -8,6 +8,7 @@
8
8
  #include "esphome/core/hal.h"
9
9
  #include "esphome/core/log.h"
10
10
  #include "esphome/core/version.h"
11
+ #include "esphome/core/application.h"
11
12
 
12
13
  #ifdef USE_DEEP_SLEEP
13
14
  #include "esphome/components/deep_sleep/deep_sleep_component.h"
@@ -146,7 +147,7 @@ void APIConnection::loop() {
146
147
  }
147
148
  return;
148
149
  } else {
149
- this->last_traffic_ = millis();
150
+ this->last_traffic_ = App.get_loop_component_start_time();
150
151
  // read a packet
151
152
  this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
152
153
  if (this->remove_)
@@ -163,7 +164,7 @@ void APIConnection::loop() {
163
164
  static uint32_t keepalive = 60000;
164
165
  static uint8_t max_ping_retries = 60;
165
166
  static uint16_t ping_retry_interval = 1000;
166
- const uint32_t now = millis();
167
+ const uint32_t now = App.get_loop_component_start_time();
167
168
  if (this->sent_ping_) {
168
169
  // Disconnect if not responded within 2.5*keepalive
169
170
  if (now - this->last_traffic_ > (keepalive * 5) / 2) {
@@ -1961,7 +1962,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
1961
1962
  }
1962
1963
  }
1963
1964
 
1964
- APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size());
1965
+ APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
1965
1966
  if (err == APIError::WOULD_BLOCK)
1966
1967
  return false;
1967
1968
  if (err != APIError::OK) {
@@ -315,7 +315,14 @@ class APIConnection : public APIServerConnection {
315
315
  ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
316
316
  // FIXME: ensure no recursive writes can happen
317
317
  this->proto_write_buffer_.clear();
318
- this->proto_write_buffer_.reserve(reserve_size);
318
+ // Get header padding size - used for both reserve and insert
319
+ uint8_t header_padding = this->helper_->frame_header_padding();
320
+ // Reserve space for header padding + message + footer
321
+ // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
322
+ // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
323
+ this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
324
+ // Insert header padding bytes so message encoding starts at the correct position
325
+ this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
319
326
  return {&this->proto_write_buffer_};
320
327
  }
321
328
  bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
@@ -493,9 +493,12 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
493
493
  std::vector<uint8_t> data;
494
494
  data.resize(reason.length() + 1);
495
495
  data[0] = 0x01; // failure
496
- for (size_t i = 0; i < reason.length(); i++) {
497
- data[i + 1] = (uint8_t) reason[i];
496
+
497
+ // Copy error message in bulk
498
+ if (!reason.empty()) {
499
+ std::memcpy(data.data() + 1, reason.c_str(), reason.length());
498
500
  }
501
+
499
502
  // temporarily remove failed state
500
503
  auto orig_state = state_;
501
504
  state_ = State::EXPLICIT_REJECT;
@@ -557,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
557
560
  return APIError::OK;
558
561
  }
559
562
  bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
560
- APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
563
+ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
561
564
  int err;
562
565
  APIError aerr;
563
566
  aerr = state_action_();
@@ -569,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
569
572
  return APIError::WOULD_BLOCK;
570
573
  }
571
574
 
575
+ std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
576
+ // Message data starts after padding
577
+ size_t payload_len = raw_buffer->size() - frame_header_padding_;
572
578
  size_t padding = 0;
573
579
  size_t msg_len = 4 + payload_len + padding;
574
- size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
575
- auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
576
- if (tmpbuf == nullptr) {
577
- HELPER_LOG("Could not allocate for writing packet");
578
- return APIError::OUT_OF_MEMORY;
579
- }
580
580
 
581
- tmpbuf[0] = 0x01; // indicator
582
- // tmpbuf[1], tmpbuf[2] to be set later
581
+ // We need to resize to include MAC space, but we already reserved it in create_buffer
582
+ raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
583
+
584
+ // Write the noise header in the padded area
585
+ // Buffer layout:
586
+ // [0] - 0x01 indicator byte
587
+ // [1-2] - Size of encrypted payload (filled after encryption)
588
+ // [3-4] - Message type (encrypted)
589
+ // [5-6] - Payload length (encrypted)
590
+ // [7...] - Actual payload data (encrypted)
591
+ uint8_t *buf_start = raw_buffer->data();
592
+ buf_start[0] = 0x01; // indicator
593
+ // buf_start[1], buf_start[2] to be set later after encryption
583
594
  const uint8_t msg_offset = 3;
584
- const uint8_t payload_offset = msg_offset + 4;
585
- tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type
586
- tmpbuf[msg_offset + 1] = (uint8_t) type;
587
- tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len
588
- tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
589
- // copy data
590
- std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
591
- // fill padding with zeros
592
- std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
595
+ buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
596
+ buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
597
+ buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
598
+ buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
599
+ // payload data is already in the buffer starting at position 7
593
600
 
594
601
  NoiseBuffer mbuf;
595
602
  noise_buffer_init(mbuf);
596
- noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
603
+ // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
604
+ noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
597
605
  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
598
606
  if (err != 0) {
599
607
  state_ = State::FAILED;
@@ -602,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
602
610
  }
603
611
 
604
612
  size_t total_len = 3 + mbuf.size;
605
- tmpbuf[1] = (uint8_t) (mbuf.size >> 8);
606
- tmpbuf[2] = (uint8_t) mbuf.size;
613
+ buf_start[1] = (uint8_t) (mbuf.size >> 8);
614
+ buf_start[2] = (uint8_t) mbuf.size;
607
615
 
608
616
  struct iovec iov;
609
- iov.iov_base = &tmpbuf[0];
617
+ // Point iov_base to the beginning of the buffer (no unused padding in Noise)
618
+ // We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
619
+ iov.iov_base = buf_start;
610
620
  iov.iov_len = total_len;
611
621
 
612
622
  // write raw to not have two packets sent if NAGLE disabled
@@ -718,6 +728,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
718
728
  return APIError::HANDSHAKESTATE_SPLIT_FAILED;
719
729
  }
720
730
 
731
+ frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
732
+
721
733
  HELPER_LOG("Handshake complete!");
722
734
  noise_handshakestate_free(handshake_);
723
735
  handshake_ = nullptr;
@@ -830,6 +842,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
830
842
  // read header
831
843
  while (!rx_header_parsed_) {
832
844
  uint8_t data;
845
+ // Reading one byte at a time is fastest in practice for ESP32 when
846
+ // there is no data on the wire (which is the common case).
847
+ // This results in faster failure detection compared to
848
+ // attempting to read multiple bytes at once.
833
849
  ssize_t received = socket_->read(&data, 1);
834
850
  if (received == -1) {
835
851
  if (errno == EWOULDBLOCK || errno == EAGAIN) {
@@ -843,27 +859,60 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
843
859
  HELPER_LOG("Connection closed");
844
860
  return APIError::CONNECTION_CLOSED;
845
861
  }
846
- rx_header_buf_.push_back(data);
847
862
 
848
- // try parse header
849
- if (rx_header_buf_[0] != 0x00) {
863
+ // Successfully read a byte
864
+
865
+ // Process byte according to current buffer position
866
+ if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte)
867
+ if (data != 0x00) {
868
+ state_ = State::FAILED;
869
+ HELPER_LOG("Bad indicator byte %u", data);
870
+ return APIError::BAD_INDICATOR;
871
+ }
872
+ // We don't store the indicator byte, just increment position
873
+ rx_header_buf_pos_ = 1; // Set to 1 directly
874
+ continue; // Need more bytes before we can parse
875
+ }
876
+
877
+ // Check buffer overflow before storing
878
+ if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed)
850
879
  state_ = State::FAILED;
851
- HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
852
- return APIError::BAD_INDICATOR;
880
+ HELPER_LOG("Header buffer overflow");
881
+ return APIError::BAD_DATA_PACKET;
853
882
  }
854
883
 
855
- size_t i = 1;
884
+ // Store byte in buffer (adjust index to account for skipped indicator byte)
885
+ rx_header_buf_[rx_header_buf_pos_ - 1] = data;
886
+
887
+ // Increment position after storing
888
+ rx_header_buf_pos_++;
889
+
890
+ // Case 3: If we only have one varint byte, we need more
891
+ if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte
892
+ continue; // Need more bytes before we can parse
893
+ }
894
+
895
+ // At this point, we have at least 3 bytes total:
896
+ // - Validated indicator byte (0x00) but not stored
897
+ // - At least 2 bytes in the buffer for the varints
898
+ // Buffer layout:
899
+ // First 1-3 bytes: Message size varint (variable length)
900
+ // - 2 bytes would only allow up to 16383, which is less than noise's 65535
901
+ // - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
902
+ // Remaining 1-2 bytes: Message type varint (variable length)
903
+ // We now attempt to parse both varints. If either is incomplete,
904
+ // we'll continue reading more bytes.
905
+
856
906
  uint32_t consumed = 0;
857
- auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
907
+ auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
858
908
  if (!msg_size_varint.has_value()) {
859
909
  // not enough data there yet
860
910
  continue;
861
911
  }
862
912
 
863
- i += consumed;
864
913
  rx_header_parsed_len_ = msg_size_varint->as_uint32();
865
914
 
866
- auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
915
+ auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
867
916
  if (!msg_type_varint.has_value()) {
868
917
  // not enough data there yet
869
918
  continue;
@@ -909,7 +958,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
909
958
  // consume msg
910
959
  rx_buf_ = {};
911
960
  rx_buf_len_ = 0;
912
- rx_header_buf_.clear();
961
+ rx_header_buf_pos_ = 0;
913
962
  rx_header_parsed_ = false;
914
963
  return APIError::OK;
915
964
  }
@@ -953,28 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
953
1002
  return APIError::OK;
954
1003
  }
955
1004
  bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
956
- APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
1005
+ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
957
1006
  if (state_ != State::DATA) {
958
1007
  return APIError::BAD_STATE;
959
1008
  }
960
1009
 
961
- std::vector<uint8_t> header;
962
- header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) +
963
- api::ProtoSize::varint(static_cast<uint32_t>(type)));
964
- header.push_back(0x00);
965
- ProtoVarInt(payload_len).encode(header);
966
- ProtoVarInt(type).encode(header);
1010
+ std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
1011
+ // Message data starts after padding (frame_header_padding_ = 6)
1012
+ size_t payload_len = raw_buffer->size() - frame_header_padding_;
967
1013
 
968
- struct iovec iov[2];
969
- iov[0].iov_base = &header[0];
970
- iov[0].iov_len = header.size();
971
- if (payload_len == 0) {
972
- return write_raw_(iov, 1);
1014
+ // Calculate varint sizes for header components
1015
+ size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
1016
+ size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
1017
+ size_t total_header_len = 1 + size_varint_len + type_varint_len;
1018
+
1019
+ if (total_header_len > frame_header_padding_) {
1020
+ // Header is too large to fit in the padding
1021
+ return APIError::BAD_ARG;
973
1022
  }
974
- iov[1].iov_base = const_cast<uint8_t *>(payload);
975
- iov[1].iov_len = payload_len;
976
1023
 
977
- return write_raw_(iov, 2);
1024
+ // Calculate where to start writing the header
1025
+ // The header starts at the latest possible position to minimize unused padding
1026
+ //
1027
+ // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
1028
+ // [0-2] - Unused padding
1029
+ // [3] - 0x00 indicator byte
1030
+ // [4] - Payload size varint (1 byte, for sizes 0-127)
1031
+ // [5] - Message type varint (1 byte, for types 0-127)
1032
+ // [6...] - Actual payload data
1033
+ //
1034
+ // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
1035
+ // [0-1] - Unused padding
1036
+ // [2] - 0x00 indicator byte
1037
+ // [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
1038
+ // [5] - Message type varint (1 byte, for types 0-127)
1039
+ // [6...] - Actual payload data
1040
+ //
1041
+ // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
1042
+ // [0] - 0x00 indicator byte
1043
+ // [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
1044
+ // [4-5] - Message type varint (2 bytes, for types 128-32767)
1045
+ // [6...] - Actual payload data
1046
+ uint8_t *buf_start = raw_buffer->data();
1047
+ size_t header_offset = frame_header_padding_ - total_header_len;
1048
+
1049
+ // Write the plaintext header
1050
+ buf_start[header_offset] = 0x00; // indicator
1051
+
1052
+ // Encode size varint directly into buffer
1053
+ ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
1054
+
1055
+ // Encode type varint directly into buffer
1056
+ ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
1057
+
1058
+ struct iovec iov;
1059
+ // Point iov_base to the beginning of our header (skip unused padding)
1060
+ // This ensures we only send the actual header and payload, not the empty padding bytes
1061
+ iov.iov_base = buf_start + header_offset;
1062
+ iov.iov_len = total_header_len + payload_len;
1063
+
1064
+ return write_raw_(&iov, 1);
978
1065
  }
979
1066
  APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
980
1067
  // try send from tx_buf
@@ -16,6 +16,8 @@
16
16
  namespace esphome {
17
17
  namespace api {
18
18
 
19
+ class ProtoWriteBuffer;
20
+
19
21
  struct ReadPacketBuffer {
20
22
  std::vector<uint8_t> container;
21
23
  uint16_t type;
@@ -65,32 +67,46 @@ class APIFrameHelper {
65
67
  virtual APIError loop() = 0;
66
68
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
67
69
  virtual bool can_write_without_blocking() = 0;
68
- virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
70
+ virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
69
71
  virtual std::string getpeername() = 0;
70
72
  virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
71
73
  virtual APIError close() = 0;
72
74
  virtual APIError shutdown(int how) = 0;
73
75
  // Give this helper a name for logging
74
76
  virtual void set_log_info(std::string info) = 0;
77
+ // Get the frame header padding required by this protocol
78
+ virtual uint8_t frame_header_padding() = 0;
79
+ // Get the frame footer size required by this protocol
80
+ virtual uint8_t frame_footer_size() = 0;
75
81
 
76
82
  protected:
77
83
  // Common implementation for writing raw data to socket
78
84
  template<typename StateEnum>
79
85
  APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
80
86
  const std::string &info, StateEnum &state, StateEnum failed_state);
87
+
88
+ uint8_t frame_header_padding_{0};
89
+ uint8_t frame_footer_size_{0};
81
90
  };
82
91
 
83
92
  #ifdef USE_API_NOISE
84
93
  class APINoiseFrameHelper : public APIFrameHelper {
85
94
  public:
86
95
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
87
- : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {}
96
+ : socket_(std::move(socket)), ctx_(std::move(ctx)) {
97
+ // Noise header structure:
98
+ // Pos 0: indicator (0x01)
99
+ // Pos 1-2: encrypted payload size (16-bit big-endian)
100
+ // Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
101
+ // Pos 7+: actual payload data
102
+ frame_header_padding_ = 7;
103
+ }
88
104
  ~APINoiseFrameHelper() override;
89
105
  APIError init() override;
90
106
  APIError loop() override;
91
107
  APIError read_packet(ReadPacketBuffer *buffer) override;
92
108
  bool can_write_without_blocking() override;
93
- APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
109
+ APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
94
110
  std::string getpeername() override { return this->socket_->getpeername(); }
95
111
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
96
112
  return this->socket_->getpeername(addr, addrlen);
@@ -99,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper {
99
115
  APIError shutdown(int how) override;
100
116
  // Give this helper a name for logging
101
117
  void set_log_info(std::string info) override { info_ = std::move(info); }
118
+ // Get the frame header padding required by this protocol
119
+ uint8_t frame_header_padding() override { return frame_header_padding_; }
120
+ // Get the frame footer size required by this protocol
121
+ uint8_t frame_footer_size() override { return frame_footer_size_; }
102
122
 
103
123
  protected:
104
124
  struct ParsedFrame {
@@ -119,6 +139,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
119
139
  std::unique_ptr<socket::Socket> socket_;
120
140
 
121
141
  std::string info_;
142
+ // Fixed-size header buffer for noise protocol:
143
+ // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
144
+ // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
122
145
  uint8_t rx_header_buf_[3];
123
146
  size_t rx_header_buf_len_ = 0;
124
147
  std::vector<uint8_t> rx_buf_;
@@ -149,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper {
149
172
  #ifdef USE_API_PLAINTEXT
150
173
  class APIPlaintextFrameHelper : public APIFrameHelper {
151
174
  public:
152
- APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
175
+ APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {
176
+ // Plaintext header structure (worst case):
177
+ // Pos 0: indicator (0x00)
178
+ // Pos 1-3: payload size varint (up to 3 bytes)
179
+ // Pos 4-5: message type varint (up to 2 bytes)
180
+ // Pos 6+: actual payload data
181
+ frame_header_padding_ = 6;
182
+ }
153
183
  ~APIPlaintextFrameHelper() override = default;
154
184
  APIError init() override;
155
185
  APIError loop() override;
156
186
  APIError read_packet(ReadPacketBuffer *buffer) override;
157
187
  bool can_write_without_blocking() override;
158
- APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
188
+ APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
159
189
  std::string getpeername() override { return this->socket_->getpeername(); }
160
190
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
161
191
  return this->socket_->getpeername(addr, addrlen);
@@ -164,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
164
194
  APIError shutdown(int how) override;
165
195
  // Give this helper a name for logging
166
196
  void set_log_info(std::string info) override { info_ = std::move(info); }
197
+ // Get the frame header padding required by this protocol
198
+ uint8_t frame_header_padding() override { return frame_header_padding_; }
199
+ // Get the frame footer size required by this protocol
200
+ uint8_t frame_footer_size() override { return frame_footer_size_; }
167
201
 
168
202
  protected:
169
203
  struct ParsedFrame {
@@ -179,7 +213,16 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
179
213
  std::unique_ptr<socket::Socket> socket_;
180
214
 
181
215
  std::string info_;
182
- std::vector<uint8_t> rx_header_buf_;
216
+ // Fixed-size header buffer for plaintext protocol:
217
+ // We only need space for the two varints since we validate the indicator byte separately.
218
+ // To match noise protocol's maximum message size (65535), we need:
219
+ // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
220
+ //
221
+ // While varints could theoretically be up to 10 bytes each for 64-bit values,
222
+ // attempting to process messages with headers that large would likely crash the
223
+ // ESP32 due to memory constraints.
224
+ uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
225
+ uint8_t rx_header_buf_pos_ = 0;
183
226
  bool rx_header_parsed_ = false;
184
227
  uint32_t rx_header_parsed_type_ = 0;
185
228
  uint32_t rx_header_parsed_len_ = 0;
@@ -20,16 +20,26 @@ class ProtoVarInt {
20
20
  explicit ProtoVarInt(uint64_t value) : value_(value) {}
21
21
 
22
22
  static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
23
- if (consumed != nullptr)
24
- *consumed = 0;
25
-
26
- if (len == 0)
23
+ if (len == 0) {
24
+ if (consumed != nullptr)
25
+ *consumed = 0;
27
26
  return {};
27
+ }
28
+
29
+ // Most common case: single-byte varint (values 0-127)
30
+ if ((buffer[0] & 0x80) == 0) {
31
+ if (consumed != nullptr)
32
+ *consumed = 1;
33
+ return ProtoVarInt(buffer[0]);
34
+ }
28
35
 
29
- uint64_t result = 0;
30
- uint8_t bitpos = 0;
36
+ // General case for multi-byte varints
37
+ // Since we know buffer[0]'s high bit is set, initialize with its value
38
+ uint64_t result = buffer[0] & 0x7F;
39
+ uint8_t bitpos = 7;
31
40
 
32
- for (uint32_t i = 0; i < len; i++) {
41
+ // Start from the second byte since we've already processed the first
42
+ for (uint32_t i = 1; i < len; i++) {
33
43
  uint8_t val = buffer[i];
34
44
  result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
35
45
  bitpos += 7;
@@ -40,7 +50,9 @@ class ProtoVarInt {
40
50
  }
41
51
  }
42
52
 
43
- return {};
53
+ if (consumed != nullptr)
54
+ *consumed = 0;
55
+ return {}; // Incomplete or invalid varint
44
56
  }
45
57
 
46
58
  uint32_t as_uint32() const { return this->value_; }
@@ -71,6 +83,34 @@ class ProtoVarInt {
71
83
  return static_cast<int64_t>(this->value_ >> 1);
72
84
  }
73
85
  }
86
+ /**
87
+ * Encode the varint value to a pre-allocated buffer without bounds checking.
88
+ *
89
+ * @param buffer The pre-allocated buffer to write the encoded varint to
90
+ * @param len The size of the buffer in bytes
91
+ *
92
+ * @note The caller is responsible for ensuring the buffer is large enough
93
+ * to hold the encoded value. Use ProtoSize::varint() to calculate
94
+ * the exact size needed before calling this method.
95
+ * @note No bounds checking is performed for performance reasons.
96
+ */
97
+ void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
98
+ uint64_t val = this->value_;
99
+ if (val <= 0x7F) {
100
+ buffer[0] = val;
101
+ return;
102
+ }
103
+ size_t i = 0;
104
+ while (val && i < len) {
105
+ uint8_t temp = val & 0x7F;
106
+ val >>= 7;
107
+ if (val) {
108
+ buffer[i++] = temp | 0x80;
109
+ } else {
110
+ buffer[i++] = temp;
111
+ }
112
+ }
113
+ }
74
114
  void encode(std::vector<uint8_t> &out) {
75
115
  uint64_t val = this->value_;
76
116
  if (val <= 0x7F) {
@@ -7,7 +7,7 @@ CODEOWNERS = ["@bazuchan"]
7
7
  ballu_ns = cg.esphome_ns.namespace("ballu")
8
8
  BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
9
9
 
10
- CONFIG_SCHEMA = climate_ir.climare_ir_with_receiver_schema(BalluClimate)
10
+ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
11
11
 
12
12
 
13
13
  async def to_code(config):
@@ -3,6 +3,7 @@
3
3
  #include "bedjet_hub.h"
4
4
  #include "bedjet_child.h"
5
5
  #include "bedjet_const.h"
6
+ #include "esphome/core/application.h"
6
7
  #include <cinttypes>
7
8
 
8
9
  namespace esphome {