diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index ba76053686..db915e9c7f 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -186,7 +186,12 @@ def _base_config_schema(class_: MockObjClass) -> cv.Schema: cv.Optional( CONF_ANSWER_TIMEOUT, ): cv.positive_time_period_milliseconds, - cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation({}), + # data points into the protocol's receive buffer, reused for the next frame + cv.Optional( + CONF_ON_STATUS_MESSAGE + ): automation.validate_synchronous_automation( + "on_status_message", "the received frame" + ), } ) .extend(uart.UART_DEVICE_SCHEMA) diff --git a/tests/component_tests/haier/__init__.py b/tests/component_tests/haier/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component_tests/haier/test_status_message_automation.py b/tests/component_tests/haier/test_status_message_automation.py new file mode 100644 index 0000000000..5f4927fe10 --- /dev/null +++ b/tests/component_tests/haier/test_status_message_automation.py @@ -0,0 +1,61 @@ +"""Tests that haier on_status_message automations reject deferring actions. + +The trigger passes a pointer into the protocol's receive buffer, which is reused for the next +frame, 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.haier.climate import ( + CONF_ON_STATUS_MESSAGE, + HonClimate, + Smartair2Climate, + _base_config_schema, +) +from esphome.core import Lambda +from esphome.cpp_generator import MockObjClass +from esphome.types import ConfigType + +CLIMATE_CLASSES = [HonClimate, Smartair2Climate] + +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( + class_: MockObjClass, +) -> Callable[[list[ConfigType]], list[ConfigType]]: + """The validator the protocol's schema applies to on_status_message.""" + return _base_config_schema(class_).schema[cv.Optional(CONF_ON_STATUS_MESSAGE)] + + +@pytest.mark.parametrize("class_", CLIMATE_CLASSES) +def test_synchronous_automation_accepted(class_: MockObjClass) -> None: + _trigger_schema(class_)([{"then": [SYNCHRONOUS_ACTION]}]) + + +@pytest.mark.parametrize("class_", CLIMATE_CLASSES) +def test_deferring_action_rejected(class_: MockObjClass) -> None: + with pytest.raises(cv.Invalid, match=DEFER_MESSAGE): + _trigger_schema(class_)([{"then": [SYNCHRONOUS_ACTION, DEFERRING_ACTION]}]) + + +@pytest.mark.parametrize("class_", CLIMATE_CLASSES) +def test_nested_deferring_action_rejected(class_: MockObjClass) -> None: + actions = [{"if": {"condition": TRUE_CONDITION, "then": [DEFERRING_ACTION]}}] + with pytest.raises(cv.Invalid, match=DEFER_MESSAGE): + _trigger_schema(class_)([{"then": actions}]) + + +@pytest.mark.parametrize("class_", CLIMATE_CLASSES) +def test_deferring_action_in_second_automation_rejected(class_: MockObjClass) -> None: + with pytest.raises(cv.Invalid, match=DEFER_MESSAGE): + _trigger_schema(class_)( + [{"then": [SYNCHRONOUS_ACTION]}, {"then": [DEFERRING_ACTION]}] + ) diff --git a/tests/components/haier/common.yaml b/tests/components/haier/common.yaml index 926a1d1b7a..ab39dfa61d 100644 --- a/tests/components/haier/common.yaml +++ b/tests/components/haier/common.yaml @@ -43,6 +43,12 @@ climate: level: DEBUG format: "Alarm deactivated. Code: %d. Message: \"%s\"" args: [code, message] + on_status_message: + then: + - logger.log: + level: DEBUG + format: "Status message: %u bytes" + args: [(unsigned) data_size] sensor: - platform: haier