Skip to content

Commit fff099e

Browse files
jbouwhmarcelveldt
authored andcommitted
Add mqtt text platform (#82884)
1 parent 7822270 commit fff099e

File tree

6 files changed

+906
-0
lines changed

6 files changed

+906
-0
lines changed

homeassistant/components/mqtt/abbreviations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@
169169
"pr_mode_stat_t": "preset_mode_state_topic",
170170
"pr_mode_val_tpl": "preset_mode_value_template",
171171
"pr_modes": "preset_modes",
172+
"ptrn": "pattern",
172173
"r_tpl": "red_template",
173174
"rel_s": "release_summary",
174175
"rel_u": "release_url",

homeassistant/components/mqtt/config_integration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
sensor as sensor_platform,
3333
siren as siren_platform,
3434
switch as switch_platform,
35+
text as text_platform,
3536
update as update_platform,
3637
vacuum as vacuum_platform,
3738
)
@@ -130,6 +131,9 @@
130131
Platform.SWITCH.value: vol.All(
131132
cv.ensure_list, [switch_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type]
132133
),
134+
Platform.TEXT.value: vol.All(
135+
cv.ensure_list, [text_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type]
136+
),
133137
Platform.UPDATE.value: vol.All(
134138
cv.ensure_list, [update_platform.PLATFORM_SCHEMA_MODERN] # type: ignore[has-type]
135139
),

homeassistant/components/mqtt/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
Platform.SENSOR,
102102
Platform.SIREN,
103103
Platform.SWITCH,
104+
Platform.TEXT,
104105
Platform.UPDATE,
105106
Platform.VACUUM,
106107
]
@@ -122,6 +123,7 @@
122123
Platform.SENSOR,
123124
Platform.SIREN,
124125
Platform.SWITCH,
126+
Platform.TEXT,
125127
Platform.UPDATE,
126128
Platform.VACUUM,
127129
]

homeassistant/components/mqtt/discovery.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"sensor",
6464
"switch",
6565
"tag",
66+
"text",
6667
"update",
6768
"vacuum",
6869
]

