diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index f0d000f79dba57..89857efc149264 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -41,6 +41,7 @@ "curr_hum_tpl": "current_humidity_template", "curr_temp_t": "current_temperature_topic", "curr_temp_tpl": "current_temperature_template", + "def_ent_id": "default_entity_id", "dev": "device", "dev_cla": "device_class", "dir_cmd_t": "direction_command_topic", diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index d1feb25b281399..90f484b1a90ef5 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -38,6 +38,7 @@ CONF_CODE_TRIGGER_REQUIRED = "code_trigger_required" CONF_COMMAND_TEMPLATE = "command_template" CONF_COMMAND_TOPIC = "command_topic" +CONF_DEFAULT_ENTITY_ID = "default_entity_id" CONF_DISCOVERY_PREFIX = "discovery_prefix" CONF_ENCODING = "encoding" CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" diff --git a/homeassistant/components/mqtt/entity.py b/homeassistant/components/mqtt/entity.py index f0e7f915551b18..ff4532381ce33c 100644 --- a/homeassistant/components/mqtt/entity.py +++ b/homeassistant/components/mqtt/entity.py @@ -29,6 +29,7 @@ CONF_MODEL_ID, CONF_NAME, CONF_UNIQUE_ID, + CONF_URL, CONF_VALUE_TEMPLATE, ) from homeassistant.core import Event, HassJobType, HomeAssistant, callback @@ -74,6 +75,7 @@ CONF_AVAILABILITY_TOPIC, CONF_CONFIGURATION_URL, CONF_CONNECTIONS, + CONF_DEFAULT_ENTITY_ID, CONF_ENABLED_BY_DEFAULT, CONF_ENCODING, CONF_ENTITY_PICTURE, @@ -83,6 +85,7 @@ CONF_JSON_ATTRS_TOPIC, CONF_MANUFACTURER, CONF_OBJECT_ID, + CONF_ORIGIN, CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_NOT_AVAILABLE, CONF_QOS, @@ -1406,12 +1409,62 @@ def __init__( ensure_via_device_exists(self.hass, self.device_info, self._config_entry) def _init_entity_id(self) -> None: - """Set entity_id from object_id if defined in config.""" - if CONF_OBJECT_ID not in self._config: + """Set entity_id from default_entity_id if defined in config.""" + object_id: str + default_entity_id: str | None + # Setting the default entity_id through the CONF_OBJECT_ID is deprecated + # Support will be removed with HA Core 2026.4 + if ( + CONF_DEFAULT_ENTITY_ID not in self._config + and CONF_OBJECT_ID not in self._config + ): return + if (default_entity_id := self._config.get(CONF_DEFAULT_ENTITY_ID)) is None: + object_id = self._config[CONF_OBJECT_ID] + else: + _, _, object_id = default_entity_id.partition(".") self.entity_id = async_generate_entity_id( - self._entity_id_format, self._config[CONF_OBJECT_ID], None, self.hass + self._entity_id_format, object_id, None, self.hass ) + if CONF_OBJECT_ID in self._config: + domain = self.entity_id.split(".")[0] + if not self._discovery: + async_create_issue( + self.hass, + DOMAIN, + self.entity_id, + issue_domain=DOMAIN, + is_fixable=False, + breaks_in_ha_version="2026.4", + severity=IssueSeverity.WARNING, + learn_more_url=f"{learn_more_url(domain)}#default_enity_id", + translation_placeholders={ + "entity_id": self.entity_id, + "object_id": self._config[CONF_OBJECT_ID], + "domain": domain, + }, + translation_key="deprecated_object_id", + ) + else: + if CONF_ORIGIN in self._config: + origin_name = self._config[CONF_ORIGIN][CONF_NAME] + url = self._config[CONF_ORIGIN].get(CONF_URL) + origin = f"[{origin_name}]({url})" if url else origin_name + else: + origin = "the integration" + _LOGGER.warning( + "The configuration for entity %s uses the deprecated option " + "`object_id` to set the default entity id. Replace the " + '`"object_id": "%s"` option with `"default_entity_id": ' + '"%s"` in your published discovery configuration to fix this ' + "issue, or contact the maintainer of %s that published this config " + "to fix this. This will stop working in Home Assistant Core 2026.4", + self.entity_id, + self._config[CONF_OBJECT_ID], + f"{domain}.{self._config[CONF_OBJECT_ID]}", + origin, + ) + if self.unique_id is None: return # Check for previous deleted entities diff --git a/homeassistant/components/mqtt/schemas.py b/homeassistant/components/mqtt/schemas.py index 5e942c24738e34..0a9609dfc6d7c9 100644 --- a/homeassistant/components/mqtt/schemas.py +++ b/homeassistant/components/mqtt/schemas.py @@ -32,6 +32,7 @@ CONF_COMPONENTS, CONF_CONFIGURATION_URL, CONF_CONNECTIONS, + CONF_DEFAULT_ENTITY_ID, CONF_DEPRECATED_VIA_HUB, CONF_ENABLED_BY_DEFAULT, CONF_ENCODING, @@ -180,6 +181,7 @@ def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, + vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string, vol.Optional(CONF_OBJECT_ID): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, } diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index fa615ed1f9194c..dce546b3e6d635 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -1,5 +1,9 @@ { "issues": { + "deprecated_object_id": { + "title": "Deprecated option object_id used", + "description": "Entity {entity_id} uses the `object_id` option which deprecated. To fix the issue, replace the `object_id: {object_id}` option with `default_entity_id: {domain}.{object_id}` in your \"configuration.yaml\", and restart Home Assistant." + }, "deprecated_vacuum_battery_feature": { "title": "Deprecated battery feature used", "description": "Vacuum entity {entity_id} implements the battery feature which is deprecated. This will stop working in Home Assistant 2026.2. Implement a separate entity for the battery state instead. To fix the issue, remove the `battery` feature from the configured supported features, and restart Home Assistant." diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index f99c48a440fa39..571308f01588a4 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -55,7 +55,7 @@ button.DOMAIN: { "command_topic": "command-topic", "name": "test", - "object_id": "test_button", + "default_entity_id": "button.test_button", "payload_press": "beer press", "qos": "2", } diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 04b4bda0d794e9..0643f7c11d11fc 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1473,6 +1473,48 @@ async def test_discover_alarm_control_panel( "Hello World 19", "device_tracker", ), + ( + "homeassistant/binary_sensor/object/bla/config", + '{ "name": "Hello World 2", "obj_id": "hello_id", ' + '"o": {"name": "X2mqtt"}, "state_topic": "test-topic" }', + "binary_sensor.hello_id", + "Hello World 2", + "binary_sensor", + ), + ( + "homeassistant/button/object/bla/config", + '{ "name": "Hello World button", "obj_id": "hello_id", ' + '"o": {"name": "X2mqtt", "url": "https://example.com/x2mqtt"}, ' + '"command_topic": "test-topic" }', + "button.hello_id", + "Hello World button", + "button", + ), + ( + "homeassistant/alarm_control_panel/object/bla/config", + '{ "name": "Hello World 1", "def_ent_id": "alarm_control_panel.hello_id", ' + '"state_topic": "test-topic", "command_topic": "test-topic" }', + "alarm_control_panel.hello_id", + "Hello World 1", + "alarm_control_panel", + ), + ( + "homeassistant/binary_sensor/object/bla/config", + '{ "name": "Hello World 2", "def_ent_id": "binary_sensor.hello_id", ' + '"o": {"name": "X2mqtt"}, "state_topic": "test-topic" }', + "binary_sensor.hello_id", + "Hello World 2", + "binary_sensor", + ), + ( + "homeassistant/button/object/bla/config", + '{ "name": "Hello World button", "def_ent_id": "button.hello_id", ' + '"o": {"name": "X2mqtt", "url": "https://example.com/x2mqtt"}, ' + '"command_topic": "test-topic" }', + "button.hello_id", + "Hello World button", + "button", + ), ], ) async def test_discovery_with_object_id( @@ -1496,20 +1538,20 @@ async def test_discovery_with_object_id( assert (domain, "object bla") in hass.data["mqtt"].discovery_already_discovered -async def test_discovery_with_object_id_for_previous_deleted_entity( +async def test_discovery_with_default_entity_id_for_previous_deleted_entity( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, ) -> None: - """Test discovering an MQTT entity with object_id and unique_id.""" + """Test discovering an MQTT entity with default_entity_id and unique_id.""" topic = "homeassistant/sensor/object/bla/config" config = ( '{ "name": "Hello World 11", "unique_id": "very_unique", ' - '"obj_id": "hello_id", "state_topic": "test-topic" }' + '"def_ent_id": "sensor.hello_id", "state_topic": "test-topic" }' ) new_config = ( '{ "name": "Hello World 11", "unique_id": "very_unique", ' - '"obj_id": "updated_hello_id", "state_topic": "test-topic" }' + '"def_ent_id": "sensor.updated_hello_id", "state_topic": "test-topic" }' ) initial_entity_id = "sensor.hello_id" new_entity_id = "sensor.updated_hello_id" @@ -1531,7 +1573,7 @@ async def test_discovery_with_object_id_for_previous_deleted_entity( await hass.async_block_till_done() assert (domain, "object bla") not in hass.data["mqtt"].discovery_already_discovered - # Rediscover with new object_id + # Rediscover with new default_entity_id async_fire_mqtt_message(hass, topic, new_config) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index fa30283962b4bd..23c63c9ba58b3a 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -368,7 +368,7 @@ async def test_name_attribute_is_set_or_not( hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Gate", "state_topic": "test-topic", "device_class": "door", ' - '"object_id": "gate",' + '"default_entity_id": "binary_sensor.gate",' '"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}' "}", ) @@ -384,7 +384,7 @@ async def test_name_attribute_is_set_or_not( hass, "homeassistant/binary_sensor/bla/config", '{ "state_topic": "test-topic", "device_class": "door", ' - '"object_id": "gate",' + '"default_entity_id": "binary_sensor.gate",' '"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}' "}", ) @@ -400,7 +400,7 @@ async def test_name_attribute_is_set_or_not( hass, "homeassistant/binary_sensor/bla/config", '{ "name": null, "state_topic": "test-topic", "device_class": "door", ' - '"object_id": "gate",' + '"default_entity_id": "binary_sensor.gate",' '"device": {"identifiers": "very_unique", "name": "xyz_door_sensor"}' "}", ) @@ -468,6 +468,40 @@ async def test_value_template_fails( ) +@pytest.mark.parametrize( + "hass_config", + [ + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "test", + "state_topic": "test-topic", + "object_id": "test", + } + } + }, + ], +) +async def test_deprecated_option_object_id_is_used_in_yaml( + hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator +) -> None: + """Test issue registry in case the deprecated option object_id was used in YAML.""" + await mqtt_mock_entry() + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state is not None + + issue_registry = ir.async_get(hass) + issue = issue_registry.async_get_issue(mqtt.DOMAIN, "sensor.test") + assert issue is not None + assert issue.translation_placeholders == { + "entity_id": "sensor.test", + "object_id": "test", + "domain": "sensor", + } + + @pytest.mark.parametrize( "mqtt_config_subentries_data", [ diff --git a/tests/components/mqtt/test_notify.py b/tests/components/mqtt/test_notify.py index 56da809d1b645a..cd919d3c94d3f4 100644 --- a/tests/components/mqtt/test_notify.py +++ b/tests/components/mqtt/test_notify.py @@ -54,7 +54,7 @@ notify.DOMAIN: { "command_topic": "command-topic", "name": "test", - "object_id": "test_notify", + "default_entity_id": "notify.test_notify", "qos": "2", } }