[emontx] Pass on_json/on_data data without copies and reject deferring actions (#20516)

This commit is contained in:
Frédéric Metrich
2026-10-11 06:52:41 -10:00
committed by GitHub
parent 1312d5eec0
commit 5186728d53
2 changed files with 74 additions and 4 deletions
+20 -4
View File
@@ -44,13 +44,29 @@ def _get_data() -> EmonTxData:
return CORE.data[DOMAIN]
def _synchronous_automation(value: ConfigType) -> ConfigType:
"""Reject deferring actions: the received line and parsed JSON are only valid while the
automation runs, and DelayAction and friends keep the trigger arguments for later."""
if automation.has_non_synchronous_actions(value):
raise cv.Invalid(
"Deferring actions (delay, wait_until, script.wait, ...) are not allowed in on_json and "
"on_data: the received data is only valid while the automation runs. Copy what you need "
"into globals first, then defer in a separate script or automation."
)
return value
def _synchronous_automation_schema() -> cv.All:
return cv.All(automation.validate_automation({}), _synchronous_automation)
# Main configuration schema
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): automation.validate_automation({}),
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -104,10 +120,10 @@ _CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_JSON,
"add_on_json_callback",
[(cg.JsonObject, "json"), (cg.std_string, "raw_json")],
[(cg.JsonObject, "json"), (cg.StringRef, "raw_json")],
),
automation.CallbackAutomation(
CONF_ON_DATA, "add_on_data_callback", [(cg.std_string, "data")]
CONF_ON_DATA, "add_on_data_callback", [(cg.StringRef, "data")]
),
)
@@ -0,0 +1,54 @@
"""Tests that emontx on_json/on_data automations reject deferring actions.
The triggers pass views into the receive buffer and the parsed JSON document, which are only valid
while the automation runs, so the deferring-actions guard is a safety property.
"""
from collections.abc import Callable
import pytest
from esphome import config_validation as cv
from esphome.components.emontx import CONF_ON_JSON, CONFIG_SCHEMA
from esphome.const import CONF_ON_DATA
from esphome.core import Lambda
from esphome.types import ConfigType
TRIGGER_KEYS = [CONF_ON_JSON, CONF_ON_DATA]
DEFERRING_ACTION = {"delay": "1s"}
SYNCHRONOUS_ACTION = {"lambda": Lambda('ESP_LOGD("test", "ran");')}
TRUE_CONDITION = {"lambda": Lambda("return true;")}
DEFER_MESSAGE = "Deferring actions"
def _trigger_schema(trigger_key: str) -> Callable[[list[ConfigType]], list[ConfigType]]:
"""The validator CONFIG_SCHEMA applies to this trigger key."""
return CONFIG_SCHEMA.schema[cv.Optional(trigger_key)]
@pytest.mark.parametrize("trigger_key", TRIGGER_KEYS)
def test_synchronous_automation_accepted(trigger_key: str) -> None:
_trigger_schema(trigger_key)([{"then": [SYNCHRONOUS_ACTION]}])
@pytest.mark.parametrize("trigger_key", TRIGGER_KEYS)
def test_deferring_action_rejected(trigger_key: str) -> None:
with pytest.raises(cv.Invalid, match=DEFER_MESSAGE):
_trigger_schema(trigger_key)([{"then": [SYNCHRONOUS_ACTION, DEFERRING_ACTION]}])
@pytest.mark.parametrize("trigger_key", TRIGGER_KEYS)
def test_nested_deferring_action_rejected(trigger_key: str) -> None:
actions = [{"if": {"condition": TRUE_CONDITION, "then": [DEFERRING_ACTION]}}]
with pytest.raises(cv.Invalid, match=DEFER_MESSAGE):
_trigger_schema(trigger_key)([{"then": actions}])
@pytest.mark.parametrize("trigger_key", TRIGGER_KEYS)
def test_deferring_action_in_second_automation_rejected(trigger_key: str) -> None:
with pytest.raises(cv.Invalid, match=DEFER_MESSAGE):
_trigger_schema(trigger_key)(
[{"then": [SYNCHRONOUS_ACTION]}, {"then": [DEFERRING_ACTION]}]
)