[api] Add max_data_length proto option and optimize entity name/object_id (#15426)

This commit is contained in:
J. Nick Koston
2026-04-06 17:31:01 -10:00
committed by GitHub
parent b6ef1a58fb
commit 10b38e1588
10 changed files with 321 additions and 231 deletions
+93 -87
View File
@@ -308,6 +308,12 @@ enum EntityCategory {
ENTITY_CATEGORY_DIAGNOSTIC = 2;
}
// Entity field max_data_length values match Python validation constants:
// name/object_id = 120 (config_validation.NAME_MAX_LENGTH)
// icon = 63 (core/config.ICON_MAX_LENGTH)
// device_class = 47 (core/config.DEVICE_CLASS_MAX_LENGTH)
// unit_of_measurement = 63 (core/config.UNIT_OF_MEASUREMENT_MAX_LENGTH)
// ==================== BINARY SENSOR ====================
message ListEntitiesBinarySensorResponse {
option (id) = 12;
@@ -315,15 +321,15 @@ message ListEntitiesBinarySensorResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string device_class = 5;
string device_class = 5 [(max_data_length) = 47];
bool is_status_binary_sensor = 6;
bool disabled_by_default = 7;
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
@@ -349,17 +355,17 @@ message ListEntitiesCoverResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool assumed_state = 5;
bool supports_position = 6;
bool supports_tilt = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 11;
bool supports_stop = 12;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
@@ -433,9 +439,9 @@ message ListEntitiesFanResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool supports_oscillation = 5;
@@ -443,7 +449,7 @@ message ListEntitiesFanResponse {
bool supports_direction = 7;
int32 supported_speed_count = 8;
bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
@@ -521,9 +527,9 @@ message ListEntitiesLightResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
@@ -540,7 +546,7 @@ message ListEntitiesLightResponse {
float max_mireds = 10;
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 13;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 15;
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
}
@@ -626,16 +632,16 @@ message ListEntitiesSensorResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string unit_of_measurement = 6;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
string unit_of_measurement = 6 [(max_data_length) = 63];
int32 accuracy_decimals = 7;
bool force_update = 8;
string device_class = 9;
string device_class = 9 [(max_data_length) = 47];
SensorStateClass state_class = 10;
// Last reset type removed in 2021.9.0
// Deprecated in API version 1.5
@@ -666,16 +672,16 @@ message ListEntitiesSwitchResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool assumed_state = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
string device_class = 9;
string device_class = 9 [(max_data_length) = 47];
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message SwitchStateResponse {
@@ -708,15 +714,15 @@ message ListEntitiesTextSensorResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message TextSensorStateResponse {
@@ -971,12 +977,12 @@ message ListEntitiesCameraResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CAMERA";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool disabled_by_default = 5;
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1056,9 +1062,9 @@ message ListEntitiesClimateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5; // Deprecated: use feature_flags
@@ -1078,7 +1084,7 @@ message ListEntitiesClimateResponse {
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22; // Deprecated: use feature_flags
@@ -1167,10 +1173,10 @@ message ListEntitiesWaterHeaterResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
string name = 3 [(max_data_length) = 120, (force) = true];
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
@@ -1243,20 +1249,20 @@ message ListEntitiesNumberResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
float min_value = 6;
float max_value = 7;
float step = 8;
bool disabled_by_default = 9;
EntityCategory entity_category = 10;
string unit_of_measurement = 11;
string unit_of_measurement = 11 [(max_data_length) = 63];
NumberMode mode = 12;
string device_class = 13;
string device_class = 13 [(max_data_length) = 47];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
message NumberStateResponse {
@@ -1292,12 +1298,12 @@ message ListEntitiesSelectResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
@@ -1336,12 +1342,12 @@ message ListEntitiesSirenResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool supports_duration = 8;
@@ -1399,12 +1405,12 @@ message ListEntitiesLockResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
bool assumed_state = 8;
@@ -1448,15 +1454,15 @@ message ListEntitiesButtonResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BUTTON";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message ButtonCommandRequest {
@@ -1515,12 +1521,12 @@ message ListEntitiesMediaPlayerResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
@@ -2103,11 +2109,11 @@ message ListEntitiesAlarmControlPanelResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 supported_features = 8;
@@ -2150,11 +2156,11 @@ message ListEntitiesTextResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
@@ -2198,12 +2204,12 @@ message ListEntitiesDateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
@@ -2245,12 +2251,12 @@ message ListEntitiesTimeResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
@@ -2292,15 +2298,15 @@ message ListEntitiesEventResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
@@ -2323,15 +2329,15 @@ message ListEntitiesValveResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
bool assumed_state = 9;
bool supports_position = 10;
@@ -2378,12 +2384,12 @@ message ListEntitiesDateTimeResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
@@ -2421,15 +2427,15 @@ message ListEntitiesUpdateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message UpdateStateResponse {
@@ -2504,10 +2510,10 @@ message ListEntitiesInfraredResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_INFRARED";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
string name = 3 [(max_data_length) = 120, (force) = true];
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
+6
View File
@@ -102,4 +102,10 @@ extend google.protobuf.FieldOptions {
// and direct byte writes instead of varint branching, since the encoded varint
// is guaranteed to be 1 byte.
optional uint32 max_value = 50017;
// max_data_length: Maximum length of a string or bytes field.
// When max_data_length < 128, the code generator emits constant-size
// length varint calculations and direct byte writes, since the length
// varint is guaranteed to be 1 byte.
optional uint32 max_data_length = 50018;
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -359,6 +359,19 @@ class ProtoEncode {
std::memcpy(pos, data, len);
pos += len;
}
/// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128.
/// Tag must be a single-byte varint (< 128). Always encodes (no zero check).
static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag,
const StringRef &ref) {
#ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
#endif
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size());
std::memcpy(pos + 2, ref.c_str(), ref.size());
pos += 2 + ref.size();
}
/// Write a precomputed tag byte + 32-bit value in one operation.
static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint8_t tag, uint32_t value) {
+6 -1
View File
@@ -79,6 +79,7 @@ from esphome.const import (
DEVICE_CLASS_WIND_SPEED,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
@@ -186,7 +187,11 @@ NUMBER_OPERATION_OPTIONS = {
}
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
validate_unit_of_measurement = cv.string_strict
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),
)
_NUMBER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
+6 -1
View File
@@ -106,6 +106,7 @@ from esphome.const import (
ENTITY_CATEGORY_CONFIG,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
entity_duplicate_validator,
setup_device_class,
@@ -290,7 +291,11 @@ ClampFilter = sensor_ns.class_("ClampFilter", Filter)
RoundFilter = sensor_ns.class_("RoundFilter", Filter)
RoundMultipleFilter = sensor_ns.class_("RoundMultipleFilter", Filter)
validate_unit_of_measurement = cv.string_strict
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),
)
validate_accuracy_decimals = cv.int_
validate_icon = cv.icon
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
+3
View File
@@ -233,6 +233,9 @@ DEVICE_CLASS_MAX_LENGTH = 47
# Keep in sync with MAX_ICON_LENGTH in esphome/core/entity_base.h
ICON_MAX_LENGTH = 63
# Max unit of measurement string length
UNIT_OF_MEASUREMENT_MAX_LENGTH = 63
AREA_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(Area),
+10 -1
View File
@@ -17,7 +17,11 @@ from esphome.const import (
CONF_UNIT_OF_MEASUREMENT,
)
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.config import DEVICE_CLASS_MAX_LENGTH, ICON_MAX_LENGTH
from esphome.core.config import (
DEVICE_CLASS_MAX_LENGTH,
ICON_MAX_LENGTH,
UNIT_OF_MEASUREMENT_MAX_LENGTH,
)
from esphome.cpp_generator import MockObj, RawStatement, add, get_variable
import esphome.final_validate as fv
from esphome.helpers import cpp_string_escape, fnv1_hash_object_id, sanitize, snake_case
@@ -200,6 +204,11 @@ 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:
raise ValueError(
f"Unit of measurement string too long ({len(value)} chars, "
f"max {UNIT_OF_MEASUREMENT_MAX_LENGTH}): '{value}'"
)
return _register_string(value, _get_pool().units, _MAX_UNITS, "unit_of_measurement")
+30 -4
View File
@@ -165,6 +165,11 @@ class TypeInfo(ABC):
"""Get the max_value option for this field, or None if not set."""
return get_field_opt(self._field, pb.max_value, None)
@property
def max_data_length(self) -> int | None:
"""Get the max_data_length option for this field, or None if not set."""
return get_field_opt(self._field, pb.max_data_length, None)
@property
def wire_type(self) -> WireType:
"""Get the wire type for the field."""
@@ -373,7 +378,11 @@ class TypeInfo(ABC):
return f"size += ProtoSize::{method}({field_id_size}, {value});"
def _get_single_byte_varint_size(
self, name: str, force: bool, extra_expr: str | None = None
self,
name: str,
force: bool,
extra_expr: str | None = None,
zero_check: str | None = None,
) -> str:
"""Size calculation when the varint is guaranteed to be 1 byte.
@@ -384,12 +393,14 @@ class TypeInfo(ABC):
name: Expression to check for zero (non-force only)
force: Whether to skip the zero check
extra_expr: Additional variable expression to add (e.g., data length)
zero_check: Override expression for the zero check (e.g., "!x.empty()")
"""
fixed = self.calculate_field_id_size() + 1
size_expr = f"{fixed} + {extra_expr}" if extra_expr else str(fixed)
if force:
return f"size += {size_expr};"
return f"size += {name} ? {size_expr} : 0;"
check = zero_check or name
return f"size += {check} ? {size_expr} : 0;"
@abstractmethod
def get_size_calculation(self, name: str, force: bool = False) -> str:
@@ -1065,8 +1076,14 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
@property
def encode_content(self) -> str:
max_len = self.max_data_length
if max_len is not None and max_len < 128 and self.force:
tag = self.calculate_tag()
if tag < 128:
return f"ProtoEncode::encode_short_string_force(pos, {tag}, this->{self.field_name});"
if result := self._encode_bytes_with_precomputed_tag(
f"this->{self.field_name}.c_str()", f"this->{self.field_name}.size()"
f"this->{self.field_name}.c_str()",
f"this->{self.field_name}.size()",
):
return result
if self.force:
@@ -1091,7 +1108,16 @@ class PointerToStringBufferType(PointerToBufferTypeBase):
return f'dump_field(out, ESPHOME_PSTR("{self.name}"), this->{self.field_name});'
def get_size_calculation(self, name: str, force: bool = False) -> str:
return f"size += ProtoSize::calc_length({self.calculate_field_id_size()}, this->{self.field_name}.size());"
size_field = f"this->{self.field_name}.size()"
max_len = self.max_data_length
if max_len is not None and max_len < 128:
return self._get_single_byte_varint_size(
size_field,
force,
extra_expr=size_field,
zero_check=f"!this->{self.field_name}.empty()",
)
return self._get_simple_size_calculation(size_field, force, "length")
def get_estimated_size(self) -> int:
return self.calculate_field_id_size() + 8 # field ID + 8 bytes typical string
@@ -28,6 +28,7 @@ from esphome.core.entity_helpers import (
get_base_entity_object_id,
register_device_class,
register_icon,
register_unit_of_measurement,
setup_device_class,
setup_entity,
setup_unit_of_measurement,
@@ -925,6 +926,22 @@ def test_register_device_class_max_length() -> None:
assert register_device_class("") == 0
def test_register_unit_of_measurement_max_length() -> None:
"""Test register_unit_of_measurement rejects units exceeding 63 characters."""
# 63 chars should succeed
max_uom = "a" * 63
idx = register_unit_of_measurement(max_uom)
assert idx > 0
# 64 chars should fail
too_long = "a" * 64
with pytest.raises(ValueError, match="Unit of measurement string too long"):
register_unit_of_measurement(too_long)
# Empty string returns 0
assert register_unit_of_measurement("") == 0
@pytest.mark.asyncio
async def test_setup_entity_with_entity_category(
setup_test_environment: list[str],