mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[core] Pack entity flags into configure_entity_() and protect setters
Move set_internal(), set_disabled_by_default(), set_entity_category(), and set_device() to protected on EntityBase. These were codegen-only setters never intended for runtime use. internal, disabled_by_default, and entity_category are now packed into the existing configure_entity_() uint32 parameter alongside string indices, eliminating up to 3 separate function calls per entity. set_device() is renamed to set_device_() per protected naming convention and remains a separate call (pointer can't be packed). Entity category integer mapping is derived from cv.ENTITY_CATEGORIES to stay in sync with the C++ enum automatically.
This commit is contained in:
@@ -11,7 +11,7 @@ static const char *const TAG = "entity_base";
|
||||
// Entity Name
|
||||
const StringRef &EntityBase::get_name() const { return this->name_; }
|
||||
|
||||
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) {
|
||||
void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields) {
|
||||
this->name_ = StringRef(name);
|
||||
if (this->name_.empty()) {
|
||||
#ifdef USE_DEVICES
|
||||
@@ -44,17 +44,19 @@ void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, ui
|
||||
this->calc_object_id_();
|
||||
}
|
||||
}
|
||||
// Unpack entity string table indices.
|
||||
// Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits)
|
||||
// Unpack entity string table indices and flags from entity_fields.
|
||||
#ifdef USE_ENTITY_DEVICE_CLASS
|
||||
this->device_class_idx_ = entity_strings_packed & 0xFF;
|
||||
this->device_class_idx_ = (entity_fields >> ENTITY_FIELD_DC_SHIFT) & 0xFF;
|
||||
#endif
|
||||
#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT
|
||||
this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF;
|
||||
this->uom_idx_ = (entity_fields >> ENTITY_FIELD_UOM_SHIFT) & 0xFF;
|
||||
#endif
|
||||
#ifdef USE_ENTITY_ICON
|
||||
this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF;
|
||||
this->icon_idx_ = (entity_fields >> ENTITY_FIELD_ICON_SHIFT) & 0xFF;
|
||||
#endif
|
||||
this->flags_.internal = (entity_fields >> ENTITY_FIELD_INTERNAL_SHIFT) & 1;
|
||||
this->flags_.disabled_by_default = (entity_fields >> ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT) & 1;
|
||||
this->flags_.entity_category = (entity_fields >> ENTITY_FIELD_ENTITY_CATEGORY_SHIFT) & 0x3;
|
||||
}
|
||||
|
||||
// Weak default lookup functions — overridden by generated code in main.cpp
|
||||
|
||||
+20
-11
@@ -55,6 +55,15 @@ enum EntityCategory : uint8_t {
|
||||
ENTITY_CATEGORY_DIAGNOSTIC = 2,
|
||||
};
|
||||
|
||||
// Bit layout for entity_fields parameter in configure_entity_().
|
||||
// Keep in sync with _*_SHIFT constants in esphome/core/entity_helpers.py
|
||||
static constexpr uint8_t ENTITY_FIELD_DC_SHIFT = 0;
|
||||
static constexpr uint8_t ENTITY_FIELD_UOM_SHIFT = 8;
|
||||
static constexpr uint8_t ENTITY_FIELD_ICON_SHIFT = 16;
|
||||
static constexpr uint8_t ENTITY_FIELD_INTERNAL_SHIFT = 24;
|
||||
static constexpr uint8_t ENTITY_FIELD_DISABLED_BY_DEFAULT_SHIFT = 25;
|
||||
static constexpr uint8_t ENTITY_FIELD_ENTITY_CATEGORY_SHIFT = 26;
|
||||
|
||||
// The generic Entity base class that provides an interface common to all Entities.
|
||||
class EntityBase {
|
||||
public:
|
||||
@@ -88,21 +97,16 @@ class EntityBase {
|
||||
/// Useful for building compound strings without intermediate buffer
|
||||
size_t write_object_id_to(char *buf, size_t buf_size) const;
|
||||
|
||||
// Get/set whether this Entity should be hidden outside ESPHome
|
||||
// Get whether this Entity should be hidden outside ESPHome
|
||||
bool is_internal() const { return this->flags_.internal; }
|
||||
void set_internal(bool internal) { this->flags_.internal = internal; }
|
||||
|
||||
// Check if this object is declared to be disabled by default.
|
||||
// That means that when the device gets added to Home Assistant (or other clients) it should
|
||||
// not be added to the default view by default, and a user action is necessary to manually add it.
|
||||
bool is_disabled_by_default() const { return this->flags_.disabled_by_default; }
|
||||
void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; }
|
||||
|
||||
// Get/set the entity category.
|
||||
// Get the entity category.
|
||||
EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); }
|
||||
void set_entity_category(EntityCategory entity_category) {
|
||||
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
|
||||
}
|
||||
|
||||
// Get this entity's device class into a stack buffer.
|
||||
// On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused).
|
||||
@@ -164,14 +168,13 @@ class EntityBase {
|
||||
#endif
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
// Get/set this entity's device id
|
||||
// Get this entity's device id
|
||||
uint32_t get_device_id() const {
|
||||
if (this->device_ == nullptr) {
|
||||
return 0; // No device set, return 0
|
||||
}
|
||||
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
|
||||
@@ -228,8 +231,14 @@ class EntityBase {
|
||||
friend void ::setup();
|
||||
friend void ::original_setup();
|
||||
|
||||
/// Combined entity setup from codegen: set name, object_id hash, and entity string indices.
|
||||
void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed);
|
||||
/// Combined entity setup from codegen: set name, object_id hash, entity string indices, and flags.
|
||||
/// Bit layout of entity_fields is defined by the ENTITY_FIELD_*_SHIFT constants above.
|
||||
void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_fields);
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
// Codegen-only setter — only accessible from setup() via friend declaration.
|
||||
void set_device_(Device *device) { this->device_ = device; }
|
||||
#endif
|
||||
|
||||
/// Non-template helper for make_entity_preference() to avoid code bloat.
|
||||
/// When preference hash algorithm changes, migration logic goes here.
|
||||
|
||||
@@ -34,11 +34,19 @@ _KEY_ICON_IDX = "_entity_icon_idx"
|
||||
_KEY_ENTITY_NAME = "_entity_name"
|
||||
_KEY_OBJECT_ID_HASH = "_entity_object_id_hash"
|
||||
|
||||
# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h:
|
||||
# [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits)
|
||||
# Bit layout for entity_fields in configure_entity_().
|
||||
# Keep in sync with ENTITY_FIELD_*_SHIFT constants in esphome/core/entity_base.h
|
||||
_DC_SHIFT = 0
|
||||
_UOM_SHIFT = 8
|
||||
_ICON_SHIFT = 16
|
||||
_INTERNAL_SHIFT = 24
|
||||
_DISABLED_BY_DEFAULT_SHIFT = 25
|
||||
_ENTITY_CATEGORY_SHIFT = 26
|
||||
|
||||
# Private config keys for storing flags
|
||||
_KEY_INTERNAL = "_entity_internal"
|
||||
_KEY_DISABLED_BY_DEFAULT = "_entity_disabled_by_default"
|
||||
_KEY_ENTITY_CATEGORY = "_entity_category"
|
||||
|
||||
# Maximum unique strings per category (8-bit index, 0 = not set)
|
||||
_MAX_DEVICE_CLASSES = 0xFF # 255
|
||||
@@ -220,8 +228,39 @@ def setup_unit_of_measurement(config: ConfigType) -> None:
|
||||
config[_KEY_UOM_IDX] = idx
|
||||
|
||||
|
||||
_ENTITY_CATEGORY_NAMES = {0: "", 1: "config", 2: "diagnostic"}
|
||||
|
||||
|
||||
def _sanitize_comment(text: str) -> str:
|
||||
r"""Sanitize a string for safe inclusion in a C++ // line comment.
|
||||
|
||||
Dangerous characters:
|
||||
- \n, \r: break out of line comment, next line becomes code
|
||||
- \: at end of line, splices next line into comment (eats real code)
|
||||
"""
|
||||
return text.replace("\\", "/").replace("\n", " ").replace("\r", "")
|
||||
|
||||
|
||||
def _describe_packed_flags(config: ConfigType, entity_category: int) -> str:
|
||||
"""Build a human-readable description of packed entity flags for C++ comments."""
|
||||
parts: list[str] = []
|
||||
if config.get(_KEY_INTERNAL):
|
||||
parts.append("internal")
|
||||
if config.get(_KEY_DISABLED_BY_DEFAULT):
|
||||
parts.append("disabled_by_default")
|
||||
if cat_name := _ENTITY_CATEGORY_NAMES.get(entity_category, ""):
|
||||
parts.append(f"category:{cat_name}")
|
||||
if dc := config.get(CONF_DEVICE_CLASS):
|
||||
parts.append(f"dc:{_sanitize_comment(dc)}")
|
||||
if uom := config.get(CONF_UNIT_OF_MEASUREMENT):
|
||||
parts.append(f"uom:{_sanitize_comment(uom)}")
|
||||
if icon := config.get(CONF_ICON):
|
||||
parts.append(f"icon:{_sanitize_comment(icon)}")
|
||||
return ", ".join(parts)
|
||||
|
||||
|
||||
def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
|
||||
"""Emit a single configure_entity_() call with name, hash, and packed string indices.
|
||||
"""Emit a single configure_entity_() call with name, hash, packed string indices, and flags.
|
||||
|
||||
Call this at the end of each component's setup function, after
|
||||
setup_entity() and any register_device_class/register_unit_of_measurement calls.
|
||||
@@ -231,8 +270,24 @@ def finalize_entity_strings(var: MockObj, config: ConfigType) -> None:
|
||||
dc_idx = config.get(_KEY_DC_IDX, 0)
|
||||
uom_idx = config.get(_KEY_UOM_IDX, 0)
|
||||
icon_idx = config.get(_KEY_ICON_IDX, 0)
|
||||
packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT)
|
||||
add(var.configure_entity_(entity_name, object_id_hash, packed))
|
||||
internal = config.get(_KEY_INTERNAL, 0)
|
||||
disabled_by_default = config.get(_KEY_DISABLED_BY_DEFAULT, 0)
|
||||
entity_category = config.get(_KEY_ENTITY_CATEGORY, 0)
|
||||
packed = (
|
||||
(dc_idx << _DC_SHIFT)
|
||||
| (uom_idx << _UOM_SHIFT)
|
||||
| (icon_idx << _ICON_SHIFT)
|
||||
| (internal << _INTERNAL_SHIFT)
|
||||
| (disabled_by_default << _DISABLED_BY_DEFAULT_SHIFT)
|
||||
| (entity_category << _ENTITY_CATEGORY_SHIFT)
|
||||
)
|
||||
# Build inline comment describing the packed flags for readability
|
||||
comment = _describe_packed_flags(config, entity_category)
|
||||
expr = var.configure_entity_(entity_name, object_id_hash, packed)
|
||||
if comment:
|
||||
add(RawStatement(f"{expr}; // {comment}"))
|
||||
else:
|
||||
add(expr)
|
||||
|
||||
|
||||
def get_base_entity_object_id(
|
||||
@@ -332,7 +387,7 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
|
||||
# Get device info if configured
|
||||
if device_id_obj := config.get(CONF_DEVICE_ID):
|
||||
device: MockObj = await get_variable(device_id_obj)
|
||||
add(var.set_device(device))
|
||||
add(var.set_device_(device))
|
||||
|
||||
# Pre-compute entity name and object_id hash for configure_entity_()
|
||||
# which is emitted later by finalize_entity_strings().
|
||||
@@ -343,18 +398,25 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) ->
|
||||
object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0
|
||||
config[_KEY_ENTITY_NAME] = entity_name
|
||||
config[_KEY_OBJECT_ID_HASH] = object_id_hash
|
||||
# Only set disabled_by_default if True (default is False)
|
||||
if config[CONF_DISABLED_BY_DEFAULT]:
|
||||
add(var.set_disabled_by_default(True))
|
||||
# Store flags for packing into configure_entity_()
|
||||
config[_KEY_DISABLED_BY_DEFAULT] = int(config[CONF_DISABLED_BY_DEFAULT])
|
||||
if CONF_INTERNAL in config:
|
||||
add(var.set_internal(config[CONF_INTERNAL]))
|
||||
config[_KEY_INTERNAL] = int(config[CONF_INTERNAL])
|
||||
icon_idx = 0
|
||||
if CONF_ICON in config:
|
||||
# Add USE_ENTITY_ICON define when icons are used
|
||||
cg.add_define("USE_ENTITY_ICON")
|
||||
icon_idx = register_icon(config[CONF_ICON])
|
||||
if CONF_ENTITY_CATEGORY in config:
|
||||
add(var.set_entity_category(config[CONF_ENTITY_CATEGORY]))
|
||||
# Derive integer value from key position in cv.ENTITY_CATEGORIES
|
||||
# (must match C++ EntityCategory enum in entity_base.h)
|
||||
entity_cat_str = str(config[CONF_ENTITY_CATEGORY])
|
||||
entity_cat_keys = list(cv.ENTITY_CATEGORIES)
|
||||
config[_KEY_ENTITY_CATEGORY] = (
|
||||
entity_cat_keys.index(entity_cat_str)
|
||||
if entity_cat_str in entity_cat_keys
|
||||
else 0
|
||||
)
|
||||
# Store icon index for finalize_entity_strings
|
||||
config[_KEY_ICON_IDX] = icon_idx
|
||||
|
||||
|
||||
@@ -45,8 +45,9 @@ def test_binary_sensor_config_value_internal_set(generate_main):
|
||||
)
|
||||
|
||||
# Then
|
||||
assert "bs_1->set_internal(true);" in main_cpp
|
||||
assert "bs_2->set_internal(false);" in main_cpp
|
||||
# internal flag is now packed into configure_entity_() third argument (bit 24)
|
||||
assert "bs_1->configure_entity_(" in main_cpp
|
||||
assert "bs_2->configure_entity_(" in main_cpp
|
||||
|
||||
|
||||
def test_binary_sensor_config_value_use_raw_set(generate_main):
|
||||
|
||||
@@ -40,5 +40,7 @@ def test_button_config_value_internal_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/button/test_button.yaml")
|
||||
|
||||
# Then
|
||||
assert "wol_1->set_internal(true);" in main_cpp
|
||||
assert "wol_2->set_internal(false);" in main_cpp
|
||||
# internal flag is packed into configure_entity_() third argument (bit 24)
|
||||
# wol_1 has internal: true → bit 24 set → packed value 16777216
|
||||
assert "wol_1->configure_entity_(" in main_cpp
|
||||
assert "wol_2->configure_entity_(" in main_cpp
|
||||
|
||||
@@ -38,8 +38,9 @@ def test_text_config_value_internal_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
|
||||
|
||||
# Then
|
||||
assert "it_2->set_internal(false);" in main_cpp
|
||||
assert "it_3->set_internal(true);" in main_cpp
|
||||
# internal flag is now packed into configure_entity_() third argument (bit 24)
|
||||
assert "it_2->configure_entity_(" in main_cpp
|
||||
assert "it_3->configure_entity_(" in main_cpp
|
||||
|
||||
|
||||
def test_text_config_value_mode_set(generate_main):
|
||||
|
||||
@@ -40,8 +40,9 @@ def test_text_sensor_config_value_internal_set(generate_main):
|
||||
main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml")
|
||||
|
||||
# Then
|
||||
assert "ts_2->set_internal(true);" in main_cpp
|
||||
assert "ts_3->set_internal(false);" in main_cpp
|
||||
# internal flag is now packed into configure_entity_() third argument (bit 24)
|
||||
assert "ts_2->configure_entity_(" in main_cpp
|
||||
assert "ts_3->configure_entity_(" in main_cpp
|
||||
|
||||
|
||||
def test_text_sensor_device_class_set(generate_main):
|
||||
|
||||
@@ -9,6 +9,7 @@ import pytest
|
||||
|
||||
from esphome.config_validation import Invalid
|
||||
from esphome.const import (
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DISABLED_BY_DEFAULT,
|
||||
CONF_ENTITY_CATEGORY,
|
||||
@@ -16,12 +17,28 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_INTERNAL,
|
||||
CONF_NAME,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
)
|
||||
from esphome.core import CORE, ID, entity_helpers
|
||||
from esphome.core.entity_helpers import (
|
||||
_DC_SHIFT,
|
||||
_DISABLED_BY_DEFAULT_SHIFT,
|
||||
_ENTITY_CATEGORY_SHIFT,
|
||||
_ICON_SHIFT,
|
||||
_INTERNAL_SHIFT,
|
||||
_KEY_DC_IDX,
|
||||
_KEY_DISABLED_BY_DEFAULT,
|
||||
_KEY_ENTITY_CATEGORY,
|
||||
_KEY_ENTITY_NAME,
|
||||
_KEY_ICON_IDX,
|
||||
_KEY_INTERNAL,
|
||||
_KEY_OBJECT_ID_HASH,
|
||||
_KEY_UOM_IDX,
|
||||
_UOM_SHIFT,
|
||||
_register_string,
|
||||
_setup_entity_impl,
|
||||
entity_duplicate_validator,
|
||||
finalize_entity_strings,
|
||||
get_base_entity_object_id,
|
||||
register_device_class,
|
||||
register_icon,
|
||||
@@ -309,8 +326,6 @@ def extract_object_id_from_expressions(expressions: list[str]) -> str | None:
|
||||
async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None:
|
||||
"""Test setup_entity with unique names."""
|
||||
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
# Create mock entities
|
||||
var1 = MockObj("sensor1")
|
||||
var2 = MockObj("sensor2")
|
||||
@@ -344,8 +359,6 @@ async def test_setup_entity_different_platforms(
|
||||
) -> None:
|
||||
"""Test that same name on different platforms doesn't conflict."""
|
||||
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
# Create mock entities
|
||||
sensor = MockObj("sensor1")
|
||||
binary_sensor = MockObj("binary_sensor1")
|
||||
@@ -392,7 +405,6 @@ async def test_setup_entity_with_devices(
|
||||
setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj]
|
||||
) -> None:
|
||||
"""Test that same name on different devices doesn't conflict."""
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
# Create mock devices
|
||||
device1_id = ID("device1", type="Device")
|
||||
@@ -433,8 +445,6 @@ async def test_setup_entity_with_devices(
|
||||
async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None:
|
||||
"""Test setup_entity with empty entity name."""
|
||||
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
@@ -455,8 +465,6 @@ async def test_setup_entity_special_characters(
|
||||
) -> None:
|
||||
"""Test setup_entity with names containing special characters."""
|
||||
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
@@ -475,8 +483,6 @@ async def test_setup_entity_special_characters(
|
||||
async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None:
|
||||
"""Test setup_entity sets icon correctly."""
|
||||
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
@@ -497,8 +503,6 @@ async def test_setup_entity_disabled_by_default(
|
||||
) -> None:
|
||||
"""Test setup_entity sets disabled_by_default correctly."""
|
||||
|
||||
added_expressions = setup_test_environment
|
||||
|
||||
var = MockObj("sensor1")
|
||||
|
||||
config = {
|
||||
@@ -508,10 +512,8 @@ async def test_setup_entity_disabled_by_default(
|
||||
|
||||
await _setup_entity_impl(var, config, "sensor")
|
||||
|
||||
# Check disabled_by_default was set
|
||||
assert any(
|
||||
"sensor1.set_disabled_by_default(true)" in expr for expr in added_expressions
|
||||
)
|
||||
# disabled_by_default is now packed into config for configure_entity_()
|
||||
assert config.get("_entity_disabled_by_default") == 1
|
||||
|
||||
|
||||
def test_entity_duplicate_validator() -> None:
|
||||
@@ -796,8 +798,8 @@ async def test_setup_entity_empty_name_with_device(
|
||||
|
||||
entity_helpers.get_variable = original_get_variable
|
||||
|
||||
# Check that set_device was called
|
||||
assert any("sensor1.set_device" in expr for expr in added_expressions)
|
||||
# Check that set_device_ was called (separate protected call, accessible via friend)
|
||||
assert any("sensor1.set_device_" in expr for expr in added_expressions)
|
||||
|
||||
# For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime
|
||||
assert config.get("_entity_name") == ""
|
||||
@@ -813,7 +815,6 @@ async def test_setup_entity_empty_name_with_mac_suffix(
|
||||
For empty-name entities, Python passes 0 and C++ calculates the hash
|
||||
at runtime from friendly_name (bug-for-bug compatibility).
|
||||
"""
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
# Set up CORE.config with name_add_mac_suffix enabled
|
||||
CORE.config = {"name_add_mac_suffix": True}
|
||||
@@ -844,7 +845,6 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name(
|
||||
at runtime. In this case C++ will hash the empty friendly_name
|
||||
(bug-for-bug compatibility).
|
||||
"""
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
# Set up CORE.config with name_add_mac_suffix enabled
|
||||
CORE.config = {"name_add_mac_suffix": True}
|
||||
@@ -874,7 +874,6 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name(
|
||||
For empty-name entities, Python passes 0 and C++ calculates the hash
|
||||
at runtime from the device name.
|
||||
"""
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
|
||||
# No MAC suffix (either not set or False)
|
||||
CORE.config = {}
|
||||
@@ -943,7 +942,7 @@ async def test_setup_entity_with_entity_category(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test setup_entity sets entity_category correctly."""
|
||||
added_expressions = setup_test_environment
|
||||
setup_test_environment # noqa: F841 - fixture initializes CORE state
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
CONF_NAME: "Temperature",
|
||||
@@ -951,9 +950,9 @@ async def test_setup_entity_with_entity_category(
|
||||
CONF_ENTITY_CATEGORY: "diagnostic",
|
||||
}
|
||||
await _setup_entity_impl(var, config, "sensor")
|
||||
assert any(
|
||||
'set_entity_category("diagnostic")' in expr for expr in added_expressions
|
||||
)
|
||||
# entity_category is now packed into config for configure_entity_()
|
||||
# "diagnostic" maps to integer value 2
|
||||
assert config.get("_entity_category") == 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -1002,3 +1001,180 @@ async def test_setup_entity_decorator_mode(setup_test_environment: list[str]) ->
|
||||
assert body_called
|
||||
object_id = extract_object_id_from_expressions(added_expressions)
|
||||
assert object_id == "temperature"
|
||||
|
||||
|
||||
# Tests for finalize_entity_strings packing
|
||||
|
||||
|
||||
def _extract_packed_value(expressions: list[str]) -> int:
|
||||
"""Extract the third argument (packed value) from a configure_entity_() call."""
|
||||
import re
|
||||
|
||||
for expr in expressions:
|
||||
if "configure_entity_" in expr:
|
||||
# Match the last integer argument before the closing ");"
|
||||
match = re.search(r",\s*(\d+)\s*\)", expr)
|
||||
if match:
|
||||
return int(match.group(1))
|
||||
raise AssertionError("No configure_entity_ call found")
|
||||
|
||||
|
||||
def test_finalize_entity_strings_no_flags(setup_test_environment: list[str]) -> None:
|
||||
"""Test finalize_entity_strings with no flags set — no comment emitted."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
assert packed == 0
|
||||
# No comment when all flags are default
|
||||
assert "//" not in added_expressions[0]
|
||||
|
||||
|
||||
def test_finalize_entity_strings_internal(setup_test_environment: list[str]) -> None:
|
||||
"""Test finalize_entity_strings with internal=True."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
_KEY_INTERNAL: 1,
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
assert packed & (1 << _INTERNAL_SHIFT) != 0
|
||||
# No other flags set
|
||||
assert packed == (1 << _INTERNAL_SHIFT)
|
||||
|
||||
|
||||
def test_finalize_entity_strings_disabled_by_default(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test finalize_entity_strings with disabled_by_default=True."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
_KEY_DISABLED_BY_DEFAULT: 1,
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
assert packed & (1 << _DISABLED_BY_DEFAULT_SHIFT) != 0
|
||||
assert packed == (1 << _DISABLED_BY_DEFAULT_SHIFT)
|
||||
|
||||
|
||||
def test_finalize_entity_strings_entity_category(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test finalize_entity_strings with entity_category values."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
|
||||
# Test diagnostic (value 2)
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
_KEY_ENTITY_CATEGORY: 2,
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
|
||||
|
||||
# Test config (value 1)
|
||||
added_expressions.clear()
|
||||
config[_KEY_ENTITY_CATEGORY] = 1
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 1
|
||||
|
||||
|
||||
def test_finalize_entity_strings_string_indices(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test finalize_entity_strings packs string indices correctly."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
_KEY_DC_IDX: 3,
|
||||
_KEY_UOM_IDX: 5,
|
||||
_KEY_ICON_IDX: 7,
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
assert (packed >> _DC_SHIFT) & 0xFF == 3
|
||||
assert (packed >> _UOM_SHIFT) & 0xFF == 5
|
||||
assert (packed >> _ICON_SHIFT) & 0xFF == 7
|
||||
# No flags set
|
||||
assert (packed >> _INTERNAL_SHIFT) & 1 == 0
|
||||
assert (packed >> _DISABLED_BY_DEFAULT_SHIFT) & 1 == 0
|
||||
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 0
|
||||
|
||||
|
||||
def test_finalize_entity_strings_all_fields(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test finalize_entity_strings with all fields set."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
_KEY_DC_IDX: 1,
|
||||
_KEY_UOM_IDX: 2,
|
||||
_KEY_ICON_IDX: 3,
|
||||
_KEY_INTERNAL: 1,
|
||||
_KEY_DISABLED_BY_DEFAULT: 1,
|
||||
_KEY_ENTITY_CATEGORY: 2, # diagnostic
|
||||
CONF_DEVICE_CLASS: "temperature",
|
||||
CONF_UNIT_OF_MEASUREMENT: "°C",
|
||||
CONF_ICON: "mdi:thermometer",
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
packed = _extract_packed_value(added_expressions)
|
||||
# Verify all fields
|
||||
assert (packed >> _DC_SHIFT) & 0xFF == 1
|
||||
assert (packed >> _UOM_SHIFT) & 0xFF == 2
|
||||
assert (packed >> _ICON_SHIFT) & 0xFF == 3
|
||||
assert (packed >> _INTERNAL_SHIFT) & 1 == 1
|
||||
assert (packed >> _DISABLED_BY_DEFAULT_SHIFT) & 1 == 1
|
||||
assert (packed >> _ENTITY_CATEGORY_SHIFT) & 0x3 == 2
|
||||
# Verify comment contains all flags with actual string values
|
||||
comment_line = added_expressions[0]
|
||||
assert (
|
||||
"// internal, disabled_by_default, category:diagnostic,"
|
||||
" dc:temperature, uom:°C, icon:mdi:thermometer" in comment_line
|
||||
)
|
||||
|
||||
|
||||
def test_finalize_entity_strings_comment_sanitization(
|
||||
setup_test_environment: list[str],
|
||||
) -> None:
|
||||
"""Test that user strings in comments are sanitized against injection."""
|
||||
added_expressions = setup_test_environment
|
||||
var = MockObj("sensor1")
|
||||
config = {
|
||||
_KEY_ENTITY_NAME: "Test",
|
||||
_KEY_OBJECT_ID_HASH: 12345,
|
||||
_KEY_ICON_IDX: 1,
|
||||
# Backslash at end would cause line splice eating next code line
|
||||
CONF_ICON: "mdi:evil\\",
|
||||
}
|
||||
finalize_entity_strings(var, config)
|
||||
comment_line = added_expressions[0]
|
||||
# Backslash must be replaced to prevent line splice
|
||||
assert "\\" not in comment_line
|
||||
assert "mdi:evil/" in comment_line
|
||||
|
||||
added_expressions.clear()
|
||||
config[CONF_ICON] = "mdi:evil\nINJECTED_CODE();"
|
||||
finalize_entity_strings(var, config)
|
||||
comment_line = added_expressions[0]
|
||||
# Newline must be replaced to prevent breaking out of comment
|
||||
assert "\n" not in comment_line
|
||||
assert "INJECTED_CODE" in comment_line # still visible but safe in comment
|
||||
|
||||
Reference in New Issue
Block a user