1
0
mirror of https://github.com/esphome/esphome.git synced 2025-01-18 12:05:41 +00:00

[climate/template]: New template climate component

This commit is contained in:
polyfloyd 2025-01-01 18:44:04 +01:00
parent 9e40d4cf45
commit 43188a4d53
4 changed files with 456 additions and 0 deletions

View File

@ -0,0 +1,126 @@
import esphome.codegen as cg
from esphome.components import climate
from esphome.components.climate import (
CLIMATE_FAN_MODES,
CLIMATE_MODES,
CLIMATE_PRESETS,
CLIMATE_SWING_MODES,
)
from esphome.components.number import Number
from esphome.components.select import Select
from esphome.components.sensor import Sensor
from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ACTION_ID,
CONF_ID,
CONF_SUPPORTED_FAN_MODES,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_PRESETS,
CONF_SUPPORTED_SWING_MODES,
)
from .. import template_ns
DEPENDENCIES = ["climate"]
CONF_AUTO_ACTION = "auto_action"
CONF_CURRENT_TEMPERATURE_ID = "current_temperature_id"
CONF_TARGET_TEMPERATURE_ID = "target_temperature_id"
CONF_MODE_ID = "mode_id"
CONF_FAN_MODE_ID = "fan_mode_id"
CONF_SWING_MODE_ID = "swing_mode_id"
CONF_PRESET_ID = "preset_id"
TemplateClimate = template_ns.class_("TemplateClimate", climate.Climate, cg.Component)
CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(TemplateClimate),
cv.Required(CONF_CURRENT_TEMPERATURE_ID): cv.use_id(Sensor),
cv.Required(CONF_TARGET_TEMPERATURE_ID): cv.use_id(Number),
cv.Required(CONF_MODE_ID): cv.use_id(Select),
cv.Optional(CONF_FAN_MODE_ID): cv.use_id(Select),
cv.Optional(CONF_SWING_MODE_ID): cv.use_id(Select),
cv.Optional(CONF_PRESET_ID): cv.use_id(Select),
cv.Required(CONF_SUPPORTED_MODES): cv.ensure_list(cv.enum(CLIMATE_MODES)),
cv.Optional(CONF_SUPPORTED_FAN_MODES): cv.ensure_list(
cv.enum(CLIMATE_FAN_MODES)
),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
cv.enum(CLIMATE_SWING_MODES)
),
cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(cv.enum(CLIMATE_PRESETS)),
cv.Optional(CONF_ACTION_ID): cv.use_id(TextSensor),
}
)
def FINAL_VALIDATE_SCHEMA(config):
if bool(CONF_FAN_MODE_ID in config) != bool(CONF_SUPPORTED_FAN_MODES in config):
raise cv.Invalid(
f"{CONF_FAN_MODE_ID} requires {CONF_SUPPORTED_FAN_MODES} and vice versa"
)
if bool(CONF_SWING_MODE_ID in config) != bool(CONF_SUPPORTED_SWING_MODES in config):
raise cv.Invalid(
f"{CONF_SWING_MODE_ID} requires {CONF_SUPPORTED_SWING_MODES} and vice versa"
)
if bool(CONF_PRESET_ID in config) != bool(CONF_SUPPORTED_PRESETS in config):
raise cv.Invalid(
f"{CONF_PRESET_ID} requires {CONF_SUPPORTED_PRESETS} and vice versa"
)
# TODO: It would be nice to be able to get the set of options for the mode selectors if defined,
# that way we can validate those as well. But how can they be accessed?
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await climate.register_climate(var, config)
cg.add(
var.set_current_temperature(
await cg.get_variable(config[CONF_CURRENT_TEMPERATURE_ID])
)
)
cg.add(
var.set_target_temperature(
await cg.get_variable(config[CONF_TARGET_TEMPERATURE_ID])
)
)
cg.add(var.set_mode(await cg.get_variable(config[CONF_MODE_ID])))
cg.add(
var.traits_.set_supported_modes(
[CLIMATE_MODES[m] for m in config[CONF_SUPPORTED_MODES]]
)
)
if v := config.get(CONF_FAN_MODE_ID):
cg.add(var.set_fan_mode(await cg.get_variable(v)))
cg.add(
var.traits_.set_supported_fan_modes(
[CLIMATE_FAN_MODES[m] for m in config[CONF_SUPPORTED_FAN_MODES]]
)
)
if v := config.get(CONF_SWING_MODE_ID):
cg.add(var.set_swing_mode(await cg.get_variable(v)))
cg.add(
var.traits_.set_supported_swing_modes(
[CLIMATE_SWING_MODES[m] for m in config[CONF_SUPPORTED_SWING_MODES]]
)
)
if v := config.get(CONF_PRESET_ID):
cg.add(var.set_preset(await cg.get_variable(v)))
cg.add(
var.traits_.set_supported_presets(
[CLIMATE_PRESETS[m] for m in config[CONF_SUPPORTED_PRESETS]]
)
)
if v := config.get(CONF_ACTION_ID):
cg.add(var.set_action(await cg.get_variable(v)))

