From 9556c2bc4c4c77871a2e48d631a2f78a452c7232 Mon Sep 17 00:00:00 2001 From: Boris Krivonog Date: Wed, 12 Aug 2026 01:39:05 +0200 Subject: [PATCH] [mitsubishi_cn105] Add vertical vane control action (#16737) Co-authored-by: J. Nick Koston --- .../components/mitsubishi_cn105/__init__.py | 99 ++++++++++++++++++- .../components/mitsubishi_cn105/automation.h | 19 ++++ .../mitsubishi_cn105_component.cpp | 7 ++ .../mitsubishi_cn105_component.h | 32 +++++- .../mitsubishi_cn105/select/__init__.py | 6 +- .../mitsubishi_cn105_vane_select_vertical.cpp | 2 +- tests/components/mitsubishi_cn105/common.h | 1 + tests/components/mitsubishi_cn105/common.yaml | 8 ++ .../mitsubishi_cn105_component_tests.cpp | 33 ++++++- 9 files changed, 190 insertions(+), 17 deletions(-) diff --git a/esphome/components/mitsubishi_cn105/__init__.py b/esphome/components/mitsubishi_cn105/__init__.py index 70ed0a7a85..450d1cd222 100644 --- a/esphome/components/mitsubishi_cn105/__init__.py +++ b/esphome/components/mitsubishi_cn105/__init__.py @@ -2,9 +2,15 @@ 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_ON_STATE, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL -from esphome.core import ID -from esphome.cpp_generator import MockObj +from esphome.const import ( + CONF_DIRECTION, + CONF_ID, + CONF_ON_STATE, + CONF_TEMPERATURE, + CONF_UPDATE_INTERVAL, +) +from esphome.core import ID, Lambda +from esphome.cpp_generator import LambdaExpression, MockObj from esphome.types import ConfigType, TemplateArgsType CODEOWNERS = ["@crnjan"] @@ -14,6 +20,7 @@ DOMAIN = "mitsubishi_cn105" CONF_MITSUBISHI_CN105_ID = f"{DOMAIN}_id" CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval" CONF_VANE = "vane" +CONF_VERTICAL = "vertical" mitsubishi_ns = cg.esphome_ns.namespace(DOMAIN) @@ -24,6 +31,20 @@ MitsubishiCN105Component = mitsubishi_ns.class_( ) VaneState = mitsubishi_ns.struct("VaneState") +VaneCall = mitsubishi_ns.class_("VaneCall") +VerticalVaneMode = mitsubishi_ns.enum("VerticalVaneMode") + +# The insertion order must match VALUES in +# select/mitsubishi_cn105_vane_select_vertical.cpp. +VERTICAL_VANE_DIRECTIONS = { + "AUTO": VerticalVaneMode.VERTICAL_VANE_MODE_AUTO, + "1": VerticalVaneMode.VERTICAL_VANE_MODE_POSITION_1, + "2": VerticalVaneMode.VERTICAL_VANE_MODE_POSITION_2, + "3": VerticalVaneMode.VERTICAL_VANE_MODE_POSITION_3, + "4": VerticalVaneMode.VERTICAL_VANE_MODE_POSITION_4, + "5": VerticalVaneMode.VERTICAL_VANE_MODE_POSITION_5, + "SWING": VerticalVaneMode.VERTICAL_VANE_MODE_SWING, +} SetRemoteTemperatureAction = mitsubishi_ns.class_( "SetRemoteTemperatureAction", @@ -37,6 +58,11 @@ ClearRemoteTemperatureAction = mitsubishi_ns.class_( cg.Parented.template(MitsubishiCN105Component), ) +VaneControlAction = mitsubishi_ns.class_( + "VaneControlAction", + automation.Action, +) + CONFIG_SCHEMA = ( cv.Schema( { @@ -152,3 +178,70 @@ async def clear_temperature_action_to_code( var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) return var + + +VANE_CONTROL_FIELDS = ( + ( + (CONF_VERTICAL, CONF_DIRECTION), + "vertical.set_direction", + VerticalVaneMode, + ), +) + +VANE_CONTROL_ACTION_SCHEMA = cv.Schema( + { + cv.Required(CONF_ID): cv.use_id(MitsubishiCN105Component), + cv.Optional(CONF_VERTICAL): cv.Schema( + { + cv.Optional(CONF_DIRECTION): cv.templatable( + cv.enum(VERTICAL_VANE_DIRECTIONS, upper=True) + ), + } + ), + } +) + + +@automation.register_action( + f"{DOMAIN}.vane.control", + VaneControlAction, + VANE_CONTROL_ACTION_SCHEMA, + synchronous=True, +) +async def vane_control_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + cg.add_global(mitsubishi_ns.using) + parent = await cg.get_variable(config[CONF_ID]) + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), name) + for t, name in args + ] + forwarded_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] + + for path, setter, type_ in VANE_CONTROL_FIELDS: + if (section := config.get(path[0])) is None: + continue + if (value := section.get(path[1])) is None: + continue + if isinstance(value, Lambda): + inner = await cg.process_lambda( + value, + normalized_args, + return_type=type_, + ) + body_lines.append(f"call.{setter}(({inner})({forwarded_args}));") + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") + + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + [(VaneCall.operator("ref"), "call"), *normalized_args], + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, parent, apply_lambda) diff --git a/esphome/components/mitsubishi_cn105/automation.h b/esphome/components/mitsubishi_cn105/automation.h index 879e556f9c..2fc6ba3c32 100644 --- a/esphome/components/mitsubishi_cn105/automation.h +++ b/esphome/components/mitsubishi_cn105/automation.h @@ -4,6 +4,8 @@ #include "esphome/core/automation.h" +#include + namespace esphome::mitsubishi_cn105 { template @@ -20,4 +22,21 @@ class ClearRemoteTemperatureAction : public Action, public Parentedparent_->clear_remote_temperature(); } }; +template class VaneControlAction : public Action { + public: + using ApplyFn = void (*)(VaneCall &, const std::remove_cvref_t &...); + + VaneControlAction(MitsubishiCN105Component *parent, ApplyFn apply) : parent_(parent), apply_(apply) {} + + void play(const Ts &...x) override { + auto call = this->parent_->make_vane_call(); + this->apply_(call, x...); + call.perform(); + } + + protected: + MitsubishiCN105Component *parent_; + ApplyFn apply_; +}; + } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp index 5314965af6..8e9e954645 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.cpp @@ -31,4 +31,11 @@ void MitsubishiCN105Component::loop() { } } +void VaneCall::perform() { + if (const auto &direction = this->vertical.get_direction(); direction.has_value()) { + this->parent_->set_vane_mode(static_cast(*direction)); + } + this->parent_->publish_status(); +} + } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h index 64077432fd..6461fb464b 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h @@ -6,6 +6,7 @@ #include "esphome/components/uart/uart.h" #include +#include namespace esphome::mitsubishi_cn105 { @@ -17,6 +18,7 @@ enum VerticalVaneMode : uint8_t { 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), + VERTICAL_VANE_MODE_UNKNOWN = static_cast(MitsubishiCN105::VaneMode::UNKNOWN), }; struct VaneState { @@ -27,6 +29,27 @@ struct VaneState { Vertical vertical; }; +class MitsubishiCN105Component; + +struct VaneCall { + struct Vertical { + void set_direction(VerticalVaneMode direction) { this->direction_ = direction; } + const std::optional &get_direction() const { return this->direction_; } + + protected: + std::optional direction_; + }; + + explicit VaneCall(MitsubishiCN105Component *parent) : parent_(parent) {} + + Vertical vertical; + + void perform(); + + protected: + MitsubishiCN105Component *parent_; +}; + class MitsubishiCN105Component : public Component, public uart::UARTDevice { public: explicit MitsubishiCN105Component() : hp_(*this) {} @@ -47,6 +70,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice { void set_fan_mode(MitsubishiCN105::FanMode fan_mode) { this->hp_.set_fan_mode(fan_mode); } void set_vane_mode(MitsubishiCN105::VaneMode vane_mode) { this->hp_.set_vane_mode(vane_mode); } void set_wide_vane_mode(MitsubishiCN105::WideVaneMode mode) { this->hp_.set_wide_vane_mode(mode); } + VaneCall make_vane_call() { return VaneCall(this); } const MitsubishiCN105::Status &status() const { return this->hp_.status(); } bool is_status_initialized() const { return this->hp_.is_status_initialized(); } @@ -69,11 +93,9 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice { 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)}, - }); - } + this->vane_state_callback_.call(VaneState{ + .vertical = {.direction = static_cast(this->status().vane_mode)}, + }); } MitsubishiCN105 hp_; diff --git a/esphome/components/mitsubishi_cn105/select/__init__.py b/esphome/components/mitsubishi_cn105/select/__init__.py index a2e0353f85..4ca12edbb4 100644 --- a/esphome/components/mitsubishi_cn105/select/__init__.py +++ b/esphome/components/mitsubishi_cn105/select/__init__.py @@ -6,6 +6,7 @@ from esphome.types import ConfigType from .. import ( MITSUBISHI_CN105_DEVICE_SCHEMA, + VERTICAL_VANE_DIRECTIONS, MitsubishiCN105Component, mitsubishi_ns, register_mitsubishi_cn105_device, @@ -15,9 +16,6 @@ DEPENDENCIES = ["mitsubishi_cn105"] CONF_VERTICAL_VANE_DIRECTION = "vertical_vane_direction" -# The insertion order must match VALUES in mitsubishi_cn105_vane_select_vertical.cpp. -VERTICAL_VANE_DIRECTIONS = ["Auto", "1", "2", "3", "4", "5", "Swing"] - MitsubishiCN105VerticalVaneDirectionSelect = mitsubishi_ns.class_( "MitsubishiCN105VerticalVaneDirectionSelect", select.Select, @@ -42,6 +40,6 @@ async def to_code(config: ConfigType) -> None: await select.register_select( var, vertical_vane_direction, - options=VERTICAL_VANE_DIRECTIONS, + options=[direction.capitalize() for direction in VERTICAL_VANE_DIRECTIONS], ) await register_mitsubishi_cn105_device(var, config) diff --git a/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.cpp b/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.cpp index 0f9142fe5e..d703ddbb02 100644 --- a/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.cpp +++ b/esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.cpp @@ -4,7 +4,7 @@ namespace esphome::mitsubishi_cn105 { -// NOTE: This order must match VERTICAL_VANE_DIRECTIONS in select.py. +// NOTE: This order must match VERTICAL_VANE_DIRECTIONS in the hub's __init__.py. // MitsubishiCN105VerticalVaneDirectionSelect uses the preferred index-based // Select API, so Python option order and this array must stay aligned. static constexpr std::array VALUES{ diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index a119a38d24..f542880eef 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -8,6 +8,7 @@ #include #include "esphome/components/uart/uart_component.h" #include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.h" +#include "esphome/components/mitsubishi_cn105/automation.h" #include "esphome/components/mitsubishi_cn105/mitsubishi_cn105_component.h" #include "esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h" diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml index fcd1b048dd..fc14724786 100644 --- a/tests/components/mitsubishi_cn105/common.yaml +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -29,3 +29,11 @@ esphome: temperature: 22.0 - mitsubishi_cn105.clear_remote_temperature: id: ac + - mitsubishi_cn105.vane.control: + id: ac + vertical: + direction: SWING + - mitsubishi_cn105.vane.control: + id: ac + vertical: + direction: !lambda return esphome::mitsubishi_cn105::VERTICAL_VANE_MODE_SWING; diff --git a/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp b/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp index 48ea6b0c29..c957759223 100644 --- a/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp +++ b/tests/components/mitsubishi_cn105/mitsubishi_cn105_component_tests.cpp @@ -24,25 +24,50 @@ TEST(MitsubishiCN105ComponentTests, PublishesVaneStateForEveryValidSnapshot) { EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_POSITION_4}); } -TEST(MitsubishiCN105ComponentTests, DoesNotPublishUnknownVaneState) { +TEST(MitsubishiCN105ComponentTests, PublishesUnknownVaneState) { TestableMitsubishiCN105Component hub; size_t status_callback_count = 0; size_t vane_callback_count = 0; + std::optional callback_direction; hub.add_on_status_callback([&]() { status_callback_count++; }); - hub.add_on_vane_state_callback([&](const VaneState &) { vane_callback_count++; }); + hub.add_on_vane_state_callback([&](const VaneState &state) { + vane_callback_count++; + callback_direction = state.vertical.direction; + }); 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); + EXPECT_EQ(vane_callback_count, 1); + EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_UNKNOWN}); hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::POSITION_4; hub.publish_status(); EXPECT_EQ(status_callback_count, 2); - EXPECT_EQ(vane_callback_count, 1); + EXPECT_EQ(vane_callback_count, 2); + EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_POSITION_4}); +} + +TEST(MitsubishiCN105ComponentTests, VaneCallAppliesVerticalDirection) { + TestableMitsubishiCN105Component hub; + + auto call = hub.make_vane_call(); + call.vertical.set_direction(VERTICAL_VANE_MODE_POSITION_5); + call.perform(); + + EXPECT_EQ(hub.status().vane_mode, MitsubishiCN105::VaneMode::POSITION_5); +} + +TEST(MitsubishiCN105ComponentTests, VaneControlActionAppliesConfiguredFields) { + TestableMitsubishiCN105Component hub; + VaneControlAction<> action(&hub, [](VaneCall &call) { call.vertical.set_direction(VERTICAL_VANE_MODE_SWING); }); + + action.play(); + + EXPECT_EQ(hub.status().vane_mode, MitsubishiCN105::VaneMode::SWING); } } // namespace esphome::mitsubishi_cn105::testing