mirror of
https://github.com/Threnklyn/esphome-dev.git
synced 2026-06-04 03:48:29 +02:00
🏗 Merge C++ into python codebase (#504)
## Description: Move esphome-core codebase into esphome (and a bunch of other refactors). See https://github.com/esphome/feature-requests/issues/97 Yes this is a shit ton of work and no there's no way to automate it :( But it will be worth it 👍 Progress: - Core support (file copy etc): 80% - Base Abstractions (light, switch): ~50% - Integrations: ~10% - Working? Yes, (but only with ported components). Other refactors: - Moves all codegen related stuff into a single class: `esphome.codegen` (imported as `cg`) - Rework coroutine syntax - Move from `component/platform.py` to `domain/component.py` structure as with HA - Move all defaults out of C++ and into config validation. - Remove `make_...` helpers from Application class. Reason: Merge conflicts with every single new integration. - Pointer Variables are stored globally instead of locally in setup(). Reason: stack size limit. Future work: - Rework const.py - Move all `CONF_...` into a conf class (usage `conf.UPDATE_INTERVAL` vs `CONF_UPDATE_INTERVAL`). Reason: Less convoluted import block - Enable loading from `custom_components` folder. **Related issue (if applicable):** https://github.com/esphome/feature-requests/issues/97 **Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):** esphome/esphome-docs#<esphome-docs PR number goes here> ## Checklist: - [ ] The code change is tested and works locally. - [ ] Tests have been added to verify that the new code works (under `tests/` folder). If user exposed functionality or configuration variables are added/changed: - [ ] Documentation added/updated in [esphomedocs](https://github.com/OttoWinter/esphomedocs).
This commit is contained in:
@@ -1,26 +1,19 @@
|
||||
import voluptuous as vol
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.automation import ACTION_REGISTRY
|
||||
from esphome.components import mqtt
|
||||
from esphome.components.mqtt import setup_mqtt_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_AWAY, CONF_ID, CONF_INTERNAL, CONF_MAX_TEMPERATURE, \
|
||||
CONF_MIN_TEMPERATURE, CONF_MODE, CONF_MQTT_ID, CONF_TARGET_TEMPERATURE, \
|
||||
CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL
|
||||
CONF_MIN_TEMPERATURE, CONF_MODE, CONF_TARGET_TEMPERATURE, \
|
||||
CONF_TARGET_TEMPERATURE_HIGH, CONF_TARGET_TEMPERATURE_LOW, CONF_TEMPERATURE_STEP, CONF_VISUAL, \
|
||||
CONF_MQTT_ID
|
||||
from esphome.core import CORE, coroutine
|
||||
from esphome.cpp_generator import Pvariable, add, get_variable, templatable
|
||||
from esphome.cpp_types import Action, App, Nameable, bool_, esphome_ns, float_
|
||||
|
||||
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend({
|
||||
climate_ns = cg.esphome_ns.namespace('climate')
|
||||
|
||||
})
|
||||
|
||||
climate_ns = esphome_ns.namespace('climate')
|
||||
|
||||
ClimateDevice = climate_ns.class_('ClimateDevice', Nameable)
|
||||
ClimateDevice = climate_ns.class_('Climate', cg.Nameable)
|
||||
ClimateCall = climate_ns.class_('ClimateCall')
|
||||
ClimateTraits = climate_ns.class_('ClimateTraits')
|
||||
MQTTClimateComponent = climate_ns.class_('MQTTClimateComponent', mqtt.MQTTComponent)
|
||||
# MQTTClimateComponent = climate_ns.class_('MQTTClimateComponent', mqtt.MQTTComponent)
|
||||
|
||||
ClimateMode = climate_ns.enum('ClimateMode')
|
||||
CLIMATE_MODES = {
|
||||
@@ -33,75 +26,80 @@ CLIMATE_MODES = {
|
||||
validate_climate_mode = cv.one_of(*CLIMATE_MODES, upper=True)
|
||||
|
||||
# Actions
|
||||
ControlAction = climate_ns.class_('ControlAction', Action)
|
||||
ControlAction = climate_ns.class_('ControlAction', cg.Action)
|
||||
|
||||
CLIMATE_SCHEMA = cv.MQTT_COMMAND_COMPONENT_SCHEMA.extend({
|
||||
cv.GenerateID(): cv.declare_variable_id(ClimateDevice),
|
||||
cv.GenerateID(CONF_MQTT_ID): cv.declare_variable_id(MQTTClimateComponent),
|
||||
vol.Optional(CONF_VISUAL, default={}): cv.Schema({
|
||||
vol.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
|
||||
vol.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
vol.Optional(CONF_TEMPERATURE_STEP): cv.temperature,
|
||||
})
|
||||
cv.OnlyWith(CONF_MQTT_ID, 'mqtt'): cv.declare_variable_id(mqtt.MQTTClimateComponent),
|
||||
cv.Optional(CONF_VISUAL, default={}): cv.Schema({
|
||||
cv.Optional(CONF_MIN_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_MAX_TEMPERATURE): cv.temperature,
|
||||
cv.Optional(CONF_TEMPERATURE_STEP): cv.temperature,
|
||||
}),
|
||||
# TODO: MQTT topic options
|
||||
})
|
||||
|
||||
CLIMATE_PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(CLIMATE_SCHEMA.schema)
|
||||
|
||||
|
||||
@coroutine
|
||||
def setup_climate_core_(climate_var, config):
|
||||
def setup_climate_core_(var, config):
|
||||
if CONF_INTERNAL in config:
|
||||
add(climate_var.set_internal(config[CONF_INTERNAL]))
|
||||
cg.add(var.set_internal(config[CONF_INTERNAL]))
|
||||
visual = config[CONF_VISUAL]
|
||||
if CONF_MIN_TEMPERATURE in visual:
|
||||
add(climate_var.set_visual_min_temperature_override(visual[CONF_MIN_TEMPERATURE]))
|
||||
cg.add(var.set_visual_min_temperature_override(visual[CONF_MIN_TEMPERATURE]))
|
||||
if CONF_MAX_TEMPERATURE in visual:
|
||||
add(climate_var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE]))
|
||||
cg.add(var.set_visual_max_temperature_override(visual[CONF_MAX_TEMPERATURE]))
|
||||
if CONF_TEMPERATURE_STEP in visual:
|
||||
add(climate_var.set_visual_temperature_step_override(visual[CONF_TEMPERATURE_STEP]))
|
||||
setup_mqtt_component(climate_var.Pget_mqtt(), config)
|
||||
cg.add(var.set_visual_temperature_step_override(visual[CONF_TEMPERATURE_STEP]))
|
||||
|
||||
if CONF_MQTT_ID in config:
|
||||
mqtt_ = cg.new_Pvariable(config[CONF_MQTT_ID], var)
|
||||
yield mqtt.register_mqtt_component(mqtt_, config)
|
||||
|
||||
|
||||
@coroutine
|
||||
def register_climate(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = Pvariable(config[CONF_ID], var, has_side_effects=True)
|
||||
add(App.register_climate(var))
|
||||
CORE.add_job(setup_climate_core_, var, config)
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
cg.add(cg.App.register_climate(var))
|
||||
yield setup_climate_core_(var, config)
|
||||
|
||||
|
||||
BUILD_FLAGS = '-DUSE_CLIMATE'
|
||||
|
||||
CONF_CLIMATE_CONTROL = 'climate.control'
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema({
|
||||
vol.Required(CONF_ID): cv.use_variable_id(ClimateDevice),
|
||||
vol.Optional(CONF_MODE): cv.templatable(validate_climate_mode),
|
||||
vol.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
vol.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
|
||||
vol.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
|
||||
vol.Optional(CONF_AWAY): cv.templatable(cv.boolean),
|
||||
cv.Required(CONF_ID): cv.use_variable_id(ClimateDevice),
|
||||
cv.Optional(CONF_MODE): cv.templatable(validate_climate_mode),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_LOW): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_TARGET_TEMPERATURE_HIGH): cv.templatable(cv.temperature),
|
||||
cv.Optional(CONF_AWAY): cv.templatable(cv.boolean),
|
||||
})
|
||||
|
||||
|
||||
@ACTION_REGISTRY.register(CONF_CLIMATE_CONTROL, CLIMATE_CONTROL_ACTION_SCHEMA)
|
||||
@ACTION_REGISTRY.register('climate.control', CLIMATE_CONTROL_ACTION_SCHEMA)
|
||||
def climate_control_to_code(config, action_id, template_arg, args):
|
||||
var = yield get_variable(config[CONF_ID])
|
||||
var = yield cg.get_variable(config[CONF_ID])
|
||||
type = ControlAction.template(template_arg)
|
||||
rhs = type.new(var)
|
||||
action = Pvariable(action_id, rhs, type=type)
|
||||
action = cg.Pvariable(action_id, rhs, type=type)
|
||||
if CONF_MODE in config:
|
||||
template_ = yield templatable(config[CONF_MODE], args, ClimateMode,
|
||||
to_exp=CLIMATE_MODES)
|
||||
add(action.set_mode(template_))
|
||||
template_ = yield cg.templatable(config[CONF_MODE], args, ClimateMode,
|
||||
to_exp=CLIMATE_MODES)
|
||||
cg.add(action.set_mode(template_))
|
||||
if CONF_TARGET_TEMPERATURE in config:
|
||||
template_ = yield templatable(config[CONF_TARGET_TEMPERATURE], args, float_)
|
||||
add(action.set_target_temperature(template_))
|
||||
template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE], args, float)
|
||||
cg.add(action.set_target_temperature(template_))
|
||||
if CONF_TARGET_TEMPERATURE_LOW in config:
|
||||
template_ = yield templatable(config[CONF_TARGET_TEMPERATURE_LOW], args, float_)
|
||||
add(action.set_target_temperature_low(template_))
|
||||
template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE_LOW], args, float)
|
||||
cg.add(action.set_target_temperature_low(template_))
|
||||
if CONF_TARGET_TEMPERATURE_HIGH in config:
|
||||
template_ = yield templatable(config[CONF_TARGET_TEMPERATURE_HIGH], args, float_)
|
||||
add(action.set_target_temperature_high(template_))
|
||||
template_ = yield cg.templatable(config[CONF_TARGET_TEMPERATURE_HIGH], args, float)
|
||||
cg.add(action.set_target_temperature_high(template_))
|
||||
if CONF_AWAY in config:
|
||||
template_ = yield templatable(config[CONF_AWAY], args, bool_)
|
||||
add(action.set_away(template_))
|
||||
template_ = yield cg.templatable(config[CONF_AWAY], args, bool)
|
||||
cg.add(action.set_away(template_))
|
||||
yield action
|
||||
|
||||
|
||||
def to_code(config):
|
||||
cg.add_define('USE_CLIMATE')
|
||||
cg.add_global(climate_ns.using)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "climate.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
template<typename... Ts> class ControlAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit ControlAction(Climate *climate) : climate_(climate) {}
|
||||
|
||||
TEMPLATABLE_VALUE(ClimateMode, mode)
|
||||
TEMPLATABLE_VALUE(float, target_temperature)
|
||||
TEMPLATABLE_VALUE(float, target_temperature_low)
|
||||
TEMPLATABLE_VALUE(float, target_temperature_high)
|
||||
TEMPLATABLE_VALUE(bool, away)
|
||||
|
||||
void play(Ts... x) override {
|
||||
auto call = this->climate_->make_call();
|
||||
call.set_target_temperature(this->mode_.optional_value(x...));
|
||||
call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...));
|
||||
call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...));
|
||||
call.set_away(this->away_.optional_value(x...));
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
Climate *climate_;
|
||||
};
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
@@ -1,65 +0,0 @@
|
||||
import voluptuous as vol
|
||||
|
||||
from esphome import automation
|
||||
from esphome.components import climate, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_AWAY_CONFIG, CONF_COOL_ACTION, \
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH, \
|
||||
CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION, CONF_ID, CONF_IDLE_ACTION, CONF_NAME, \
|
||||
CONF_SENSOR
|
||||
from esphome.cpp_generator import Pvariable, add, get_variable
|
||||
from esphome.cpp_helpers import setup_component
|
||||
from esphome.cpp_types import App
|
||||
|
||||
BangBangClimate = climate.climate_ns.class_('BangBangClimate', climate.ClimateDevice)
|
||||
BangBangClimateTargetTempConfig = climate.climate_ns.struct('BangBangClimateTargetTempConfig')
|
||||
|
||||
PLATFORM_SCHEMA = cv.nameable(climate.CLIMATE_PLATFORM_SCHEMA.extend({
|
||||
cv.GenerateID(): cv.declare_variable_id(BangBangClimate),
|
||||
vol.Required(CONF_SENSOR): cv.use_variable_id(sensor.Sensor),
|
||||
vol.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
vol.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
vol.Required(CONF_IDLE_ACTION): automation.validate_automation(single=True),
|
||||
vol.Optional(CONF_COOL_ACTION): automation.validate_automation(single=True),
|
||||
vol.Optional(CONF_HEAT_ACTION): automation.validate_automation(single=True),
|
||||
vol.Optional(CONF_AWAY_CONFIG): cv.Schema({
|
||||
vol.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
|
||||
vol.Required(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
|
||||
}),
|
||||
}).extend(cv.COMPONENT_SCHEMA.schema))
|
||||
|
||||
|
||||
def to_code(config):
|
||||
rhs = App.register_component(BangBangClimate.new(config[CONF_NAME]))
|
||||
control = Pvariable(config[CONF_ID], rhs)
|
||||
climate.register_climate(control, config)
|
||||
setup_component(control, config)
|
||||
|
||||
var = yield get_variable(config[CONF_SENSOR])
|
||||
add(control.set_sensor(var))
|
||||
|
||||
normal_config = BangBangClimateTargetTempConfig(
|
||||
config[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
|
||||
config[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
|
||||
)
|
||||
add(control.set_normal_config(normal_config))
|
||||
|
||||
automation.build_automations(control.get_idle_trigger(), [], config[CONF_IDLE_ACTION])
|
||||
|
||||
if CONF_COOL_ACTION in config:
|
||||
automation.build_automations(control.get_cool_trigger(), [], config[CONF_COOL_ACTION])
|
||||
add(control.set_supports_cool(True))
|
||||
if CONF_HEAT_ACTION in config:
|
||||
automation.build_automations(control.get_heat_trigger(), [], config[CONF_HEAT_ACTION])
|
||||
add(control.set_supports_heat(True))
|
||||
|
||||
if CONF_AWAY_CONFIG in config:
|
||||
away = config[CONF_AWAY_CONFIG]
|
||||
away_config = BangBangClimateTargetTempConfig(
|
||||
away[CONF_DEFAULT_TARGET_TEMPERATURE_LOW],
|
||||
away[CONF_DEFAULT_TARGET_TEMPERATURE_HIGH]
|
||||
)
|
||||
add(control.set_away_config(away_config))
|
||||
|
||||
|
||||
BUILD_FLAGS = '-DUSE_BANG_BANG_CLIMATE'
|
||||
@@ -0,0 +1,259 @@
|
||||
#include "climate.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
static const char *TAG = "climate";
|
||||
|
||||
void ClimateCall::perform() {
|
||||
ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
|
||||
this->validate_();
|
||||
if (this->mode_.has_value()) {
|
||||
const char *mode_s = climate_mode_to_string(*this->mode_);
|
||||
ESP_LOGD(TAG, " Mode: %s", mode_s);
|
||||
}
|
||||
if (this->target_temperature_.has_value()) {
|
||||
ESP_LOGD(TAG, " Target Temperature: %.2f", *this->target_temperature_);
|
||||
}
|
||||
if (this->target_temperature_low_.has_value()) {
|
||||
ESP_LOGD(TAG, " Target Temperature Low: %.2f", *this->target_temperature_low_);
|
||||
}
|
||||
if (this->target_temperature_high_.has_value()) {
|
||||
ESP_LOGD(TAG, " Target Temperature High: %.2f", *this->target_temperature_high_);
|
||||
}
|
||||
if (this->away_.has_value()) {
|
||||
ESP_LOGD(TAG, " Away Mode: %s", ONOFF(*this->away_));
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
void ClimateCall::validate_() {
|
||||
auto traits = this->parent_->get_traits();
|
||||
if (this->mode_.has_value()) {
|
||||
auto mode = *this->mode_;
|
||||
if (!traits.supports_mode(mode)) {
|
||||
ESP_LOGW(TAG, " Mode %s is not supported by this device!", climate_mode_to_string(mode));
|
||||
this->mode_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_.has_value()) {
|
||||
auto target = *this->target_temperature_;
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
ESP_LOGW(TAG, " Cannot set target temperature for climate device "
|
||||
"with two-point target temperature!");
|
||||
this->target_temperature_.reset();
|
||||
} else if (isnan(target)) {
|
||||
ESP_LOGW(TAG, " Target temperature must not be NAN!");
|
||||
this->target_temperature_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
|
||||
if (!traits.get_supports_two_point_target_temperature()) {
|
||||
ESP_LOGW(TAG, " Cannot set low/high target temperature for this device!");
|
||||
this->target_temperature_low_.reset();
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() && isnan(*this->target_temperature_low_)) {
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
||||
this->target_temperature_low_.reset();
|
||||
}
|
||||
if (this->target_temperature_high_.has_value() && isnan(*this->target_temperature_high_)) {
|
||||
ESP_LOGW(TAG, " Target temperature low must not be NAN!");
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
|
||||
float low = *this->target_temperature_low_;
|
||||
float high = *this->target_temperature_high_;
|
||||
if (low > high) {
|
||||
ESP_LOGW(TAG, " Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
|
||||
this->target_temperature_low_.reset();
|
||||
this->target_temperature_high_.reset();
|
||||
}
|
||||
}
|
||||
if (this->away_.has_value()) {
|
||||
if (!traits.get_supports_away()) {
|
||||
ESP_LOGW(TAG, " Cannot set away mode for this device!");
|
||||
this->away_.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
|
||||
this->mode_ = mode;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
||||
if (str_equals_case_insensitive(mode, "OFF")) {
|
||||
this->set_mode(CLIMATE_MODE_OFF);
|
||||
} else if (str_equals_case_insensitive(mode, "AUTO")) {
|
||||
this->set_mode(CLIMATE_MODE_AUTO);
|
||||
} else if (str_equals_case_insensitive(mode, "COOL")) {
|
||||
this->set_mode(CLIMATE_MODE_COOL);
|
||||
} else if (str_equals_case_insensitive(mode, "HEAT")) {
|
||||
this->set_mode(CLIMATE_MODE_HEAT);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
|
||||
this->target_temperature_ = target_temperature;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
|
||||
this->target_temperature_low_ = target_temperature_low;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
return *this;
|
||||
}
|
||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
|
||||
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
|
||||
const optional<bool> &ClimateCall::get_away() const { return this->away_; }
|
||||
ClimateCall &ClimateCall::set_away(bool away) {
|
||||
this->away_ = away;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_away(optional<bool> away) {
|
||||
this->away_ = away;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
||||
this->target_temperature_high_ = target_temperature_high;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
|
||||
this->target_temperature_low_ = target_temperature_low;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
|
||||
this->target_temperature_ = target_temperature;
|
||||
return *this;
|
||||
}
|
||||
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
||||
this->mode_ = mode;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Climate::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
optional<ClimateDeviceRestoreState> Climate::restore_state_() {
|
||||
this->rtc_ = global_preferences.make_preference<ClimateDeviceRestoreState>(this->get_object_id_hash());
|
||||
ClimateDeviceRestoreState recovered{};
|
||||
if (!this->rtc_.load(&recovered))
|
||||
return {};
|
||||
return recovered;
|
||||
}
|
||||
void Climate::save_state_() {
|
||||
ClimateDeviceRestoreState state{};
|
||||
// initialize as zero to prevent random data on stack triggering erase
|
||||
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
|
||||
|
||||
state.mode = this->mode;
|
||||
auto traits = this->get_traits();
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
state.target_temperature_low = this->target_temperature_low;
|
||||
state.target_temperature_high = this->target_temperature_high;
|
||||
} else {
|
||||
state.target_temperature = this->target_temperature;
|
||||
}
|
||||
if (traits.get_supports_away()) {
|
||||
state.away = this->away;
|
||||
}
|
||||
|
||||
this->rtc_.save(&state);
|
||||
}
|
||||
void Climate::publish_state() {
|
||||
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
|
||||
auto traits = this->get_traits();
|
||||
|
||||
ESP_LOGD(TAG, " Mode: %s", climate_mode_to_string(this->mode));
|
||||
if (traits.get_supports_current_temperature()) {
|
||||
ESP_LOGD(TAG, " Current Temperature: %.2f°C", this->current_temperature);
|
||||
}
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
ESP_LOGD(TAG, " Target Temperature: Low: %.2f°C High: %.2f°C", this->target_temperature_low,
|
||||
this->target_temperature_high);
|
||||
} else {
|
||||
ESP_LOGD(TAG, " Target Temperature: %.2f°C", this->target_temperature);
|
||||
}
|
||||
if (traits.get_supports_away()) {
|
||||
ESP_LOGD(TAG, " Away: %s", ONOFF(this->away));
|
||||
}
|
||||
|
||||
// Send state to frontend
|
||||
this->state_callback_.call();
|
||||
// Save state
|
||||
this->save_state_();
|
||||
}
|
||||
uint32_t Climate::hash_base() { return 3104134496UL; }
|
||||
|
||||
ClimateTraits Climate::get_traits() {
|
||||
auto traits = this->traits();
|
||||
if (this->visual_min_temperature_override_.has_value()) {
|
||||
traits.set_visual_min_temperature(*this->visual_min_temperature_override_);
|
||||
}
|
||||
if (this->visual_max_temperature_override_.has_value()) {
|
||||
traits.set_visual_max_temperature(*this->visual_max_temperature_override_);
|
||||
}
|
||||
if (this->visual_temperature_step_override_.has_value()) {
|
||||
traits.set_visual_temperature_step(*this->visual_temperature_step_override_);
|
||||
}
|
||||
return traits;
|
||||
}
|
||||
|
||||
#ifdef USE_MQTT_CLIMATE
|
||||
MQTTClimateComponent *Climate::get_mqtt() const { return this->mqtt_; }
|
||||
void Climate::set_mqtt(MQTTClimateComponent *mqtt) { this->mqtt_ = mqtt; }
|
||||
#endif
|
||||
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
void Climate::set_visual_temperature_step_override(float visual_temperature_step_override) {
|
||||
this->visual_temperature_step_override_ = visual_temperature_step_override;
|
||||
}
|
||||
Climate::Climate(const std::string &name) : Nameable(name) {}
|
||||
Climate::Climate() : Climate("") {}
|
||||
ClimateCall Climate::make_call() { return ClimateCall(this); }
|
||||
|
||||
ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
auto call = climate->make_call();
|
||||
auto traits = climate->get_traits();
|
||||
call.set_mode(this->mode);
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
call.set_target_temperature_low(this->target_temperature_low);
|
||||
call.set_target_temperature_high(this->target_temperature_high);
|
||||
} else {
|
||||
call.set_target_temperature(this->target_temperature);
|
||||
}
|
||||
if (traits.get_supports_away()) {
|
||||
call.set_away(this->away);
|
||||
}
|
||||
return call;
|
||||
}
|
||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
auto traits = climate->get_traits();
|
||||
climate->mode = this->mode;
|
||||
if (traits.get_supports_two_point_target_temperature()) {
|
||||
climate->target_temperature_low = this->target_temperature_low;
|
||||
climate->target_temperature_high = this->target_temperature_high;
|
||||
} else {
|
||||
climate->target_temperature = this->target_temperature;
|
||||
}
|
||||
if (traits.get_supports_away()) {
|
||||
climate->away = this->away;
|
||||
}
|
||||
climate->publish_state();
|
||||
}
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,218 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "climate_mode.h"
|
||||
#include "climate_traits.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
class Climate;
|
||||
|
||||
/** This class is used to encode all control actions on a climate device.
|
||||
*
|
||||
* It is supposed to be used by all code that wishes to control a climate device (mqtt, api, lambda etc).
|
||||
* Create an instance of this class by calling `id(climate_device).make_call();`. Then set all attributes
|
||||
* with the `set_x` methods. Finally, to apply the changes call `.perform();`.
|
||||
*
|
||||
* The integration that implements the climate device receives this instance with the `control` method.
|
||||
* It should check all the properties it implements and apply them as needed. It should do so by
|
||||
* getting all properties it controls with the getter methods in this class. If the optional value is
|
||||
* set (check with `.has_value()`) that means the user wants to control this property. Get the value
|
||||
* of the optional with the star operator (`*call.get_mode()`) and apply it.
|
||||
*/
|
||||
class ClimateCall {
|
||||
public:
|
||||
explicit ClimateCall(Climate *parent) : parent_(parent) {}
|
||||
|
||||
/// Set the mode of the climate device.
|
||||
ClimateCall &set_mode(ClimateMode mode);
|
||||
/// Set the mode of the climate device.
|
||||
ClimateCall &set_mode(optional<ClimateMode> mode);
|
||||
/// Set the mode of the climate device based on a string.
|
||||
ClimateCall &set_mode(const std::string &mode);
|
||||
/// Set the target temperature of the climate device.
|
||||
ClimateCall &set_target_temperature(float target_temperature);
|
||||
/// Set the target temperature of the climate device.
|
||||
ClimateCall &set_target_temperature(optional<float> target_temperature);
|
||||
/** Set the low point target temperature of the climate device
|
||||
*
|
||||
* For climate devices with two point target temperature control
|
||||
*/
|
||||
ClimateCall &set_target_temperature_low(float target_temperature_low);
|
||||
/** Set the low point target temperature of the climate device
|
||||
*
|
||||
* For climate devices with two point target temperature control
|
||||
*/
|
||||
ClimateCall &set_target_temperature_low(optional<float> target_temperature_low);
|
||||
/** Set the high point target temperature of the climate device
|
||||
*
|
||||
* For climate devices with two point target temperature control
|
||||
*/
|
||||
ClimateCall &set_target_temperature_high(float target_temperature_high);
|
||||
/** Set the high point target temperature of the climate device
|
||||
*
|
||||
* For climate devices with two point target temperature control
|
||||
*/
|
||||
ClimateCall &set_target_temperature_high(optional<float> target_temperature_high);
|
||||
ClimateCall &set_away(bool away);
|
||||
ClimateCall &set_away(optional<bool> away);
|
||||
|
||||
void perform();
|
||||
|
||||
const optional<ClimateMode> &get_mode() const;
|
||||
const optional<float> &get_target_temperature() const;
|
||||
const optional<float> &get_target_temperature_low() const;
|
||||
const optional<float> &get_target_temperature_high() const;
|
||||
const optional<bool> &get_away() const;
|
||||
|
||||
protected:
|
||||
void validate_();
|
||||
|
||||
Climate *const parent_;
|
||||
optional<ClimateMode> mode_;
|
||||
optional<float> target_temperature_;
|
||||
optional<float> target_temperature_low_;
|
||||
optional<float> target_temperature_high_;
|
||||
optional<bool> away_;
|
||||
};
|
||||
|
||||
/// Struct used to save the state of the climate device in restore memory.
|
||||
struct ClimateDeviceRestoreState {
|
||||
ClimateMode mode;
|
||||
bool away;
|
||||
union {
|
||||
float target_temperature;
|
||||
struct {
|
||||
float target_temperature_low;
|
||||
float target_temperature_high;
|
||||
};
|
||||
};
|
||||
|
||||
/// Convert this struct to a climate call that can be performed.
|
||||
ClimateCall to_call(Climate *climate);
|
||||
/// Apply these settings to the climate device.
|
||||
void apply(Climate *climate);
|
||||
} __attribute__((packed));
|
||||
|
||||
/**
|
||||
* ClimateDevice - This is the base class for all climate integrations. Each integration
|
||||
* needs to extend this class and implement two functions:
|
||||
*
|
||||
* - get_traits() - return the static traits of the climate device
|
||||
* - control(ClimateDeviceCall call) - Apply the given changes from call.
|
||||
*
|
||||
* To write data to the frontend, the integration must first set the properties using
|
||||
* this->property = value; (for example this->current_temperature = 42.0;); then the integration
|
||||
* must call this->publish_state(); to send the entire state to the frontend.
|
||||
*
|
||||
* The entire state of the climate device is encoded in public properties of the base class (current_temperature,
|
||||
* mode etc). These are read-only for the user and rw for integrations. The reason these are public
|
||||
* is for simple access to them from lambdas `if (id(my_climate).mode == climate::CLIMATE_MODE_AUTO) ...`
|
||||
*/
|
||||
class Climate : public Nameable {
|
||||
public:
|
||||
/// Construct a climate device with a name.
|
||||
Climate(const std::string &name);
|
||||
/// Construct a climate device with empty name (will be set later).
|
||||
Climate();
|
||||
|
||||
/// The active mode of the climate device.
|
||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||
/// The current temperature of the climate device, as reported from the integration.
|
||||
float current_temperature{NAN};
|
||||
|
||||
union {
|
||||
/// The target temperature of the climate device.
|
||||
float target_temperature;
|
||||
struct {
|
||||
/// The minimum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_low;
|
||||
/// The maximum target temperature of the climate device, for climate devices with split target temperature.
|
||||
float target_temperature_high;
|
||||
};
|
||||
};
|
||||
|
||||
/** Whether the climate device is in away mode.
|
||||
*
|
||||
* Away allows climate devices to have two different target temperature configs:
|
||||
* one for normal mode and one for away mode.
|
||||
*/
|
||||
bool away{false};
|
||||
|
||||
/** Add a callback for the climate device state, each time the state of the climate device is updated
|
||||
* (using publish_state), this callback will be called.
|
||||
*
|
||||
* @param callback The callback to call.
|
||||
*/
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
|
||||
* for more info.
|
||||
* @return A new ClimateCall instance targeting this climate device.
|
||||
*/
|
||||
ClimateCall make_call();
|
||||
|
||||
/** Publish the state of the climate device, to be called from integrations.
|
||||
*
|
||||
* This will schedule the climate device to publish its state to all listeners and save the current state
|
||||
* to recover memory.
|
||||
*/
|
||||
void publish_state();
|
||||
|
||||
/** Get the traits of this climate device with all overrides applied.
|
||||
*
|
||||
* Traits are static data that encode the capabilities and static data for a climate device such as supported
|
||||
* modes, temperature range etc.
|
||||
*/
|
||||
ClimateTraits get_traits();
|
||||
|
||||
#ifdef USE_MQTT_COVER
|
||||
MQTTClimateComponent *get_mqtt() const;
|
||||
void set_mqtt(MQTTClimateComponent *mqtt);
|
||||
#endif
|
||||
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override);
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override);
|
||||
void set_visual_temperature_step_override(float visual_temperature_step_override);
|
||||
|
||||
protected:
|
||||
friend ClimateCall;
|
||||
|
||||
/** Get the default traits of this climate device.
|
||||
*
|
||||
* Traits are static data that encode the capabilities and static data for a climate device such as supported
|
||||
* modes, temperature range etc. Each integration must implement this method and the return value must
|
||||
* be constant during all of execution time.
|
||||
*/
|
||||
virtual ClimateTraits traits() = 0;
|
||||
|
||||
/** Control the climate device, this is a virtual method that each climate integration must implement.
|
||||
*
|
||||
* See more info in ClimateCall. The integration should check all of its values in this method and
|
||||
* set them accordingly. At the end of the call, the integration must call `publish_state()` to
|
||||
* notify the frontend of a changed state.
|
||||
*
|
||||
* @param call The ClimateCall instance encoding all attribute changes.
|
||||
*/
|
||||
virtual void control(const ClimateCall &call) = 0;
|
||||
/// Restore the state of the climate device, call this from your setup() method.
|
||||
optional<ClimateDeviceRestoreState> restore_state_();
|
||||
/** Internal method to save the state of the climate device to recover memory. This is automatically
|
||||
* called from publish_state()
|
||||
*/
|
||||
void save_state_();
|
||||
|
||||
uint32_t hash_base() override;
|
||||
|
||||
CallbackManager<void()> state_callback_{};
|
||||
ESPPreferenceObject rtc_;
|
||||
optional<float> visual_min_temperature_override_{};
|
||||
optional<float> visual_max_temperature_override_{};
|
||||
optional<float> visual_temperature_step_override_{};
|
||||
};
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,22 @@
|
||||
#include "climate_mode.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
const char *climate_mode_to_string(ClimateMode mode) {
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return "OFF";
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return "AUTO";
|
||||
case CLIMATE_MODE_COOL:
|
||||
return "COOL";
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return "HEAT";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
/// Enum for all modes a climate device can be in.
|
||||
enum ClimateMode : uint8_t {
|
||||
/// The climate device is off (not in auto, heat or cool mode)
|
||||
CLIMATE_MODE_OFF = 0,
|
||||
/// The climate device is set to automatically change the heating/cooling cycle
|
||||
CLIMATE_MODE_AUTO = 1,
|
||||
/// The climate device is manually set to cool mode (not in auto mode!)
|
||||
CLIMATE_MODE_COOL = 2,
|
||||
/// The climate device is manually set to heat mode (not in auto mode!)
|
||||
CLIMATE_MODE_HEAT = 3,
|
||||
};
|
||||
|
||||
/// Convert the given ClimateMode to a human-readable string.
|
||||
const char *climate_mode_to_string(ClimateMode mode);
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,57 @@
|
||||
#include "climate_traits.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
bool ClimateTraits::supports_mode(ClimateMode mode) const {
|
||||
switch (mode) {
|
||||
case CLIMATE_MODE_OFF:
|
||||
return true;
|
||||
case CLIMATE_MODE_AUTO:
|
||||
return this->supports_auto_mode_;
|
||||
case CLIMATE_MODE_COOL:
|
||||
return this->supports_cool_mode_;
|
||||
case CLIMATE_MODE_HEAT:
|
||||
return this->supports_heat_mode_;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ClimateTraits::get_supports_current_temperature() const { return supports_current_temperature_; }
|
||||
void ClimateTraits::set_supports_current_temperature(bool supports_current_temperature) {
|
||||
supports_current_temperature_ = supports_current_temperature;
|
||||
}
|
||||
bool ClimateTraits::get_supports_two_point_target_temperature() const { return supports_two_point_target_temperature_; }
|
||||
void ClimateTraits::set_supports_two_point_target_temperature(bool supports_two_point_target_temperature) {
|
||||
supports_two_point_target_temperature_ = supports_two_point_target_temperature;
|
||||
}
|
||||
void ClimateTraits::set_supports_auto_mode(bool supports_auto_mode) { supports_auto_mode_ = supports_auto_mode; }
|
||||
void ClimateTraits::set_supports_cool_mode(bool supports_cool_mode) { supports_cool_mode_ = supports_cool_mode; }
|
||||
void ClimateTraits::set_supports_heat_mode(bool supports_heat_mode) { supports_heat_mode_ = supports_heat_mode; }
|
||||
void ClimateTraits::set_supports_away(bool supports_away) { supports_away_ = supports_away; }
|
||||
float ClimateTraits::get_visual_min_temperature() const { return visual_min_temperature_; }
|
||||
void ClimateTraits::set_visual_min_temperature(float visual_min_temperature) {
|
||||
visual_min_temperature_ = visual_min_temperature;
|
||||
}
|
||||
float ClimateTraits::get_visual_max_temperature() const { return visual_max_temperature_; }
|
||||
void ClimateTraits::set_visual_max_temperature(float visual_max_temperature) {
|
||||
visual_max_temperature_ = visual_max_temperature;
|
||||
}
|
||||
float ClimateTraits::get_visual_temperature_step() const { return visual_temperature_step_; }
|
||||
int8_t ClimateTraits::get_temperature_accuracy_decimals() const {
|
||||
// use printf %g to find number of digits based on temperature step
|
||||
char buf[32];
|
||||
sprintf(buf, "%.5g", this->visual_temperature_step_);
|
||||
std::string str{buf};
|
||||
size_t dot_pos = str.find('.');
|
||||
if (dot_pos == std::string::npos)
|
||||
return 0;
|
||||
|
||||
return str.length() - dot_pos - 1;
|
||||
}
|
||||
void ClimateTraits::set_visual_temperature_step(float temperature_step) { visual_temperature_step_ = temperature_step; }
|
||||
bool ClimateTraits::get_supports_away() const { return supports_away_; }
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "climate_mode.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace climate {
|
||||
|
||||
/** This class contains all static data for climate devices.
|
||||
*
|
||||
* All climate devices must support these features:
|
||||
* - OFF mode
|
||||
* - Target Temperature
|
||||
*
|
||||
* All other properties and modes are optional and the integration must mark
|
||||
* each of them as supported by setting the appropriate flag here.
|
||||
*
|
||||
* - supports current temperature - if the climate device supports reporting a current temperature
|
||||
* - supports two point target temperature - if the climate device's target temperature should be
|
||||
* split in target_temperature_low and target_temperature_high instead of just the single target_temperature
|
||||
* - supports modes:
|
||||
* - auto mode (automatic control)
|
||||
* - cool mode (lowers current temperature)
|
||||
* - heat mode (increases current temperature)
|
||||
* - supports away - away mode means that the climate device supports two different
|
||||
* target temperature settings: one target temp setting for "away" mode and one for non-away mode.
|
||||
*
|
||||
* This class also contains static data for the climate device display:
|
||||
* - visual min/max temperature - tells the frontend what range of temperatures the climate device
|
||||
* should display (gauge min/max values)
|
||||
* - temperature step - the step with which to increase/decrease target temperature.
|
||||
* This also affects with how many decimal places the temperature is shown
|
||||
*/
|
||||
class ClimateTraits {
|
||||
public:
|
||||
bool get_supports_current_temperature() const;
|
||||
void set_supports_current_temperature(bool supports_current_temperature);
|
||||
bool get_supports_two_point_target_temperature() const;
|
||||
void set_supports_two_point_target_temperature(bool supports_two_point_target_temperature);
|
||||
void set_supports_auto_mode(bool supports_auto_mode);
|
||||
void set_supports_cool_mode(bool supports_cool_mode);
|
||||
void set_supports_heat_mode(bool supports_heat_mode);
|
||||
void set_supports_away(bool supports_away);
|
||||
bool get_supports_away() const;
|
||||
bool supports_mode(ClimateMode mode) const;
|
||||
|
||||
float get_visual_min_temperature() const;
|
||||
void set_visual_min_temperature(float visual_min_temperature);
|
||||
float get_visual_max_temperature() const;
|
||||
void set_visual_max_temperature(float visual_max_temperature);
|
||||
float get_visual_temperature_step() const;
|
||||
int8_t get_temperature_accuracy_decimals() const;
|
||||
void set_visual_temperature_step(float temperature_step);
|
||||
|
||||
protected:
|
||||
bool supports_current_temperature_{false};
|
||||
bool supports_two_point_target_temperature_{false};
|
||||
bool supports_auto_mode_{false};
|
||||
bool supports_cool_mode_{false};
|
||||
bool supports_heat_mode_{false};
|
||||
bool supports_away_{false};
|
||||
|
||||
float visual_min_temperature_{10};
|
||||
float visual_max_temperature_{30};
|
||||
float visual_temperature_step_{0.1};
|
||||
};
|
||||
|
||||
} // namespace climate
|
||||
} // namespace esphome
|
||||
Reference in New Issue
Block a user