homeassistant/components/mqtt/text.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
"""Support for MQTT text platform."""
2+
from __future__ import annotations
3+
4+
from collections.abc import Callable
5+
import functools
6+
import logging
7+
import re
8+
from typing import Any
9+
10+
import voluptuous as vol
11+
12+
from homeassistant.components import text
13+
from homeassistant.components.text import TextEntity
14+
from homeassistant.config_entries import ConfigEntry
15+
from homeassistant.const import (
16+
CONF_MODE,
17+
CONF_NAME,
18+
CONF_OPTIMISTIC,
19+
CONF_VALUE_TEMPLATE,
20+
MAX_LENGTH_STATE_STATE,
21+
)
22+
from homeassistant.core import HomeAssistant, callback
23+
from homeassistant.helpers import config_validation as cv
24+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
25+
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
26+
27+
from . import subscription
28+
from .config import MQTT_RW_SCHEMA
29+
from .const import (
30+
CONF_COMMAND_TEMPLATE,
31+
CONF_COMMAND_TOPIC,
32+
CONF_ENCODING,
33+
CONF_QOS,
34+
CONF_RETAIN,
35+
CONF_STATE_TOPIC,
36+
)
37+
from .debug_info import log_messages
38+
from .mixins import MQTT_ENTITY_COMMON_SCHEMA, MqttEntity, async_setup_entry_helper
39+
from .models import (
40+
MqttCommandTemplate,
41+
MqttValueTemplate,
42+
PublishPayloadType,
43+
ReceiveMessage,
44+
ReceivePayloadType,
45+
)
46+
from .util import get_mqtt_data
47+
48+
_LOGGER = logging.getLogger(__name__)
49+
50+
CONF_MAX = "max"
51+
CONF_MIN = "min"
52+
CONF_PATTERN = "pattern"
53+
54+
DEFAULT_NAME = "MQTT Text"
55+
DEFAULT_OPTIMISTIC = False
56+
DEFAULT_PAYLOAD_RESET = "None"
57+
58+
MQTT_TEXT_ATTRIBUTES_BLOCKED = frozenset(
59+
{
60+
text.ATTR_MAX,
61+
text.ATTR_MIN,
62+
text.ATTR_MODE,
63+
text.ATTR_PATTERN,
64+
}
65+
)
66+
67+
68+
def valid_text_size_configuration(config: ConfigType) -> ConfigType:
69+
"""Validate that the text length configuration is valid, throws if it isn't."""
70+
if config[CONF_MIN] >= config[CONF_MAX]:
71+
raise ValueError("text length min must be >= max")
72+
if config[CONF_MAX] > MAX_LENGTH_STATE_STATE:
73+
raise ValueError(f"max text length must be <= {MAX_LENGTH_STATE_STATE}")
74+
75+
return config
76+
77+
78+
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
79+
{
80+
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
81+
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
82+
vol.Optional(CONF_MAX, default=MAX_LENGTH_STATE_STATE): cv.positive_int,
83+
vol.Optional(CONF_MIN, default=0): cv.positive_int,
84+
vol.Optional(CONF_MODE, default=text.TextMode.TEXT): vol.In(
85+
[text.TextMode.TEXT, text.TextMode.PASSWORD]
86+
),
87+
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
88+
vol.Optional(CONF_PATTERN): cv.is_regex,
89+
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
90+
},
91+
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
92+
93+
94+
DISCOVERY_SCHEMA = vol.All(
95+
_PLATFORM_SCHEMA_BASE.extend({}, extra=vol.REMOVE_EXTRA),
96+
valid_text_size_configuration,
97+
)
98+
99+
PLATFORM_SCHEMA_MODERN = vol.All(_PLATFORM_SCHEMA_BASE, valid_text_size_configuration)
100+
101+
102+
async def async_setup_entry(
103+
hass: HomeAssistant,
104+
config_entry: ConfigEntry,
105+
async_add_entities: AddEntitiesCallback,
106+
) -> None:
107+
"""Set up MQTT text through configuration.yaml and dynamically through MQTT discovery."""
108+
setup = functools.partial(
109+
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
110+
)
111+
await async_setup_entry_helper(hass, text.DOMAIN, setup, DISCOVERY_SCHEMA)
112+
113+
114+
async def _async_setup_entity(
115+
hass: HomeAssistant,
116+
async_add_entities: AddEntitiesCallback,
117+
config: ConfigType,
118+
config_entry: ConfigEntry,
119+
discovery_data: DiscoveryInfoType | None = None,
120+
) -> None:
121+
"""Set up the MQTT text."""
122+
async_add_entities([MqttTextEntity(hass, config, config_entry, discovery_data)])
123+
124+
125+
class MqttTextEntity(MqttEntity, TextEntity):
126+
"""Representation of the MQTT text entity."""
127+
128+
_attributes_extra_blocked = MQTT_TEXT_ATTRIBUTES_BLOCKED
129+
_entity_id_format = text.ENTITY_ID_FORMAT
130+
131+
_compiled_pattern: re.Pattern[Any] | None
132+
_optimistic: bool
133+
_command_template: Callable[[PublishPayloadType], PublishPayloadType]
134+
_value_template: Callable[[ReceivePayloadType], ReceivePayloadType]
135+
136+
def __init__(
137+
self,
138+
hass: HomeAssistant,
139+
config: ConfigType,
140+
config_entry: ConfigEntry,
141+
discovery_data: DiscoveryInfoType | None = None,
142+
) -> None:
143+
"""Initialize MQTT text entity."""
144+
self._attr_native_value = None
145+
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
146+
147+
@staticmethod
148+
def config_schema() -> vol.Schema:
149+
"""Return the config schema."""
150+
return DISCOVERY_SCHEMA
151+
152+
def _setup_from_config(self, config: ConfigType) -> None:
153+
"""(Re)Setup the entity."""
154+
self._attr_native_max = config[CONF_MAX]
155+
self._attr_native_min = config[CONF_MIN]
156+
self._attr_mode = config[CONF_MODE]
157+
self._compiled_pattern = config.get(CONF_PATTERN)
158+
self._attr_pattern = (
159+
self._compiled_pattern.pattern if self._compiled_pattern else None
160+
)
161+
162+
self._command_template = MqttCommandTemplate(
163+
config.get(CONF_COMMAND_TEMPLATE),
164+
entity=self,
165+
).async_render
166+
self._value_template = MqttValueTemplate(
167+
config.get(CONF_VALUE_TEMPLATE),
168+
entity=self,
169+
).async_render_with_possible_json_value
170+
optimistic: bool = config[CONF_OPTIMISTIC]
171+
self._optimistic = optimistic or config.get(CONF_STATE_TOPIC) is None
172+
173+
def _prepare_subscribe_topics(self) -> None:
174+
"""(Re)Subscribe to topics."""
175+
topics: dict[str, Any] = {}
176+
177+
def add_subscription(
178+
topics: dict[str, Any], topic: str, msg_callback: Callable
179+
) -> None:
180+
if self._config.get(topic) is not None:
181+
topics[topic] = {
182+
"topic": self._config[topic],
183+
"msg_callback": msg_callback,
184+
"qos": self._config[CONF_QOS],
185+
"encoding": self._config[CONF_ENCODING] or None,
186+
}
187+
188+
@callback
189+
@log_messages(self.hass, self.entity_id)
190+
def handle_state_message_received(msg: ReceiveMessage) -> None:
191+
"""Handle receiving state message via MQTT."""
192+
payload = str(self._value_template(msg.payload))
193+
self._attr_native_value = payload
194+
get_mqtt_data(self.hass).state_write_requests.write_state_request(self)
195+
196+
add_subscription(topics, CONF_STATE_TOPIC, handle_state_message_received)
197+
198+
self._sub_state = subscription.async_prepare_subscribe_topics(
199+
self.hass, self._sub_state, topics
200+
)
201+
202+
async def _subscribe_topics(self) -> None:
203+
"""(Re)Subscribe to topics."""
204+
await subscription.async_subscribe_topics(self.hass, self._sub_state)
205+
206+
@property
207+
def assumed_state(self) -> bool:
208+
"""Return true if we do optimistic updates."""
209+
return self._optimistic
210+
211+
async def async_set_value(self, value: str) -> None:
212+
"""Change the text."""
213+
payload = self._command_template(value)
214+
215+
await self.async_publish(
216+
self._config[CONF_COMMAND_TOPIC],
217+
payload,
218+
self._config[CONF_QOS],
219+
self._config[CONF_RETAIN],
220+
self._config[CONF_ENCODING],
221+
)
222+
if self._optimistic:
223+
self._attr_native_value = value
224+
self.async_write_ha_state()

0 commit comments

Comments
 (0)