View File

@ -0,0 +1,197 @@
#include "template_climate.h"
namespace esphome {
namespace template_ {
static const char *const TAG = "template_climate";
ClimateMode mode_from_string(const std::string &s) {
if (str_equals_case_insensitive(s, "OFF")) {
return CLIMATE_MODE_OFF;
} else if (str_equals_case_insensitive(s, "HEAT_COOL")) {
return CLIMATE_MODE_HEAT_COOL;
} else if (str_equals_case_insensitive(s, "COOL")) {
return CLIMATE_MODE_COOL;
} else if (str_equals_case_insensitive(s, "HEAT")) {
return CLIMATE_MODE_HEAT;
} else if (str_equals_case_insensitive(s, "FAN_ONLY")) {
return CLIMATE_MODE_FAN_ONLY;
} else if (str_equals_case_insensitive(s, "DRY")) {
return CLIMATE_MODE_DRY;
} else if (str_equals_case_insensitive(s, "AUTO")) {
return CLIMATE_MODE_AUTO;
} else {
ESP_LOGW(TAG, "Unrecognized mode %s", s.c_str());
return CLIMATE_MODE_OFF;
}
}
ClimateFanMode fan_mode_from_string(const std::string &s) {
if (str_equals_case_insensitive(s, "ON")) {
return CLIMATE_FAN_ON;
} else if (str_equals_case_insensitive(s, "OFF")) {
return CLIMATE_FAN_OFF;
} else if (str_equals_case_insensitive(s, "AUTO")) {
return CLIMATE_FAN_AUTO;
} else if (str_equals_case_insensitive(s, "LOW")) {
return CLIMATE_FAN_LOW;
} else if (str_equals_case_insensitive(s, "MEDIUM")) {
return CLIMATE_FAN_MEDIUM;
} else if (str_equals_case_insensitive(s, "HIGH")) {
return CLIMATE_FAN_HIGH;
} else if (str_equals_case_insensitive(s, "MIDDLE")) {
return CLIMATE_FAN_MIDDLE;
} else if (str_equals_case_insensitive(s, "FOCUS")) {
return CLIMATE_FAN_FOCUS;
} else if (str_equals_case_insensitive(s, "DIFFUSE")) {
return CLIMATE_FAN_DIFFUSE;
} else if (str_equals_case_insensitive(s, "QUIET")) {
return CLIMATE_FAN_QUIET;
} else {
ESP_LOGW(TAG, "Unrecognized fan mode %s", s.c_str());
return CLIMATE_FAN_AUTO;
}
}
ClimateSwingMode swing_mode_from_string(const std::string &s) {
if (str_equals_case_insensitive(s, "OFF")) {
return CLIMATE_SWING_OFF;
} else if (str_equals_case_insensitive(s, "BOTH")) {
return CLIMATE_SWING_BOTH;
} else if (str_equals_case_insensitive(s, "VERTICAL")) {
return CLIMATE_SWING_VERTICAL;
} else if (str_equals_case_insensitive(s, "HORIZONTAL")) {
return CLIMATE_SWING_HORIZONTAL;
} else {
ESP_LOGW(TAG, "Unrecognized swing mode %s", s.c_str());
return CLIMATE_SWING_BOTH;
}
}
ClimatePreset preset_from_string(const std::string &s) {
if (str_equals_case_insensitive(s, "NONE")) {
return CLIMATE_PRESET_NONE;
} else if (str_equals_case_insensitive(s, "HOME")) {
return CLIMATE_PRESET_HOME;
} else if (str_equals_case_insensitive(s, "AWAY")) {
return CLIMATE_PRESET_AWAY;
} else if (str_equals_case_insensitive(s, "BOOST")) {
return CLIMATE_PRESET_BOOST;
} else if (str_equals_case_insensitive(s, "COMFORT")) {
return CLIMATE_PRESET_COMFORT;
} else if (str_equals_case_insensitive(s, "ECO")) {
return CLIMATE_PRESET_ECO;
} else if (str_equals_case_insensitive(s, "SLEEP")) {
return CLIMATE_PRESET_SLEEP;
} else if (str_equals_case_insensitive(s, "ACTIVITY")) {
return CLIMATE_PRESET_ACTIVITY;
} else {
ESP_LOGW(TAG, "Unrecognized preset %s", s.c_str());
return CLIMATE_PRESET_NONE;
}
}
ClimateAction action_from_string(const std::string &s) {
if (str_equals_case_insensitive(s, "OFF")) {
return CLIMATE_ACTION_OFF;
} else if (str_equals_case_insensitive(s, "COOLING")) {
return CLIMATE_ACTION_COOLING;
} else if (str_equals_case_insensitive(s, "HEATING")) {
return CLIMATE_ACTION_HEATING;
} else if (str_equals_case_insensitive(s, "IDLE")) {
return CLIMATE_ACTION_IDLE;
} else if (str_equals_case_insensitive(s, "DRYING")) {
return CLIMATE_ACTION_DRYING;
} else if (str_equals_case_insensitive(s, "FAN")) {
return CLIMATE_ACTION_FAN;
} else {
ESP_LOGW(TAG, "Unrecognized action %s", s.c_str());
return CLIMATE_ACTION_OFF;
}
}
void TemplateClimate::setup() {
this->current_temperature_->add_on_state_callback([this](float x) {
this->current_temperature = x;
this->publish_state();
});
this->current_temperature = this->current_temperature_->state;
this->target_temperature_->add_on_state_callback([this](float x) {
this->target_temperature = x;
this->publish_state();
});
this->current_temperature = this->target_temperature_->state;
this->mode_->add_on_state_callback([this](const std::string &x, size_t i) {
this->mode = mode_from_string(x);
this->publish_state();
});
this->mode = mode_from_string(this->mode_->state);
if (this->fan_mode_ != nullptr) {
this->fan_mode_->add_on_state_callback([this](const std::string &x, size_t i) {
this->fan_mode = fan_mode_from_string(x);
this->publish_state();
});
this->fan_mode = fan_mode_from_string(this->fan_mode_->state);
}
if (this->swing_mode_ != nullptr) {
this->swing_mode_->add_on_state_callback([this](const std::string &x, size_t i) {
this->swing_mode = swing_mode_from_string(x);
this->publish_state();
});
this->swing_mode = swing_mode_from_string(this->swing_mode_->state);
}
if (this->preset_ != nullptr) {
this->preset_->add_on_state_callback([this](const std::string &x, size_t i) {
this->preset = preset_from_string(x);
this->publish_state();
});
this->preset = preset_from_string(this->preset_->state);
}
if (this->action_ != nullptr) {
this->action_->add_on_state_callback([this](const std::string &x) {
this->action = action_from_string(x);
this->publish_state();
});
this->action = action_from_string(this->action_->state);
}
}
void TemplateClimate::control(const ClimateCall &call) {
auto mode = call.get_mode();
if (mode) {
std::string opt = LOG_STR_ARG(climate_mode_to_string(mode.value()));
this->mode_->make_call().set_option(opt).perform();
}
auto fan_mode = call.get_fan_mode();
if (fan_mode && this->fan_mode_ != nullptr) {
std::string opt = LOG_STR_ARG(climate_fan_mode_to_string(fan_mode.value()));
this->fan_mode_->make_call().set_option(opt).perform();
}
auto swing_mode = call.get_swing_mode();
if (swing_mode && this->swing_mode_ != nullptr) {
std::string opt = LOG_STR_ARG(climate_swing_mode_to_string(swing_mode.value()));
this->swing_mode_->make_call().set_option(opt).perform();
}
auto preset = call.get_preset();
if (preset && this->preset_ != nullptr) {
std::string opt = LOG_STR_ARG(climate_preset_to_string(preset.value()));
this->preset_->make_call().set_option(opt).perform();
}
auto target_temperature = call.get_target_temperature();
if (target_temperature) {
this->target_temperature_->make_call().set_value(target_temperature.value()).perform();
}
}
} // namespace template_
} // namespace esphome

