mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Allow raw mqtt payload to be in mqtt publish action (#123900)
* Publish raw rendered mqtt payload as raw for mqtt publish action * Move check out of try block * Only try to eval `bytes` is payload starts with supported string Co-authored-by: Erik Montnemery <erik@montnemery.com> * Improve docst * Add `evaluate_bytes` option to publish action * Rename to `evaluate_payload` * Update homeassistant/components/mqtt/strings.json Co-authored-by: Erik Montnemery <erik@montnemery.com> * Extend test to assert literal_eval is called or not --------- Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
f3e2d06922
commit
183c191d63
@ -90,6 +90,7 @@ from .models import ( # noqa: F401
|
|||||||
PublishPayloadType,
|
PublishPayloadType,
|
||||||
ReceiveMessage,
|
ReceiveMessage,
|
||||||
ReceivePayloadType,
|
ReceivePayloadType,
|
||||||
|
convert_outgoing_mqtt_payload,
|
||||||
)
|
)
|
||||||
from .subscription import ( # noqa: F401
|
from .subscription import ( # noqa: F401
|
||||||
EntitySubscription,
|
EntitySubscription,
|
||||||
@ -115,6 +116,7 @@ SERVICE_DUMP = "dump"
|
|||||||
|
|
||||||
ATTR_TOPIC_TEMPLATE = "topic_template"
|
ATTR_TOPIC_TEMPLATE = "topic_template"
|
||||||
ATTR_PAYLOAD_TEMPLATE = "payload_template"
|
ATTR_PAYLOAD_TEMPLATE = "payload_template"
|
||||||
|
ATTR_EVALUATE_PAYLOAD = "evaluate_payload"
|
||||||
|
|
||||||
MAX_RECONNECT_WAIT = 300 # seconds
|
MAX_RECONNECT_WAIT = 300 # seconds
|
||||||
|
|
||||||
@ -166,6 +168,7 @@ MQTT_PUBLISH_SCHEMA = vol.All(
|
|||||||
vol.Exclusive(ATTR_TOPIC_TEMPLATE, CONF_TOPIC): cv.string,
|
vol.Exclusive(ATTR_TOPIC_TEMPLATE, CONF_TOPIC): cv.string,
|
||||||
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string,
|
vol.Exclusive(ATTR_PAYLOAD, CONF_PAYLOAD): cv.string,
|
||||||
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string,
|
vol.Exclusive(ATTR_PAYLOAD_TEMPLATE, CONF_PAYLOAD): cv.string,
|
||||||
|
vol.Optional(ATTR_EVALUATE_PAYLOAD): cv.boolean,
|
||||||
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
vol.Optional(ATTR_QOS, default=DEFAULT_QOS): valid_qos_schema,
|
||||||
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
vol.Optional(ATTR_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||||
},
|
},
|
||||||
@ -295,6 +298,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
msg_topic: str | None = call.data.get(ATTR_TOPIC)
|
msg_topic: str | None = call.data.get(ATTR_TOPIC)
|
||||||
msg_topic_template: str | None = call.data.get(ATTR_TOPIC_TEMPLATE)
|
msg_topic_template: str | None = call.data.get(ATTR_TOPIC_TEMPLATE)
|
||||||
payload: PublishPayloadType = call.data.get(ATTR_PAYLOAD)
|
payload: PublishPayloadType = call.data.get(ATTR_PAYLOAD)
|
||||||
|
evaluate_payload: bool = call.data.get(ATTR_EVALUATE_PAYLOAD, False)
|
||||||
payload_template: str | None = call.data.get(ATTR_PAYLOAD_TEMPLATE)
|
payload_template: str | None = call.data.get(ATTR_PAYLOAD_TEMPLATE)
|
||||||
qos: int = call.data[ATTR_QOS]
|
qos: int = call.data[ATTR_QOS]
|
||||||
retain: bool = call.data[ATTR_RETAIN]
|
retain: bool = call.data[ATTR_RETAIN]
|
||||||
@ -354,6 +358,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
payload = MqttCommandTemplate(
|
payload = MqttCommandTemplate(
|
||||||
template.Template(payload_template, hass)
|
template.Template(payload_template, hass)
|
||||||
).async_render()
|
).async_render()
|
||||||
|
elif evaluate_payload:
|
||||||
|
# Convert quoted binary literal to raw data
|
||||||
|
payload = convert_outgoing_mqtt_payload(payload)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert msg_topic is not None
|
assert msg_topic is not None
|
||||||
|
@ -51,6 +51,22 @@ ATTR_THIS = "this"
|
|||||||
type PublishPayloadType = str | bytes | int | float | None
|
type PublishPayloadType = str | bytes | int | float | None
|
||||||
|
|
||||||
|
|
||||||
|
def convert_outgoing_mqtt_payload(
|
||||||
|
payload: PublishPayloadType,
|
||||||
|
) -> PublishPayloadType:
|
||||||
|
"""Ensure correct raw MQTT payload is passed as bytes for publishing."""
|
||||||
|
if isinstance(payload, str) and payload.startswith(("b'", 'b"')):
|
||||||
|
try:
|
||||||
|
native_object = literal_eval(payload)
|
||||||
|
except (ValueError, TypeError, SyntaxError, MemoryError):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(native_object, bytes):
|
||||||
|
return native_object
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PublishMessage:
|
class PublishMessage:
|
||||||
"""MQTT Message for publishing."""
|
"""MQTT Message for publishing."""
|
||||||
@ -173,22 +189,6 @@ class MqttCommandTemplate:
|
|||||||
variables: TemplateVarsType = None,
|
variables: TemplateVarsType = None,
|
||||||
) -> PublishPayloadType:
|
) -> PublishPayloadType:
|
||||||
"""Render or convert the command template with given value or variables."""
|
"""Render or convert the command template with given value or variables."""
|
||||||
|
|
||||||
def _convert_outgoing_payload(
|
|
||||||
payload: PublishPayloadType,
|
|
||||||
) -> PublishPayloadType:
|
|
||||||
"""Ensure correct raw MQTT payload is passed as bytes for publishing."""
|
|
||||||
if isinstance(payload, str):
|
|
||||||
try:
|
|
||||||
native_object = literal_eval(payload)
|
|
||||||
if isinstance(native_object, bytes):
|
|
||||||
return native_object
|
|
||||||
|
|
||||||
except (ValueError, TypeError, SyntaxError, MemoryError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
if self._command_template is None:
|
if self._command_template is None:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ class MqttCommandTemplate:
|
|||||||
self._command_template,
|
self._command_template,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return _convert_outgoing_payload(
|
return convert_outgoing_mqtt_payload(
|
||||||
self._command_template.async_render(values, parse_result=False)
|
self._command_template.async_render(values, parse_result=False)
|
||||||
)
|
)
|
||||||
except TemplateError as exc:
|
except TemplateError as exc:
|
||||||
|
@ -12,6 +12,11 @@ publish:
|
|||||||
example: "The temperature is {{ states('sensor.temperature') }}"
|
example: "The temperature is {{ states('sensor.temperature') }}"
|
||||||
selector:
|
selector:
|
||||||
template:
|
template:
|
||||||
|
evaluate_payload:
|
||||||
|
advanced: true
|
||||||
|
default: false
|
||||||
|
selector:
|
||||||
|
boolean:
|
||||||
qos:
|
qos:
|
||||||
advanced: true
|
advanced: true
|
||||||
default: 0
|
default: 0
|
||||||
|
@ -230,6 +230,10 @@
|
|||||||
"name": "Publish",
|
"name": "Publish",
|
||||||
"description": "Publishes a message to an MQTT topic.",
|
"description": "Publishes a message to an MQTT topic.",
|
||||||
"fields": {
|
"fields": {
|
||||||
|
"evaluate_payload": {
|
||||||
|
"name": "Evaluate payload",
|
||||||
|
"description": "When `payload` is a Python bytes literal, evaluate the bytes literal and publish the raw data."
|
||||||
|
},
|
||||||
"topic": {
|
"topic": {
|
||||||
"name": "Topic",
|
"name": "Topic",
|
||||||
"description": "Topic to publish to."
|
"description": "Topic to publish to."
|
||||||
|
@ -420,6 +420,74 @@ async def test_mqtt_publish_action_call_with_template_payload_renders_template(
|
|||||||
mqtt_mock.reset_mock()
|
mqtt_mock.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("attr_payload", "payload", "evaluate_payload", "literal_eval_calls"),
|
||||||
|
[
|
||||||
|
("b'\\xde\\xad\\xbe\\xef'", b"\xde\xad\xbe\xef", True, 1),
|
||||||
|
("b'\\xde\\xad\\xbe\\xef'", "b'\\xde\\xad\\xbe\\xef'", False, 0),
|
||||||
|
("DEADBEEF", "DEADBEEF", False, 0),
|
||||||
|
(
|
||||||
|
"b'\\xde",
|
||||||
|
"b'\\xde",
|
||||||
|
True,
|
||||||
|
1,
|
||||||
|
), # Bytes literal is invalid, fall back to string
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_mqtt_publish_action_call_with_raw_data(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mqtt_mock_entry: MqttMockHAClientGenerator,
|
||||||
|
attr_payload: str,
|
||||||
|
payload: str | bytes,
|
||||||
|
evaluate_payload: bool,
|
||||||
|
literal_eval_calls: int,
|
||||||
|
) -> None:
|
||||||
|
"""Test the mqtt publish action call raw data.
|
||||||
|
|
||||||
|
When `payload` represents a `bytes` object, it should be published
|
||||||
|
as raw data if `evaluate_payload` is set.
|
||||||
|
"""
|
||||||
|
mqtt_mock = await mqtt_mock_entry()
|
||||||
|
await hass.services.async_call(
|
||||||
|
mqtt.DOMAIN,
|
||||||
|
mqtt.SERVICE_PUBLISH,
|
||||||
|
{
|
||||||
|
mqtt.ATTR_TOPIC: "test/topic",
|
||||||
|
mqtt.ATTR_PAYLOAD: attr_payload,
|
||||||
|
mqtt.ATTR_EVALUATE_PAYLOAD: evaluate_payload,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert mqtt_mock.async_publish.called
|
||||||
|
assert mqtt_mock.async_publish.call_args[0][1] == payload
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mqtt.models.literal_eval"
|
||||||
|
) as literal_eval_mock:
|
||||||
|
await hass.services.async_call(
|
||||||
|
mqtt.DOMAIN,
|
||||||
|
mqtt.SERVICE_PUBLISH,
|
||||||
|
{
|
||||||
|
mqtt.ATTR_TOPIC: "test/topic",
|
||||||
|
mqtt.ATTR_PAYLOAD: attr_payload,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
literal_eval_mock.assert_not_called()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
mqtt.DOMAIN,
|
||||||
|
mqtt.SERVICE_PUBLISH,
|
||||||
|
{
|
||||||
|
mqtt.ATTR_TOPIC: "test/topic",
|
||||||
|
mqtt.ATTR_PAYLOAD: attr_payload,
|
||||||
|
mqtt.ATTR_EVALUATE_PAYLOAD: evaluate_payload,
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(literal_eval_mock.mock_calls) == literal_eval_calls
|
||||||
|
|
||||||
|
|
||||||
# The use of a payload_template in an mqtt publish action call
|
# The use of a payload_template in an mqtt publish action call
|
||||||
# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0
|
# has been deprecated with HA Core 2024.8.0 and will be removed with HA Core 2025.2.0
|
||||||
async def test_publish_action_call_with_bad_payload_template(
|
async def test_publish_action_call_with_bad_payload_template(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user