[mitsubishi_cn105] Add vertical vane control action (#16737)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Boris Krivonog
2026-08-11 18:39:05 -05:00
committed by GitHub
co-authored by J. Nick Koston
parent 3540012529
commit 9556c2bc4c
9 changed files with 190 additions and 17 deletions
@@ -2,9 +2,15 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import uart from esphome.components import uart
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL from esphome.const import (
from esphome.core import ID CONF_DIRECTION,
from esphome.cpp_generator import MockObj 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 from esphome.types import ConfigType, TemplateArgsType
CODEOWNERS = ["@crnjan"] CODEOWNERS = ["@crnjan"]
@@ -14,6 +20,7 @@ DOMAIN = "mitsubishi_cn105"
CONF_MITSUBISHI_CN105_ID = f"{DOMAIN}_id" CONF_MITSUBISHI_CN105_ID = f"{DOMAIN}_id"
CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval" CONF_TELEMETRY_REQUEST_MIN_INTERVAL = "telemetry_request_min_interval"
CONF_VANE = "vane" CONF_VANE = "vane"
CONF_VERTICAL = "vertical"
mitsubishi_ns = cg.esphome_ns.namespace(DOMAIN) mitsubishi_ns = cg.esphome_ns.namespace(DOMAIN)
@@ -24,6 +31,20 @@ MitsubishiCN105Component = mitsubishi_ns.class_(
) )
VaneState = mitsubishi_ns.struct("VaneState") 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 = mitsubishi_ns.class_(
"SetRemoteTemperatureAction", "SetRemoteTemperatureAction",
@@ -37,6 +58,11 @@ ClearRemoteTemperatureAction = mitsubishi_ns.class_(
cg.Parented.template(MitsubishiCN105Component), cg.Parented.template(MitsubishiCN105Component),
) )
VaneControlAction = mitsubishi_ns.class_(
"VaneControlAction",
automation.Action,
)
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
@@ -152,3 +178,70 @@ async def clear_temperature_action_to_code(
var = cg.new_Pvariable(action_id, template_arg) var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID]) await cg.register_parented(var, config[CONF_ID])
return var 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)
@@ -4,6 +4,8 @@
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include <type_traits>
namespace esphome::mitsubishi_cn105 { namespace esphome::mitsubishi_cn105 {
template<typename... Ts> template<typename... Ts>
@@ -20,4 +22,21 @@ class ClearRemoteTemperatureAction : public Action<Ts...>, public Parented<Mitsu
void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); } void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); }
}; };
template<typename... Ts> class VaneControlAction : public Action<Ts...> {
public:
using ApplyFn = void (*)(VaneCall &, const std::remove_cvref_t<Ts> &...);
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 } // namespace esphome::mitsubishi_cn105
@@ -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<MitsubishiCN105::VaneMode>(*direction));
}
this->parent_->publish_status();
}
} // namespace esphome::mitsubishi_cn105 } // namespace esphome::mitsubishi_cn105
@@ -6,6 +6,7 @@
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include <utility> #include <utility>
#include <optional>
namespace esphome::mitsubishi_cn105 { namespace esphome::mitsubishi_cn105 {
@@ -17,6 +18,7 @@ enum VerticalVaneMode : uint8_t {
VERTICAL_VANE_MODE_POSITION_4 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_4), VERTICAL_VANE_MODE_POSITION_4 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_4),
VERTICAL_VANE_MODE_POSITION_5 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_5), VERTICAL_VANE_MODE_POSITION_5 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_5),
VERTICAL_VANE_MODE_SWING = static_cast<uint8_t>(MitsubishiCN105::VaneMode::SWING), VERTICAL_VANE_MODE_SWING = static_cast<uint8_t>(MitsubishiCN105::VaneMode::SWING),
VERTICAL_VANE_MODE_UNKNOWN = static_cast<uint8_t>(MitsubishiCN105::VaneMode::UNKNOWN),
}; };
struct VaneState { struct VaneState {
@@ -27,6 +29,27 @@ struct VaneState {
Vertical vertical; Vertical vertical;
}; };
class MitsubishiCN105Component;
struct VaneCall {
struct Vertical {
void set_direction(VerticalVaneMode direction) { this->direction_ = direction; }
const std::optional<VerticalVaneMode> &get_direction() const { return this->direction_; }
protected:
std::optional<VerticalVaneMode> direction_;
};
explicit VaneCall(MitsubishiCN105Component *parent) : parent_(parent) {}
Vertical vertical;
void perform();
protected:
MitsubishiCN105Component *parent_;
};
class MitsubishiCN105Component : public Component, public uart::UARTDevice { class MitsubishiCN105Component : public Component, public uart::UARTDevice {
public: public:
explicit MitsubishiCN105Component() : hp_(*this) {} 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_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_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); } 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(); } const MitsubishiCN105::Status &status() const { return this->hp_.status(); }
bool is_status_initialized() const { return this->hp_.is_status_initialized(); } bool is_status_initialized() const { return this->hp_.is_status_initialized(); }
@@ -69,11 +93,9 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
protected: protected:
void notify_status_listeners_() { void notify_status_listeners_() {
this->status_callback_.call(); this->status_callback_.call();
if (this->status().vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) { this->vane_state_callback_.call(VaneState{
this->vane_state_callback_.call(VaneState{ .vertical = {.direction = static_cast<VerticalVaneMode>(this->status().vane_mode)},
.vertical = {.direction = static_cast<VerticalVaneMode>(this->status().vane_mode)}, });
});
}
} }
MitsubishiCN105 hp_; MitsubishiCN105 hp_;
@@ -6,6 +6,7 @@ from esphome.types import ConfigType
from .. import ( from .. import (
MITSUBISHI_CN105_DEVICE_SCHEMA, MITSUBISHI_CN105_DEVICE_SCHEMA,
VERTICAL_VANE_DIRECTIONS,
MitsubishiCN105Component, MitsubishiCN105Component,
mitsubishi_ns, mitsubishi_ns,
register_mitsubishi_cn105_device, register_mitsubishi_cn105_device,
@@ -15,9 +16,6 @@ DEPENDENCIES = ["mitsubishi_cn105"]
CONF_VERTICAL_VANE_DIRECTION = "vertical_vane_direction" 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 = mitsubishi_ns.class_(
"MitsubishiCN105VerticalVaneDirectionSelect", "MitsubishiCN105VerticalVaneDirectionSelect",
select.Select, select.Select,
@@ -42,6 +40,6 @@ async def to_code(config: ConfigType) -> None:
await select.register_select( await select.register_select(
var, var,
vertical_vane_direction, vertical_vane_direction,
options=VERTICAL_VANE_DIRECTIONS, options=[direction.capitalize() for direction in VERTICAL_VANE_DIRECTIONS],
) )
await register_mitsubishi_cn105_device(var, config) await register_mitsubishi_cn105_device(var, config)
@@ -4,7 +4,7 @@
namespace esphome::mitsubishi_cn105 { 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 // MitsubishiCN105VerticalVaneDirectionSelect uses the preferred index-based
// Select API, so Python option order and this array must stay aligned. // Select API, so Python option order and this array must stay aligned.
static constexpr std::array VALUES{ static constexpr std::array VALUES{
@@ -8,6 +8,7 @@
#include <vector> #include <vector>
#include "esphome/components/uart/uart_component.h" #include "esphome/components/uart/uart_component.h"
#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.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_component.h"
#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h" #include "esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h"
@@ -29,3 +29,11 @@ esphome:
temperature: 22.0 temperature: 22.0
- mitsubishi_cn105.clear_remote_temperature: - mitsubishi_cn105.clear_remote_temperature:
id: ac 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;
@@ -24,25 +24,50 @@ TEST(MitsubishiCN105ComponentTests, PublishesVaneStateForEveryValidSnapshot) {
EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_POSITION_4}); EXPECT_EQ(callback_direction, std::optional{VERTICAL_VANE_MODE_POSITION_4});
} }
TEST(MitsubishiCN105ComponentTests, DoesNotPublishUnknownVaneState) { TEST(MitsubishiCN105ComponentTests, PublishesUnknownVaneState) {
TestableMitsubishiCN105Component hub; TestableMitsubishiCN105Component hub;
size_t status_callback_count = 0; size_t status_callback_count = 0;
size_t vane_callback_count = 0; size_t vane_callback_count = 0;
std::optional<VerticalVaneMode> callback_direction;
hub.add_on_status_callback([&]() { status_callback_count++; }); 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().room_temperature = 20.0f;
hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::UNKNOWN; hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::UNKNOWN;
hub.publish_status(); hub.publish_status();
EXPECT_EQ(status_callback_count, 1); 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.mutable_status().vane_mode = MitsubishiCN105::VaneMode::POSITION_4;
hub.publish_status(); hub.publish_status();
EXPECT_EQ(status_callback_count, 2); 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 } // namespace esphome::mitsubishi_cn105::testing