View File

@ -0,0 +1,62 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/number/number.h"
#include "esphome/components/select/select.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome {
namespace template_ {
using namespace climate;
using namespace number;
using namespace select;
using namespace sensor;
using namespace text_sensor;
class TemplateClimate : public Climate, public Component {
public:
ClimateTraits traits_;
void setup() override;
ClimateTraits traits() override { return this->traits_; }
void set_current_temperature(Sensor *sensor) {
this->current_temperature_ = sensor;
this->traits_.set_supports_current_temperature(true);
}
void set_target_temperature(Number *num) { this->target_temperature_ = num; }
void set_mode(Select *s) { this->mode_ = s; }
void set_fan_mode(Select *s) { this->fan_mode_ = s; }
void set_swing_mode(Select *s) { this->swing_mode_ = s; }
void set_preset(Select *s) { this->preset_ = s; }
void set_action(TextSensor *s) {
this->action_ = s;
this->traits_.set_supports_action(true);
}
protected:
void control(const ClimateCall &call) override;
Sensor *current_temperature_{nullptr};
Number *target_temperature_{nullptr};
Select *mode_{nullptr};
Select *fan_mode_{nullptr};
Select *swing_mode_{nullptr};
Select *preset_{nullptr};
TextSensor *action_{nullptr};
};
} // namespace template_
} // namespace esphome

