Thermostat fixes+updates 1 (#2032)

Co-authored-by: Otto Winter <otto@otto-winter.com>
This commit is contained in:
Keith Burzinski 2021-07-21 16:09:31 -05:00 committed by GitHub
parent c9062599df
commit 0a32321c85
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 107 additions and 17 deletions

View file

@ -7,6 +7,7 @@ from esphome.const import (
CONF_AWAY_CONFIG,
CONF_COOL_ACTION,
CONF_COOL_MODE,
CONF_DEFAULT_MODE,
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH,
CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_DRY_ACTION,
@ -33,10 +34,12 @@ from esphome.const import (
CONF_SWING_HORIZONTAL_ACTION,
CONF_SWING_OFF_ACTION,
CONF_SWING_VERTICAL_ACTION,
CONF_TARGET_TEMPERATURE_CHANGE_ACTION,
)
CODEOWNERS = ["@kbx81"]
climate_ns = cg.esphome_ns.namespace("climate")
thermostat_ns = cg.esphome_ns.namespace("thermostat")
ThermostatClimate = thermostat_ns.class_(
"ThermostatClimate", climate.Climate, cg.Component
@ -44,6 +47,17 @@ ThermostatClimate = thermostat_ns.class_(
ThermostatClimateTargetTempConfig = thermostat_ns.struct(
"ThermostatClimateTargetTempConfig"
)
ClimateMode = climate_ns.enum("ClimateMode")
CLIMATE_MODES = {
"OFF": ClimateMode.CLIMATE_MODE_OFF,
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
"AUTO": ClimateMode.CLIMATE_MODE_AUTO,
}
validate_climate_mode = cv.enum(CLIMATE_MODES, upper=True)
def validate_thermostat(config):
@ -141,6 +155,21 @@ def validate_thermostat(config):
CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_HEAT_ACTION
)
)
# verify default climate mode is valid given above configuration
default_mode = config[CONF_DEFAULT_MODE]
requirements = {
"HEAT_COOL": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
"COOL": [CONF_COOL_ACTION],
"HEAT": [CONF_HEAT_ACTION],
"DRY": [CONF_DRY_ACTION],
"FAN_ONLY": [CONF_FAN_ONLY_ACTION],
"AUTO": [CONF_COOL_ACTION, CONF_HEAT_ACTION],
}.get(default_mode, [])
for req in requirements:
if req not in config:
raise cv.Invalid(
f"{CONF_DEFAULT_MODE} is set to {default_mode} but {req} is not present in the configuration"
)
return config
@ -204,6 +233,12 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_SWING_VERTICAL_ACTION): automation.validate_automation(
single=True
),
cv.Optional(
CONF_TARGET_TEMPERATURE_CHANGE_ACTION
): automation.validate_automation(single=True),
cv.Optional(CONF_DEFAULT_MODE, default="OFF"): cv.templatable(
validate_climate_mode
),
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_HIGH): cv.temperature,
cv.Optional(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
cv.Optional(CONF_HYSTERESIS, default=0.5): cv.temperature,
@ -233,6 +268,7 @@ async def to_code(config):
)
sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_default_mode(config[CONF_DEFAULT_MODE]))
cg.add(var.set_sensor(sens))
cg.add(var.set_hysteresis(config[CONF_HYSTERESIS]))
@ -380,6 +416,12 @@ async def to_code(config):
config[CONF_SWING_VERTICAL_ACTION],
)
cg.add(var.set_supports_swing_mode_vertical(True))
if CONF_TARGET_TEMPERATURE_CHANGE_ACTION in config:
await automation.build_automation(
var.get_temperature_change_trigger(),
[],
config[CONF_TARGET_TEMPERATURE_CHANGE_ACTION],
)
if CONF_AWAY_CONFIG in config:
away = config[CONF_AWAY_CONFIG]

View file

