diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index 0352c5b5f58..471b6d048a7 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -33,6 +33,7 @@ from homeassistant.components.sensor import ( SensorDeviceClass, SensorStateClass, ) +from homeassistant.components.switch import SwitchDeviceClass from homeassistant.config_entries import ( SOURCE_RECONFIGURE, ConfigEntry, @@ -55,6 +56,7 @@ from homeassistant.const import ( CONF_DISCOVERY, CONF_HOST, CONF_NAME, + CONF_OPTIMISTIC, CONF_PASSWORD, CONF_PAYLOAD, CONF_PLATFORM, @@ -233,7 +235,7 @@ KEY_UPLOAD_SELECTOR = FileSelector( ) # Subentry selectors -SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR] +SUBENTRY_PLATFORMS = [Platform.NOTIFY, Platform.SENSOR, Platform.SWITCH] SUBENTRY_PLATFORM_SELECTOR = SelectSelector( SelectSelectorConfig( options=[platform.value for platform in SUBENTRY_PLATFORMS], @@ -286,6 +288,15 @@ EXPIRE_AFTER_SELECTOR = NumberSelector( NumberSelectorConfig(mode=NumberSelectorMode.BOX, min=0) ) +# Switch specific selectors +SWITCH_DEVICE_CLASS_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[device_class.value for device_class in SwitchDeviceClass], + mode=SelectSelectorMode.DROPDOWN, + translation_key="device_class_switch", + ) +) + @callback def validate_sensor_platform_config( @@ -390,6 +401,9 @@ PLATFORM_ENTITY_FIELDS = { conditions=({"device_class": "enum"},), ), }, + Platform.SWITCH.value: { + CONF_DEVICE_CLASS: PlatformField(SWITCH_DEVICE_CLASS_SELECTOR, False, str), + }, } PLATFORM_MQTT_FIELDS = { Platform.NOTIFY.value: { @@ -419,6 +433,22 @@ PLATFORM_MQTT_FIELDS = { EXPIRE_AFTER_SELECTOR, False, cv.positive_int, section="advanced_settings" ), }, + Platform.SWITCH.value: { + CONF_COMMAND_TOPIC: PlatformField( + TEXT_SELECTOR, True, valid_publish_topic, "invalid_publish_topic" + ), + CONF_COMMAND_TEMPLATE: PlatformField( + TEMPLATE_SELECTOR, False, cv.template, "invalid_template" + ), + CONF_STATE_TOPIC: PlatformField( + TEXT_SELECTOR, False, valid_subscribe_topic, "invalid_subscribe_topic" + ), + CONF_VALUE_TEMPLATE: PlatformField( + TEMPLATE_SELECTOR, False, cv.template, "invalid_template" + ), + CONF_RETAIN: PlatformField(BOOLEAN_SELECTOR, False, bool), + CONF_OPTIMISTIC: PlatformField(BOOLEAN_SELECTOR, False, bool), + }, } ENTITY_CONFIG_VALIDATOR: dict[ str, @@ -426,6 +456,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ ] = { Platform.NOTIFY.value: None, Platform.SENSOR.value: validate_sensor_platform_config, + Platform.SWITCH.value: None, } MQTT_DEVICE_PLATFORM_FIELDS = { diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index e44a6c0d44a..052af8fd72a 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -246,7 +246,9 @@ "value_template": "Value template", "last_reset_value_template": "Last reset value template", "force_update": "Force update", - "retain": "Retain" + "optimistic": "Optimistic", + "retain": "Retain", + "qos": "QoS" }, "data_description": { "command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)", @@ -255,7 +257,9 @@ "value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value.", "last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)", "force_update": "Sends update events even if the value hasn’t changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)", - "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker." + "optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)", + "retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.", + "qos": "The QoS value {platform} entity should use." }, "sections": { "advanced_settings": { @@ -462,10 +466,17 @@ "wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]" } }, + "device_class_switch": { + "options": { + "outlet": "[%key:component::switch::entity_component::outlet::name%]", + "switch": "[%key:component::switch::title%]" + } + }, "platform": { "options": { "notify": "Notify", - "sensor": "Sensor" + "sensor": "Sensor", + "switch": "Switch" } }, "set_ca_cert": { diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 372d1354e85..e4a368f0d71 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -125,6 +125,19 @@ MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET = { "entity_picture": "https://example.com/e9261f6feed443e7b7d5f3fbe2a47412", }, } +MOCK_SUBENTRY_SWITCH_COMPONENT = { + "3faf1318016c46c5aea26707eeb6f12e": { + "platform": "switch", + "name": "Outlet", + "device_class": "outlet", + "command_topic": "test-topic", + "state_topic": "test-topic", + "command_template": "{{ value }}", + "value_template": "{{ value_json.value }}", + "entity_picture": "https://example.com/3faf1318016c46c5aea26707eeb6f12e", + "optimistic": True, + }, +} # Bogus light component just for code coverage # Note that light cannot be setup through the UI yet @@ -223,7 +236,17 @@ MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE = { }, "components": MOCK_SUBENTRY_SENSOR_COMPONENT_LAST_RESET, } - +MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS = { + "device": { + "name": "Test switch", + "sw_version": "1.0", + "hw_version": "2.1 rev a", + "model": "Model XL", + "model_id": "mn002", + "configuration_url": "https://example.com", + }, + "components": MOCK_SUBENTRY_SWITCH_COMPONENT, +} MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA = { "device": { "name": "Milk notifier", @@ -246,7 +269,8 @@ MOCK_SUBENTRY_DATA_SET_MIX = { }, "components": MOCK_SUBENTRY_NOTIFY_COMPONENT1 | MOCK_SUBENTRY_NOTIFY_COMPONENT2 - | MOCK_SUBENTRY_LIGHT_COMPONENT, + | MOCK_SUBENTRY_LIGHT_COMPONENT + | MOCK_SUBENTRY_SWITCH_COMPONENT, } | MOCK_SUBENTRY_AVAILABILITY_DATA _SENTINEL = object() diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index a20fa4aeec6..2635263ae8e 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -39,6 +39,7 @@ from .common import ( MOCK_SENSOR_SUBENTRY_DATA_SINGLE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_LAST_RESET_TEMPLATE, MOCK_SENSOR_SUBENTRY_DATA_SINGLE_STATE_CLASS, + MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS, ) from tests.common import MockConfigEntry, MockMqttReasonCode @@ -2733,12 +2734,41 @@ async def test_migrate_of_incompatible_config_entry( (), "Test sensor Energy", ), + ( + MOCK_SWITCH_SUBENTRY_DATA_SINGLE_STATE_CLASS, + {"name": "Test switch", "mqtt_settings": {"qos": 0}}, + {"name": "Outlet"}, + {"device_class": "outlet"}, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "value_template": "{{ value_json.value }}", + "optimistic": True, + }, + ( + ( + {"command_topic": "test-topic#invalid"}, + {"command_topic": "invalid_publish_topic"}, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + }, + {"state_topic": "invalid_subscribe_topic"}, + ), + ), + "Test switch Outlet", + ), ], ids=[ "notify_with_entity_name", "notify_no_entity_name", "sensor_options", "sensor_total", + "switch", ], ) async def test_subentry_configflow(