View File

@ -29,6 +29,11 @@ sensor:
- timeout:
timeout: 1d
- platform: template
id: climate_current_temperature
lambda: |-
return 42;
esphome:
on_boot:
- sensor.template.publish:
@ -125,6 +130,13 @@ number:
max_value: 100
step: 1
- platform: template
id: climate_target_temperature
optimistic: true
min_value: 10
max_value: 30
step: 0.1
select:
- platform: template
name: "Template select"
@ -135,6 +147,34 @@ select:
- three
initial_option: two
- platform: template
id: climate_mode
optimistic: true
options:
- "OFF"
- "HEAT"
- platform: template
id: climate_fan_mode
optimistic: true
options:
- "ON"
- "AUTO"
- platform: template
id: climate_swing_mode
optimistic: true
options:
- "OFF"
- "HORIZONTAL"
- platform: template
id: climate_preset
optimistic: true
options:
- "NONE"
- "ECO"
lock:
- platform: template
name: "Template Lock"
@ -187,6 +227,10 @@ text:
format: Template Text set to %s
args: ["x.c_str()"]
text_sensor:
- platform: template
id: climate_action
alarm_control_panel:
- platform: template
name: Alarm Panel
@ -240,6 +284,33 @@ datetime:
- x.minute
- x.second
climate:
- platform: template
name: Nice Climate Template Component
target_temperature_id: climate_target_temperature
current_temperature_id: climate_current_temperature
mode_id: climate_mode
fan_mode_id: climate_fan_mode
swing_mode_id: climate_swing_mode
preset_id: climate_preset
action_id: climate_action
supported_modes:
- "OFF"
- "COOL"
- "HEAT"
- "AUTO"
supported_fan_modes:
- "ON"
- "QUIET"
supported_swing_modes:
- "OFF"
- "HORIZONTAL"
supported_presets:
- "NONE"
- "ECO"
visual:
temperature_step: 0.1C
time:
- platform: sntp # Required for datetime