diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index dbed1c8aa9e8b..06f91403057b0 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -89,7 +89,7 @@ CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_NIGHT, default=DEFAULT_ARM_NIGHT): cv.string, @@ -136,6 +136,7 @@ async def _async_setup_entity( class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Representation of a MQTT alarm status.""" + _default_name = DEFAULT_NAME _entity_id_format = alarm.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_ALARM_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index 50af9ef8a555e..0d4b2c4a7b457 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -61,7 +61,7 @@ vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OFF_DELAY): cv.positive_int, vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, @@ -97,6 +97,7 @@ async def _async_setup_entity( class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity): """Representation a binary sensor that is updated by MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = binary_sensor.ENTITY_ID_FORMAT _expired: bool | None _expire_after: int | None diff --git a/homeassistant/components/mqtt/button.py b/homeassistant/components/mqtt/button.py index 46ecc16d38551..9b3b04a54f5c6 100644 --- a/homeassistant/components/mqtt/button.py +++ b/homeassistant/components/mqtt/button.py @@ -35,7 +35,7 @@ vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } @@ -70,6 +70,7 @@ async def _async_setup_entity( class MqttButton(MqttEntity, ButtonEntity): """Representation of a switch that can be toggled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = button.ENTITY_ID_FORMAT def __init__( diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 75ab25efcfa28..166bfdd38ccdc 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -41,7 +41,7 @@ PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional(CONF_IMAGE_ENCODING): "b64", } @@ -80,6 +80,7 @@ async def _async_setup_entity( class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" + _default_name = DEFAULT_NAME _entity_id_format: str = camera.ENTITY_ID_FORMAT _attributes_extra_blocked: frozenset[str] = MQTT_CAMERA_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index 676e5b50f4944..f29a114620ace 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -296,7 +296,7 @@ def valid_humidity_state_configuration(config: ConfigType) -> ConfigType: ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, @@ -597,6 +597,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: class MqttClimate(MqttTemperatureControlEntity, ClimateEntity): """Representation of an MQTT climate device.""" + _default_name = DEFAULT_NAME _entity_id_format = climate.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_CLIMATE_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 0b435db0b7a97..c11cf2dfb85d6 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -159,7 +159,7 @@ def validate_options(config: ConfigType) -> ConfigType: vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None), vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_CLOSE, default=DEFAULT_PAYLOAD_CLOSE): vol.Any( cv.string, None @@ -236,6 +236,7 @@ async def _async_setup_entity( class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format: str = cover.ENTITY_ID_FORMAT _attributes_extra_blocked: frozenset[str] = MQTT_COVER_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/device_tracker.py b/homeassistant/components/mqtt/device_tracker.py index a9c4017593c9a..dd4eca9878a4e 100644 --- a/homeassistant/components/mqtt/device_tracker.py +++ b/homeassistant/components/mqtt/device_tracker.py @@ -61,7 +61,7 @@ def valid_config(config: ConfigType) -> ConfigType: { vol.Optional(CONF_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, vol.Optional(CONF_PAYLOAD_NOT_HOME, default=STATE_NOT_HOME): cv.string, vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, @@ -104,6 +104,7 @@ async def _async_setup_entity( class MqttDeviceTracker(MqttEntity, TrackerEntity): """Representation of a device tracker using MQTT.""" + _default_name = None _entity_id_format = device_tracker.ENTITY_ID_FORMAT _value_template: Callable[..., ReceivePayloadType] diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index f5e92d8ecf958..58189c3cb3e8c 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -127,7 +127,7 @@ def valid_preset_mode_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_DIRECTION_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_DIRECTION_COMMAND_TEMPLATE): cv.template, @@ -215,6 +215,7 @@ async def _async_setup_entity( class MqttFan(MqttEntity, FanEntity): """A MQTT fan component.""" + _default_name = DEFAULT_NAME _entity_id_format = fan.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_FAN_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/humidifier.py b/homeassistant/components/mqtt/humidifier.py index 392a112bcdb79..aebb05c19f7cd 100644 --- a/homeassistant/components/mqtt/humidifier.py +++ b/homeassistant/components/mqtt/humidifier.py @@ -136,7 +136,7 @@ def valid_humidity_range_configuration(config: ConfigType) -> ConfigType: vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_VALUE_TEMPLATE): cv.template, @@ -207,6 +207,7 @@ async def _async_setup_entity( class MqttHumidifier(MqttEntity, HumidifierEntity): """A MQTT humidifier component.""" + _default_name = DEFAULT_NAME _entity_id_format = humidifier.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_HUMIDIFIER_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/image.py b/homeassistant/components/mqtt/image.py index 2764539770daf..a21d45369f8fd 100644 --- a/homeassistant/components/mqtt/image.py +++ b/homeassistant/components/mqtt/image.py @@ -61,7 +61,7 @@ def validate_topic_required(config: ConfigType) -> ConfigType: PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend( { vol.Optional(CONF_CONTENT_TYPE): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Exclusive(CONF_URL_TOPIC, "image_topic"): valid_subscribe_topic, vol.Exclusive(CONF_IMAGE_TOPIC, "image_topic"): valid_subscribe_topic, vol.Optional(CONF_IMAGE_ENCODING): "b64", @@ -102,6 +102,7 @@ async def _async_setup_entity( class MqttImage(MqttEntity, ImageEntity): """representation of a MQTT image.""" + _default_name = DEFAULT_NAME _entity_id_format: str = image.ENTITY_ID_FORMAT _last_image: bytes | None = None _client: httpx.AsyncClient diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index fe09667ca4aaa..2a726075bb0da 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -190,7 +190,7 @@ vol.Optional(CONF_HS_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_ON_COMMAND_TYPE, default=DEFAULT_ON_COMMAND_TYPE): vol.In( VALUES_ON_COMMAND_TYPE ), @@ -242,6 +242,7 @@ async def async_setup_entity_basic( class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT light.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED _topic: dict[str, str | None] diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 70992887ca7f2..8f710eb5ea66b 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -132,7 +132,7 @@ def valid_color_configuration(config: ConfigType) -> ConfigType: vol.Optional(CONF_HS, default=DEFAULT_HS): cv.boolean, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_QOS, default=DEFAULT_QOS): vol.All( vol.Coerce(int), vol.In([0, 1, 2]) ), @@ -180,6 +180,7 @@ async def async_setup_entity_json( class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 063895d738c99..98ee7648eebd8 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -100,7 +100,7 @@ vol.Optional(CONF_GREEN_TEMPLATE): cv.template, vol.Optional(CONF_MAX_MIREDS): cv.positive_int, vol.Optional(CONF_MIN_MIREDS): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_RED_TEMPLATE): cv.template, vol.Optional(CONF_STATE_TEMPLATE): cv.template, } @@ -128,6 +128,7 @@ async def async_setup_entity_template( class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED _optimistic: bool diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 966cbc211055e..cb586c0630929 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -76,7 +76,7 @@ { vol.Optional(CONF_CODE_FORMAT): cv.is_regex, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, vol.Optional(CONF_PAYLOAD_UNLOCK, default=DEFAULT_PAYLOAD_UNLOCK): cv.string, vol.Optional(CONF_PAYLOAD_OPEN): cv.string, @@ -126,6 +126,7 @@ async def _async_setup_entity( class MqttLock(MqttEntity, LockEntity): """Representation of a lock that can be toggled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = lock.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LOCK_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py index 57ec933cd589c..ec437f08d39bc 100644 --- a/homeassistant/components/mqtt/mixins.py +++ b/homeassistant/components/mqtt/mixins.py @@ -50,7 +50,12 @@ async_track_device_registry_updated_event, async_track_entity_registry_updated_event, ) -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from homeassistant.helpers.typing import ( + UNDEFINED, + ConfigType, + DiscoveryInfoType, + UndefinedType, +) from homeassistant.util.json import json_loads from . import debug_info, subscription @@ -999,7 +1004,9 @@ class MqttEntity( ): """Representation of an MQTT entity.""" + _attr_has_entity_name = True _attr_should_poll = False + _default_name: str | None _entity_id_format: str def __init__( @@ -1016,8 +1023,8 @@ def __init__( self._sub_state: dict[str, EntitySubscription] = {} # Load config - self._setup_common_attributes_from_config(self._config) self._setup_from_config(self._config) + self._setup_common_attributes_from_config(self._config) # Initialize entity_id from config self._init_entity_id() @@ -1058,8 +1065,8 @@ async def discovery_update(self, discovery_payload: MQTTDiscoveryPayload) -> Non async_handle_schema_error(discovery_payload, err) return self._config = config - self._setup_common_attributes_from_config(self._config) self._setup_from_config(self._config) + self._setup_common_attributes_from_config(self._config) # Prepare MQTT subscriptions self.attributes_prepare_discovery_update(config) @@ -1107,6 +1114,23 @@ async def async_publish( def config_schema() -> vol.Schema: """Return the config schema.""" + def _set_entity_name(self, config: ConfigType) -> None: + """Help setting the entity name if needed.""" + entity_name: str | None | UndefinedType = config.get(CONF_NAME, UNDEFINED) + # Only set _attr_name if it is needed + if entity_name is not UNDEFINED: + self._attr_name = entity_name + elif not self._default_to_device_class_name(): + # Assign the default name + self._attr_name = self._default_name + if CONF_DEVICE in config: + if CONF_NAME not in config[CONF_DEVICE]: + _LOGGER.info( + "MQTT device information always needs to include a name, got %s, " + "if device information is shared between multiple entities, the device " + "name must be included in each entity's device configuration", + ) + def _setup_common_attributes_from_config(self, config: ConfigType) -> None: """(Re)Setup the common attributes for the entity.""" self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -1114,7 +1138,8 @@ def _setup_common_attributes_from_config(self, config: ConfigType) -> None: config.get(CONF_ENABLED_BY_DEFAULT) ) self._attr_icon = config.get(CONF_ICON) - self._attr_name = config.get(CONF_NAME) + # Set the entity name if needed + self._set_entity_name(config) def _setup_from_config(self, config: ConfigType) -> None: """(Re)Setup the entity.""" diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py index 5986eab1207e6..971b44b43bfcc 100644 --- a/homeassistant/components/mqtt/number.py +++ b/homeassistant/components/mqtt/number.py @@ -87,7 +87,7 @@ def validate_config(config: ConfigType) -> ConfigType: vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float), vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float), vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_RESET, default=DEFAULT_PAYLOAD_RESET): cv.string, vol.Optional(CONF_STEP, default=DEFAULT_STEP): vol.All( vol.Coerce(float), vol.Range(min=1e-3) @@ -134,6 +134,7 @@ async def _async_setup_entity( class MqttNumber(MqttEntity, RestoreNumber): """representation of an MQTT number.""" + _default_name = DEFAULT_NAME _entity_id_format = number.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_NUMBER_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index f716e4fe46f8d..5e12f67a698f3 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -34,7 +34,7 @@ { vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic, vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ON): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, @@ -77,6 +77,7 @@ class MqttScene( ): """Representation of a scene that can be activated using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = scene.DOMAIN + ".{}" def __init__( diff --git a/homeassistant/components/mqtt/select.py b/homeassistant/components/mqtt/select.py index 26e72af919239..df8cf024bd26c 100644 --- a/homeassistant/components/mqtt/select.py +++ b/homeassistant/components/mqtt/select.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Required(CONF_OPTIONS): cv.ensure_list, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, @@ -89,6 +89,7 @@ async def _async_setup_entity( class MqttSelect(MqttEntity, SelectEntity, RestoreEntity): """representation of an MQTT select.""" + _default_name = DEFAULT_NAME _entity_id_format = select.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SELECT_ATTRIBUTES_BLOCKED _command_template: Callable[[PublishPayloadType], PublishPayloadType] diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index e4b5f61bda0c2..ae94b0df0ce68 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -78,7 +78,7 @@ vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_SUGGESTED_DISPLAY_PRECISION): cv.positive_int, vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None), vol.Optional(CONF_UNIT_OF_MEASUREMENT): vol.Any(cv.string, None), @@ -126,6 +126,7 @@ async def _async_setup_entity( class MqttSensor(MqttEntity, RestoreSensor): """Representation of a sensor that can be updated using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attr_last_reset: datetime | None = None _attributes_extra_blocked = MQTT_SENSOR_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/siren.py b/homeassistant/components/mqtt/siren.py index d30080f4647d1..328812a6e49eb 100644 --- a/homeassistant/components/mqtt/siren.py +++ b/homeassistant/components/mqtt/siren.py @@ -79,7 +79,7 @@ vol.Optional(CONF_AVAILABLE_TONES): cv.ensure_list, vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_COMMAND_OFF_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, @@ -138,6 +138,7 @@ async def _async_setup_entity( class MqttSiren(MqttEntity, SirenEntity): """Representation of a siren that can be controlled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_SIREN_ATTRIBUTES_BLOCKED _extra_attributes: dict[str, Any] diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 7f4f609f265ef..107b0b1cb10d9 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -49,7 +49,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string, vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string, vol.Optional(CONF_STATE_OFF): cv.string, @@ -88,6 +88,7 @@ async def _async_setup_entity( class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" + _default_name = DEFAULT_NAME _entity_id_format = switch.ENTITY_ID_FORMAT _optimistic: bool diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index 01622c10a6de6..13677b7f35b22 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -78,7 +78,7 @@ def valid_text_size_configuration(config: ConfigType) -> ConfigType: _PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TEMPLATE): cv.template, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_MAX, default=MAX_LENGTH_STATE_STATE): cv.positive_int, vol.Optional(CONF_MIN, default=0): cv.positive_int, vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In( @@ -125,6 +125,7 @@ class MqttTextEntity(MqttEntity, TextEntity): """Representation of the MQTT text entity.""" _attributes_extra_blocked = MQTT_TEXT_ATTRIBUTES_BLOCKED + _default_name = DEFAULT_NAME _entity_id_format = text.ENTITY_ID_FORMAT _compiled_pattern: re.Pattern[Any] | None diff --git a/homeassistant/components/mqtt/update.py b/homeassistant/components/mqtt/update.py index 930f4d225063c..f6db0d3fd64c4 100644 --- a/homeassistant/components/mqtt/update.py +++ b/homeassistant/components/mqtt/update.py @@ -57,7 +57,7 @@ vol.Optional(CONF_ENTITY_PICTURE): cv.string, vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template, vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_PAYLOAD_INSTALL): cv.string, vol.Optional(CONF_RELEASE_SUMMARY): cv.string, vol.Optional(CONF_RELEASE_URL): cv.string, @@ -107,6 +107,7 @@ async def _async_setup_entity( class MqttUpdate(MqttEntity, UpdateEntity, RestoreEntity): """Representation of the MQTT update entity.""" + _default_name = DEFAULT_NAME _entity_id_format = update.ENTITY_ID_FORMAT def __init__( diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 7c73e57911248..516a7772c1117 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -131,7 +131,7 @@ ), vol.Inclusive(CONF_FAN_SPEED_TEMPLATE, "fan_speed"): cv.template, vol.Inclusive(CONF_FAN_SPEED_TOPIC, "fan_speed"): valid_publish_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT ): cv.string, @@ -215,6 +215,7 @@ async def async_setup_entity_legacy( class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_LEGACY_VACUUM_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index ee06131af0251..5113e19f097e1 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -126,7 +126,7 @@ vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional( CONF_PAYLOAD_CLEAN_SPOT, default=DEFAULT_PAYLOAD_CLEAN_SPOT ): cv.string, @@ -170,6 +170,7 @@ async def async_setup_entity_state( class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" + _default_name = DEFAULT_NAME _entity_id_format = ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_VACUUM_ATTRIBUTES_BLOCKED diff --git a/homeassistant/components/mqtt/water_heater.py b/homeassistant/components/mqtt/water_heater.py index 0f622d55b84f7..17e9430dba34f 100644 --- a/homeassistant/components/mqtt/water_heater.py +++ b/homeassistant/components/mqtt/water_heater.py @@ -123,7 +123,7 @@ ): cv.ensure_list, vol.Optional(CONF_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_MODE_STATE_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME): vol.Any(cv.string, None), vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_ON, default="ON"): cv.string, vol.Optional(CONF_PAYLOAD_OFF, default="OFF"): cv.string, @@ -180,6 +180,7 @@ async def _async_setup_entity( class MqttWaterHeater(MqttTemperatureControlEntity, WaterHeaterEntity): """Representation of an MQTT water heater device.""" + _default_name = DEFAULT_NAME _entity_id_format = water_heater.ENTITY_ID_FORMAT _attributes_extra_blocked = MQTT_WATER_HEATER_ATTRIBUTES_BLOCKED diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index d1b1d6b68b38a..e69839e6b1631 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -55,6 +55,7 @@ help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_publishing_with_custom_encoding, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1120,3 +1121,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None)], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = alarm_control_panel.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index d32754625f423..28bf5f558cbcf 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -41,6 +41,7 @@ help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_reload_with_config, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1227,3 +1228,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Door", "door"), ("Battery", "battery"), ("Motion", "motion")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = binary_sensor.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_button.py b/tests/components/mqtt/test_button.py index fa16ef7781780..481e98f00997e 100644 --- a/tests/components/mqtt/test_button.py +++ b/tests/components/mqtt/test_button.py @@ -30,6 +30,7 @@ help_test_entity_device_info_with_connection, help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, + help_test_entity_name, help_test_publishing_with_custom_encoding, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -569,3 +570,26 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [ + ("test", None), + ("Update", "update"), + ("Identify", "identify"), + ("Restart", "restart"), + ], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = button.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index fd760044f3c80..9d580da073edb 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -1117,6 +1117,45 @@ async def help_test_entity_device_info_update( assert device.name == "Milk" +async def help_test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + domain: str, + config: ConfigType, + expected_friendly_name: str | None = None, + device_class: str | None = None, +) -> None: + """Test device name setup with and without a device_class set. + + This is a test helper for the _setup_common_attributes_from_config mixin. + """ + await mqtt_mock_entry() + # Add device settings to config + config = copy.deepcopy(config[mqtt.DOMAIN][domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + expected_entity_name = "test" + if device_class is not None: + config["device_class"] = device_class + # Do not set a name + config.pop("name") + expected_entity_name = device_class + + registry = dr.async_get(hass) + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}) + assert device is not None + + entity_id = f"{domain}.beer_{expected_entity_name}" + state = hass.states.get(entity_id) + assert state is not None + assert state.name == f"Beer {expected_friendly_name}" + + async def help_test_entity_id_update_subscriptions( hass: HomeAssistant, mqtt_mock_entry: MqttMockHAClientGenerator, @@ -1390,7 +1429,7 @@ async def help_test_entity_debug_info_message( with patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt if service: - service_data = {ATTR_ENTITY_ID: f"{domain}.test"} + service_data = {ATTR_ENTITY_ID: f"{domain}.beer_test"} if service_parameters: service_data.update(service_parameters) @@ -1458,7 +1497,7 @@ async def help_test_entity_debug_info_remove( "subscriptions" ] assert len(debug_info_data["triggers"]) == 0 - assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.beer_test" entity_id = debug_info_data["entities"][0]["entity_id"] async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") @@ -1503,7 +1542,7 @@ async def help_test_entity_debug_info_update_entity_id( == f"homeassistant/{domain}/bla/config" ) assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config - assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.beer_test" assert len(debug_info_data["entities"][0]["subscriptions"]) == 1 assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ "subscriptions" @@ -1511,7 +1550,7 @@ async def help_test_entity_debug_info_update_entity_id( assert len(debug_info_data["triggers"]) == 0 entity_registry.async_update_entity( - f"{domain}.test", new_entity_id=f"{domain}.milk" + f"{domain}.beer_test", new_entity_id=f"{domain}.milk" ) await hass.async_block_till_done() await hass.async_block_till_done() @@ -1529,7 +1568,7 @@ async def help_test_entity_debug_info_update_entity_id( "subscriptions" ] assert len(debug_info_data["triggers"]) == 0 - assert f"{domain}.test" not in hass.data["mqtt"].debug_info_entities + assert f"{domain}.beer_test" not in hass.data["mqtt"].debug_info_entities async def help_test_entity_disabled_by_default( diff --git a/tests/components/mqtt/test_diagnostics.py b/tests/components/mqtt/test_diagnostics.py index fb1033848743c..eb923ac2f0768 100644 --- a/tests/components/mqtt/test_diagnostics.py +++ b/tests/components/mqtt/test_diagnostics.py @@ -80,7 +80,7 @@ async def test_entry_diagnostics( expected_debug_info = { "entities": [ { - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "subscriptions": [{"topic": "foobar/sensor", "messages": []}], "discovery_data": { "payload": config_sensor, @@ -109,13 +109,13 @@ async def test_entry_diagnostics( "disabled": False, "disabled_by": None, "entity_category": None, - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "icon": None, "original_device_class": None, "original_icon": None, "state": { "attributes": {"friendly_name": "MQTT Sensor"}, - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "last_changed": ANY, "last_updated": ANY, "state": "unknown", diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 62b87bdb791c0..d3b8a145df7d8 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -729,10 +729,10 @@ async def test_cleanup_device( # Verify device and registry entries are created device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None # Remove MQTT from the device @@ -753,11 +753,11 @@ async def test_cleanup_device( # Verify device and registry entries are cleared device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -788,10 +788,10 @@ async def test_cleanup_device_mqtt( # Verify device and registry entries are created device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", "") @@ -801,11 +801,11 @@ async def test_cleanup_device_mqtt( # Verify device and registry entries are cleared device_entry = device_registry.async_get_device(identifiers={("mqtt", "0AFFD2")}) assert device_entry is None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -873,10 +873,10 @@ async def test_cleanup_device_multiple_config_entries( mqtt_config_entry.entry_id, config_entry.entry_id, } - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None # Remove MQTT from the device @@ -900,12 +900,12 @@ async def test_cleanup_device_multiple_config_entries( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -973,10 +973,10 @@ async def test_cleanup_device_multiple_config_entries_mqtt( mqtt_config_entry.entry_id, config_entry.entry_id, } - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert entity_entry is not None - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is not None # Send MQTT messages to remove @@ -992,12 +992,12 @@ async def test_cleanup_device_multiple_config_entries_mqtt( connections={("mac", "12:34:56:AB:CD:EF")} ) assert device_entry is not None - entity_entry = entity_registry.async_get("sensor.mqtt_sensor") + entity_entry = entity_registry.async_get("sensor.none_mqtt_sensor") assert device_entry.config_entries == {config_entry.entry_id} assert entity_entry is None # Verify state is removed - state = hass.states.get("sensor.mqtt_sensor") + state = hass.states.get("sensor.none_mqtt_sensor") assert state is None await hass.async_block_till_done() @@ -1474,13 +1474,12 @@ async def test_clear_config_topic_disabled_entity( mqtt_mock = await mqtt_mock_entry() # discover an entity that is not enabled by default config = { - "name": "sbfspot_12345", "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", "unique_id": "sbfspot_12345", "enabled_by_default": False, "device": { "identifiers": ["sbfspot_12345"], - "name": "sbfspot_12345", + "name": "abc123", "sw_version": "1.0", "connections": [["mac", "12:34:56:AB:CD:EF"]], }, @@ -1512,9 +1511,9 @@ async def test_clear_config_topic_disabled_entity( await hass.async_block_till_done() assert "Platform mqtt does not generate unique IDs" in caplog.text - assert hass.states.get("sensor.sbfspot_12345") is None # disabled - assert hass.states.get("sensor.sbfspot_12345_1") is not None # enabled - assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + assert hass.states.get("sensor.abc123_sbfspot_12345") is None # disabled + assert hass.states.get("sensor.abc123_sbfspot_12345_1") is not None # enabled + assert hass.states.get("sensor.abc123_sbfspot_12345_2") is None # not unique # Verify device is created device_entry = device_registry.async_get_device( @@ -1603,13 +1602,12 @@ async def test_unique_id_collission_has_priority( """Test the unique_id collision detection has priority over registry disabled items.""" await mqtt_mock_entry() config = { - "name": "sbfspot_12345", "state_topic": "homeassistant_test/sensor/sbfspot_0/sbfspot_12345/", "unique_id": "sbfspot_12345", "enabled_by_default": False, "device": { "identifiers": ["sbfspot_12345"], - "name": "sbfspot_12345", + "name": "abc123", "sw_version": "1.0", "connections": [["mac", "12:34:56:AB:CD:EF"]], }, @@ -1633,13 +1631,13 @@ async def test_unique_id_collission_has_priority( ) await hass.async_block_till_done() - assert hass.states.get("sensor.sbfspot_12345_1") is None # not enabled - assert hass.states.get("sensor.sbfspot_12345_2") is None # not unique + assert hass.states.get("sensor.abc123_sbfspot_12345_1") is None # not enabled + assert hass.states.get("sensor.abc123_sbfspot_12345_2") is None # not unique # Verify the first entity is created - assert entity_registry.async_get("sensor.sbfspot_12345_1") is not None + assert entity_registry.async_get("sensor.abc123_sbfspot_12345_1") is not None # Verify the second entity is not created because it is not unique - assert entity_registry.async_get("sensor.sbfspot_12345_2") is None + assert entity_registry.async_get("sensor.abc123_sbfspot_12345_2") is None @patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3395dc0825f23..c0d7a94de5b79 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -2821,7 +2821,7 @@ async def test_mqtt_ws_get_device_debug_info( expected_result = { "entities": [ { - "entity_id": "sensor.mqtt_sensor", + "entity_id": "sensor.none_mqtt_sensor", "subscriptions": [{"topic": "foobar/sensor", "messages": []}], "discovery_data": { "payload": config_sensor, @@ -2884,7 +2884,7 @@ async def test_mqtt_ws_get_device_debug_info_binary( expected_result = { "entities": [ { - "entity_id": "camera.mqtt_camera", + "entity_id": "camera.none_mqtt_camera", "subscriptions": [ { "topic": "foobar/image", diff --git a/tests/components/mqtt/test_mixins.py b/tests/components/mqtt/test_mixins.py index c7285f0fa5f49..5a30a3a65de05 100644 --- a/tests/components/mqtt/test_mixins.py +++ b/tests/components/mqtt/test_mixins.py @@ -5,8 +5,12 @@ import pytest from homeassistant.components import mqtt, sensor +from homeassistant.components.mqtt.sensor import DEFAULT_NAME as DEFAULT_SENSOR_NAME from homeassistant.const import EVENT_STATE_CHANGED, Platform from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import ( + device_registry as dr, +) from tests.common import async_fire_mqtt_message from tests.typing import MqttMockHAClientGenerator @@ -73,3 +77,179 @@ def test_callback(event) -> None: # The availability is changed but the topic is shared, # hence there the state will be written when the value is updated assert len(events) == 1 + + +@pytest.mark.parametrize( + ("hass_config", "entity_id", "friendly_name", "device_name", "assert_log"), + [ + ( # default_entity_name_without_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.none_mqtt_sensor", + DEFAULT_SENSOR_NAME, + None, + True, + ), + ( # default_entity_name_with_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test_mqtt_sensor", + "Test MQTT Sensor", + "Test", + False, + ), + ( # name_follows_device_class + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test_humidity", + "Test Humidity", + "Test", + False, + ), + ( # name_follows_device_class_without_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.none_humidity", + "Humidity", + None, + True, + ), + ( # name_overrides_device_class + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "MySensor", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test_mysensor", + "Test MySensor", + "Test", + False, + ), + ( # name_set_no_device_name_set + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": "MySensor", + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.none_mysensor", + "MySensor", + None, + True, + ), + ( # none_entity_name_with_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": None, + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"name": "Test", "identifiers": ["helloworld"]}, + } + } + }, + "sensor.test", + "Test", + "Test", + False, + ), + ( # none_entity_name_without_device_name + { + mqtt.DOMAIN: { + sensor.DOMAIN: { + "name": None, + "state_topic": "test-topic", + "unique_id": "veryunique", + "device_class": "humidity", + "device": {"identifiers": ["helloworld"]}, + } + } + }, + "sensor.mqtt_veryunique", + "mqtt veryunique", + None, + True, + ), + ], + ids=[ + "default_entity_name_without_device_name", + "default_entity_name_with_device_name", + "name_follows_device_class", + "name_follows_device_class_without_device_name", + "name_overrides_device_class", + "name_set_no_device_name_set", + "none_entity_name_with_device_name", + "none_entity_name_without_device_name", + ], +) +@patch("homeassistant.components.mqtt.PLATFORMS", [Platform.SENSOR]) +async def test_default_entity_and_device_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, + entity_id: str, + friendly_name: str, + device_name: str | None, + assert_log: bool, +) -> None: + """Test device name setup with and without a device_class set. + + This is a test helper for the _setup_common_attributes_from_config mixin. + """ + await mqtt_mock_entry() + + registry = dr.async_get(hass) + + device = registry.async_get_device({("mqtt", "helloworld")}) + assert device is not None + assert device.name == device_name + + state = hass.states.get(entity_id) + assert state is not None + assert state.name == friendly_name + + assert ( + "MQTT device information always needs to include a name" in caplog.text + ) is assert_log diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index 96d9cdcef6417..dbdd373a659d8 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -48,6 +48,7 @@ help_test_entity_device_info_with_identifier, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_publishing_with_custom_encoding, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1121,3 +1122,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Humidity", "humidity"), ("Temperature", "temperature")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = number.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index d6ab692af5274..30eb0fd193971 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -53,6 +53,7 @@ help_test_entity_disabled_by_default, help_test_entity_id_update_discovery_update, help_test_entity_id_update_subscriptions, + help_test_entity_name, help_test_reload_with_config, help_test_reloadable, help_test_setting_attribute_via_mqtt_json_message, @@ -1409,3 +1410,21 @@ async def test_unload_entry( await help_test_unload_config_entry_with_platform( hass, mqtt_mock_entry, domain, config ) + + +@pytest.mark.parametrize( + ("expected_friendly_name", "device_class"), + [("test", None), ("Humidity", "humidity"), ("Temperature", "temperature")], +) +async def test_entity_name( + hass: HomeAssistant, + mqtt_mock_entry: MqttMockHAClientGenerator, + expected_friendly_name: str | None, + device_class: str | None, +) -> None: + """Test the entity name setup.""" + domain = sensor.DOMAIN + config = DEFAULT_CONFIG + await help_test_entity_name( + hass, mqtt_mock_entry, domain, config, expected_friendly_name, device_class + )