Add entity category option to entities set up via an MQTT subentry (#146776)

* Add entity category option to entities set up via an MQTT subentry

* Rephrase

* typo

* Move entity category to entity details - remove service to action

* Move entity category to entity platform config flow step
This commit is contained in:
Jan Bouwhuis 2025-06-25 09:28:37 +02:00 committed by GitHub
parent 51fb1ab8b6
commit 85e9919bbd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 135 additions and 57 deletions

View File

@ -66,6 +66,7 @@ from homeassistant.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_DISCOVERY, CONF_DISCOVERY,
CONF_EFFECT, CONF_EFFECT,
CONF_ENTITY_CATEGORY,
CONF_HOST, CONF_HOST,
CONF_NAME, CONF_NAME,
CONF_OPTIMISTIC, CONF_OPTIMISTIC,
@ -84,6 +85,7 @@ from homeassistant.const import (
STATE_CLOSING, STATE_CLOSING,
STATE_OPEN, STATE_OPEN,
STATE_OPENING, STATE_OPENING,
EntityCategory,
) )
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section
@ -411,6 +413,14 @@ SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
): TEXT_SELECTOR, ): TEXT_SELECTOR,
} }
) )
ENTITY_CATEGORY_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[category.value for category in EntityCategory],
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_ENTITY_CATEGORY,
sort=True,
)
)
# Sensor specific selectors # Sensor specific selectors
SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector( SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
@ -429,6 +439,15 @@ BINARY_SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
sort=True, sort=True,
) )
) )
SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
SelectSelectorConfig(
options=[EntityCategory.DIAGNOSTIC.value],
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_ENTITY_CATEGORY,
sort=True,
)
)
BUTTON_DEVICE_CLASS_SELECTOR = SelectSelector( BUTTON_DEVICE_CLASS_SELECTOR = SelectSelector(
SelectSelectorConfig( SelectSelectorConfig(
options=[device_class.value for device_class in ButtonDeviceClass], options=[device_class.value for device_class in ButtonDeviceClass],
@ -735,12 +754,25 @@ COMMON_ENTITY_FIELDS: dict[str, PlatformField] = {
), ),
} }
SHARED_PLATFORM_ENTITY_FIELDS: dict[str, PlatformField] = {
CONF_ENTITY_CATEGORY: PlatformField(
selector=ENTITY_CATEGORY_SELECTOR,
required=False,
default=None,
),
}
PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
Platform.BINARY_SENSOR.value: { Platform.BINARY_SENSOR.value: {
CONF_DEVICE_CLASS: PlatformField( CONF_DEVICE_CLASS: PlatformField(
selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR, selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR,
required=False, required=False,
), ),
CONF_ENTITY_CATEGORY: PlatformField(
selector=SENSOR_ENTITY_CATEGORY_SELECTOR,
required=False,
default=None,
),
}, },
Platform.BUTTON.value: { Platform.BUTTON.value: {
CONF_DEVICE_CLASS: PlatformField( CONF_DEVICE_CLASS: PlatformField(
@ -804,6 +836,11 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
required=False, required=False,
conditions=({"device_class": "enum"},), conditions=({"device_class": "enum"},),
), ),
CONF_ENTITY_CATEGORY: PlatformField(
selector=SENSOR_ENTITY_CATEGORY_SELECTOR,
required=False,
default=None,
),
}, },
Platform.SWITCH.value: { Platform.SWITCH.value: {
CONF_DEVICE_CLASS: PlatformField( CONF_DEVICE_CLASS: PlatformField(
@ -2070,8 +2107,6 @@ def data_schema_from_fields(
if field_details.section == schema_section if field_details.section == schema_section
and field_details.exclude_from_reconfig and field_details.exclude_from_reconfig
} }
if not data_element_options:
continue
if schema_section is None: if schema_section is None:
data_schema.update(data_schema_element) data_schema.update(data_schema_element)
continue continue
@ -2834,7 +2869,9 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
assert self._component_id is not None assert self._component_id is not None
component_data = self._subentry_data["components"][self._component_id] component_data = self._subentry_data["components"][self._component_id]
platform = component_data[CONF_PLATFORM] platform = component_data[CONF_PLATFORM]
data_schema_fields = PLATFORM_ENTITY_FIELDS[platform] data_schema_fields = (
SHARED_PLATFORM_ENTITY_FIELDS | PLATFORM_ENTITY_FIELDS[platform]
)
errors: dict[str, str] = {} errors: dict[str, str] = {}
data_schema = data_schema_from_fields( data_schema = data_schema_from_fields(
@ -2845,8 +2882,6 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
component_data=component_data, component_data=component_data,
user_input=user_input, user_input=user_input,
) )
if not data_schema.schema:
return await self.async_step_mqtt_platform_config()
if user_input is not None: if user_input is not None:
# Test entity fields against the validator # Test entity fields against the validator
merged_user_input, errors = validate_user_input( merged_user_input, errors = validate_user_input(
@ -2940,6 +2975,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
platform = component_data[CONF_PLATFORM] platform = component_data[CONF_PLATFORM]
platform_fields: dict[str, PlatformField] = ( platform_fields: dict[str, PlatformField] = (
COMMON_ENTITY_FIELDS COMMON_ENTITY_FIELDS
| SHARED_PLATFORM_ENTITY_FIELDS
| PLATFORM_ENTITY_FIELDS[platform] | PLATFORM_ENTITY_FIELDS[platform]
| PLATFORM_MQTT_FIELDS[platform] | PLATFORM_MQTT_FIELDS[platform]
) )

View File

@ -313,6 +313,11 @@ def async_setup_entity_entry_helper(
component_config.pop("platform") component_config.pop("platform")
component_config.update(availability_config) component_config.update(availability_config)
component_config.update(device_mqtt_options) component_config.update(device_mqtt_options)
if (
CONF_ENTITY_CATEGORY in component_config
and component_config[CONF_ENTITY_CATEGORY] is None
):
component_config.pop(CONF_ENTITY_CATEGORY)
try: try:
config = platform_schema_modern(component_config) config = platform_schema_modern(component_config)

View File

@ -210,6 +210,7 @@
"description": "Please configure specific details for {platform} entity \"{entity}\":", "description": "Please configure specific details for {platform} entity \"{entity}\":",
"data": { "data": {
"device_class": "Device class", "device_class": "Device class",
"entity_category": "Entity category",
"fan_feature_speed": "Speed support", "fan_feature_speed": "Speed support",
"fan_feature_preset_modes": "Preset modes support", "fan_feature_preset_modes": "Preset modes support",
"fan_feature_oscillation": "Oscillation support", "fan_feature_oscillation": "Oscillation support",
@ -222,6 +223,7 @@
}, },
"data_description": { "data_description": {
"device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)", "device_class": "The Device class of the {platform} entity. [Learn more.]({url}#device_class)",
"entity_category": "Allow marking an entity as device configuration or diagnostics. An entity with a category will not be exposed to cloud, Alexa, or Google Assistant components, nor included in indirect action calls to devices or areas. Sensor entities cannot be assigned a device configiuration class. [Learn more.](https://developers.home-assistant.io/docs/core/entity/#registry-properties)",
"fan_feature_speed": "The fan supports multiple speeds.", "fan_feature_speed": "The fan supports multiple speeds.",
"fan_feature_preset_modes": "The fan supports preset modes.", "fan_feature_preset_modes": "The fan supports preset modes.",
"fan_feature_oscillation": "The fan supports oscillation.", "fan_feature_oscillation": "The fan supports oscillation.",
@ -883,6 +885,12 @@
"switch": "[%key:component::switch::title%]" "switch": "[%key:component::switch::title%]"
} }
}, },
"entity_category": {
"options": {
"config": "Config",
"diagnostic": "Diagnostic"
}
},
"light_schema": { "light_schema": {
"options": { "options": {
"basic": "Default schema", "basic": "Default schema",

View File

@ -71,6 +71,7 @@ MOCK_SUBENTRY_BINARY_SENSOR_COMPONENT = {
"platform": "binary_sensor", "platform": "binary_sensor",
"name": "Hatch", "name": "Hatch",
"device_class": "door", "device_class": "door",
"entity_category": None,
"state_topic": "test-topic", "state_topic": "test-topic",
"payload_on": "ON", "payload_on": "ON",
"payload_off": "OFF", "payload_off": "OFF",
@ -86,6 +87,7 @@ MOCK_SUBENTRY_BUTTON_COMPONENT = {
"name": "Restart", "name": "Restart",
"device_class": "restart", "device_class": "restart",
"command_topic": "test-topic", "command_topic": "test-topic",
"entity_category": None,
"payload_press": "PRESS", "payload_press": "PRESS",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"retain": False, "retain": False,
@ -97,6 +99,7 @@ MOCK_SUBENTRY_COVER_COMPONENT = {
"platform": "cover", "platform": "cover",
"name": "Blind", "name": "Blind",
"device_class": "blind", "device_class": "blind",
"entity_category": None,
"command_topic": "test-topic", "command_topic": "test-topic",
"payload_stop": None, "payload_stop": None,
"payload_stop_tilt": "STOP", "payload_stop_tilt": "STOP",
@ -132,6 +135,7 @@ MOCK_SUBENTRY_FAN_COMPONENT = {
"platform": "fan", "platform": "fan",
"name": "Breezer", "name": "Breezer",
"command_topic": "test-topic", "command_topic": "test-topic",
"entity_category": None,
"state_topic": "test-topic", "state_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"value_template": "{{ value_json.value }}", "value_template": "{{ value_json.value }}",
@ -169,6 +173,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT1 = {
"363a7ecad6be4a19b939a016ea93e994": { "363a7ecad6be4a19b939a016ea93e994": {
"platform": "notify", "platform": "notify",
"name": "Milkman alert", "name": "Milkman alert",
"entity_category": None,
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"entity_picture": "https://example.com/363a7ecad6be4a19b939a016ea93e994", "entity_picture": "https://example.com/363a7ecad6be4a19b939a016ea93e994",
@ -179,6 +184,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT2 = {
"6494827dac294fa0827c54b02459d309": { "6494827dac294fa0827c54b02459d309": {
"platform": "notify", "platform": "notify",
"name": "The second notifier", "name": "The second notifier",
"entity_category": None,
"command_topic": "test-topic2", "command_topic": "test-topic2",
"entity_picture": "https://example.com/6494827dac294fa0827c54b02459d309", "entity_picture": "https://example.com/6494827dac294fa0827c54b02459d309",
}, },
@ -187,6 +193,7 @@ MOCK_SUBENTRY_NOTIFY_COMPONENT_NO_NAME = {
"5269352dd9534c908d22812ea5d714cd": { "5269352dd9534c908d22812ea5d714cd": {
"platform": "notify", "platform": "notify",
"name": None, "name": None,
"entity_category": None,
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
"entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd", "entity_picture": "https://example.com/5269352dd9534c908d22812ea5d714cd",
@ -198,6 +205,7 @@ MOCK_SUBENTRY_SENSOR_COMPONENT = {
"e9261f6feed443e7b7d5f3fbe2a47412": { "e9261f6feed443e7b7d5f3fbe2a47412": {
"platform": "sensor", "platform": "sensor",
"name": "Energy", "name": "Energy",
"entity_category": None,
"device_class": "enum", "device_class": "enum",
"state_topic": "test-topic", "state_topic": "test-topic",
"options": ["low", "medium", "high"], "options": ["low", "medium", "high"],
@ -210,6 +218,7 @@ MOCK_SUBENTRY_SENSOR_COMPONENT_STATE_CLASS = {
"a0f85790a95d4889924602effff06b6e": { "a0f85790a95d4889924602effff06b6e": {
"platform": "sensor", "platform": "sensor",
"name": "Energy", "name": "Energy",
"entity_category": None,
"state_class": "measurement", "state_class": "measurement",
"state_topic": "test-topic", "state_topic": "test-topic",
"entity_picture": "https://example.com/a0f85790a95d4889924602effff06b6e", "entity_picture": "https://example.com/a0f85790a95d4889924602effff06b6e",
@ -219,6 +228,7 @@ MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = {
"e9261f6feed443e7b7d5f3fbe2a47412": { "e9261f6feed443e7b7d5f3fbe2a47412": {
"platform": "sensor", "platform": "sensor",
"name": "Energy", "name": "Energy",
"entity_category": None,
"state_class": "total", "state_class": "total",
"last_reset_value_template": "{{ value_json.value }}", "last_reset_value_template": "{{ value_json.value }}",
"state_topic": "test-topic", "state_topic": "test-topic",
@ -229,6 +239,7 @@ MOCK_SUBENTRY_SWITCH_COMPONENT = {
"3faf1318016c46c5aea26707eeb6f12e": { "3faf1318016c46c5aea26707eeb6f12e": {
"platform": "switch", "platform": "switch",
"name": "Outlet", "name": "Outlet",
"entity_category": None,
"device_class": "outlet", "device_class": "outlet",
"command_topic": "test-topic", "command_topic": "test-topic",
"state_topic": "test-topic", "state_topic": "test-topic",
@ -250,6 +261,7 @@ MOCK_SUBENTRY_LIGHT_BASIC_KELVIN_COMPONENT = {
"payload_off": "OFF", "payload_off": "OFF",
"payload_on": "ON", "payload_on": "ON",
"command_topic": "test-topic", "command_topic": "test-topic",
"entity_category": None,
"schema": "basic", "schema": "basic",
"state_topic": "test-topic", "state_topic": "test-topic",
"color_temp_kelvin": True, "color_temp_kelvin": True,

View File

@ -2941,8 +2941,8 @@ async def test_migrate_of_incompatible_config_entry(
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE, MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 1}}, {"name": "Milk notifier", "mqtt_settings": {"qos": 1}},
{"name": "Milkman alert"}, {"name": "Milkman alert"},
None, {},
None, (),
{ {
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
@ -2960,8 +2960,8 @@ async def test_migrate_of_incompatible_config_entry(
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME, MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, {"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{}, {},
None, {},
None, (),
{ {
"command_topic": "test-topic", "command_topic": "test-topic",
"command_template": "{{ value }}", "command_template": "{{ value }}",
@ -3220,9 +3220,7 @@ async def test_subentry_configflow(
"url": learn_more_url(component["platform"]), "url": learn_more_url(component["platform"]),
} }
# Process extra step if the platform supports it # Process entity details setep
if mock_entity_details_user_input is not None:
# Extra entity details flow step
assert result["step_id"] == "entity_platform_config" assert result["step_id"] == "entity_platform_config"
# First test validators if set of test # First test validators if set of test
@ -3248,9 +3246,6 @@ async def test_subentry_configflow(
"entity": entity_name, "entity": entity_name,
"url": learn_more_url(component["platform"]), "url": learn_more_url(component["platform"]),
} }
else:
# No details form step
assert result["step_id"] == "mqtt_platform_config"
# Process mqtt platform config flow # Process mqtt platform config flow
# Test an invalid mqtt user input case # Test an invalid mqtt user input case
@ -3501,6 +3496,16 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
}, },
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "entity_platform_config"
# submit the platform specific entity data with changed entity_category
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input={
"entity_category": "config",
},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "mqtt_platform_config" assert result["step_id"] == "mqtt_platform_config"
# submit the new platform specific entity data # submit the new platform specific entity data
@ -3547,7 +3552,7 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
), ),
), ),
(), (),
None, {},
{ {
"command_topic": "test-topic1-updated", "command_topic": "test-topic1-updated",
"command_template": "{{ value }}", "command_template": "{{ value }}",
@ -3608,8 +3613,8 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
title="Mock subentry", title="Mock subentry",
), ),
), ),
None, (),
None, {},
{ {
"command_topic": "test-topic1-updated", "command_topic": "test-topic1-updated",
"state_topic": "test-topic1-updated", "state_topic": "test-topic1-updated",
@ -3636,7 +3641,7 @@ async def test_subentry_reconfigure_edit_entity_single_entity(
tuple[dict[str, Any], dict[str, str] | None], ... tuple[dict[str, Any], dict[str, str] | None], ...
] ]
| None, | None,
user_input_platform_config: dict[str, Any] | None, user_input_platform_config: dict[str, Any],
user_input_mqtt: dict[str, Any], user_input_mqtt: dict[str, Any],
component_data: dict[str, Any], component_data: dict[str, Any],
removed_options: tuple[str, ...], removed_options: tuple[str, ...],
@ -3694,12 +3699,9 @@ async def test_subentry_reconfigure_edit_entity_single_entity(
user_input={}, user_input={},
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "entity_platform_config"
if user_input_platform_config is None: # entity platform config flow step
# Skip entity flow step
assert result["step_id"] == "mqtt_platform_config"
else:
# Additional entity flow step
assert result["step_id"] == "entity_platform_config" assert result["step_id"] == "entity_platform_config"
for entity_validation_config, errors in user_input_platform_config_validation: for entity_validation_config, errors in user_input_platform_config_validation:
result = await hass.config_entries.subentries.async_configure( result = await hass.config_entries.subentries.async_configure(
@ -3880,7 +3882,12 @@ async def test_subentry_reconfigure_edit_entity_reset_fields(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("mqtt_config_subentries_data", "user_input_entity", "user_input_mqtt"), (
"mqtt_config_subentries_data",
"user_input_entity",
"user_input_entity_platform_config",
"user_input_mqtt",
),
[ [
( (
( (
@ -3895,6 +3902,7 @@ async def test_subentry_reconfigure_edit_entity_reset_fields(
"name": "The second notifier", "name": "The second notifier",
"entity_picture": "https://example.com", "entity_picture": "https://example.com",
}, },
{"entity_category": "diagnostic"},
{ {
"command_topic": "test-topic2", "command_topic": "test-topic2",
}, },
@ -3908,6 +3916,7 @@ async def test_subentry_reconfigure_add_entity(
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
user_input_entity: dict[str, Any], user_input_entity: dict[str, Any],
user_input_entity_platform_config: dict[str, Any],
user_input_mqtt: dict[str, Any], user_input_mqtt: dict[str, Any],
) -> None: ) -> None:
"""Test the subentry ConfigFlow reconfigure and add an entity.""" """Test the subentry ConfigFlow reconfigure and add an entity."""
@ -3960,6 +3969,14 @@ async def test_subentry_reconfigure_add_entity(
user_input=user_input_entity, user_input=user_input_entity,
) )
assert result["type"] is FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "entity_platform_config"
# submit the new entity platform config
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=user_input_entity_platform_config,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "mqtt_platform_config" assert result["step_id"] == "mqtt_platform_config"
# submit the new platform specific entity data # submit the new platform specific entity data