@ -21,7 +21,7 @@ void ThermostatClimate::setup() {
restore->to_call(this).perform();
} else {
// restore from defaults, change_away handles temps for us
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->mode = this->default_mode_;
this->change_away_(false);
}
// refresh the climate action based on the restored settings
@ -35,9 +35,18 @@ void ThermostatClimate::refresh() {
this->switch_to_action_(compute_action_());
this->switch_to_fan_mode_(this->fan_mode.value());
this->switch_to_swing_mode_(this->swing_mode);
this->check_temperature_change_trigger_();
this->publish_state();
}
void ThermostatClimate::control(const climate::ClimateCall &call) {
if (call.get_preset().has_value()) {
// setup_complete_ blocks modifying/resetting the temps immediately after boot
if (this->setup_complete_) {
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
} else {
this->preset = *call.get_preset();
}
}
if (call.get_mode().has_value())
this->mode = *call.get_mode();
if (call.get_fan_mode().has_value())
@ -50,15 +59,6 @@ void ThermostatClimate::control(const climate::ClimateCall &call) {
this->target_temperature_low = *call.get_target_temperature_low();
if (call.get_target_temperature_high().has_value())
this->target_temperature_high = *call.get_target_temperature_high();
if (call.get_preset().has_value()) {
// setup_complete_ blocks modifying/resetting the temps immediately after boot
if (this->setup_complete_) {
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
} else {
this->preset = *call.get_preset();
;
}
}
// set point validation
if (this->supports_two_points_) {
if (this->target_temperature_low < this->get_traits().get_visual_min_temperature())
@ -128,7 +128,16 @@ climate::ClimateTraits ThermostatClimate::traits() {
return traits;
}
climate::ClimateAction ThermostatClimate::compute_action_() {
// we need to know the current climate action before anything else happens here
climate::ClimateAction target_action = this->action;
// if the climate mode is OFF then the climate action must be OFF
if (this->mode == climate::CLIMATE_MODE_OFF) {
return climate::CLIMATE_ACTION_OFF;
} else if (this->action == climate::CLIMATE_ACTION_OFF) {
// ...but if the climate mode is NOT OFF then the climate action must not be OFF
target_action = climate::CLIMATE_ACTION_IDLE;
}
if (this->supports_two_points_) {
if (isnan(this->current_temperature) || isnan(this->target_temperature_low) ||
isnan(this->target_temperature_high) || isnan(this->hysteresis_))
@ -153,9 +162,6 @@ climate::ClimateAction ThermostatClimate::compute_action_() {
case climate::CLIMATE_MODE_DRY:
target_action = climate::CLIMATE_ACTION_DRYING;
break;
case climate::CLIMATE_MODE_OFF:
target_action = climate::CLIMATE_ACTION_OFF;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_HEAT:
@ -200,9 +206,6 @@ climate::ClimateAction ThermostatClimate::compute_action_() {
case climate::CLIMATE_MODE_DRY:
target_action = climate::CLIMATE_ACTION_DRYING;
break;
case climate::CLIMATE_MODE_OFF:
target_action = climate::CLIMATE_ACTION_OFF;
break;
case climate::CLIMATE_MODE_COOL:
if (this->supports_cool_) {
if (this->current_temperature > this->target_temperature + this->hysteresis_)
@ -410,6 +413,30 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo
this->prev_swing_mode_ = swing_mode;
this->prev_swing_mode_trigger_ = trig;
}
void ThermostatClimate::check_temperature_change_trigger_() {
if (this->supports_two_points_) {
// setup_complete_ helps us ensure an action is called immediately after boot
if ((this->prev_target_temperature_low_ == this->target_temperature_low) &&
(this->prev_target_temperature_high_ == this->target_temperature_high) && this->setup_complete_) {
return; // nothing changed, no reason to trigger
} else {
// save the new temperatures so we can check them again later; the trigger will fire below
this->prev_target_temperature_low_ = this->target_temperature_low;
this->prev_target_temperature_high_ = this->target_temperature_high;
}
} else {
if ((this->prev_target_temperature_ == this->target_temperature) && this->setup_complete_) {
return; // nothing changed, no reason to trigger
} else {
// save the new temperature so we can check it again later; the trigger will fire below
this->prev_target_temperature_ = this->target_temperature;
}
}
// trigger the action
Trigger<> *trig = this->temperature_change_trigger_;
assert(trig != nullptr);
trig->trigger();
}
void ThermostatClimate::change_away_(bool away) {
if (!away) {
if (this->supports_two_points_) {
@ -457,7 +484,9 @@ ThermostatClimate::ThermostatClimate()
swing_mode_both_trigger_(new Trigger<>()),
swing_mode_off_trigger_(new Trigger<>()),
swing_mode_horizontal_trigger_(new Trigger<>()),
swing_mode_vertical_trigger_(new Trigger<>()) {}
swing_mode_vertical_trigger_(new Trigger<>()),
temperature_change_trigger_(new Trigger<>()) {}
void ThermostatClimate::set_default_mode(climate::ClimateMode default_mode) { this->default_mode_ = default_mode; }
void ThermostatClimate::set_hysteresis(float hysteresis) { this->hysteresis_ = hysteresis; }
void ThermostatClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void ThermostatClimate::set_supports_heat_cool(bool supports_heat_cool) {
@ -534,6 +563,7 @@ Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this-
Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; }
Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; }
Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; }
void ThermostatClimate::dump_config() {
LOG_CLIMATE("", "Thermostat", this);
if (this->supports_heat_) {

View file

@ -26,6 +26,7 @@ class ThermostatClimate : public climate::Climate, public Component {
void setup() override;
void dump_config() override;
void set_default_mode(climate::ClimateMode default_mode);
void set_hysteresis(float hysteresis);
void set_sensor(sensor::Sensor *sensor);
void set_supports_auto(bool supports_auto);
@ -76,6 +77,7 @@ class ThermostatClimate : public climate::Climate, public Component {
Trigger<> *get_swing_mode_horizontal_trigger() const;
Trigger<> *get_swing_mode_off_trigger() const;
Trigger<> *get_swing_mode_vertical_trigger() const;
Trigger<> *get_temperature_change_trigger() const;
/// Get current hysteresis value
float hysteresis();
/// Call triggers based on updated climate states (modes/actions)
@ -106,6 +108,9 @@ class ThermostatClimate : public climate::Climate, public Component {
/// Switch the climate device to the given climate swing mode.
void switch_to_swing_mode_(climate::ClimateSwingMode swing_mode);
/// Check if the temperature change trigger should be called.
void check_temperature_change_trigger_();
/// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr};
@ -242,6 +247,9 @@ class ThermostatClimate : public climate::Climate, public Component {
/// The trigger to call when the controller should switch the swing mode to "vertical".
Trigger<> *swing_mode_vertical_trigger_{nullptr};
/// The trigger to call when the target temperature(s) change(es).
Trigger<> *temperature_change_trigger_{nullptr};
/// A reference to the trigger that was previously active.
///
/// This is so that the previous trigger can be stopped before enabling a new one
@ -256,8 +264,16 @@ class ThermostatClimate : public climate::Climate, public Component {
/// These are used to determine when a trigger/action needs to be called
climate::ClimateFanMode prev_fan_mode_{climate::CLIMATE_FAN_ON};
climate::ClimateMode prev_mode_{climate::CLIMATE_MODE_OFF};
climate::ClimateMode default_mode_{climate::CLIMATE_MODE_OFF};
climate::ClimateSwingMode prev_swing_mode_{climate::CLIMATE_SWING_OFF};
/// Store previously-known temperatures
///
/// These are used to determine when the temperature change trigger/action needs to be called
float prev_target_temperature_{NAN};
float prev_target_temperature_low_{NAN};
float prev_target_temperature_high_{NAN};
/// Temperature data for normal/home and away modes
ThermostatClimateTargetTempConfig normal_config_{};
ThermostatClimateTargetTempConfig away_config_{};

View file

@ -159,6 +159,7 @@ CONF_DAYS_OF_WEEK = "days_of_week"
CONF_DC_PIN = "dc_pin"
CONF_DEBOUNCE = "debounce"
CONF_DECELERATION = "deceleration"
CONF_DEFAULT_MODE = "default_mode"
CONF_DEFAULT_TARGET_TEMPERATURE_HIGH = "default_target_temperature_high"
CONF_DEFAULT_TARGET_TEMPERATURE_LOW = "default_target_temperature_low"
CONF_DEFAULT_TRANSITION_LENGTH = "default_transition_length"
@ -572,6 +573,7 @@ CONF_TABLET = "tablet"
CONF_TAG = "tag"
CONF_TARGET = "target"
CONF_TARGET_TEMPERATURE = "target_temperature"
CONF_TARGET_TEMPERATURE_CHANGE_ACTION = "target_temperature_change_action"
CONF_TARGET_TEMPERATURE_HIGH = "target_temperature_high"
CONF_TARGET_TEMPERATURE_LOW = "target_temperature_low"
CONF_TEMPERATURE = "temperature"