Compare commits

...

2 Commits

Author SHA1 Message Date
Jan Bouwhuis
466fb0131e Merge branch 'dev' into mqtt-subentry-text 2025-11-16 15:02:10 +01:00
jbouwh
d479bba351 Add MQTT text subentry support 2025-11-16 12:12:35 +00:00
6 changed files with 195 additions and 7 deletions

View File

@@ -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),

View File

@@ -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"

View File

@@ -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": {

View File

@@ -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(
{

View File

@@ -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,

View File

@@ -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(