diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 19ab57349d..3d61e36fd1 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -16,7 +16,6 @@ void EntityBase::set_name(const char *name) { } else { this->has_own_name_ = true; } - this->calc_object_id_(); } // Entity Internal @@ -41,13 +40,37 @@ EntityCategory EntityBase::get_entity_category() const { return this->entity_cat void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; } // Entity Object ID -const std::string &EntityBase::get_object_id() { return this->object_id_; } +std::string EntityBase::get_object_id() const { + // Check if `App.get_friendly_name()` is constant or dynamic. + if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { + // `App.get_friendly_name()` is dynamic. + return str_sanitize(str_snake_case(App.get_friendly_name())); + } else { + // `App.get_friendly_name()` is constant. + if (this->object_id_c_str_ == nullptr) { + return ""; + } + return this->object_id_c_str_; + } +} +void EntityBase::set_object_id(const char *object_id) { + this->object_id_c_str_ = object_id; + this->calc_object_id_(); +} // Calculate Object ID Hash from Entity Name void EntityBase::calc_object_id_() { - this->object_id_ = str_sanitize(str_snake_case(this->name_)); - // FNV-1 hash - this->object_id_hash_ = fnv1_hash(this->object_id_); + // Check if `App.get_friendly_name()` is constant or dynamic. + if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { + // `App.get_friendly_name()` is dynamic. + const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(object_id); + } else { + // `App.get_friendly_name()` is constant. + // FNV-1 hash + this->object_id_hash_ = fnv1_hash(this->object_id_c_str_); + } } uint32_t EntityBase::get_object_id_hash() { return this->object_id_hash_; } diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index e25aab21a9..0a53c3fccc 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -22,8 +22,9 @@ class EntityBase { // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->has_own_name_; } - // Get the sanitized name of this Entity as an ID. Caching it internally. - const std::string &get_object_id(); + // Get the sanitized name of this Entity as an ID. + std::string get_object_id() const; + void set_object_id(const char *object_id); // Get the unique Object ID of this Entity uint32_t get_object_id_hash(); @@ -54,7 +55,7 @@ class EntityBase { StringRef name_; bool has_own_name_{false}; - std::string object_id_; + const char *object_id_c_str_{nullptr}; const char *icon_c_str_{nullptr}; uint32_t object_id_hash_; bool internal_{false}; diff --git a/esphome/cpp_helpers.py b/esphome/cpp_helpers.py index ab5231e055..cc53f491f5 100644 --- a/esphome/cpp_helpers.py +++ b/esphome/cpp_helpers.py @@ -20,6 +20,7 @@ from esphome.types import ConfigType, ConfigFragmentType from esphome.cpp_generator import add, get_variable from esphome.cpp_types import App from esphome.util import Registry, RegistryEntry +from esphome.helpers import snake_case, sanitize _LOGGER = logging.getLogger(__name__) @@ -101,6 +102,10 @@ async def register_parented(var, value): async def setup_entity(var, config): """Set up generic properties of an Entity""" add(var.set_name(config[CONF_NAME])) + if not config[CONF_NAME]: + add(var.set_object_id(sanitize(snake_case(CORE.friendly_name)))) + else: + add(var.set_object_id(sanitize(snake_case(config[CONF_NAME])))) add(var.set_disabled_by_default(config[CONF_DISABLED_BY_DEFAULT])) if CONF_INTERNAL in config: add(var.set_internal(config[CONF_INTERNAL])) diff --git a/esphome/helpers.py b/esphome/helpers.py index b5a6306342..884f640d7b 100644 --- a/esphome/helpers.py +++ b/esphome/helpers.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Union import tempfile from urllib.parse import urlparse +import re _LOGGER = logging.getLogger(__name__) @@ -334,3 +335,13 @@ def add_class_to_obj(value, cls): if type(value) is type_: # pylint: disable=unidiomatic-typecheck return add_class_to_obj(func(value), cls) raise + + +def snake_case(value): + """Same behaviour as `helpers.cpp` method `str_snake_case`.""" + return value.replace(" ", "_").lower() + + +def sanitize(value): + """Same behaviour as `helpers.cpp` method `str_sanitize`.""" + return re.sub("[^-_0-9a-zA-Z]", r"", value) diff --git a/tests/unit_tests/test_helpers.py b/tests/unit_tests/test_helpers.py index f883b8b44f..b98838024f 100644 --- a/tests/unit_tests/test_helpers.py +++ b/tests/unit_tests/test_helpers.py @@ -229,3 +229,37 @@ def test_file_compare(fixture_path, file1, file2, expected): actual = helpers.file_compare(path1, path2) assert actual == expected + + +@pytest.mark.parametrize( + "text, expected", + ( + ("foo", "foo"), + ("foo bar", "foo_bar"), + ("foo Bar", "foo_bar"), + ("foo BAR", "foo_bar"), + ("foo.bar", "foo.bar"), + ("fooBAR", "foobar"), + ("Foo-bar_EEK", "foo-bar_eek"), + (" foo", "__foo"), + ), +) +def test_snake_case(text, expected): + actual = helpers.snake_case(text) + + assert actual == expected + + +@pytest.mark.parametrize( + "text, expected", + ( + ("foo_bar", "foo_bar"), + ('!"§$%&/()=?foo_bar', "foo_bar"), + ('foo_!"§$%&/()=?bar', "foo_bar"), + ('foo_bar!"§$%&/()=?', "foo_bar"), + ), +) +def test_sanitize(text, expected): + actual = helpers.sanitize(text) + + assert actual == expected