mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[mitsubishi_cn105] Add vertical vane control action (#16737)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
parent
3540012529
commit
9556c2bc4c
@@ -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)
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
#include <type_traits>
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
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(); }
|
||||
};
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <utility>
|
||||
#include <optional>
|
||||
|
||||
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_5 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_5),
|
||||
VERTICAL_VANE_MODE_SWING = static_cast<uint8_t>(MitsubishiCN105::VaneMode::SWING),
|
||||
VERTICAL_VANE_MODE_UNKNOWN = static_cast<uint8_t>(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<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 {
|
||||
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<VerticalVaneMode>(this->status().vane_mode)},
|
||||
});
|
||||
}
|
||||
this->vane_state_callback_.call(VaneState{
|
||||
.vertical = {.direction = static_cast<VerticalVaneMode>(this->status().vane_mode)},
|
||||
});
|
||||
}
|
||||
|
||||
MitsubishiCN105 hp_;
|
||||
|
||||
@@ -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)
|
||||
|
||||
+1
-1
@@ -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{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
#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"
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<VerticalVaneMode> 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
|
||||
|
||||
Reference in New Issue
Block a user