From cdec29ffb798ccf810a35d7cfeb4890df5e73849 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Fri, 10 Oct 2025 13:46:21 +0200 Subject: [PATCH] Add MQTT select subentry support (#153637) --- homeassistant/components/mqtt/config_flow.py | 31 ++++++++++++++++++++ homeassistant/components/mqtt/strings.json | 3 ++ tests/components/mqtt/common.py | 18 ++++++++++++ tests/components/mqtt/test_config_flow.py | 19 ++++++++++++ 4 files changed, 71 insertions(+) diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 26b663f1c11..64601433e7e 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -458,6 +458,7 @@ SUBENTRY_PLATFORMS = [ Platform.LOCK, Platform.NOTIFY, Platform.NUMBER, + Platform.SELECT, Platform.SENSOR, Platform.SWITCH, ] @@ -1141,6 +1142,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ Platform.LOCK.value: None, Platform.NOTIFY.value: None, Platform.NUMBER.value: validate_number_platform_config, + Platform.SELECT: None, Platform.SENSOR.value: validate_sensor_platform_config, Platform.SWITCH.value: None, } @@ -1367,6 +1369,7 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = { custom_filtering=True, ), }, + Platform.SELECT.value: {}, Platform.SENSOR.value: { CONF_DEVICE_CLASS: PlatformField( selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False @@ -3103,6 +3106,34 @@ PLATFORM_MQTT_FIELDS: dict[str, dict[str, PlatformField]] = { ), CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, + Platform.SELECT.value: { + CONF_COMMAND_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=True, + validator=valid_publish_topic, + error="invalid_publish_topic", + ), + CONF_COMMAND_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_STATE_TOPIC: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=valid_subscribe_topic, + error="invalid_subscribe_topic", + ), + CONF_VALUE_TEMPLATE: PlatformField( + selector=TEMPLATE_SELECTOR, + required=False, + validator=validate(cv.template), + error="invalid_template", + ), + CONF_OPTIONS: PlatformField(selector=OPTIONS_SELECTOR, required=True), + CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + }, Platform.SENSOR.value: { CONF_STATE_TOPIC: PlatformField( selector=TEXT_SELECTOR, diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index fe848ea43c6..438d64c48df 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -346,6 +346,7 @@ "mode_state_template": "Operation mode value template", "on_command_type": "ON command type", "optimistic": "Optimistic", + "options": "Set options", "payload_off": "Payload \"off\"", "payload_on": "Payload \"on\"", "payload_press": "Payload \"press\"", @@ -393,6 +394,7 @@ "mode_state_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the operation mode state. [Learn more.]({url}#mode_state_template)", "on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.", "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", + "options": "List of options that can be selected.", "payload_off": "The payload that represents the \"off\" state.", "payload_on": "The payload that represents the \"on\" state.", "payload_press": "The payload to send when the button is triggered.", @@ -1334,6 +1336,7 @@ "lock": "[%key:component::lock::title%]", "notify": "[%key:component::notify::title%]", "number": "[%key:component::number::title%]", + "select": "[%key:component::select::title%]", "sensor": "[%key:component::sensor::title%]", "switch": "[%key:component::switch::title%]" } diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 762cb98ad29..dce45e207c3 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -517,6 +517,20 @@ MOCK_SUBENTRY_NUMBER_COMPONENT_NO_UNIT = { "entity_picture": "https://example.com/f9261f6feed443e7b7d5f3fbe2a47414", }, } +MOCK_SUBENTRY_SELECT_COMPONENT = { + "fa261f6feed443e7b7d5f3fbe2a47414": { + "platform": "select", + "name": "Mode", + "entity_category": None, + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "options": ["beer", "milk"], + "value_template": "{{ value_json.value }}", + "retain": False, + "entity_picture": "https://example.com/fa261f6feed443e7b7d5f3fbe2a47414", + }, +} MOCK_SUBENTRY_SENSOR_COMPONENT = { "e9261f6feed443e7b7d5f3fbe2a47412": { "platform": "sensor", @@ -668,6 +682,10 @@ MOCK_NUMBER_SUBENTRY_DATA_NO_UNIT = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_NUMBER_COMPONENT_NO_UNIT, } +MOCK_SELECT_SUBENTRY_DATA = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_SELECT_COMPONENT, +} MOCK_SENSOR_SUBENTRY_DATA_SINGLE = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_SENSOR_COMPONENT, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index a0faef9c699..9dfc3009136 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -53,6 +53,7 @@ from .common import ( MOCK_NUMBER_SUBENTRY_DATA_CUSTOM_UNIT, MOCK_NUMBER_SUBENTRY_DATA_DEVICE_CLASS_UNIT, MOCK_NUMBER_SUBENTRY_DATA_NO_UNIT, + MOCK_SELECT_SUBENTRY_DATA, MOCK_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS, @@ -3553,6 +3554,24 @@ async def test_migrate_of_incompatible_config_entry( "Milk notifier Speed", id="number_no_unit", ), + pytest.param( + MOCK_SELECT_SUBENTRY_DATA, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "Mode"}, + {}, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "options": ["beer", "milk"], + "value_template": "{{ value_json.value }}", + "retain": False, + }, + (), + "Milk notifier Mode", + id="select", + ), pytest.param( MOCK_SENSOR_SUBENTRY_DATA_SINGLE, {"name": "Milk notifier", "mqtt_settings": {"qos": 0}},