mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[mitsubishi_cn105] Add Fahrenheit support (#15488)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
co-authored by
J. Nick Koston
parent
a30e82459f
commit
65704e881f
@@ -8,6 +8,7 @@ from esphome.const import (
|
|||||||
CONF_ON_STATE,
|
CONF_ON_STATE,
|
||||||
CONF_TEMPERATURE,
|
CONF_TEMPERATURE,
|
||||||
CONF_UPDATE_INTERVAL,
|
CONF_UPDATE_INTERVAL,
|
||||||
|
CONF_USE_FAHRENHEIT,
|
||||||
)
|
)
|
||||||
from esphome.core import ID, Lambda
|
from esphome.core import ID, Lambda
|
||||||
from esphome.cpp_generator import LambdaExpression, MockObj
|
from esphome.cpp_generator import LambdaExpression, MockObj
|
||||||
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = (
|
|||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s"
|
CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s"
|
||||||
): cv.update_interval,
|
): cv.update_interval,
|
||||||
|
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_VANE): cv.Schema(
|
cv.Optional(CONF_VANE): cv.Schema(
|
||||||
{
|
{
|
||||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||||
@@ -114,6 +116,7 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL]
|
config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
cg.add(var.set_use_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||||
if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE):
|
if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE):
|
||||||
cg.add_global(mitsubishi_ns.using)
|
cg.add_global(mitsubishi_ns.using)
|
||||||
for conf in on_state:
|
for conf in on_state:
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ class MitsubishiCN105 {
|
|||||||
return this->is_telemetry_polling_enabled() ? !std::isnan(this->status_.room_temperature)
|
return this->is_telemetry_polling_enabled() ? !std::isnan(this->status_.room_temperature)
|
||||||
: !std::isnan(this->status_.target_temperature);
|
: !std::isnan(this->status_.target_temperature);
|
||||||
}
|
}
|
||||||
|
bool is_temperature_encoding_b() const { return this->property_context_.use_temperature_encoding_b; }
|
||||||
|
|
||||||
void set_power(bool power_on);
|
void set_power(bool power_on);
|
||||||
void set_target_temperature(float target_temperature);
|
void set_target_temperature(float target_temperature);
|
||||||
|
|||||||
@@ -50,7 +50,11 @@ static constexpr std::optional<Left> reverse_map_lookup(const std::array<std::pa
|
|||||||
return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt;
|
return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
void MitsubishiCN105Climate::dump_config() { LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); }
|
void MitsubishiCN105Climate::dump_config() {
|
||||||
|
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
||||||
|
ESP_LOGCONFIG(TAG, " Temperature unit: °%c",
|
||||||
|
this->parent_->get_temperature_mapping().get_use_fahrenheit() ? 'F' : 'C');
|
||||||
|
}
|
||||||
|
|
||||||
void MitsubishiCN105Climate::setup() {
|
void MitsubishiCN105Climate::setup() {
|
||||||
this->parent_->add_on_status_callback([this]() { this->apply_values_(); });
|
this->parent_->add_on_status_callback([this]() { this->apply_values_(); });
|
||||||
@@ -72,13 +76,15 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
|||||||
|
|
||||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||||
|
|
||||||
traits.set_visual_min_temperature(16.0f);
|
const bool use_fahrenheit = this->parent_->get_temperature_mapping().get_use_fahrenheit();
|
||||||
traits.set_visual_max_temperature(31.0f);
|
traits.set_temperature_unit(use_fahrenheit ? TemperatureUnit::FAHRENHEIT : TemperatureUnit::CELSIUS);
|
||||||
|
traits.set_visual_min_temperature(use_fahrenheit ? 61.0f : 16.0f);
|
||||||
|
traits.set_visual_max_temperature(use_fahrenheit ? 88.0f : 31.0f);
|
||||||
traits.set_visual_temperature_step(1.0f);
|
traits.set_visual_temperature_step(1.0f);
|
||||||
|
|
||||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
traits.set_visual_current_temperature_step(0.5f);
|
traits.set_visual_current_temperature_step(use_fahrenheit ? 1.0f : 0.5f);
|
||||||
}
|
}
|
||||||
|
|
||||||
return traits;
|
return traits;
|
||||||
@@ -86,7 +92,7 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
|||||||
|
|
||||||
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||||
if (const auto target_temperature = call.get_target_temperature()) {
|
if (const auto target_temperature = call.get_target_temperature()) {
|
||||||
this->parent_->set_target_temperature(*target_temperature);
|
this->parent_->set_target_temperature(this->parent_->get_temperature_mapping().to_mitsubishi(*target_temperature));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const auto mode = call.get_mode()) {
|
if (const auto mode = call.get_mode()) {
|
||||||
@@ -139,10 +145,10 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
|||||||
void MitsubishiCN105Climate::apply_values_() {
|
void MitsubishiCN105Climate::apply_values_() {
|
||||||
const auto &status = this->parent_->status();
|
const auto &status = this->parent_->status();
|
||||||
|
|
||||||
this->target_temperature = status.target_temperature;
|
this->target_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.target_temperature);
|
||||||
|
|
||||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||||
this->current_temperature = status.room_temperature;
|
this->current_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.room_temperature);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status.power_on) {
|
if (status.power_on) {
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ void MitsubishiCN105Component::setup() { this->hp_.initialize(); }
|
|||||||
|
|
||||||
void MitsubishiCN105Component::loop() {
|
void MitsubishiCN105Component::loop() {
|
||||||
if (this->hp_.update()) {
|
if (this->hp_.update()) {
|
||||||
|
// Encoding A only supports whole °C values and cannot represent native °F setpoints accurately.
|
||||||
|
// See https://github.com/esphome/esphome/pull/15488#issuecomment-5268304343
|
||||||
|
if (this->temperature_mapping_.get_use_fahrenheit() && !this->hp_.is_temperature_encoding_b()) {
|
||||||
|
ESP_LOGE(TAG, "Unit reports encoding A, which cannot accurately convert °F setpoints; disable 'use_fahrenheit'");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this->notify_status_listeners_();
|
this->notify_status_listeners_();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,43 @@
|
|||||||
#include "mitsubishi_cn105.h"
|
#include "mitsubishi_cn105.h"
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
|
|
||||||
#include <utility>
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace esphome::mitsubishi_cn105 {
|
namespace esphome::mitsubishi_cn105 {
|
||||||
|
|
||||||
|
struct TemperatureMapping {
|
||||||
|
float to_mitsubishi(float value) const {
|
||||||
|
if (!this->use_fahrenheit_) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
const int fahrenheit = std::clamp(static_cast<int>(std::round(value)), 61, 88);
|
||||||
|
return 0.5f * (fahrenheit - 28 + (fahrenheit > 68) - (fahrenheit < 68));
|
||||||
|
}
|
||||||
|
|
||||||
|
float from_mitsubishi(float value) const {
|
||||||
|
if (!this->use_fahrenheit_) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value < 16.0f || value > 30.5f) {
|
||||||
|
return celsius_to_fahrenheit(value);
|
||||||
|
}
|
||||||
|
const int mitsubishi_half_degrees = static_cast<int>(std::round(value * 2.0f));
|
||||||
|
return mitsubishi_half_degrees + 29 - (mitsubishi_half_degrees >= 40) - (mitsubishi_half_degrees > 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get_use_fahrenheit() const { return this->use_fahrenheit_; }
|
||||||
|
void set_use_fahrenheit(bool value) { this->use_fahrenheit_ = value; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool use_fahrenheit_{false};
|
||||||
|
};
|
||||||
|
|
||||||
enum VerticalVaneMode : uint8_t {
|
enum VerticalVaneMode : uint8_t {
|
||||||
VERTICAL_VANE_MODE_AUTO = static_cast<uint8_t>(MitsubishiCN105::VaneMode::AUTO),
|
VERTICAL_VANE_MODE_AUTO = static_cast<uint8_t>(MitsubishiCN105::VaneMode::AUTO),
|
||||||
VERTICAL_VANE_MODE_POSITION_1 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_1),
|
VERTICAL_VANE_MODE_POSITION_1 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_1),
|
||||||
@@ -60,6 +90,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
|||||||
|
|
||||||
void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); }
|
void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); }
|
||||||
void set_telemetry_request_min_interval(uint32_t ms) { this->hp_.set_telemetry_request_min_interval(ms); }
|
void set_telemetry_request_min_interval(uint32_t ms) { this->hp_.set_telemetry_request_min_interval(ms); }
|
||||||
|
void set_use_fahrenheit(bool value) { this->temperature_mapping_.set_use_fahrenheit(value); }
|
||||||
|
|
||||||
void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); }
|
void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); }
|
||||||
void clear_remote_temperature() { this->hp_.clear_remote_temperature(); }
|
void clear_remote_temperature() { this->hp_.clear_remote_temperature(); }
|
||||||
@@ -75,6 +106,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
|||||||
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(); }
|
||||||
bool is_telemetry_polling_enabled() const { return this->hp_.is_telemetry_polling_enabled(); }
|
bool is_telemetry_polling_enabled() const { return this->hp_.is_telemetry_polling_enabled(); }
|
||||||
|
const TemperatureMapping &get_temperature_mapping() const { return this->temperature_mapping_; }
|
||||||
|
|
||||||
template<typename F> void add_on_status_callback(F &&callback) {
|
template<typename F> void add_on_status_callback(F &&callback) {
|
||||||
this->status_callback_.add(std::forward<F>(callback));
|
this->status_callback_.add(std::forward<F>(callback));
|
||||||
@@ -99,6 +131,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MitsubishiCN105 hp_;
|
MitsubishiCN105 hp_;
|
||||||
|
TemperatureMapping temperature_mapping_;
|
||||||
CallbackManager<void()> status_callback_;
|
CallbackManager<void()> status_callback_;
|
||||||
LazyCallbackManager<void(const VaneState &)> vane_state_callback_;
|
LazyCallbackManager<void(const VaneState &)> vane_state_callback_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -118,8 +118,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
|||||||
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f;
|
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f;
|
||||||
// current_temp_step
|
// current_temp_step
|
||||||
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
|
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
|
||||||
// temperature units are always coerced to Celsius internally
|
root[MQTT_TEMPERATURE_UNIT] = traits.get_temperature_unit() == TemperatureUnit::FAHRENHEIT ? "F" : "C";
|
||||||
root[MQTT_TEMPERATURE_UNIT] = "C";
|
|
||||||
|
|
||||||
// min_humidity
|
// min_humidity
|
||||||
root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
|
root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
|
||||||
|
|||||||
@@ -1,7 +1,67 @@
|
|||||||
|
#include <array>
|
||||||
|
#include <utility>
|
||||||
#include "../common.h"
|
#include "../common.h"
|
||||||
|
|
||||||
namespace esphome::mitsubishi_cn105::testing {
|
namespace esphome::mitsubishi_cn105::testing {
|
||||||
|
|
||||||
|
TEST(MitsubishiCN105ClimateTests, CelsiusTemperatureMappingAndTraitsMatchExpectedValues) {
|
||||||
|
TestableMitsubishiCN105Climate sut;
|
||||||
|
const auto mapping = TemperatureMapping();
|
||||||
|
|
||||||
|
for (int temperature = 16; temperature <= 31; ++temperature) {
|
||||||
|
EXPECT_EQ(mapping.to_mitsubishi(temperature), temperature);
|
||||||
|
EXPECT_EQ(mapping.from_mitsubishi(temperature), temperature);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto traits = sut.traits();
|
||||||
|
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::CELSIUS);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 16.0f);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 31.0f);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingAndTraitsMatchExpectedValues) {
|
||||||
|
TestableMitsubishiCN105Climate sut;
|
||||||
|
auto mapping = TemperatureMapping();
|
||||||
|
mapping.set_use_fahrenheit(true);
|
||||||
|
sut.set_use_fahrenheit(true);
|
||||||
|
|
||||||
|
const std::array cases{
|
||||||
|
std::pair{61, 16.0f}, std::pair{62, 16.5f}, std::pair{63, 17.0f}, std::pair{64, 17.5f}, std::pair{65, 18.0f},
|
||||||
|
std::pair{66, 18.5f}, std::pair{67, 19.0f}, std::pair{68, 20.0f}, std::pair{69, 21.0f}, std::pair{70, 21.5f},
|
||||||
|
std::pair{71, 22.0f}, std::pair{72, 22.5f}, std::pair{73, 23.0f}, std::pair{74, 23.5f}, std::pair{75, 24.0f},
|
||||||
|
std::pair{76, 24.5f}, std::pair{77, 25.0f}, std::pair{78, 25.5f}, std::pair{79, 26.0f}, std::pair{80, 26.5f},
|
||||||
|
std::pair{81, 27.0f}, std::pair{82, 27.5f}, std::pair{83, 28.0f}, std::pair{84, 28.5f}, std::pair{85, 29.0f},
|
||||||
|
std::pair{86, 29.5f}, std::pair{87, 30.0f}, std::pair{88, 30.5f},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &[fahrenheit, mitsubishi_celsius] : cases) {
|
||||||
|
EXPECT_FLOAT_EQ(mapping.to_mitsubishi(fahrenheit), mitsubishi_celsius);
|
||||||
|
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(mitsubishi_celsius), fahrenheit);
|
||||||
|
}
|
||||||
|
const auto traits = sut.traits();
|
||||||
|
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 61.0f);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 88.0f);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 1.0f);
|
||||||
|
EXPECT_FLOAT_EQ(traits.get_visual_current_temperature_step(), 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(MitsubishiCN105ClimateTests, FahrenheitTemperatureMappingUsesLinearConversionOutsideSetpointRange) {
|
||||||
|
auto mapping = TemperatureMapping();
|
||||||
|
mapping.set_use_fahrenheit(true);
|
||||||
|
|
||||||
|
const std::array cases{
|
||||||
|
std::pair{0.0f, 32.0f}, std::pair{10.0f, 50.0f}, std::pair{15.5f, 59.9f},
|
||||||
|
std::pair{31.0f, 87.8f}, std::pair{35.0f, 95.0f}, std::pair{40.0f, 104.0f},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto &[celsius, fahrenheit] : cases) {
|
||||||
|
EXPECT_FLOAT_EQ(mapping.from_mitsubishi(celsius), fahrenheit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST(MitsubishiCN105ClimateTests, SupportedSwingModeOffLeavesTraitsEmpty) {
|
TEST(MitsubishiCN105ClimateTests, SupportedSwingModeOffLeavesTraitsEmpty) {
|
||||||
TestableMitsubishiCN105Climate sut;
|
TestableMitsubishiCN105Climate sut;
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class TestableMitsubishiCN105Climate : public MitsubishiCN105Climate {
|
|||||||
using MitsubishiCN105Climate::last_non_swing_wide_vane_mode_;
|
using MitsubishiCN105Climate::last_non_swing_wide_vane_mode_;
|
||||||
|
|
||||||
MitsubishiCN105::Status &status() { return const_cast<MitsubishiCN105::Status &>(this->component_.status()); }
|
MitsubishiCN105::Status &status() { return const_cast<MitsubishiCN105::Status &>(this->component_.status()); }
|
||||||
|
void set_use_fahrenheit(bool value) { this->component_.set_use_fahrenheit(value); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
MitsubishiCN105Component component_;
|
MitsubishiCN105Component component_;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ mitsubishi_cn105:
|
|||||||
uart_id: uart_bus
|
uart_id: uart_bus
|
||||||
update_interval: 30s
|
update_interval: 30s
|
||||||
telemetry_request_min_interval: 120s
|
telemetry_request_min_interval: 120s
|
||||||
|
use_fahrenheit: true
|
||||||
vane:
|
vane:
|
||||||
on_state:
|
on_state:
|
||||||
- logger.log:
|
- logger.log:
|
||||||
|
|||||||
Reference in New Issue
Block a user