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.
- esphome/__main__.py +16 -14
- esphome/components/api/api_connection.cpp +4 -3
- esphome/components/api/api_connection.h +8 -1
- esphome/components/api/api_frame_helper.cpp +136 -49
- esphome/components/api/api_frame_helper.h +49 -6
- esphome/components/api/proto.h +48 -8
- esphome/components/ballu/climate.py +1 -1
- esphome/components/bedjet/bedjet_hub.cpp +1 -0
- esphome/components/binary_sensor/binary_sensor.cpp +10 -6
- esphome/components/binary_sensor/binary_sensor.h +1 -1
- esphome/components/binary_sensor/filter.cpp +21 -21
- esphome/components/binary_sensor/filter.h +10 -10
- esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +2 -1
- esphome/components/ccs811/sensor.py +9 -6
- esphome/components/climate_ir/__init__.py +3 -3
- esphome/components/climate_ir_lg/climate.py +1 -1
- esphome/components/coolix/climate.py +1 -1
- esphome/components/cse7766/cse7766.cpp +2 -1
- esphome/components/current_based/current_based_cover.cpp +2 -1
- esphome/components/daikin/climate.py +1 -1
- esphome/components/daikin_arc/climate.py +1 -1
- esphome/components/daikin_brc/climate.py +1 -1
- esphome/components/daly_bms/daly_bms.cpp +2 -1
- esphome/components/debug/debug_component.cpp +1 -1
- esphome/components/delonghi/climate.py +1 -1
- esphome/components/dps310/sensor.py +6 -6
- esphome/components/ee895/sensor.py +9 -9
- esphome/components/emmeti/climate.py +1 -1
- esphome/components/endstop/endstop_cover.cpp +2 -1
- esphome/components/ens160_base/__init__.py +12 -9
- esphome/components/esp32_ble/ble_advertising.cpp +2 -1
- esphome/components/esp32_camera/esp32_camera.cpp +2 -1
- esphome/components/esp32_camera/esp32_camera.h +1 -1
- esphome/components/esp32_improv/esp32_improv_component.cpp +1 -1
- esphome/components/esp32_touch/esp32_touch.cpp +1 -1
- esphome/components/ethernet/ethernet_component.cpp +1 -1
- esphome/components/feedback/feedback_cover.cpp +2 -1
- esphome/components/fujitsu_general/climate.py +1 -1
- esphome/components/gcja5/gcja5.cpp +2 -1
- esphome/components/gps/__init__.py +37 -16
- esphome/components/gps/gps.cpp +33 -17
- esphome/components/gps/gps.h +16 -15
- esphome/components/gree/climate.py +1 -1
- esphome/components/growatt_solar/growatt_solar.cpp +2 -1
- esphome/components/heatpumpir/climate.py +1 -1
- esphome/components/hitachi_ac344/climate.py +1 -1
- esphome/components/hitachi_ac424/climate.py +1 -1
- esphome/components/hte501/sensor.py +6 -6
- esphome/components/hyt271/sensor.py +6 -6
- esphome/components/kuntze/kuntze.cpp +2 -1
- esphome/components/logger/__init__.py +1 -0
- esphome/components/logger/logger.cpp +53 -32
- esphome/components/logger/logger.h +55 -5
- esphome/components/matrix_keypad/matrix_keypad.cpp +2 -1
- esphome/components/max7219digit/max7219digit.cpp +2 -1
- esphome/components/mhz19/sensor.py +11 -7
- esphome/components/midea_ir/climate.py +1 -1
- esphome/components/mitsubishi/climate.py +1 -1
- esphome/components/modbus/modbus.cpp +2 -1
- esphome/components/mqtt/mqtt_client.cpp +1 -1
- esphome/components/ms5611/sensor.py +6 -6
- esphome/components/ms8607/sensor.py +3 -3
- esphome/components/noblex/climate.py +1 -1
- esphome/components/pmsx003/pmsx003.cpp +2 -1
- esphome/components/pzem004t/pzem004t.cpp +2 -1
- esphome/components/rf_bridge/rf_bridge.cpp +2 -1
- esphome/components/sds011/sds011.cpp +2 -1
- esphome/components/sen5x/sen5x.cpp +55 -36
- esphome/components/senseair/sensor.py +3 -3
- esphome/components/sgp30/sensor.py +14 -16
- esphome/components/shtcx/sensor.py +6 -6
- esphome/components/slow_pwm/slow_pwm_output.cpp +2 -1
- esphome/components/sprinkler/sprinkler.cpp +6 -5
- esphome/components/t6615/sensor.py +3 -3
- esphome/components/t6615/t6615.cpp +2 -1
- esphome/components/tcl112/climate.py +1 -1
- esphome/components/time_based/time_based_cover.cpp +2 -1
- esphome/components/toshiba/climate.py +1 -1
- esphome/components/uart/switch/uart_switch.cpp +2 -1
- esphome/components/uponor_smatrix/climate/uponor_smatrix_climate.cpp +2 -1
- esphome/components/uponor_smatrix/uponor_smatrix.cpp +2 -1
- esphome/components/whirlpool/climate.py +1 -1
- esphome/components/whynter/climate.py +1 -1
- esphome/components/zhlt01/climate.py +1 -1
- esphome/config.py +13 -13
- esphome/const.py +1 -1
- esphome/core/application.cpp +22 -10
- esphome/core/application.h +5 -1
- esphome/core/component.cpp +10 -5
- esphome/core/component.h +5 -1
- esphome/core/scheduler.cpp +4 -1
- esphome/log.py +15 -19
- esphome/mqtt.py +2 -2
- esphome/voluptuous_schema.py +3 -1
- esphome/wizard.py +45 -35
- {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/METADATA +1 -1
- {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/RECORD +101 -101
- {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/WHEEL +0 -0
- {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/entry_points.txt +0 -0
- {esphome-2025.5.0b2.dist-info → esphome-2025.5.0b3.dist-info}/licenses/LICENSE +0 -0
- {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
|
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(
|
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(
|
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(
|
606
|
+
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
|
607
607
|
success[f] = True
|
608
608
|
else:
|
609
|
-
print_bar(f"[{color(
|
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(
|
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(
|
620
|
+
print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
|
621
621
|
else:
|
622
|
-
print(f" - {f}: {color(
|
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
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
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_ =
|
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 =
|
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_->
|
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
|
-
|
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
|
-
|
497
|
-
|
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::
|
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
|
-
|
582
|
-
|
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
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
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
|
-
|
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
|
-
|
606
|
-
|
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
|
-
|
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
|
-
//
|
849
|
-
|
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("
|
852
|
-
return APIError::
|
880
|
+
HELPER_LOG("Header buffer overflow");
|
881
|
+
return APIError::BAD_DATA_PACKET;
|
853
882
|
}
|
854
883
|
|
855
|
-
|
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_[
|
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_[
|
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
|
-
|
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::
|
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>
|
962
|
-
|
963
|
-
|
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
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
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
|
-
|
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
|
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(
|
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
|
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
|
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
|
-
|
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;
|
esphome/components/api/proto.h
CHANGED
@@ -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 (
|
24
|
-
|
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
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
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.
|
10
|
+
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
|
11
11
|
|
12
12
|
|
13
13
|
async def to_code(config):
|