mirror of
https://github.com/home-assistant/core.git
synced 2025-07-25 06:07:17 +00:00
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:
parent
51fb1ab8b6
commit
85e9919bbd
@ -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]
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user