mirror of
https://github.com/esphome/esphome.git
synced 2026-01-09 11:40:50 -07:00
Merge branch 'object_id_no_ram' into no_send_object_id
This commit is contained in:
@@ -1 +1 @@
|
||||
5ac05ac603766d76b86a05cdf6a43febcaae807fe9e2406d812c47d4b5fed91d
|
||||
94557f94be073390342833aff12ef8676a8b597db5fa770a5a1232e9425cb48f
|
||||
|
||||
@@ -91,6 +91,7 @@ esphome/components/bmp3xx_spi/* @latonita
|
||||
esphome/components/bmp581/* @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
esphome/components/bytebuffer/* @clydebarrow
|
||||
esphome/components/camera/* @bdraco @DT-art1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include requirements.txt
|
||||
recursive-include esphome *.yaml
|
||||
recursive-include esphome *.cpp *.h *.tcc *.c
|
||||
recursive-include esphome *.py.script
|
||||
recursive-include esphome LICENSE.txt
|
||||
|
||||
@@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*"
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
RUN if command -v apk > /dev/null; then \
|
||||
apk add --no-cache build-base; \
|
||||
else \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
|
||||
|
||||
@@ -789,7 +789,13 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = compile_program(args, config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
if CORE.is_host:
|
||||
from esphome.platformio_api import get_idedata
|
||||
|
||||
program_path = str(get_idedata(config).firmware_elf_path)
|
||||
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
|
||||
else:
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -839,10 +845,8 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if CORE.is_host:
|
||||
from esphome.platformio_api import get_idedata
|
||||
|
||||
idedata = get_idedata(config)
|
||||
if idedata is None:
|
||||
return 1
|
||||
program_path = idedata.raw["prog_path"]
|
||||
program_path = str(get_idedata(config).firmware_elf_path)
|
||||
_LOGGER.info("Running program from path '%s'", program_path)
|
||||
return run_external_process(program_path)
|
||||
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
|
||||
@@ -102,7 +102,7 @@ message HelloRequest {
|
||||
// For example "Home Assistant"
|
||||
// Not strictly necessary to send but nice for debugging
|
||||
// purposes.
|
||||
string client_info = 1 [(pointer_to_buffer) = true];
|
||||
string client_info = 1;
|
||||
uint32 api_version_major = 2;
|
||||
uint32 api_version_minor = 3;
|
||||
}
|
||||
@@ -139,7 +139,7 @@ message AuthenticationRequest {
|
||||
option (ifdef) = "USE_API_PASSWORD";
|
||||
|
||||
// The password to log in with
|
||||
string password = 1 [(pointer_to_buffer) = true];
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
// Confirmation of successful connection. After this the connection is available for all traffic.
|
||||
@@ -477,7 +477,7 @@ message FanCommandRequest {
|
||||
bool has_speed_level = 10;
|
||||
int32 speed_level = 11;
|
||||
bool has_preset_mode = 12;
|
||||
string preset_mode = 13 [(pointer_to_buffer) = true];
|
||||
string preset_mode = 13;
|
||||
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -579,7 +579,7 @@ message LightCommandRequest {
|
||||
bool has_flash_length = 16;
|
||||
uint32 flash_length = 17;
|
||||
bool has_effect = 18;
|
||||
string effect = 19 [(pointer_to_buffer) = true];
|
||||
string effect = 19;
|
||||
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -824,9 +824,9 @@ message HomeAssistantStateResponse {
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
|
||||
string entity_id = 1 [(pointer_to_buffer) = true];
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
string attribute = 3 [(pointer_to_buffer) = true];
|
||||
string entity_id = 1;
|
||||
string state = 2;
|
||||
string attribute = 3;
|
||||
}
|
||||
|
||||
// ==================== IMPORT TIME ====================
|
||||
@@ -841,7 +841,7 @@ message GetTimeResponse {
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 epoch_seconds = 1;
|
||||
string timezone = 2 [(pointer_to_buffer) = true];
|
||||
string timezone = 2;
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
@@ -1091,11 +1091,11 @@ message ClimateCommandRequest {
|
||||
bool has_swing_mode = 14;
|
||||
ClimateSwingMode swing_mode = 15;
|
||||
bool has_custom_fan_mode = 16;
|
||||
string custom_fan_mode = 17 [(pointer_to_buffer) = true];
|
||||
string custom_fan_mode = 17;
|
||||
bool has_preset = 18;
|
||||
ClimatePreset preset = 19;
|
||||
bool has_custom_preset = 20;
|
||||
string custom_preset = 21 [(pointer_to_buffer) = true];
|
||||
string custom_preset = 21;
|
||||
bool has_target_humidity = 22;
|
||||
float target_humidity = 23;
|
||||
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -1274,7 +1274,7 @@ message SelectCommandRequest {
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
string state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
|
||||
@@ -473,7 +473,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
if (msg.has_direction)
|
||||
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
|
||||
if (msg.has_preset_mode)
|
||||
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
|
||||
call.set_preset_mode(msg.preset_mode.c_str(), msg.preset_mode.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -559,7 +559,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
if (msg.has_flash_length)
|
||||
call.set_flash_length(msg.flash_length);
|
||||
if (msg.has_effect)
|
||||
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
|
||||
call.set_effect(msg.effect.c_str(), msg.effect.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -738,11 +738,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
|
||||
if (msg.has_fan_mode)
|
||||
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
|
||||
if (msg.has_custom_fan_mode)
|
||||
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
|
||||
call.set_fan_mode(msg.custom_fan_mode.c_str(), msg.custom_fan_mode.size());
|
||||
if (msg.has_preset)
|
||||
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
|
||||
if (msg.has_custom_preset)
|
||||
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
|
||||
call.set_preset(msg.custom_preset.c_str(), msg.custom_preset.size());
|
||||
if (msg.has_swing_mode)
|
||||
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
|
||||
call.perform();
|
||||
@@ -931,7 +931,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||
call.set_option(reinterpret_cast<const char *>(msg.state), msg.state_len);
|
||||
call.set_option(msg.state.c_str(), msg.state.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1153,9 +1153,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
if (value.timezone_len > 0) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
|
||||
value.timezone_len);
|
||||
if (!value.timezone.empty()) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1522,7 +1521,7 @@ void APIConnection::complete_authentication_() {
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
|
||||
this->client_info_.name.assign(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_info_.peername = this->helper_->getpeername();
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
@@ -1550,7 +1549,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
|
||||
AuthenticationResponse resp;
|
||||
// bool invalid_password = 1;
|
||||
resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len);
|
||||
resp.invalid_password = !this->parent_->check_password(msg.password.byte(), msg.password.size());
|
||||
if (!resp.invalid_password) {
|
||||
this->complete_authentication_();
|
||||
}
|
||||
@@ -1693,27 +1692,28 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
// Skip if entity_id is empty (invalid message)
|
||||
if (msg.entity_id_len == 0) {
|
||||
if (msg.entity_id.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
// Compare entity_id: check length matches and content matches
|
||||
size_t entity_id_len = strlen(it.entity_id);
|
||||
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
|
||||
if (entity_id_len != msg.entity_id.size() ||
|
||||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare attribute: either both have matching attribute, or both have none
|
||||
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
|
||||
if (sub_attr_len != msg.attribute_len ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
|
||||
if (sub_attr_len != msg.attribute.size() ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create temporary string for callback (callback takes const std::string &)
|
||||
// Handle empty state (nullptr with len=0)
|
||||
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
|
||||
// Handle empty state
|
||||
std::string state(!msg.state.empty() ? msg.state.c_str() : "", msg.state.size());
|
||||
it.callback(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->client_info = value.data();
|
||||
this->client_info_len = value.size();
|
||||
this->client_info = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -49,9 +47,7 @@ void HelloResponse::calculate_size(ProtoSize &size) const {
|
||||
bool AuthenticationRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->password = value.data();
|
||||
this->password_len = value.size();
|
||||
this->password = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -448,9 +444,7 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 13: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->preset_mode = value.data();
|
||||
this->preset_mode_len = value.size();
|
||||
this->preset_mode = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -615,9 +609,7 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 19: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->effect = value.data();
|
||||
this->effect_len = value.size();
|
||||
this->effect = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -859,7 +851,6 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
|
||||
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->key = value.data();
|
||||
this->key_len = value.size();
|
||||
break;
|
||||
@@ -936,12 +927,12 @@ bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt v
|
||||
}
|
||||
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 3:
|
||||
this->error_message = value.as_string();
|
||||
case 3: {
|
||||
this->error_message = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
case 4: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->response_data = value.data();
|
||||
this->response_data_len = value.size();
|
||||
break;
|
||||
@@ -967,21 +958,15 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
|
||||
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->entity_id = value.data();
|
||||
this->entity_id_len = value.size();
|
||||
this->entity_id = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->state = value.data();
|
||||
this->state_len = value.size();
|
||||
this->state = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->attribute = value.data();
|
||||
this->attribute_len = value.size();
|
||||
this->attribute = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -993,9 +978,7 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
|
||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->timezone = value.data();
|
||||
this->timezone_len = value.size();
|
||||
this->timezone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -1060,9 +1043,10 @@ bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value)
|
||||
}
|
||||
bool ExecuteServiceArgument::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 4:
|
||||
this->string_ = value.as_string();
|
||||
case 4: {
|
||||
this->string_ = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 9:
|
||||
this->string_array.push_back(value.as_string());
|
||||
break;
|
||||
@@ -1153,7 +1137,7 @@ void ExecuteServiceResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_bool(1, this->success);
|
||||
size.add_length(1, this->error_message_ref_.size());
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
size.add_length(4, this->response_data_len);
|
||||
size.add_length(1, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
@@ -1408,15 +1392,11 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
|
||||
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 17: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->custom_fan_mode = value.data();
|
||||
this->custom_fan_mode_len = value.size();
|
||||
this->custom_fan_mode = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 21: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->custom_preset = value.data();
|
||||
this->custom_preset_len = value.size();
|
||||
this->custom_preset = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -1702,9 +1682,7 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->state = value.data();
|
||||
this->state_len = value.size();
|
||||
this->state = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -1808,9 +1786,10 @@ bool SirenCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool SirenCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 5:
|
||||
this->tone = value.as_string();
|
||||
case 5: {
|
||||
this->tone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1899,9 +1878,10 @@ bool LockCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool LockCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 4:
|
||||
this->code = value.as_string();
|
||||
case 4: {
|
||||
this->code = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2069,9 +2049,10 @@ bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt val
|
||||
}
|
||||
bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 7:
|
||||
this->media_url = value.as_string();
|
||||
case 7: {
|
||||
this->media_url = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2279,7 +2260,6 @@ bool BluetoothGATTWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt val
|
||||
bool BluetoothGATTWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 4: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
@@ -2318,7 +2298,6 @@ bool BluetoothGATTWriteDescriptorRequest::decode_varint(uint32_t field_id, Proto
|
||||
bool BluetoothGATTWriteDescriptorRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 3: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
@@ -2502,12 +2481,14 @@ bool VoiceAssistantResponse::decode_varint(uint32_t field_id, ProtoVarInt value)
|
||||
}
|
||||
bool VoiceAssistantEventData::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->name = value.as_string();
|
||||
case 1: {
|
||||
this->name = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 2:
|
||||
this->value = value.as_string();
|
||||
}
|
||||
case 2: {
|
||||
this->value = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2583,12 +2564,14 @@ bool VoiceAssistantTimerEventResponse::decode_varint(uint32_t field_id, ProtoVar
|
||||
}
|
||||
bool VoiceAssistantTimerEventResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
this->timer_id = value.as_string();
|
||||
case 2: {
|
||||
this->timer_id = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 3:
|
||||
this->name = value.as_string();
|
||||
}
|
||||
case 3: {
|
||||
this->name = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2606,15 +2589,18 @@ bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt
|
||||
}
|
||||
bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->media_id = value.as_string();
|
||||
case 1: {
|
||||
this->media_id = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 2:
|
||||
this->text = value.as_string();
|
||||
}
|
||||
case 2: {
|
||||
this->text = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 3:
|
||||
this->preannounce_media_id = value.as_string();
|
||||
}
|
||||
case 3: {
|
||||
this->preannounce_media_id = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2650,24 +2636,29 @@ bool VoiceAssistantExternalWakeWord::decode_varint(uint32_t field_id, ProtoVarIn
|
||||
}
|
||||
bool VoiceAssistantExternalWakeWord::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->id = value.as_string();
|
||||
case 1: {
|
||||
this->id = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 2:
|
||||
this->wake_word = value.as_string();
|
||||
}
|
||||
case 2: {
|
||||
this->wake_word = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
this->trained_languages.push_back(value.as_string());
|
||||
break;
|
||||
case 4:
|
||||
this->model_type = value.as_string();
|
||||
case 4: {
|
||||
this->model_type = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 6:
|
||||
this->model_hash = value.as_string();
|
||||
}
|
||||
case 6: {
|
||||
this->model_hash = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
case 7:
|
||||
this->url = value.as_string();
|
||||
}
|
||||
case 7: {
|
||||
this->url = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2777,9 +2768,10 @@ bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarI
|
||||
}
|
||||
bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 3:
|
||||
this->code = value.as_string();
|
||||
case 3: {
|
||||
this->code = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2861,9 +2853,10 @@ bool TextCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool TextCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
this->state = value.as_string();
|
||||
case 2: {
|
||||
this->state = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -3331,7 +3324,6 @@ bool UpdateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
bool ZWaveProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
@@ -3356,7 +3348,6 @@ bool ZWaveProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
bool ZWaveProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
@@ -3372,7 +3363,7 @@ void ZWaveProxyRequest::encode(ProtoWriteBuffer buffer) const {
|
||||
}
|
||||
void ZWaveProxyRequest::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->type));
|
||||
size.add_length(2, this->data_len);
|
||||
size.add_length(1, this->data_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -357,12 +357,11 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
||||
class HelloRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 1;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "hello_request"; }
|
||||
#endif
|
||||
const uint8_t *client_info{nullptr};
|
||||
uint16_t client_info_len{0};
|
||||
StringRef client_info{};
|
||||
uint32_t api_version_major{0};
|
||||
uint32_t api_version_minor{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -398,12 +397,11 @@ class HelloResponse final : public ProtoMessage {
|
||||
class AuthenticationRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 3;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 9;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "authentication_request"; }
|
||||
#endif
|
||||
const uint8_t *password{nullptr};
|
||||
uint16_t password_len{0};
|
||||
StringRef password{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -784,7 +782,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
|
||||
class FanCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 31;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 48;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 38;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "fan_command_request"; }
|
||||
#endif
|
||||
@@ -797,8 +795,7 @@ class FanCommandRequest final : public CommandProtoMessage {
|
||||
bool has_speed_level{false};
|
||||
int32_t speed_level{0};
|
||||
bool has_preset_mode{false};
|
||||
const uint8_t *preset_mode{nullptr};
|
||||
uint16_t preset_mode_len{0};
|
||||
StringRef preset_mode{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -860,7 +857,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
|
||||
class LightCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 32;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 122;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 112;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "light_command_request"; }
|
||||
#endif
|
||||
@@ -889,8 +886,7 @@ class LightCommandRequest final : public CommandProtoMessage {
|
||||
bool has_flash_length{false};
|
||||
uint32_t flash_length{0};
|
||||
bool has_effect{false};
|
||||
const uint8_t *effect{nullptr};
|
||||
uint16_t effect_len{0};
|
||||
StringRef effect{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1171,7 +1167,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
|
||||
#endif
|
||||
uint32_t call_id{0};
|
||||
bool success{false};
|
||||
std::string error_message{};
|
||||
StringRef error_message{};
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
const uint8_t *response_data{nullptr};
|
||||
uint16_t response_data_len{0};
|
||||
@@ -1222,16 +1218,13 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
|
||||
class HomeAssistantStateResponse final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 40;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 57;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 27;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "home_assistant_state_response"; }
|
||||
#endif
|
||||
const uint8_t *entity_id{nullptr};
|
||||
uint16_t entity_id_len{0};
|
||||
const uint8_t *state{nullptr};
|
||||
uint16_t state_len{0};
|
||||
const uint8_t *attribute{nullptr};
|
||||
uint16_t attribute_len{0};
|
||||
StringRef entity_id{};
|
||||
StringRef state{};
|
||||
StringRef attribute{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1256,13 +1249,12 @@ class GetTimeRequest final : public ProtoMessage {
|
||||
class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 37;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 24;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 14;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "get_time_response"; }
|
||||
#endif
|
||||
uint32_t epoch_seconds{0};
|
||||
const uint8_t *timezone{nullptr};
|
||||
uint16_t timezone_len{0};
|
||||
StringRef timezone{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1310,7 +1302,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
|
||||
bool bool_{false};
|
||||
int32_t legacy_int{0};
|
||||
float float_{0.0f};
|
||||
std::string string_{};
|
||||
StringRef string_{};
|
||||
int32_t int_{0};
|
||||
FixedVector<bool> bool_array{};
|
||||
FixedVector<int32_t> int_array{};
|
||||
@@ -1499,7 +1491,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
|
||||
class ClimateCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 48;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 104;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 84;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "climate_command_request"; }
|
||||
#endif
|
||||
@@ -1516,13 +1508,11 @@ class ClimateCommandRequest final : public CommandProtoMessage {
|
||||
bool has_swing_mode{false};
|
||||
enums::ClimateSwingMode swing_mode{};
|
||||
bool has_custom_fan_mode{false};
|
||||
const uint8_t *custom_fan_mode{nullptr};
|
||||
uint16_t custom_fan_mode_len{0};
|
||||
StringRef custom_fan_mode{};
|
||||
bool has_preset{false};
|
||||
enums::ClimatePreset preset{};
|
||||
bool has_custom_preset{false};
|
||||
const uint8_t *custom_preset{nullptr};
|
||||
uint16_t custom_preset_len{0};
|
||||
StringRef custom_preset{};
|
||||
bool has_target_humidity{false};
|
||||
float target_humidity{0.0f};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1695,12 +1685,11 @@ class SelectStateResponse final : public StateResponseProtoMessage {
|
||||
class SelectCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 54;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 28;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 18;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "select_command_request"; }
|
||||
#endif
|
||||
const uint8_t *state{nullptr};
|
||||
uint16_t state_len{0};
|
||||
StringRef state{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1756,7 +1745,7 @@ class SirenCommandRequest final : public CommandProtoMessage {
|
||||
bool has_state{false};
|
||||
bool state{false};
|
||||
bool has_tone{false};
|
||||
std::string tone{};
|
||||
StringRef tone{};
|
||||
bool has_duration{false};
|
||||
uint32_t duration{0};
|
||||
bool has_volume{false};
|
||||
@@ -1817,7 +1806,7 @@ class LockCommandRequest final : public CommandProtoMessage {
|
||||
#endif
|
||||
enums::LockCommand command{};
|
||||
bool has_code{false};
|
||||
std::string code{};
|
||||
StringRef code{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1927,7 +1916,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage {
|
||||
bool has_volume{false};
|
||||
float volume{0.0f};
|
||||
bool has_media_url{false};
|
||||
std::string media_url{};
|
||||
StringRef media_url{};
|
||||
bool has_announcement{false};
|
||||
bool announcement{false};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -2503,8 +2492,8 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage {
|
||||
};
|
||||
class VoiceAssistantEventData final : public ProtoDecodableMessage {
|
||||
public:
|
||||
std::string name{};
|
||||
std::string value{};
|
||||
StringRef name{};
|
||||
StringRef value{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2562,8 +2551,8 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage {
|
||||
const char *message_name() const override { return "voice_assistant_timer_event_response"; }
|
||||
#endif
|
||||
enums::VoiceAssistantTimerEvent event_type{};
|
||||
std::string timer_id{};
|
||||
std::string name{};
|
||||
StringRef timer_id{};
|
||||
StringRef name{};
|
||||
uint32_t total_seconds{0};
|
||||
uint32_t seconds_left{0};
|
||||
bool is_active{false};
|
||||
@@ -2582,9 +2571,9 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "voice_assistant_announce_request"; }
|
||||
#endif
|
||||
std::string media_id{};
|
||||
std::string text{};
|
||||
std::string preannounce_media_id{};
|
||||
StringRef media_id{};
|
||||
StringRef text{};
|
||||
StringRef preannounce_media_id{};
|
||||
bool start_conversation{false};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
@@ -2627,13 +2616,13 @@ class VoiceAssistantWakeWord final : public ProtoMessage {
|
||||
};
|
||||
class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
|
||||
public:
|
||||
std::string id{};
|
||||
std::string wake_word{};
|
||||
StringRef id{};
|
||||
StringRef wake_word{};
|
||||
std::vector<std::string> trained_languages{};
|
||||
std::string model_type{};
|
||||
StringRef model_type{};
|
||||
uint32_t model_size{0};
|
||||
std::string model_hash{};
|
||||
std::string url{};
|
||||
StringRef model_hash{};
|
||||
StringRef url{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2734,7 +2723,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage {
|
||||
const char *message_name() const override { return "alarm_control_panel_command_request"; }
|
||||
#endif
|
||||
enums::AlarmControlPanelStateCommand command{};
|
||||
std::string code{};
|
||||
StringRef code{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2791,7 +2780,7 @@ class TextCommandRequest final : public CommandProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "text_command_request"; }
|
||||
#endif
|
||||
std::string state{};
|
||||
StringRef state{};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -736,7 +736,7 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
||||
void HelloRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HelloRequest");
|
||||
out.append(" client_info: ");
|
||||
out.append(format_hex_pretty(this->client_info, this->client_info_len));
|
||||
out.append("'").append(this->client_info.c_str(), this->client_info.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "api_version_major", this->api_version_major);
|
||||
dump_field(out, "api_version_minor", this->api_version_minor);
|
||||
@@ -752,7 +752,7 @@ void HelloResponse::dump_to(std::string &out) const {
|
||||
void AuthenticationRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "AuthenticationRequest");
|
||||
out.append(" password: ");
|
||||
out.append(format_hex_pretty(this->password, this->password_len));
|
||||
out.append("'").append(this->password.c_str(), this->password.size()).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
void AuthenticationResponse::dump_to(std::string &out) const {
|
||||
@@ -965,7 +965,7 @@ void FanCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "speed_level", this->speed_level);
|
||||
dump_field(out, "has_preset_mode", this->has_preset_mode);
|
||||
out.append(" preset_mode: ");
|
||||
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
|
||||
out.append("'").append(this->preset_mode.c_str(), this->preset_mode.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
@@ -1043,7 +1043,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "flash_length", this->flash_length);
|
||||
dump_field(out, "has_effect", this->has_effect);
|
||||
out.append(" effect: ");
|
||||
out.append(format_hex_pretty(this->effect, this->effect_len));
|
||||
out.append("'").append(this->effect.c_str(), this->effect.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
@@ -1205,7 +1205,9 @@ void HomeassistantActionResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantActionResponse");
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
dump_field(out, "success", this->success);
|
||||
dump_field(out, "error_message", this->error_message);
|
||||
out.append(" error_message: ");
|
||||
out.append("'").append(this->error_message.c_str(), this->error_message.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
out.append(" response_data: ");
|
||||
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
||||
@@ -1226,13 +1228,13 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
|
||||
void HomeAssistantStateResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeAssistantStateResponse");
|
||||
out.append(" entity_id: ");
|
||||
out.append(format_hex_pretty(this->entity_id, this->entity_id_len));
|
||||
out.append("'").append(this->entity_id.c_str(), this->entity_id.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" state: ");
|
||||
out.append(format_hex_pretty(this->state, this->state_len));
|
||||
out.append("'").append(this->state.c_str(), this->state.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" attribute: ");
|
||||
out.append(format_hex_pretty(this->attribute, this->attribute_len));
|
||||
out.append("'").append(this->attribute.c_str(), this->attribute.size()).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
#endif
|
||||
@@ -1241,7 +1243,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "GetTimeResponse");
|
||||
dump_field(out, "epoch_seconds", this->epoch_seconds);
|
||||
out.append(" timezone: ");
|
||||
out.append(format_hex_pretty(this->timezone, this->timezone_len));
|
||||
out.append("'").append(this->timezone.c_str(), this->timezone.size()).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
@@ -1266,7 +1268,9 @@ void ExecuteServiceArgument::dump_to(std::string &out) const {
|
||||
dump_field(out, "bool_", this->bool_);
|
||||
dump_field(out, "legacy_int", this->legacy_int);
|
||||
dump_field(out, "float_", this->float_);
|
||||
dump_field(out, "string_", this->string_);
|
||||
out.append(" string_: ");
|
||||
out.append("'").append(this->string_.c_str(), this->string_.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "int_", this->int_);
|
||||
for (const auto it : this->bool_array) {
|
||||
dump_field(out, "bool_array", static_cast<bool>(it), 4);
|
||||
@@ -1424,13 +1428,13 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
|
||||
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
|
||||
out.append(" custom_fan_mode: ");
|
||||
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
|
||||
out.append("'").append(this->custom_fan_mode.c_str(), this->custom_fan_mode.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "has_preset", this->has_preset);
|
||||
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
|
||||
dump_field(out, "has_custom_preset", this->has_custom_preset);
|
||||
out.append(" custom_preset: ");
|
||||
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
|
||||
out.append("'").append(this->custom_preset.c_str(), this->custom_preset.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "has_target_humidity", this->has_target_humidity);
|
||||
dump_field(out, "target_humidity", this->target_humidity);
|
||||
@@ -1558,7 +1562,7 @@ void SelectCommandRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "SelectCommandRequest");
|
||||
dump_field(out, "key", this->key);
|
||||
out.append(" state: ");
|
||||
out.append(format_hex_pretty(this->state, this->state_len));
|
||||
out.append("'").append(this->state.c_str(), this->state.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
@@ -1599,7 +1603,9 @@ void SirenCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "has_state", this->has_state);
|
||||
dump_field(out, "state", this->state);
|
||||
dump_field(out, "has_tone", this->has_tone);
|
||||
dump_field(out, "tone", this->tone);
|
||||
out.append(" tone: ");
|
||||
out.append("'").append(this->tone.c_str(), this->tone.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "has_duration", this->has_duration);
|
||||
dump_field(out, "duration", this->duration);
|
||||
dump_field(out, "has_volume", this->has_volume);
|
||||
@@ -1641,7 +1647,9 @@ void LockCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "key", this->key);
|
||||
dump_field(out, "command", static_cast<enums::LockCommand>(this->command));
|
||||
dump_field(out, "has_code", this->has_code);
|
||||
dump_field(out, "code", this->code);
|
||||
out.append(" code: ");
|
||||
out.append("'").append(this->code.c_str(), this->code.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
@@ -1719,7 +1727,9 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "has_volume", this->has_volume);
|
||||
dump_field(out, "volume", this->volume);
|
||||
dump_field(out, "has_media_url", this->has_media_url);
|
||||
dump_field(out, "media_url", this->media_url);
|
||||
out.append(" media_url: ");
|
||||
out.append("'").append(this->media_url.c_str(), this->media_url.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "has_announcement", this->has_announcement);
|
||||
dump_field(out, "announcement", this->announcement);
|
||||
#ifdef USE_DEVICES
|
||||
@@ -1949,8 +1959,12 @@ void VoiceAssistantResponse::dump_to(std::string &out) const {
|
||||
}
|
||||
void VoiceAssistantEventData::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "VoiceAssistantEventData");
|
||||
dump_field(out, "name", this->name);
|
||||
dump_field(out, "value", this->value);
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name.c_str(), this->name.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" value: ");
|
||||
out.append("'").append(this->value.c_str(), this->value.size()).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
void VoiceAssistantEventResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "VoiceAssistantEventResponse");
|
||||
@@ -1975,17 +1989,27 @@ void VoiceAssistantAudio::dump_to(std::string &out) const {
|
||||
void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "VoiceAssistantTimerEventResponse");
|
||||
dump_field(out, "event_type", static_cast<enums::VoiceAssistantTimerEvent>(this->event_type));
|
||||
dump_field(out, "timer_id", this->timer_id);
|
||||
dump_field(out, "name", this->name);
|
||||
out.append(" timer_id: ");
|
||||
out.append("'").append(this->timer_id.c_str(), this->timer_id.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" name: ");
|
||||
out.append("'").append(this->name.c_str(), this->name.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "total_seconds", this->total_seconds);
|
||||
dump_field(out, "seconds_left", this->seconds_left);
|
||||
dump_field(out, "is_active", this->is_active);
|
||||
}
|
||||
void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "VoiceAssistantAnnounceRequest");
|
||||
dump_field(out, "media_id", this->media_id);
|
||||
dump_field(out, "text", this->text);
|
||||
dump_field(out, "preannounce_media_id", this->preannounce_media_id);
|
||||
out.append(" media_id: ");
|
||||
out.append("'").append(this->media_id.c_str(), this->media_id.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" text: ");
|
||||
out.append("'").append(this->text.c_str(), this->text.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" preannounce_media_id: ");
|
||||
out.append("'").append(this->preannounce_media_id.c_str(), this->preannounce_media_id.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "start_conversation", this->start_conversation);
|
||||
}
|
||||
void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
|
||||
@@ -1999,15 +2023,25 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const {
|
||||
}
|
||||
void VoiceAssistantExternalWakeWord::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "VoiceAssistantExternalWakeWord");
|
||||
dump_field(out, "id", this->id);
|
||||
dump_field(out, "wake_word", this->wake_word);
|
||||
out.append(" id: ");
|
||||
out.append("'").append(this->id.c_str(), this->id.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" wake_word: ");
|
||||
out.append("'").append(this->wake_word.c_str(), this->wake_word.size()).append("'");
|
||||
out.append("\n");
|
||||
for (const auto &it : this->trained_languages) {
|
||||
dump_field(out, "trained_languages", it, 4);
|
||||
}
|
||||
dump_field(out, "model_type", this->model_type);
|
||||
out.append(" model_type: ");
|
||||
out.append("'").append(this->model_type.c_str(), this->model_type.size()).append("'");
|
||||
out.append("\n");
|
||||
dump_field(out, "model_size", this->model_size);
|
||||
dump_field(out, "model_hash", this->model_hash);
|
||||
dump_field(out, "url", this->url);
|
||||
out.append(" model_hash: ");
|
||||
out.append("'").append(this->model_hash.c_str(), this->model_hash.size()).append("'");
|
||||
out.append("\n");
|
||||
out.append(" url: ");
|
||||
out.append("'").append(this->url.c_str(), this->url.size()).append("'");
|
||||
out.append("\n");
|
||||
}
|
||||
void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "VoiceAssistantConfigurationRequest");
|
||||
@@ -2066,7 +2100,9 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "AlarmControlPanelCommandRequest");
|
||||
dump_field(out, "key", this->key);
|
||||
dump_field(out, "command", static_cast<enums::AlarmControlPanelStateCommand>(this->command));
|
||||
dump_field(out, "code", this->code);
|
||||
out.append(" code: ");
|
||||
out.append("'").append(this->code.c_str(), this->code.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
@@ -2103,7 +2139,9 @@ void TextStateResponse::dump_to(std::string &out) const {
|
||||
void TextCommandRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "TextCommandRequest");
|
||||
dump_field(out, "key", this->key);
|
||||
dump_field(out, "state", this->state);
|
||||
out.append(" state: ");
|
||||
out.append("'").append(this->state.c_str(), this->state.size()).append("'");
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars)
|
||||
static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64;
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
// placeholder class for static TAG .
|
||||
@@ -151,7 +155,10 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)];
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len));
|
||||
#endif
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
|
||||
@@ -50,6 +50,7 @@ TYPES = [
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(cg.Component),
|
||||
cv.GenerateID(CONF_BME68X_BSEC2_ID): cv.use_id(BME68xBSEC2Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
|
||||
36
esphome/components/bthome_mithermometer/__init__.py
Normal file
36
esphome/components/bthome_mithermometer/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
|
||||
|
||||
CODEOWNERS = ["@nagyrobi"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
BLE_DEVICE_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA
|
||||
|
||||
bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer")
|
||||
BTHomeMiThermometer = bthome_mithermometer_ns.class_(
|
||||
"BTHomeMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
||||
)
|
||||
|
||||
|
||||
def bthome_mithermometer_base_schema(extra_schema=None):
|
||||
if extra_schema is None:
|
||||
extra_schema = {}
|
||||
return (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
}
|
||||
)
|
||||
.extend(BLE_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(extra_schema)
|
||||
)
|
||||
|
||||
|
||||
async def setup_bthome_mithermometer(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_ble_device(var, config)
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
298
esphome/components/bthome_mithermometer/bthome_ble.cpp
Normal file
298
esphome/components/bthome_mithermometer/bthome_ble.cpp
Normal file
@@ -0,0 +1,298 @@
|
||||
#include "bthome_ble.h"
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
|
||||
static const char *const TAG = "bthome_mithermometer";
|
||||
|
||||
static std::string format_mac_address(uint64_t address) {
|
||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||
mac[i] = (address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
|
||||
}
|
||||
|
||||
char buffer[MAC_ADDRESS_SIZE * 3];
|
||||
format_mac_addr_upper(mac.data(), buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
static bool get_bthome_value_length(uint8_t obj_type, size_t &value_length) {
|
||||
switch (obj_type) {
|
||||
case 0x00: // packet id
|
||||
case 0x01: // battery
|
||||
case 0x09: // count (uint8)
|
||||
case 0x0F: // generic boolean
|
||||
case 0x10: // power (bool)
|
||||
case 0x11: // opening
|
||||
case 0x15: // battery low
|
||||
case 0x16: // battery charging
|
||||
case 0x17: // carbon monoxide
|
||||
case 0x18: // cold
|
||||
case 0x19: // connectivity
|
||||
case 0x1A: // door
|
||||
case 0x1B: // garage door
|
||||
case 0x1C: // gas
|
||||
case 0x1D: // heat
|
||||
case 0x1E: // light
|
||||
case 0x1F: // lock
|
||||
case 0x20: // moisture
|
||||
case 0x21: // motion
|
||||
case 0x22: // moving
|
||||
case 0x23: // occupancy
|
||||
case 0x24: // plug
|
||||
case 0x25: // presence
|
||||
case 0x26: // problem
|
||||
case 0x27: // running
|
||||
case 0x28: // safety
|
||||
case 0x29: // smoke
|
||||
case 0x2A: // sound
|
||||
case 0x2B: // tamper
|
||||
case 0x2C: // vibration
|
||||
case 0x2D: // water leak
|
||||
case 0x2E: // humidity (uint8)
|
||||
case 0x2F: // moisture (uint8)
|
||||
case 0x46: // UV index
|
||||
case 0x57: // temperature (sint8)
|
||||
case 0x58: // temperature (0.35C step)
|
||||
case 0x59: // count (sint8)
|
||||
case 0x60: // channel
|
||||
value_length = 1;
|
||||
return true;
|
||||
case 0x02: // temperature (0.01C)
|
||||
case 0x03: // humidity
|
||||
case 0x06: // mass (kg)
|
||||
case 0x07: // mass (lb)
|
||||
case 0x08: // dewpoint
|
||||
case 0x0C: // voltage (mV)
|
||||
case 0x0D: // pm2.5
|
||||
case 0x0E: // pm10
|
||||
case 0x12: // CO2
|
||||
case 0x13: // TVOC
|
||||
case 0x14: // moisture
|
||||
case 0x3D: // count (uint16)
|
||||
case 0x3F: // rotation
|
||||
case 0x40: // distance (mm)
|
||||
case 0x41: // distance (m)
|
||||
case 0x43: // current (A)
|
||||
case 0x44: // speed
|
||||
case 0x45: // temperature (0.1C)
|
||||
case 0x47: // volume (L)
|
||||
case 0x48: // volume (mL)
|
||||
case 0x49: // volume flow rate
|
||||
case 0x4A: // voltage (0.1V)
|
||||
case 0x51: // acceleration
|
||||
case 0x52: // gyroscope
|
||||
case 0x56: // conductivity
|
||||
case 0x5A: // count (sint16)
|
||||
case 0x5D: // current (sint16)
|
||||
case 0x5E: // direction
|
||||
case 0x5F: // precipitation
|
||||
case 0x61: // rotational speed
|
||||
case 0xF0: // button event
|
||||
value_length = 2;
|
||||
return true;
|
||||
case 0x04: // pressure
|
||||
case 0x05: // illuminance
|
||||
case 0x0A: // energy
|
||||
case 0x0B: // power
|
||||
case 0x42: // duration
|
||||
case 0x4B: // gas (uint24)
|
||||
case 0xF2: // firmware version (uint24)
|
||||
value_length = 3;
|
||||
return true;
|
||||
case 0x3E: // count (uint32)
|
||||
case 0x4C: // gas (uint32)
|
||||
case 0x4D: // energy (uint32)
|
||||
case 0x4E: // volume (uint32)
|
||||
case 0x4F: // water (uint32)
|
||||
case 0x50: // timestamp
|
||||
case 0x55: // volume storage
|
||||
case 0x5B: // count (sint32)
|
||||
case 0x5C: // power (sint32)
|
||||
case 0x62: // speed (sint32)
|
||||
case 0x63: // acceleration (sint32)
|
||||
case 0xF1: // firmware version (uint32)
|
||||
value_length = 4;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void BTHomeMiThermometer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
||||
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(this->address_).c_str());
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||
LOG_SENSOR(" ", "Signal Strength", this->signal_strength_);
|
||||
}
|
||||
|
||||
bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
bool matched = false;
|
||||
for (auto &service_data : device.get_service_datas()) {
|
||||
if (this->handle_service_data_(service_data, device)) {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if (matched && this->signal_strength_ != nullptr) {
|
||||
this->signal_strength_->publish_state(device.get_rssi());
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &data = service_data.data;
|
||||
if (data.size() < 2) {
|
||||
ESP_LOGVV(TAG, "BTHome data too short: %zu", data.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t adv_info = data[0];
|
||||
const bool is_encrypted = adv_info & 0x01;
|
||||
const bool mac_included = adv_info & 0x02;
|
||||
const bool is_trigger_based = adv_info & 0x04;
|
||||
const uint8_t version = (adv_info >> 5) & 0x07;
|
||||
|
||||
if (version != 0x02) {
|
||||
ESP_LOGVV(TAG, "Unsupported BTHome version %u", version);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_encrypted) {
|
||||
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t payload_index = 1;
|
||||
uint64_t source_address = device.address_uint64();
|
||||
|
||||
if (mac_included) {
|
||||
if (data.size() < 7) {
|
||||
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
||||
return false;
|
||||
}
|
||||
source_address = 0;
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
source_address = (source_address << 8) | data[1 + i];
|
||||
}
|
||||
payload_index = 7;
|
||||
}
|
||||
|
||||
if (source_address != this->address_) {
|
||||
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(source_address).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload_index >= data.size()) {
|
||||
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reported = false;
|
||||
size_t offset = payload_index;
|
||||
uint8_t last_type = 0;
|
||||
|
||||
while (offset < data.size()) {
|
||||
const uint8_t obj_type = data[offset++];
|
||||
size_t value_length = 0;
|
||||
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
||||
|
||||
if (has_length_byte) {
|
||||
if (offset >= data.size()) {
|
||||
break;
|
||||
}
|
||||
value_length = data[offset++];
|
||||
} else {
|
||||
if (!get_bthome_value_length(obj_type, value_length)) {
|
||||
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value_length == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (offset + value_length > data.size()) {
|
||||
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t *value = &data[offset];
|
||||
offset += value_length;
|
||||
|
||||
if (obj_type < last_type) {
|
||||
ESP_LOGVV(TAG, "BTHome objects not in ascending order");
|
||||
}
|
||||
last_type = obj_type;
|
||||
|
||||
switch (obj_type) {
|
||||
case 0x00: { // packet id
|
||||
const uint8_t packet_id = value[0];
|
||||
if (this->last_packet_id_.has_value() && *this->last_packet_id_ == packet_id) {
|
||||
return reported;
|
||||
}
|
||||
this->last_packet_id_ = packet_id;
|
||||
break;
|
||||
}
|
||||
case 0x01: { // battery percentage
|
||||
if (this->battery_level_ != nullptr) {
|
||||
this->battery_level_->publish_state(value[0]);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x0C: { // battery voltage (mV)
|
||||
if (this->battery_voltage_ != nullptr) {
|
||||
const uint16_t raw = encode_uint16(value[1], value[0]);
|
||||
this->battery_voltage_->publish_state(raw * 0.001f);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x02: { // temperature
|
||||
if (this->temperature_ != nullptr) {
|
||||
const int16_t raw = encode_uint16(value[1], value[0]);
|
||||
this->temperature_->publish_state(raw * 0.01f);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x03: { // humidity
|
||||
if (this->humidity_ != nullptr) {
|
||||
const uint16_t raw = encode_uint16(value[1], value[0]);
|
||||
this->humidity_->publish_state(raw * 0.01f);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reported) {
|
||||
ESP_LOGD(TAG, "BTHome data%sfrom %s", is_trigger_based ? " (triggered) " : " ", device.address_str().c_str());
|
||||
}
|
||||
|
||||
return reported;
|
||||
}
|
||||
|
||||
} // namespace bthome_mithermometer
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
44
esphome/components/bthome_mithermometer/bthome_ble.h
Normal file
44
esphome/components/bthome_mithermometer/bthome_ble.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
|
||||
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
public:
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
|
||||
void set_battery_voltage(sensor::Sensor *battery_voltage) { this->battery_voltage_ = battery_voltage; }
|
||||
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
|
||||
|
||||
void dump_config() override;
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
|
||||
protected:
|
||||
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
const esp32_ble_tracker::ESPBTDevice &device);
|
||||
|
||||
uint64_t address_{0};
|
||||
optional<uint8_t> last_packet_id_{};
|
||||
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
sensor::Sensor *battery_level_{nullptr};
|
||||
sensor::Sensor *battery_voltage_{nullptr};
|
||||
sensor::Sensor *signal_strength_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace bthome_mithermometer
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
88
esphome/components/bthome_mithermometer/sensor.py
Normal file
88
esphome/components/bthome_mithermometer/sensor.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_SIGNAL_STRENGTH,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
from . import bthome_mithermometer_base_schema, setup_bthome_mithermometer
|
||||
|
||||
CODEOWNERS = ["@nagyrobi"]
|
||||
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
CONFIG_SCHEMA = bthome_mithermometer_base_schema(
|
||||
{
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon="mdi:battery-plus",
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await setup_bthome_mithermometer(var, config)
|
||||
|
||||
if temp_sens := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temp_sens)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if humi_sens := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humi_sens)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if batl_sens := config.get(CONF_BATTERY_LEVEL):
|
||||
sens = await sensor.new_sensor(batl_sens)
|
||||
cg.add(var.set_battery_level(sens))
|
||||
if batv_sens := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(batv_sens)
|
||||
cg.add(var.set_battery_voltage(sens))
|
||||
if sgnl_sens := config.get(CONF_SIGNAL_STRENGTH):
|
||||
sens = await sensor.new_sensor(sgnl_sens)
|
||||
cg.add(var.set_signal_strength(sens))
|
||||
@@ -51,7 +51,7 @@ void DallasTemperatureSensor::update() {
|
||||
}
|
||||
|
||||
float tempc = this->get_temp_c_();
|
||||
ESP_LOGD(TAG, "'%s': Got Temperature=%.1f°C", this->get_name().c_str(), tempc);
|
||||
ESP_LOGD(TAG, "'%s': Got Temperature=%f°C", this->get_name().c_str(), tempc);
|
||||
this->publish_state(tempc);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ee895 {
|
||||
|
||||
static const char *const TAG = "ee895";
|
||||
|
||||
// Serial number is 16 bytes
|
||||
static constexpr size_t EE895_SERIAL_NUMBER_SIZE = 16;
|
||||
|
||||
static const uint16_t CRC16_ONEWIRE_START = 0xFFFF;
|
||||
static const uint8_t FUNCTION_CODE_READ = 0x03;
|
||||
static const uint16_t SERIAL_NUMBER = 0x0000;
|
||||
@@ -26,7 +29,10 @@ void EE895Component::setup() {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex(serial_number + 2, 16).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char serial_hex[format_hex_size(EE895_SERIAL_NUMBER_SIZE)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, " Serial Number: 0x%s", format_hex_to(serial_hex, serial_number + 2, EE895_SERIAL_NUMBER_SIZE));
|
||||
}
|
||||
|
||||
void EE895Component::dump_config() {
|
||||
|
||||
@@ -76,6 +76,12 @@ class EPaperBase : public Display,
|
||||
return 0;
|
||||
}
|
||||
void fill(Color color) override {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
auto pixel_color = color_to_bit(color) ? 0xFF : 0x00;
|
||||
|
||||
// We store 8 pixels per byte
|
||||
|
||||
@@ -97,6 +97,12 @@ void EPaperSpectraE6::deep_sleep() {
|
||||
}
|
||||
|
||||
void EPaperSpectraE6::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
EPaperBase::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
auto pixel_color = color_to_hex(color);
|
||||
|
||||
// We store 2 pixels per byte
|
||||
|
||||
@@ -357,11 +357,12 @@ def _is_framework_url(source: str) -> bool:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 3, 2),
|
||||
"latest": cv.Version(3, 3, 4),
|
||||
"dev": cv.Version(3, 3, 4),
|
||||
"recommended": cv.Version(3, 3, 5),
|
||||
"latest": cv.Version(3, 3, 5),
|
||||
"dev": cv.Version(3, 3, 5),
|
||||
}
|
||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
|
||||
cv.Version(3, 3, 4): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 3): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(3, 3, 2): cv.Version(55, 3, 31, "2"),
|
||||
@@ -374,15 +375,33 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(3, 1, 1): cv.Version(53, 3, 11),
|
||||
cv.Version(3, 1, 0): cv.Version(53, 3, 10),
|
||||
}
|
||||
# Maps Arduino framework versions to a compatible ESP-IDF version
|
||||
# These versions correspond to pioarduino/esp-idf releases
|
||||
# See: https://github.com/pioarduino/esp-idf/releases
|
||||
ARDUINO_IDF_VERSION_LOOKUP = {
|
||||
cv.Version(3, 3, 5): cv.Version(5, 5, 2),
|
||||
cv.Version(3, 3, 4): cv.Version(5, 5, 1),
|
||||
cv.Version(3, 3, 3): cv.Version(5, 5, 1),
|
||||
cv.Version(3, 3, 2): cv.Version(5, 5, 1),
|
||||
cv.Version(3, 3, 1): cv.Version(5, 5, 1),
|
||||
cv.Version(3, 3, 0): cv.Version(5, 5, 0),
|
||||
cv.Version(3, 2, 1): cv.Version(5, 4, 2),
|
||||
cv.Version(3, 2, 0): cv.Version(5, 4, 2),
|
||||
cv.Version(3, 1, 3): cv.Version(5, 3, 2),
|
||||
cv.Version(3, 1, 2): cv.Version(5, 3, 2),
|
||||
cv.Version(3, 1, 1): cv.Version(5, 3, 1),
|
||||
cv.Version(3, 1, 0): cv.Version(5, 3, 0),
|
||||
}
|
||||
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(5, 5, 1),
|
||||
"latest": cv.Version(5, 5, 1),
|
||||
"dev": cv.Version(5, 5, 1),
|
||||
"recommended": cv.Version(5, 5, 2),
|
||||
"latest": cv.Version(5, 5, 2),
|
||||
"dev": cv.Version(5, 5, 2),
|
||||
}
|
||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
cv.Version(5, 5, 2): cv.Version(55, 3, 35),
|
||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "2"),
|
||||
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||
@@ -399,9 +418,9 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(55, 3, 31, "2"),
|
||||
"latest": cv.Version(55, 3, 31, "2"),
|
||||
"dev": cv.Version(55, 3, 31, "2"),
|
||||
"recommended": cv.Version(55, 3, 35),
|
||||
"latest": cv.Version(55, 3, 35),
|
||||
"dev": cv.Version(55, 3, 35),
|
||||
}
|
||||
|
||||
|
||||
@@ -727,12 +746,14 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
# Remove this class in 2026.7.0
|
||||
class _FrameworkMigrationWarning:
|
||||
shown = False
|
||||
|
||||
|
||||
def _show_framework_migration_message(name: str, variant: str) -> None:
|
||||
"""Show a friendly message about framework migration when defaulting to Arduino."""
|
||||
"""Show a message about the framework default change and how to switch back to Arduino."""
|
||||
# Remove this function in 2026.7.0
|
||||
if _FrameworkMigrationWarning.shown:
|
||||
return
|
||||
_FrameworkMigrationWarning.shown = True
|
||||
@@ -742,41 +763,27 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
|
||||
message = (
|
||||
color(
|
||||
AnsiFore.BOLD_CYAN,
|
||||
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
|
||||
f"💡 NOTICE: {name} does not have a framework specified.",
|
||||
)
|
||||
+ "\n\n"
|
||||
+ f"Currently, {variant} defaults to the Arduino framework.\n"
|
||||
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
|
||||
+ f"Starting with ESPHome 2026.1.0, the default framework for {variant} is ESP-IDF.\n"
|
||||
+ "(We've been warning about this change since ESPHome 2025.8.0)\n"
|
||||
+ "\n"
|
||||
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
|
||||
+ "\n"
|
||||
+ "Why change? ESP-IDF offers:\n"
|
||||
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
|
||||
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
|
||||
+ "Why we made this change:\n"
|
||||
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n")
|
||||
+ color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n")
|
||||
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
|
||||
+ color(
|
||||
AnsiFore.GREEN,
|
||||
" 🔧 Active development and testing by ESPHome developers\n",
|
||||
)
|
||||
+ color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n")
|
||||
+ color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n")
|
||||
+ "\n"
|
||||
+ "Trade-offs:\n"
|
||||
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
|
||||
+ "To continue using Arduino, add this to your YAML under 'esp32:':\n"
|
||||
+ color(AnsiFore.WHITE, " framework:\n")
|
||||
+ color(AnsiFore.WHITE, " type: arduino\n")
|
||||
+ "\n"
|
||||
+ "What should I do?\n"
|
||||
+ color(AnsiFore.CYAN, " Option 1")
|
||||
+ ": Migrate to ESP-IDF (recommended)\n"
|
||||
+ " Add this to your YAML under 'esp32:':\n"
|
||||
+ color(AnsiFore.WHITE, " framework:\n")
|
||||
+ color(AnsiFore.WHITE, " type: esp-idf\n")
|
||||
+ "To silence this message with ESP-IDF, explicitly set:\n"
|
||||
+ color(AnsiFore.WHITE, " framework:\n")
|
||||
+ color(AnsiFore.WHITE, " type: esp-idf\n")
|
||||
+ "\n"
|
||||
+ color(AnsiFore.CYAN, " Option 2")
|
||||
+ ": Keep using Arduino (still supported)\n"
|
||||
+ " Add this to your YAML under 'esp32:':\n"
|
||||
+ color(AnsiFore.WHITE, " framework:\n")
|
||||
+ color(AnsiFore.WHITE, " type: arduino\n")
|
||||
+ "\n"
|
||||
+ "Need help? Check out the migration guide:\n"
|
||||
+ "Migration guide: "
|
||||
+ color(
|
||||
AnsiFore.BLUE,
|
||||
"https://esphome.io/guides/esp32_arduino_to_idf/",
|
||||
@@ -791,13 +798,13 @@ def _set_default_framework(config):
|
||||
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
|
||||
if CONF_TYPE not in config[CONF_FRAMEWORK]:
|
||||
variant = config[CONF_VARIANT]
|
||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
|
||||
# Show migration message for variants that previously defaulted to Arduino
|
||||
# Remove this message in 2026.7.0
|
||||
if variant in ARDUINO_ALLOWED_VARIANTS:
|
||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
|
||||
_show_framework_migration_message(
|
||||
config.get(CONF_NAME, "This device"), variant
|
||||
)
|
||||
else:
|
||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
|
||||
|
||||
return config
|
||||
|
||||
@@ -991,6 +998,13 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
||||
# the ESP-IDF framework
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
|
||||
)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
|
||||
@@ -1488,6 +1488,10 @@ BOARDS = {
|
||||
"name": "Arduino Nano ESP32",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"arduino_nesso_n1": {
|
||||
"name": "Arduino Nesso-N1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"atd147_s3": {
|
||||
"name": "ArtronShop ATD1.47-S3",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
@@ -1656,6 +1660,10 @@ BOARDS = {
|
||||
"name": "Espressif ESP32-C6-DevKitM-1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"esp32-c61-devkitc1-n8r2": {
|
||||
"name": "Espressif ESP32-C61-DevKitC-1 N8R2 (8 MB Flash Quad, 2 MB PSRAM Quad)",
|
||||
"variant": VARIANT_ESP32C61,
|
||||
},
|
||||
"esp32-devkitlipo": {
|
||||
"name": "OLIMEX ESP32-DevKit-LiPo",
|
||||
"variant": VARIANT_ESP32,
|
||||
@@ -1673,11 +1681,15 @@ BOARDS = {
|
||||
"variant": VARIANT_ESP32H2,
|
||||
},
|
||||
"esp32-p4": {
|
||||
"name": "Espressif ESP32-P4 generic",
|
||||
"name": "Espressif ESP32-P4 ES (pre rev.300) generic",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-p4-evboard": {
|
||||
"name": "Espressif ESP32-P4 Function EV Board",
|
||||
"name": "Espressif ESP32-P4 Function EV Board (ES pre rev.300)",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-p4_r3": {
|
||||
"name": "Espressif ESP32-P4 rev.300 generic",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"esp32-pico-devkitm-2": {
|
||||
@@ -2093,7 +2105,7 @@ BOARDS = {
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-tab5-p4": {
|
||||
"name": "M5STACK Tab5 esp32-p4 Board",
|
||||
"name": "M5STACK Tab5 esp32-p4 Board (ES pre rev.300)",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"m5stack-timer-cam": {
|
||||
|
||||
@@ -37,6 +37,9 @@ namespace esphome::esp32_ble_tracker {
|
||||
|
||||
static const char *const TAG = "esp32_ble_tracker";
|
||||
|
||||
// BLE advertisement max: 31 bytes adv data + 31 bytes scan response
|
||||
static constexpr size_t BLE_ADV_MAX_LOG_BYTES = 62;
|
||||
|
||||
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
const char *client_state_to_string(ClientState state) {
|
||||
@@ -445,6 +448,7 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
|
||||
uuid.to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " Service UUID: %s", uuid_buf);
|
||||
}
|
||||
char hex_buf[format_hex_pretty_size(BLE_ADV_MAX_LOG_BYTES)];
|
||||
for (auto &data : this->manufacturer_datas_) {
|
||||
auto ibeacon = ESPBLEiBeacon::from_manufacturer_data(data);
|
||||
if (ibeacon.has_value()) {
|
||||
@@ -458,7 +462,8 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
|
||||
} else {
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
data.uuid.to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf, format_hex_pretty(data.data).c_str());
|
||||
ESP_LOGVV(TAG, " Manufacturer ID: %s, data: %s", uuid_buf,
|
||||
format_hex_pretty_to(hex_buf, data.data.data(), data.data.size()));
|
||||
}
|
||||
}
|
||||
for (auto &data : this->service_datas_) {
|
||||
@@ -466,11 +471,11 @@ void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
|
||||
char uuid_buf[esp32_ble::UUID_STR_LEN];
|
||||
data.uuid.to_str(uuid_buf);
|
||||
ESP_LOGVV(TAG, " UUID: %s", uuid_buf);
|
||||
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
|
||||
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty_to(hex_buf, data.data.data(), data.data.size()));
|
||||
}
|
||||
|
||||
ESP_LOGVV(TAG, " Adv data: %s",
|
||||
format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str());
|
||||
format_hex_pretty_to(hex_buf, scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.const import (
|
||||
CONF_SAFE_MODE,
|
||||
CONF_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
@@ -28,17 +28,7 @@ CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
|
||||
def supports_sha256() -> bool:
|
||||
"""Check if the current platform supports SHA256 for OTA authentication."""
|
||||
return bool(CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny)
|
||||
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
"""Conditionally auto-load sha256 only on platforms that support it."""
|
||||
base_components = ["md5", "socket"]
|
||||
if supports_sha256():
|
||||
return base_components + ["sha256"]
|
||||
return base_components
|
||||
AUTO_LOAD = ["sha256", "socket"]
|
||||
|
||||
|
||||
esphome = cg.esphome_ns.namespace("esphome")
|
||||
@@ -155,11 +145,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config.get(CONF_PASSWORD):
|
||||
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
|
||||
cg.add_define("USE_OTA_PASSWORD")
|
||||
# Only include hash algorithms when password is configured
|
||||
cg.add_define("USE_OTA_MD5")
|
||||
# Only include SHA256 support on platforms that have it
|
||||
if supports_sha256():
|
||||
cg.add_define("USE_OTA_SHA256")
|
||||
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
#include "ota_esphome.h"
|
||||
#ifdef USE_OTA
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
#ifdef USE_OTA_MD5
|
||||
#include "esphome/components/md5/md5.h"
|
||||
#endif
|
||||
#ifdef USE_OTA_SHA256
|
||||
#include "esphome/components/sha256/sha256.h"
|
||||
#endif
|
||||
#endif
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/components/ota/ota_backend_esp8266.h"
|
||||
@@ -31,15 +26,6 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
|
||||
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
#ifdef USE_OTA_MD5
|
||||
static constexpr size_t MD5_HEX_SIZE = 32; // MD5 hash as hex string (16 bytes * 2)
|
||||
#endif
|
||||
#ifdef USE_OTA_SHA256
|
||||
static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
|
||||
#endif
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
void ESPHomeOTAComponent::setup() {
|
||||
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
|
||||
if (this->server_ == nullptr) {
|
||||
@@ -108,15 +94,7 @@ void ESPHomeOTAComponent::loop() {
|
||||
}
|
||||
|
||||
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
#ifdef USE_OTA_SHA256
|
||||
static const uint8_t FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||
#endif
|
||||
|
||||
// Temporary flag to allow MD5 downgrade for ~3 versions (until 2026.1.0)
|
||||
// This allows users to downgrade via OTA if they encounter issues after updating.
|
||||
// Without this, users would need to do a serial flash to downgrade.
|
||||
// TODO: Remove this flag and all associated code in 2026.1.0
|
||||
#define ALLOW_OTA_DOWNGRADE_MD5
|
||||
|
||||
void ESPHomeOTAComponent::handle_handshake_() {
|
||||
/// Handle the OTA handshake and authentication.
|
||||
@@ -547,26 +525,8 @@ void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
|
||||
void ESPHomeOTAComponent::log_auth_warning_(const LogString *msg) { ESP_LOGW(TAG, "Auth: %s", LOG_STR_ARG(msg)); }
|
||||
|
||||
bool ESPHomeOTAComponent::select_auth_type_() {
|
||||
#ifdef USE_OTA_SHA256
|
||||
bool client_supports_sha256 = (this->ota_features_ & FEATURE_SUPPORTS_SHA256_AUTH) != 0;
|
||||
|
||||
#ifdef ALLOW_OTA_DOWNGRADE_MD5
|
||||
// Allow fallback to MD5 if client doesn't support SHA256
|
||||
if (client_supports_sha256) {
|
||||
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_OTA_MD5
|
||||
this->log_auth_warning_(LOG_STR("Using deprecated MD5"));
|
||||
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
|
||||
return true;
|
||||
#else
|
||||
this->log_auth_warning_(LOG_STR("SHA256 required"));
|
||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
|
||||
return false;
|
||||
#endif // USE_OTA_MD5
|
||||
|
||||
#else // !ALLOW_OTA_DOWNGRADE_MD5
|
||||
// Require SHA256
|
||||
if (!client_supports_sha256) {
|
||||
this->log_auth_warning_(LOG_STR("SHA256 required"));
|
||||
@@ -575,20 +535,6 @@ bool ESPHomeOTAComponent::select_auth_type_() {
|
||||
}
|
||||
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_SHA256_AUTH;
|
||||
return true;
|
||||
#endif // ALLOW_OTA_DOWNGRADE_MD5
|
||||
|
||||
#else // !USE_OTA_SHA256
|
||||
#ifdef USE_OTA_MD5
|
||||
// Only MD5 available
|
||||
this->auth_type_ = ota::OTA_RESPONSE_REQUEST_AUTH;
|
||||
return true;
|
||||
#else
|
||||
// No auth methods available
|
||||
this->log_auth_warning_(LOG_STR("No auth methods available"));
|
||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
|
||||
return false;
|
||||
#endif // USE_OTA_MD5
|
||||
#endif // USE_OTA_SHA256
|
||||
}
|
||||
|
||||
bool ESPHomeOTAComponent::handle_auth_send_() {
|
||||
@@ -612,31 +558,12 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
|
||||
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
|
||||
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
|
||||
|
||||
// Declare both hash objects in same stack frame, use pointer to select.
|
||||
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
|
||||
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
|
||||
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
|
||||
#ifdef USE_OTA_SHA256
|
||||
sha256::SHA256 sha_hasher;
|
||||
#endif
|
||||
#ifdef USE_OTA_MD5
|
||||
md5::MD5Digest md5_hasher;
|
||||
#endif
|
||||
HashBase *hasher = nullptr;
|
||||
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
|
||||
// (no passing to other functions). All hash operations must happen in this function.
|
||||
sha256::SHA256 hasher;
|
||||
|
||||
#ifdef USE_OTA_SHA256
|
||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
||||
hasher = &sha_hasher;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_MD5
|
||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
|
||||
hasher = &md5_hasher;
|
||||
}
|
||||
#endif
|
||||
|
||||
const size_t hex_size = hasher->get_size() * 2;
|
||||
const size_t nonce_len = hasher->get_size() / 4;
|
||||
const size_t hex_size = hasher.get_size() * 2;
|
||||
const size_t nonce_len = hasher.get_size() / 4;
|
||||
const size_t auth_buf_size = 1 + 3 * hex_size;
|
||||
this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
|
||||
this->auth_buf_pos_ = 0;
|
||||
@@ -648,22 +575,17 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
hasher->init();
|
||||
hasher->add(buf, nonce_len);
|
||||
hasher->calculate();
|
||||
hasher.init();
|
||||
hasher.add(buf, nonce_len);
|
||||
hasher.calculate();
|
||||
this->auth_buf_[0] = this->auth_type_;
|
||||
hasher->get_hex(buf);
|
||||
hasher.get_hex(buf);
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
|
||||
memcpy(log_buf, buf, hex_size);
|
||||
log_buf[hex_size] = '\0';
|
||||
ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf);
|
||||
}
|
||||
|
||||
// Try to write auth_type + nonce
|
||||
size_t hex_size = this->get_auth_hex_size_();
|
||||
constexpr size_t hex_size = SHA256_HEX_SIZE;
|
||||
const size_t to_write = 1 + hex_size;
|
||||
size_t remaining = to_write - this->auth_buf_pos_;
|
||||
|
||||
@@ -685,7 +607,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
|
||||
}
|
||||
|
||||
bool ESPHomeOTAComponent::handle_auth_read_() {
|
||||
size_t hex_size = this->get_auth_hex_size_();
|
||||
constexpr size_t hex_size = SHA256_HEX_SIZE;
|
||||
const size_t to_read = hex_size * 2; // CNonce + Response
|
||||
|
||||
// Try to read remaining bytes (CNonce + Response)
|
||||
@@ -710,55 +632,25 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
|
||||
const char *cnonce = nonce + hex_size;
|
||||
const char *response = cnonce + hex_size;
|
||||
|
||||
// CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions).
|
||||
// Declare both hash objects in same stack frame, use pointer to select.
|
||||
// NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
|
||||
// hardware SHA acceleration - the object must exist in this stack frame for all operations.
|
||||
// Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
|
||||
#ifdef USE_OTA_SHA256
|
||||
sha256::SHA256 sha_hasher;
|
||||
#endif
|
||||
#ifdef USE_OTA_MD5
|
||||
md5::MD5Digest md5_hasher;
|
||||
#endif
|
||||
HashBase *hasher = nullptr;
|
||||
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
|
||||
// (no passing to other functions). All hash operations must happen in this function.
|
||||
sha256::SHA256 hasher;
|
||||
|
||||
#ifdef USE_OTA_SHA256
|
||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
||||
hasher = &sha_hasher;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_MD5
|
||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
|
||||
hasher = &md5_hasher;
|
||||
}
|
||||
#endif
|
||||
|
||||
hasher->init();
|
||||
hasher->add(this->password_.c_str(), this->password_.length());
|
||||
hasher->add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
|
||||
hasher->calculate();
|
||||
hasher.init();
|
||||
hasher.add(this->password_.c_str(), this->password_.length());
|
||||
hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
|
||||
hasher.calculate();
|
||||
|
||||
ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char log_buf[65]; // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
|
||||
// Log CNonce
|
||||
memcpy(log_buf, cnonce, hex_size);
|
||||
log_buf[hex_size] = '\0';
|
||||
ESP_LOGV(TAG, "Auth: CNonce is %s", log_buf);
|
||||
|
||||
// Log computed hash
|
||||
hasher->get_hex(log_buf);
|
||||
log_buf[hex_size] = '\0';
|
||||
ESP_LOGV(TAG, "Auth: Result is %s", log_buf);
|
||||
|
||||
// Log received response
|
||||
memcpy(log_buf, response, hex_size);
|
||||
log_buf[hex_size] = '\0';
|
||||
ESP_LOGV(TAG, "Auth: Response is %s", log_buf);
|
||||
char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator)
|
||||
hasher.get_hex(computed_hash);
|
||||
ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash);
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response);
|
||||
|
||||
// Compare response
|
||||
bool matches = hasher->equals_hex(response);
|
||||
bool matches = hasher.equals_hex(response);
|
||||
|
||||
if (!matches) {
|
||||
this->log_auth_warning_(LOG_STR("Password mismatch"));
|
||||
@@ -772,21 +664,6 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
|
||||
#ifdef USE_OTA_SHA256
|
||||
if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
|
||||
return SHA256_HEX_SIZE;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_MD5
|
||||
return MD5_HEX_SIZE;
|
||||
#else
|
||||
#ifndef USE_OTA_SHA256
|
||||
#error "Either USE_OTA_MD5 or USE_OTA_SHA256 must be defined when USE_OTA_PASSWORD is enabled"
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESPHomeOTAComponent::cleanup_auth_() {
|
||||
this->auth_buf_ = nullptr;
|
||||
this->auth_buf_pos_ = 0;
|
||||
|
||||
@@ -44,10 +44,10 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
||||
void handle_handshake_();
|
||||
void handle_data_();
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
|
||||
bool handle_auth_send_();
|
||||
bool handle_auth_read_();
|
||||
bool select_auth_type_();
|
||||
size_t get_auth_hex_size_() const;
|
||||
void cleanup_auth_();
|
||||
void log_auth_warning_(const LogString *msg);
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_event.h>
|
||||
@@ -299,9 +300,10 @@ void ESPNowComponent::loop() {
|
||||
// Intentionally left as if instead of else in case the peer is added above
|
||||
if (esp_now_is_peer_exist(info.src_addr)) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(ESP_NOW_MAX_DATA_LEN)];
|
||||
ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(),
|
||||
format_mac_address_pretty(info.des_addr).c_str(),
|
||||
format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str());
|
||||
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
|
||||
#endif
|
||||
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
for (auto *handler : this->broadcasted_handlers_) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "ethernet_component.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
|
||||
@@ -39,6 +40,9 @@ namespace ethernet {
|
||||
|
||||
static const char *const TAG = "ethernet";
|
||||
|
||||
// PHY register size for hex logging
|
||||
static constexpr size_t PHY_REG_SIZE = 2;
|
||||
|
||||
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
|
||||
@@ -773,7 +777,10 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
uint32_t phy_control_2;
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
@@ -790,7 +797,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty((u_int8_t *) &phy_control_2, 2).c_str());
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet_info {
|
||||
namespace esphome::ethernet_info {
|
||||
|
||||
static const char *const TAG = "ethernet_info";
|
||||
|
||||
@@ -12,7 +11,6 @@ void IPAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo IP
|
||||
void DNSAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo DNS Address", this); }
|
||||
void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo MAC Address", this); }
|
||||
|
||||
} // namespace ethernet_info
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ethernet_info
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ethernet_info {
|
||||
namespace esphome::ethernet_info {
|
||||
|
||||
class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
|
||||
public:
|
||||
@@ -40,21 +39,27 @@ class IPAddressEthernetInfo : public PollingComponent, public text_sensor::TextS
|
||||
class DNSAddressEthernetInfo : public PollingComponent, public text_sensor::TextSensor {
|
||||
public:
|
||||
void update() override {
|
||||
auto dns_one = ethernet::global_eth_component->get_dns_address(0);
|
||||
auto dns_two = ethernet::global_eth_component->get_dns_address(1);
|
||||
auto dns1 = ethernet::global_eth_component->get_dns_address(0);
|
||||
auto dns2 = ethernet::global_eth_component->get_dns_address(1);
|
||||
|
||||
std::string dns_results = dns_one.str() + " " + dns_two.str();
|
||||
|
||||
if (dns_results != this->last_results_) {
|
||||
this->last_results_ = dns_results;
|
||||
this->publish_state(dns_results);
|
||||
if (dns1 != this->last_dns1_ || dns2 != this->last_dns2_) {
|
||||
this->last_dns1_ = dns1;
|
||||
this->last_dns2_ = dns2;
|
||||
// IP_ADDRESS_BUFFER_SIZE (40) = max IP (39) + null; space reuses first null's slot
|
||||
char buf[network::IP_ADDRESS_BUFFER_SIZE * 2];
|
||||
dns1.str_to(buf);
|
||||
size_t len1 = strlen(buf);
|
||||
buf[len1] = ' ';
|
||||
dns2.str_to(buf + len1 + 1);
|
||||
this->publish_state(buf);
|
||||
}
|
||||
}
|
||||
float get_setup_priority() const override { return setup_priority::ETHERNET; }
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
std::string last_results_;
|
||||
network::IPAddress last_dns1_;
|
||||
network::IPAddress last_dns2_;
|
||||
};
|
||||
|
||||
class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor {
|
||||
@@ -64,7 +69,6 @@ class MACAddressEthernetInfo : public Component, public text_sensor::TextSensor
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace ethernet_info
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ethernet_info
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace esphome::hlk_fm22x {
|
||||
|
||||
static const char *const TAG = "hlk_fm22x";
|
||||
|
||||
// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name)
|
||||
static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36;
|
||||
|
||||
void HlkFm22xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X...");
|
||||
this->set_enrolling_(false);
|
||||
@@ -142,7 +145,10 @@ void HlkFm22xComponent::recv_command_() {
|
||||
data.push_back(byte);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty(data).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)];
|
||||
ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
#endif
|
||||
|
||||
byte = this->read();
|
||||
if (byte != checksum) {
|
||||
|
||||
@@ -30,7 +30,7 @@ class HmacMD5 {
|
||||
void get_bytes(uint8_t *output);
|
||||
|
||||
/// Retrieve the HMAC-MD5 digest as hex characters.
|
||||
/// The output must be able to hold 32 bytes or more.
|
||||
/// The output must be able to hold 33 bytes or more (32 hex chars + null terminator).
|
||||
void get_hex(char *output);
|
||||
|
||||
/// Compare the digest against a provided byte-encoded digest (16 bytes).
|
||||
|
||||
@@ -35,7 +35,7 @@ class HmacSHA256 {
|
||||
void get_bytes(uint8_t *output);
|
||||
|
||||
/// Retrieve the HMAC-SHA256 digest as hex characters.
|
||||
/// The output must be able to hold 64 bytes or more.
|
||||
/// The output must be able to hold 65 bytes or more (64 hex chars + null terminator).
|
||||
void get_hex(char *output);
|
||||
|
||||
/// Compare the digest against a provided byte-encoded digest (32 bytes).
|
||||
|
||||
@@ -111,6 +111,9 @@ void HOT HUB75Display::draw_pixel_at(int x, int y, Color color) {
|
||||
if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) [[unlikely]]
|
||||
return;
|
||||
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
|
||||
driver_->set_pixel(x, y, color.r, color.g, color.b);
|
||||
App.feed_wdt();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace i2c {
|
||||
|
||||
static const char *const TAG = "i2c.arduino";
|
||||
|
||||
// Maximum bytes to log in hex format (truncates larger transfers)
|
||||
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
|
||||
|
||||
void ArduinoI2CBus::setup() {
|
||||
recover_();
|
||||
|
||||
@@ -107,7 +110,10 @@ ErrorCode ArduinoI2CBus::write_readv(uint8_t address, const uint8_t *write_buffe
|
||||
return ERROR_NOT_INITIALIZED;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)];
|
||||
ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count));
|
||||
#endif
|
||||
|
||||
uint8_t status = 0;
|
||||
if (write_count != 0 || read_count == 0) {
|
||||
|
||||
@@ -15,6 +15,9 @@ namespace i2c {
|
||||
|
||||
static const char *const TAG = "i2c.idf";
|
||||
|
||||
// Maximum bytes to log in hex format (truncates larger transfers)
|
||||
static constexpr size_t I2C_MAX_LOG_BYTES = 32;
|
||||
|
||||
void IDFI2CBus::setup() {
|
||||
static i2c_port_t next_hp_port = I2C_NUM_0;
|
||||
#if SOC_LP_I2C_SUPPORTED
|
||||
@@ -147,7 +150,10 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
|
||||
jobs[num_jobs++].write.total_bytes = 1;
|
||||
} else {
|
||||
if (write_count != 0) {
|
||||
ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty(write_buffer, write_count).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(I2C_MAX_LOG_BYTES)];
|
||||
ESP_LOGV(TAG, "0x%02X TX %s", address, format_hex_pretty_to(hex_buf, write_buffer, write_count));
|
||||
#endif
|
||||
jobs[num_jobs++].command = I2C_MASTER_CMD_START;
|
||||
jobs[num_jobs].command = I2C_MASTER_CMD_WRITE;
|
||||
jobs[num_jobs].write.ack_check = true;
|
||||
|
||||
@@ -131,6 +131,13 @@ float ILI9XXXDisplay::get_setup_priority() const { return setup_priority::HARDWA
|
||||
void ILI9XXXDisplay::fill(Color color) {
|
||||
if (!this->check_buffer_())
|
||||
return;
|
||||
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t new_color = 0;
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = 0;
|
||||
|
||||
@@ -293,6 +293,13 @@ void Inkplate::fill(Color color) {
|
||||
ESP_LOGV(TAG, "Fill called");
|
||||
uint32_t start_time = millis();
|
||||
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->greyscale_) {
|
||||
uint8_t fill = ((color.red * 2126 / 10000) + (color.green * 7152 / 10000) + (color.blue * 722 / 10000)) >> 5;
|
||||
memset(this->buffer_, (fill << 4) | fill, this->get_buffer_length_());
|
||||
|
||||
@@ -8,8 +8,9 @@ extern "C" {
|
||||
uint8_t temprature_sens_read();
|
||||
}
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "driver/temperature_sensor.h"
|
||||
#endif // USE_ESP32_VARIANT
|
||||
#endif // USE_ESP32
|
||||
@@ -27,9 +28,9 @@ namespace internal_temperature {
|
||||
|
||||
static const char *const TAG = "internal_temperature";
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
static temperature_sensor_handle_t tsensNew = NULL;
|
||||
#endif // USE_ESP32_VARIANT
|
||||
#endif // USE_ESP32
|
||||
@@ -44,8 +45,9 @@ void InternalTemperatureSensor::update() {
|
||||
temperature = (raw - 32) / 1.8f;
|
||||
success = (raw != 128);
|
||||
#elif defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
esp_err_t result = temperature_sensor_get_celsius(tsensNew, &temperature);
|
||||
success = (result == ESP_OK);
|
||||
if (!success) {
|
||||
@@ -81,9 +83,9 @@ void InternalTemperatureSensor::update() {
|
||||
|
||||
void InternalTemperatureSensor::setup() {
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32C2) || defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32H2) || \
|
||||
defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
temperature_sensor_config_t tsens_config = TEMPERATURE_SENSOR_CONFIG_DEFAULT(-10, 80);
|
||||
|
||||
esp_err_t result = temperature_sensor_install(&tsens_config, &tsensNew);
|
||||
|
||||
@@ -36,7 +36,7 @@ static const char *get_color_mode_json_str(ColorMode mode) {
|
||||
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
if (state.supports_effects()) {
|
||||
root[ESPHOME_F("effect")] = state.get_effect_name();
|
||||
root[ESPHOME_F("effect")] = state.get_effect_name_ref();
|
||||
root[ESPHOME_F("effect_index")] = state.get_current_effect_index();
|
||||
root[ESPHOME_F("effect_count")] = state.get_effect_count();
|
||||
}
|
||||
|
||||
@@ -85,11 +85,11 @@ class ArcType(NumberType):
|
||||
lv.arc_set_range(w.obj, min_value, max_value)
|
||||
|
||||
await w.set_property(
|
||||
CONF_START_ANGLE,
|
||||
"bg_start_angle",
|
||||
await lv_angle_degrees.process(config.get(CONF_START_ANGLE)),
|
||||
)
|
||||
await w.set_property(
|
||||
CONF_END_ANGLE, await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
|
||||
"bg_end_angle", await lv_angle_degrees.process(config.get(CONF_END_ANGLE))
|
||||
)
|
||||
await w.set_property(
|
||||
CONF_ROTATION, await lv_angle_degrees.process(config.get(CONF_ROTATION))
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32P4
|
||||
#include <utility>
|
||||
#include "mipi_dsi.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_dsi {
|
||||
|
||||
// Maximum bytes to log for init commands (truncated if larger)
|
||||
static constexpr size_t MIPI_DSI_MAX_CMD_LOG_BYTES = 64;
|
||||
|
||||
static bool notify_refresh_ready(esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) {
|
||||
auto *sem = static_cast<SemaphoreHandle_t *>(user_ctx);
|
||||
BaseType_t need_yield = pdFALSE;
|
||||
@@ -121,8 +125,11 @@ void MIPI_DSI::setup() {
|
||||
}
|
||||
}
|
||||
const auto *ptr = vec.data() + index;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_DSI_MAX_CMD_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Command %02X, length %d, byte(s) %s", cmd, num_args,
|
||||
format_hex_pretty(ptr, num_args, '.', false).c_str());
|
||||
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
|
||||
err = esp_lcd_panel_io_tx_param(this->io_handle_, cmd, ptr, num_args);
|
||||
if (err != ESP_OK) {
|
||||
this->smark_failed(LOG_STR("lcd_panel_io_tx_param failed"), err);
|
||||
@@ -293,6 +300,13 @@ void MIPI_DSI::draw_pixel_at(int x, int y, Color color) {
|
||||
void MIPI_DSI::fill(Color color) {
|
||||
if (!this->check_buffer_())
|
||||
return;
|
||||
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (this->color_depth_) {
|
||||
case display::COLOR_BITNESS_565: {
|
||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#ifdef USE_ESP32_VARIANT_ESP32S3
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esp_lcd_panel_rgb.h"
|
||||
@@ -8,6 +9,9 @@ namespace esphome {
|
||||
namespace mipi_rgb {
|
||||
|
||||
static const uint8_t DELAY_FLAG = 0xFF;
|
||||
|
||||
// Maximum bytes to log for init commands (truncated if larger)
|
||||
static constexpr size_t MIPI_RGB_MAX_CMD_LOG_BYTES = 64;
|
||||
static constexpr uint8_t MADCTL_MY = 0x80; // Bit 7 Bottom to top
|
||||
static constexpr uint8_t MADCTL_MX = 0x40; // Bit 6 Right to left
|
||||
static constexpr uint8_t MADCTL_MV = 0x20; // Bit 5 Swap axes
|
||||
@@ -91,8 +95,9 @@ void MipiRgbSpi::write_init_sequence_() {
|
||||
delay(120); // NOLINT
|
||||
}
|
||||
const auto *ptr = vec.data() + index;
|
||||
char hex_buf[format_hex_pretty_size(MIPI_RGB_MAX_CMD_LOG_BYTES)];
|
||||
ESP_LOGD(TAG, "Write command %02X, length %d, byte(s) %s", cmd, num_args,
|
||||
format_hex_pretty(ptr, num_args, '.', false).c_str());
|
||||
format_hex_pretty_to(hex_buf, ptr, num_args, '.'));
|
||||
index += num_args;
|
||||
this->write_command_(cmd);
|
||||
while (num_args-- != 0)
|
||||
@@ -300,6 +305,13 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) {
|
||||
void MipiRgb::fill(Color color) {
|
||||
if (!this->check_buffer_())
|
||||
return;
|
||||
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
auto *ptr_16 = reinterpret_cast<uint16_t *>(this->buffer_);
|
||||
uint8_t hi_byte = static_cast<uint8_t>(color.r & 0xF8) | (color.g >> 5);
|
||||
uint8_t lo_byte = static_cast<uint8_t>((color.g & 0x1C) << 3) | (color.b >> 3);
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esphome/components/display/display_color_utils.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace mipi_spi {
|
||||
|
||||
constexpr static const char *const TAG = "display.mipi_spi";
|
||||
|
||||
// Maximum bytes to log for commands (truncated if larger)
|
||||
static constexpr size_t MIPI_SPI_MAX_CMD_LOG_BYTES = 64;
|
||||
static constexpr uint8_t SW_RESET_CMD = 0x01;
|
||||
static constexpr uint8_t SLEEP_OUT = 0x11;
|
||||
static constexpr uint8_t NORON = 0x13;
|
||||
@@ -241,7 +245,10 @@ class MipiSpi : public display::Display,
|
||||
|
||||
// Writes a command to the display, with the given bytes.
|
||||
void write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MIPI_SPI_MAX_CMD_LOG_BYTES)];
|
||||
esph_log_v(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty_to(hex_buf, bytes, len));
|
||||
#endif
|
||||
if constexpr (BUS_TYPE == BUS_TYPE_QUAD) {
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||
@@ -562,6 +569,12 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
|
||||
// Fills the display with a color.
|
||||
void fill(Color color) override {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
display::Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
this->x_low_ = 0;
|
||||
this->y_low_ = this->start_line_;
|
||||
this->x_high_ = WIDTH - 1;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "mitsubishi.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -6,6 +7,9 @@ namespace mitsubishi {
|
||||
|
||||
static const char *const TAG = "mitsubishi.climate";
|
||||
|
||||
// IR frame size for Mitsubishi climate
|
||||
static constexpr size_t MITSUBISHI_FRAME_SIZE = 18;
|
||||
|
||||
const uint8_t MITSUBISHI_OFF = 0x00;
|
||||
|
||||
const uint8_t MITSUBISHI_MODE_AUTO = 0x20;
|
||||
@@ -388,7 +392,10 @@ bool MitsubishiClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
break;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty(state_frame, 18).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MITSUBISHI_FRAME_SIZE)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Receiving: %s", format_hex_pretty_to(hex_buf, state_frame, MITSUBISHI_FRAME_SIZE));
|
||||
|
||||
this->publish_state();
|
||||
return true;
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace modbus {
|
||||
|
||||
static const char *const TAG = "modbus";
|
||||
|
||||
// Maximum bytes to log for Modbus frames (truncated if larger)
|
||||
static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
|
||||
|
||||
void Modbus::setup() {
|
||||
if (this->flow_control_pin_ != nullptr) {
|
||||
this->flow_control_pin_->setup();
|
||||
@@ -255,7 +258,10 @@ void Modbus::send(uint8_t address, uint8_t function_code, uint16_t start_address
|
||||
this->flow_control_pin_->digital_write(false);
|
||||
waiting_for_response = address;
|
||||
last_send_ = millis();
|
||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty(data).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Modbus write: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
}
|
||||
|
||||
// Helper function for lambdas
|
||||
@@ -276,7 +282,10 @@ void Modbus::send_raw(const std::vector<uint8_t> &payload) {
|
||||
if (this->flow_control_pin_ != nullptr)
|
||||
this->flow_control_pin_->digital_write(false);
|
||||
waiting_for_response = payload[0];
|
||||
ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty(payload).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Modbus write raw: %s", format_hex_pretty_to(hex_buf, payload.data(), payload.size()));
|
||||
last_send_ = millis();
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,9 @@ static const uint16_t SERVICE_UUID = 0xADA0;
|
||||
static const uint8_t MANUFACTURER_DATA_LENGTH = 23;
|
||||
static const uint16_t MANUFACTURER_ID = 0x000D;
|
||||
|
||||
// Maximum bytes to log in very verbose hex output
|
||||
static constexpr size_t MOPEKA_MAX_LOG_BYTES = 32;
|
||||
|
||||
void MopekaStdCheck::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Mopeka Std Check");
|
||||
ESP_LOGCONFIG(TAG, " Propane Butane mix: %.0f%%", this->propane_butane_mix_ * 100);
|
||||
@@ -60,7 +63,11 @@ bool MopekaStdCheck::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
|
||||
const auto &manu_data = manu_datas[0];
|
||||
|
||||
ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(), format_hex_pretty(manu_data.data).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MOPEKA_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "[%s] Manufacturer data: %s", device.address_str().c_str(),
|
||||
format_hex_pretty_to(hex_buf, manu_data.data.data(), manu_data.data.size()));
|
||||
|
||||
if (manu_data.data.size() != MANUFACTURER_DATA_LENGTH) {
|
||||
ESP_LOGE(TAG, "[%s] Unexpected manu_data size (%d)", device.address_str().c_str(), manu_data.data.size());
|
||||
|
||||
@@ -117,6 +117,12 @@ void PCD8544::update() {
|
||||
}
|
||||
|
||||
void PCD8544::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fill = color.is_on() ? 0xFF : 0x00;
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->buffer_[i] = fill;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "pn532_spi.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Based on:
|
||||
@@ -11,6 +12,9 @@ namespace pn532_spi {
|
||||
|
||||
static const char *const TAG = "pn532_spi";
|
||||
|
||||
// Maximum bytes to log in verbose hex output
|
||||
static constexpr size_t PN532_MAX_LOG_BYTES = 64;
|
||||
|
||||
void PN532Spi::setup() {
|
||||
this->spi_setup();
|
||||
|
||||
@@ -32,7 +36,10 @@ bool PN532Spi::write_data(const std::vector<uint8_t> &data) {
|
||||
delay(2);
|
||||
// First byte, communication mode: Write data
|
||||
this->write_byte(0x01);
|
||||
ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty(data).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Writing data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
|
||||
this->write_array(data.data(), data.size());
|
||||
this->disable();
|
||||
|
||||
@@ -55,7 +62,10 @@ bool PN532Spi::read_data(std::vector<uint8_t> &data, uint8_t len) {
|
||||
this->read_array(data.data(), len);
|
||||
this->disable();
|
||||
data.insert(data.begin(), 0x01);
|
||||
ESP_LOGV(TAG, "Read data: %s", format_hex_pretty(data).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Read data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -73,7 +83,10 @@ bool PN532Spi::read_response(uint8_t command, std::vector<uint8_t> &data) {
|
||||
std::vector<uint8_t> header(7);
|
||||
this->read_array(header.data(), 7);
|
||||
|
||||
ESP_LOGV(TAG, "Header data: %s", format_hex_pretty(header).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PN532_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Header data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), header.data(), header.size()));
|
||||
|
||||
if (header[0] != 0x00 && header[1] != 0x00 && header[2] != 0xFF) {
|
||||
// invalid packet
|
||||
@@ -103,7 +116,7 @@ bool PN532Spi::read_response(uint8_t command, std::vector<uint8_t> &data) {
|
||||
this->read_array(data.data(), len + 1);
|
||||
this->disable();
|
||||
|
||||
ESP_LOGV(TAG, "Response data: %s", format_hex_pretty(data).c_str());
|
||||
ESP_LOGV(TAG, "Response data: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data.data(), data.size()));
|
||||
|
||||
uint8_t checksum = header[5] + header[6]; // TFI + Command response code
|
||||
for (int i = 0; i < len - 1; i++) {
|
||||
|
||||
@@ -60,13 +60,13 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
|
||||
* Valid values are from -99.5 to 1999.5. Smaller values are displayed as Lo, higher as Hi.
|
||||
* It will printed as it fits in the screen.
|
||||
*/
|
||||
void print_bignum(float bignum) { this->bignum_ = bignum * 10; }
|
||||
void print_bignum(float bignum) { this->bignum_ = static_cast<int16_t>(bignum * 10); }
|
||||
/**
|
||||
* Print the small number
|
||||
*
|
||||
* Valid values are from -9 to 99. Smaller values are displayed as Lo, higher as Hi.
|
||||
*/
|
||||
void print_smallnum(float smallnum) { this->smallnum_ = smallnum; }
|
||||
void print_smallnum(float smallnum) { this->smallnum_ = static_cast<int16_t>(smallnum); }
|
||||
/**
|
||||
* Print a happy face
|
||||
*
|
||||
@@ -107,8 +107,8 @@ class PVVXDisplay : public ble_client::BLEClientNode, public PollingComponent {
|
||||
bool auto_clear_enabled_{true};
|
||||
uint32_t disconnect_delay_ms_ = 5000;
|
||||
uint16_t validity_period_ = 300;
|
||||
uint16_t bignum_ = 0;
|
||||
uint16_t smallnum_ = 0;
|
||||
int16_t bignum_ = 0;
|
||||
int16_t smallnum_ = 0;
|
||||
uint8_t cfg_ = 0;
|
||||
|
||||
void setcfgbit_(uint8_t bit, bool value);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#if defined(USE_ESP32) && defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "qspi_dbi.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace qspi_dbi {
|
||||
|
||||
// Maximum bytes to log in verbose hex output
|
||||
static constexpr size_t QSPI_DBI_MAX_LOG_BYTES = 64;
|
||||
|
||||
void QspiDbi::setup() {
|
||||
this->spi_setup();
|
||||
if (this->enable_pin_ != nullptr) {
|
||||
@@ -174,7 +178,11 @@ void QspiDbi::write_to_display_(int x_start, int y_start, int w, int h, const ui
|
||||
this->disable();
|
||||
}
|
||||
void QspiDbi::write_command_(uint8_t cmd, const uint8_t *bytes, size_t len) {
|
||||
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len, format_hex_pretty(bytes, len).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(QSPI_DBI_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Command %02X, length %d, bytes %s", cmd, len,
|
||||
format_hex_pretty_to(hex_buf, sizeof(hex_buf), bytes, len));
|
||||
this->enable();
|
||||
this->write_cmd_addr_data(8, 0x02, 24, cmd << 8, bytes, len);
|
||||
this->disable();
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace seeed_mr60bha2 {
|
||||
|
||||
static const char *const TAG = "seeed_mr60bha2";
|
||||
|
||||
// Maximum bytes to log in verbose hex output
|
||||
static constexpr size_t MR60BHA2_MAX_LOG_BYTES = 64;
|
||||
|
||||
// Prints the component's configuration data. dump_config() prints all of the component's configuration
|
||||
// items in an easy-to-read format, including the configuration key-value pairs.
|
||||
void MR60BHA2Component::dump_config() {
|
||||
@@ -110,7 +113,10 @@ bool MR60BHA2Component::validate_message_() {
|
||||
if (at == 7) {
|
||||
if (!validate_checksum(data, 7, header_checksum)) {
|
||||
ESP_LOGE(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", header_checksum);
|
||||
ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, 8));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -125,14 +131,22 @@ bool MR60BHA2Component::validate_message_() {
|
||||
if (at == 8 + length) {
|
||||
if (!validate_checksum(data + 8, length, data_checksum)) {
|
||||
ESP_LOGE(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", data_checksum);
|
||||
ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty(data, 8 + length).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "GET FRAME: %s", format_hex_pretty_to(hex_buf, sizeof(hex_buf), data, 8 + length));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const uint8_t *frame_data = data + 8;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf1[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)];
|
||||
char hex_buf2[format_hex_pretty_size(MR60BHA2_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Received Frame: ID: 0x%04x, Type: 0x%04x, Data: [%s] Raw Data: [%s]", frame_id, frame_type,
|
||||
format_hex_pretty(frame_data, length).c_str(), format_hex_pretty(this->rx_message_).c_str());
|
||||
format_hex_pretty_to(hex_buf1, sizeof(hex_buf1), frame_data, length),
|
||||
format_hex_pretty_to(hex_buf2, sizeof(hex_buf2), this->rx_message_.data(), this->rx_message_.size()));
|
||||
this->process_frame_(frame_id, frame_type, data + 8, length);
|
||||
|
||||
// Return false to reset rx buffer
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace seeed_mr60fda2 {
|
||||
|
||||
static const char *const TAG = "seeed_mr60fda2";
|
||||
|
||||
// Maximum bytes to log in verbose hex output
|
||||
static constexpr size_t MR60FDA2_MAX_LOG_BYTES = 64;
|
||||
|
||||
// Prints the component's configuration data. dump_config() prints all of the component's configuration
|
||||
// items in an easy-to-read format, including the configuration key-value pairs.
|
||||
void MR60FDA2Component::dump_config() {
|
||||
@@ -202,9 +205,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) {
|
||||
this->current_frame_locate_++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char frame_buf[format_hex_pretty_size(MR60FDA2_MAX_LOG_BYTES)];
|
||||
char byte_buf[format_hex_pretty_size(1)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "CURRENT_FRAME: %s %s",
|
||||
format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(),
|
||||
format_hex_pretty(&buffer, 1).c_str());
|
||||
format_hex_pretty_to(frame_buf, this->current_frame_buf_, this->current_frame_len_),
|
||||
format_hex_pretty_to(byte_buf, &buffer, 1));
|
||||
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
|
||||
}
|
||||
break;
|
||||
@@ -228,9 +235,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) {
|
||||
this->process_frame_();
|
||||
} else {
|
||||
ESP_LOGD(TAG, "DATA_CKSUM_FRAME ERROR: 0x%02x", buffer);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char frame_buf[format_hex_pretty_size(MR60FDA2_MAX_LOG_BYTES)];
|
||||
char byte_buf[format_hex_pretty_size(1)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "GET CURRENT_FRAME: %s %s",
|
||||
format_hex_pretty(this->current_frame_buf_, this->current_frame_len_).c_str(),
|
||||
format_hex_pretty(&buffer, 1).c_str());
|
||||
format_hex_pretty_to(frame_buf, this->current_frame_buf_, this->current_frame_len_),
|
||||
format_hex_pretty_to(byte_buf, &buffer, 1));
|
||||
|
||||
this->current_frame_locate_ = LOCATE_FRAME_HEADER;
|
||||
}
|
||||
@@ -328,7 +339,10 @@ void MR60FDA2Component::set_install_height(uint8_t index) {
|
||||
float_to_bytes(INSTALL_HEIGHT[index], &send_data[8]);
|
||||
send_data[12] = calculate_checksum(send_data + 8, 4);
|
||||
this->write_array(send_data, 13);
|
||||
ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty(send_data, 13).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(13)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "SEND INSTALL HEIGHT FRAME: %s", format_hex_pretty_to(hex_buf, send_data, 13));
|
||||
}
|
||||
|
||||
void MR60FDA2Component::set_height_threshold(uint8_t index) {
|
||||
@@ -336,7 +350,10 @@ void MR60FDA2Component::set_height_threshold(uint8_t index) {
|
||||
float_to_bytes(HEIGHT_THRESHOLD[index], &send_data[8]);
|
||||
send_data[12] = calculate_checksum(send_data + 8, 4);
|
||||
this->write_array(send_data, 13);
|
||||
ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty(send_data, 13).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(13)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "SEND HEIGHT THRESHOLD: %s", format_hex_pretty_to(hex_buf, send_data, 13));
|
||||
}
|
||||
|
||||
void MR60FDA2Component::set_sensitivity(uint8_t index) {
|
||||
@@ -346,19 +363,28 @@ void MR60FDA2Component::set_sensitivity(uint8_t index) {
|
||||
|
||||
send_data[12] = calculate_checksum(send_data + 8, 4);
|
||||
this->write_array(send_data, 13);
|
||||
ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty(send_data, 13).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(13)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "SEND SET SENSITIVITY: %s", format_hex_pretty_to(hex_buf, send_data, 13));
|
||||
}
|
||||
|
||||
void MR60FDA2Component::get_radar_parameters() {
|
||||
uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x06, 0xF6};
|
||||
this->write_array(send_data, 8);
|
||||
ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty(send_data, 8).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(8)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "SEND GET PARAMETERS: %s", format_hex_pretty_to(hex_buf, send_data, 8));
|
||||
}
|
||||
|
||||
void MR60FDA2Component::factory_reset() {
|
||||
uint8_t send_data[8] = {0x01, 0x00, 0x00, 0x00, 0x00, 0x21, 0x10, 0xCF};
|
||||
this->write_array(send_data, 8);
|
||||
ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty(send_data, 8).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(8)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "SEND RESET: %s", format_hex_pretty_to(hex_buf, send_data, 8));
|
||||
this->get_radar_parameters();
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ void SN74HC595GPIOComponent::write_gpio() {
|
||||
void SN74HC595SPIComponent::write_gpio() {
|
||||
for (uint8_t &output_byte : std::ranges::reverse_view(this->output_bytes_)) {
|
||||
this->enable();
|
||||
this->transfer_byte(output_byte);
|
||||
this->write_byte(output_byte);
|
||||
this->disable();
|
||||
}
|
||||
SN74HC595Component::write_gpio();
|
||||
|
||||
@@ -49,21 +49,60 @@ SPIDevice = spi_ns.class_("SPIDevice")
|
||||
SPIDataRate = spi_ns.enum("SPIDataRate")
|
||||
SPIMode = spi_ns.enum("SPIMode")
|
||||
|
||||
SPI_DATA_RATE_OPTIONS = {
|
||||
80e6: SPIDataRate.DATA_RATE_80MHZ,
|
||||
40e6: SPIDataRate.DATA_RATE_40MHZ,
|
||||
20e6: SPIDataRate.DATA_RATE_20MHZ,
|
||||
10e6: SPIDataRate.DATA_RATE_10MHZ,
|
||||
8e6: SPIDataRate.DATA_RATE_8MHZ,
|
||||
5e6: SPIDataRate.DATA_RATE_5MHZ,
|
||||
4e6: SPIDataRate.DATA_RATE_4MHZ,
|
||||
2e6: SPIDataRate.DATA_RATE_2MHZ,
|
||||
1e6: SPIDataRate.DATA_RATE_1MHZ,
|
||||
2e5: SPIDataRate.DATA_RATE_200KHZ,
|
||||
75e3: SPIDataRate.DATA_RATE_75KHZ,
|
||||
1e3: SPIDataRate.DATA_RATE_1KHZ,
|
||||
PLATFORM_SPI_CLOCKS = {
|
||||
PLATFORM_ESP8266: 40e6,
|
||||
PLATFORM_ESP32: 80e6,
|
||||
PLATFORM_RP2040: 62.5e6,
|
||||
}
|
||||
SPI_DATA_RATE_SCHEMA = cv.All(cv.frequency, cv.enum(SPI_DATA_RATE_OPTIONS))
|
||||
|
||||
MAX_DATA_RATE_ERROR = 0.05 # Max allowable actual data rate difference from requested
|
||||
|
||||
|
||||
def _render_hz(value: float) -> str:
|
||||
"""Render a frequency in Hz as a human-readable string using Hz, KHz or MHz.
|
||||
|
||||
Examples:
|
||||
500 -> "500 Hz"
|
||||
1500 -> "1.5 kHz"
|
||||
2000000 -> "2 MHz"
|
||||
"""
|
||||
if value >= 1e6:
|
||||
unit = "MHz"
|
||||
num = value / 1e6
|
||||
elif value >= 1e3:
|
||||
unit = "kHz"
|
||||
num = value / 1e3
|
||||
else:
|
||||
unit = "Hz"
|
||||
num = value
|
||||
|
||||
# Format with up to 2 decimal places, then strip unnecessary trailing zeros and dot
|
||||
formatted = f"{int(num)}" if unit == "Hz" else f"{num:.2f}".rstrip("0").rstrip(".")
|
||||
return formatted + unit
|
||||
|
||||
|
||||
def _frequency_validator(value):
|
||||
platform = get_target_platform()
|
||||
frequency = PLATFORM_SPI_CLOCKS[platform]
|
||||
value = cv.frequency(value)
|
||||
if value > frequency:
|
||||
raise cv.Invalid(
|
||||
f"The configured SPI data rate ({_render_hz(value)}) exceeds the maximum for this platform ({_render_hz(frequency)})"
|
||||
)
|
||||
if value < 1000:
|
||||
raise cv.Invalid("The configured SPI data rate must be at least 1000Hz")
|
||||
divisor = round(frequency / value)
|
||||
actual = frequency / divisor
|
||||
error = abs(actual - value) / value
|
||||
if error > MAX_DATA_RATE_ERROR:
|
||||
raise cv.Invalid(
|
||||
f"The configured SPI data rate ({_render_hz(value)}) is not available for this chip - closest is {_render_hz(actual)}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
SPI_DATA_RATE_SCHEMA = _frequency_validator
|
||||
|
||||
|
||||
SPI_MODE_OPTIONS = {
|
||||
"MODE0": SPIMode.MODE0,
|
||||
@@ -393,19 +432,20 @@ def spi_device_schema(
|
||||
:param mode Choose single, quad or octal mode.
|
||||
:return: The SPI device schema, `extend` this in your config schema.
|
||||
"""
|
||||
schema = {
|
||||
cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]),
|
||||
cv.Optional(CONF_DATA_RATE, default=default_data_rate): SPI_DATA_RATE_SCHEMA,
|
||||
cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum(
|
||||
SPI_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32),
|
||||
}
|
||||
if cs_pin_required:
|
||||
schema[cv.Required(CONF_CS_PIN)] = pins.gpio_output_pin_schema
|
||||
else:
|
||||
schema[cv.Optional(CONF_CS_PIN)] = pins.gpio_output_pin_schema
|
||||
return cv.Schema(schema)
|
||||
cs_pin_option = cv.Required if cs_pin_required else cv.Optional
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_SPI_ID): cv.use_id(TYPE_CLASS[mode]),
|
||||
cv.Optional(
|
||||
CONF_DATA_RATE, default=default_data_rate
|
||||
): SPI_DATA_RATE_SCHEMA,
|
||||
cv.Optional(CONF_SPI_MODE, default=default_mode): cv.enum(
|
||||
SPI_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_RELEASE_DEVICE): cv.All(cv.boolean, cv.only_on_esp32),
|
||||
cs_pin_option(CONF_CS_PIN): pins.gpio_output_pin_schema,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def register_spi_device(var, config):
|
||||
|
||||
@@ -329,6 +329,12 @@ void HOT SSD1306::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
}
|
||||
}
|
||||
void SSD1306::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fill = color.is_on() ? 0xFF : 0x00;
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
this->buffer_[i] = fill;
|
||||
|
||||
@@ -174,6 +174,12 @@ void HOT SSD1322::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
this->buffer_[pos] |= color4;
|
||||
}
|
||||
void SSD1322::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color);
|
||||
uint8_t fill = (color4 & SSD1322_COLORMASK) | ((color4 & SSD1322_COLORMASK) << SSD1322_COLORSHIFT);
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
|
||||
@@ -150,6 +150,12 @@ void HOT SSD1327::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
this->buffer_[pos] |= color4;
|
||||
}
|
||||
void SSD1327::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t color4 = display::ColorUtil::color_to_grayscale4(color);
|
||||
uint8_t fill = (color4 & SSD1327_COLORMASK) | ((color4 & SSD1327_COLORMASK) << SSD1327_COLORSHIFT);
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
|
||||
@@ -128,6 +128,12 @@ void HOT SSD1331::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
this->buffer_[pos] = color565 & 0xff;
|
||||
}
|
||||
void SSD1331::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t color565 = display::ColorUtil::color_to_565(color);
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) {
|
||||
if (i & 1) {
|
||||
|
||||
@@ -160,6 +160,12 @@ void HOT SSD1351::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
this->buffer_[pos] = color565 & 0xff;
|
||||
}
|
||||
void SSD1351::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t color565 = display::ColorUtil::color_to_565(color);
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++) {
|
||||
if (i & 1) {
|
||||
|
||||
@@ -131,7 +131,16 @@ void HOT ST7567::draw_absolute_pixel_internal(int x, int y, Color color) {
|
||||
}
|
||||
}
|
||||
|
||||
void ST7567::fill(Color color) { memset(buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
|
||||
void ST7567::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fill = color.is_on() ? 0xFF : 0x00;
|
||||
memset(buffer_, fill, this->get_buffer_length_());
|
||||
}
|
||||
|
||||
void ST7567::init_reset_() {
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
|
||||
@@ -89,7 +89,16 @@ void HOT ST7920::write_display_data() {
|
||||
}
|
||||
}
|
||||
|
||||
void ST7920::fill(Color color) { memset(this->buffer_, color.is_on() ? 0xFF : 0x00, this->get_buffer_length_()); }
|
||||
void ST7920::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t fill = color.is_on() ? 0xFF : 0x00;
|
||||
memset(this->buffer_, fill, this->get_buffer_length_());
|
||||
}
|
||||
|
||||
void ST7920::dump_config() {
|
||||
LOG_DISPLAY("", "ST7920", this);
|
||||
|
||||
@@ -28,7 +28,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_TRIGGER_PIN): pins.gpio_output_pin_schema,
|
||||
cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance,
|
||||
cv.Optional(
|
||||
|
||||
@@ -1,64 +1,96 @@
|
||||
#include "ultrasonic_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ultrasonic {
|
||||
namespace esphome::ultrasonic {
|
||||
|
||||
static const char *const TAG = "ultrasonic.sensor";
|
||||
|
||||
static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering)
|
||||
static constexpr uint32_t TIMEOUT_MARGIN_US = 1000; // Extra margin for sensor processing time
|
||||
|
||||
void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) {
|
||||
uint32_t now = micros();
|
||||
if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) {
|
||||
arg->echo_start_us = now;
|
||||
arg->echo_start = true;
|
||||
} else {
|
||||
arg->echo_end_us = now;
|
||||
arg->echo_end = true;
|
||||
}
|
||||
}
|
||||
|
||||
void IRAM_ATTR UltrasonicSensorComponent::send_trigger_pulse_() {
|
||||
InterruptLock lock;
|
||||
this->store_.echo_start_us = 0;
|
||||
this->store_.echo_end_us = 0;
|
||||
this->store_.echo_start = false;
|
||||
this->store_.echo_end = false;
|
||||
this->trigger_pin_isr_.digital_write(true);
|
||||
delayMicroseconds(this->pulse_time_us_);
|
||||
this->trigger_pin_isr_.digital_write(false);
|
||||
this->measurement_pending_ = true;
|
||||
this->measurement_start_us_ = micros();
|
||||
}
|
||||
|
||||
void UltrasonicSensorComponent::setup() {
|
||||
this->trigger_pin_->setup();
|
||||
this->trigger_pin_->digital_write(false);
|
||||
this->trigger_pin_isr_ = this->trigger_pin_->to_isr();
|
||||
this->echo_pin_->setup();
|
||||
// isr is faster to access
|
||||
echo_isr_ = echo_pin_->to_isr();
|
||||
this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE);
|
||||
}
|
||||
|
||||
void UltrasonicSensorComponent::update() {
|
||||
this->trigger_pin_->digital_write(true);
|
||||
delayMicroseconds(this->pulse_time_us_);
|
||||
this->trigger_pin_->digital_write(false);
|
||||
if (this->measurement_pending_) {
|
||||
return;
|
||||
}
|
||||
this->send_trigger_pulse_();
|
||||
}
|
||||
|
||||
const uint32_t start = micros();
|
||||
while (micros() - start < timeout_us_ && echo_isr_.digital_read())
|
||||
;
|
||||
while (micros() - start < timeout_us_ && !echo_isr_.digital_read())
|
||||
;
|
||||
const uint32_t pulse_start = micros();
|
||||
while (micros() - start < timeout_us_ && echo_isr_.digital_read())
|
||||
;
|
||||
const uint32_t pulse_end = micros();
|
||||
void UltrasonicSensorComponent::loop() {
|
||||
if (!this->measurement_pending_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Echo took %" PRIu32 "µs", pulse_end - pulse_start);
|
||||
|
||||
if (pulse_end - start >= timeout_us_) {
|
||||
ESP_LOGD(TAG, "'%s' - Distance measurement timed out!", this->name_.c_str());
|
||||
this->publish_state(NAN);
|
||||
} else {
|
||||
float result = UltrasonicSensorComponent::us_to_m(pulse_end - pulse_start);
|
||||
if (this->store_.echo_end) {
|
||||
uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us;
|
||||
ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration);
|
||||
float result = UltrasonicSensorComponent::us_to_m(pulse_duration);
|
||||
ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result);
|
||||
this->publish_state(result);
|
||||
this->measurement_pending_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t elapsed = micros() - this->measurement_start_us_;
|
||||
if (elapsed >= this->timeout_us_ + TIMEOUT_MARGIN_US) {
|
||||
ESP_LOGD(TAG,
|
||||
"'%s' - Timeout after %" PRIu32 "us (measurement_start=%" PRIu32 ", echo_start=%" PRIu32
|
||||
", echo_end=%" PRIu32 ")",
|
||||
this->name_.c_str(), elapsed, this->measurement_start_us_, this->store_.echo_start_us,
|
||||
this->store_.echo_end_us);
|
||||
this->publish_state(NAN);
|
||||
this->measurement_pending_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void UltrasonicSensorComponent::dump_config() {
|
||||
LOG_SENSOR("", "Ultrasonic Sensor", this);
|
||||
LOG_PIN(" Echo Pin: ", this->echo_pin_);
|
||||
LOG_PIN(" Trigger Pin: ", this->trigger_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Pulse time: %" PRIu32 " µs\n"
|
||||
" Timeout: %" PRIu32 " µs",
|
||||
" Pulse time: %" PRIu32 " us\n"
|
||||
" Timeout: %" PRIu32 " us",
|
||||
this->pulse_time_us_, this->timeout_us_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float UltrasonicSensorComponent::us_to_m(uint32_t us) {
|
||||
const float speed_sound_m_per_s = 343.0f;
|
||||
const float time_s = us / 1e6f;
|
||||
const float total_dist = time_s * speed_sound_m_per_s;
|
||||
return total_dist / 2.0f;
|
||||
}
|
||||
float UltrasonicSensorComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
void UltrasonicSensorComponent::set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; }
|
||||
void UltrasonicSensorComponent::set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; }
|
||||
|
||||
} // namespace ultrasonic
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ultrasonic
|
||||
|
||||
@@ -6,41 +6,49 @@
|
||||
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace ultrasonic {
|
||||
namespace esphome::ultrasonic {
|
||||
|
||||
struct UltrasonicSensorStore {
|
||||
static void gpio_intr(UltrasonicSensorStore *arg);
|
||||
|
||||
volatile uint32_t echo_start_us{0};
|
||||
volatile uint32_t echo_end_us{0};
|
||||
volatile bool echo_start{false};
|
||||
volatile bool echo_end{false};
|
||||
};
|
||||
|
||||
class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent {
|
||||
public:
|
||||
void set_trigger_pin(GPIOPin *trigger_pin) { trigger_pin_ = trigger_pin; }
|
||||
void set_echo_pin(InternalGPIOPin *echo_pin) { echo_pin_ = echo_pin; }
|
||||
void set_trigger_pin(InternalGPIOPin *trigger_pin) { this->trigger_pin_ = trigger_pin; }
|
||||
void set_echo_pin(InternalGPIOPin *echo_pin) { this->echo_pin_ = echo_pin; }
|
||||
|
||||
/// Set the timeout for waiting for the echo in µs.
|
||||
void set_timeout_us(uint32_t timeout_us);
|
||||
void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; }
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
/// Set up pins and register interval.
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
void update() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
/// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04)
|
||||
void set_pulse_time_us(uint32_t pulse_time_us);
|
||||
void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; }
|
||||
|
||||
protected:
|
||||
/// Helper function to convert the specified echo duration in µs to meters.
|
||||
static float us_to_m(uint32_t us);
|
||||
/// Helper function to convert the specified distance in meters to the echo duration in µs.
|
||||
void send_trigger_pulse_();
|
||||
|
||||
GPIOPin *trigger_pin_;
|
||||
InternalGPIOPin *trigger_pin_;
|
||||
ISRInternalGPIOPin trigger_pin_isr_;
|
||||
InternalGPIOPin *echo_pin_;
|
||||
ISRInternalGPIOPin echo_isr_;
|
||||
uint32_t timeout_us_{}; /// 2 meters.
|
||||
UltrasonicSensorStore store_;
|
||||
uint32_t timeout_us_{};
|
||||
uint32_t pulse_time_us_{};
|
||||
|
||||
uint32_t measurement_start_us_{0};
|
||||
bool measurement_pending_{false};
|
||||
};
|
||||
|
||||
} // namespace ultrasonic
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ultrasonic
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
|
||||
#include "usb_cdc_acm.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <sys/param.h>
|
||||
@@ -16,6 +17,9 @@ namespace esphome::usb_cdc_acm {
|
||||
|
||||
static const char *TAG = "usb_cdc_acm";
|
||||
|
||||
// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t USB_CDC_MAX_LOG_BYTES = 168;
|
||||
|
||||
static constexpr size_t USB_TX_TASK_STACK_SIZE = 4096;
|
||||
static constexpr size_t USB_TX_TASK_STACK_SIZE_VV = 8192;
|
||||
|
||||
@@ -43,7 +47,10 @@ static void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event) {
|
||||
esp_err_t ret =
|
||||
tinyusb_cdcacm_read(static_cast<tinyusb_cdcacm_itf_t>(itf), rx_buf, CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);
|
||||
ESP_LOGV(TAG, "tinyusb_cdc_rx_callback itf=%d (size: %u)", itf, rx_size);
|
||||
ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty(rx_buf, rx_size).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char rx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "rx_buf = %s", format_hex_pretty_to(rx_hex_buf, rx_buf, rx_size));
|
||||
|
||||
if (ret == ESP_OK && rx_size > 0) {
|
||||
RingbufHandle_t rx_ringbuf = instance->get_rx_ringbuf();
|
||||
@@ -306,7 +313,10 @@ void USBCDCACMInstance::usb_tx_task() {
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "USB TX itf=%d: Read %d bytes from buffer", this->itf_, tx_data_size);
|
||||
ESP_LOGVV(TAG, "data = %s", format_hex_pretty(data, tx_data_size).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char tx_hex_buf[format_hex_pretty_size(USB_CDC_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "data = %s", format_hex_pretty_to(tx_hex_buf, data, tx_data_size));
|
||||
|
||||
// Serial data will be split up into 64 byte chunks to be sent over USB so this
|
||||
// usually will take multiple iterations
|
||||
|
||||
@@ -53,7 +53,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
|
||||
}
|
||||
),
|
||||
only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||
only_on_variant(supported=[VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3]),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -627,9 +627,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
ESP_LOGD(TAG, "Assist Pipeline running");
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
this->started_streaming_tts_ = false;
|
||||
for (auto arg : msg.data) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if (arg.name == "url") {
|
||||
this->tts_response_url_ = std::move(arg.value);
|
||||
this->tts_response_url_ = arg.value;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -648,9 +648,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
break;
|
||||
case api::enums::VOICE_ASSISTANT_STT_END: {
|
||||
std::string text;
|
||||
for (auto arg : msg.data) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if (arg.name == "text") {
|
||||
text = std::move(arg.value);
|
||||
text = arg.value;
|
||||
}
|
||||
}
|
||||
if (text.empty()) {
|
||||
@@ -693,9 +693,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
break;
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_INTENT_END: {
|
||||
for (auto arg : msg.data) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if (arg.name == "conversation_id") {
|
||||
this->conversation_id_ = std::move(arg.value);
|
||||
this->conversation_id_ = arg.value;
|
||||
} else if (arg.name == "continue_conversation") {
|
||||
this->continue_conversation_ = (arg.value == "1");
|
||||
}
|
||||
@@ -705,9 +705,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_TTS_START: {
|
||||
std::string text;
|
||||
for (auto arg : msg.data) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if (arg.name == "text") {
|
||||
text = std::move(arg.value);
|
||||
text = arg.value;
|
||||
}
|
||||
}
|
||||
if (text.empty()) {
|
||||
@@ -731,9 +731,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
}
|
||||
case api::enums::VOICE_ASSISTANT_TTS_END: {
|
||||
std::string url;
|
||||
for (auto arg : msg.data) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if (arg.name == "url") {
|
||||
url = std::move(arg.value);
|
||||
url = arg.value;
|
||||
}
|
||||
}
|
||||
if (url.empty()) {
|
||||
@@ -778,11 +778,11 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
|
||||
case api::enums::VOICE_ASSISTANT_ERROR: {
|
||||
std::string code = "";
|
||||
std::string message = "";
|
||||
for (auto arg : msg.data) {
|
||||
for (const auto &arg : msg.data) {
|
||||
if (arg.name == "code") {
|
||||
code = std::move(arg.value);
|
||||
code = arg.value;
|
||||
} else if (arg.name == "message") {
|
||||
message = std::move(arg.value);
|
||||
message = arg.value;
|
||||
}
|
||||
}
|
||||
if (code == "wake-word-timeout" || code == "wake_word_detection_aborted" || code == "no_wake_word") {
|
||||
|
||||
@@ -172,6 +172,12 @@ void WaveshareEPaperBase::update() {
|
||||
this->display();
|
||||
}
|
||||
void WaveshareEPaper::fill(Color color) {
|
||||
// If clipping is active, fall back to base implementation
|
||||
if (this->get_clipping().is_set()) {
|
||||
Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
// flip logic
|
||||
const uint8_t fill = color.is_on() ? 0x00 : 0xFF;
|
||||
for (uint32_t i = 0; i < this->get_buffer_length_(); i++)
|
||||
@@ -234,6 +240,12 @@ uint8_t WaveshareEPaper7C::color_to_hex(Color color) {
|
||||
return hex_code;
|
||||
}
|
||||
void WaveshareEPaper7C::fill(Color color) {
|
||||
// If clipping is active, use base class (3-bit packing is complex for partial fills)
|
||||
if (this->get_clipping().is_set()) {
|
||||
display::Display::fill(color);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t pixel_color;
|
||||
if (color.is_on()) {
|
||||
pixel_color = this->color_to_hex(color);
|
||||
|
||||
@@ -45,62 +45,144 @@ static constexpr size_t PSTR_LOCAL_SIZE = 18;
|
||||
#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1)
|
||||
|
||||
// Parse URL and return match info
|
||||
static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain) {
|
||||
UrlMatch match{};
|
||||
// URL formats (disambiguated by HTTP method for 3-segment case):
|
||||
// GET /{domain}/{entity_name} - main device state
|
||||
// POST /{domain}/{entity_name}/{action} - main device action
|
||||
// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only)
|
||||
// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only)
|
||||
static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) {
|
||||
// URL must start with '/' and have content after it
|
||||
if (url_len < 2 || url_ptr[0] != '/')
|
||||
return UrlMatch{};
|
||||
|
||||
// URL must start with '/'
|
||||
if (url_len < 2 || url_ptr[0] != '/') {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Skip leading '/'
|
||||
const char *start = url_ptr + 1;
|
||||
const char *p = url_ptr + 1;
|
||||
const char *end = url_ptr + url_len;
|
||||
|
||||
// Find domain (everything up to next '/' or end)
|
||||
const char *domain_end = (const char *) memchr(start, '/', end - start);
|
||||
if (!domain_end) {
|
||||
// No second slash found - original behavior returns invalid
|
||||
return match;
|
||||
}
|
||||
// Helper to find next segment: returns pointer after '/' or nullptr if no more slashes
|
||||
auto next_segment = [&end](const char *start) -> const char * {
|
||||
const char *slash = (const char *) memchr(start, '/', end - start);
|
||||
return slash ? slash + 1 : nullptr;
|
||||
};
|
||||
|
||||
// Set domain
|
||||
match.domain = start;
|
||||
match.domain_len = domain_end - start;
|
||||
// Helper to make StringRef from segment start to next segment (or end)
|
||||
auto make_ref = [&end](const char *start, const char *next_start) -> StringRef {
|
||||
return StringRef(start, (next_start ? next_start - 1 : end) - start);
|
||||
};
|
||||
|
||||
// Parse domain segment
|
||||
const char *s1 = p;
|
||||
const char *s2 = next_segment(s1);
|
||||
|
||||
// Must have domain with trailing slash
|
||||
if (!s2)
|
||||
return UrlMatch{};
|
||||
|
||||
UrlMatch match{};
|
||||
match.domain = make_ref(s1, s2);
|
||||
match.valid = true;
|
||||
|
||||
if (only_domain) {
|
||||
if (only_domain || s2 >= end)
|
||||
return match;
|
||||
}
|
||||
|
||||
// Parse ID if present
|
||||
if (domain_end + 1 >= end) {
|
||||
return match; // Nothing after domain slash
|
||||
}
|
||||
// Parse remaining segments only when needed
|
||||
const char *s3 = next_segment(s2);
|
||||
const char *s4 = s3 ? next_segment(s3) : nullptr;
|
||||
|
||||
const char *id_start = domain_end + 1;
|
||||
const char *id_end = (const char *) memchr(id_start, '/', end - id_start);
|
||||
StringRef seg2 = make_ref(s2, s3);
|
||||
StringRef seg3 = s3 ? make_ref(s3, s4) : StringRef();
|
||||
StringRef seg4 = s4 ? make_ref(s4, nullptr) : StringRef();
|
||||
|
||||
if (!id_end) {
|
||||
// No more slashes, entire remaining string is ID
|
||||
match.id = id_start;
|
||||
match.id_len = end - id_start;
|
||||
return match;
|
||||
}
|
||||
// Reject empty segments
|
||||
if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty()))
|
||||
return UrlMatch{};
|
||||
|
||||
// Set ID
|
||||
match.id = id_start;
|
||||
match.id_len = id_end - id_start;
|
||||
|
||||
// Parse method if present
|
||||
if (id_end + 1 < end) {
|
||||
match.method = id_end + 1;
|
||||
match.method_len = end - (id_end + 1);
|
||||
// Interpret based on segment count
|
||||
if (!s3) {
|
||||
// 1 segment after domain: /{domain}/{entity}
|
||||
match.id = seg2;
|
||||
} else if (!s4) {
|
||||
// 2 segments after domain: /{domain}/{X}/{Y}
|
||||
// HTTP method disambiguates: GET = device/entity, POST = entity/action
|
||||
if (is_post) {
|
||||
match.id = seg2;
|
||||
match.method = seg3;
|
||||
return match;
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
#else
|
||||
return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES
|
||||
#endif
|
||||
} else {
|
||||
// 3 segments after domain: /{domain}/{device}/{entity}/{action}
|
||||
#ifdef USE_DEVICES
|
||||
if (!is_post) {
|
||||
return UrlMatch{}; // 4-segment GET not supported (action requires POST)
|
||||
}
|
||||
match.device_name = seg2;
|
||||
match.id = seg3;
|
||||
match.method = seg4;
|
||||
#else
|
||||
return UrlMatch{}; // Not supported without USE_DEVICES
|
||||
#endif
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
|
||||
EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const {
|
||||
EntityMatchResult result{false, this->method.empty()};
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
Device *entity_device = entity->get_device();
|
||||
bool url_has_device = !this->device_name.empty();
|
||||
bool entity_has_device = (entity_device != nullptr);
|
||||
|
||||
// Device matching: URL device segment must match entity's device
|
||||
if (url_has_device != entity_has_device) {
|
||||
return result; // Mismatch: one has device, other doesn't
|
||||
}
|
||||
if (url_has_device && this->device_name != entity_device->get_name()) {
|
||||
return result; // Device name doesn't match
|
||||
}
|
||||
#endif
|
||||
|
||||
// Try matching by entity name (new format)
|
||||
if (this->id == entity->get_name()) {
|
||||
result.matched = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Fall back to object_id (deprecated format)
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
StringRef object_id = entity->get_object_id_to(object_id_buf);
|
||||
if (this->id == object_id) {
|
||||
result.matched = true;
|
||||
// Log deprecation warning
|
||||
#ifdef USE_DEVICES
|
||||
Device *device = entity->get_device();
|
||||
if (device != nullptr) {
|
||||
ESP_LOGW(TAG,
|
||||
"Deprecated URL format: /%.*s/%.*s/%.*s - use entity name '/%.*s/%s/%s' instead. "
|
||||
"Object ID URLs will be removed in 2026.7.0.",
|
||||
(int) this->domain.size(), this->domain.c_str(), (int) this->device_name.size(),
|
||||
this->device_name.c_str(), (int) this->id.size(), this->id.c_str(), (int) this->domain.size(),
|
||||
this->domain.c_str(), device->get_name(), entity->get_name().c_str());
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
ESP_LOGW(TAG,
|
||||
"Deprecated URL format: /%.*s/%.*s - use entity name '/%.*s/%s' instead. "
|
||||
"Object ID URLs will be removed in 2026.7.0.",
|
||||
(int) this->domain.size(), this->domain.c_str(), (int) this->id.size(), this->id.c_str(),
|
||||
(int) this->domain.size(), this->domain.c_str(), entity->get_name().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
|
||||
// helper for allowing only unique entries in the queue
|
||||
void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
|
||||
@@ -397,15 +479,53 @@ void WebServer::handle_js_request(AsyncWebServerRequest *request) {
|
||||
#endif
|
||||
|
||||
// Helper functions to reduce code size by avoiding macro expansion
|
||||
// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name}
|
||||
// Uses names (not object_id) to avoid UTF-8 collision issues
|
||||
static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
|
||||
char id_buf[160]; // prefix + dash + object_id (up to 128) + null
|
||||
size_t len = strlen(prefix);
|
||||
memcpy(id_buf, prefix, len); // NOLINT(bugprone-not-null-terminated-result) - null added by write_object_id_to
|
||||
id_buf[len++] = '-';
|
||||
obj->write_object_id_to(id_buf + len, sizeof(id_buf) - len);
|
||||
const StringRef &name = obj->get_name();
|
||||
size_t prefix_len = strlen(prefix);
|
||||
size_t name_len = name.size();
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
Device *device = obj->get_device();
|
||||
const char *device_name = device ? device->get_name() : nullptr;
|
||||
size_t device_len = device_name ? strlen(device_name) : 0;
|
||||
#endif
|
||||
|
||||
// Build id into stack buffer - ArduinoJson copies the string
|
||||
// Format: {prefix}/{device?}/{name}
|
||||
// Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120):
|
||||
// With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin
|
||||
// Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin
|
||||
#ifdef USE_DEVICES
|
||||
char id_buf[280];
|
||||
#else
|
||||
char id_buf[150];
|
||||
#endif
|
||||
char *p = id_buf;
|
||||
memcpy(p, prefix, prefix_len);
|
||||
p += prefix_len;
|
||||
*p++ = '/';
|
||||
#ifdef USE_DEVICES
|
||||
if (device_name) {
|
||||
memcpy(p, device_name, device_len);
|
||||
p += device_len;
|
||||
*p++ = '/';
|
||||
}
|
||||
#endif
|
||||
memcpy(p, name.c_str(), name_len);
|
||||
p[name_len] = '\0';
|
||||
|
||||
root[ESPHOME_F("id")] = id_buf;
|
||||
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root[ESPHOME_F("name")] = obj->get_name();
|
||||
root[ESPHOME_F("domain")] = prefix;
|
||||
root[ESPHOME_F("name")] = name;
|
||||
#ifdef USE_DEVICES
|
||||
if (device_name) {
|
||||
root[ESPHOME_F("device")] = device_name;
|
||||
}
|
||||
#endif
|
||||
root[ESPHOME_F("icon")] = obj->get_icon_ref();
|
||||
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
|
||||
bool is_disabled = obj->is_disabled_by_default();
|
||||
@@ -444,10 +564,11 @@ void WebServer::on_sensor_update(sensor::Sensor *obj) {
|
||||
}
|
||||
void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (sensor::Sensor *obj : App.get_sensors()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (match.method_empty()) {
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->sensor_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -490,10 +611,11 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) {
|
||||
}
|
||||
void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (match.method_empty()) {
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->text_sensor_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -532,10 +654,11 @@ void WebServer::on_switch_update(switch_::Switch *obj) {
|
||||
}
|
||||
void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (switch_::Switch *obj : App.get_switches()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->switch_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -601,9 +724,10 @@ std::string WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail
|
||||
#ifdef USE_BUTTON
|
||||
void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (button::Button *obj : App.get_buttons()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->button_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -645,10 +769,11 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
|
||||
}
|
||||
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (match.method_empty()) {
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->binary_sensor_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -686,10 +811,11 @@ void WebServer::on_fan_update(fan::Fan *obj) {
|
||||
}
|
||||
void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (fan::Fan *obj : App.get_fans()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->fan_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -766,10 +892,11 @@ void WebServer::on_light_update(light::LightState *obj) {
|
||||
}
|
||||
void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (light::LightState *obj : App.get_lights()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->light_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -844,10 +971,11 @@ void WebServer::on_cover_update(cover::Cover *obj) {
|
||||
}
|
||||
void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (cover::Cover *obj : App.get_covers()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->cover_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -932,10 +1060,11 @@ void WebServer::on_number_update(number::Number *obj) {
|
||||
}
|
||||
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_numbers()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->number_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -999,9 +1128,10 @@ void WebServer::on_date_update(datetime::DateEntity *obj) {
|
||||
}
|
||||
void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_dates()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->date_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1062,9 +1192,10 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) {
|
||||
}
|
||||
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_times()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->time_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1124,9 +1255,10 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
|
||||
}
|
||||
void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_datetimes()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->datetime_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1188,10 +1320,11 @@ void WebServer::on_text_update(text::Text *obj) {
|
||||
}
|
||||
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_texts()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->text_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1244,10 +1377,11 @@ void WebServer::on_select_update(select::Select *obj) {
|
||||
}
|
||||
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_selects()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->select_json_(obj, obj->has_state() ? obj->current_option() : "", detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1301,10 +1435,11 @@ void WebServer::on_climate_update(climate::Climate *obj) {
|
||||
}
|
||||
void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (auto *obj : App.get_climates()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->climate_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1451,10 +1586,11 @@ void WebServer::on_lock_update(lock::Lock *obj) {
|
||||
}
|
||||
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (lock::Lock *obj : App.get_locks()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->lock_json_(obj, obj->state, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1525,10 +1661,11 @@ void WebServer::on_valve_update(valve::Valve *obj) {
|
||||
}
|
||||
void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (valve::Valve *obj : App.get_valves()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->valve_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1609,10 +1746,11 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
|
||||
}
|
||||
void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1690,11 +1828,12 @@ void WebServer::on_event(event::Event *obj) {
|
||||
|
||||
void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (event::Event *obj : App.get_events()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
|
||||
if (match.method_empty()) {
|
||||
if (entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->event_json_(obj, "", detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1759,10 +1898,11 @@ void WebServer::on_update(update::UpdateEntity *obj) {
|
||||
}
|
||||
void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
for (update::UpdateEntity *obj : App.get_updates()) {
|
||||
if (!match.id_equals_entity(obj))
|
||||
auto entity_match = match.match_entity(obj);
|
||||
if (!entity_match.matched)
|
||||
continue;
|
||||
|
||||
if (request->method() == HTTP_GET && match.method_empty()) {
|
||||
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
|
||||
auto detail = get_request_detail(request);
|
||||
std::string data = this->update_json_(obj, detail);
|
||||
request->send(200, "application/json", data.c_str());
|
||||
@@ -1973,7 +2113,8 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) {
|
||||
#endif
|
||||
|
||||
// Parse URL for component routing
|
||||
UrlMatch match = match_url(url.c_str(), url.length(), false);
|
||||
// Pass HTTP method to disambiguate 3-segment URLs (GET=sub-device state, POST=main device action)
|
||||
UrlMatch match = match_url(url.c_str(), url.length(), false, request->method() == HTTP_POST);
|
||||
|
||||
// Route to appropriate handler based on domain
|
||||
// NOLINTNEXTLINE(readability-simplify-boolean-expr)
|
||||
|
||||
@@ -35,33 +35,29 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
/// Result of matching a URL against an entity
|
||||
struct EntityMatchResult {
|
||||
bool matched; ///< True if entity matched the URL
|
||||
bool action_is_empty; ///< True if no action/method segment in URL
|
||||
};
|
||||
|
||||
/// Internal helper struct that is used to parse incoming URLs
|
||||
struct UrlMatch {
|
||||
const char *domain; ///< Pointer to domain within URL, for example "sensor"
|
||||
const char *id; ///< Pointer to id within URL, for example "living_room_fan"
|
||||
const char *method; ///< Pointer to method within URL, for example "turn_on"
|
||||
uint8_t domain_len; ///< Length of domain string
|
||||
uint8_t id_len; ///< Length of id string
|
||||
uint8_t method_len; ///< Length of method string
|
||||
bool valid; ///< Whether this match is valid
|
||||
StringRef domain; ///< Domain within URL, for example "sensor"
|
||||
StringRef id; ///< Entity name/id within URL, for example "Temperature"
|
||||
StringRef method; ///< Method within URL, for example "turn_on"
|
||||
#ifdef USE_DEVICES
|
||||
StringRef device_name; ///< Device name within URL, empty for main device
|
||||
#endif
|
||||
bool valid{false}; ///< Whether this match is valid
|
||||
|
||||
// Helper methods for string comparisons
|
||||
bool domain_equals(const char *str) const {
|
||||
return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0;
|
||||
}
|
||||
bool domain_equals(const char *str) const { return this->domain == str; }
|
||||
bool method_equals(const char *str) const { return this->method == str; }
|
||||
|
||||
bool id_equals_entity(EntityBase *entity) const {
|
||||
// Get object_id with zero heap allocation
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
StringRef object_id = entity->get_object_id_to(object_id_buf);
|
||||
return id && id_len == object_id.size() && memcmp(id, object_id.c_str(), id_len) == 0;
|
||||
}
|
||||
|
||||
bool method_equals(const char *str) const {
|
||||
return method && method_len == strlen(str) && memcmp(method, str, method_len) == 0;
|
||||
}
|
||||
|
||||
bool method_empty() const { return method_len == 0; }
|
||||
/// Match entity by name first, then fall back to object_id with deprecation warning
|
||||
/// Returns EntityMatchResult with match status and whether action segment is empty
|
||||
EntityMatchResult match_entity(EntityBase *entity) const;
|
||||
};
|
||||
|
||||
#ifdef USE_WEBSERVER_SORTING
|
||||
|
||||
@@ -5,6 +5,29 @@
|
||||
|
||||
namespace esphome::web_server {
|
||||
|
||||
// Write HTML-escaped text to stream (escapes ", &, <, >)
|
||||
static void write_html_escaped(AsyncResponseStream *stream, const char *text) {
|
||||
for (const char *p = text; *p; ++p) {
|
||||
switch (*p) {
|
||||
case '"':
|
||||
stream->print(""");
|
||||
break;
|
||||
case '&':
|
||||
stream->print("&");
|
||||
break;
|
||||
case '<':
|
||||
stream->print("<");
|
||||
break;
|
||||
case '>':
|
||||
stream->print(">");
|
||||
break;
|
||||
default:
|
||||
stream->write(*p);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action,
|
||||
const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) {
|
||||
stream->print("<tr class=\"");
|
||||
@@ -16,8 +39,27 @@ void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &
|
||||
stream->print("-");
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
stream->print(obj->get_object_id_to(object_id_buf).c_str());
|
||||
// Add data attributes for hierarchical URL support
|
||||
stream->print("\" data-domain=\"");
|
||||
stream->print(klass.c_str());
|
||||
stream->print("\" data-name=\"");
|
||||
write_html_escaped(stream, obj->get_name().c_str());
|
||||
#ifdef USE_DEVICES
|
||||
Device *device = obj->get_device();
|
||||
if (device != nullptr) {
|
||||
stream->print("\" data-device=\"");
|
||||
write_html_escaped(stream, device->get_name());
|
||||
}
|
||||
#endif
|
||||
stream->print("\"><td>");
|
||||
stream->print(obj->get_name().c_str());
|
||||
#ifdef USE_DEVICES
|
||||
if (device != nullptr) {
|
||||
stream->print("[");
|
||||
write_html_escaped(stream, device->get_name());
|
||||
stream->print("] ");
|
||||
}
|
||||
#endif
|
||||
write_html_escaped(stream, obj->get_name().c_str());
|
||||
stream->print("</td><td></td><td>");
|
||||
stream->print(action.c_str());
|
||||
if (action_func) {
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace web_server_idf {
|
||||
|
||||
static const char *const TAG = "web_server_idf_utils";
|
||||
|
||||
void url_decode(char *str) {
|
||||
size_t url_decode(char *str) {
|
||||
char *start = str;
|
||||
char *ptr = str, buf;
|
||||
for (; *str; str++, ptr++) {
|
||||
if (*str == '%') {
|
||||
@@ -31,7 +32,8 @@ void url_decode(char *str) {
|
||||
*ptr = *str;
|
||||
}
|
||||
}
|
||||
*ptr = *str;
|
||||
*ptr = '\0';
|
||||
return ptr - start;
|
||||
}
|
||||
|
||||
bool request_has_header(httpd_req_t *req, const char *name) { return httpd_req_get_hdr_value_len(req, name); }
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
namespace esphome {
|
||||
namespace web_server_idf {
|
||||
|
||||
/// Decode URL-encoded string in-place (e.g., %20 -> space, + -> space)
|
||||
/// Returns the new length of the decoded string
|
||||
size_t url_decode(char *str);
|
||||
|
||||
bool request_has_header(httpd_req_t *req, const char *name);
|
||||
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
|
||||
optional<std::string> request_get_url_query(httpd_req_t *req);
|
||||
|
||||
@@ -247,11 +247,20 @@ optional<std::string> AsyncWebServerRequest::get_header(const char *name) const
|
||||
}
|
||||
|
||||
std::string AsyncWebServerRequest::url() const {
|
||||
auto *str = strchr(this->req_->uri, '?');
|
||||
if (str == nullptr) {
|
||||
return this->req_->uri;
|
||||
auto *query_start = strchr(this->req_->uri, '?');
|
||||
std::string result;
|
||||
if (query_start == nullptr) {
|
||||
result = this->req_->uri;
|
||||
} else {
|
||||
result = std::string(this->req_->uri, query_start - this->req_->uri);
|
||||
}
|
||||
return std::string(this->req_->uri, str - this->req_->uri);
|
||||
// Decode URL-encoded characters in-place (e.g., %20 -> space)
|
||||
// This matches AsyncWebServer behavior on Arduino
|
||||
if (!result.empty()) {
|
||||
size_t new_len = url_decode(&result[0]);
|
||||
result.resize(new_len);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string AsyncWebServerRequest::host() const { return this->get_header("Host").value(); }
|
||||
|
||||
@@ -12,12 +12,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#include <WiFi.h>
|
||||
#include <WiFiType.h>
|
||||
#include <esp_wifi.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIBRETINY
|
||||
#include <WiFi.h>
|
||||
#endif
|
||||
@@ -578,10 +572,6 @@ class WiFiComponent : public Component {
|
||||
static void s_wifi_scan_done_callback(void *arg, STATUS status);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info);
|
||||
void wifi_scan_done_callback_();
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
void wifi_process_event_(IDFWiFiEvent *data);
|
||||
#endif
|
||||
|
||||
@@ -483,6 +483,12 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
|
||||
s_sta_connected = false;
|
||||
s_sta_connect_error = false;
|
||||
s_sta_connect_not_found = false;
|
||||
// Reset IP address flags - ensures we don't report connected before DHCP completes
|
||||
// (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect)
|
||||
this->got_ipv4_address_ = false;
|
||||
#if USE_NETWORK_IPV6
|
||||
this->num_ipv6_addresses_ = 0;
|
||||
#endif
|
||||
|
||||
err = esp_wifi_connect();
|
||||
if (err != ESP_OK) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_cgd1.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,11 +9,14 @@ namespace xiaomi_cgd1 {
|
||||
|
||||
static const char *const TAG = "xiaomi_cgd1";
|
||||
|
||||
static constexpr size_t CGD1_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiCGD1::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(CGD1_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi CGD1\n"
|
||||
" Bindkey: %s",
|
||||
format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
format_hex_pretty_to(bindkey_hex, this->bindkey_, CGD1_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_cgdk2.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,11 +9,14 @@ namespace xiaomi_cgdk2 {
|
||||
|
||||
static const char *const TAG = "xiaomi_cgdk2";
|
||||
|
||||
static constexpr size_t CGDK2_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiCGDK2::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(CGDK2_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi CGDK2\n"
|
||||
" Bindkey: %s",
|
||||
format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
format_hex_pretty_to(bindkey_hex, this->bindkey_, CGDK2_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_cgg1.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,11 +9,14 @@ namespace xiaomi_cgg1 {
|
||||
|
||||
static const char *const TAG = "xiaomi_cgg1";
|
||||
|
||||
static constexpr size_t CGG1_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiCGG1::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(CGG1_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi CGG1\n"
|
||||
" Bindkey: %s",
|
||||
format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
format_hex_pretty_to(bindkey_hex, this->bindkey_, CGG1_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_lywsd02mmc.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,11 +9,14 @@ namespace xiaomi_lywsd02mmc {
|
||||
|
||||
static const char *const TAG = "xiaomi_lywsd02mmc";
|
||||
|
||||
static constexpr size_t LYWSD02MMC_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiLYWSD02MMC::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(LYWSD02MMC_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi LYWSD02MMC\n"
|
||||
" Bindkey: %s",
|
||||
format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
format_hex_pretty_to(bindkey_hex, this->bindkey_, LYWSD02MMC_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_lywsd03mmc.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,11 +9,14 @@ namespace xiaomi_lywsd03mmc {
|
||||
|
||||
static const char *const TAG = "xiaomi_lywsd03mmc";
|
||||
|
||||
static constexpr size_t LYWSD03MMC_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiLYWSD03MMC::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(LYWSD03MMC_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi LYWSD03MMC\n"
|
||||
" Bindkey: %s",
|
||||
format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
format_hex_pretty_to(bindkey_hex, this->bindkey_, LYWSD03MMC_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_mhoc401.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,11 +9,14 @@ namespace xiaomi_mhoc401 {
|
||||
|
||||
static const char *const TAG = "xiaomi_mhoc401";
|
||||
|
||||
static constexpr size_t MHOC401_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiMHOC401::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(MHOC401_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Xiaomi MHOC401\n"
|
||||
" Bindkey: %s",
|
||||
format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
format_hex_pretty_to(bindkey_hex, this->bindkey_, MHOC401_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_rtcgq02lm.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,9 +9,12 @@ namespace xiaomi_rtcgq02lm {
|
||||
|
||||
static const char *const TAG = "xiaomi_rtcgq02lm";
|
||||
|
||||
static constexpr size_t RTCGQ02LM_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiRTCGQ02LM::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(RTCGQ02LM_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG, "Xiaomi RTCGQ02LM");
|
||||
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, RTCGQ02LM_BINDKEY_SIZE, '.'));
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
LOG_BINARY_SENSOR(" ", "Motion", this->motion_);
|
||||
LOG_BINARY_SENSOR(" ", "Light", this->light_);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "xiaomi_xmwsdj04mmc.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -8,9 +9,12 @@ namespace xiaomi_xmwsdj04mmc {
|
||||
|
||||
static const char *const TAG = "xiaomi_xmwsdj04mmc";
|
||||
|
||||
static constexpr size_t XMWSDJ04MMC_BINDKEY_SIZE = 16;
|
||||
|
||||
void XiaomiXMWSDJ04MMC::dump_config() {
|
||||
char bindkey_hex[format_hex_pretty_size(XMWSDJ04MMC_BINDKEY_SIZE)];
|
||||
ESP_LOGCONFIG(TAG, "Xiaomi XMWSDJ04MMC");
|
||||
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty(this->bindkey_, 16).c_str());
|
||||
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, XMWSDJ04MMC_BINDKEY_SIZE, '.'));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace esphome::zwave_proxy {
|
||||
|
||||
static const char *const TAG = "zwave_proxy";
|
||||
|
||||
// Maximum bytes to log in very verbose hex output (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t ZWAVE_MAX_LOG_BYTES = 168;
|
||||
|
||||
static constexpr uint8_t ZWAVE_COMMAND_GET_NETWORK_IDS = 0x20;
|
||||
// GET_NETWORK_IDS response: [SOF][LENGTH][TYPE][CMD][HOME_ID(4)][NODE_ID][...]
|
||||
static constexpr uint8_t ZWAVE_COMMAND_TYPE_RESPONSE = 0x01; // Response type field value
|
||||
@@ -123,10 +126,11 @@ void ZWaveProxy::process_uart_() {
|
||||
}
|
||||
|
||||
void ZWaveProxy::dump_config() {
|
||||
char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Z-Wave Proxy:\n"
|
||||
" Home ID: %s",
|
||||
format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
|
||||
format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
|
||||
}
|
||||
|
||||
void ZWaveProxy::api_connection_authenticated(api::APIConnection *conn) {
|
||||
@@ -167,7 +171,8 @@ bool ZWaveProxy::set_home_id(const uint8_t *new_home_id) {
|
||||
return false; // No change
|
||||
}
|
||||
std::memcpy(this->home_id_.data(), new_home_id, this->home_id_.size());
|
||||
ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty(this->home_id_.data(), this->home_id_.size(), ':', false).c_str());
|
||||
char hex_buf[format_hex_pretty_size(ZWAVE_HOME_ID_SIZE)];
|
||||
ESP_LOGI(TAG, "Home ID: %s", format_hex_pretty_to(hex_buf, this->home_id_.data(), this->home_id_.size()));
|
||||
this->home_id_ready_ = true;
|
||||
return true; // Home ID was changed
|
||||
}
|
||||
@@ -177,7 +182,10 @@ void ZWaveProxy::send_frame(const uint8_t *data, size_t length) {
|
||||
ESP_LOGV(TAG, "Skipping sending duplicate response: 0x%02X", data[0]);
|
||||
return;
|
||||
}
|
||||
ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty(data, length).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Sending: %s", format_hex_pretty_to(hex_buf, data, length));
|
||||
this->write_array(data, length);
|
||||
}
|
||||
|
||||
@@ -250,7 +258,10 @@ bool ZWaveProxy::parse_byte_(uint8_t byte) {
|
||||
this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_NAK;
|
||||
} else {
|
||||
this->parsing_state_ = ZWAVE_PARSING_STATE_SEND_ACK;
|
||||
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(this->buffer_.data(), this->buffer_index_).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(ZWAVE_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_index_));
|
||||
frame_completed = true;
|
||||
}
|
||||
this->response_handler_();
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
namespace esphome::zwave_proxy {
|
||||
|
||||
static constexpr size_t MAX_ZWAVE_FRAME_SIZE = 257; // Maximum Z-Wave frame size
|
||||
static constexpr size_t ZWAVE_HOME_ID_SIZE = 4; // Z-Wave Home ID size in bytes
|
||||
|
||||
enum ZWaveResponseTypes : uint8_t {
|
||||
ZWAVE_FRAME_TYPE_ACK = 0x06,
|
||||
@@ -73,8 +74,8 @@ class ZWaveProxy : public uart::UARTDevice, public Component {
|
||||
|
||||
// Pre-allocated message - always ready to send
|
||||
api::ZWaveProxyFrame outgoing_proto_msg_;
|
||||
std::array<uint8_t, MAX_ZWAVE_FRAME_SIZE> buffer_; // Fixed buffer for incoming data
|
||||
std::array<uint8_t, 4> home_id_{0, 0, 0, 0}; // Fixed buffer for home ID
|
||||
std::array<uint8_t, MAX_ZWAVE_FRAME_SIZE> buffer_; // Fixed buffer for incoming data
|
||||
std::array<uint8_t, ZWAVE_HOME_ID_SIZE> home_id_{}; // Fixed buffer for home ID
|
||||
|
||||
// Pointers and 32-bit values (aligned together)
|
||||
api::APIConnection *api_connection_{nullptr}; // Current subscribed client
|
||||
|
||||
@@ -1981,6 +1981,26 @@ MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
def _validate_no_slash(value):
|
||||
"""Validate that a name does not contain '/' characters.
|
||||
|
||||
The '/' character is used as a path separator in web server URLs,
|
||||
so it cannot be used in entity or device names.
|
||||
"""
|
||||
if "/" in value:
|
||||
raise Invalid(
|
||||
f"Name cannot contain '/' character (used as URL path separator): {value}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
# Maximum length for entity, device, and area names
|
||||
# This ensures web server URL IDs fit in a 280-byte buffer:
|
||||
# domain(20) + "/" + device(120) + "/" + name(120) + null = 263 bytes
|
||||
# Note: Must be < 255 because web_server UrlMatch uses uint8_t for length fields
|
||||
NAME_MAX_LENGTH = 120
|
||||
|
||||
|
||||
def _validate_entity_name(value):
|
||||
value = string(value)
|
||||
try:
|
||||
@@ -1991,9 +2011,28 @@ def _validate_entity_name(value):
|
||||
requires_friendly_name(
|
||||
"Name cannot be None when esphome->friendly_name is not set!"
|
||||
)(value)
|
||||
if value is not None:
|
||||
# Validate length for web server URL compatibility
|
||||
if len(value) > NAME_MAX_LENGTH:
|
||||
raise Invalid(
|
||||
f"Name is too long ({len(value)} chars). "
|
||||
f"Maximum length is {NAME_MAX_LENGTH} characters."
|
||||
)
|
||||
# Validate no '/' in name for web server URL compatibility
|
||||
_validate_no_slash(value)
|
||||
return value
|
||||
|
||||
|
||||
def string_no_slash(value):
|
||||
"""Validate a string that cannot contain '/' characters.
|
||||
|
||||
Used for device and area names where '/' is reserved as a URL path separator.
|
||||
Use with cv.Length() to also enforce maximum length.
|
||||
"""
|
||||
value = string(value)
|
||||
return _validate_no_slash(value)
|
||||
|
||||
|
||||
ENTITY_BASE_SCHEMA = Schema(
|
||||
{
|
||||
Optional(CONF_NAME): _validate_entity_name,
|
||||
|
||||
@@ -186,14 +186,14 @@ else:
|
||||
AREA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(Area),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
|
||||
}
|
||||
)
|
||||
|
||||
DEVICE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(Device),
|
||||
cv.Required(CONF_NAME): cv.string,
|
||||
cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)),
|
||||
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
|
||||
}
|
||||
)
|
||||
@@ -207,7 +207,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.valid_name,
|
||||
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)),
|
||||
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(
|
||||
cv.string_no_slash, cv.Length(max=120)
|
||||
),
|
||||
cv.Optional(CONF_AREA): validate_area_config,
|
||||
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
|
||||
cv.Required(CONF_BUILD_PATH): cv.string,
|
||||
|
||||
@@ -144,10 +144,7 @@
|
||||
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#define USE_ONLINE_IMAGE_JPEG_SUPPORT
|
||||
#define USE_OTA
|
||||
#define USE_OTA_MD5
|
||||
#define USE_OTA_PASSWORD
|
||||
#define USE_OTA_SHA256
|
||||
#define ALLOW_OTA_DOWNGRADE_MD5
|
||||
#define USE_OTA_STATE_LISTENER
|
||||
#define USE_OTA_VERSION 2
|
||||
#define USE_TIME_TIMEZONE
|
||||
@@ -221,7 +218,7 @@
|
||||
#define USB_HOST_MAX_REQUESTS 16
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 5)
|
||||
#define USE_ETHERNET
|
||||
#define USE_ETHERNET_KSZ8081
|
||||
#define USE_ETHERNET_MANUAL_IP
|
||||
|
||||
@@ -99,6 +99,8 @@ class EntityBase {
|
||||
return this->device_->get_device_id();
|
||||
}
|
||||
void set_device(Device *device) { this->device_ = device; }
|
||||
// Get the device this entity belongs to (nullptr if main device)
|
||||
Device *get_device() const { return this->device_; }
|
||||
#endif
|
||||
|
||||
// Check if this entity has state
|
||||
|
||||
@@ -3,20 +3,12 @@
|
||||
#include <cstdint>
|
||||
#include "gpio.h"
|
||||
|
||||
#if defined(USE_ESP32_FRAMEWORK_ESP_IDF)
|
||||
#if defined(USE_ESP32)
|
||||
#include <esp_attr.h>
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
#elif defined(USE_ESP32_FRAMEWORK_ARDUINO)
|
||||
|
||||
#include <esp_attr.h>
|
||||
|
||||
#ifndef PROGMEM
|
||||
#define PROGMEM
|
||||
#endif
|
||||
|
||||
#elif defined(USE_ESP8266)
|
||||
|
||||
#include <c_types.h>
|
||||
|
||||
@@ -25,14 +25,8 @@ class HashBase {
|
||||
/// Retrieve the hash as bytes
|
||||
void get_bytes(uint8_t *output) { memcpy(output, this->digest_, this->get_size()); }
|
||||
|
||||
/// Retrieve the hash as hex characters
|
||||
void get_hex(char *output) {
|
||||
for (size_t i = 0; i < this->get_size(); i++) {
|
||||
uint8_t byte = this->digest_[i];
|
||||
output[i * 2] = format_hex_char(byte >> 4);
|
||||
output[i * 2 + 1] = format_hex_char(byte & 0x0F);
|
||||
}
|
||||
}
|
||||
/// Retrieve the hash as hex characters. Output buffer must hold get_size() * 2 + 1 bytes.
|
||||
void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); }
|
||||
|
||||
/// Compare the hash against a provided byte-encoded hash
|
||||
bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; }
|
||||
|
||||
@@ -286,43 +286,60 @@ std::string format_mac_address_pretty(const uint8_t *mac) {
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string format_hex(const uint8_t *data, size_t length) {
|
||||
std::string ret;
|
||||
ret.resize(length * 2);
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
ret[2 * i] = format_hex_char(data[i] >> 4);
|
||||
ret[2 * i + 1] = format_hex_char(data[i] & 0x0F);
|
||||
// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase
|
||||
static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator,
|
||||
char base) {
|
||||
if (length == 0) {
|
||||
buffer[0] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
// With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator)
|
||||
// Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator)
|
||||
uint8_t stride = separator ? 3 : 2;
|
||||
size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride);
|
||||
if (max_bytes == 0) {
|
||||
buffer[0] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
|
||||
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
|
||||
size_t max_bytes = (buffer_size - 1) / 2;
|
||||
if (length > max_bytes) {
|
||||
length = max_bytes;
|
||||
}
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
buffer[2 * i] = format_hex_char(data[i] >> 4);
|
||||
buffer[2 * i + 1] = format_hex_char(data[i] & 0x0F);
|
||||
size_t pos = i * stride;
|
||||
buffer[pos] = format_hex_char(data[i] >> 4, base);
|
||||
buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base);
|
||||
if (separator && i < length - 1) {
|
||||
buffer[pos + 2] = separator;
|
||||
}
|
||||
}
|
||||
buffer[length * 2] = '\0';
|
||||
buffer[length * stride - (separator ? 1 : 0)] = '\0';
|
||||
return buffer;
|
||||
}
|
||||
|
||||
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) {
|
||||
return format_hex_internal(buffer, buffer_size, data, length, 0, 'a');
|
||||
}
|
||||
|
||||
std::string format_hex(const uint8_t *data, size_t length) {
|
||||
std::string ret;
|
||||
ret.resize(length * 2);
|
||||
format_hex_to(&ret[0], length * 2 + 1, data, length);
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
|
||||
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) {
|
||||
return format_hex_internal(buffer, buffer_size, data, length, separator, 'A');
|
||||
}
|
||||
|
||||
// Shared implementation for uint8_t and string hex formatting
|
||||
static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
|
||||
ret.resize(multiple * length - (separator ? 1 : 0));
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
ret[multiple * i] = format_hex_pretty_char(data[i] >> 4);
|
||||
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||
if (separator && i != length - 1)
|
||||
ret[multiple * i + 2] = separator;
|
||||
}
|
||||
size_t hex_len = separator ? (length * 3 - 1) : (length * 2);
|
||||
ret.resize(hex_len);
|
||||
format_hex_pretty_to(&ret[0], hex_len + 1, data, length, separator);
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
return ret;
|
||||
|
||||
@@ -691,12 +691,14 @@ constexpr uint8_t parse_hex_char(char c) {
|
||||
return 255;
|
||||
}
|
||||
|
||||
/// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase)
|
||||
inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; }
|
||||
|
||||
/// Convert a nibble (0-15) to lowercase hex char
|
||||
inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; }
|
||||
inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); }
|
||||
|
||||
/// Convert a nibble (0-15) to uppercase hex char (used for pretty printing)
|
||||
/// This always uses uppercase (A-F) for pretty/human-readable output
|
||||
inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
|
||||
inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); }
|
||||
|
||||
/// Write int8 value to buffer without modulo operations.
|
||||
/// Buffer must have at least 4 bytes free. Returns pointer past last char written.
|
||||
@@ -722,28 +724,6 @@ inline char *int8_to_str(char *buf, int8_t val) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase)
|
||||
inline void format_mac_addr_upper(const uint8_t *mac, char *output) {
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
uint8_t byte = mac[i];
|
||||
output[i * 3] = format_hex_pretty_char(byte >> 4);
|
||||
output[i * 3 + 1] = format_hex_pretty_char(byte & 0x0F);
|
||||
if (i < 5)
|
||||
output[i * 3 + 2] = ':';
|
||||
}
|
||||
output[17] = '\0';
|
||||
}
|
||||
|
||||
/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators)
|
||||
inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) {
|
||||
for (size_t i = 0; i < 6; i++) {
|
||||
uint8_t byte = mac[i];
|
||||
output[i * 2] = format_hex_char(byte >> 4);
|
||||
output[i * 2 + 1] = format_hex_char(byte & 0x0F);
|
||||
}
|
||||
output[12] = '\0';
|
||||
}
|
||||
|
||||
/// Format byte array as lowercase hex to buffer (base implementation).
|
||||
char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length);
|
||||
|
||||
@@ -762,6 +742,49 @@ inline char *format_hex_to(char (&buffer)[N], T val) {
|
||||
return format_hex_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
|
||||
}
|
||||
|
||||
/// Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1
|
||||
constexpr size_t format_hex_size(size_t byte_count) { return byte_count * 2 + 1; }
|
||||
|
||||
/// Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0"
|
||||
constexpr size_t format_hex_pretty_size(size_t byte_count) { return byte_count * 3; }
|
||||
|
||||
/** Format byte array as uppercase hex to buffer (base implementation).
|
||||
*
|
||||
* @param buffer Output buffer to write to.
|
||||
* @param buffer_size Size of the output buffer.
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @param separator Character to use between hex bytes, or '\0' for no separator.
|
||||
* @return Pointer to buffer.
|
||||
*
|
||||
* Buffer size needed: length * 3 with separator (for "XX:XX:XX\0"), length * 2 + 1 without.
|
||||
*/
|
||||
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator = ':');
|
||||
|
||||
/// Format byte array as uppercase hex with separator to buffer. Automatically deduces buffer size.
|
||||
template<size_t N>
|
||||
inline char *format_hex_pretty_to(char (&buffer)[N], const uint8_t *data, size_t length, char separator = ':') {
|
||||
static_assert(N >= 3, "Buffer must hold at least one hex byte");
|
||||
return format_hex_pretty_to(buffer, N, data, length, separator);
|
||||
}
|
||||
|
||||
/// MAC address size in bytes
|
||||
static constexpr size_t MAC_ADDRESS_SIZE = 6;
|
||||
/// Buffer size for MAC address with separators: "XX:XX:XX:XX:XX:XX\0"
|
||||
static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = format_hex_pretty_size(MAC_ADDRESS_SIZE);
|
||||
/// Buffer size for MAC address without separators: "XXXXXXXXXXXX\0"
|
||||
static constexpr size_t MAC_ADDRESS_BUFFER_SIZE = MAC_ADDRESS_SIZE * 2 + 1;
|
||||
|
||||
/// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
|
||||
inline void format_mac_addr_upper(const uint8_t *mac, char *output) {
|
||||
format_hex_pretty_to(output, MAC_ADDRESS_PRETTY_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE, ':');
|
||||
}
|
||||
|
||||
/// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators)
|
||||
inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) {
|
||||
format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE);
|
||||
}
|
||||
|
||||
/// Format the six-byte array \p mac into a MAC address.
|
||||
std::string format_mac_address_pretty(const uint8_t mac[6]);
|
||||
/// Format the byte array \p data of length \p len in lowercased hex.
|
||||
@@ -1217,12 +1240,6 @@ class HighFrequencyLoopRequester {
|
||||
/// Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
|
||||
void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter)
|
||||
|
||||
/// Buffer size for MAC address in lowercase hex notation (12 hex chars + null terminator)
|
||||
constexpr size_t MAC_ADDRESS_BUFFER_SIZE = 13;
|
||||
|
||||
/// Buffer size for MAC address in colon-separated uppercase hex notation (17 chars + null terminator)
|
||||
constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = 18;
|
||||
|
||||
/// Get the device MAC address as a string, in lowercase hex notation.
|
||||
std::string get_mac_address();
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32)
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Lock-free queue for single-producer single-consumer scenarios.
|
||||
@@ -95,7 +95,7 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
|
||||
}
|
||||
|
||||
protected:
|
||||
T *buffer_[SIZE];
|
||||
T *buffer_[SIZE]{};
|
||||
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
|
||||
std::atomic<uint16_t> dropped_count_; // 65535 max - more than enough for drop tracking
|
||||
// Atomic: written by consumer (pop), read by producer (push) to check if full
|
||||
@@ -106,6 +106,7 @@ template<class T, uint8_t SIZE> class LockFreeQueue {
|
||||
std::atomic<uint8_t> tail_;
|
||||
};
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// Extended queue with task notification support
|
||||
template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQueue<T, SIZE> {
|
||||
public:
|
||||
@@ -140,7 +141,6 @@ template<class T, uint8_t SIZE> class NotifyingLockFreeQueue : public LockFreeQu
|
||||
private:
|
||||
TaskHandle_t task_to_notify_;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // defined(USE_ESP32)
|
||||
|
||||
@@ -103,14 +103,11 @@ def storage_should_clean(old: StorageJSON | None, new: StorageJSON) -> bool:
|
||||
|
||||
|
||||
def storage_should_update_cmake_cache(old: StorageJSON, new: StorageJSON) -> bool:
|
||||
if (
|
||||
# ESP32 uses CMake for both Arduino and ESP-IDF frameworks
|
||||
return (
|
||||
old.loaded_integrations != new.loaded_integrations
|
||||
or old.loaded_platforms != new.loaded_platforms
|
||||
) and new.core_platform == PLATFORM_ESP32:
|
||||
from esphome.components.esp32 import FRAMEWORK_ESP_IDF
|
||||
|
||||
return new.framework == FRAMEWORK_ESP_IDF
|
||||
return False
|
||||
) and new.core_platform == PLATFORM_ESP32
|
||||
|
||||
|
||||
def update_storage_json() -> None:
|
||||
|
||||
@@ -133,9 +133,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.2/esp32-3.3.2.zip
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.5/esp32-3.3.5.zip
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
lib_deps =
|
||||
@@ -170,9 +170,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.35/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.1/esp-idf-v5.5.1.zip
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.2/esp-idf-v5.5.2.tar.xz
|
||||
|
||||
framework = espidf
|
||||
lib_deps =
|
||||
|
||||
@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==43.9.0
|
||||
aioesphomeapi==43.9.1
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.17 # dashboard_import
|
||||
@@ -22,7 +22,7 @@ pillow==11.3.0
|
||||
cairosvg==2.8.2
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
bleak==2.1.0
|
||||
bleak==2.1.1
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.0
|
||||
|
||||
@@ -374,20 +374,16 @@ def create_field_type_info(
|
||||
# Traditional fixed array approach with copy
|
||||
return FixedArrayBytesType(field, fixed_size)
|
||||
|
||||
# Check for pointer_to_buffer option on string fields
|
||||
if field.type == 9:
|
||||
has_pointer_to_buffer = get_field_opt(field, pb.pointer_to_buffer, False)
|
||||
|
||||
if has_pointer_to_buffer:
|
||||
# Zero-copy pointer approach for strings
|
||||
return PointerToBytesBufferType(field, None)
|
||||
|
||||
# Special handling for bytes fields
|
||||
if field.type == 12:
|
||||
return BytesType(field, needs_decode, needs_encode)
|
||||
|
||||
# Special handling for string fields
|
||||
if field.type == 9:
|
||||
# For SOURCE_CLIENT only messages (decode but no encode), use StringRef
|
||||
# for zero-copy access to the receive buffer
|
||||
if needs_decode and not needs_encode:
|
||||
return PointerToStringBufferType(field, None)
|
||||
return StringType(field, needs_decode, needs_encode)
|
||||
|
||||
validate_field_type(field.type, field.name)
|
||||
@@ -840,8 +836,8 @@ class BytesType(TypeInfo):
|
||||
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical bytes
|
||||
|
||||
|
||||
class PointerToBytesBufferType(TypeInfo):
|
||||
"""Type for bytes fields that use pointer_to_buffer option for zero-copy."""
|
||||
class PointerToBufferTypeBase(TypeInfo):
|
||||
"""Base class for pointer_to_buffer types (bytes and strings) for zero-copy decoding."""
|
||||
|
||||
@classmethod
|
||||
def can_use_dump_field(cls) -> bool:
|
||||
@@ -851,29 +847,34 @@ class PointerToBytesBufferType(TypeInfo):
|
||||
self, field: descriptor.FieldDescriptorProto, size: int | None = None
|
||||
) -> None:
|
||||
super().__init__(field)
|
||||
# Size is not used for pointer_to_buffer - we always use size_t for length
|
||||
self.array_size = 0
|
||||
|
||||
@property
|
||||
def cpp_type(self) -> str:
|
||||
return "const uint8_t*"
|
||||
def decode_length(self) -> str | None:
|
||||
# This is handled in decode_length_content
|
||||
return None
|
||||
|
||||
@property
|
||||
def default_value(self) -> str:
|
||||
return "nullptr"
|
||||
def wire_type(self) -> WireType:
|
||||
"""Get the wire type for this field."""
|
||||
return WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||
|
||||
@property
|
||||
def reference_type(self) -> str:
|
||||
return "const uint8_t*"
|
||||
def get_estimated_size(self) -> int:
|
||||
# field ID + length varint + typical data (assume small for pointer fields)
|
||||
return self.calculate_field_id_size() + 2 + 16
|
||||
|
||||
@property
|
||||
def const_reference_type(self) -> str:
|
||||
return "const uint8_t*"
|
||||
|
||||
class PointerToBytesBufferType(PointerToBufferTypeBase):
|
||||
"""Type for bytes fields that use pointer_to_buffer option for zero-copy."""
|
||||
|
||||
cpp_type = "const uint8_t*"
|
||||
default_value = "nullptr"
|
||||
reference_type = "const uint8_t*"
|
||||
const_reference_type = "const uint8_t*"
|
||||
|
||||
@property
|
||||
def public_content(self) -> list[str]:
|
||||
# Use uint16_t for length - max packet size is well below 65535
|
||||
# Add pointer and length fields
|
||||
return [
|
||||
f"const uint8_t* {self.field_name}{{nullptr}};",
|
||||
f"uint16_t {self.field_name}_len{{0}};",
|
||||
@@ -885,24 +886,12 @@ class PointerToBytesBufferType(TypeInfo):
|
||||
|
||||
@property
|
||||
def decode_length_content(self) -> str | None:
|
||||
# Decode directly stores the pointer to avoid allocation
|
||||
return f"""case {self.number}: {{
|
||||
// Use raw data directly to avoid allocation
|
||||
this->{self.field_name} = value.data();
|
||||
this->{self.field_name}_len = value.size();
|
||||
break;
|
||||
}}"""
|
||||
|
||||
@property
|
||||
def decode_length(self) -> str | None:
|
||||
# This is handled in decode_length_content
|
||||
return None
|
||||
|
||||
@property
|
||||
def wire_type(self) -> WireType:
|
||||
"""Get the wire type for this bytes field."""
|
||||
return WireType.LENGTH_DELIMITED # Uses wire type 2
|
||||
|
||||
def dump(self, name: str) -> str:
|
||||
return (
|
||||
f"format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len)"
|
||||
@@ -910,7 +899,6 @@ class PointerToBytesBufferType(TypeInfo):
|
||||
|
||||
@property
|
||||
def dump_content(self) -> str:
|
||||
# Custom dump that doesn't use dump_field template
|
||||
return (
|
||||
f'out.append(" {self.name}: ");\n'
|
||||
+ f"out.append({self.dump(self.field_name)});\n"
|
||||
@@ -918,11 +906,48 @@ class PointerToBytesBufferType(TypeInfo):
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
return f"size.add_length({self.number}, this->{self.field_name}_len);"
|
||||
return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len);"
|
||||
|
||||
def get_estimated_size(self) -> int:
|
||||
# field ID + length varint + typical data (assume small for pointer fields)
|
||||
return self.calculate_field_id_size() + 2 + 16
|
||||
|
||||
class PointerToStringBufferType(PointerToBufferTypeBase):
|
||||
"""Type for string fields that use pointer_to_buffer option for zero-copy.
|
||||
|
||||
Uses StringRef instead of separate pointer and length fields.
|
||||
"""
|
||||
|
||||
cpp_type = "StringRef"
|
||||
default_value = ""
|
||||
reference_type = "StringRef &"
|
||||
const_reference_type = "const StringRef &"
|
||||
|
||||
@property
|
||||
def public_content(self) -> list[str]:
|
||||
return [f"StringRef {self.field_name}{{}};"]
|
||||
|
||||
@property
|
||||
def encode_content(self) -> str:
|
||||
return f"buffer.encode_string({self.number}, this->{self.field_name});"
|
||||
|
||||
@property
|
||||
def decode_length_content(self) -> str | None:
|
||||
return f"""case {self.number}: {{
|
||||
this->{self.field_name} = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}}"""
|
||||
|
||||
def dump(self, name: str) -> str:
|
||||
return f'out.append("\'").append(this->{self.field_name}.c_str(), this->{self.field_name}.size()).append("\'");'
|
||||
|
||||
@property
|
||||
def dump_content(self) -> str:
|
||||
return (
|
||||
f'out.append(" {self.name}: ");\n'
|
||||
+ f"{self.dump(self.field_name)}\n"
|
||||
+ 'out.append("\\n");'
|
||||
)
|
||||
|
||||
def get_size_calculation(self, name: str, force: bool = False) -> str:
|
||||
return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}.size());"
|
||||
|
||||
|
||||
class FixedArrayBytesType(TypeInfo):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user