From 55bd63732d984ab8b8227015594201d565ed82ff Mon Sep 17 00:00:00 2001 From: Boris Krivonog Date: Tue, 11 Aug 2026 20:56:12 +0200 Subject: [PATCH] [mitsubishi_cn105] Add vertical vane state trigger (#16727) Co-authored-by: J. Nick Koston --- .../components/mitsubishi_cn105/__init__.py | 19 +++++++- .../mitsubishi_cn105_component.cpp | 2 +- .../mitsubishi_cn105_component.h | 34 ++++++++++++- tests/components/mitsubishi_cn105/common.h | 7 +++ tests/components/mitsubishi_cn105/common.yaml | 5 ++ .../mitsubishi_cn105_component_tests.cpp | 48 +++++++++++++++++++ ...bishi_cn105_vane_select_vertical_tests.cpp | 7 --- 7 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp diff --git a/esphome/components/mitsubishi_cn105/__init__.py b/esphome/components/mitsubishi_cn105/__init__.py index 7d5594495a..70ed0a7a85 100644 --- a/esphome/components/mitsubishi_cn105/__init__.py +++ b/esphome/components/mitsubishi_cn105/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL +from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL from esphome.core import ID from esphome.cpp_generator import MockObj from esphome.types import ConfigType, TemplateArgsType @@ -13,6 +13,7 @@ DOMAIN = "mitsubishi_cn105" CONF_MITSUBISHI_CN105_ID = f"{DOMAIN}_id" CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval" +CONF_VANE = "vane" mitsubishi_ns = cg.esphome_ns.namespace(DOMAIN) @@ -22,6 +23,8 @@ MitsubishiCN105Component = mitsubishi_ns.class_( uart.UARTDevice, ) +VaneState = mitsubishi_ns.struct("VaneState") + SetRemoteTemperatureAction = mitsubishi_ns.class_( "SetRemoteTemperatureAction", automation.Action, @@ -42,6 +45,11 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s" ): cv.update_interval, + cv.Optional(CONF_VANE): cv.Schema( + { + cv.Optional(CONF_ON_STATE): automation.validate_automation({}), + } + ), } ) .extend(cv.COMPONENT_SCHEMA) @@ -80,6 +88,15 @@ async def to_code(config: ConfigType) -> None: config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL] ) ) + if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE): + cg.add_global(mitsubishi_ns.using) + for conf in on_state: + await automation.build_callback_automation( + var, + "add_on_vane_state_callback", + [(VaneState.operator("const").operator("ref"), "x")], + conf, + ) REMOTE_TEMPERATURE_ACTION_SCHEMA = cv.Schema( diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp index 166e7fbf88..5314965af6 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp @@ -27,7 +27,7 @@ void MitsubishiCN105Component::setup() { this->hp_.initialize(); } void MitsubishiCN105Component::loop() { if (this->hp_.update()) { - this->status_callback_.call(); + this->notify_status_listeners_(); } } diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h index 1caf779f40..64077432fd 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h @@ -9,6 +9,24 @@ namespace esphome::mitsubishi_cn105 { +enum VerticalVaneMode : uint8_t { + VERTICAL_VANE_MODE_AUTO = static_cast(MitsubishiCN105::VaneMode::AUTO), + VERTICAL_VANE_MODE_POSITION_1 = static_cast(MitsubishiCN105::VaneMode::POSITION_1), + VERTICAL_VANE_MODE_POSITION_2 = static_cast(MitsubishiCN105::VaneMode::POSITION_2), + VERTICAL_VANE_MODE_POSITION_3 = static_cast(MitsubishiCN105::VaneMode::POSITION_3), + VERTICAL_VANE_MODE_POSITION_4 = static_cast(MitsubishiCN105::VaneMode::POSITION_4), + VERTICAL_VANE_MODE_POSITION_5 = static_cast(MitsubishiCN105::VaneMode::POSITION_5), + VERTICAL_VANE_MODE_SWING = static_cast(MitsubishiCN105::VaneMode::SWING), +}; + +struct VaneState { + struct Vertical { + VerticalVaneMode direction; + }; + + Vertical vertical; +}; + class MitsubishiCN105Component : public Component, public uart::UARTDevice { public: explicit MitsubishiCN105Component() : hp_(*this) {} @@ -38,15 +56,29 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice { this->status_callback_.add(std::forward(callback)); } + template void add_on_vane_state_callback(F &&callback) { + this->vane_state_callback_.add(std::forward(callback)); + } + void publish_status() { if (this->is_status_initialized()) { - this->status_callback_.call(); + this->notify_status_listeners_(); } } protected: + void notify_status_listeners_() { + this->status_callback_.call(); + if (this->status().vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) { + this->vane_state_callback_.call(VaneState{ + .vertical = {.direction = static_cast(this->status().vane_mode)}, + }); + } + } + MitsubishiCN105 hp_; CallbackManager status_callback_; + LazyCallbackManager vane_state_callback_; }; } // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index b90ddf3995..a119a38d24 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -77,4 +77,11 @@ class TestableMitsubishiCN105Climate : public MitsubishiCN105Climate { MitsubishiCN105Component component_; }; +class TestableMitsubishiCN105Component : public MitsubishiCN105Component { + public: + MitsubishiCN105::Status &mutable_status() { return const_cast(this->status()); } + + void notify_status() { this->status_callback_.call(); } +}; + } // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml index 12a3b8ce9d..fcd1b048dd 100644 --- a/tests/components/mitsubishi_cn105/common.yaml +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -3,6 +3,11 @@ mitsubishi_cn105: uart_id: uart_bus update_interval: 30s telemetry_request_min_interval: 120s + vane: + on_state: + - logger.log: + format: "TRIGGER: vane on_state is auto: %s" + args: ['x.vertical.direction == VERTICAL_VANE_MODE_AUTO ? "yes" : "no"'] climate: - platform: mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp b/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp new file mode 100644 index 0000000000..48ea6b0c29 --- /dev/null +++ b/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp @@ -0,0 +1,48 @@ +#include "common.h" + +namespace esphome::mitsubishi_cn105::testing { + +TEST(MitsubishiCN105ComponentTests, PublishesVaneStateForEveryValidSnapshot) { + TestableMitsubishiCN105Component hub; + size_t callback_count = 0; + std::optional callback_direction; + hub.add_on_vane_state_callback([&](const VaneState &state) { + callback_count++; + callback_direction = state.vertical.direction; + }); + + hub.mutable_status().room_temperature = 20.0f; + hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::POSITION_4; + hub.publish_status(); + + EXPECT_EQ(callback_count, 1); + EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_POSITION_4}); + + hub.publish_status(); + + EXPECT_EQ(callback_count, 2); + EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_POSITION_4}); +} + +TEST(MitsubishiCN105ComponentTests, DoesNotPublishUnknownVaneState) { + TestableMitsubishiCN105Component hub; + size_t status_callback_count = 0; + size_t vane_callback_count = 0; + hub.add_on_status_callback([&]() { status_callback_count++; }); + hub.add_on_vane_state_callback([&](const VaneState &) { vane_callback_count++; }); + + hub.mutable_status().room_temperature = 20.0f; + hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::UNKNOWN; + hub.publish_status(); + + EXPECT_EQ(status_callback_count, 1); + EXPECT_EQ(vane_callback_count, 0); + + hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::POSITION_4; + hub.publish_status(); + + EXPECT_EQ(status_callback_count, 2); + EXPECT_EQ(vane_callback_count, 1); +} + +} // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical_tests.cpp b/tests/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical_tests.cpp index 4c980d69d8..1f928e3bf4 100644 --- a/tests/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical_tests.cpp +++ b/tests/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical_tests.cpp @@ -3,13 +3,6 @@ namespace esphome::mitsubishi_cn105::testing { -class TestableMitsubishiCN105Component : public MitsubishiCN105Component { - public: - MitsubishiCN105::Status &mutable_status() { return const_cast(this->status()); } - - void notify_status() { this->status_callback_.call(); } -}; - class TestableMitsubishiCN105VerticalVaneDirectionSelect : public MitsubishiCN105VerticalVaneDirectionSelect { public: using MitsubishiCN105VerticalVaneDirectionSelect::control;