diff --git a/homeassistant/components/mqtt/config_flow.py b/homeassistant/components/mqtt/config_flow.py index b66cee2bce73..ec452bc95627 100644 --- a/homeassistant/components/mqtt/config_flow.py +++ b/homeassistant/components/mqtt/config_flow.py @@ -239,6 +239,7 @@ from .const import ( CONF_OSCILLATION_COMMAND_TOPIC, CONF_OSCILLATION_STATE_TOPIC, CONF_OSCILLATION_VALUE_TEMPLATE, + CONF_PATTERN, CONF_PAYLOAD_ARM_AWAY, CONF_PAYLOAD_ARM_CUSTOM_BYPASS, CONF_PAYLOAD_ARM_HOME, @@ -465,6 +466,7 @@ SUBENTRY_PLATFORMS = [ Platform.SENSOR, Platform.SIREN, Platform.SWITCH, + Platform.TEXT, ] _CODE_VALIDATION_MODE = { @@ -819,6 +821,16 @@ TEMPERATURE_UNIT_SELECTOR = SelectSelector( mode=SelectSelectorMode.DROPDOWN, ) ) +TEXT_MODE_SELECTOR = SelectSelector( + SelectSelectorConfig( + options=[TextSelectorType.TEXT.value, TextSelectorType.PASSWORD.value], + mode=SelectSelectorMode.DROPDOWN, + translation_key="text_mode", + ) +) +TEXT_SIZE_SELECTOR = NumberSelector( + NumberSelectorConfig(min=0, max=255, step=1, mode=NumberSelectorMode.BOX) +) @callback @@ -1151,6 +1163,22 @@ def validate_sensor_platform_config( return errors +@callback +def validate_text_platform_config( + config: dict[str, Any], +) -> dict[str, str]: + """Validate the text entity options.""" + errors: dict[str, str] = {} + if ( + CONF_MIN in config + and CONF_MAX in config + and config[CONF_MIN] > config[CONF_MAX] + ): + errors["text_advanced_settings"] = "max_below_min" + + return errors + + ENTITY_CONFIG_VALIDATOR: dict[ str, Callable[[dict[str, Any]], dict[str, str]] | None, @@ -1170,6 +1198,7 @@ ENTITY_CONFIG_VALIDATOR: dict[ Platform.SENSOR: validate_sensor_platform_config, Platform.SIREN: None, Platform.SWITCH: None, + Platform.TEXT: validate_text_platform_config, } @@ -1430,6 +1459,7 @@ PLATFORM_ENTITY_FIELDS: dict[Platform, dict[str, PlatformField]] = { selector=SWITCH_DEVICE_CLASS_SELECTOR, required=False ), }, + Platform.TEXT: {}, } PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = { Platform.ALARM_CONTROL_PANEL: { @@ -3298,6 +3328,58 @@ PLATFORM_MQTT_FIELDS: dict[Platform, dict[str, PlatformField]] = { CONF_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), CONF_OPTIMISTIC: PlatformField(selector=BOOLEAN_SELECTOR, required=False), }, + Platform.TEXT: { + 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_RETAIN: PlatformField(selector=BOOLEAN_SELECTOR, required=False), + CONF_MIN: PlatformField( + selector=TEXT_SIZE_SELECTOR, + required=True, + default=0, + section="text_advanced_settings", + ), + CONF_MAX: PlatformField( + selector=TEXT_SIZE_SELECTOR, + required=True, + default=255, + section="text_advanced_settings", + ), + CONF_MODE: PlatformField( + selector=TEXT_MODE_SELECTOR, + required=True, + default=TextSelectorType.TEXT.value, + section="text_advanced_settings", + ), + CONF_PATTERN: PlatformField( + selector=TEXT_SELECTOR, + required=False, + validator=validate(cv.is_regex), + error="invalid_regular_expression", + section="text_advanced_settings", + ), + }, } MQTT_DEVICE_PLATFORM_FIELDS = { ATTR_NAME: PlatformField(selector=TEXT_SELECTOR, required=True), diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 80fbc9059461..7dfdac5634fa 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -138,6 +138,7 @@ CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic" CONF_OSCILLATION_COMMAND_TEMPLATE = "oscillation_command_template" CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic" CONF_OSCILLATION_VALUE_TEMPLATE = "oscillation_value_template" +CONF_PATTERN = "pattern" CONF_PAYLOAD_ARM_AWAY = "payload_arm_away" CONF_PAYLOAD_ARM_CUSTOM_BYPASS = "payload_arm_custom_bypass" CONF_PAYLOAD_ARM_HOME = "payload_arm_home" diff --git a/homeassistant/components/mqtt/strings.json b/homeassistant/components/mqtt/strings.json index 1335f58cbd4d..f79d1ac54f26 100644 --- a/homeassistant/components/mqtt/strings.json +++ b/homeassistant/components/mqtt/strings.json @@ -970,6 +970,21 @@ "temperature_state_topic": "The MQTT topic to subscribe for changes of the target temperature. [Learn more.]({url}#temperature_state_topic)" }, "name": "Target temperature settings" + }, + "text_advanced_settings": { + "data": { + "max": "Maximum length", + "min": "Mininum length", + "mode": "Mode", + "pattern": "Pattern" + }, + "data_description": { + "max": "Maximum length of the text input", + "min": "Mininum length of the text input", + "mode": "Mode of the text input", + "pattern": "A valid regex pattern" + }, + "name": "Advanced text settings" } }, "title": "Configure MQTT device \"{mqtt_device}\"" @@ -1387,7 +1402,8 @@ "select": "[%key:component::select::title%]", "sensor": "[%key:component::sensor::title%]", "siren": "[%key:component::siren::title%]", - "switch": "[%key:component::switch::title%]" + "switch": "[%key:component::switch::title%]", + "text": "[%key:component::text::title%]" } }, "set_ca_cert": { @@ -1424,6 +1440,12 @@ "none": "No target temperature", "single": "Single target temperature" } + }, + "text_mode": { + "options": { + "password": "[%key:common::config_flow::data::password%]", + "text": "[%key:component::text::entity_component::_::state_attributes::mode::state::text%]" + } } }, "services": { diff --git a/homeassistant/components/mqtt/text.py b/homeassistant/components/mqtt/text.py index d306fc0819bd..c1b6024d9101 100644 --- a/homeassistant/components/mqtt/text.py +++ b/homeassistant/components/mqtt/text.py @@ -27,7 +27,14 @@ from homeassistant.helpers.typing import ConfigType, VolSchemaType from . import subscription from .config import MQTT_RW_SCHEMA -from .const import CONF_COMMAND_TEMPLATE, CONF_COMMAND_TOPIC, CONF_STATE_TOPIC +from .const import ( + CONF_COMMAND_TEMPLATE, + CONF_COMMAND_TOPIC, + CONF_MAX, + CONF_MIN, + CONF_PATTERN, + CONF_STATE_TOPIC, +) from .entity import MqttEntity, async_setup_entity_entry_helper from .models import ( MqttCommandTemplate, @@ -42,12 +49,7 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 0 -CONF_MAX = "max" -CONF_MIN = "min" -CONF_PATTERN = "pattern" - DEFAULT_NAME = "MQTT Text" -DEFAULT_PAYLOAD_RESET = "None" MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset( { diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index 5b02453e44e0..b69c61258552 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -600,6 +600,23 @@ MOCK_SUBENTRY_SWITCH_COMPONENT = { "optimistic": True, }, } +MOCK_SUBENTRY_TEXT_COMPONENT = { + "09261f6feed443e7b7d5f3fbe2a47413": { + "platform": "text", + "name": "MOTD", + "entity_category": None, + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "min": 0.0, + "max": 10.0, + "mode": "password", + "pattern": "^[a-z_]*$", + "value_template": "{{ value_json.value }}", + "retain": False, + "entity_picture": "https://example.com/09261f6feed443e7b7d5f3fbe2a47413", + }, +} MOCK_SUBENTRY_AVAILABILITY_DATA = { "availability": { @@ -725,6 +742,10 @@ MOCK_SWITCH_SUBENTRY_DATA = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_SWITCH_COMPONENT, } +MOCK_TEXT_SUBENTRY_DATA = { + "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, + "components": MOCK_SUBENTRY_TEXT_COMPONENT, +} MOCK_SUBENTRY_DATA_BAD_COMPONENT_SCHEMA = { "device": MOCK_SUBENTRY_DEVICE_DATA | {"mqtt_settings": {"qos": 0}}, "components": MOCK_SUBENTRY_NOTIFY_BAD_SCHEMA, diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 4b889e4bda41..777a2dd3dd65 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -62,6 +62,7 @@ from .common import ( MOCK_SENSOR_SUBENTRY_DATA_STATE_CLASS, MOCK_SIREN_SUBENTRY_DATA, MOCK_SWITCH_SUBENTRY_DATA, + MOCK_TEXT_SUBENTRY_DATA, ) from tests.common import MockConfigEntry, MockMqttReasonCode, get_schema_suggested_value @@ -3720,6 +3721,65 @@ async def test_migrate_of_incompatible_config_entry( "Milk notifier Outlet", id="switch", ), + pytest.param( + MOCK_TEXT_SUBENTRY_DATA, + {"name": "Milk notifier", "mqtt_settings": {"qos": 0}}, + {"name": "MOTD"}, + {}, + (), + { + "command_topic": "test-topic", + "command_template": "{{ value }}", + "state_topic": "test-topic", + "value_template": "{{ value_json.value }}", + "retain": False, + "text_advanced_settings": { + "min": 0, + "max": 10, + "mode": "password", + "pattern": "^[a-z_]*$", + }, + }, + ( + ( + {"command_topic": "test-topic#invalid"}, + {"command_topic": "invalid_publish_topic"}, + ), + ( + { + "command_topic": "test-topic", + "state_topic": "test-topic#invalid", + }, + {"state_topic": "invalid_subscribe_topic"}, + ), + ( + { + "command_topic": "test-topic", + "text_advanced_settings": { + "min": 20, + "max": 10, + "mode": "password", + "pattern": "^[a-z_]*$", + }, + }, + {"text_advanced_settings": "max_below_min"}, + ), + ( + { + "command_topic": "test-topic", + "text_advanced_settings": { + "min": 0, + "max": 10, + "mode": "password", + "pattern": "(", + }, + }, + {"text_advanced_settings": "invalid_regular_expression"}, + ), + ), + "Milk notifier MOTD", + id="text", + ), ], ) async def test_subentry_configflow(