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_DISCOVERY,
CONF_EFFECT,
CONF_ENTITY_CATEGORY,
CONF_HOST,
CONF_NAME,
CONF_OPTIMISTIC,
@ -84,6 +85,7 @@ from homeassistant.const import (
STATE_CLOSING,
STATE_OPEN,
STATE_OPENING,
EntityCategory,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow, SectionConfig, section
@ -411,6 +413,14 @@ SUBENTRY_AVAILABILITY_SCHEMA = vol.Schema(
): 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_DEVICE_CLASS_SELECTOR = SelectSelector(
@ -429,6 +439,15 @@ BINARY_SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
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(
SelectSelectorConfig(
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.BINARY_SENSOR.value: {
CONF_DEVICE_CLASS: PlatformField(
selector=BINARY_SENSOR_DEVICE_CLASS_SELECTOR,
required=False,
),
CONF_ENTITY_CATEGORY: PlatformField(
selector=SENSOR_ENTITY_CATEGORY_SELECTOR,
required=False,
default=None,
),
},
Platform.BUTTON.value: {
CONF_DEVICE_CLASS: PlatformField(
@ -804,6 +836,11 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
required=False,
conditions=({"device_class": "enum"},),
),
CONF_ENTITY_CATEGORY: PlatformField(
selector=SENSOR_ENTITY_CATEGORY_SELECTOR,
required=False,
default=None,
),
},
Platform.SWITCH.value: {
CONF_DEVICE_CLASS: PlatformField(
@ -2070,8 +2107,6 @@ def data_schema_from_fields(
if field_details.section == schema_section
and field_details.exclude_from_reconfig
}
if not data_element_options:
continue
if schema_section is None:
data_schema.update(data_schema_element)
continue
@ -2834,7 +2869,9 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
assert self._component_id is not None
component_data = self._subentry_data["components"][self._component_id]
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] = {}
data_schema = data_schema_from_fields(
@ -2845,8 +2882,6 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
component_data=component_data,
user_input=user_input,
)
if not data_schema.schema:
return await self.async_step_mqtt_platform_config()
if user_input is not None:
# Test entity fields against the validator
merged_user_input, errors = validate_user_input(
@ -2940,6 +2975,7 @@ class MQTTSubentryFlowHandler(ConfigSubentryFlow):
platform = component_data[CONF_PLATFORM]
platform_fields: dict[str, PlatformField] = (
COMMON_ENTITY_FIELDS
| SHARED_PLATFORM_ENTITY_FIELDS
| PLATFORM_ENTITY_FIELDS[platform]
| PLATFORM_MQTT_FIELDS[platform]
)

View File

@ -313,6 +313,11 @@ def async_setup_entity_entry_helper(
component_config.pop("platform")
component_config.update(availability_config)
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:
config = platform_schema_modern(component_config)

View File

@ -210,6 +210,7 @@
"description": "Please configure specific details for {platform} entity \"{entity}\":",
"data": {
"device_class": "Device class",
"entity_category": "Entity category",
"fan_feature_speed": "Speed support",
"fan_feature_preset_modes": "Preset modes support",
"fan_feature_oscillation": "Oscillation support",
@ -222,6 +223,7 @@
},
"data_description": {
"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_preset_modes": "The fan supports preset modes.",
"fan_feature_oscillation": "The fan supports oscillation.",
@ -883,6 +885,12 @@
"switch": "[%key:component::switch::title%]"
}
},
"entity_category": {
"options": {
"config": "Config",
"diagnostic": "Diagnostic"
}
},
"light_schema": {
"options": {
"basic": "Default schema",

View File

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

View File

@ -2941,8 +2941,8 @@ async def test_migrate_of_incompatible_config_entry(
MOCK_NOTIFY_SUBENTRY_DATA_SINGLE,
{"name": "Milk notifier", "mqtt_settings": {"qos": 1}},
{"name": "Milkman alert"},
None,
None,
{},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
@ -2960,8 +2960,8 @@ async def test_migrate_of_incompatible_config_entry(
MOCK_NOTIFY_SUBENTRY_DATA_NO_NAME,
{"name": "Milk notifier", "mqtt_settings": {"qos": 0}},
{},
None,
None,
{},
(),
{
"command_topic": "test-topic",
"command_template": "{{ value }}",
@ -3220,37 +3220,32 @@ async def test_subentry_configflow(
"url": learn_more_url(component["platform"]),
}
# Process extra step if the platform supports it
if mock_entity_details_user_input is not None:
# Extra entity details flow step
assert result["step_id"] == "entity_platform_config"
# Process entity details setep
assert result["step_id"] == "entity_platform_config"
# First test validators if set of test
for failed_user_input, failed_errors in mock_entity_details_failed_user_input:
# Test an invalid entity details user input case
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=failed_user_input,
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == failed_errors
# Now try again with valid data
# First test validators if set of test
for failed_user_input, failed_errors in mock_entity_details_failed_user_input:
# Test an invalid entity details user input case
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=mock_entity_details_user_input,
user_input=failed_user_input,
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
assert result["description_placeholders"] == {
"mqtt_device": device_name,
"platform": component["platform"],
"entity": entity_name,
"url": learn_more_url(component["platform"]),
}
else:
# No details form step
assert result["step_id"] == "mqtt_platform_config"
assert result["errors"] == failed_errors
# Now try again with valid data
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=mock_entity_details_user_input,
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {}
assert result["description_placeholders"] == {
"mqtt_device": device_name,
"platform": component["platform"],
"entity": entity_name,
"url": learn_more_url(component["platform"]),
}
# Process mqtt platform config flow
# 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["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"
# 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_template": "{{ value }}",
@ -3608,8 +3613,8 @@ async def test_subentry_reconfigure_edit_entity_multi_entitites(
title="Mock subentry",
),
),
None,
None,
(),
{},
{
"command_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], ...
]
| None,
user_input_platform_config: dict[str, Any] | None,
user_input_platform_config: dict[str, Any],
user_input_mqtt: dict[str, Any],
component_data: dict[str, Any],
removed_options: tuple[str, ...],
@ -3694,28 +3699,25 @@ async def test_subentry_reconfigure_edit_entity_single_entity(
user_input={},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "entity_platform_config"
if user_input_platform_config is None:
# Skip entity flow step
assert result["step_id"] == "mqtt_platform_config"
else:
# Additional entity flow step
assert result["step_id"] == "entity_platform_config"
for entity_validation_config, errors in user_input_platform_config_validation:
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=entity_validation_config,
)
assert result["step_id"] == "entity_platform_config"
assert result.get("errors") == errors
assert result["type"] is FlowResultType.FORM
# entity platform config flow step
assert result["step_id"] == "entity_platform_config"
for entity_validation_config, errors in user_input_platform_config_validation:
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=user_input_platform_config,
user_input=entity_validation_config,
)
assert result["step_id"] == "entity_platform_config"
assert result.get("errors") == errors
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "mqtt_platform_config"
result = await hass.config_entries.subentries.async_configure(
result["flow_id"],
user_input=user_input_platform_config,
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "mqtt_platform_config"
# submit the new platform specific entity data,
result = await hass.config_entries.subentries.async_configure(
@ -3880,7 +3882,12 @@ async def test_subentry_reconfigure_edit_entity_reset_fields(
@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",
"entity_picture": "https://example.com",
},
{"entity_category": "diagnostic"},
{
"command_topic": "test-topic2",
},
@ -3908,6 +3916,7 @@ async def test_subentry_reconfigure_add_entity(
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
user_input_entity: dict[str, Any],
user_input_entity_platform_config: dict[str, Any],
user_input_mqtt: dict[str, Any],
) -> None:
"""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,
)
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"
# submit the new platform specific entity data