mirror of
https://github.com/esphome/esphome.git
synced 2026-07-10 17:05:36 +00:00
Merge remote-tracking branch 'origin/api-max-data-length-force' into integration
This commit is contained in:
@@ -129,11 +129,12 @@ message HelloResponse {
|
||||
|
||||
// A string identifying the server (ESP); like client info this may be empty
|
||||
// and only exists for debugging/logging purposes.
|
||||
// For example "ESPHome v1.10.0 on ESP8266"
|
||||
string server_info = 3;
|
||||
// Currently set to ESPHOME_VERSION string literal.
|
||||
string server_info = 3 [(max_data_length) = 32, (force) = true];
|
||||
|
||||
// The name of the server (App.get_name())
|
||||
string name = 4;
|
||||
// The name of the server (App.get_name() - device hostname)
|
||||
// max_data_length matches ESPHOME_DEVICE_NAME_MAX_LEN (validated by validate_hostname)
|
||||
string name = 4 [(max_data_length) = 31, (force) = true];
|
||||
}
|
||||
|
||||
// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported.
|
||||
@@ -196,12 +197,14 @@ message DeviceInfoRequest {
|
||||
|
||||
message AreaInfo {
|
||||
uint32 area_id = 1;
|
||||
string name = 2;
|
||||
// max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA
|
||||
string name = 2 [(max_data_length) = 120, (force) = true];
|
||||
}
|
||||
|
||||
message DeviceInfo {
|
||||
uint32 device_id = 1;
|
||||
string name = 2;
|
||||
// max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via DEVICE_SCHEMA
|
||||
string name = 2 [(max_data_length) = 120, (force) = true];
|
||||
uint32 area_id = 3;
|
||||
}
|
||||
|
||||
@@ -216,6 +219,16 @@ message SerialProxyInfo {
|
||||
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
|
||||
}
|
||||
|
||||
// DeviceInfoResponse max_data_length values:
|
||||
// name = 31 (ESPHOME_DEVICE_NAME_MAX_LEN, validated by validate_hostname)
|
||||
// friendly_name = 120 (core/config.FRIENDLY_NAME_MAX_LEN)
|
||||
// mac_address/bluetooth_mac_address = 17 (MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1, constexpr)
|
||||
// esphome_version = 32 (ESPHOME_VERSION string literal)
|
||||
// compilation_time = 25 (Application::BUILD_TIME_STR_SIZE - 1, constexpr)
|
||||
// manufacturer = 20 (longest hardcoded literal: "Nordic Semiconductor")
|
||||
// model = 127 (core/config.BOARD_MAX_LENGTH, validated in platform schemas)
|
||||
// project_name/project_version = 127 (core/config.PROJECT_MAX_LENGTH)
|
||||
// suggested_area = 120 (core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA)
|
||||
message DeviceInfoResponse {
|
||||
option (id) = 10;
|
||||
option (source) = SOURCE_SERVER;
|
||||
@@ -224,28 +237,30 @@ message DeviceInfoResponse {
|
||||
// with older ESPHome versions that still send this field.
|
||||
bool uses_password = 1 [deprecated = true];
|
||||
|
||||
// The name of the node, given by "App.set_name()"
|
||||
string name = 2;
|
||||
// The name of the node, given by "App.set_name()" - device hostname
|
||||
string name = 2 [(max_data_length) = 31, (force) = true];
|
||||
|
||||
// The mac address of the device. For example "AC:BC:32:89:0E:A9"
|
||||
string mac_address = 3;
|
||||
string mac_address = 3 [(max_data_length) = 17, (force) = true];
|
||||
|
||||
// A string describing the ESPHome version. For example "1.10.0"
|
||||
string esphome_version = 4;
|
||||
string esphome_version = 4 [(max_data_length) = 32, (force) = true];
|
||||
|
||||
// A string describing the date of compilation, this is generated by the compiler
|
||||
// and therefore may not be in the same format all the time.
|
||||
// If the user isn't using ESPHome, this will also not be set.
|
||||
string compilation_time = 5;
|
||||
string compilation_time = 5 [(max_data_length) = 25, (force) = true];
|
||||
|
||||
// The model of the board. For example NodeMCU
|
||||
string model = 6;
|
||||
// max_data_length matches core/config.BOARD_MAX_LENGTH (validated in platform schemas)
|
||||
string model = 6 [(max_data_length) = 127, (force) = true];
|
||||
|
||||
bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"];
|
||||
|
||||
// The esphome project details if set
|
||||
string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
// max_data_length matches core/config.PROJECT_MAX_LENGTH
|
||||
string project_name = 8 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
string project_version = 9 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
|
||||
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
|
||||
|
||||
@@ -253,18 +268,18 @@ message DeviceInfoResponse {
|
||||
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
|
||||
string manufacturer = 12;
|
||||
string manufacturer = 12 [(max_data_length) = 20, (force) = true];
|
||||
|
||||
string friendly_name = 13;
|
||||
string friendly_name = 13 [(max_data_length) = 120, (force) = true];
|
||||
|
||||
// Deprecated in API version 1.10
|
||||
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
|
||||
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
||||
string suggested_area = 16 [(max_data_length) = 120, (force) = true, (field_ifdef) = "USE_AREAS"];
|
||||
|
||||
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
|
||||
string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
string bluetooth_mac_address = 18 [(max_data_length) = 17, (force) = true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
|
||||
// Supports receiving and saving api encryption key
|
||||
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
|
||||
|
||||
@@ -72,6 +72,14 @@ static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
|
||||
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
|
||||
// Cross-validate C++ constants against proto max_data_length annotations in api.proto
|
||||
static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1 == 17,
|
||||
"Update max_data_length for mac_address/bluetooth_mac_address in api.proto");
|
||||
static_assert(Application::BUILD_TIME_STR_SIZE - 1 == 25, "Update max_data_length for compilation_time in api.proto");
|
||||
static_assert(sizeof(ESPHOME_VERSION) - 1 <= 32, "Update max_data_length for esphome_version in api.proto");
|
||||
static_assert(ESPHOME_DEVICE_NAME_MAX_LEN <= 31, "Update max_data_length for name in api.proto");
|
||||
static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto");
|
||||
|
||||
static const char *const TAG = "api.connection";
|
||||
#ifdef USE_CAMERA
|
||||
static const int CAMERA_STOP_STREAM = 5000;
|
||||
@@ -1716,6 +1724,7 @@ bool APIConnection::send_device_info_response_() {
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
|
||||
resp.manufacturer = MANUFACTURER;
|
||||
#endif
|
||||
static_assert(sizeof(ESPHOME_MANUFACTURER) - 1 <= 20, "Update max_data_length for manufacturer in api.proto");
|
||||
#undef ESPHOME_MANUFACTURER
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
|
||||
@@ -35,29 +35,29 @@ uint8_t *HelloResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->api_version_major);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->api_version_minor);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->server_info);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->name);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->server_info);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, this->name);
|
||||
return pos;
|
||||
}
|
||||
uint32_t HelloResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->api_version_major);
|
||||
size += ProtoSize::calc_uint32(1, this->api_version_minor);
|
||||
size += ProtoSize::calc_length(1, this->server_info.size());
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += 2 + this->server_info.size();
|
||||
size += 2 + this->name.size();
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_AREAS
|
||||
uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->area_id);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, this->name);
|
||||
return pos;
|
||||
}
|
||||
uint32_t AreaInfo::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->area_id);
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += 2 + this->name.size();
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
@@ -65,14 +65,14 @@ uint32_t AreaInfo::calculate_size() const {
|
||||
uint8_t *DeviceInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->device_id);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, this->name);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->area_id);
|
||||
return pos;
|
||||
}
|
||||
uint32_t DeviceInfo::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->device_id);
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += 2 + this->name.size();
|
||||
size += ProtoSize::calc_uint32(1, this->area_id);
|
||||
return size;
|
||||
}
|
||||
@@ -93,19 +93,19 @@ uint32_t SerialProxyInfo::calculate_size() const {
|
||||
#endif
|
||||
uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, this->name);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->mac_address);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->esphome_version);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->compilation_time);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, this->model);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 18, this->name);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->mac_address);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 34, this->esphome_version);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 42, this->compilation_time);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 50, this->model);
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 7, this->has_deep_sleep);
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 8, this->project_name);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 66, this->project_name);
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 9, this->project_version);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 74, this->project_version);
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->webserver_port);
|
||||
@@ -113,16 +113,16 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 15, this->bluetooth_proxy_feature_flags);
|
||||
#endif
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 12, this->manufacturer);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 13, this->friendly_name);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 98, this->manufacturer);
|
||||
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 106, this->friendly_name);
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 17, this->voice_assistant_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 16, this->suggested_area);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 16, this->suggested_area, true);
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 18, this->bluetooth_mac_address);
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 18, this->bluetooth_mac_address, true);
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 19, this->api_encryption_supported);
|
||||
@@ -155,19 +155,19 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
|
||||
}
|
||||
uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += ProtoSize::calc_length(1, this->mac_address.size());
|
||||
size += ProtoSize::calc_length(1, this->esphome_version.size());
|
||||
size += ProtoSize::calc_length(1, this->compilation_time.size());
|
||||
size += ProtoSize::calc_length(1, this->model.size());
|
||||
size += 2 + this->name.size();
|
||||
size += 2 + this->mac_address.size();
|
||||
size += 2 + this->esphome_version.size();
|
||||
size += 2 + this->compilation_time.size();
|
||||
size += 2 + this->model.size();
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
size += ProtoSize::calc_bool(1, this->has_deep_sleep);
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
size += ProtoSize::calc_length(1, this->project_name.size());
|
||||
size += 2 + this->project_name.size();
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
size += ProtoSize::calc_length(1, this->project_version.size());
|
||||
size += 2 + this->project_version.size();
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
size += ProtoSize::calc_uint32(1, this->webserver_port);
|
||||
@@ -175,16 +175,16 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
size += ProtoSize::calc_uint32(1, this->bluetooth_proxy_feature_flags);
|
||||
#endif
|
||||
size += ProtoSize::calc_length(1, this->manufacturer.size());
|
||||
size += ProtoSize::calc_length(1, this->friendly_name.size());
|
||||
size += 2 + this->manufacturer.size();
|
||||
size += 2 + this->friendly_name.size();
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
size += ProtoSize::calc_uint32(2, this->voice_assistant_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
size += ProtoSize::calc_length(2, this->suggested_area.size());
|
||||
size += 3 + this->suggested_area.size();
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
size += ProtoSize::calc_length(2, this->bluetooth_mac_address.size());
|
||||
size += 3 + this->bluetooth_mac_address.size();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
size += ProtoSize::calc_bool(2, this->api_encryption_supported);
|
||||
|
||||
@@ -44,6 +44,7 @@ from esphome.const import (
|
||||
__version__,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.coroutine import CoroPriority, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
|
||||
@@ -1403,7 +1404,9 @@ CONF_PARTITIONS = "partitions"
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_BOARD): cv.string_strict,
|
||||
cv.Optional(CONF_BOARD): cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
|
||||
),
|
||||
cv.Optional(CONF_CPU_FREQUENCY): cv.one_of(
|
||||
*FULL_CPU_FREQUENCIES, upper=True
|
||||
),
|
||||
|
||||
@@ -20,6 +20,7 @@ from esphome.const import (
|
||||
ThreadModel,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -203,7 +204,9 @@ BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"]
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Required(CONF_BOARD): cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
|
||||
),
|
||||
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
|
||||
cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
|
||||
cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean,
|
||||
|
||||
@@ -23,6 +23,7 @@ from esphome.const import (
|
||||
__version__,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.storage_json import StorageJSON
|
||||
|
||||
from . import gpio # noqa
|
||||
@@ -266,7 +267,9 @@ CONFIG_SCHEMA = cv.All(_notify_old_style)
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LTComponent),
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Required(CONF_BOARD): cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
|
||||
),
|
||||
cv.Optional(CONF_FAMILY): cv.one_of(*FAMILIES, upper=True),
|
||||
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
|
||||
},
|
||||
|
||||
@@ -46,6 +46,7 @@ from esphome.const import (
|
||||
ThreadModel,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
import esphome.final_validate as fv
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
@@ -145,7 +146,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
set_core_data,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Required(CONF_BOARD): cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
|
||||
),
|
||||
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
|
||||
cv.Optional(CONF_DFU): cv.Schema(
|
||||
{
|
||||
|
||||
@@ -190,7 +190,7 @@ validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
|
||||
validate_unit_of_measurement = cv.All(
|
||||
cv.string_strict,
|
||||
# Keep in sync with max_data_length in api.proto
|
||||
cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH),
|
||||
cv.ByteLength(max=UNIT_OF_MEASUREMENT_MAX_LENGTH),
|
||||
)
|
||||
|
||||
_NUMBER_SCHEMA = (
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
ThreadModel,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import copy_file_if_changed, read_file, write_file_if_changed
|
||||
|
||||
from . import boards
|
||||
@@ -174,7 +175,9 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Required(CONF_BOARD): cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
|
||||
),
|
||||
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
|
||||
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="8388ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
|
||||
@@ -294,7 +294,7 @@ RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
|
||||
validate_unit_of_measurement = cv.All(
|
||||
cv.string_strict,
|
||||
# Keep in sync with max_data_length in api.proto
|
||||
cv.Length(max=UNIT_OF_MEASUREMENT_MAX_LENGTH),
|
||||
cv.ByteLength(max=UNIT_OF_MEASUREMENT_MAX_LENGTH),
|
||||
)
|
||||
validate_accuracy_decimals = cv.int_
|
||||
validate_icon = cv.icon
|
||||
|
||||
@@ -130,6 +130,26 @@ RequiredFieldInvalid = vol.RequiredFieldInvalid
|
||||
# the rest of the error path is relative to the root config path
|
||||
ROOT_CONFIG_PATH = object()
|
||||
|
||||
|
||||
def ByteLength(*, max: int):
|
||||
"""Validate that the UTF-8 byte length of a string does not exceed max.
|
||||
|
||||
Use instead of Length() when the limit must apply to encoded bytes,
|
||||
not characters (e.g. for protobuf length-varint constraints).
|
||||
"""
|
||||
|
||||
def validator(value):
|
||||
byte_len = len(str(value).encode("utf-8"))
|
||||
if byte_len > max:
|
||||
raise Invalid(
|
||||
f"String is too long ({byte_len} bytes, max {max}). "
|
||||
f"Multibyte characters count as multiple bytes."
|
||||
)
|
||||
return value
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
RESERVED_IDS = [
|
||||
# C++ keywords https://en.cppreference.com/w/cpp/keyword
|
||||
"alarm",
|
||||
@@ -411,9 +431,10 @@ def icon(value):
|
||||
raise Invalid(
|
||||
'Icons must match the format "[icon pack]:[icon]", e.g. "mdi:home-assistant"'
|
||||
)
|
||||
if len(value) > ICON_MAX_LENGTH:
|
||||
byte_len = len(value.encode("utf-8"))
|
||||
if byte_len > ICON_MAX_LENGTH:
|
||||
raise Invalid(
|
||||
f"Icon string is too long ({len(value)} chars, max {ICON_MAX_LENGTH}). "
|
||||
f"Icon string is too long ({byte_len} bytes, max {ICON_MAX_LENGTH}). "
|
||||
"Icons are stored in PROGMEM with a 64-byte buffer limit."
|
||||
)
|
||||
return value
|
||||
@@ -2067,11 +2088,12 @@ def _validate_entity_name(value):
|
||||
"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:
|
||||
# Validate byte length for web server URL and proto encoding compatibility
|
||||
byte_len = len(value.encode("utf-8"))
|
||||
if byte_len > NAME_MAX_LENGTH:
|
||||
raise Invalid(
|
||||
f"Name is too long ({len(value)} chars). "
|
||||
f"Maximum length is {NAME_MAX_LENGTH} characters."
|
||||
f"Name is too long ({byte_len} bytes). "
|
||||
f"Maximum length is {NAME_MAX_LENGTH} bytes."
|
||||
)
|
||||
# Validate no '/' in name for web server URL compatibility
|
||||
value = _validate_no_slash(value)
|
||||
|
||||
+15
-5
@@ -236,11 +236,17 @@ ICON_MAX_LENGTH = 63
|
||||
# Max unit of measurement string length
|
||||
UNIT_OF_MEASUREMENT_MAX_LENGTH = 63
|
||||
|
||||
# Max project name/version string length (must fit in single-byte varint for proto encoding)
|
||||
PROJECT_MAX_LENGTH = 127
|
||||
|
||||
# Max board/model string length (must fit in single-byte varint for proto encoding)
|
||||
BOARD_MAX_LENGTH = 127
|
||||
|
||||
AREA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(Area),
|
||||
cv.Required(CONF_NAME): cv.All(
|
||||
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
|
||||
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -249,7 +255,7 @@ DEVICE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(Device),
|
||||
cv.Required(CONF_NAME): cv.All(
|
||||
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
|
||||
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
|
||||
),
|
||||
cv.Optional(CONF_AREA_ID): cv.use_id(Area),
|
||||
}
|
||||
@@ -266,7 +272,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Required(CONF_NAME): cv.valid_name,
|
||||
# Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h
|
||||
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(
|
||||
cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN)
|
||||
cv.string_no_slash, cv.ByteLength(max=FRIENDLY_NAME_MAX_LEN)
|
||||
),
|
||||
cv.Optional(CONF_AREA): validate_area_config,
|
||||
cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),
|
||||
@@ -306,9 +312,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_PROJECT): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.All(
|
||||
cv.string_strict, valid_project_name
|
||||
cv.string_strict,
|
||||
valid_project_name,
|
||||
cv.ByteLength(max=PROJECT_MAX_LENGTH),
|
||||
),
|
||||
cv.Required(CONF_VERSION): cv.All(
|
||||
cv.string_strict, cv.ByteLength(max=PROJECT_MAX_LENGTH)
|
||||
),
|
||||
cv.Required(CONF_VERSION): cv.string_strict,
|
||||
cv.Optional(CONF_ON_UPDATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
|
||||
@@ -193,9 +193,10 @@ def _register_string(
|
||||
|
||||
def register_device_class(value: str) -> int:
|
||||
"""Register a device_class string and return its 1-based index."""
|
||||
if value and len(value) > DEVICE_CLASS_MAX_LENGTH:
|
||||
byte_len = len(value.encode("utf-8")) if value else 0
|
||||
if byte_len > DEVICE_CLASS_MAX_LENGTH:
|
||||
raise ValueError(
|
||||
f"Device class string too long ({len(value)} chars, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'"
|
||||
f"Device class string too long ({byte_len} bytes, max {DEVICE_CLASS_MAX_LENGTH}): '{value}'"
|
||||
)
|
||||
return _register_string(
|
||||
value, _get_pool().device_classes, _MAX_DEVICE_CLASSES, "device_class"
|
||||
@@ -204,9 +205,10 @@ def register_device_class(value: str) -> int:
|
||||
|
||||
def register_unit_of_measurement(value: str) -> int:
|
||||
"""Register a unit_of_measurement string and return its 1-based index."""
|
||||
if value and len(value) > UNIT_OF_MEASUREMENT_MAX_LENGTH:
|
||||
byte_len = len(value.encode("utf-8")) if value else 0
|
||||
if byte_len > UNIT_OF_MEASUREMENT_MAX_LENGTH:
|
||||
raise ValueError(
|
||||
f"Unit of measurement string too long ({len(value)} chars, "
|
||||
f"Unit of measurement string too long ({byte_len} bytes, "
|
||||
f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'"
|
||||
)
|
||||
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
|
||||
@@ -214,9 +216,10 @@ def register_unit_of_measurement(value: str) -> int:
|
||||
|
||||
def register_icon(value: str) -> int:
|
||||
"""Register an icon string and return its 1-based index."""
|
||||
if value and len(value) > ICON_MAX_LENGTH:
|
||||
byte_len = len(value.encode("utf-8")) if value else 0
|
||||
if byte_len > ICON_MAX_LENGTH:
|
||||
raise ValueError(
|
||||
f"Icon string too long ({len(value)} chars, max {ICON_MAX_LENGTH}): '{value}'"
|
||||
f"Icon string too long ({byte_len} bytes, max {ICON_MAX_LENGTH}): '{value}'"
|
||||
)
|
||||
return _register_string(value, _get_pool().icons, _MAX_ICONS, "icon")
|
||||
|
||||
|
||||
@@ -149,17 +149,36 @@ def test_icon__invalid():
|
||||
|
||||
|
||||
def test_icon__max_length():
|
||||
"""Test that icons exceeding 63 characters are rejected."""
|
||||
# Exactly 63 chars should pass
|
||||
max_icon = "mdi:" + "a" * 59 # 63 chars total
|
||||
"""Test that icons exceeding 63 bytes are rejected."""
|
||||
# Exactly 63 bytes should pass
|
||||
max_icon = "mdi:" + "a" * 59 # 63 bytes total
|
||||
assert config_validation.icon(max_icon) == max_icon
|
||||
|
||||
# 64 chars should fail
|
||||
too_long = "mdi:" + "a" * 60 # 64 chars total
|
||||
# 64 bytes should fail
|
||||
too_long = "mdi:" + "a" * 60 # 64 bytes total
|
||||
with pytest.raises(Invalid, match="Icon string is too long"):
|
||||
config_validation.icon(too_long)
|
||||
|
||||
|
||||
def test_byte_length() -> None:
|
||||
"""Test ByteLength validator checks UTF-8 byte length, not char count."""
|
||||
validator = config_validation.ByteLength(max=10) # pylint: disable=no-member
|
||||
|
||||
# ASCII: 10 chars = 10 bytes, should pass
|
||||
assert validator("a" * 10) == "a" * 10
|
||||
|
||||
# ASCII: 11 chars = 11 bytes, should fail
|
||||
with pytest.raises(Invalid, match="too long.*11 bytes.*max 10"):
|
||||
validator("a" * 11)
|
||||
|
||||
# Multibyte: 3 chars × 3 bytes = 9 bytes, should pass
|
||||
assert validator("温度传") == "温度传"
|
||||
|
||||
# Multibyte: 4 chars × 3 bytes = 12 bytes, should fail
|
||||
with pytest.raises(Invalid, match="too long.*12 bytes.*max 10"):
|
||||
validator("温度传感")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ("True", "YES", "on", "enAblE", True))
|
||||
def test_boolean__valid_true(value):
|
||||
assert config_validation.boolean(value) is True
|
||||
@@ -567,14 +586,23 @@ def test_validate_entity_name__slash_replaced_with_warning(
|
||||
|
||||
|
||||
def test_validate_entity_name__max_length() -> None:
|
||||
# 120 chars should pass
|
||||
# 120 bytes should pass
|
||||
assert config_validation._validate_entity_name("x" * 120) == "x" * 120
|
||||
|
||||
# 121 chars should fail
|
||||
with pytest.raises(Invalid, match="too long.*121 chars.*Maximum.*120"):
|
||||
# 121 bytes should fail
|
||||
with pytest.raises(Invalid, match="too long.*121 bytes.*Maximum.*120"):
|
||||
config_validation._validate_entity_name("x" * 121)
|
||||
|
||||
|
||||
def test_validate_entity_name__multibyte_byte_length() -> None:
|
||||
# 40 chars of 3-byte UTF-8 = 120 bytes, should pass
|
||||
assert config_validation._validate_entity_name("温" * 40) == "温" * 40
|
||||
|
||||
# 41 chars of 3-byte UTF-8 = 123 bytes, should fail (over 120 byte limit)
|
||||
with pytest.raises(Invalid, match="too long.*123 bytes.*Maximum.*120"):
|
||||
config_validation._validate_entity_name("温" * 41)
|
||||
|
||||
|
||||
def test_validate_entity_name__none_without_friendly_name() -> None:
|
||||
# When name is "None" and friendly_name is not set, it should fail
|
||||
CORE.friendly_name = None
|
||||
|
||||
Reference in New Issue
Block a user